Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,8 @@ jobs:
print("All frontmatter valid.")
PY

- name: Support-bundle unit tests
run: node skills/support/scripts/test-build-bundle.js

- name: Brand and guardrail lint
run: bash scripts/brand-check.sh
23 changes: 20 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
.agent-mail/
.a5c/

# Fetched service-config cache. Carries internal Taboola infrastructure
# hostnames and datasource credentials — this repo is public.
.claude/transcript-uploader/

# OS
.DS_Store
Thumbs.db
Expand All @@ -24,7 +28,20 @@ venv/
.vscode/
*.swp

# Local PR / review / reply drafts at repo root (not for upstream)
# Local PR / review / reply drafts at repo root (not for upstream).
# Patterns are deliberately loose on the tail: the earlier
# /pr*-comment-*.txt and /reply-to-*.txt required a trailing segment and
# so missed pr2-readme-comment.txt and reply-mcp-*-feedback.txt.
/pr-description-*.txt
/pr*-comment-*.txt
/reply-to-*.txt
/pr*-comment*.txt
/reply-*.txt

# Local working data — eval inputs, hypothesis verdicts, dataset scratch.
# Routinely holds real customer campaign data (account and campaign IDs,
# spend, performance). This repo is public, so one `git add -A` turns a
# messy working tree into a disclosure. Keep it untracked.
/inputs/

# Support bundles, wherever they land. build-bundle.js already refuses to
# write inside a git work tree; this is the second line, not the first.
realize-support-*.md
39 changes: 39 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

