@rcs-campaigns/agent-toolkit 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,14 +24,18 @@ key** (Admin only). Pick the scopes the agent needs, then **copy the key — it
24
24
  shown exactly once** (`rcsk_live_…`). Treat it like a password; you can
25
25
  revoke/rotate it from the same screen.
26
26
 
27
- Scopes: `templates:read`, `templates:write`, `templates:submit`,
27
+ Scopes: `campaigns:read`, `campaigns:write`, `campaigns:submit`,
28
28
  `marketplace:clone`, `messages:send`, `usage:read`, `surveys:read`,
29
29
  `surveys:write`, `surveys:submit`, `surveys:send`, `results:read`.
30
30
 
31
- ### 2. Set the gateway URL
31
+ ### 2. Gateway URL (optional)
32
+
33
+ The toolkit talks to the hosted production gateway by default, so a production
34
+ key needs nothing else. Set `RCS_GATEWAY_URL` (or pass `--url`) only to target
35
+ dev or a local gateway:
32
36
 
33
37
  ```bash
34
- RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com
38
+ RCS_GATEWAY_URL=http://localhost:3002
35
39
  ```
36
40
 
37
41
  > The key is tied to the environment it was minted in — a key from one
@@ -67,7 +71,6 @@ pnpm --filter @rcs-campaigns/agent-toolkit build # -> dist/mcp.js, dist/cli.js
67
71
  ### 4. Verify the key works (before wiring up a host)
68
72
 
69
73
  ```bash
70
- export RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com
71
74
  export RCS_AGENT_KEY=rcsk_live_…
72
75
  npx -y -p @rcs-campaigns/agent-toolkit rcs capabilities
73
76
  ```
@@ -88,7 +91,6 @@ name.
88
91
 
89
92
  ```bash
90
93
  claude mcp add rcs-campaigns \
91
- --env RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com \
92
94
  --env RCS_AGENT_KEY=rcsk_live_… \
93
95
  -- npx -y -p @rcs-campaigns/agent-toolkit rcs-mcp
94
96
  ```
@@ -110,7 +112,6 @@ Edit `claude_desktop_config.json`
110
112
  "command": "npx",
111
113
  "args": ["-y", "-p", "@rcs-campaigns/agent-toolkit", "rcs-mcp"],
112
114
  "env": {
113
- "RCS_GATEWAY_URL": "https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com",
114
115
  "RCS_AGENT_KEY": "rcsk_live_…"
115
116
  }
116
117
  }
@@ -160,7 +161,7 @@ out — pipe into `jq`, wire into CI, or call from a skill.
160
161
  | Symptom | Cause / fix |
161
162
  |---|---|
162
163
  | `No agent key configured` | `RCS_AGENT_KEY` not reaching the process — for MCP hosts it must be in the server's `env` block, not your shell. |
163
- | `Gateway 401` | Key wrong, revoked, or minted in a different environment than `RCS_GATEWAY_URL` points at. |
164
+ | `Gateway 401` | Key wrong, revoked, or minted in a different environment than the gateway you're calling (production unless `RCS_GATEWAY_URL` says otherwise). |
164
165
  | `Gateway 403` (scope) | The key lacks the tool's scope — mint a new key with it (scopes are fixed at creation). |
165
166
  | Host shows no tools | Wrong path to `dist/mcp.js`, or the package wasn't built (`pnpm --filter @rcs-campaigns/agent-toolkit build`). |
166
167
  | Empty search results | The org has no published/owned templates yet — clone one from the marketplace first. |
