@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.
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +143 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/alarm.d.ts +3 -0
- package/dist/commands/alarm.d.ts.map +1 -0
- package/dist/commands/alarm.js +92 -0
- package/dist/commands/alarm.js.map +1 -0
- package/dist/commands/audio.d.ts +3 -0
- package/dist/commands/audio.d.ts.map +1 -0
- package/dist/commands/audio.js +77 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +199 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/batch.d.ts +3 -0
- package/dist/commands/batch.d.ts.map +1 -0
- package/dist/commands/batch.js +108 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/call.d.ts +3 -0
- package/dist/commands/call.d.ts.map +1 -0
- package/dist/commands/call.js +348 -0
- package/dist/commands/call.js.map +1 -0
- package/dist/commands/eval.d.ts +3 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/eval.js +66 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/guard.d.ts +3 -0
- package/dist/commands/guard.d.ts.map +1 -0
- package/dist/commands/guard.js +100 -0
- package/dist/commands/guard.js.map +1 -0
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.d.ts.map +1 -0
- package/dist/commands/knowledge.js +136 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/listen.d.ts +3 -0
- package/dist/commands/listen.d.ts.map +1 -0
- package/dist/commands/listen.js +98 -0
- package/dist/commands/listen.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +22 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/number.d.ts +3 -0
- package/dist/commands/number.d.ts.map +1 -0
- package/dist/commands/number.js +225 -0
- package/dist/commands/number.js.map +1 -0
- package/dist/commands/pathway.d.ts +3 -0
- package/dist/commands/pathway.d.ts.map +1 -0
- package/dist/commands/pathway.js +977 -0
- package/dist/commands/pathway.js.map +1 -0
- package/dist/commands/persona.d.ts +3 -0
- package/dist/commands/persona.d.ts.map +1 -0
- package/dist/commands/persona.js +234 -0
- package/dist/commands/persona.js.map +1 -0
- package/dist/commands/release.d.ts +3 -0
- package/dist/commands/release.d.ts.map +1 -0
- package/dist/commands/release.js +67 -0
- package/dist/commands/release.js.map +1 -0
- package/dist/commands/secret.d.ts +3 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +57 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/commands/sip.d.ts +3 -0
- package/dist/commands/sip.d.ts.map +1 -0
- package/dist/commands/sip.js +45 -0
- package/dist/commands/sip.js.map +1 -0
- package/dist/commands/sms.d.ts +3 -0
- package/dist/commands/sms.d.ts.map +1 -0
- package/dist/commands/sms.js +83 -0
- package/dist/commands/sms.js.map +1 -0
- package/dist/commands/tool.d.ts +3 -0
- package/dist/commands/tool.d.ts.map +1 -0
- package/dist/commands/tool.js +200 -0
- package/dist/commands/tool.js.map +1 -0
- package/dist/commands/voice.d.ts +3 -0
- package/dist/commands/voice.d.ts.map +1 -0
- package/dist/commands/voice.js +95 -0
- package/dist/commands/voice.js.map +1 -0
- package/dist/commands/widget.d.ts +3 -0
- package/dist/commands/widget.d.ts.map +1 -0
- package/dist/commands/widget.js +77 -0
- package/dist/commands/widget.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +17 -0
- package/dist/lib/api.d.ts.map +1 -0
- package/dist/lib/api.js +89 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +117 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +15 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +43 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/output.d.ts +30 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +131 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/pathway-file.d.ts +31 -0
- package/dist/lib/pathway-file.d.ts.map +1 -0
- package/dist/lib/pathway-file.js +236 -0
- package/dist/lib/pathway-file.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +375 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/types/api.d.ts +302 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +2 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/config.d.ts +14 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +2 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/pathway.d.ts +55 -0
- package/dist/types/pathway.d.ts.map +1 -0
- package/dist/types/pathway.js +2 -0
- package/dist/types/pathway.js.map +1 -0
- package/package.json +51 -0
- 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
|