63 changes: 57 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ This is a thin Claude Code plugin that wraps the [Realize remote MCP](https://gi
├──► reports skill → 4 report tools (CSV output)
├──► optimize-campaign skill → diagnoses underperformance; hands write
│ prescriptions to manage-campaigns
└──► manage-campaigns skill → 6 write tools: create_campaign, update_campaign,
create_native_item, update_native_item,
create_display_item, update_display_item.
Tiered preview-then-confirm with mandatory
▶ WRITE TARGET account header.
UI fallback for delete/duplicate/bulk ops.
├──► manage-campaigns skill → 6 write tools: create_campaign, update_campaign,
│ create_native_item, update_native_item,
│ create_display_item, update_display_item.
│ Tiered preview-then-confirm with mandatory
│ ▶ WRITE TARGET account header.
│ UI fallback for delete/duplicate/bulk ops.
└──► support skill → NO MCP tools. Reads the local Claude Code
session transcript and renders one Markdown
file the user emails to Support@taboola.com.
Preview-then-confirm; writes locally only,
transmits nothing. Entry point: /support.
┌────────────────────────────────────────┐
Expand All @@ -47,6 +53,51 @@ This is a thin Claude Code plugin that wraps the [Realize remote MCP](https://gi
### No hooks
This plugin does not use Claude Code hooks. The remote MCP handles token refresh at the transport layer, so adding hooks here would be overhead without benefit.

### The support bundle exports the transcript, never a summary
`support` is the one skill that touches no MCP tool. It reads the local Claude Code session transcript and renders it for Taboola Professional Services.

Everything in the bundle above the transcript — the Summary, the diagnostic table, the failed-action list, the ordered action log — is extracted **mechanically by the script**, not written by the model. This is deliberate and worth preserving: the bundle exists precisely for cases where the plugin got something wrong, and a model-authored summary of its own mistake reproduces the mistake. Don't "improve" this by having the model narrate what went wrong.

The model now authors **nothing** in the bundle. The case subject was the last model-written field and is now the user's complaint text (see *The Summary section is for PS's case Description* below).

Three constraints that are easy to break by accident:

- **Session identification uses `CLAUDE_CODE_SESSION_ID`.** A project folder normally holds several `.jsonl` sessions, so the newest-file fallback really can grab the wrong conversation. It exists only as a last resort and surfaces `confidence: guessed` when used — keep that surfaced.
- **Redaction strips credentials but keeps business IDs.** `account_id` / `campaign_id` / `item_id` are preserved on purpose; PS can't reproduce anything without them. Don't "harden" this by masking them.
- **No upload path, by design.** The script writes one local file and prints the path. Adding transmission would turn a local diagnostic into an outbound flow of customer campaign data — that's a privacy-review decision, not a refactor.
- **User text goes in by file, never as a shell argument.** `--complaint-file` / `--title-file` exist because a quoted shell argument silently rewrites the text: `$12.40` becomes `2.40`, `$500` becomes `00`, and backticks or `$(…)` execute. Currency is everywhere in this domain and users paste error text they didn't write. Collapsing this back to `--complaint "…"` for brevity reintroduces both the corruption and the injection.
- **Redact before shortening, and cover both value shapes.** Slicing first can cut a credential below the length the patterns match on. The actions table renders inputs as `key=value`, which a JSON-shaped pattern never matches — that gap leaked plaintext secrets in review.
- **Redaction targets credentials, not prose.** Where the parameter name is known, match on the key (`redactValue`); reserve pattern-matching for free text. An earlier regex that matched `secret:` / `authorization:` anywhere shredded legitimate ad copy — "Secret: Summer Sale" became "Secret: `<redacted>` Sale". Over-redaction destroys the evidence the bundle exists to carry, which is worse than the leak it guards.
- **The git-work-tree refusal is a control, not a suggestion.** `findGitRoot` blocks writes inside any repo. Customer data committed to this public repo is the worst outcome this feature can produce, and it is one bad `--out` away. Don't relax it to a warning.

A `--allow-git` override exists for maintainers who need a bundle inside a checkout deliberately. It is **intentionally absent from the refusal message and from SKILL.md**: the model that hits the error reads that message as its next instruction, and the correct next step is a different path, not a bypass. Don't "improve" the error by naming the flag.

- **Realize tool results get a much larger truncation budget than other output** (`MAX_REALIZE_RESULT_CHARS` vs `MAX_RESULT_CHARS`). A report CSV usually *is* the case — "the CPA here disagrees with the UI" is answered by the rows behind the number. Under the old uniform 2,000-char cap only ~13 of 250 rows survived, so the disputed row was typically the one missing. Bulk output from other tools stays tightly capped so the bundle remains email-attachable.

### The Summary section is for PS's case Description
Requested by the PS manager, and shaped by the constraint above. Case intake copies an email's **subject into the case Subject** and its **body into the case Description**, so the bundle now renders:

- a copy-ready **subject** = the user's complaint, one line, capped at `MAX_SUBJECT_CHARS`, with the first account ID appended for triage;
- **§1 Summary** = `EMAIL_PROLOG` (*"This case has been created by the Realize Plugin…"*) followed by mechanically-extracted facts, which the user pastes as the email body.

The tempting mistake is to satisfy "we want a summary" with a model-written narrative. That is the exact thing the section above forbids, so §1 carries only counts, tool names, and file paths. What makes it genuinely useful to PS is the attribution: **which Realize tools ran, which skills were invoked, and which knowledge files were read.** That separates *"the plugin read the right guidance and still got it wrong"* from *"the plugin never read it"* — different bugs with different fixes, previously indistinguishable from the outside. `knowledgeRef` is deliberately narrow (only `knowledge/`, `os/`, `agents/`, `skills/**/SKILL.md`, `skills/**/references/`); widening it to every file touched turns a signal into noise.

It is also **anchored to the plugin root**, not pattern-matched anywhere in the path. Unanchored, a user's own `~/Documents/os/notes.md` was reported to PS as plugin guidance — worse than reporting nothing, because the section exists to answer "did it read the guidance?" and a false entry answers it wrongly. The root is derived from `__dirname`; `knowledgeRef` takes an injectable root so tests don't depend on the checkout location.

Two more traps in this area, both found in review:

- **The subject must not be squeezed out by its own suffix.** `account_id` is an opaque API string with no length bound. A `slice(0, MAX - suffix.length - 1)` goes negative on a long one and slices *from the end*, which replaced the entire complaint with a bare `…`. `MIN_SUBJECT_TEXT_CHARS` now guarantees the user's words win and the account suffix is dropped instead.
- **The complaint is redacted, not trusted.** It is the user's own prose, so it is never rewritten — but users paste error output they never read, and that string now travels into an email subject. `redact()` is safe to apply here precisely because its flat rule is `=`-only with a length floor, so sentences survive while `Bearer …` does not.

§1 also states in the file that it is mechanical and points at the transcript. Keep that line — it is what stops a reader treating the bullet list as the plugin's testimony about itself.

Run `node skills/support/scripts/test-build-bundle.js` after touching the script — CI runs it too. Add cases there rather than testing via inline `node -e`: the rules are dense with backslashes and dollar signs, and shell escaping produced two false results during review.

### The guardrails carve-out for `/support` is load-bearing
`os/guardrails.md` bans surfacing skill names, `@taboola.com` addresses, and local file paths. The escalation message needs all three, so *Internal tools, skills, and infrastructure — never reference* carries an explicit carve-out.

If you tighten those bans later, **re-check the carve-out** — without it the model silently stops offering `/support`, and the failure is invisible (a feature that quietly never fires, not an error). Scenario 18 in `tests/test-scenarios-read.md` is the regression test.

### No direct curl / no API client code
All Realize API access flows through MCP tools. Do not add Bash curl calls that hit Realize endpoints directly — that bypasses the MCP's rate limiting, auth handling, and safety guarantees.

Expand Down
19 changes: 18 additions & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,28 @@ Use this to iterate on skills, run test scenarios, or work in a restricted/air-g

```bash
git clone https://github.com/taboola/realize-claude-plugin
claude --plugin-dir ./realize-claude-plugin
cd realize-claude-plugin
claude --plugin-dir .
```

`--plugin-dir` loads the plugin (skills, agent, and MCP wiring) directly from the local directory — no marketplace required.

> **The path is relative to where you launch `claude`.** From *inside* the repo use `--plugin-dir .`; from the parent folder use `--plugin-dir ./realize-claude-plugin`. A path that doesn't exist loads **nothing and prints no error** — the session starts normally and every plugin command comes back as `Unknown command`.
>
> Check it loaded before doing anything else:
>
> ```bash
> claude --plugin-dir . plugin list
> ```
>
> You want a `realize-plugin@inline … Status: ✔ loaded` entry. To see which skills and commands registered:
>
> ```bash
> claude --plugin-dir . plugin details realize-plugin@inline
> ```

**Invoking commands:** plugin components are namespaced by plugin name, so the support command is `/realize-plugin:support`, not `/support`. Typing `/` lists everything available in the session.

**Picking up code changes:** after `git pull`, run `/reload-plugins` inside the session to refresh without restarting the CLI.

**Loading multiple plugins at once:** repeat the flag, e.g. `claude --plugin-dir ./realize-claude-plugin --plugin-dir ./other-plugin`.
Expand Down
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ Clone the repo and load it as a local plugin via `--plugin-dir`:

```bash
git clone https://github.com/taboola/realize-claude-plugin
claude --plugin-dir ./realize-claude-plugin
cd realize-claude-plugin
claude --plugin-dir .
```

> **The path is relative to where you launch `claude`.** From inside the repo use `.`; from the parent folder use `./realize-claude-plugin`. A path that doesn't exist loads **nothing and prints no error** — the session looks normal but every plugin command returns `Unknown command`. Verify with `claude --plugin-dir . plugin list` and look for `realize-plugin@inline … ✔ loaded`.

Plugin components are namespaced by plugin name, so commands are invoked as `/realize-plugin:<name>` (e.g. `/realize-plugin:support`), not bare `/support`. Typing `/` lists what's actually available in the session.

This loads the skills, agent, and MCP wiring directly from the repo without requiring a marketplace install. Use this for iterating on skills, testing scenarios, or running the plugin inside a restricted/air-gapped environment.

On first tool call, Claude Code opens a browser for Taboola SSO login and returns you to the terminal once authenticated.
Expand Down Expand Up @@ -67,6 +72,7 @@ This plugin wraps the remote [realize-mcp](https://github.com/taboola/realize-mc
| [`reports`](skills/reports/SKILL.md) | Pull the four Realize performance reports and interpret the CSV output |
| [`optimize-campaign`](skills/optimize-campaign/SKILL.md) | Diagnose underperforming campaigns against the toolkit's signal-quality thresholds (100+ clicks per item, daily spend ≥ 8× CPA goal, 7–14 day learning phase) and prescribe concrete actions (most now applied via `manage-campaigns`) |
| [`manage-campaigns`](skills/manage-campaigns/SKILL.md) | Create and update campaigns and Native + Display items. Tiered preview-and-confirm pattern surfaces the target account on every write. Falls back to a UI reference for actions not supported here (delete, duplicate, bulk ops, Custom Rules, conversion-rule creation, CRM uploads) |
| [`support`](skills/support/SKILL.md) | Package the conversation into one file you can email to Taboola Support — see [`/realize-plugin:support`](#getting-help-with-a-problem) |

**Start with a skill, not the MCP** — the skills carry the account-resolution rules, CSV conventions, optimization playbook, and write-preview gate that raw MCP calls bypass. The [`realize-analyst`](agents/realize-analyst.md) agent auto-routes natural-language questions to the right skill, or you can invoke one explicitly (e.g. `/realize-plugin:optimize-campaign`). The most common miss: treating "performance review" or "insights" as ad-hoc analysis when it belongs in `optimize-campaign`.

Expand Down Expand Up @@ -132,10 +138,10 @@ The query window genuinely had no data, or you queried a campaign that didn't ru
All three are **opaque identifiers** returned by the API. `account_id` is a string (e.g., `advertiser_12345_prod`) returned exclusively by `search_accounts`. `campaign_id` and `item_id` come from campaign/item tools. Pass them to follow-up calls exactly as received — don't reformat or coerce to numbers.

**The plugin tried to create a campaign and failed.**
Claude routes write-intent requests to the `manage-campaigns` skill, which previews the resolved payload (and the target account) and asks for confirmation before submitting. Delete, duplicate, and bulk operations aren't supported here yet — those fall back to the Realize UI. If Claude attempted something unexpected, please [file an issue](https://github.com/taboola/realize-claude-plugin/issues) with the transcript.
Claude routes write-intent requests to the `manage-campaigns` skill, which previews the resolved payload (and the target account) and asks for confirmation before submitting. Delete, duplicate, and bulk operations aren't supported here yet — those fall back to the Realize UI. If Claude attempted something unexpected, run `/realize-plugin:support` to capture the transcript, then [file an issue](https://github.com/taboola/realize-claude-plugin/issues) with that file attached.

**A write went to the wrong account.**
Every write preview must lead with `▶ WRITE TARGET: <account name> (<account id>)`. If you saw the wrong account in that header before approving, the issue is at the account-resolution step — re-run the `accounts` skill to confirm the right account is selected before retrying. If the header was missing entirely, that's a bug; please file an issue with the transcript.
Every write preview must lead with `▶ WRITE TARGET: <account name> (<account id>)`. If you saw the wrong account in that header before approving, the issue is at the account-resolution step — re-run the `accounts` skill to confirm the right account is selected before retrying. If the header was missing entirely, that's a bug — run `/realize-plugin:support` and send the resulting file to Support@taboola.com.

**CSV output was truncated.**
Very large result sets are auto-truncated server-side. Narrow the query (shorter date range, specific `campaign_id`, higher sort discrimination) and retry.
Expand All @@ -144,6 +150,38 @@ Very large result sets are auto-truncated server-side. Narrow the query (shorter

## Support

### Getting help with a problem

These conversations happen in your terminal, so Taboola Support can't see them. If the plugin gave you a wrong answer or you're stuck, run:

```
/realize-plugin:support
```

Optionally describe the problem in the same line:

```
/realize-plugin:support the CPA it reported doesn't match what I see in the Realize UI
```

You'll get a preview of what will be captured. Once you confirm, it saves a single Markdown file to your Desktop containing the full conversation — every question you asked, every action the plugin took, and every response it got back — plus a summary of which tools and guidance were used, and the account IDs involved.

**Email it to [Support@taboola.com](mailto:Support@taboola.com).** The file opens with a "How to send this" section giving you the three pieces:

| Email part | What to use |
|---|---|
| Subject | The copy-ready line at the top of the file (your own description of the problem) |
| Body | **Section 1. Summary** — copy it as-is |
| Attachment | The file itself, so Support gets the failed actions and full transcript |

Subject and body are separated because Taboola's case system reads them into different fields — the subject becomes the case Subject, the body becomes the case Description.

Why the whole transcript rather than a summary: if the plugin misread your question, a summary written by that same plugin would repeat the misreading. The raw exchange lets Support see what actually happened.

**Your data:** nothing is sent anywhere automatically — the file is saved locally and emailing it is your decision. Credentials and tokens are stripped out. Account, campaign, and item IDs are deliberately kept, since Support can't reproduce an issue without them. The file also contains the campaign data shown in the session and your local folder paths; the preview tells you this before anything is written.

### Bugs and feature requests

For product or security concerns, bug reports, and feature requests, open an issue at
[github.com/taboola/realize-claude-plugin/issues](https://github.com/taboola/realize-claude-plugin/issues).

Expand Down
Loading
Loading