@@ -172,7 +173,7 @@ out — pipe into `jq`, wire into CI, or call from a skill.
172
173
  ```ts
173
174
  import { GatewayClient, TOOLS } from '@rcs-campaigns/agent-toolkit';
174
175
 
175
- const client = GatewayClient.fromEnv(); // reads RCS_GATEWAY_URL + RCS_AGENT_KEY
176
+ const client = GatewayClient.fromEnv(); // reads RCS_AGENT_KEY (+ optional RCS_GATEWAY_URL)
176
177
  const { items } = await client.call({
177
178
  method: 'GET',
178
179
  path: '/templates',
package/dist/cli.js CHANGED
@@ -12,7 +12,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  * org's own brand) and reads RCS_*. Either prefix is accepted under either bin.
13
13
  *
14
14
  * Config (flags override env):
15
- * --url <gateway base url> | WA_GATEWAY_URL / RCS_GATEWAY_URL
15
+ * --url <gateway base url> | WA_GATEWAY_URL / RCS_GATEWAY_URL (optional;
16
+ * defaults to the hosted production gateway)
16
17
  * --key <rcsk_…> | WA_AGENT_KEY / RCS_AGENT_KEY
17
18
  * --channel RCS|WHATSAPP | WA_DEFAULT_CHANNEL / RCS_DEFAULT_CHANNEL
18
19
  *
@@ -29,30 +30,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
30
  */
30
31
  const node_fs_1 = require("node:fs");
31
32
  const client_1 = require("./client");
32
- /** Minimal argv parser: --flag value, --bool, and repeatable --to/--var. */
33
- function parseArgs(argv) {
34
- const out = { _: [], flags: {}, multi: { to: [], var: [], audience: [] } };
35
- for (let i = 0; i < argv.length; i++) {
36
- const a = argv[i];
37
- if (a.startsWith('--')) {
38
- const key = a.slice(2);
39
- const next = argv[i + 1];
40
- const takesValue = next !== undefined && !next.startsWith('--');
41
- const value = takesValue ? argv[++i] : true;
42
- if (key === 'to' || key === 'var' || key === 'audience') {
43
- if (typeof value === 'string')
44
- out.multi[key].push(value);
45
- }
46
- else {
47
- out.flags[key] = value;
48
- }
49
- }
50
- else {
51
- out._.push(a);
52
- }
53
- }
54
- return out;
55
- }
33
+ const tools_1 = require("./tools");
34
+ const cliDispatch_1 = require("./cliDispatch");
56
35
  /**
57
36
  * Read a JSON body from a file, letting the skin's default channel apply when
58
37
  * the document doesn't name one. An explicit `channel` in the file always wins,
@@ -83,11 +62,28 @@ function kvPairs(pairs) {
83
62
  function print(data) {
84
63
  process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
85
64
  }
65
+ // Catalog dispatch: everything without a curated command below is reached
66
+ // through the tool catalog, so a new gateway capability is callable from the
67
+ // CLI as soon as it has a `tools.ts` entry. The parsing lives in
68
+ // ./cliDispatch so it can be tested without running this entry point.
69
+ async function dispatchCatalog(words, argv, client, defaultChannel) {
70
+ const match = (0, cliDispatch_1.resolveTool)(words);
71
+ if (!match) {
72
+ fail(`Unknown command: ${words.join(' ') || '(none)'}\n\n${USAGE}`);
73
+ }
74
+ const { tool, used } = match;
75
+ const args = (0, cliDispatch_1.argsFromArgv)(tool, words.slice(used), argv);
76
+ return print(await client.call(tool.toRequest((0, tools_1.withDefaultChannel)(tool, args, defaultChannel))));
77
+ }
86
78
  const SKIN = (0, client_1.detectSkin)();
87
- const BIN = SKIN === 'wa' ? 'wa' : 'rcs';
79
+ const BRAND = (0, client_1.detectBrand)();
80
+ /** The bin name as the user typed it; the surveys packages ship their own. */
81
+ const BIN = BRAND === 'wa-surveys' || BRAND === 'rcs-surveys' ? BRAND : SKIN === 'wa' ? 'wa' : 'rcs';
82
+ /** Env var prefix: WA_* or RCS_*, never the bin name (wa-surveys → WA_*). */
83
+ const ENV_PREFIX = SKIN === 'wa' ? 'WA' : 'RCS';
88
84
  /** Show the channel this skin actually defaults to in its own help text. */
89
85
  const CHANNEL_EG = SKIN === 'wa' ? 'WHATSAPP' : 'RCS';
90
- const USAGE = `${BIN} — ${(0, client_1.skinLabels)(SKIN).product} agent CLI v${(0, client_1.readVersion)()}
86
+ const USAGE = `${BIN} — ${(0, client_1.skinLabels)(BRAND).product} agent CLI v${(0, client_1.readVersion)()}
91
87
 
92
88
  Usage:
93
89
  ${BIN} capabilities
@@ -111,11 +107,20 @@ Surveys:
111
107
  ${BIN} survey send <id> [--to +1555... ...] [--audience <audienceId> ...]
112
108
  ${BIN} survey send-test <id> --to +1555...
113
109
  ${BIN} survey sends <id>
114
- ${BIN} survey results <id>
110
+ ${BIN} survey results <id> [--campaign <campaignId>]
111
+
112
+ Everything else, straight from the tool catalog (scope shown after each):
113
+ ${(0, cliDispatch_1.catalogUsage)(BIN)}
115
114
 
116
- Config: --url/${BIN.toUpperCase()}_GATEWAY_URL, --key/${BIN.toUpperCase()}_AGENT_KEY`;
115
+ Any catalog tool name also works as typed (${BIN} list_conversations, or
116
+ ${BIN} list-conversations). Structured arguments come from --file <doc.json>:
117
+ an object merges at the top level, a bare array fills the tool's list argument.
118
+ Repeat a flag or comma-separate it for lists (--event a --event b).
119
+
120
+ Config: --key/${ENV_PREFIX}_AGENT_KEY (required),
121
+ --url/${ENV_PREFIX}_GATEWAY_URL (optional; defaults to the hosted gateway)`;
117
122
  async function run() {
118
- const argv = parseArgs(process.argv.slice(2));
123
+ const argv = (0, cliDispatch_1.parseArgs)(process.argv.slice(2));
119
124
  const cmd = argv._[0];
120
125
  if (!cmd || cmd === 'help' || argv.flags.help) {
121
126
  process.stdout.write(`${USAGE}\n`);
@@ -334,18 +339,20 @@ async function run() {
334
339
  path: `/surveys/${enc(id)}/sends`
335
340
  }));
336
341
  case 'results':
337
- requireArg(id, 'survey results <id>');
342
+ requireArg(id, 'survey results <id> [--campaign <campaignId>]');
338
343
  return print(await client.call({
339
344
  method: 'GET',
340
- path: `/surveys/${enc(id)}/results`
345
+ path: `/surveys/${enc(id)}/results`,
346
+ // One send's results (the campaignId `survey send` returns).
347
+ query: { campaignId: str(argv.flags.campaign) }
341
348
  }));
342
349
  default:
343
- fail(`Unknown survey subcommand: ${sub ?? '(none)'}\n\n${USAGE}`);
350
+ // `survey render|clone|delete` and anything the catalog grows later.
351
+ return dispatchCatalog(argv._, argv, client, config.defaultChannel);
344
352
  }
345
- return;
346
353
  }
347
354
  default:
348
- fail(`Unknown command: ${cmd}\n\n${USAGE}`);
355
+ return dispatchCatalog(argv._, argv, client, config.defaultChannel);
349
356
  }
350
357
  }
351
358
  function str(v) {
@@ -363,6 +370,10 @@ function fail(message) {
363
370
  process.exit(2);
364
371
  }
365
372
  run().catch((err) => {
373
+ if (err instanceof cliDispatch_1.UsageError) {
374
+ // Bad arguments, not a failed call — same exit 2 the curated commands use.
375
+ fail(err.message);
376
+ }
366
377
  if (err instanceof client_1.GatewayError) {
367
378
  process.stderr.write(`Gateway ${err.status}: ${err.message}\n`);
368
379
  if (err.details)
@@ -0,0 +1,65 @@
1
+ import { type ToolDef } from './tools';
2
+ export interface ParsedArgs {
3
+ _: string[];
4
+ flags: Record<string, string | boolean>;
5
+ multi: Record<string, string[]>;
6
+ /**
7
+ * EVERY flag's values in order, including the ones `multi` claims and the
8
+ * ones `flags` collapses to their last occurrence. Catalog dispatch needs
9
+ * this: an array-typed argument (`--event a --event b`) cannot be expressed
10
+ * by either of the other two, and hardcoding another name into `multi` would
11
+ * put us back to editing this file whenever the gateway grows an argument.
12
+ */
13
+ repeats: Record<string, string[]>;
14
+ }
15
+ /**
16
+ * A caller error — bad or missing arguments, unknown command.
17
+ *
18
+ * Thrown rather than exiting so these functions stay testable; `cli.ts` turns
19
+ * it into the same stderr + exit 2 every usage error has always produced.
20
+ */
21
+ export declare class UsageError extends Error {
22
+ }
23
+ /** Minimal argv parser: --flag value, --bool, and repeatable --to/--var. */
24
+ export declare function parseArgs(argv: string[]): ParsedArgs;
25
+ /**
26
+ * Catalog tools that already have a curated command, listed only so generated
27
+ * help does not print a second, uglier spelling of a command that has a good
28
+ * one. Being in this set does not disable catalog dispatch for the tool.
29
+ */
30
+ export declare const CURATED: Set<string>;
31
+ /**
32
+ * Friendlier spellings for generically-dispatched tools: `wa audience get`
33
+ * rather than `wa get_audience`. The catalog name always works too, in snake or
34
+ * kebab case, so a tool added tomorrow is callable before anyone writes it an
35
+ * alias here.
36
+ */
37
+ export declare const ALIASES: Record<string, string>;
38
+ /** The `:param` names in a tool's route, in the order the route names them. */
39
+ export declare function pathParams(tool: ToolDef): string[];
40
+ /** The friendly spelling of a tool, for help text and error messages. */
41
+ export declare function cliName(tool: ToolDef): string;
42
+ /**
43
+ * Resolve argv's leading words to a catalog tool. Two words are tried first so
44
+ * `audience get` wins over a tool literally named `audience`; the number of
45
+ * words consumed comes back so the caller knows where positionals start.
46
+ */
47
+ export declare function resolveTool(words: string[]): {
48
+ tool: ToolDef;
49
+ used: number;
50
+ } | undefined;
51
+ /** Coerce raw flag strings to the JSON-Schema type the tool declares. */
52
+ export declare function coerce(schema: any, values: string[]): unknown;
53
+ /**
54
+ * Build a tool's arguments from argv: positionals fill the route's `:params`,
55
+ * flags fill everything else, and `--file` supplies the structured ones — an
56
+ * object merges at the top level, a bare array fills the tool's single
57
+ * array-typed argument, so `--file contacts.json` works whether the file holds
58
+ * `[…]` or `{"contacts":[…]}`.
59
+ */
60
+ export declare function argsFromArgv(tool: ToolDef, positionals: string[], argv: ParsedArgs, readFile?: (path: string) => string): Record<string, any>;
61
+ /** One tool's usage line, derived from its schema rather than written by hand. */
62
+ export declare function toolUsage(tool: ToolDef, bin?: string): string;
63
+ /** The generated half of the help: every catalog tool without a curated command. */
64
+ export declare function catalogUsage(bin?: string): string;
65
+ //# sourceMappingURL=cliDispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cliDispatch.d.ts","sourceRoot":"","sources":["../src/cliDispatch.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,EAAE,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAChC;;;;;;OAMG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACnC;AAED;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,4EAA4E;AAC5E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAyBpD;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,aAyBlB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmC1C,CAAC;AAEF,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAElD;AAED,yEAAyE;AACzE,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAG7C;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EAAE,GACd;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAa7C;AAED,yEAAyE;AACzE,wBAAgB,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CA4B7D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,MAAM,EAAE,EACrB,IAAI,EAAE,UAAU,EAChB,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAuC,GAClE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAiDrB;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,SAAQ,GAAG,MAAM,CAc5D;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,GAAG,SAAQ,GAAG,MAAM,CAIhD"}
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALIASES = exports.CURATED = exports.UsageError = void 0;
4
+ exports.parseArgs = parseArgs;
5
+ exports.pathParams = pathParams;
6
+ exports.cliName = cliName;
7
+ exports.resolveTool = resolveTool;
8
+ exports.coerce = coerce;
9
+ exports.argsFromArgv = argsFromArgv;
10
+ exports.toolUsage = toolUsage;
11
+ exports.catalogUsage = catalogUsage;
12
+ /**
13
+ * Argument parsing and catalog dispatch for the `rcs` / `wa` CLI.
14
+ *
15
+ * Split out of `cli.ts` because that file is an entry point: it calls `run()`
16
+ * at module scope, so importing it to test anything would run the CLI. These
17
+ * are the pure parts — argv in, tool arguments out — and they are where the
18
+ * behaviour worth testing lives.
19
+ *
20
+ * WHY CATALOG DISPATCH EXISTS: `tools.ts` already declares every gateway
21
+ * capability (route, schema, scope) and a CI test fails when a route has no
22
+ * entry. The CLI used to hand-write its own routes anyway, so it drifted to
23
+ * ~20 of the catalog's 48 tools while the MCP server had all of them. Now the
24
+ * curated commands stay hand-written — they earn it, folding several flags into
25
+ * one endpoint — and everything else is reached through the catalog, so a new
26
+ * capability is callable from the CLI the moment it has a catalog entry.
27
+ */
28
+ const node_fs_1 = require("node:fs");
29
+ const tools_1 = require("./tools");
30
+ /**
31
+ * A caller error — bad or missing arguments, unknown command.
32
+ *
33
+ * Thrown rather than exiting so these functions stay testable; `cli.ts` turns
34
+ * it into the same stderr + exit 2 every usage error has always produced.
35
+ */
36
+ class UsageError extends Error {
37
+ }
38
+ exports.UsageError = UsageError;
39
+ /** Minimal argv parser: --flag value, --bool, and repeatable --to/--var. */
40
+ function parseArgs(argv) {
41
+ const out = {
42
+ _: [],
43
+ flags: {},
44
+ multi: { to: [], var: [], audience: [] },
45
+ repeats: {}
46
+ };
47
+ for (let i = 0; i < argv.length; i++) {
48
+ const a = argv[i];
49
+ if (a.startsWith('--')) {
50
+ const key = a.slice(2);
51
+ const next = argv[i + 1];
52
+ const takesValue = next !== undefined && !next.startsWith('--');
53
+ const value = takesValue ? argv[++i] : true;
54
+ if (typeof value === 'string')
55
+ (out.repeats[key] ??= []).push(value);
56
+ if (key === 'to' || key === 'var' || key === 'audience') {
57
+ if (typeof value === 'string')
58
+ out.multi[key].push(value);
59
+ }
60
+ else {
61
+ out.flags[key] = value;
62
+ }
63
+ }
64
+ else {
65
+ out._.push(a);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Catalog tools that already have a curated command, listed only so generated
72
+ * help does not print a second, uglier spelling of a command that has a good
73
+ * one. Being in this set does not disable catalog dispatch for the tool.
74
+ */
75
+ exports.CURATED = new Set([
76
+ 'capabilities',
77
+ 'search_templates',
78
+ 'get_template',
79
+ 'render_template',
80
+ 'create_template',
81
+ 'clone_template',
82
+ 'submit_template',
83
+ 'send_message',
84
+ 'upload_media',
85
+ 'get_knowledge',
86
+ 'set_knowledge',
87
+ 'delete_knowledge',
88
+ 'enable_knowledge',
89
+ 'disable_knowledge',
90
+ 'get_usage',
91
+ 'search_surveys',
92
+ 'get_survey',
93
+ 'create_survey',
94
+ 'update_survey',
95
+ 'submit_survey',
96
+ 'send_survey',
97
+ 'send_survey_test',
98
+ 'list_survey_sends',
99
+ 'get_results'
100
+ ]);
101
+ /**
102
+ * Friendlier spellings for generically-dispatched tools: `wa audience get`
103
+ * rather than `wa get_audience`. The catalog name always works too, in snake or
104
+ * kebab case, so a tool added tomorrow is callable before anyone writes it an
105
+ * alias here.
106
+ */
107
+ exports.ALIASES = {
108
+ audiences: 'search_audiences',
109
+ 'audience get': 'get_audience',
110
+ 'audience contacts': 'list_audience_contacts',
111
+ 'audience create': 'create_audience',
112
+ 'audience import': 'import_contacts',
113
+ 'audience update': 'update_audience',
114
+ 'audience remove': 'remove_audience_contacts',
115
+ 'audience delete': 'delete_audience',
116
+ contacts: 'list_contacts',
117
+ 'contact get': 'get_contact',
118
+ 'contact update': 'update_contact',
119
+ conversations: 'list_conversations',
120
+ 'conversation get': 'get_conversation',
121
+ 'conversation assign': 'assign_conversation',
122
+ webhooks: 'list_webhooks',
123
+ 'webhook create': 'create_webhook',
124
+ 'webhook update': 'update_webhook',
125
+ 'webhook delete': 'delete_webhook',
126
+ 'webhook test': 'test_webhook',
127
+ 'template update': 'update_template',
128
+ 'template duplicate': 'duplicate_template',
129
+ 'template delete': 'delete_template',
130
+ 'survey render': 'render_survey',
131
+ 'survey clone': 'clone_survey',
132
+ 'survey delete': 'delete_survey',
133
+ campaigns: 'list_campaigns',
134
+ analytics: 'get_campaign_analytics',
135
+ groups: 'list_campaign_groups',
136
+ 'group get': 'get_campaign_group',
137
+ 'group create': 'create_campaign_group',
138
+ 'group update': 'update_campaign_group',
139
+ 'group delete': 'delete_campaign_group',
140
+ 'group add': 'add_campaigns_to_group',
141
+ 'group remove': 'remove_campaigns_from_group'
142
+ };
143
+ /** The `:param` names in a tool's route, in the order the route names them. */
144
+ function pathParams(tool) {
145
+ return (tool.path.match(/:([A-Za-z0-9_]+)/g) ?? []).map((s) => s.slice(1));
146
+ }
147
+ /** The friendly spelling of a tool, for help text and error messages. */
148
+ function cliName(tool) {
149
+ const alias = Object.entries(exports.ALIASES).find(([, name]) => name === tool.name);
150
+ return alias ? alias[0] : tool.name;
151
+ }
152
+ /**
153
+ * Resolve argv's leading words to a catalog tool. Two words are tried first so
154
+ * `audience get` wins over a tool literally named `audience`; the number of
155
+ * words consumed comes back so the caller knows where positionals start.
156
+ */
157
+ function resolveTool(words) {
158
+ const two = words.slice(0, 2).join(' ');
159
+ if (words.length >= 2 && exports.ALIASES[two]) {
160
+ const tool = (0, tools_1.findTool)(exports.ALIASES[two]);
161
+ if (tool)
162
+ return { tool, used: 2 };
163
+ }
164
+ const one = words[0];
165
+ if (!one)
166
+ return undefined;
167
+ const direct = (exports.ALIASES[one] ? (0, tools_1.findTool)(exports.ALIASES[one]) : undefined) ??
168
+ (0, tools_1.findTool)(one) ??
169
+ (0, tools_1.findTool)(one.replace(/-/g, '_'));
170
+ return direct ? { tool: direct, used: 1 } : undefined;
171
+ }
172
+ /** Coerce raw flag strings to the JSON-Schema type the tool declares. */
173
+ function coerce(schema, values) {
174
+ const raw = schema?.type;
175
+ const type = Array.isArray(raw) ? raw.find((t) => t !== 'null') : raw;
176
+ if (type === 'array') {
177
+ // Repeat the flag, or comma-separate it — both read naturally in a shell.
178
+ const items = values.length > 1 ? values : values[0].split(',');
179
+ const itemType = schema.items?.type;
180
+ return items.map((v) => itemType === 'number' || itemType === 'integer'
181
+ ? Number(v)
182
+ : itemType === 'boolean'
183
+ ? v !== 'false'
184
+ : v);
185
+ }
186
+ const last = values[values.length - 1];
187
+ if (type === 'number' || type === 'integer')
188
+ return Number(last);
189
+ if (type === 'boolean')
190
+ return last !== 'false';
191
+ if (type === 'object') {
192
+ try {
193
+ return JSON.parse(last);
194
+ }
195
+ catch {
196
+ throw new UsageError('an object argument must be JSON on the command line, or come from --file');
197
+ }
198
+ }
199
+ return last;
200
+ }
201
+ /**
202
+ * Build a tool's arguments from argv: positionals fill the route's `:params`,
203
+ * flags fill everything else, and `--file` supplies the structured ones — an
204
+ * object merges at the top level, a bare array fills the tool's single
205
+ * array-typed argument, so `--file contacts.json` works whether the file holds
206
+ * `[…]` or `{"contacts":[…]}`.
207
+ */
208
+ function argsFromArgv(tool, positionals, argv, readFile = (p) => (0, node_fs_1.readFileSync)(p, 'utf8')) {
209
+ const props = (tool.inputSchema.properties ?? {});
210
+ const args = {};
211
+ const file = typeof argv.flags.file === 'string' ? argv.flags.file : undefined;
212
+ if (file) {
213
+ const doc = JSON.parse(readFile(file));
214
+ if (Array.isArray(doc)) {
215
+ const arrayProp = Object.keys(props).find((k) => props[k]?.type === 'array');
216
+ if (!arrayProp) {
217
+ throw new UsageError(`${cliName(tool)} takes no list argument — --file must hold a JSON object`);
218
+ }
219
+ args[arrayProp] = doc;
220
+ }
221
+ else if (doc && typeof doc === 'object') {
222
+ Object.assign(args, doc);
223
+ }
224
+ else {
225
+ throw new UsageError('--file must hold a JSON object or array');
226
+ }
227
+ }
228
+ for (const [key, schema] of Object.entries(props)) {
229
+ if (key === 'file')
230
+ continue;
231
+ const values = argv.repeats[key];
232
+ if (values?.length) {
233
+ args[key] = coerce(schema, values);
234
+ }
235
+ else if (schema?.type === 'boolean' && argv.flags[key] === true) {
236
+ args[key] = true;
237
+ }
238
+ }
239
+ pathParams(tool).forEach((param, i) => {
240
+ if (args[param] === undefined && positionals[i] !== undefined) {
241
+ args[param] = positionals[i];
242
+ }
243
+ });
244
+ const missing = (tool.inputSchema.required ?? []).filter((k) => args[k] === undefined);
245
+ if (missing.length) {
246
+ throw new UsageError(`${cliName(tool)} is missing: ${missing.join(', ')}\n\n${toolUsage(tool)}`);
247
+ }
248
+ return args;
249
+ }
250
+ /** One tool's usage line, derived from its schema rather than written by hand. */
251
+ function toolUsage(tool, bin = 'rcs') {
252
+ const props = (tool.inputSchema.properties ?? {});
253
+ const required = new Set(tool.inputSchema.required ?? []);
254
+ const params = pathParams(tool);
255
+ const flags = Object.keys(props)
256
+ .filter((k) => !params.includes(k))
257
+ .map((k) => {
258
+ // A nullable argument declares `["string","null"]`; read it as string|null.
259
+ const raw = props[k]?.type;
260
+ const type = Array.isArray(raw) ? raw.join('|') : (raw ?? 'value');
261
+ const one = type === 'boolean' ? `--${k}` : `--${k} <${type}>`;
262
+ return required.has(k) ? one : `[${one}]`;
263
+ });
264
+ return ` ${bin} ${[cliName(tool), ...params.map((p) => `<${p}>`), ...flags].join(' ')}`;
265
+ }
266
+ /** The generated half of the help: every catalog tool without a curated command. */
267
+ function catalogUsage(bin = 'rcs') {
268
+ return tools_1.TOOLS.filter((t) => !exports.CURATED.has(t.name))
269
+ .map((t) => `${toolUsage(t, bin)} # ${t.scope}`)
270
+ .join('\n');
271
+ }
package/dist/client.d.ts CHANGED
@@ -41,6 +41,14 @@ export interface GatewayConfig {
41
41
  * argv, a bundler-rewritten path, or a direct import all fall through to 'rcs'.
42
42
  */
43
43
  export declare function detectSkin(argv?: readonly string[]): 'wa' | 'rcs';
44
+ /**
45
+ * The product a bin is branded as. The surveys packages (@wa-surveys/…,
46
+ * @rcs-surveys/…) are aliases like @wa-campaigns/…: same gateway, same catalog.
47
+ * The brand only changes the words the host and the model read; the channel
48
+ * default and env prefix still come from {@link detectSkin}.
49
+ */
50
+ export type Brand = 'rcs-campaigns' | 'wa-campaigns' | 'rcs-surveys' | 'wa-surveys';
51
+ export declare function detectBrand(argv?: readonly string[]): Brand;
44
52
  /**
45
53
  * The version of the package the user actually installed, read at runtime rather
46
54
  * than restated in a constant. A hardcoded copy silently goes stale the first
@@ -61,12 +69,22 @@ export declare function detectSkin(argv?: readonly string[]): 'wa' | 'rcs';
61
69
  * and `dist/mcp.js` alike.
62
70
  */
63
71
  export declare function readVersion(argv?: readonly string[]): string;
64
- /** Human-readable product + channel names for the running skin. */
65
- export declare function skinLabels(skin?: 'wa' | 'rcs'): {
72
+ /**
73
+ * Human-readable product + channel names for the running bin. Accepts a skin
74
+ * ('wa' | 'rcs', meaning the campaigns brand) or a full brand.
75
+ */
76
+ export declare function skinLabels(skinOrBrand?: 'wa' | 'rcs' | Brand): {
66
77
  product: string;
67
78
  channel: string;
68
- slug: string;
79
+ slug: Brand;
69
80
  };
81
+ /**
82
+ * The hosted production gateway. Both skins (wa and rcs) talk to this one
83
+ * deployment, so a customer only has to supply an agent key. Set
84
+ * WA_GATEWAY_URL / RCS_GATEWAY_URL (or --url) to point at dev or a local
85
+ * gateway instead.
86
+ */
87
+ export declare const DEFAULT_GATEWAY_URL = "https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com";
70
88
  /**
71
89
  * Normalize a channel from configuration or a tool argument. Accepts the channel
72
90
  * enum itself (any case) AND a brand id — canonical or legacy — resolving it to
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA4E;AAC5E,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,IAAI,GAAG,KAAK,CAG/E;AAmCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,MAAM,CAO1E;AAED,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,IAAI,GAAE,IAAI,GAAG,KAAoB;;;;EAI3D;AA6BD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAMpE;AAOD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;gBACN,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CA4B7E;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAElD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa;IAI/D,+EAA+E;IACzE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAmCvD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA4E;AAC5E,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,UAAU,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,IAAI,GAAG,KAAK,CAE/E;AAED;;;;;GAKG;AACH,MAAM,MAAM,KAAK,GAAG,eAAe,GAAG,cAAc,GAAG,aAAa,GAAG,YAAY,CAAC;AAEpF,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,KAAK,CAczE;AA8CD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,MAAM,CAO1E;AASD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,WAAW,GAAE,IAAI,GAAG,KAAK,GAAG,KAAqB;aAX/B,MAAM;aAAW,MAAM;UAAQ,KAAK;EAehF;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,iEACgC,CAAC;AA6BjE;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CAMpE;AAOD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpD,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;gBACN,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa,CA4B7E;AAED,qBAAa,aAAa;IACZ,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,aAAa;IAElD,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,aAAa;IAI/D,+EAA+E;IACzE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;CAmCvD"}
package/dist/client.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GatewayClient = exports.GatewayError = void 0;
3
+ exports.GatewayClient = exports.GatewayError = exports.DEFAULT_GATEWAY_URL = void 0;
4
4
  exports.detectSkin = detectSkin;
5
+ exports.detectBrand = detectBrand;
5
6
  exports.readVersion = readVersion;
6
7
  exports.skinLabels = skinLabels;
7
8
  exports.normalizeChannel = normalizeChannel;
@@ -18,8 +19,22 @@ const node_path_1 = require("node:path");
18
19
  * argv, a bundler-rewritten path, or a direct import all fall through to 'rcs'.
19
20
  */
20
21
  function detectSkin(argv = process.argv) {
21
- const name = invokedBinName(argv);
22
- return name === 'wa' || name === 'wa-mcp' ? 'wa' : 'rcs';
22
+ return detectBrand(argv).startsWith('wa-') ? 'wa' : 'rcs';
23
+ }
24
+ function detectBrand(argv = process.argv) {
25
+ switch (invokedBinName(argv)) {
26
+ case 'wa':
27
+ case 'wa-mcp':
28
+ return 'wa-campaigns';
29
+ case 'wa-surveys':
30
+ case 'wa-surveys-mcp':
31
+ return 'wa-surveys';
32
+ case 'rcs-surveys':
33
+ case 'rcs-surveys-mcp':
34
+ return 'rcs-surveys';
35
+ default:
36
+ return 'rcs-campaigns';
37
+ }
23
38
  }
24
39
  /**
25
40
  * The bin basename this process was launched as — "wa-mcp", "mcp", "cli" — or
@@ -42,7 +57,18 @@ function invokedBinName(argv) {
42
57
  * — hence "mcp" and "cli" rather than "rcs-mcp" and "rcs". Anything not in this
43
58
  * set is not one of ours, and is never trusted to locate a package.
44
59
  */
45
- const OWN_BINS = new Set(['wa', 'wa-mcp', 'rcs', 'rcs-mcp', 'mcp', 'cli']);
60
+ const OWN_BINS = new Set([
61
+ 'wa',
62
+ 'wa-mcp',
63
+ 'rcs',
64
+ 'rcs-mcp',
65
+ 'mcp',
66
+ 'cli',
67
+ 'wa-surveys',
68
+ 'wa-surveys-mcp',
69
+ 'rcs-surveys',
70
+ 'rcs-surveys-mcp'
71
+ ]);
46
72
  /** The `version` of the package.json in `dir`, or undefined if unreadable. */
47
73
  function packageVersionAt(dir) {
48
74
  try {
@@ -81,12 +107,27 @@ function readVersion(argv = process.argv) {
81
107
  }
82
108
  return packageVersionAt((0, node_path_1.join)(__dirname, '..')) ?? '0.0.0';
83
109
  }
84
- /** Human-readable product + channel names for the running skin. */
85
- function skinLabels(skin = detectSkin()) {
86
- return skin === 'wa'
87
- ? { product: 'WhatsApp Campaigns', channel: 'WhatsApp', slug: 'wa-campaigns' }
88
- : { product: 'RCS Campaigns', channel: 'RCS', slug: 'rcs-campaigns' };
110
+ const BRAND_LABELS = {
111
+ 'wa-campaigns': { product: 'WhatsApp Campaigns', channel: 'WhatsApp', slug: 'wa-campaigns' },
112
+ 'rcs-campaigns': { product: 'RCS Campaigns', channel: 'RCS', slug: 'rcs-campaigns' },
113
+ 'wa-surveys': { product: 'WhatsApp Surveys', channel: 'WhatsApp', slug: 'wa-surveys' },
114
+ 'rcs-surveys': { product: 'RCS Surveys', channel: 'RCS', slug: 'rcs-surveys' }
115
+ };
116
+ /**
117
+ * Human-readable product + channel names for the running bin. Accepts a skin
118
+ * ('wa' | 'rcs', meaning the campaigns brand) or a full brand.
119
+ */
120
+ function skinLabels(skinOrBrand = detectBrand()) {
121
+ const brand = skinOrBrand === 'wa' ? 'wa-campaigns' : skinOrBrand === 'rcs' ? 'rcs-campaigns' : skinOrBrand;
122
+ return { ...BRAND_LABELS[brand] };
89
123
  }
124
+ /**
125
+ * The hosted production gateway. Both skins (wa and rcs) talk to this one
126
+ * deployment, so a customer only has to supply an agent key. Set
127
+ * WA_GATEWAY_URL / RCS_GATEWAY_URL (or --url) to point at dev or a local
128
+ * gateway instead.
129
+ */
130
+ exports.DEFAULT_GATEWAY_URL = 'https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com';
90
131
  /** First environment variable that is set and non-empty, under either prefix. */
91
132
  function fromEnv(...names) {
92
133
  for (const name of names) {
@@ -159,7 +200,7 @@ function resolveConfig(partial) {
159
200
  const [keyFirst, keySecond] = skin === 'wa'
160
201
  ? ['WA_AGENT_KEY', 'RCS_AGENT_KEY']
161
202
  : ['RCS_AGENT_KEY', 'WA_AGENT_KEY'];
162
- const baseUrl = partial?.baseUrl ?? fromEnv(urlFirst, urlSecond) ?? 'http://localhost:3002';
203
+ const baseUrl = partial?.baseUrl ?? fromEnv(urlFirst, urlSecond) ?? exports.DEFAULT_GATEWAY_URL;
163
204
  const apiKey = partial?.apiKey ?? fromEnv(keyFirst, keySecond) ?? '';
164
205
  if (!apiKey) {
165
206
  throw new Error(`No agent key configured. Set ${keyFirst} or pass apiKey explicitly.`);
package/dist/mcp.js CHANGED
@@ -11,13 +11,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  * runtime dependencies — nothing to install, nothing to drift, and it slots into
12
12
  * the workspace without touching the lockfile's external graph.
13
13
  *
14
- * Credentials: the agent key + gateway URL come from the environment
15
- * (RCS_AGENT_KEY / RCS_GATEWAY_URL). This process holds the key only to forward
14
+ * Credentials: the agent key comes from the environment (RCS_AGENT_KEY); the
15
+ * gateway URL defaults to the hosted production gateway and can be overridden
16
+ * with RCS_GATEWAY_URL. This process holds the key only to forward
16
17
  * it as a Bearer token to the gateway — all business logic stays server-side.
17
18
  *
18
19
  * Wire it up (e.g. Claude Code):
19
20
  * claude mcp add rcs-campaigns -- npx -y -p @rcs-campaigns/agent-toolkit rcs-mcp
20
- * (with RCS_AGENT_KEY + RCS_GATEWAY_URL set in the environment)
21
+ * (with RCS_AGENT_KEY set in the environment)
21
22
  */
22
23
  const node_readline_1 = require("node:readline");
23
24
  const client_1 = require("./client");
@@ -30,8 +31,7 @@ const PROTOCOL_VERSION = '2024-11-05';
30
31
  * "RCS-first" in its own tool list, contradicting the docs that sent it here.
31
32
  * Same catalog, same gateway — only the words the model reads differ.
32
33
  */
33
- const SKIN = (0, client_1.detectSkin)();
34
- const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)(SKIN);
34
+ const { product: PRODUCT, channel: CHANNEL, slug: SLUG } = (0, client_1.skinLabels)((0, client_1.detectBrand)());
35
35
  const SERVER_INFO = { name: SLUG, version: (0, client_1.readVersion)() };
36
36
  /**
37
37
  * Surfaced to MCP hosts as the initialize result's `instructions` string — hosts
@@ -47,7 +47,7 @@ const INSTRUCTIONS = [
47
47
  ' search_templates → clone_template (into your org) → render_template (preview)',
48
48
  ' → submit_template (provider approval) → send_message.',
49
49
  'Surveys mirror it: search_surveys/create_survey → submit_survey → send_survey →',
50
- 'get_results.',
50
+ 'get_results (pass the campaignId send_survey returns for that send only).',
51
51
  '',
52
52
  `IMPORTANT — rich ${CHANNEL} requires approval. A freshly created or cloned`,
53
53
  'template delivers as SMS fallback ONLY until it is submitted AND',
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5E,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,4EAA4E;IAC5E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,qEAAqE;IACrE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC;CACvD;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,8QA+BtB,CAAC;AAEX,eAAO,MAAM,KAAK,EAAE,OAAO,EA4uC1B,CAAC;AAEF,8BAA8B;AAC9B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAE1D;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAKrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,cAAc,CAAC,EAAE,OAAO,GACvB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAoB,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5E,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,4EAA4E;IAC5E,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ,CAAC;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACpC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;KAChC,CAAC;IACF,qEAAqE;IACrE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC;CACvD;AAmBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,mBAAmB,8QA+BtB,CAAC;AAEX,eAAO,MAAM,KAAK,EAAE,OAAO,EAgjD1B,CAAC;AAEF,8BAA8B;AAC9B,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAE1D;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAKrD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzB,cAAc,CAAC,EAAE,OAAO,GACvB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAcrB"}
package/dist/tools.js CHANGED
@@ -101,8 +101,8 @@ exports.TOOLS = [
101
101
  },
102
102
  {
103
103
  name: 'search_templates',
104
- description: 'Search the published RCS template marketplace. Returns normalized listings (id, name, rating, cloneCount, categoryKeys).',
105
- scope: 'templates:read',
104
+ description: 'Search the published template MARKETPLACE (templates from every org, to clone). Returns normalized listings (id, name, rating, cloneCount, categoryKeys). For your OWN templates use list_templates.',
105
+ scope: 'campaigns:read',
106
106
  method: 'GET',
107
107
  path: '/templates',
108
108
  inputSchema: {
@@ -136,10 +136,45 @@ exports.TOOLS = [
136
136
  }
137
137
  })
138
138
  },
139
+ {
140
+ name: 'list_templates',
141
+ description: "List YOUR org's own message templates (not the marketplace), newest first, with channel and provider approval status (APPROVED / PENDING / REJECTED / NOT_SUBMITTED) and any rejection reason. Filter by text, channel or approvalStatus. Archived templates only with includeArchived.",
142
+ scope: 'campaigns:read',
143
+ method: 'GET',
144
+ path: '/my-templates',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ q: { type: 'string', description: 'Free-text search over name + description.' },
149
+ channel: { type: 'string', enum: ['RCS', 'WHATSAPP', 'SMS'] },
150
+ approvalStatus: {
151
+ type: 'string',
152
+ enum: ['APPROVED', 'PENDING', 'REJECTED', 'NOT_SUBMITTED'],
153
+ description: 'Only templates in this provider approval state.'
154
+ },
155
+ includeArchived: { type: 'boolean', default: false },
156
+ page: { type: 'number', minimum: 1 },
157
+ pageSize: { type: 'number', minimum: 1, maximum: 100 }
158
+ },
159
+ additionalProperties: false
160
+ },
161
+ toRequest: (a) => ({
162
+ method: 'GET',
163
+ path: '/my-templates',
164
+ query: {
165
+ q: a.q,
166
+ channel: a.channel,
167
+ approvalStatus: a.approvalStatus,
168
+ includeArchived: a.includeArchived,
169
+ page: a.page,
170
+ pageSize: a.pageSize
171
+ }
172
+ })
173
+ },
139
174
  {
140
175
  name: 'get_template',
141
176
  description: 'Fetch one template by id (a published marketplace listing or one your org owns).',
142
- scope: 'templates:read',
177
+ scope: 'campaigns:read',
143
178
  method: 'GET',
144
179
  path: '/templates/:id',
145
180
  inputSchema: {
@@ -156,7 +191,7 @@ exports.TOOLS = [
156
191
  {
157
192
  name: 'render_template',
158
193
  description: 'Validate a template and return the rendered RCS payload + variable-bound SMS/MMS fallback, without sending. Use to inspect before cloning/sending.',
159
- scope: 'templates:read',
194
+ scope: 'campaigns:read',
160
195
  method: 'POST',
161
196
  path: '/templates/:id/render',
162
197
  inputSchema: {
@@ -181,7 +216,7 @@ exports.TOOLS = [
181
216
  {
182
217
  name: 'create_template',
183
218
  description: 'Create a template draft in your org. Compose a TemplateContent (text/card/carousel) with a REQUIRED non-empty smsFallback; the shared RCS validator gates it. Submit it (submit_template) and await provider approval before send_message for rich RCS; an unsubmitted template delivers as SMS fallback.',
184
- scope: 'templates:write',
219
+ scope: 'campaigns:write',
185
220
  method: 'POST',
186
221
  path: '/templates',
187
222
  successStatus: 201,
@@ -220,7 +255,7 @@ exports.TOOLS = [
220
255
  {
221
256
  name: 'update_template',
222
257
  description: 'Partially update a template your org owns: name, description, category, tags, knowledgeDoc, and content (re-validated by the shared RCS validator — a non-empty smsFallback is still required). Only the fields you pass change. Editing the CONTENT of an already-approved template invalidates that approval: submit_template it again and await approval, or it delivers as SMS fallback until then. Use get_template first to edit incrementally rather than overwriting blind.',
223
- scope: 'templates:write',
258
+ scope: 'campaigns:write',
224
259
  method: 'PUT',
225
260
  path: '/templates/:id',
226
261
  inputSchema: {
@@ -265,7 +300,7 @@ exports.TOOLS = [
265
300
  {
266
301
  name: 'duplicate_template',
267
302
  description: 'Duplicate a template your org OWNS into a fresh private draft (e.g. to iterate a variant without touching the original). The copy keeps the content, channel, category, tags and knowledge base, but starts unsubmitted — it has no provider approval of its own until you submit_template it. For copying someone else\'s PUBLISHED marketplace listing, use clone_template instead.',
268
- scope: 'templates:write',
303
+ scope: 'campaigns:write',
269
304
  method: 'POST',
270
305
  path: '/templates/:id/duplicate',
271
306
  successStatus: 201,
@@ -283,7 +318,7 @@ exports.TOOLS = [
283
318
  {
284
319
  name: 'delete_template',
285
320
  description: 'Delete a template your org owns. IRREVERSIBLE, and it also removes the template from the provider (Twilio), so an approved template loses its approval and can no longer be sent. Broadcasts that already went out are unaffected. Prefer this only for drafts and abandoned iterations; ask the user before deleting anything they have submitted or sent.',
286
- scope: 'templates:write',
321
+ scope: 'campaigns:write',
287
322
  method: 'DELETE',
288
323
  path: '/templates/:id',
289
324
  inputSchema: {
@@ -318,7 +353,7 @@ exports.TOOLS = [
318
353
  {
319
354
  name: 'submit_template',
320
355
  description: 'Submit an org-owned template to the provider (Twilio) for approval. Returns the contentSid + approval status.',
321
- scope: 'templates:submit',
356
+ scope: 'campaigns:submit',
322
357
  method: 'POST',
323
358
  path: '/templates/:id/submit',
324
359
  inputSchema: {
@@ -523,7 +558,7 @@ exports.TOOLS = [
523
558
  },
524
559
  {
525
560
  name: 'send_survey',
526
- description: 'Send a survey to explicit E.164 numbers (the CRM-triggered single-customer send) and/or saved audiences. Flips the survey ACTIVE. Bills your org. Returns { recipients, sent }.',
561
+ description: 'Send a survey to explicit E.164 numbers (the CRM-triggered single-customer send) and/or saved audiences. Flips the survey ACTIVE. Bills your org. Returns { recipients, sent }. Every send is a campaign (WhatsApp sends the approved Flow, RCS the question-by-question run) and also returns campaignId and queued: sending: true means the rest is still going out in the background (do not send again), and halted / heldByAllowance / groundStopped say why a send stopped short.',
527
562
  scope: 'surveys:send',
528
563
  method: 'POST',
529
564
  path: '/surveys/:id/send',
@@ -630,19 +665,26 @@ exports.TOOLS = [
630
665
  },
631
666
  {
632
667
  name: 'get_results',
633
- description: 'Read a survey’s aggregated results: per-question option counts / rating averages / text samples + completion totals (multi-part questions aggregate per part; free-text answers are auto-translated to English).',
668
+ description: 'Read a survey’s aggregated results: per-question option counts / rating averages / text samples + completion totals (multi-part questions aggregate per part; free-text answers are auto-translated to English). Pass campaignId (returned by send_survey, or a SURVEY campaign’s id from list_campaigns) for that one send’s results; without it the totals span the whole survey, test sends and every campaign included.',
634
669
  scope: 'results:read',
635
670
  method: 'GET',
636
671
  path: '/surveys/:id/results',
637
672
  inputSchema: {
638
673
  type: 'object',
639
- properties: { id: { type: 'string' } },
674
+ properties: {
675
+ id: { type: 'string' },
676
+ campaignId: {
677
+ type: 'string',
678
+ description: 'Only the responses answered from this survey campaign (24-hex id). A campaign that is not this survey’s is a 404.'
679
+ }
680
+ },
640
681
  required: ['id'],
641
682
  additionalProperties: false
642
683
  },
643
684
  toRequest: (a) => ({
644
685
  method: 'GET',
645
- path: `/surveys/${encodeURIComponent(a.id)}/results`
686
+ path: `/surveys/${encodeURIComponent(a.id)}/results`,
687
+ query: { campaignId: a.campaignId }
646
688
  })
647
689
  },
648
690
  {
@@ -685,7 +727,7 @@ exports.TOOLS = [
685
727
  {
686
728
  name: 'get_knowledge',
687
729
  description: "Read a template's knowledge base: the grounding document text plus whether answering is enabled. Use before set_knowledge to edit a doc incrementally instead of overwriting it blind.",
688
- scope: 'templates:read',
730
+ scope: 'campaigns:read',
689
731
  method: 'GET',
690
732
  path: '/templates/:id/knowledge',
691
733
  inputSchema: {
@@ -702,7 +744,7 @@ exports.TOOLS = [
702
744
  {
703
745
  name: 'set_knowledge',
704
746
  description: 'Set/replace a template\'s knowledge base: the grounding document AI answers are generated from when recipients reply. Include operating instructions in the doc itself — e.g. "Any question not answered by this document must be answered exactly: …". Setting a doc re-enables answering.',
705
- scope: 'templates:write',
747
+ scope: 'campaigns:write',
706
748
  method: 'PUT',
707
749
  path: '/templates/:id/knowledge',
708
750
  inputSchema: {
@@ -726,7 +768,7 @@ exports.TOOLS = [
726
768
  {
727
769
  name: 'delete_knowledge',
728
770
  description: 'Delete a template\'s knowledge base document.',
729
- scope: 'templates:write',
771
+ scope: 'campaigns:write',
730
772
  method: 'DELETE',
731
773
  path: '/templates/:id/knowledge',
732
774
  inputSchema: {
@@ -743,7 +785,7 @@ exports.TOOLS = [
743
785
  {
744
786
  name: 'enable_knowledge',
745
787
  description: 'Enable knowledge answering on a template (the doc is used again).',
746
- scope: 'templates:write',
788
+ scope: 'campaigns:write',
747
789
  method: 'POST',
748
790
  path: '/templates/:id/knowledge/enable',
749
791
  inputSchema: {
@@ -760,7 +802,7 @@ exports.TOOLS = [
760
802
  {
761
803
  name: 'disable_knowledge',
762
804
  description: 'Disable knowledge answering on a template WITHOUT deleting the doc (pause).',
763
- scope: 'templates:write',
805
+ scope: 'campaigns:write',
764
806
  method: 'POST',
765
807
  path: '/templates/:id/knowledge/disable',
766
808
  inputSchema: {
@@ -930,6 +972,54 @@ exports.TOOLS = [
930
972
  body: { contacts: a.contacts }
931
973
  })
932
974
  },
975
+ {
976
+ name: 'update_audience',
977
+ description: 'Rename an audience or change its description (null or an empty string clears it). Only the fields you pass change; its contacts are untouched.',
978
+ scope: 'audiences:write',
979
+ method: 'PATCH',
980
+ path: '/audiences/:id',
981
+ inputSchema: {
982
+ type: 'object',
983
+ properties: {
984
+ id: { type: 'string' },
985
+ name: { type: 'string', minLength: 1, maxLength: 120 },
986
+ description: { type: ['string', 'null'], maxLength: 500 }
987
+ },
988
+ required: ['id'],
989
+ additionalProperties: false
990
+ },
991
+ toRequest: (a) => ({
992
+ method: 'PATCH',
993
+ path: `/audiences/${encodeURIComponent(a.id)}`,
994
+ body: { name: a.name, description: a.description }
995
+ })
996
+ },
997
+ {
998
+ name: 'remove_audience_contacts',
999
+ description: "Take people off an audience, by each row's `id` from list_audience_contacts (its `contactId` works too). This is not an opt-out: the person, their message history and their other audiences are untouched, and ids not in the audience are ignored. Returns the audience with its new contactCount and how many were removed.",
1000
+ scope: 'audiences:write',
1001
+ method: 'POST',
1002
+ path: '/audiences/:id/contacts/remove',
1003
+ inputSchema: {
1004
+ type: 'object',
1005
+ properties: {
1006
+ id: { type: 'string' },
1007
+ contactIds: {
1008
+ type: 'array',
1009
+ items: { type: 'string' },
1010
+ minItems: 1,
1011
+ maxItems: 500
1012
+ }
1013
+ },
1014
+ required: ['id', 'contactIds'],
1015
+ additionalProperties: false
1016
+ },
1017
+ toRequest: (a) => ({
1018
+ method: 'POST',
1019
+ path: `/audiences/${encodeURIComponent(a.id)}/contacts/remove`,
1020
+ body: { contactIds: a.contactIds }
1021
+ })
1022
+ },
933
1023
  {
934
1024
  name: 'delete_audience',
935
1025
  description: 'Delete an audience and all of its contacts (org-scoped, irreversible).',
@@ -1293,6 +1383,227 @@ exports.TOOLS = [
1293
1383
  tags: a.tags
1294
1384
  }
1295
1385
  })
1386
+ },
1387
+ // ── Campaign reads (the Messages list + the Analytics page, aggregate only) ──
1388
+ {
1389
+ name: 'list_campaigns',
1390
+ description: "List YOUR org's campaigns (broadcasts), newest first, with status (DRAFT / SCHEDULED / SENDING / SENT / FAILED / CANCELLED), channel, the template sent, audience count, schedule (scheduledAt in the campaign's own timezone) and the delivery funnel (total, delivered, read, failed, fellBackToSms, clicked). Archived campaigns are hidden. Read-only: creating, scheduling or sending a campaign happens in the web app.",
1391
+ scope: 'campaigns:read',
1392
+ method: 'GET',
1393
+ path: '/campaigns',
1394
+ inputSchema: {
1395
+ type: 'object',
1396
+ properties: {
1397
+ q: { type: 'string', maxLength: 120, description: 'Free-text search over the campaign name.' },
1398
+ status: {
1399
+ type: 'string',
1400
+ enum: ['DRAFT', 'SCHEDULED', 'SENDING', 'SENT', 'FAILED', 'CANCELLED']
1401
+ },
1402
+ channel: { type: 'string', enum: ['RCS', 'WHATSAPP'] },
1403
+ page: { type: 'number', minimum: 1 },
1404
+ pageSize: { type: 'number', minimum: 1, maximum: 100 }
1405
+ },
1406
+ additionalProperties: false
1407
+ },
1408
+ toRequest: (a) => ({
1409
+ method: 'GET',
1410
+ path: '/campaigns',
1411
+ query: {
1412
+ q: a.q,
1413
+ status: a.status,
1414
+ channel: a.channel,
1415
+ page: a.page,
1416
+ pageSize: a.pageSize
1417
+ }
1418
+ })
1419
+ },
1420
+ {
1421
+ name: 'get_campaign_analytics',
1422
+ description: "Campaign analytics, the same figures as the web app's Analytics page: totals and rates (delivered, read, clicked, failed, fell back to SMS) over the last N days (default 30, max 365), a daily series (UTC days; hourly for a 1-day window), failure classes by provider error code, after-delivery outcomes (replied, opportunity, not interested) and a per-campaign table. Pass broadcastId (a list_campaigns id) to narrow every figure to one campaign over its own lifetime. Aggregate only — no phone numbers or message text.",
1423
+ scope: 'campaigns:read',
1424
+ method: 'GET',
1425
+ path: '/analytics',
1426
+ inputSchema: {
1427
+ type: 'object',
1428
+ properties: {
1429
+ channel: {
1430
+ type: 'string',
1431
+ enum: ['RCS', 'WHATSAPP'],
1432
+ description: 'Only this channel. Omit for every campaign.'
1433
+ },
1434
+ days: { type: 'number', minimum: 1, maximum: 365 },
1435
+ broadcastId: { type: 'string', description: 'One campaign (its list_campaigns id).' }
1436
+ },
1437
+ additionalProperties: false
1438
+ },
1439
+ toRequest: (a) => ({
1440
+ method: 'GET',
1441
+ path: '/analytics',
1442
+ query: { channel: a.channel, days: a.days, broadcastId: a.broadcastId }
1443
+ })
1444
+ },
1445
+ // ── Campaign groups (named sets of campaigns — bookkeeping, never a send) ──
1446
+ {
1447
+ name: 'list_campaign_groups',
1448
+ description: "List YOUR org's campaign groups — named sets of campaigns filed together (an event, a launch, a quarter) — most recently changed first, each with its member campaign ids, campaign count and combined delivery funnel (every member's funnel added together, from cached figures). Pass broadcastId (a list_campaigns id) for only the groups that campaign is in.",
1449
+ scope: 'campaigns:read',
1450
+ method: 'GET',
1451
+ path: '/campaign-groups',
1452
+ inputSchema: {
1453
+ type: 'object',
1454
+ properties: {
1455
+ broadcastId: {
1456
+ type: 'string',
1457
+ description: 'Only the groups this campaign (a list_campaigns id) is in.'
1458
+ }
1459
+ },
1460
+ additionalProperties: false
1461
+ },
1462
+ toRequest: (a) => ({
1463
+ method: 'GET',
1464
+ path: '/campaign-groups',
1465
+ query: { broadcastId: a.broadcastId }
1466
+ })
1467
+ },
1468
+ {
1469
+ name: 'get_campaign_group',
1470
+ description: 'One campaign group: its member campaigns (status, channel, schedule and each funnel, refreshed when stale) and their combined funnel. Campaigns deleted since they were filed are left out.',
1471
+ scope: 'campaigns:read',
1472
+ method: 'GET',
1473
+ path: '/campaign-groups/:id',
1474
+ inputSchema: {
1475
+ type: 'object',
1476
+ properties: { id: { type: 'string' } },
1477
+ required: ['id'],
1478
+ additionalProperties: false
1479
+ },
1480
+ toRequest: (a) => ({
1481
+ method: 'GET',
1482
+ path: `/campaign-groups/${encodeURIComponent(a.id)}`
1483
+ })
1484
+ },
1485
+ {
1486
+ name: 'create_campaign_group',
1487
+ description: 'Create a campaign group, optionally filing campaigns (list_campaigns ids) in it at once. Names are unique per org, ignoring case and spacing. A group never changes who a campaign reaches or when it sends; a campaign may be in several groups.',
1488
+ scope: 'campaigns:write',
1489
+ method: 'POST',
1490
+ path: '/campaign-groups',
1491
+ successStatus: 201,
1492
+ inputSchema: {
1493
+ type: 'object',
1494
+ properties: {
1495
+ name: { type: 'string', minLength: 1, maxLength: 120 },
1496
+ description: { type: 'string', maxLength: 500 },
1497
+ broadcastIds: {
1498
+ type: 'array',
1499
+ items: { type: 'string' },
1500
+ maxItems: 500,
1501
+ description: 'Campaigns (list_campaigns ids) to start the group with.'
1502
+ }
1503
+ },
1504
+ required: ['name'],
1505
+ additionalProperties: false
1506
+ },
1507
+ toRequest: (a) => ({
1508
+ method: 'POST',
1509
+ path: '/campaign-groups',
1510
+ body: {
1511
+ name: a.name,
1512
+ description: a.description,
1513
+ broadcastIds: a.broadcastIds
1514
+ }
1515
+ })
1516
+ },
1517
+ {
1518
+ name: 'update_campaign_group',
1519
+ description: "Rename a campaign group or change its description (null clears it). Only the fields you pass change; use add_campaigns_to_group / remove_campaigns_from_group to change its campaigns.",
1520
+ scope: 'campaigns:write',
1521
+ method: 'PATCH',
1522
+ path: '/campaign-groups/:id',
1523
+ inputSchema: {
1524
+ type: 'object',
1525
+ properties: {
1526
+ id: { type: 'string' },
1527
+ name: { type: 'string', minLength: 1, maxLength: 120 },
1528
+ description: { type: ['string', 'null'], maxLength: 500 }
1529
+ },
1530
+ required: ['id'],
1531
+ additionalProperties: false
1532
+ },
1533
+ toRequest: (a) => ({
1534
+ method: 'PATCH',
1535
+ path: `/campaign-groups/${encodeURIComponent(a.id)}`,
1536
+ body: { name: a.name, description: a.description }
1537
+ })
1538
+ },
1539
+ {
1540
+ name: 'delete_campaign_group',
1541
+ description: 'Delete a campaign group. Only the group goes: its campaigns, their recipients and their results are untouched.',
1542
+ scope: 'campaigns:write',
1543
+ method: 'DELETE',
1544
+ path: '/campaign-groups/:id',
1545
+ inputSchema: {
1546
+ type: 'object',
1547
+ properties: { id: { type: 'string' } },
1548
+ required: ['id'],
1549
+ additionalProperties: false
1550
+ },
1551
+ toRequest: (a) => ({
1552
+ method: 'DELETE',
1553
+ path: `/campaign-groups/${encodeURIComponent(a.id)}`
1554
+ })
1555
+ },
1556
+ {
1557
+ name: 'add_campaigns_to_group',
1558
+ description: 'File campaigns (list_campaigns ids) in a campaign group. Idempotent: a campaign already in the group stays where it is. Returns the group with its campaigns.',
1559
+ scope: 'campaigns:write',
1560
+ method: 'POST',
1561
+ path: '/campaign-groups/:id/campaigns',
1562
+ inputSchema: {
1563
+ type: 'object',
1564
+ properties: {
1565
+ id: { type: 'string' },
1566
+ broadcastIds: {
1567
+ type: 'array',
1568
+ items: { type: 'string' },
1569
+ minItems: 1,
1570
+ maxItems: 500
1571
+ }
1572
+ },
1573
+ required: ['id', 'broadcastIds'],
1574
+ additionalProperties: false
1575
+ },
1576
+ toRequest: (a) => ({
1577
+ method: 'POST',
1578
+ path: `/campaign-groups/${encodeURIComponent(a.id)}/campaigns`,
1579
+ body: { broadcastIds: a.broadcastIds }
1580
+ })
1581
+ },
1582
+ {
1583
+ name: 'remove_campaigns_from_group',
1584
+ description: 'Take campaigns out of a campaign group. The campaigns themselves are untouched; ids not in the group are ignored. Returns the group with its campaigns.',
1585
+ scope: 'campaigns:write',
1586
+ method: 'POST',
1587
+ path: '/campaign-groups/:id/campaigns/remove',
1588
+ inputSchema: {
1589
+ type: 'object',
1590
+ properties: {
1591
+ id: { type: 'string' },
1592
+ broadcastIds: {
1593
+ type: 'array',
1594
+ items: { type: 'string' },
1595
+ minItems: 1,
1596
+ maxItems: 500
1597
+ }
1598
+ },
1599
+ required: ['id', 'broadcastIds'],
1600
+ additionalProperties: false
1601
+ },
1602
+ toRequest: (a) => ({
1603
+ method: 'POST',
1604
+ path: `/campaign-groups/${encodeURIComponent(a.id)}/campaigns/remove`,
1605
+ body: { broadcastIds: a.broadcastIds }
1606
+ })
1296
1607
  }
1297
1608
  ];
1298
1609
  /** Look up a tool by name. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rcs-campaigns/agent-toolkit",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "mcpName": "com.rcs-campaigns/campaigns",
5
5
  "description": "Agent skins for the RCS Campaigns gateway: an MCP server (rcs-mcp / wa-mcp) and a CLI (rcs / wa) over one shared client.",
6
6
  "license": "UNLICENSED",
@@ -63,7 +63,7 @@
63
63
  "scripts": {
64
64
  "build": "tsc",
65
65
  "dev": "tsc --watch",
66
- "typecheck": "tsc --noEmit",
66
+ "typecheck": "tsc --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsc/typecheck.tsbuildinfo",
67
67
  "test": "vitest run",
68
68
  "test:watch": "vitest"
69
69
  }