@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 +176 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +375 -0
- package/dist/client.d.ts +97 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +175 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +178 -0
- package/dist/openapi.d.ts +36 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +590 -0
- package/dist/tools.d.ts +93 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1333 -0
- package/package.json +69 -0
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 @@
|
|
|
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
|
+
});
|
package/dist/client.d.ts
ADDED
|
@@ -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"}
|