@bland-ai/cli 0.1.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.
Files changed (126) hide show
  1. package/dist/commands/agent.d.ts +3 -0
  2. package/dist/commands/agent.d.ts.map +1 -0
  3. package/dist/commands/agent.js +143 -0
  4. package/dist/commands/agent.js.map +1 -0
  5. package/dist/commands/alarm.d.ts +3 -0
  6. package/dist/commands/alarm.d.ts.map +1 -0
  7. package/dist/commands/alarm.js +92 -0
  8. package/dist/commands/alarm.js.map +1 -0
  9. package/dist/commands/audio.d.ts +3 -0
  10. package/dist/commands/audio.d.ts.map +1 -0
  11. package/dist/commands/audio.js +77 -0
  12. package/dist/commands/audio.js.map +1 -0
  13. package/dist/commands/auth.d.ts +3 -0
  14. package/dist/commands/auth.d.ts.map +1 -0
  15. package/dist/commands/auth.js +199 -0
  16. package/dist/commands/auth.js.map +1 -0
  17. package/dist/commands/batch.d.ts +3 -0
  18. package/dist/commands/batch.d.ts.map +1 -0
  19. package/dist/commands/batch.js +108 -0
  20. package/dist/commands/batch.js.map +1 -0
  21. package/dist/commands/call.d.ts +3 -0
  22. package/dist/commands/call.d.ts.map +1 -0
  23. package/dist/commands/call.js +348 -0
  24. package/dist/commands/call.js.map +1 -0
  25. package/dist/commands/eval.d.ts +3 -0
  26. package/dist/commands/eval.d.ts.map +1 -0
  27. package/dist/commands/eval.js +66 -0
  28. package/dist/commands/eval.js.map +1 -0
  29. package/dist/commands/guard.d.ts +3 -0
  30. package/dist/commands/guard.d.ts.map +1 -0
  31. package/dist/commands/guard.js +100 -0
  32. package/dist/commands/guard.js.map +1 -0
  33. package/dist/commands/knowledge.d.ts +3 -0
  34. package/dist/commands/knowledge.d.ts.map +1 -0
  35. package/dist/commands/knowledge.js +136 -0
  36. package/dist/commands/knowledge.js.map +1 -0
  37. package/dist/commands/listen.d.ts +3 -0
  38. package/dist/commands/listen.d.ts.map +1 -0
  39. package/dist/commands/listen.js +98 -0
  40. package/dist/commands/listen.js.map +1 -0
  41. package/dist/commands/mcp.d.ts +3 -0
  42. package/dist/commands/mcp.d.ts.map +1 -0
  43. package/dist/commands/mcp.js +22 -0
  44. package/dist/commands/mcp.js.map +1 -0
  45. package/dist/commands/number.d.ts +3 -0
  46. package/dist/commands/number.d.ts.map +1 -0
  47. package/dist/commands/number.js +225 -0
  48. package/dist/commands/number.js.map +1 -0
  49. package/dist/commands/pathway.d.ts +3 -0
  50. package/dist/commands/pathway.d.ts.map +1 -0
  51. package/dist/commands/pathway.js +977 -0
  52. package/dist/commands/pathway.js.map +1 -0
  53. package/dist/commands/persona.d.ts +3 -0
  54. package/dist/commands/persona.d.ts.map +1 -0
  55. package/dist/commands/persona.js +234 -0
  56. package/dist/commands/persona.js.map +1 -0
  57. package/dist/commands/release.d.ts +3 -0
  58. package/dist/commands/release.d.ts.map +1 -0
  59. package/dist/commands/release.js +67 -0
  60. package/dist/commands/release.js.map +1 -0
  61. package/dist/commands/secret.d.ts +3 -0
  62. package/dist/commands/secret.d.ts.map +1 -0
  63. package/dist/commands/secret.js +57 -0
  64. package/dist/commands/secret.js.map +1 -0
  65. package/dist/commands/sip.d.ts +3 -0
  66. package/dist/commands/sip.d.ts.map +1 -0
  67. package/dist/commands/sip.js +45 -0
  68. package/dist/commands/sip.js.map +1 -0
  69. package/dist/commands/sms.d.ts +3 -0
  70. package/dist/commands/sms.d.ts.map +1 -0
  71. package/dist/commands/sms.js +83 -0
  72. package/dist/commands/sms.js.map +1 -0
  73. package/dist/commands/tool.d.ts +3 -0
  74. package/dist/commands/tool.d.ts.map +1 -0
  75. package/dist/commands/tool.js +200 -0
  76. package/dist/commands/tool.js.map +1 -0
  77. package/dist/commands/voice.d.ts +3 -0
  78. package/dist/commands/voice.d.ts.map +1 -0
  79. package/dist/commands/voice.js +95 -0
  80. package/dist/commands/voice.js.map +1 -0
  81. package/dist/commands/widget.d.ts +3 -0
  82. package/dist/commands/widget.d.ts.map +1 -0
  83. package/dist/commands/widget.js +77 -0
  84. package/dist/commands/widget.js.map +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.d.ts.map +1 -0
  87. package/dist/index.js +52 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/lib/api.d.ts +17 -0
  90. package/dist/lib/api.d.ts.map +1 -0
  91. package/dist/lib/api.js +89 -0
  92. package/dist/lib/api.js.map +1 -0
  93. package/dist/lib/config.d.ts +16 -0
  94. package/dist/lib/config.d.ts.map +1 -0
  95. package/dist/lib/config.js +117 -0
  96. package/dist/lib/config.js.map +1 -0
  97. package/dist/lib/errors.d.ts +15 -0
  98. package/dist/lib/errors.d.ts.map +1 -0
  99. package/dist/lib/errors.js +43 -0
  100. package/dist/lib/errors.js.map +1 -0
  101. package/dist/lib/output.d.ts +30 -0
  102. package/dist/lib/output.d.ts.map +1 -0
  103. package/dist/lib/output.js +131 -0
  104. package/dist/lib/output.js.map +1 -0
  105. package/dist/lib/pathway-file.d.ts +31 -0
  106. package/dist/lib/pathway-file.d.ts.map +1 -0
  107. package/dist/lib/pathway-file.js +236 -0
  108. package/dist/lib/pathway-file.js.map +1 -0
  109. package/dist/mcp/server.d.ts +2 -0
  110. package/dist/mcp/server.d.ts.map +1 -0
  111. package/dist/mcp/server.js +375 -0
  112. package/dist/mcp/server.js.map +1 -0
  113. package/dist/types/api.d.ts +302 -0
  114. package/dist/types/api.d.ts.map +1 -0
  115. package/dist/types/api.js +2 -0
  116. package/dist/types/api.js.map +1 -0
  117. package/dist/types/config.d.ts +14 -0
  118. package/dist/types/config.d.ts.map +1 -0
  119. package/dist/types/config.js +2 -0
  120. package/dist/types/config.js.map +1 -0
  121. package/dist/types/pathway.d.ts +55 -0
  122. package/dist/types/pathway.d.ts.map +1 -0
  123. package/dist/types/pathway.js +2 -0
  124. package/dist/types/pathway.js.map +1 -0
  125. package/package.json +51 -0
  126. package/templates/pathway.yaml +30 -0
