bland-cli 0.1.2

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