@wabery/cli 0.3.0 → 0.5.0
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 +93 -12
- package/dist/api-client.d.ts +7 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +24 -12
- package/dist/api-client.js.map +1 -1
- package/dist/cli.js +372 -27
- package/dist/cli.js.map +1 -1
- package/dist/config-example.d.ts +4 -0
- package/dist/config-example.d.ts.map +1 -1
- package/dist/config-example.js +6 -0
- package/dist/config-example.js.map +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/local-config.d.ts +86 -0
- package/dist/local-config.d.ts.map +1 -1
- package/dist/local-config.js +259 -15
- package/dist/local-config.js.map +1 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +43 -11
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,11 +8,15 @@ import { createInterface } from "node:readline";
|
|
|
8
8
|
import { WaberyApiClient, WaberyApiError } from "./api-client.js";
|
|
9
9
|
import { createExampleConfig } from "./config-example.js";
|
|
10
10
|
import { checkWaberyConnection } from "./diagnostics.js";
|
|
11
|
-
import { getConfigPath,
|
|
11
|
+
import { clearCredentials, findCredentialByProject, getConfigPath, readCredentials, removeCredential, repoConfigWritePath, resolveProjectId, upsertCredential, writeRepoProjectId, } from "./local-config.js";
|
|
12
12
|
import { runMcpServer } from "./mcp-server.js";
|
|
13
13
|
async function main() {
|
|
14
14
|
const parsed = parseArgs(process.argv.slice(2));
|
|
15
15
|
const [command, subcommand, ...rest] = parsed.positionals;
|
|
16
|
+
// Global output shaping for scripting: `--id-only` or `--field <dot.path>`
|
|
17
|
+
// print a single raw value instead of JSON (no jq needed).
|
|
18
|
+
outputIdOnly = booleanFlag(parsed.flags, "id-only") === true;
|
|
19
|
+
outputField = stringFlag(parsed.flags, "field");
|
|
16
20
|
if (!command ||
|
|
17
21
|
command === "help" ||
|
|
18
22
|
command === "--help" ||
|
|
@@ -29,6 +33,14 @@ async function main() {
|
|
|
29
33
|
await handleLogin(parsed.flags);
|
|
30
34
|
return;
|
|
31
35
|
}
|
|
36
|
+
if (command === "logout") {
|
|
37
|
+
await handleLogout(parsed.flags);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (command === "use") {
|
|
41
|
+
await handleUse(subcommand ?? rest[0]);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
32
44
|
if (command === "mcp-config") {
|
|
33
45
|
await handleMcpConfig(subcommand, parsed.flags);
|
|
34
46
|
return;
|
|
@@ -50,11 +62,20 @@ async function main() {
|
|
|
50
62
|
apiKey: stringFlag(parsed.flags, "api-key"),
|
|
51
63
|
baseUrl: stringFlag(parsed.flags, "base-url"),
|
|
52
64
|
apiVersion: stringFlag(parsed.flags, "api-version"),
|
|
65
|
+
projectId: stringFlag(parsed.flags, "project-id"),
|
|
53
66
|
});
|
|
54
67
|
if (command === "mcp") {
|
|
55
68
|
await runMcpServer(client);
|
|
56
69
|
return;
|
|
57
70
|
}
|
|
71
|
+
if (command === "whoami") {
|
|
72
|
+
await handleWhoami(client);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (command === "init") {
|
|
76
|
+
await handleInit(client, parsed.flags);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
58
79
|
if (command === "doctor" || command === "check") {
|
|
59
80
|
await printJson(await checkWaberyConnection(client));
|
|
60
81
|
return;
|
|
@@ -68,7 +89,7 @@ async function main() {
|
|
|
68
89
|
return;
|
|
69
90
|
}
|
|
70
91
|
if (command === "projects") {
|
|
71
|
-
await handleProjects(client, subcommand, parsed.flags);
|
|
92
|
+
await handleProjects(client, subcommand, rest, parsed.flags);
|
|
72
93
|
return;
|
|
73
94
|
}
|
|
74
95
|
if (command === "channels" && subcommand === "list") {
|
|
@@ -139,12 +160,13 @@ async function handleConfig(client, subcommand, rest, flags) {
|
|
|
139
160
|
throw new Error("Usage: wabery config schema|export|example|init|validate|diff|apply [wabery.config.json]");
|
|
140
161
|
}
|
|
141
162
|
async function handleLocalConfig(subcommand, rest, flags) {
|
|
163
|
+
const resolvedProjectId = stringFlag(flags, "project-id") ?? resolveProjectId({}).projectId;
|
|
142
164
|
if (subcommand === "example") {
|
|
143
|
-
await printJson(createExampleConfig(
|
|
165
|
+
await printJson(createExampleConfig(resolvedProjectId));
|
|
144
166
|
return;
|
|
145
167
|
}
|
|
146
168
|
const outputPath = rest[0] ?? stringFlag(flags, "file") ?? "wabery.config.json";
|
|
147
|
-
const config = createExampleConfig(
|
|
169
|
+
const config = createExampleConfig(resolvedProjectId);
|
|
148
170
|
await writeFile(outputPath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
149
171
|
flag: booleanFlag(flags, "force") ? "w" : "wx",
|
|
150
172
|
});
|
|
@@ -245,8 +267,98 @@ async function handleLogin(flags) {
|
|
|
245
267
|
if (!apiKey.startsWith("wab_live_")) {
|
|
246
268
|
throw new Error("That does not look like a Wabery CLI key (it should start with wab_live_). Run `wabery login` again.");
|
|
247
269
|
}
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
// Identify which project this key is scoped to via /v1/me, then store the
|
|
271
|
+
// credential keyed by project so multiple projects can coexist locally.
|
|
272
|
+
const client = new WaberyApiClient({ apiKey, baseUrl });
|
|
273
|
+
const identity = (await client.get("/me"));
|
|
274
|
+
const projectId = identity.project?.id;
|
|
275
|
+
if (!projectId) {
|
|
276
|
+
throw new Error("This key is not scoped to a project. Create a key from a project's settings in the Wabery dashboard.");
|
|
277
|
+
}
|
|
278
|
+
const configPath = upsertCredential({
|
|
279
|
+
projectId,
|
|
280
|
+
projectName: identity.project?.name,
|
|
281
|
+
organizationId: identity.organization?.id,
|
|
282
|
+
apiKey,
|
|
283
|
+
baseUrl,
|
|
284
|
+
});
|
|
285
|
+
let boundLine = "";
|
|
286
|
+
if (!resolveProjectId({}).projectId) {
|
|
287
|
+
// No repo binding yet — bind this directory to the project we logged into.
|
|
288
|
+
const repoPath = writeRepoProjectId(repoConfigWritePath(), projectId);
|
|
289
|
+
boundLine = `Bound this directory to project ${projectId} (${repoPath}).\n`;
|
|
290
|
+
}
|
|
291
|
+
await printText(`\nLogged in to project ${identity.project?.name ?? projectId} (${projectId}).\nCredentials saved to ${configPath}\n${boundLine}`);
|
|
292
|
+
}
|
|
293
|
+
/** Clear stored credentials: one project, or all of them with --all. */
|
|
294
|
+
async function handleLogout(flags) {
|
|
295
|
+
if (booleanFlag(flags, "all")) {
|
|
296
|
+
clearCredentials();
|
|
297
|
+
await printText("Logged out. Removed all stored Wabery credentials.\n");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const target = stringFlag(flags, "project") ?? stringFlag(flags, "project-id");
|
|
301
|
+
const credentials = readCredentials();
|
|
302
|
+
let projectId = target;
|
|
303
|
+
if (!projectId) {
|
|
304
|
+
// Default to the project resolved for the current directory.
|
|
305
|
+
projectId = resolveProjectId({}).projectId;
|
|
306
|
+
}
|
|
307
|
+
if (!projectId && credentials.length === 1) {
|
|
308
|
+
projectId = credentials[0].projectId;
|
|
309
|
+
}
|
|
310
|
+
if (!projectId) {
|
|
311
|
+
throw new Error("Specify which project to log out of: `wabery logout --project <id|name>`, or `wabery logout --all`.");
|
|
312
|
+
}
|
|
313
|
+
// Allow matching by name too.
|
|
314
|
+
const match = credentials.find((c) => c.projectId === projectId) ??
|
|
315
|
+
credentials.find((c) => c.projectName?.toLowerCase() === projectId?.toLowerCase());
|
|
316
|
+
if (!match) {
|
|
317
|
+
throw new Error(`No stored credential for "${projectId}".`);
|
|
318
|
+
}
|
|
319
|
+
removeCredential(match.projectId);
|
|
320
|
+
await printText(`Logged out of project ${match.projectName ?? match.projectId} (${match.projectId}).\n`);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Bind the current repo to one of the projects you've logged into, by writing
|
|
324
|
+
* a secret-free `.wabery.json`. Resolves a project id or (case-insensitive)
|
|
325
|
+
* name against the locally stored credentials.
|
|
326
|
+
*/
|
|
327
|
+
async function handleUse(query) {
|
|
328
|
+
if (!query) {
|
|
329
|
+
throw new Error("Usage: wabery use <project-id-or-name>");
|
|
330
|
+
}
|
|
331
|
+
const credentials = readCredentials();
|
|
332
|
+
if (credentials.length === 0) {
|
|
333
|
+
throw new Error("No projects are logged in yet. Run `wabery login` first.");
|
|
334
|
+
}
|
|
335
|
+
const match = credentials.find((c) => c.projectId === query) ??
|
|
336
|
+
credentials.find((c) => c.projectName?.toLowerCase() === query.toLowerCase());
|
|
337
|
+
if (!match) {
|
|
338
|
+
const available = credentials
|
|
339
|
+
.map((c) => ` - ${c.projectName ?? "(unnamed)"} (${c.projectId})`)
|
|
340
|
+
.join("\n");
|
|
341
|
+
throw new Error(`No logged-in project matches "${query}". Available:\n${available}\n\nRun \`wabery login\` to add another project.`);
|
|
342
|
+
}
|
|
343
|
+
const path = writeRepoProjectId(repoConfigWritePath(), match.projectId);
|
|
344
|
+
await printJson({
|
|
345
|
+
object: "repo_default",
|
|
346
|
+
path,
|
|
347
|
+
project_id: match.projectId,
|
|
348
|
+
project_name: match.projectName ?? null,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/** Show the active key (masked), org, and bound project + how it resolved. */
|
|
352
|
+
async function handleWhoami(client) {
|
|
353
|
+
const identity = (await client.get("/me"));
|
|
354
|
+
const resolved = resolveProjectId({});
|
|
355
|
+
await printJson({
|
|
356
|
+
...identity,
|
|
357
|
+
resolution: {
|
|
358
|
+
project_id: client.projectId ?? null,
|
|
359
|
+
source: resolved.source,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
250
362
|
}
|
|
251
363
|
/** Read a single line from the terminal, echoing what the user types. */
|
|
252
364
|
function promptLine(question) {
|
|
@@ -281,22 +393,163 @@ function openUrl(url) {
|
|
|
281
393
|
function normalizeAppBaseUrl(value) {
|
|
282
394
|
return value.replace(/\/api\/v1\/?$/, "").replace(/\/+$/, "");
|
|
283
395
|
}
|
|
284
|
-
async function handleProjects(client, subcommand, flags) {
|
|
396
|
+
async function handleProjects(client, subcommand, rest, flags) {
|
|
285
397
|
if (subcommand === "list") {
|
|
286
398
|
await printJson(await client.get("/projects"));
|
|
287
399
|
return;
|
|
288
400
|
}
|
|
289
401
|
if (subcommand === "create") {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
402
|
+
throw new Error("API keys are scoped to one project and cannot create projects. Create a project in the Wabery dashboard, then run `wabery login` for that project.");
|
|
403
|
+
}
|
|
404
|
+
if (subcommand === "ensure") {
|
|
405
|
+
// API keys are scoped to one project. Ensure configures that scoped
|
|
406
|
+
// project idempotently; it does not create sibling projects.
|
|
407
|
+
const name = requiredStringFlag(flags, "name");
|
|
408
|
+
await printJson(await ensureProject(client, name, flags));
|
|
296
409
|
return;
|
|
297
410
|
}
|
|
298
|
-
|
|
411
|
+
const projectId = rest[0] ?? stringFlag(flags, "project-id");
|
|
412
|
+
if (subcommand === "get") {
|
|
413
|
+
await printJson(await client.get(`/projects/${requiredValue(projectId, "project-id")}`));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (subcommand === "update") {
|
|
417
|
+
const body = {};
|
|
418
|
+
const name = stringFlag(flags, "name");
|
|
419
|
+
if (name !== undefined)
|
|
420
|
+
body.name = name;
|
|
421
|
+
const description = stringFlag(flags, "description");
|
|
422
|
+
if (description !== undefined)
|
|
423
|
+
body.description = description;
|
|
424
|
+
const webhookUrl = stringFlag(flags, "webhook-url");
|
|
425
|
+
if (webhookUrl !== undefined)
|
|
426
|
+
body.webhook_url = webhookUrl;
|
|
427
|
+
const signing = booleanFlag(flags, "webhook-signing-enabled");
|
|
428
|
+
if (signing !== undefined)
|
|
429
|
+
body.webhook_signing_enabled = signing;
|
|
430
|
+
const secret = stringFlag(flags, "webhook-secret");
|
|
431
|
+
if (secret !== undefined)
|
|
432
|
+
body.webhook_secret = secret;
|
|
433
|
+
await printJson(await client.patch(`/projects/${requiredValue(projectId, "project-id")}`, body));
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (subcommand === "rotate-secret") {
|
|
437
|
+
const secret = stringFlag(flags, "webhook-secret");
|
|
438
|
+
await printJson(await client.post(`/projects/${requiredValue(projectId, "project-id")}/rotate-webhook-secret`, secret ? { webhook_secret: secret } : undefined));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
throw new Error("Usage: wabery projects list|get|ensure|update|rotate-secret [<project_id>] [--name --description --webhook-url --webhook-signing-enabled --webhook-secret]");
|
|
442
|
+
}
|
|
443
|
+
/** Configure the project visible to this API key, then return it. Idempotent. */
|
|
444
|
+
async function ensureProject(client, name, flags) {
|
|
445
|
+
const list = (await client.get("/projects"));
|
|
446
|
+
const existing = list.data?.[0];
|
|
447
|
+
const id = existing?.project_id ?? existing?.id;
|
|
448
|
+
if (!existing || !id) {
|
|
449
|
+
throw new Error("No project is visible to this API key. Create a project in the Wabery dashboard, then run `wabery login` again.");
|
|
450
|
+
}
|
|
451
|
+
const webhookBody = {};
|
|
452
|
+
if (name !== undefined && existing.name !== name)
|
|
453
|
+
webhookBody.name = name;
|
|
454
|
+
const description = stringFlag(flags, "description");
|
|
455
|
+
if (description !== undefined)
|
|
456
|
+
webhookBody.description = description;
|
|
457
|
+
const webhookUrl = stringFlag(flags, "webhook-url");
|
|
458
|
+
if (webhookUrl !== undefined)
|
|
459
|
+
webhookBody.webhook_url = webhookUrl;
|
|
460
|
+
const signing = booleanFlag(flags, "webhook-signing-enabled");
|
|
461
|
+
if (signing !== undefined)
|
|
462
|
+
webhookBody.webhook_signing_enabled = signing;
|
|
463
|
+
const secret = stringFlag(flags, "webhook-secret");
|
|
464
|
+
if (secret !== undefined)
|
|
465
|
+
webhookBody.webhook_secret = secret;
|
|
466
|
+
if (Object.keys(webhookBody).length > 0) {
|
|
467
|
+
return client.patch(`/projects/${id}`, webhookBody);
|
|
468
|
+
}
|
|
469
|
+
return client.get(`/projects/${id}`);
|
|
299
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Scaffold a runnable Wabery setup in the current directory:
|
|
473
|
+
* ensure a project, then write wabery.config.json, a .env block, and a webhook
|
|
474
|
+
* handler stub. Non-destructive — existing files are left untouched unless --force.
|
|
475
|
+
*/
|
|
476
|
+
async function handleInit(client, flags) {
|
|
477
|
+
const name = stringFlag(flags, "name");
|
|
478
|
+
const force = booleanFlag(flags, "force") === true;
|
|
479
|
+
const ensured = (await ensureProject(client, name, flags));
|
|
480
|
+
const projectId = ensured.project_id ?? ensured.id;
|
|
481
|
+
if (!projectId)
|
|
482
|
+
throw new Error("Could not resolve a project id.");
|
|
483
|
+
// Single GET reveals the webhook secret + url for the env block.
|
|
484
|
+
const project = (await client.get(`/projects/${projectId}`));
|
|
485
|
+
const channelId = project.channel_id ?? project.enrollment_channel?.id ?? undefined;
|
|
486
|
+
const cred = findCredentialByProject(projectId);
|
|
487
|
+
const baseUrl = stringFlag(flags, "base-url") ??
|
|
488
|
+
process.env.WABERY_BASE_URL ??
|
|
489
|
+
cred?.baseUrl ??
|
|
490
|
+
"https://api.wabery.com/v1";
|
|
491
|
+
const apiKey = stringFlag(flags, "api-key") ??
|
|
492
|
+
process.env.WABERY_API_KEY ??
|
|
493
|
+
cred?.apiKey ??
|
|
494
|
+
"wab_live_...";
|
|
495
|
+
const written = [];
|
|
496
|
+
const skipped = [];
|
|
497
|
+
const writeFlag = force ? "w" : "wx";
|
|
498
|
+
const tryWrite = async (path, content) => {
|
|
499
|
+
try {
|
|
500
|
+
await writeFile(path, content, { flag: writeFlag });
|
|
501
|
+
written.push(path);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
if (error.code === "EEXIST") {
|
|
505
|
+
skipped.push(path);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const envLines = [
|
|
512
|
+
`WABERY_API_KEY=${apiKey}`,
|
|
513
|
+
`WABERY_BASE_URL=${baseUrl}`,
|
|
514
|
+
`WABERY_PROJECT_ID=${projectId}`,
|
|
515
|
+
`WABERY_CHANNEL_ID=${channelId ?? ""}`,
|
|
516
|
+
];
|
|
517
|
+
if (project.webhook_url)
|
|
518
|
+
envLines.push(`WABERY_WEBHOOK_URL=${project.webhook_url}`);
|
|
519
|
+
envLines.push(`WABERY_WEBHOOK_SECRET=${project.webhook_secret ?? ""}`);
|
|
520
|
+
await tryWrite(".env.wabery", `${envLines.join("\n")}\n`);
|
|
521
|
+
await tryWrite("wabery.config.json", `${JSON.stringify(createExampleConfig(projectId), null, 2)}\n`);
|
|
522
|
+
await tryWrite("wabery-webhook-handler.ts", WEBHOOK_HANDLER_STUB);
|
|
523
|
+
await printText(`Initialized Wabery for project ${projectId}.\n` +
|
|
524
|
+
(written.length ? `Wrote: ${written.join(", ")}\n` : "") +
|
|
525
|
+
(skipped.length
|
|
526
|
+
? `Skipped (already exist, use --force to overwrite): ${skipped.join(", ")}\n`
|
|
527
|
+
: "") +
|
|
528
|
+
"\nNext: load .env.wabery, edit the webhook URL, then `wabery config apply wabery.config.json`.\n");
|
|
529
|
+
}
|
|
530
|
+
const WEBHOOK_HANDLER_STUB = `// Verify and handle Wabery webhook deliveries.
|
|
531
|
+
// npm i @wabery/sdk
|
|
532
|
+
import { Wabery } from "@wabery/sdk";
|
|
533
|
+
|
|
534
|
+
const wabery = new Wabery(); // reads WABERY_API_KEY
|
|
535
|
+
const secret = process.env.WABERY_WEBHOOK_SECRET ?? "";
|
|
536
|
+
|
|
537
|
+
// Express-style handler; adapt to your framework. Pass the RAW request body.
|
|
538
|
+
export async function handleWaberyWebhook(rawBody: string, signature: string) {
|
|
539
|
+
\tconst event = wabery.webhooks.constructEvent(rawBody, signature, secret);
|
|
540
|
+
\tswitch (event.event) {
|
|
541
|
+
\t\tcase "message.received":
|
|
542
|
+
\t\t\t// event.payload.text, .from, .conversation_id ...
|
|
543
|
+
\t\t\tbreak;
|
|
544
|
+
\t\tcase "flow.completed":
|
|
545
|
+
\t\t\t// event.payload.submission ...
|
|
546
|
+
\t\t\tbreak;
|
|
547
|
+
\t\tdefault:
|
|
548
|
+
\t\t\tbreak;
|
|
549
|
+
\t}
|
|
550
|
+
\treturn event;
|
|
551
|
+
}
|
|
552
|
+
`;
|
|
300
553
|
async function handleContacts(client, subcommand, rest, flags) {
|
|
301
554
|
if (subcommand === "list") {
|
|
302
555
|
await printJson(await client.get("/contacts", {
|
|
@@ -313,7 +566,8 @@ async function handleContacts(client, subcommand, rest, flags) {
|
|
|
313
566
|
if (subcommand === "enroll") {
|
|
314
567
|
await printJson(await client.post("/contacts", {
|
|
315
568
|
phone: requiredStringFlag(flags, "phone"),
|
|
316
|
-
|
|
569
|
+
// Keys are project-scoped; default to the key's project.
|
|
570
|
+
project_id: stringFlag(flags, "project-id") ?? client.projectId,
|
|
317
571
|
name: stringFlag(flags, "name"),
|
|
318
572
|
reference_id: stringFlag(flags, "reference-id"),
|
|
319
573
|
metadata: optionalJsonFlag(flags, "metadata"),
|
|
@@ -321,13 +575,21 @@ async function handleContacts(client, subcommand, rest, flags) {
|
|
|
321
575
|
}));
|
|
322
576
|
return;
|
|
323
577
|
}
|
|
578
|
+
if (subcommand === "update") {
|
|
579
|
+
const contactId = rest[0] ?? stringFlag(flags, "contact-id");
|
|
580
|
+
await printJson(await client.patch(`/contacts/${requiredValue(contactId, "contact-id")}`, {
|
|
581
|
+
preferred_language: stringFlag(flags, "preferred-language"),
|
|
582
|
+
metadata: optionalJsonFlag(flags, "metadata"),
|
|
583
|
+
}));
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
324
586
|
if (subcommand === "unenroll" || subcommand === "delete") {
|
|
325
587
|
const contactId = rest[0] ?? stringFlag(flags, "contact-id");
|
|
326
588
|
const erase = booleanFlag(flags, "erase");
|
|
327
589
|
await printJson(await client.delete(`/contacts/${requiredValue(contactId, "contact-id")}${erase ? "?erase=true" : ""}`));
|
|
328
590
|
return;
|
|
329
591
|
}
|
|
330
|
-
throw new Error("Usage: wabery contacts list|get|enroll|unenroll --phone <phone> --project-id <id>");
|
|
592
|
+
throw new Error("Usage: wabery contacts list|get|enroll|update|unenroll --phone <phone> --preferred-language <lang> --metadata <json> --project-id <id>");
|
|
331
593
|
}
|
|
332
594
|
async function handleConversations(client, subcommand, rest, flags) {
|
|
333
595
|
if (subcommand === "list") {
|
|
@@ -420,7 +682,8 @@ async function handleFlows(client, subcommand, rest, flags) {
|
|
|
420
682
|
return;
|
|
421
683
|
}
|
|
422
684
|
if (subcommand === "send") {
|
|
423
|
-
|
|
685
|
+
const configKey = stringFlag(flags, "config-key");
|
|
686
|
+
const common = {
|
|
424
687
|
channel_id: requiredStringFlag(flags, "channel-id"),
|
|
425
688
|
contact_id: stringFlag(flags, "contact-id"),
|
|
426
689
|
conversation_id: stringFlag(flags, "conversation-id"),
|
|
@@ -431,11 +694,20 @@ async function handleFlows(client, subcommand, rest, flags) {
|
|
|
431
694
|
footer_text: stringFlag(flags, "footer-text"),
|
|
432
695
|
first_screen: stringFlag(flags, "first-screen"),
|
|
433
696
|
screen_data: optionalJsonFlag(flags, "screen-data"),
|
|
434
|
-
}
|
|
697
|
+
};
|
|
698
|
+
// `--config-key` resolves the locale variant by the contact's language;
|
|
699
|
+
// otherwise send a specific flow id.
|
|
700
|
+
await printJson(configKey
|
|
701
|
+
? await client.post("/flows/send", {
|
|
702
|
+
config_key: configKey,
|
|
703
|
+
locale: stringFlag(flags, "locale"),
|
|
704
|
+
...common,
|
|
705
|
+
})
|
|
706
|
+
: await client.post(`/flows/${requiredValue(flowId, "flow-id")}/send`, common));
|
|
435
707
|
return;
|
|
436
708
|
}
|
|
437
709
|
// `flows test` is handled before client creation (no API key needed).
|
|
438
|
-
throw new Error("Usage: wabery flows list|get|publish|send|test");
|
|
710
|
+
throw new Error("Usage: wabery flows list|get|publish|send|test (send: <flow-id> | --config-key <key> [--locale <lang>])");
|
|
439
711
|
}
|
|
440
712
|
/**
|
|
441
713
|
* Poll a flow until Meta reports PUBLISHED (sendable), or throw with the rejection
|
|
@@ -471,27 +743,37 @@ async function handleEnv(client, flags) {
|
|
|
471
743
|
const projects = (await client.get("/projects"));
|
|
472
744
|
projectId = projects.data?.[0]?.project_id ?? projects.data?.[0]?.id;
|
|
473
745
|
}
|
|
746
|
+
// Single GET reveals webhook_url + webhook_secret (the list never does).
|
|
747
|
+
let project;
|
|
748
|
+
if (projectId) {
|
|
749
|
+
project = (await client.get(`/projects/${projectId}`));
|
|
750
|
+
}
|
|
751
|
+
if (!channelId) {
|
|
752
|
+
channelId =
|
|
753
|
+
project?.channel_id ?? project?.enrollment_channel?.id ?? undefined;
|
|
754
|
+
}
|
|
474
755
|
if (!channelId) {
|
|
475
756
|
const channels = (await client.get("/channels"));
|
|
476
757
|
channelId = channels.data?.[0]?.channel_id ?? channels.data?.[0]?.id;
|
|
477
758
|
}
|
|
759
|
+
const cred = findCredentialByProject(client.projectId ?? projectId ?? "");
|
|
478
760
|
const apiKey = stringFlag(flags, "api-key") ??
|
|
479
761
|
process.env.WABERY_API_KEY ??
|
|
480
|
-
|
|
762
|
+
cred?.apiKey ??
|
|
481
763
|
"wab_live_...";
|
|
482
764
|
const baseUrl = stringFlag(flags, "base-url") ??
|
|
483
765
|
process.env.WABERY_BASE_URL ??
|
|
484
|
-
|
|
766
|
+
cred?.baseUrl ??
|
|
485
767
|
"https://api.wabery.com/v1";
|
|
486
768
|
const lines = [
|
|
487
769
|
`WABERY_API_KEY=${apiKey}`,
|
|
488
770
|
`WABERY_BASE_URL=${baseUrl}`,
|
|
489
771
|
`WABERY_PROJECT_ID=${projectId ?? "<no project found>"}`,
|
|
490
772
|
`WABERY_CHANNEL_ID=${channelId ?? "<no channel found>"}`,
|
|
491
|
-
"# WABERY_WEBHOOK_SECRET is not retrievable via API — copy it from the",
|
|
492
|
-
"# dashboard (Project → Webhooks) and paste it here:",
|
|
493
|
-
"WABERY_WEBHOOK_SECRET=",
|
|
494
773
|
];
|
|
774
|
+
if (project?.webhook_url)
|
|
775
|
+
lines.push(`WABERY_WEBHOOK_URL=${project.webhook_url}`);
|
|
776
|
+
lines.push(`WABERY_WEBHOOK_SECRET=${project?.webhook_secret ?? ""}`);
|
|
495
777
|
await printText(`${lines.join("\n")}\n`);
|
|
496
778
|
}
|
|
497
779
|
/** Sign a body the same way Wabery does: `sha256=<hex>` HMAC-SHA256. */
|
|
@@ -737,7 +1019,48 @@ function requiredValue(value, name) {
|
|
|
737
1019
|
throw new Error(`Missing required --${name}`);
|
|
738
1020
|
return value;
|
|
739
1021
|
}
|
|
1022
|
+
// Global output shaping, set from --id-only / --field at startup.
|
|
1023
|
+
let outputIdOnly = false;
|
|
1024
|
+
let outputField;
|
|
1025
|
+
const ID_FIELDS = [
|
|
1026
|
+
"id",
|
|
1027
|
+
"project_id",
|
|
1028
|
+
"contact_id",
|
|
1029
|
+
"flow_id",
|
|
1030
|
+
"channel_id",
|
|
1031
|
+
"flow_token",
|
|
1032
|
+
"dispatch_id",
|
|
1033
|
+
];
|
|
1034
|
+
function extractPath(data, path) {
|
|
1035
|
+
return path.split(".").reduce((node, key) => {
|
|
1036
|
+
if (node && typeof node === "object") {
|
|
1037
|
+
return node[key];
|
|
1038
|
+
}
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}, data);
|
|
1041
|
+
}
|
|
1042
|
+
function formatScalar(value) {
|
|
1043
|
+
if (value === undefined || value === null)
|
|
1044
|
+
return "";
|
|
1045
|
+
if (typeof value === "object")
|
|
1046
|
+
return JSON.stringify(value);
|
|
1047
|
+
return String(value);
|
|
1048
|
+
}
|
|
740
1049
|
async function printJson(data) {
|
|
1050
|
+
if (outputIdOnly) {
|
|
1051
|
+
const record = data && typeof data === "object"
|
|
1052
|
+
? data
|
|
1053
|
+
: undefined;
|
|
1054
|
+
const id = record
|
|
1055
|
+
? ID_FIELDS.map((key) => record[key]).find((v) => typeof v === "string")
|
|
1056
|
+
: undefined;
|
|
1057
|
+
process.stdout.write(`${formatScalar(id)}\n`);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (outputField) {
|
|
1061
|
+
process.stdout.write(`${formatScalar(extractPath(data, outputField))}\n`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
741
1064
|
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
742
1065
|
}
|
|
743
1066
|
async function printText(text) {
|
|
@@ -760,19 +1083,38 @@ function redactSensitive(value) {
|
|
|
760
1083
|
function printHelp() {
|
|
761
1084
|
process.stdout.write(`Wabery CLI and MCP server
|
|
762
1085
|
|
|
1086
|
+
Projects & keys:
|
|
1087
|
+
API keys are scoped to a single project. Log into each project once; the CLI
|
|
1088
|
+
stores one key per project and auto-selects the right one based on the repo.
|
|
1089
|
+
A repo declares its project in a committed, secret-free .wabery.json
|
|
1090
|
+
({"project_id": "..."}); the CLI walks up from the current directory to find
|
|
1091
|
+
it (falling back to wabery.config.json's project_id).
|
|
1092
|
+
|
|
763
1093
|
Environment:
|
|
764
|
-
WABERY_API_KEY API key override
|
|
1094
|
+
WABERY_API_KEY API key override (used directly, bypasses the store).
|
|
1095
|
+
WABERY_PROJECT_ID Select which stored project's key to use.
|
|
765
1096
|
WABERY_BASE_URL Defaults to https://api.wabery.com/v1.
|
|
766
1097
|
WABERY_API_VERSION Optional dated API version.
|
|
767
1098
|
|
|
768
1099
|
Global flags:
|
|
769
1100
|
--api-key <key> Override WABERY_API_KEY.
|
|
1101
|
+
--project-id <id> Select which stored project's key to use.
|
|
770
1102
|
--base-url <url> Override WABERY_BASE_URL.
|
|
771
1103
|
--api-version <ver> Override WABERY_API_VERSION.
|
|
1104
|
+
--id-only Print just the result id (for shell: ID=$(wabery ... --id-only)).
|
|
1105
|
+
--field <dot.path> Print just one field, e.g. --field webhook_secret.
|
|
1106
|
+
|
|
1107
|
+
Getting started:
|
|
1108
|
+
wabery login # log into a project (stores its key)
|
|
1109
|
+
wabery whoami # active key (masked), org, project
|
|
1110
|
+
wabery init [--name "My project"] # scaffold .env.wabery + wabery.config.json + handler
|
|
772
1111
|
|
|
773
1112
|
Authentication:
|
|
774
|
-
wabery login
|
|
1113
|
+
wabery login # add/refresh the current project's key
|
|
775
1114
|
wabery login --no-open
|
|
1115
|
+
wabery use <project-id-or-name> # bind this repo to a logged-in project
|
|
1116
|
+
wabery logout [--project <id|name>] # remove one stored key
|
|
1117
|
+
wabery logout --all # remove every stored key
|
|
776
1118
|
Config file: ${getConfigPath()}
|
|
777
1119
|
|
|
778
1120
|
MCP:
|
|
@@ -797,7 +1139,10 @@ Config:
|
|
|
797
1139
|
|
|
798
1140
|
Projects and contacts:
|
|
799
1141
|
wabery projects list
|
|
800
|
-
wabery projects
|
|
1142
|
+
wabery projects get <project_id> # reveals webhook_secret
|
|
1143
|
+
wabery projects ensure --name "Demo" # idempotently configure the key's scoped project
|
|
1144
|
+
wabery projects update <project_id> [--name <n>] [--webhook-url <url>] [--webhook-signing-enabled true|false] [--webhook-secret <secret>]
|
|
1145
|
+
wabery projects rotate-secret <project_id> [--webhook-secret <secret>]
|
|
801
1146
|
wabery contacts enroll --project-id <id> --phone +15555550123 --name "Jane" --opt-in '{"source":"checkout"}'
|
|
802
1147
|
wabery contacts get <contact_id>
|
|
803
1148
|
wabery contacts unenroll <contact_id>
|