@@ -0,0 +1,977 @@
1
+ import { input, select, confirm, editor } from "@inquirer/prompts";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as yaml from "yaml";
7
+ import { api } from "../lib/api.js";
8
+ import * as output from "../lib/output.js";
9
+ import { handleError } from "../lib/errors.js";
10
+ import { readPathwayFile, writePathwayFile, findPathwayFile, apiToFile, fileToApi, createStarterPathway, createStarterTestCases, } from "../lib/pathway-file.js";
11
+ import { writeProjectConfig, readProjectConfig } from "../lib/config.js";
12
+ import * as readline from "readline";
13
+ export function registerPathwayCommand(program) {
14
+ const pathway = program
15
+ .command("pathway")
16
+ .description("Manage and develop conversational pathways");
17
+ // ──────────────────────────────────────
18
+ // CRUD
19
+ // ──────────────────────────────────────
20
+ // ── bland pathway list ──
21
+ pathway
22
+ .command("list")
23
+ .description("List all pathways")
24
+ .option("--json", "Output as JSON")
25
+ .action(async (opts) => {
26
+ try {
27
+ const spinner = ora("Fetching pathways...").start();
28
+ const pathways = await api.get("/v1/convo_pathway");
29
+ spinner.stop();
30
+ if (opts.json) {
31
+ output.json(pathways);
32
+ return;
33
+ }
34
+ if (!pathways || pathways.length === 0) {
35
+ console.log(chalk.dim(" No pathways found."));
36
+ return;
37
+ }
38
+ output.table(pathways.map((p) => ({
39
+ ...p,
40
+ published: p.published_at ? chalk.green("live") : chalk.dim("draft"),
41
+ version: p.production_version_number ?? chalk.dim("—"),
42
+ })), [
43
+ { key: "id", header: "ID", width: 38 },
44
+ { key: "name", header: "Name", width: 34 },
45
+ { key: "published", header: "Status", width: 8 },
46
+ { key: "version", header: "Version", width: 9 },
47
+ ]);
48
+ }
49
+ catch (err) {
50
+ handleError(err);
51
+ }
52
+ });
53
+ // ── bland pathway get ──
54
+ pathway
55
+ .command("get")
56
+ .description("Get pathway details")
57
+ .argument("<id>", "Pathway ID")
58
+ .option("--json", "Output as JSON")
59
+ .action(async (id, opts) => {
60
+ try {
61
+ const spinner = ora("Fetching pathway...").start();
62
+ const pw = await api.get(`/v1/convo_pathway/${id}`);
63
+ spinner.stop();
64
+ if (opts.json) {
65
+ output.json(pw);
66
+ return;
67
+ }
68
+ console.log();
69
+ output.detail([
70
+ ["ID", pw.id],
71
+ ["Name", pw.name],
72
+ ["Description", pw.description || "—"],
73
+ ["Nodes", pw.nodes?.length || 0],
74
+ ["Edges", pw.edges?.length || 0],
75
+ ["Created", output.formatDate(pw.created_at)],
76
+ ["Updated", output.formatDate(pw.updated_at)],
77
+ ]);
78
+ if (pw.nodes && pw.nodes.length > 0) {
79
+ output.header("Nodes");
80
+ output.table(pw.nodes.map((n) => ({
81
+ id: n.id,
82
+ name: n.data.name || n.id,
83
+ type: n.data.type || "default",
84
+ prompt: n.data.prompt
85
+ ? output.truncate(n.data.prompt, 50)
86
+ : chalk.dim("—"),
87
+ global: n.data.isGlobal ? chalk.cyan("global") : "",
88
+ })), [
89
+ { key: "name", header: "Name", width: 25 },
90
+ { key: "type", header: "Type", width: 15 },
91
+ { key: "prompt", header: "Prompt", width: 52 },
92
+ { key: "global", header: "", width: 8 },
93
+ ]);
94
+ }
95
+ }
96
+ catch (err) {
97
+ handleError(err);
98
+ }
99
+ });
100
+ // ── bland pathway create ──
101
+ pathway
102
+ .command("create")
103
+ .description("Create a new pathway")
104
+ .argument("[name]", "Pathway name")
105
+ .option("--from-file <path>", "Create from local YAML/JSON file")
106
+ .option("--json", "Output as JSON")
107
+ .action(async (nameArg, opts) => {
108
+ try {
109
+ let name = nameArg;
110
+ if (opts.fromFile) {
111
+ const file = readPathwayFile(opts.fromFile);
112
+ name = name || file.name;
113
+ const apiData = fileToApi(file);
114
+ apiData.name = name;
115
+ const spinner = ora("Creating pathway...").start();
116
+ const result = await api.post("/v1/convo_pathway", apiData);
117
+ spinner.succeed(`Pathway "${name}" created ${chalk.dim(`(${result.id})`)}`);
118
+ if (opts.json)
119
+ output.json(result);
120
+ return;
121
+ }
122
+ if (!name) {
123
+ name = await input({
124
+ message: "Pathway name:",
125
+ validate: (v) => v.trim().length > 0 || "Name is required",
126
+ });
127
+ }
128
+ const spinner = ora("Creating pathway...").start();
129
+ const result = await api.post("/v1/convo_pathway", {
130
+ name,
131
+ nodes: [],
132
+ edges: [],
133
+ });
134
+ spinner.succeed(`Pathway "${name}" created ${chalk.dim(`(${result.id})`)}`);
135
+ if (opts.json)
136
+ output.json(result);
137
+ }
138
+ catch (err) {
139
+ handleError(err);
140
+ }
141
+ });
142
+ // ── bland pathway delete ──
143
+ pathway
144
+ .command("delete")
145
+ .description("Delete a pathway")
146
+ .argument("<id>", "Pathway ID")
147
+ .action(async (id) => {
148
+ try {
149
+ const proceed = await confirm({
150
+ message: `Delete pathway ${id}? This cannot be undone.`,
151
+ });
152
+ if (!proceed)
153
+ return;
154
+ const spinner = ora("Deleting pathway...").start();
155
+ await api.delete(`/v1/convo_pathway/${id}`);
156
+ spinner.succeed("Pathway deleted.");
157
+ }
158
+ catch (err) {
159
+ handleError(err);
160
+ }
161
+ });
162
+ // ── bland pathway duplicate ──
163
+ pathway
164
+ .command("duplicate")
165
+ .description("Duplicate a pathway")
166
+ .argument("<id>", "Pathway ID to duplicate")
167
+ .option("--name <name>", "Name for the copy")
168
+ .option("--json", "Output as JSON")
169
+ .action(async (id, opts) => {
170
+ try {
171
+ const spinner = ora("Duplicating pathway...").start();
172
+ const body = { pathway_id: id };
173
+ if (opts.name)
174
+ body.name = opts.name;
175
+ const result = await api.post("/v1/convo_pathway/duplicate", body);
176
+ spinner.succeed(`Duplicated as "${result.name}" ${chalk.dim(`(${result.id})`)}`);
177
+ if (opts.json)
178
+ output.json(result);
179
+ }
180
+ catch (err) {
181
+ handleError(err);
182
+ }
183
+ });
184
+ // ──────────────────────────────────────
185
+ // LOCAL FILE WORKFLOW
186
+ // ──────────────────────────────────────
187
+ // ── bland pathway init ──
188
+ pathway
189
+ .command("init")
190
+ .description("Initialize a local pathway project")
191
+ .argument("[dir]", "Directory to initialize", ".")
192
+ .option("--name <name>", "Pathway name")
193
+ .action(async (dir, opts) => {
194
+ try {
195
+ const targetDir = path.resolve(dir);
196
+ let name = opts.name;
197
+ if (!name) {
198
+ name = await input({
199
+ message: "Pathway name:",
200
+ default: path.basename(targetDir),
201
+ validate: (v) => v.trim().length > 0 || "Name is required",
202
+ });
203
+ }
204
+ // Create directory structure
205
+ if (!fs.existsSync(targetDir)) {
206
+ fs.mkdirSync(targetDir, { recursive: true });
207
+ }
208
+ const testsDir = path.join(targetDir, "tests");
209
+ if (!fs.existsSync(testsDir)) {
210
+ fs.mkdirSync(testsDir, { recursive: true });
211
+ }
212
+ // Create pathway file
213
+ const pathwayFilePath = path.join(targetDir, "bland-pathway.yaml");
214
+ if (!fs.existsSync(pathwayFilePath)) {
215
+ const starterPathway = createStarterPathway(name);
216
+ writePathwayFile(pathwayFilePath, starterPathway);
217
+ }
218
+ // Create test cases file
219
+ const testFilePath = path.join(testsDir, "test-cases.yaml");
220
+ if (!fs.existsSync(testFilePath)) {
221
+ const starterTests = createStarterTestCases(name);
222
+ fs.writeFileSync(testFilePath, yaml.stringify(starterTests), "utf-8");
223
+ }
224
+ // Create project config
225
+ writeProjectConfig({
226
+ pathway_file: "bland-pathway.yaml",
227
+ test_file: "tests/test-cases.yaml",
228
+ }, targetDir);
229
+ output.success(`Initialized pathway project in ${targetDir}`);
230
+ console.log();
231
+ console.log(` ${chalk.dim("├──")} bland-pathway.yaml`);
232
+ console.log(` ${chalk.dim("├──")} tests/`);
233
+ console.log(` ${chalk.dim("│ └──")} test-cases.yaml`);
234
+ console.log(` ${chalk.dim("└──")} .blandrc`);
235
+ console.log();
236
+ console.log(chalk.dim(" Edit bland-pathway.yaml, then run `bland pathway push` to upload."));
237
+ }
238
+ catch (err) {
239
+ handleError(err);
240
+ }
241
+ });
242
+ // ── bland pathway pull ──
243
+ pathway
244
+ .command("pull")
245
+ .description("Download a pathway as local YAML files")
246
+ .argument("<id>", "Pathway ID")
247
+ .argument("[dir]", "Target directory", ".")
248
+ .option("--format <fmt>", "File format: yaml or json", "yaml")
249
+ .action(async (id, dir, opts) => {
250
+ try {
251
+ const spinner = ora("Fetching pathway...").start();
252
+ const pw = await api.get(`/v1/convo_pathway/${id}`);
253
+ spinner.stop();
254
+ const targetDir = path.resolve(dir);
255
+ if (!fs.existsSync(targetDir)) {
256
+ fs.mkdirSync(targetDir, { recursive: true });
257
+ }
258
+ const file = apiToFile(pw);
259
+ const ext = opts.format === "json" ? ".json" : ".yaml";
260
+ const filePath = path.join(targetDir, `bland-pathway${ext}`);
261
+ writePathwayFile(filePath, file);
262
+ // Save project config
263
+ writeProjectConfig({
264
+ pathway_id: id,
265
+ pathway_file: `bland-pathway${ext}`,
266
+ }, targetDir);
267
+ output.success(`Pulled "${pw.name}" to ${filePath}`);
268
+ console.log(chalk.dim(` ${pw.nodes?.length || 0} nodes, ${pw.edges?.length || 0} edges`));
269
+ }
270
+ catch (err) {
271
+ handleError(err);
272
+ }
273
+ });
274
+ // ── bland pathway push ──
275
+ pathway
276
+ .command("push")
277
+ .description("Upload local pathway files to Bland")
278
+ .argument("[dir]", "Directory with pathway files", ".")
279
+ .option("--create", "Create new pathway if no ID is linked")
280
+ .option("--json", "Output as JSON")
281
+ .action(async (dir, opts) => {
282
+ try {
283
+ const targetDir = path.resolve(dir);
284
+ const projectConfig = readProjectConfig(targetDir);
285
+ const pathwayFilePath = findPathwayFile(targetDir) ||
286
+ (projectConfig?.pathway_file
287
+ ? path.join(targetDir, projectConfig.pathway_file)
288
+ : null);
289
+ if (!pathwayFilePath) {
290
+ console.error(chalk.red("No pathway file found. Run `bland pathway init` first."));
291
+ process.exit(1);
292
+ }
293
+ const file = readPathwayFile(pathwayFilePath);
294
+ const apiData = fileToApi(file);
295
+ const spinner = ora("Pushing pathway...").start();
296
+ let result;
297
+ if (projectConfig?.pathway_id) {
298
+ // Update existing
299
+ result = await api.post(`/v1/convo_pathway/${projectConfig.pathway_id}`, apiData);
300
+ spinner.succeed(`Updated "${file.name}" ${chalk.dim(`(${projectConfig.pathway_id})`)}`);
301
+ }
302
+ else if (opts.create) {
303
+ // Create new
304
+ result = await api.post("/v1/convo_pathway", apiData);
305
+ writeProjectConfig({ ...projectConfig, pathway_id: result.id }, targetDir);
306
+ spinner.succeed(`Created "${file.name}" ${chalk.dim(`(${result.id})`)}`);
307
+ }
308
+ else {
309
+ spinner.fail("No pathway ID linked.");
310
+ console.log(chalk.dim(" Use --create to create a new pathway, or `bland pathway pull <id>` to link an existing one."));
311
+ process.exit(1);
312
+ }
313
+ if (opts.json)
314
+ output.json(result);
315
+ console.log(chalk.dim(` ${apiData.nodes.length} nodes, ${apiData.edges.length} edges`));
316
+ }
317
+ catch (err) {
318
+ handleError(err);
319
+ }
320
+ });
321
+ // ── bland pathway diff ──
322
+ pathway
323
+ .command("diff")
324
+ .description("Show diff between local and remote pathway")
325
+ .argument("[dir]", "Directory with pathway files", ".")
326
+ .action(async (dir) => {
327
+ try {
328
+ const targetDir = path.resolve(dir);
329
+ const projectConfig = readProjectConfig(targetDir);
330
+ if (!projectConfig?.pathway_id) {
331
+ console.error(chalk.red("No pathway ID linked. Run `bland pathway pull <id>` first."));
332
+ process.exit(1);
333
+ }
334
+ const pathwayFilePath = findPathwayFile(targetDir);
335
+ if (!pathwayFilePath) {
336
+ console.error(chalk.red("No pathway file found."));
337
+ process.exit(1);
338
+ }
339
+ const spinner = ora("Comparing...").start();
340
+ const localFile = readPathwayFile(pathwayFilePath);
341
+ const remotePw = await api.get(`/v1/convo_pathway/${projectConfig.pathway_id}`);
342
+ const remoteFile = apiToFile(remotePw);
343
+ spinner.stop();
344
+ const localYaml = yaml.stringify(localFile);
345
+ const remoteYaml = yaml.stringify(remoteFile);
346
+ if (localYaml === remoteYaml) {
347
+ output.success("Local and remote are in sync.");
348
+ return;
349
+ }
350
+ console.log();
351
+ console.log(chalk.bold("Differences found:"));
352
+ console.log();
353
+ // Compare node names
354
+ const localNodes = new Set(Object.keys(localFile.nodes));
355
+ const remoteNodes = new Set(Object.keys(remoteFile.nodes));
356
+ for (const node of localNodes) {
357
+ if (!remoteNodes.has(node)) {
358
+ console.log(chalk.green(` + Node "${node}" (local only)`));
359
+ }
360
+ }
361
+ for (const node of remoteNodes) {
362
+ if (!localNodes.has(node)) {
363
+ console.log(chalk.red(` - Node "${node}" (remote only)`));
364
+ }
365
+ }
366
+ // Compare shared nodes
367
+ for (const node of localNodes) {
368
+ if (remoteNodes.has(node)) {
369
+ const localNode = localFile.nodes[node];
370
+ const remoteNode = remoteFile.nodes[node];
371
+ if (yaml.stringify(localNode) !== yaml.stringify(remoteNode)) {
372
+ console.log(chalk.yellow(` ~ Node "${node}" differs`));
373
+ // Show prompt diff if different
374
+ if (localNode.prompt !== remoteNode.prompt) {
375
+ console.log(chalk.dim(` prompt changed`));
376
+ }
377
+ if (JSON.stringify(localNode.edges) !==
378
+ JSON.stringify(remoteNode.edges)) {
379
+ console.log(chalk.dim(` edges changed`));
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ catch (err) {
386
+ handleError(err);
387
+ }
388
+ });
389
+ // ── bland pathway validate ──
390
+ pathway
391
+ .command("validate")
392
+ .description("Validate local pathway files")
393
+ .argument("[dir]", "Directory with pathway files", ".")
394
+ .action(async (dir) => {
395
+ try {
396
+ const targetDir = path.resolve(dir);
397
+ const pathwayFilePath = findPathwayFile(targetDir);
398
+ if (!pathwayFilePath) {
399
+ console.error(chalk.red("No pathway file found."));
400
+ process.exit(1);
401
+ }
402
+ const file = readPathwayFile(pathwayFilePath);
403
+ const nodeCount = Object.keys(file.nodes).length;
404
+ // Count total edges
405
+ let edgeCount = 0;
406
+ let toolCount = 0;
407
+ for (const node of Object.values(file.nodes)) {
408
+ edgeCount += node.edges?.length || 0;
409
+ toolCount += node.tools?.length || 0;
410
+ }
411
+ output.success(`${nodeCount} nodes, ${edgeCount} edges, ${toolCount} tools — all valid`);
412
+ }
413
+ catch (err) {
414
+ // readPathwayFile throws BlandError on validation failures
415
+ handleError(err);
416
+ }
417
+ });
418
+ // ── bland pathway watch ──
419
+ pathway
420
+ .command("watch")
421
+ .description("Watch for changes and auto-push")
422
+ .argument("[dir]", "Directory to watch", ".")
423
+ .action(async (dir) => {
424
+ try {
425
+ const targetDir = path.resolve(dir);
426
+ const projectConfig = readProjectConfig(targetDir);
427
+ if (!projectConfig?.pathway_id) {
428
+ console.error(chalk.red("No pathway ID linked. Run `bland pathway pull <id>` first."));
429
+ process.exit(1);
430
+ }
431
+ const pathwayFilePath = findPathwayFile(targetDir);
432
+ if (!pathwayFilePath) {
433
+ console.error(chalk.red("No pathway file found."));
434
+ process.exit(1);
435
+ }
436
+ console.log(`Watching ${chalk.bold(pathwayFilePath)} for changes...`);
437
+ console.log(chalk.dim("Press Ctrl+C to stop."));
438
+ console.log();
439
+ // Use fs.watch for simplicity (avoids chokidar dependency at runtime for now)
440
+ let debounceTimer = null;
441
+ fs.watch(pathwayFilePath, () => {
442
+ if (debounceTimer)
443
+ clearTimeout(debounceTimer);
444
+ debounceTimer = setTimeout(async () => {
445
+ const ts = new Date().toLocaleTimeString();
446
+ process.stdout.write(`${chalk.dim(`[${ts}]`)} File changed → pushing... `);
447
+ try {
448
+ const file = readPathwayFile(pathwayFilePath);
449
+ const apiData = fileToApi(file);
450
+ await api.post(`/v1/convo_pathway/${projectConfig.pathway_id}`, apiData);
451
+ console.log(chalk.green("✓"));
452
+ }
453
+ catch (err) {
454
+ console.log(chalk.red("✗"));
455
+ console.error(chalk.red(` ${err instanceof Error ? err.message : "Push failed"}`));
456
+ }
457
+ }, 500);
458
+ });
459
+ // Keep process alive
460
+ await new Promise(() => { });
461
+ }
462
+ catch (err) {
463
+ handleError(err);
464
+ }
465
+ });
466
+ // ──────────────────────────────────────
467
+ // INTERACTIVE EDITING
468
+ // ──────────────────────────────────────
469
+ // ── bland pathway edit ──
470
+ pathway
471
+ .command("edit")
472
+ .description("Edit a pathway node's prompt interactively")
473
+ .argument("<id>", "Pathway ID")
474
+ .action(async (id) => {
475
+ try {
476
+ const spinner = ora("Fetching pathway...").start();
477
+ const pw = await api.get(`/v1/convo_pathway/${id}`);
478
+ spinner.stop();
479
+ if (!pw.nodes || pw.nodes.length === 0) {
480
+ console.log(chalk.dim(" Pathway has no nodes."));
481
+ return;
482
+ }
483
+ const nodeChoice = await select({
484
+ message: "Select node to edit:",
485
+ choices: pw.nodes.map((n) => ({
486
+ name: `${n.data.name || n.id} ${chalk.dim(`(${n.data.type || "default"})`)}`,
487
+ value: n.id,
488
+ })),
489
+ });
490
+ const node = pw.nodes.find((n) => n.id === nodeChoice);
491
+ if (!node)
492
+ return;
493
+ console.log();
494
+ console.log(chalk.bold(`Editing node: ${node.data.name || node.id}`));
495
+ console.log(chalk.dim("Current prompt:"));
496
+ console.log(chalk.dim(node.data.prompt || "(empty)"));
497
+ console.log();
498
+ const newPrompt = await editor({
499
+ message: "Edit prompt (opens in $EDITOR):",
500
+ default: node.data.prompt || "",
501
+ });
502
+ if (newPrompt.trim() === (node.data.prompt || "").trim()) {
503
+ console.log(chalk.dim(" No changes made."));
504
+ return;
505
+ }
506
+ node.data.prompt = newPrompt.trim();
507
+ const updateSpinner = ora("Updating pathway...").start();
508
+ await api.post(`/v1/convo_pathway/${id}`, {
509
+ nodes: pw.nodes,
510
+ edges: pw.edges,
511
+ });
512
+ updateSpinner.succeed("Node prompt updated.");
513
+ }
514
+ catch (err) {
515
+ handleError(err);
516
+ }
517
+ });
518
+ // ──────────────────────────────────────
519
+ // TESTING & SIMULATION
520
+ // ──────────────────────────────────────
521
+ // ── bland pathway chat ──
522
+ pathway
523
+ .command("chat")
524
+ .description("Interactive text chat with a pathway")
525
+ .argument("<id>", "Pathway ID")
526
+ .option("--start-node <node_id>", "Start at specific node")
527
+ .option("--variables <json>", "Inject variables (JSON)")
528
+ .option("--version <v>", "Pathway version")
529
+ .option("--verbose", "Show node transitions and tool calls")
530
+ .action(async (id, opts) => {
531
+ try {
532
+ // Create chat session
533
+ const chatBody = {
534
+ pathway_id: id,
535
+ };
536
+ if (opts.startNode)
537
+ chatBody.start_node_id = opts.startNode;
538
+ if (opts.version)
539
+ chatBody.pathway_version = opts.version;
540
+ if (opts.variables) {
541
+ try {
542
+ chatBody.request_data = JSON.parse(opts.variables);
543
+ }
544
+ catch {
545
+ console.error(chalk.red("Error: --variables must be valid JSON"));
546
+ process.exit(1);
547
+ }
548
+ }
549
+ const spinner = ora("Creating chat session...").start();
550
+ const session = await api.post("/v1/pathway/chat", chatBody);
551
+ spinner.stop();
552
+ console.log();
553
+ console.log(chalk.bold(`Chat with pathway ${chalk.dim(`(${id})`)}`));
554
+ console.log(chalk.dim("Type your messages. Ctrl+C to exit."));
555
+ console.log();
556
+ // Interactive REPL
557
+ const rl = readline.createInterface({
558
+ input: process.stdin,
559
+ output: process.stdout,
560
+ });
561
+ const chat = async (userMessage) => {
562
+ try {
563
+ const response = await api.post(`/v1/pathway/chat/${session.chat_id}`, { message: userMessage });
564
+ if (opts.verbose && response.current_node) {
565
+ console.log(chalk.dim(` ─── Node: ${response.current_node} ───`));
566
+ }
567
+ if (opts.verbose && response.variables) {
568
+ const vars = Object.entries(response.variables);
569
+ if (vars.length > 0) {
570
+ for (const [k, v] of vars) {
571
+ console.log(chalk.dim(` ─── Extracted: ${k} = ${JSON.stringify(v)} ───`));
572
+ }
573
+ }
574
+ }
575
+ console.log(` ${chalk.cyan("Agent")}: ${response.message}`);
576
+ console.log();
577
+ }
578
+ catch (err) {
579
+ console.error(chalk.red(` Error: ${err instanceof Error ? err.message : "Chat failed"}`));
580
+ }
581
+ };
582
+ const askQuestion = () => {
583
+ rl.question(chalk.green("You: "), async (answer) => {
584
+ if (!answer.trim()) {
585
+ askQuestion();
586
+ return;
587
+ }
588
+ await chat(answer.trim());
589
+ askQuestion();
590
+ });
591
+ };
592
+ askQuestion();
593
+ // Handle Ctrl+C
594
+ rl.on("close", () => {
595
+ console.log();
596
+ console.log(chalk.dim(" Chat ended."));
597
+ process.exit(0);
598
+ });
599
+ }
600
+ catch (err) {
601
+ handleError(err);
602
+ }
603
+ });
604
+ // ── bland pathway test ──
605
+ pathway
606
+ .command("test")
607
+ .description("Run test cases against a pathway")
608
+ .argument("<id>", "Pathway ID")
609
+ .option("--file <path>", "Test case file (YAML)")
610
+ .option("--json", "Output as JSON")
611
+ .action(async (id, opts) => {
612
+ try {
613
+ let testFile = opts.file;
614
+ if (!testFile) {
615
+ const projectConfig = readProjectConfig();
616
+ if (projectConfig?.test_file) {
617
+ testFile = projectConfig.test_file;
618
+ }
619
+ else {
620
+ // Try default location
621
+ const defaultPath = path.join("tests", "test-cases.yaml");
622
+ if (fs.existsSync(defaultPath)) {
623
+ testFile = defaultPath;
624
+ }
625
+ }
626
+ }
627
+ if (!testFile || !fs.existsSync(testFile)) {
628
+ console.error(chalk.red("No test file found. Use --file or create tests/test-cases.yaml"));
629
+ process.exit(1);
630
+ }
631
+ const content = fs.readFileSync(testFile, "utf-8");
632
+ const testData = yaml.parse(content);
633
+ const tests = testData.tests || [];
634
+ if (tests.length === 0) {
635
+ console.log(chalk.dim(" No test cases found."));
636
+ return;
637
+ }
638
+ console.log(`Running ${chalk.bold(tests.length)} test case(s)...`);
639
+ console.log();
640
+ const results = [];
641
+ for (const test of tests) {
642
+ const start = Date.now();
643
+ process.stdout.write(` ${chalk.dim("⠋")} ${test.name}...`);
644
+ try {
645
+ // Create chat session for test
646
+ const session = await api.post("/v1/pathway/chat", { pathway_id: id });
647
+ // Send scenario as first message
648
+ const response = await api.post(`/v1/pathway/chat/${session.chat_id}`, { message: test.scenario });
649
+ const duration = (Date.now() - start) / 1000;
650
+ // Basic validation: check if we got a response
651
+ const passed = !!response.message;
652
+ results.push({
653
+ name: test.name,
654
+ passed,
655
+ duration,
656
+ });
657
+ const icon = passed ? chalk.green("✓") : chalk.red("✗");
658
+ process.stdout.write(`\r ${icon} ${test.name} ${chalk.dim(`(${duration.toFixed(1)}s)`)}\n`);
659
+ }
660
+ catch (err) {
661
+ const duration = (Date.now() - start) / 1000;
662
+ const errorMsg = err instanceof Error ? err.message : "Unknown error";
663
+ results.push({
664
+ name: test.name,
665
+ passed: false,
666
+ duration,
667
+ error: errorMsg,
668
+ });
669
+ process.stdout.write(`\r ${chalk.red("✗")} ${test.name} ${chalk.dim(`(${duration.toFixed(1)}s)`)} — ${chalk.red(errorMsg)}\n`);
670
+ }
671
+ }
672
+ const passed = results.filter((r) => r.passed).length;
673
+ const total = results.length;
674
+ console.log();
675
+ if (passed === total) {
676
+ output.success(`${passed}/${total} passed`);
677
+ }
678
+ else {
679
+ console.log(chalk.red(` ${passed}/${total} passed`));
680
+ }
681
+ if (opts.json)
682
+ output.json(results);
683
+ }
684
+ catch (err) {
685
+ handleError(err);
686
+ }
687
+ });
688
+ // ── bland pathway simulate ──
689
+ pathway
690
+ .command("simulate")
691
+ .description("Run AI simulation on a pathway")
692
+ .argument("<id>", "Pathway ID")
693
+ .option("--scenario <text>", "Scenario description")
694
+ .option("--count <n>", "Number of simulations", "1")
695
+ .option("--json", "Output as JSON")
696
+ .action(async (id, opts) => {
697
+ try {
698
+ const spinner = ora("Starting simulation...").start();
699
+ const body = {
700
+ pathway_id: id,
701
+ count: parseInt(opts.count, 10),
702
+ };
703
+ if (opts.scenario)
704
+ body.scenario = opts.scenario;
705
+ const result = await api.post("/v1/pathway/simulations", body);
706
+ spinner.succeed(`Simulation started ${chalk.dim(`(${result.simulation_id})`)}`);
707
+ if (opts.json)
708
+ output.json(result);
709
+ }
710
+ catch (err) {
711
+ handleError(err);
712
+ }
713
+ });
714
+ // ── bland pathway promote ──
715
+ pathway
716
+ .command("promote")
717
+ .description("Promote pathway draft to production")
718
+ .argument("<id>", "Pathway ID")
719
+ .option("--version <v>", "Specific version to promote")
720
+ .action(async (id, opts) => {
721
+ try {
722
+ const proceed = await confirm({
723
+ message: `Promote pathway ${id} to production?`,
724
+ });
725
+ if (!proceed)
726
+ return;
727
+ const spinner = ora("Promoting to production...").start();
728
+ const body = { pathway_id: id };
729
+ if (opts.version)
730
+ body.version = opts.version;
731
+ await api.post(`/v1/convo_pathway/${id}/promote`, body);
732
+ spinner.succeed("Promoted to production.");
733
+ }
734
+ catch (err) {
735
+ handleError(err);
736
+ }
737
+ });
738
+ // ── bland pathway versions ──
739
+ pathway
740
+ .command("versions")
741
+ .description("List pathway versions")
742
+ .argument("<id>", "Pathway ID")
743
+ .option("--json", "Output as JSON")
744
+ .action(async (id, opts) => {
745
+ try {
746
+ const spinner = ora("Fetching versions...").start();
747
+ const versions = await api.get(`/v1/convo_pathway/${id}/versions`);
748
+ spinner.stop();
749
+ if (opts.json) {
750
+ output.json(versions);
751
+ return;
752
+ }
753
+ if (!versions || versions.length === 0) {
754
+ console.log(chalk.dim(" No versions found."));
755
+ return;
756
+ }
757
+ output.json(versions);
758
+ }
759
+ catch (err) {
760
+ handleError(err);
761
+ }
762
+ });
763
+ // ── bland pathway generate ──
764
+ pathway
765
+ .command("generate")
766
+ .description("AI-generate a pathway from a description")
767
+ .option("--description <text>", "What the pathway should do")
768
+ .option("--json", "Output as JSON")
769
+ .action(async (opts) => {
770
+ try {
771
+ let description = opts.description;
772
+ if (!description) {
773
+ description = await input({
774
+ message: "Describe what the pathway should do:",
775
+ validate: (v) => v.trim().length > 0 || "Description is required",
776
+ });
777
+ }
778
+ const spinner = ora("Generating pathway...").start();
779
+ const result = await api.post("/v1/pathway/generate", { description });
780
+ spinner.succeed(`Generated "${result.name}" ${chalk.dim(`(${result.id})`)}`);
781
+ if (opts.json)
782
+ output.json(result);
783
+ }
784
+ catch (err) {
785
+ handleError(err);
786
+ }
787
+ });
788
+ // ──────────────────────────────────────
789
+ // NODE-LEVEL COMMANDS
790
+ // ──────────────────────────────────────
791
+ const node = pathway
792
+ .command("node")
793
+ .description("Manage individual pathway nodes");
794
+ // ── bland pathway node list ──
795
+ node
796
+ .command("list")
797
+ .description("List all nodes in a pathway")
798
+ .argument("<pathway_id>", "Pathway ID")
799
+ .option("--json", "Output as JSON")
800
+ .action(async (pathwayId, opts) => {
801
+ try {
802
+ const spinner = ora("Fetching nodes...").start();
803
+ const pw = await api.get(`/v1/convo_pathway/${pathwayId}`);
804
+ spinner.stop();
805
+ if (opts.json) {
806
+ output.json(pw.nodes);
807
+ return;
808
+ }
809
+ if (!pw.nodes || pw.nodes.length === 0) {
810
+ console.log(chalk.dim(" No nodes."));
811
+ return;
812
+ }
813
+ output.table(pw.nodes.map((n) => ({
814
+ id: n.id,
815
+ name: n.data.name || n.id,
816
+ type: n.data.type || "default",
817
+ prompt: n.data.prompt
818
+ ? output.truncate(n.data.prompt, 60)
819
+ : chalk.dim("—"),
820
+ vars: n.data.extractVars?.length || 0,
821
+ global: n.data.isGlobal ? "yes" : "",
822
+ })), [
823
+ { key: "name", header: "Name", width: 25 },
824
+ { key: "type", header: "Type", width: 15 },
825
+ { key: "prompt", header: "Prompt", width: 62 },
826
+ { key: "vars", header: "Vars", width: 6 },
827
+ { key: "global", header: "Global", width: 8 },
828
+ ]);
829
+ }
830
+ catch (err) {
831
+ handleError(err);
832
+ }
833
+ });
834
+ // ── bland pathway node test ──
835
+ node
836
+ .command("test")
837
+ .description("Test an individual node")
838
+ .argument("<pathway_id>", "Pathway ID")
839
+ .argument("<node_id>", "Node ID")
840
+ .option("--prompt <text>", "Override node prompt")
841
+ .option("--conversation <call_id>", "Use real call as context")
842
+ .option("--permutations <n>", "Number of variations", "5")
843
+ .option("--version <v>", "Pathway version")
844
+ .option("--json", "Output as JSON")
845
+ .action(async (pathwayId, nodeId, opts) => {
846
+ try {
847
+ const spinner = ora("Running node test...").start();
848
+ const body = {
849
+ pathway_id: pathwayId,
850
+ node_id: nodeId,
851
+ n_permutations: parseInt(opts.permutations, 10),
852
+ };
853
+ if (opts.prompt)
854
+ body.new_prompt = opts.prompt;
855
+ if (opts.version)
856
+ body.pathway_version = opts.version;
857
+ if (opts.conversation) {
858
+ body.conversations = [
859
+ { id: opts.conversation, type: "call" },
860
+ ];
861
+ }
862
+ const result = await api.post("/v1/node_tests/run", body);
863
+ spinner.succeed(`Node test started ${chalk.dim(`(run_id: ${result.run_id})`)}`);
864
+ if (opts.json)
865
+ output.json(result);
866
+ console.log(chalk.dim(" Use `bland pathway node test-results <run_id>` to check results."));
867
+ }
868
+ catch (err) {
869
+ handleError(err);
870
+ }
871
+ });
872
+ // ──────────────────────────────────────
873
+ // CODE TOOL TESTING
874
+ // ──────────────────────────────────────
875
+ const code = pathway
876
+ .command("code")
877
+ .description("Test custom code nodes");
878
+ // ── bland pathway code test ──
879
+ code
880
+ .command("test")
881
+ .description("Test a custom code node in isolation")
882
+ .argument("<pathway_id>", "Pathway ID")
883
+ .argument("<node_id>", "Node ID")
884
+ .option("--input <json>", "Test input data (JSON)")
885
+ .option("--json", "Output as JSON")
886
+ .action(async (pathwayId, nodeId, opts) => {
887
+ try {
888
+ let testInput = {};
889
+ if (opts.input) {
890
+ try {
891
+ testInput = JSON.parse(opts.input);
892
+ }
893
+ catch {
894
+ console.error(chalk.red("Error: --input must be valid JSON"));
895
+ process.exit(1);
896
+ }
897
+ }
898
+ const spinner = ora("Executing custom code...").start();
899
+ const start = Date.now();
900
+ const result = await api.post(`/v1/blandcode/test`, {
901
+ pathway_id: pathwayId,
902
+ node_id: nodeId,
903
+ input: testInput,
904
+ });
905
+ const duration = Date.now() - start;
906
+ spinner.stop();
907
+ if (opts.json) {
908
+ output.json({ ...result, duration_ms: duration });
909
+ return;
910
+ }
911
+ if (result.error) {
912
+ console.log(chalk.red(` Error: ${result.error}`));
913
+ }
914
+ else {
915
+ output.header("Input");
916
+ console.log(` ${JSON.stringify(testInput, null, 2).split("\n").join("\n ")}`);
917
+ output.header("Output");
918
+ console.log(` ${JSON.stringify(result.output, null, 2).split("\n").join("\n ")}`);
919
+ if (result.logs && result.logs.length > 0) {
920
+ output.header("Logs");
921
+ for (const log of result.logs) {
922
+ console.log(` ${chalk.dim(log)}`);
923
+ }
924
+ }
925
+ output.header("Execution Time");
926
+ console.log(` ${duration}ms`);
927
+ console.log();
928
+ output.success("Code executed successfully");
929
+ }
930
+ }
931
+ catch (err) {
932
+ handleError(err);
933
+ }
934
+ });
935
+ // ──────────────────────────────────────
936
+ // FOLDERS
937
+ // ──────────────────────────────────────
938
+ const folder = pathway
939
+ .command("folder")
940
+ .description("Manage pathway folders");
941
+ folder
942
+ .command("list")
943
+ .description("List pathway folders")
944
+ .option("--json", "Output as JSON")
945
+ .action(async (opts) => {
946
+ try {
947
+ const spinner = ora("Fetching folders...").start();
948
+ const folders = await api.get("/v1/pathway/folders");
949
+ spinner.stop();
950
+ if (opts.json) {
951
+ output.json(folders);
952
+ return;
953
+ }
954
+ output.json(folders);
955
+ }
956
+ catch (err) {
957
+ handleError(err);
958
+ }
959
+ });
960
+ folder
961
+ .command("create")
962
+ .description("Create a pathway folder")
963
+ .argument("<name>", "Folder name")
964
+ .action(async (name) => {
965
+ try {
966
+ const spinner = ora("Creating folder...").start();
967
+ const result = await api.post("/v1/pathway/folders", {
968
+ name,
969
+ });
970
+ spinner.succeed(`Folder "${name}" created ${chalk.dim(`(${result.id})`)}`);
971
+ }
972
+ catch (err) {
973
+ handleError(err);
974
+ }
975
+ });
976
+ }
977
+ //# sourceMappingURL=pathway.js.map