@rcs-campaigns/agent-toolkit 0.1.5

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 ADDED
@@ -0,0 +1,176 @@
1
+ # @rcs-campaigns/agent-toolkit
2
+
3
+ Agent skins for the RCS Campaigns **agent gateway** (`/api/agent/*`): an **MCP
4
+ server** (`rcs-mcp`) and a **CLI** (`rcs`), sharing one typed gateway client and
5
+ one declarative tool catalog. Zero runtime dependencies.
6
+
7
+ > Part of the agent-enablement work — see
8
+ > [`docs/design/AGENT-ENABLEMENT-AND-X402-DESIGN.md`](../../docs/design/AGENT-ENABLEMENT-AND-X402-DESIGN.md).
9
+ > This implements **P0–P4** (gateway client, MCP, CLI, skills). The x402
10
+ > pay-per-call rail (P1/P5) is intentionally **not** included yet.
11
+
12
+ The MCP server lets Claude (Claude Code, Claude Desktop, or any MCP host) search,
13
+ read, render, create, clone, submit, and send RCS templates through your org's
14
+ gateway — authenticated with one agent key.
15
+
16
+ ---
17
+
18
+ ## Setup (5 minutes)
19
+
20
+ ### 1. Get an agent key
21
+
22
+ In the app at **https://app.rcs-campaigns.com**: **Settings → API keys → Create
23
+ key** (Admin only). Pick the scopes the agent needs, then **copy the key — it is
24
+ shown exactly once** (`rcsk_live_…`). Treat it like a password; you can
25
+ revoke/rotate it from the same screen.
26
+
27
+ Scopes: `templates:read`, `templates:write`, `templates:submit`,
28
+ `marketplace:clone`, `messages:send`, `usage:read`, `surveys:read`,
29
+ `surveys:write`, `surveys:submit`, `surveys:send`, `results:read`.
30
+
31
+ ### 2. Set the gateway URL
32
+
33
+ ```bash
34
+ RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com
35
+ ```
36
+
37
+ > The key is tied to the environment it was minted in — a key from one
38
+ > environment will **401** against another (they're different databases).
39
+
40
+ ### 3. Run it — published on npm
41
+
42
+ The package is on the public registry, so the quickest path needs no checkout or
43
+ build. It ships **two** bins (`rcs` and `rcs-mcp`), so under `npx` you must select
44
+ one with `-p` (the package) followed by the bin name — `npx @rcs-campaigns/agent-toolkit rcs …`
45
+ fails with *"could not determine executable to run"* because `agent-toolkit` is
46
+ not itself a bin:
47
+
48
+ ```bash
49
+ npx -y -p @rcs-campaigns/agent-toolkit rcs capabilities
50
+ ```
51
+
52
+ Or install the bins globally and call them directly:
53
+
54
+ ```bash
55
+ npm i -g @rcs-campaigns/agent-toolkit
56
+ rcs capabilities
57
+ rcs-mcp # stdio MCP server
58
+ ```
59
+
60
+ Monorepo contributors can run from source instead — build once from the root:
61
+
62
+ ```bash
63
+ pnpm install
64
+ pnpm --filter @rcs-campaigns/agent-toolkit build # -> dist/mcp.js, dist/cli.js
65
+ ```
66
+
67
+ ### 4. Verify the key works (before wiring up a host)
68
+
69
+ ```bash
70
+ export RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com
71
+ export RCS_AGENT_KEY=rcsk_live_…
72
+ npx -y -p @rcs-campaigns/agent-toolkit rcs capabilities
73
+ ```
74
+
75
+ A healthy response lists `grantedScopes` and the tool catalog. If you get
76
+ `Gateway 401`, the key is wrong/rotated or points at the wrong environment.
77
+
78
+ ---
79
+
80
+ ## Add to an MCP host
81
+
82
+ ### Claude Code
83
+
84
+ ```bash
85
+ claude mcp add rcs-campaigns \
86
+ --env RCS_GATEWAY_URL=https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com \
87
+ --env RCS_AGENT_KEY=rcsk_live_… \
88
+ -- npx -y -p @rcs-campaigns/agent-toolkit rcs-mcp
89
+ ```
90
+
91
+ Running from a local checkout instead? Swap the launch command for the built
92
+ path: `-- node /ABSOLUTE/PATH/TO/rcs-templates/packages/agent-toolkit/dist/mcp.js`
93
+ (use an **absolute** path). Confirm with `claude mcp list` (should show
94
+ `rcs-campaigns`), then ask Claude to “list the RCS Campaigns tools.”
95
+
96
+ ### Claude Desktop
97
+
98
+ Edit `claude_desktop_config.json`
99
+ (macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`) and add:
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "rcs-campaigns": {
105
+ "command": "npx",
106
+ "args": ["-y", "-p", "@rcs-campaigns/agent-toolkit", "rcs-mcp"],
107
+ "env": {
108
+ "RCS_GATEWAY_URL": "https://rcs-templates-api-dot-msg2ai-server.ue.r.appspot.com",
109
+ "RCS_AGENT_KEY": "rcsk_live_…"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ Restart Claude Desktop. The tools appear under the 🔌 menu. (From a local
117
+ checkout, set `"command": "node"` and `"args"` to the absolute `dist/mcp.js` path
118
+ instead.)
119
+
120
+ ### Tools exposed
121
+
122
+ Templates: `capabilities`, `search_templates`, `get_template`,
123
+ `render_template`, `create_template`, `clone_template`, `submit_template`,
124
+ `send_message`, `get_usage`. Surveys: `search_surveys`, `get_survey`,
125
+ `render_survey`, `create_survey`, `clone_survey`, `submit_survey`,
126
+ `send_survey`, `get_results`. Each `*:write` / `*:send` tool needs the matching
127
+ scope on your key.
128
+
129
+ ---
130
+
131
+ ## CLI (`rcs`)
132
+
133
+ Same client, same auth — handy for scripts and a quick smoke test. Run it via
134
+ `npx` (`npx -y -p @rcs-campaigns/agent-toolkit rcs <cmd>`), the global `rcs`
135
+ binary, or `node dist/cli.js` from a checkout — all equivalent:
136
+
137
+ ```bash
138
+ rcs capabilities
139
+ rcs search "appointment reminder" --channel RCS --sort rating
140
+ rcs get <id>
141
+ rcs render <id> --var firstName=Ada
142
+ rcs clone <id>
143
+ rcs create --file template.json
144
+ rcs send <blueprintId> --to +15551112222 --var firstName=Ada
145
+ rcs usage
146
+ ```
147
+
148
+ Config flags override env: `--url <gateway>` / `--key <rcsk_…>`. JSON in / JSON
149
+ out — pipe into `jq`, wire into CI, or call from a skill.
150
+
151
+ ---
152
+
153
+ ## Troubleshooting
154
+
155
+ | Symptom | Cause / fix |
156
+ |---|---|
157
+ | `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. |
158
+ | `Gateway 401` | Key wrong, revoked, or minted in a different environment than `RCS_GATEWAY_URL` points at. |
159
+ | `Gateway 403` (scope) | The key lacks the tool's scope — mint a new key with it (scopes are fixed at creation). |
160
+ | Host shows no tools | Wrong path to `dist/mcp.js`, or the package wasn't built (`pnpm --filter @rcs-campaigns/agent-toolkit build`). |
161
+ | Empty search results | The org has no published/owned templates yet — clone one from the marketplace first. |
162
+
163
+ ---
164
+
165
+ ## Embed the client
166
+
167
+ ```ts
168
+ import { GatewayClient, TOOLS } from '@rcs-campaigns/agent-toolkit';
169
+
170
+ const client = GatewayClient.fromEnv(); // reads RCS_GATEWAY_URL + RCS_AGENT_KEY
171
+ const { items } = await client.call({
172
+ method: 'GET',
173
+ path: '/templates',
174
+ query: { q: 'promo' }
175
+ });
176
+ ```
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * `rcs` / `wa` — a thin CLI over the agent gateway. JSON in / JSON out so it
6
+ * composes in shells, CI, and skill-chain steps. Zero dependencies (Node 22
7
+ * argv + global fetch). It shares the SAME gateway client + tool catalog as the
8
+ * MCP server, so the two skins never diverge.
9
+ *
10
+ * One build, two bins. Invoked as `wa` it defaults to the WhatsApp channel and
11
+ * reads WA_* config; invoked as `rcs` it leaves the channel to the server (the
12
+ * org's own brand) and reads RCS_*. Either prefix is accepted under either bin.
13
+ *
14
+ * Config (flags override env):
15
+ * --url <gateway base url> | WA_GATEWAY_URL / RCS_GATEWAY_URL
16
+ * --key <rcsk_…> | WA_AGENT_KEY / RCS_AGENT_KEY
17
+ * --channel RCS|WHATSAPP | WA_DEFAULT_CHANNEL / RCS_DEFAULT_CHANNEL
18
+ *
19
+ * Commands:
20
+ * rcs capabilities
21
+ * rcs search [query] [--channel ${CHANNEL_EG}] [--category EVENTS] [--sort rating|used]
22
+ * rcs get <id>
23
+ * rcs render <id> [--var key=value ...]
24
+ * rcs create --file <template.json>
25
+ * rcs clone <id>
26
+ * rcs submit <id>
27
+ * rcs send <blueprintId> --to +1555... [--to ...] [--var key=value ...] [--name "..."]
28
+ * rcs usage
29
+ */
30
+ const node_fs_1 = require("node:fs");
31
+ 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
+ }
56
+ /**
57
+ * Read a JSON body from a file, letting the skin's default channel apply when
58
+ * the document doesn't name one. An explicit `channel` in the file always wins,
59
+ * so this only decides what "unspecified" means — WhatsApp under `wa`, the org's
60
+ * own brand channel under `rcs`.
61
+ */
62
+ function readBody(file, defaultChannel) {
63
+ const body = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
64
+ if (defaultChannel &&
65
+ body &&
66
+ typeof body === 'object' &&
67
+ !Array.isArray(body) &&
68
+ body.channel === undefined) {
69
+ return { ...body, channel: defaultChannel };
70
+ }
71
+ return body;
72
+ }
73
+ /** Turn ["key=value", ...] into { key: value }. */
74
+ function kvPairs(pairs) {
75
+ const obj = {};
76
+ for (const p of pairs) {
77
+ const eq = p.indexOf('=');
78
+ if (eq > 0)
79
+ obj[p.slice(0, eq)] = p.slice(eq + 1);
80
+ }
81
+ return obj;
82
+ }
83
+ function print(data) {
84
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
85
+ }
86
+ const SKIN = (0, client_1.detectSkin)();
87
+ const BIN = SKIN === 'wa' ? 'wa' : 'rcs';
88
+ /** Show the channel this skin actually defaults to in its own help text. */
89
+ const CHANNEL_EG = SKIN === 'wa' ? 'WHATSAPP' : 'RCS';
90
+ const USAGE = `${BIN} — ${(0, client_1.skinLabels)(SKIN).product} agent CLI v${(0, client_1.readVersion)()}
91
+
92
+ Usage:
93
+ ${BIN} capabilities
94
+ ${BIN} search [query] [--channel ${CHANNEL_EG}] [--category KEY] [--sort rating|used]
95
+ ${BIN} get <id>
96
+ ${BIN} render <id> [--var key=value ...]
97
+ ${BIN} create --file <template.json>
98
+ ${BIN} clone <id>
99
+ ${BIN} submit <id>
100
+ ${BIN} send <blueprintId> --to +1555... [--to ...] [--var key=value ...] [--name "..."]
101
+ ${BIN} upload-image (--file <image.png> | --from-url <https-url>) # returns the GCS URL
102
+ ${BIN} knowledge <id> (--get | --set-file <doc.md> | --delete | --enable | --disable)
103
+ ${BIN} usage
104
+
105
+ Surveys:
106
+ ${BIN} surveys [query] [--status DRAFT|SCHEDULED|ACTIVE|CLOSED]
107
+ ${BIN} survey get <id>
108
+ ${BIN} survey create --file <survey.json>
109
+ ${BIN} survey update <id> --file <patch.json>
110
+ ${BIN} survey submit <id>
111
+ ${BIN} survey send <id> [--to +1555... ...] [--audience <audienceId> ...]
112
+ ${BIN} survey send-test <id> --to +1555...
113
+ ${BIN} survey sends <id>
114
+ ${BIN} survey results <id>
115
+
116
+ Config: --url/${BIN.toUpperCase()}_GATEWAY_URL, --key/${BIN.toUpperCase()}_AGENT_KEY`;
117
+ async function run() {
118
+ const argv = parseArgs(process.argv.slice(2));
119
+ const cmd = argv._[0];
120
+ if (!cmd || cmd === 'help' || argv.flags.help) {
121
+ process.stdout.write(`${USAGE}\n`);
122
+ return;
123
+ }
124
+ const config = (0, client_1.resolveConfig)({
125
+ baseUrl: typeof argv.flags.url === 'string' ? argv.flags.url : undefined,
126
+ apiKey: typeof argv.flags.key === 'string' ? argv.flags.key : undefined
127
+ });
128
+ const client = new client_1.GatewayClient(config);
129
+ switch (cmd) {
130
+ case 'capabilities':
131
+ return print(await client.call({ method: 'GET', path: '/capabilities' }));
132
+ case 'search':
133
+ return print(await client.call({
134
+ method: 'GET',
135
+ path: '/templates',
136
+ query: {
137
+ // Join the remaining positional words so `rcs search appointment
138
+ // reminder` searches the full phrase, not just the first word.
139
+ q: argv._.slice(1).join(' ') || undefined,
140
+ channel: str(argv.flags.channel) ?? config.defaultChannel,
141
+ category: str(argv.flags.category),
142
+ sort: str(argv.flags.sort)
143
+ }
144
+ }));
145
+ case 'get':
146
+ requireArg(argv._[1], 'get <id>');
147
+ return print(await client.call({
148
+ method: 'GET',
149
+ path: `/templates/${enc(argv._[1])}`
150
+ }));
151
+ case 'render':
152
+ requireArg(argv._[1], 'render <id>');
153
+ return print(await client.call({
154
+ method: 'POST',
155
+ path: `/templates/${enc(argv._[1])}/render`,
156
+ body: { variables: kvPairs(argv.multi.var) }
157
+ }));
158
+ case 'create': {
159
+ const file = str(argv.flags.file);
160
+ if (!file)
161
+ fail('create requires --file <template.json>');
162
+ const body = readBody(file, config.defaultChannel);
163
+ return print(await client.call({ method: 'POST', path: '/templates', body }));
164
+ }
165
+ case 'clone':
166
+ requireArg(argv._[1], 'clone <id>');
167
+ return print(await client.call({
168
+ method: 'POST',
169
+ path: `/marketplace/${enc(argv._[1])}/clone`
170
+ }));
171
+ case 'submit':
172
+ requireArg(argv._[1], 'submit <id>');
173
+ return print(await client.call({
174
+ method: 'POST',
175
+ path: `/templates/${enc(argv._[1])}/submit`
176
+ }));
177
+ case 'send': {
178
+ requireArg(argv._[1], 'send <blueprintId> --to +1...');
179
+ if (argv.multi.to.length === 0)
180
+ fail('send requires at least one --to <E.164>');
181
+ return print(await client.call({
182
+ method: 'POST',
183
+ path: '/messages/send',
184
+ body: {
185
+ blueprintId: argv._[1],
186
+ to: argv.multi.to,
187
+ variables: kvPairs(argv.multi.var),
188
+ name: str(argv.flags.name)
189
+ }
190
+ }));
191
+ }
192
+ case 'upload-image': {
193
+ // Store a picture on the platform GCS bucket (the only URLs survey/
194
+ // template media should use). --from-url re-hosts a web image server-side.
195
+ const file = str(argv.flags.file);
196
+ const fromUrl = str(argv.flags['from-url']);
197
+ if (!file === !fromUrl) {
198
+ fail('upload-image requires exactly one of --file or --from-url');
199
+ }
200
+ if (fromUrl) {
201
+ return print(await client.call({
202
+ method: 'POST',
203
+ path: '/media/upload',
204
+ body: { sourceUrl: fromUrl }
205
+ }));
206
+ }
207
+ const MIME_BY_EXT = {
208
+ png: 'image/png',
209
+ jpg: 'image/jpeg',
210
+ jpeg: 'image/jpeg',
211
+ webp: 'image/webp',
212
+ gif: 'image/gif'
213
+ };
214
+ const ext = file.split('.').pop()?.toLowerCase() ?? '';
215
+ const mime = MIME_BY_EXT[ext];
216
+ if (!mime)
217
+ fail('upload-image --file must be .png, .jpg, .webp, or .gif');
218
+ const bytes = (0, node_fs_1.readFileSync)(file);
219
+ return print(await client.call({
220
+ method: 'POST',
221
+ path: '/media/upload',
222
+ body: {
223
+ base64: bytes.toString('base64'),
224
+ mime,
225
+ filename: file.split('/').pop()
226
+ }
227
+ }));
228
+ }
229
+ case 'knowledge': {
230
+ // Knowledge base management: read the grounding doc, set/replace it,
231
+ // delete it, or flip the answering master switch without touching it.
232
+ requireArg(argv._[1], 'knowledge <id> ...');
233
+ const id = enc(argv._[1]);
234
+ const setFile = str(argv.flags['set-file']);
235
+ const picked = [
236
+ argv.flags.get ? 'get' : null,
237
+ setFile ? 'set-file' : null,
238
+ argv.flags.delete ? 'delete' : null,
239
+ argv.flags.enable ? 'enable' : null,
240
+ argv.flags.disable ? 'disable' : null
241
+ ].filter(Boolean);
242
+ if (picked.length !== 1) {
243
+ fail('knowledge requires exactly one of --get, --set-file <doc>, --delete, --enable, --disable');
244
+ }
245
+ if (argv.flags.get) {
246
+ return print(await client.call({ method: 'GET', path: `/templates/${id}/knowledge` }));
247
+ }
248
+ if (setFile) {
249
+ return print(await client.call({
250
+ method: 'PUT',
251
+ path: `/templates/${id}/knowledge`,
252
+ body: { doc: (0, node_fs_1.readFileSync)(setFile, 'utf8') }
253
+ }));
254
+ }
255
+ if (argv.flags.delete) {
256
+ return print(await client.call({ method: 'DELETE', path: `/templates/${id}/knowledge` }));
257
+ }
258
+ return print(await client.call({
259
+ method: 'POST',
260
+ path: `/templates/${id}/knowledge/${argv.flags.enable ? 'enable' : 'disable'}`
261
+ }));
262
+ }
263
+ case 'usage':
264
+ return print(await client.call({ method: 'GET', path: '/usage' }));
265
+ case 'surveys':
266
+ return print(await client.call({
267
+ method: 'GET',
268
+ path: '/surveys',
269
+ query: {
270
+ q: argv._.slice(1).join(' ') || undefined,
271
+ status: str(argv.flags.status)
272
+ }
273
+ }));
274
+ case 'survey': {
275
+ const sub = argv._[1];
276
+ const id = argv._[2];
277
+ switch (sub) {
278
+ case 'get':
279
+ requireArg(id, 'survey get <id>');
280
+ return print(await client.call({ method: 'GET', path: `/surveys/${enc(id)}` }));
281
+ case 'create': {
282
+ const file = str(argv.flags.file);
283
+ if (!file)
284
+ fail('survey create requires --file <survey.json>');
285
+ const body = readBody(file, config.defaultChannel);
286
+ return print(await client.call({ method: 'POST', path: '/surveys', body }));
287
+ }
288
+ case 'update': {
289
+ requireArg(id, 'survey update <id> --file <patch.json>');
290
+ const file = str(argv.flags.file);
291
+ if (!file)
292
+ fail('survey update requires --file <patch.json>');
293
+ const body = readBody(file, config.defaultChannel);
294
+ return print(await client.call({
295
+ method: 'PATCH',
296
+ path: `/surveys/${enc(id)}`,
297
+ body
298
+ }));
299
+ }
300
+ case 'submit':
301
+ requireArg(id, 'survey submit <id>');
302
+ return print(await client.call({
303
+ method: 'POST',
304
+ path: `/surveys/${enc(id)}/submit`
305
+ }));
306
+ case 'send': {
307
+ requireArg(id, 'survey send <id> [--to +1555...] [--audience <id>]');
308
+ if (argv.multi.to.length === 0 && argv.multi.audience.length === 0)
309
+ fail('survey send requires --to and/or --audience');
310
+ return print(await client.call({
311
+ method: 'POST',
312
+ path: `/surveys/${enc(id)}/send`,
313
+ body: {
314
+ to: argv.multi.to.length ? argv.multi.to : undefined,
315
+ audienceIds: argv.multi.audience.length
316
+ ? argv.multi.audience
317
+ : undefined
318
+ }
319
+ }));
320
+ }
321
+ case 'send-test':
322
+ requireArg(id, 'survey send-test <id> --to +1555...');
323
+ if (argv.multi.to.length !== 1)
324
+ fail('survey send-test requires exactly one --to <E.164>');
325
+ return print(await client.call({
326
+ method: 'POST',
327
+ path: `/surveys/${enc(id)}/send-test`,
328
+ body: { number: argv.multi.to[0] }
329
+ }));
330
+ case 'sends':
331
+ requireArg(id, 'survey sends <id>');
332
+ return print(await client.call({
333
+ method: 'GET',
334
+ path: `/surveys/${enc(id)}/sends`
335
+ }));
336
+ case 'results':
337
+ requireArg(id, 'survey results <id>');
338
+ return print(await client.call({
339
+ method: 'GET',
340
+ path: `/surveys/${enc(id)}/results`
341
+ }));
342
+ default:
343
+ fail(`Unknown survey subcommand: ${sub ?? '(none)'}\n\n${USAGE}`);
344
+ }
345
+ return;
346
+ }
347
+ default:
348
+ fail(`Unknown command: ${cmd}\n\n${USAGE}`);
349
+ }
350
+ }
351
+ function str(v) {
352
+ return typeof v === 'string' ? v : undefined;
353
+ }
354
+ function enc(v) {
355
+ return encodeURIComponent(v);
356
+ }
357
+ function requireArg(v, usage) {
358
+ if (!v)
359
+ fail(`Missing argument. Usage: rcs ${usage}`);
360
+ }
361
+ function fail(message) {
362
+ process.stderr.write(`${message}\n`);
363
+ process.exit(2);
364
+ }
365
+ run().catch((err) => {
366
+ if (err instanceof client_1.GatewayError) {
367
+ process.stderr.write(`Gateway ${err.status}: ${err.message}\n`);
368
+ if (err.details)
369
+ process.stderr.write(`${JSON.stringify(err.details)}\n`);
370
+ }
371
+ else {
372
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
373
+ }
374
+ process.exit(1);
375
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Typed HTTP client for the RCS Campaigns `/api/agent/*` gateway.
3
+ *
4
+ * Shared by BOTH skins in this package (the MCP server and the CLI) so they can
5
+ * never drift on how they talk to the gateway — the same single contract the
6
+ * design calls for ("one gateway, three skins"). Zero dependencies: Node 22's
7
+ * global `fetch` is used directly.
8
+ *
9
+ * Auth is the agent key (`rcsk_…`) the org minted; it is sent as a Bearer token,
10
+ * exactly what agentAuth expects on the server. The base URL + key come from the
11
+ * environment or are passed explicitly.
12
+ *
13
+ * Two skins, one package: the same build ships as the RCS-branded `rcs`/`rcs-mcp`
14
+ * bins and the WhatsApp-branded `wa`/`wa-mcp` bins. They differ only in naming
15
+ * and in which channel they default to — the gateway, the key, and the tool
16
+ * catalog are identical. Environment variables are therefore accepted under
17
+ * BOTH prefixes (WA_* and RCS_*), so a user of either skin can configure it with
18
+ * the names that skin's docs use.
19
+ */
20
+ /** The messaging channel a call acts on. Mirrors the gateway's own enum. */
21
+ export type Channel = 'RCS' | 'WHATSAPP';
22
+ export interface GatewayConfig {
23
+ /** Base URL of the RCS Campaigns server, e.g. https://api.example.com */
24
+ baseUrl: string;
25
+ /** The org-minted agent key (rcsk_live_…). */
26
+ apiKey: string;
27
+ /**
28
+ * Channel to assume when a call accepts one and the caller omits it. Undefined
29
+ * leaves the decision to the server, which falls back to the org's own brand
30
+ * channel — the historical behaviour, and what the `rcs` skin still does.
31
+ */
32
+ defaultChannel?: Channel;
33
+ }
34
+ /**
35
+ * Which skin this process was launched as, from the bin name in argv.
36
+ *
37
+ * `wa`/`wa-mcp` mean the caller came in through the WhatsApp-branded docs and
38
+ * expects WhatsApp unless they say otherwise. Anything else (including being
39
+ * imported as a library) is treated as the neutral RCS skin, which leaves the
40
+ * channel to the server. Detection is deliberately conservative: an unknown
41
+ * argv, a bundler-rewritten path, or a direct import all fall through to 'rcs'.
42
+ */
43
+ export declare function detectSkin(argv?: readonly string[]): 'wa' | 'rcs';
44
+ /**
45
+ * The package's own version, read at runtime rather than restated in a
46
+ * constant. A hardcoded copy silently goes stale the first time anyone bumps
47
+ * for a release, and both skins show this string to a user. From dist/ and from
48
+ * src/ alike the package root is one level up.
49
+ */
50
+ export declare function readVersion(): string;
51
+ /** Human-readable product + channel names for the running skin. */
52
+ export declare function skinLabels(skin?: 'wa' | 'rcs'): {
53
+ product: string;
54
+ channel: string;
55
+ slug: string;
56
+ };
57
+ /**
58
+ * Normalize a channel from configuration or a tool argument. Accepts the channel
59
+ * enum itself (any case) AND a brand id — canonical or legacy — resolving it to
60
+ * that brand's channel. Always returns the canonical channel enum; unknown
61
+ * values are ignored.
62
+ */
63
+ export declare function normalizeChannel(value: unknown): Channel | undefined;
64
+ export interface RequestSpec {
65
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
66
+ /** Path under the gateway root, e.g. "/templates" or "/marketplace/:id/clone". */
67
+ path: string;
68
+ /**
69
+ * Query parameters. Booleans serialise as the literal `true` / `false` the
70
+ * gateway's flag filters parse — never as a truthy/falsy coercion, which is
71
+ * how `?unread=false` would otherwise come out meaning "unread only".
72
+ */
73
+ query?: Record<string, string | number | boolean | undefined>;
74
+ body?: unknown;
75
+ }
76
+ /** A gateway error carrying the HTTP status + the server's error envelope. */
77
+ export declare class GatewayError extends Error {
78
+ status: number;
79
+ details?: unknown;
80
+ constructor(message: string, status: number, details?: unknown);
81
+ }
82
+ /**
83
+ * Resolve config from explicit values, falling back to the environment.
84
+ *
85
+ * WA_* and RCS_* are accepted interchangeably. When both are set the one
86
+ * matching the running skin wins, so a machine configured for both does the
87
+ * expected thing under either bin.
88
+ */
89
+ export declare function resolveConfig(partial?: Partial<GatewayConfig>): GatewayConfig;
90
+ export declare class GatewayClient {
91
+ private readonly config;
92
+ constructor(config: GatewayConfig);
93
+ static fromEnv(partial?: Partial<GatewayConfig>): GatewayClient;
94
+ /** Issue a request against the gateway and return `data` from the envelope. */
95
+ call<T = unknown>(spec: RequestSpec): Promise<T>;
96
+ }
97
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +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,CAO/E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,MAAM,CASpC;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"}