@nodetool-ai/cli-workflow 0.0.0-stage → 0.8.9-beta.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 (71) hide show
  1. package/README.md +14 -2
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/api-client.d.ts +580 -0
  4. package/dist/api-client.d.ts.map +1 -0
  5. package/dist/api-client.js +34 -0
  6. package/dist/api-client.js.map +1 -0
  7. package/dist/commands/collections.d.ts +10 -0
  8. package/dist/commands/collections.d.ts.map +1 -0
  9. package/dist/commands/collections.js +257 -0
  10. package/dist/commands/collections.js.map +1 -0
  11. package/dist/commands/costs.d.ts +15 -0
  12. package/dist/commands/costs.d.ts.map +1 -0
  13. package/dist/commands/costs.js +201 -0
  14. package/dist/commands/costs.js.map +1 -0
  15. package/dist/commands/debug.d.ts +13 -0
  16. package/dist/commands/debug.d.ts.map +1 -0
  17. package/dist/commands/debug.js +149 -0
  18. package/dist/commands/debug.js.map +1 -0
  19. package/dist/commands/errors.d.ts +11 -0
  20. package/dist/commands/errors.d.ts.map +1 -0
  21. package/dist/commands/errors.js +177 -0
  22. package/dist/commands/errors.js.map +1 -0
  23. package/dist/commands/generate.d.ts +17 -0
  24. package/dist/commands/generate.d.ts.map +1 -0
  25. package/dist/commands/generate.js +326 -0
  26. package/dist/commands/generate.js.map +1 -0
  27. package/dist/commands/generations.d.ts +17 -0
  28. package/dist/commands/generations.d.ts.map +1 -0
  29. package/dist/commands/generations.js +408 -0
  30. package/dist/commands/generations.js.map +1 -0
  31. package/dist/commands/install-missing-packs.d.ts +21 -0
  32. package/dist/commands/install-missing-packs.d.ts.map +1 -0
  33. package/dist/commands/install-missing-packs.js +48 -0
  34. package/dist/commands/install-missing-packs.js.map +1 -0
  35. package/dist/commands/node.d.ts +10 -0
  36. package/dist/commands/node.d.ts.map +1 -0
  37. package/dist/commands/node.js +130 -0
  38. package/dist/commands/node.js.map +1 -0
  39. package/dist/commands/projects.d.ts +11 -0
  40. package/dist/commands/projects.d.ts.map +1 -0
  41. package/dist/commands/projects.js +145 -0
  42. package/dist/commands/projects.js.map +1 -0
  43. package/dist/commands/resource-read.d.ts +28 -0
  44. package/dist/commands/resource-read.d.ts.map +1 -0
  45. package/dist/commands/resource-read.js +314 -0
  46. package/dist/commands/resource-read.js.map +1 -0
  47. package/dist/commands/runs.d.ts +7 -0
  48. package/dist/commands/runs.d.ts.map +1 -0
  49. package/dist/commands/runs.js +302 -0
  50. package/dist/commands/runs.js.map +1 -0
  51. package/dist/commands/validate.d.ts +24 -0
  52. package/dist/commands/validate.d.ts.map +1 -0
  53. package/dist/commands/validate.js +132 -0
  54. package/dist/commands/validate.js.map +1 -0
  55. package/dist/index.d.ts +16 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +1034 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/nodetool.d.ts +3 -0
  60. package/dist/nodetool.d.ts.map +1 -0
  61. package/dist/nodetool.js +4 -0
  62. package/dist/nodetool.js.map +1 -0
  63. package/dist/packs/command-service.d.ts +3 -0
  64. package/dist/packs/command-service.d.ts.map +1 -0
  65. package/dist/packs/command-service.js +9 -0
  66. package/dist/packs/command-service.js.map +1 -0
  67. package/dist/workflow-packs.d.ts +7 -0
  68. package/dist/workflow-packs.d.ts.map +1 -0
  69. package/dist/workflow-packs.js +31 -0
  70. package/dist/workflow-packs.js.map +1 -0
  71. package/package.json +73 -4
package/dist/index.js ADDED
@@ -0,0 +1,1034 @@
1
+ import { trimHyphens } from "@nodetool-ai/cli-common/trimHyphens";
2
+ import { resolveWorkflowPacks } from "./workflow-packs.js";
3
+ import { installLocalModelInterfaces } from "@nodetool-ai/cli-execution/local-model-interfaces";
4
+ import { configureCliPackHost } from "@nodetool-ai/cli-execution/packs/host";
5
+ import { initializeCliRuntime } from "@nodetool-ai/cli-common/runtime-env";
6
+ import { resolveLocalSecret } from "@nodetool-ai/cli-local/local-secrets";
7
+ import { Command as CommanderCommand } from "commander";
8
+ import { createRequire } from "node:module";
9
+ import { dirname, resolve } from "node:path";
10
+ import { workflowToDsl, workflowToScript } from "@nodetool-ai/dsl";
11
+ import { initDb, migrateSqliteDb, Workflow, Asset, Secret, getSecret } from "@nodetool-ai/models";
12
+ import { initMasterKey } from "@nodetool-ai/security";
13
+ import { getDefaultDbPath, getDefaultAssetsPath } from "@nodetool-ai/config";
14
+ import { ExecutionSession } from "@nodetool-ai/execution";
15
+ import { NodeRegistry, createGraphNodeTypeResolver } from "@nodetool-ai/node-sdk";
16
+ import { registerCliPacks } from "@nodetool-ai/cli-execution/packs/host";
17
+ import { CLI_LOCAL_BUILTIN_PACK_POLICY, CLI_DSL_BUILTIN_PACK_POLICY } from "@nodetool-ai/cli-execution/node-registry";
18
+ import { ProcessingContext, createLocalWorkspace, initTelemetry } from "@nodetool-ai/runtime";
19
+ import { FileStorageAdapter } from "@nodetool-ai/storage";
20
+ import { mkdirSync } from "node:fs";
21
+ import { addSupervisorOptions, createSupervisorHandle, parseSupervisorFlags, printSupervisedSummary, recordSupervisorCost, streamInterventionLines } from "@nodetool-ai/cli-execution/supervisor";
22
+ import { installMissingPacksAndReexec } from "./commands/install-missing-packs.js";
23
+ import { registerDebugCommands } from "./commands/debug.js";
24
+ import { registerValidateCommand } from "./commands/validate.js";
25
+ import { registerNodeCommands } from "./commands/node.js";
26
+ import { registerGenerateCommand } from "./commands/generate.js";
27
+ import { registerCollectionCommands } from "./commands/collections.js";
28
+ import { registerCostsCommands } from "./commands/costs.js";
29
+ import { registerErrorsCommands } from "./commands/errors.js";
30
+ import { registerGenerationsCommands } from "./commands/generations.js";
31
+ import { registerRunsCommands } from "./commands/runs.js";
32
+ import { registerProjectCommands } from "./commands/projects.js";
33
+ import { finishCliTelemetry, startCliTelemetry, trackCliEvent } from "@nodetool-ai/cli-common/telemetry";
34
+ import { bucketDuration, bucketNodeCount, categorizeError } from "@nodetool-ai/analytics";
35
+ import { registerResourceReadCommands } from "./commands/resource-read.js";
36
+ import { createApiClient } from "./api-client.js";
37
+ import { closePythonProviders } from "@nodetool-ai/cli-providers";
38
+ import { isString } from "@nodetool-ai/cli-common/predicates";
39
+ import { printTable, asJson, readSecretValue } from "@nodetool-ai/cli-common/commands/output";
40
+ import { printCommandError, commandMissingRuntime, commandMissingPack } from "@nodetool-ai/pack-manager/command-errors";
41
+ const CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
42
+ function trimTrailingSlashes(value) {
43
+ let end = value.length;
44
+ while (end > 0 && value.charCodeAt(end - 1) === 47) {
45
+ end--;
46
+ }
47
+ return value.slice(0, end);
48
+ }
49
+ export async function createProgram() {
50
+ const program = new CommanderCommand();
51
+ // ---------------------------------------------------------------------------
52
+ // DB setup (for secrets commands)
53
+ // ---------------------------------------------------------------------------
54
+ // The local single-user id every direct-DB command runs as, matching the
55
+ // `workflows run` / `secrets` commands and the stdio MCP server.
56
+ const LOCAL_USER_ID = "1";
57
+ // Open the local SQLite database for a read-only command. Unlike setupDb() this
58
+ // skips master-key unlocking (no secret decryption needed to list/read rows),
59
+ // so it never emits a keychain warning. Idempotent — initDb() is a no-op once
60
+ // a connection is open.
61
+ function ensureDb() {
62
+ initDb(getDefaultDbPath());
63
+ }
64
+ async function setupDb() {
65
+ const dbPath = getDefaultDbPath();
66
+ await migrateSqliteDb(dbPath);
67
+ initDb(dbPath);
68
+ // Resolve the master encryption key from keychain / env / AWS so that
69
+ // both encryption (Secret.upsert) and decryption (Secret.getDecryptedValue)
70
+ // use the same persistent key. Without this, a fresh process would
71
+ // auto-generate a one-shot key inside `getMasterKey()`, encrypt with it,
72
+ // then lose it on exit — and the next launch could not decrypt.
73
+ try {
74
+ await initMasterKey();
75
+ }
76
+ catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ process.stderr.write(`Could not unlock the secret store: ${msg}\n` +
79
+ `Tip: set SECRETS_MASTER_KEY or grant keychain access.\n`);
80
+ }
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // API clients
84
+ // ---------------------------------------------------------------------------
85
+ // REST GET (text) — used for the DSL export endpoint which stays REST
86
+ // because it returns a TypeScript source string rather than JSON.
87
+ async function apiGetText(apiUrl, path) {
88
+ const res = await fetch(`${apiUrl}${path}`);
89
+ if (!res.ok)
90
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
91
+ return res.text();
92
+ }
93
+ // This CLI's package version, stamped into exported bundle manifests.
94
+ function cliVersion() {
95
+ try {
96
+ const req = createRequire(import.meta.url);
97
+ return req("../package.json").version;
98
+ }
99
+ catch {
100
+ // Version is best-effort metadata; omit it if the file can't be read.
101
+ return undefined;
102
+ }
103
+ }
104
+ // Download asset bytes from a running server, used by the export commands to
105
+ // resolve `asset://` / `/api/storage/` (and optionally http) refs into bytes.
106
+ function makeAssetFetcher(apiUrl) {
107
+ return async (ref) => {
108
+ let url = null;
109
+ if (ref.startsWith("asset://")) {
110
+ const id = ref.slice("asset://".length);
111
+ const bareId = id.replace(/\.[^.]+$/, "");
112
+ try {
113
+ const meta = await fetch(`${apiUrl}/api/assets/${encodeURIComponent(bareId)}`);
114
+ if (meta.ok) {
115
+ const j = (await meta.json());
116
+ if (j.get_url) {
117
+ url = j.get_url.startsWith("/")
118
+ ? `${apiUrl}${j.get_url}`
119
+ : j.get_url;
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // Metadata lookup failed — fall back to the storage route below.
125
+ }
126
+ url ??= `${apiUrl}/api/storage/${encodeURIComponent(id)}`;
127
+ }
128
+ else if (ref.includes("/api/storage/")) {
129
+ url = /^https?:\/\//.test(ref)
130
+ ? ref
131
+ : `${apiUrl}${ref.startsWith("/") ? ref : `/${ref}`}`;
132
+ }
133
+ else if (/^https?:\/\//.test(ref)) {
134
+ url = ref;
135
+ }
136
+ if (!url)
137
+ return null;
138
+ const res = await fetch(url);
139
+ if (!res.ok)
140
+ return null;
141
+ return new Uint8Array(await res.arrayBuffer());
142
+ };
143
+ }
144
+ // Resolve asset bytes from the local assets directory, so the export commands
145
+ // work by id without a running server. `asset://<key>` is tried verbatim as a
146
+ // storage key first (that's how import-bundle writes refs), then via the DB
147
+ // record to derive `<id>.<ext>` from the content type. Remote http(s) refs
148
+ // still fetch over the network.
149
+ async function makeLocalAssetFetcher() {
150
+ const { getAssetFileName } = await import("@nodetool-ai/protocol");
151
+ const storage = new FileStorageAdapter(getDefaultAssetsPath());
152
+ return async (ref) => {
153
+ if (ref.startsWith("asset://")) {
154
+ const raw = ref.slice("asset://".length);
155
+ const direct = await storage.retrieve(`/api/storage/${raw}`);
156
+ if (direct)
157
+ return direct;
158
+ const bareId = raw.replace(/\.[^.]+$/, "");
159
+ const asset = await Asset.find(LOCAL_USER_ID, bareId);
160
+ if (!asset)
161
+ return null;
162
+ const key = getAssetFileName(asset.id, asset.content_type);
163
+ return storage.retrieve(`/api/storage/${key}`);
164
+ }
165
+ if (ref.includes("/api/storage/")) {
166
+ const local = await storage.retrieve(ref);
167
+ if (local)
168
+ return local;
169
+ }
170
+ if (/^https?:\/\//.test(ref)) {
171
+ const res = await fetch(ref);
172
+ if (!res.ok)
173
+ return null;
174
+ return new Uint8Array(await res.arrayBuffer());
175
+ }
176
+ return null;
177
+ };
178
+ }
179
+ // Asset and timeline persistence for every context any command builds, here
180
+ program
181
+ .name("nodetool")
182
+ .description("NodeTool CLI")
183
+ .version(CLI_VERSION)
184
+ .option("--trace-file <path>", "Append every LLM/agent/workflow span as JSONL to <path>")
185
+ .option("--trace-stdout [format]", "Stream spans to stdout: 'pretty' (default) or 'json' (JSONL)")
186
+ .option("--no-trace-stdout", "Disable stdout span output (overrides NODETOOL_TRACE_STDOUT)")
187
+ // A command that started the Python worker must stop it, or the worker
188
+ // child process keeps the CLI alive after the output.
189
+ .hook("postAction", async () => {
190
+ closePythonProviders();
191
+ await finishCliTelemetry();
192
+ })
193
+ .hook("preAction", async (thisCommand, actionCommand) => {
194
+ // Anonymous usage analytics, off unless `nodetool telemetry enable` ran.
195
+ // Only the matched top-level command name is used, never argv.
196
+ startCliTelemetry(topLevelCommandName(actionCommand));
197
+ // With `--trace`, the `debug` command owns telemetry: it routes spans into
198
+ // its bundle's trace.jsonl via its own initTelemetry call. Initializing here
199
+ // first would win the one-shot init and drop that file sink, so skip it.
200
+ // Without `--trace`, fall through so the global --trace-file/--trace-stdout
201
+ // flags still work for a debug run.
202
+ if (actionCommand?.name() === "debug" && actionCommand.opts()["trace"])
203
+ return;
204
+ const opts = thisCommand.opts();
205
+ await initTelemetry({
206
+ ...(opts.traceFile && { traceFile: opts.traceFile }),
207
+ ...(opts.traceStdout !== undefined && {
208
+ stdout: parseTraceStdout(opts.traceStdout)
209
+ })
210
+ });
211
+ });
212
+ /** The name of the top-level command an action belongs to. */
213
+ function topLevelCommandName(command) {
214
+ let current = command;
215
+ while (current?.parent && current.parent.parent) {
216
+ current = current.parent;
217
+ }
218
+ return current?.name();
219
+ }
220
+ function parseTraceStdout(v) {
221
+ if (v === false)
222
+ return false;
223
+ if (v === true)
224
+ return "pretty";
225
+ if (isString(v)) {
226
+ const lower = v.toLowerCase();
227
+ if (lower === "false" || lower === "0" || lower === "no")
228
+ return false;
229
+ if (lower === "json")
230
+ return "json";
231
+ if (lower === "pretty" || lower === "true" || lower === "1")
232
+ return "pretty";
233
+ }
234
+ throw new Error(`--trace-stdout must be 'pretty' or 'json' (got ${JSON.stringify(v)})`);
235
+ }
236
+ // ---------------------------------------------------------------------------
237
+ // run — execute a TypeScript/JavaScript DSL workflow file
238
+ // ---------------------------------------------------------------------------
239
+ addSupervisorOptions(program
240
+ .command("run <dsl-file>")
241
+ .description("Run a TypeScript/JavaScript DSL workflow file")
242
+ .option("--json", "Output results as JSON (default: pretty-print)")
243
+ .option("--install-missing", "Authorize missing node pack installation, then re-execute once")).action(async (dslFile, opts) => {
244
+ try {
245
+ const { resolve } = await import("node:path");
246
+ const { runDslFile } = await import("@nodetool-ai/cli-execution/run-dsl");
247
+ const { NodeRegistry } = await import("@nodetool-ai/node-sdk");
248
+ await registerCliPacks(NodeRegistry.global, CLI_DSL_BUILTIN_PACK_POLICY);
249
+ const supervisorConfig = parseSupervisorFlags(opts);
250
+ const absolutePath = resolve(dslFile);
251
+ let results;
252
+ let interventions = [];
253
+ if (supervisorConfig) {
254
+ // The supervisor's provider key comes from the secret store, and its
255
+ // spend is written to the local ledger — both need the DB.
256
+ await setupDb();
257
+ const { runDslFileSupervised } = await import("@nodetool-ai/cli-execution/run-dsl-supervised");
258
+ const run = await runDslFileSupervised(absolutePath, supervisorConfig, opts.json === true);
259
+ results = run.results;
260
+ interventions = run.interventions;
261
+ }
262
+ else {
263
+ results = await runDslFile(absolutePath);
264
+ }
265
+ const { printRunJson, previewJson } = await import("@nodetool-ai/cli-execution/run-json");
266
+ if (opts.json) {
267
+ // A supervised run reports what the supervisor did alongside the
268
+ // outputs; an unsupervised one keeps the bare results shape.
269
+ await printRunJson(supervisorConfig ? { results, interventions } : results);
270
+ }
271
+ else {
272
+ for (const [workflowName, outputs] of Object.entries(results)) {
273
+ console.log(`\n${workflowName}:`);
274
+ for (const [key, value] of Object.entries(outputs)) {
275
+ console.log(` ${key}: ${previewJson(value)}`);
276
+ }
277
+ }
278
+ }
279
+ }
280
+ catch (e) {
281
+ const exit = await installMissingPacksAndReexec(e, {
282
+ command: "run",
283
+ installMissing: opts.installMissing
284
+ });
285
+ if (exit !== null) {
286
+ process.exit(exit);
287
+ }
288
+ process.exit(printCommandError(e, opts.json));
289
+ }
290
+ });
291
+ const workflows = program
292
+ .command("workflows")
293
+ .description("Workflow management");
294
+ addSupervisorOptions(workflows
295
+ .command("run <workflow_id_or_file>")
296
+ .description("Run a workflow by ID, JSON file, or TypeScript DSL file")
297
+ .option("--params <json>", "JSON params string")
298
+ .option("--json", "Output as JSON")
299
+ .option("--install-missing", "Authorize missing node pack installation, then re-execute once")
300
+ .option("--asset-output-mode <mode>", "How media outputs are materialized: native | raw | data_uri | workspace | storage_url | temp_url", "native")
301
+ .option("--workspace <dir>", "Workspace directory for workspace-mode asset output (default: ./nodetool-output)")
302
+ .option("--trigger-event <json>", 'Wake a trigger node as the scheduler/webhook adapter would: \'{"node_id":"watch","payload":{"path":"/tmp/a.csv","event":"created"}}\'. ' +
303
+ "Without it a webhook, file-watch or manual trigger has no event to " +
304
+ "emit and the run stalls until its timeout — so this is the only way " +
305
+ "to exercise a trigger-driven workflow from the CLI.")).action(async (idOrFile, opts) => {
306
+ try {
307
+ const supervisorConfig = parseSupervisorFlags(opts);
308
+ await setupDb();
309
+ let graph;
310
+ let workflowId = null;
311
+ const params = opts.params
312
+ ? JSON.parse(opts.params)
313
+ : {};
314
+ // A trigger node emits from `emitTriggerEvent`, not `genProcess`, and
315
+ // only when the run carries an event addressed to it. The kernel has
316
+ // always accepted one (`RunJobRequest.trigger_event`); nothing on the
317
+ // CLI could supply it, so a webhook or file-watch workflow could be
318
+ // validated but never executed. `input_id` defaults because every
319
+ // caller would otherwise invent the same throwaway id.
320
+ const triggerEvent = opts.triggerEvent
321
+ ? (() => {
322
+ const parsed = JSON.parse(opts.triggerEvent);
323
+ if (!parsed.node_id) {
324
+ throw new Error('--trigger-event needs a "node_id" naming the trigger node to wake.');
325
+ }
326
+ return {
327
+ node_id: parsed.node_id,
328
+ payload: parsed.payload ?? {},
329
+ input_id: parsed.input_id ?? `cli-${Date.now()}`
330
+ };
331
+ })()
332
+ : null;
333
+ // Determine if argument is a file path or workflow ID
334
+ if (idOrFile.endsWith(".json") ||
335
+ idOrFile.endsWith(".ts") ||
336
+ idOrFile.endsWith(".tsx") ||
337
+ idOrFile.includes("/") ||
338
+ idOrFile.includes("\\")) {
339
+ /** A DSL/JSON workflow file: the graph, optionally wrapped. */
340
+ let raw;
341
+ if (idOrFile.endsWith(".ts") || idOrFile.endsWith(".tsx")) {
342
+ const { readFileTarget } = await import("@nodetool-ai/cli-execution/debug/target");
343
+ raw = (await readFileTarget(idOrFile)).raw;
344
+ }
345
+ else {
346
+ const { readFileSync } = await import("node:fs");
347
+ raw = JSON.parse(readFileSync(idOrFile, "utf8"));
348
+ }
349
+ graph = raw.graph ?? raw;
350
+ workflowId = raw.workflow_id ?? raw.id ?? null;
351
+ if (raw.params)
352
+ Object.assign(params, raw.params);
353
+ }
354
+ else {
355
+ const wf = await Workflow.get(idOrFile);
356
+ if (!wf)
357
+ throw new Error(`Workflow not found: ${idOrFile}`);
358
+ graph = wf.graph;
359
+ workflowId = idOrFile;
360
+ }
361
+ if (!graph?.nodes || !graph?.edges) {
362
+ throw new Error("Invalid workflow: missing nodes or edges");
363
+ }
364
+ // Editor-only nodes (Comment, Group, Reroute) and the data→properties
365
+ // rename are handled by `ExecutionSession.create` below, which runs
366
+ // the graph through `normalizeGraph` (@nodetool-ai/execution) — no
367
+ // need to duplicate that here.
368
+ const registry = new NodeRegistry();
369
+ await registerCliPacks(registry, CLI_LOCAL_BUILTIN_PACK_POLICY);
370
+ const jobId = `job-${Date.now()}`;
371
+ // Resolve asset URIs (e.g. /api/storage/<key>) against the local
372
+ // assets directory, so workflows referencing stored assets run the
373
+ // same decode path the server does.
374
+ // Apply the workflow's assigned workspace (if any) so file ops and
375
+ // workspace-driving nodes land in the same directory as a server run.
376
+ const { resolveWorkflowWorkspace } = await import("@nodetool-ai/execution/service");
377
+ const assignedWorkspace = workflowId
378
+ ? await resolveWorkflowWorkspace(workflowId, "1")
379
+ : null;
380
+ const ASSET_OUTPUT_MODES = [
381
+ "native",
382
+ "raw",
383
+ "data_uri",
384
+ "workspace",
385
+ "storage_url",
386
+ "temp_url"
387
+ ];
388
+ const assetOutputMode = (opts.assetOutputMode ??
389
+ "native");
390
+ if (!ASSET_OUTPUT_MODES.includes(assetOutputMode)) {
391
+ throw new Error(`Unknown --asset-output-mode "${assetOutputMode}". ` +
392
+ `Expected one of: ${ASSET_OUTPUT_MODES.join(", ")}`);
393
+ }
394
+ // `--workspace <dir>` wins, then whatever the workflow is assigned.
395
+ // `workspace` asset mode writes each output into <workspace>/assets, so
396
+ // it needs somewhere to write even when the run has no saved workflow (a
397
+ // JSON or DSL file target). Fall back to ./nodetool-output so a bare
398
+ // `workflows run file.json --asset-output-mode workspace` produces files
399
+ // in the current directory rather than failing.
400
+ const overrideWorkspaceDir = opts.workspace ??
401
+ (assignedWorkspace
402
+ ? null
403
+ : assetOutputMode === "workspace"
404
+ ? resolve(process.cwd(), "nodetool-output")
405
+ : null);
406
+ if (overrideWorkspaceDir) {
407
+ mkdirSync(overrideWorkspaceDir, { recursive: true });
408
+ }
409
+ const runWorkspace = overrideWorkspaceDir
410
+ ? createLocalWorkspace(overrideWorkspaceDir)
411
+ : assignedWorkspace;
412
+ const assetStorage = new FileStorageAdapter(getDefaultAssetsPath());
413
+ const context = new ProcessingContext({
414
+ jobId,
415
+ workflowId,
416
+ userId: "1",
417
+ secretResolver: resolveLocalSecret,
418
+ storage: assetStorage,
419
+ assetOutputMode,
420
+ workspace: runWorkspace
421
+ });
422
+ const supervisor = supervisorConfig
423
+ ? await createSupervisorHandle({
424
+ config: supervisorConfig,
425
+ context
426
+ })
427
+ : null;
428
+ console.error(`Running workflow${workflowId ? ` ${workflowId}` : ""}` +
429
+ (supervisor
430
+ ? ` supervised by ${supervisor.providerId}/${supervisor.model}`
431
+ : "") +
432
+ "...");
433
+ // ExecutionSession owns graph hydration, Python-bridge connection
434
+ // (NODETOOL_WORKER_URL/NODETOOL_WORKER_TOKEN → remote worker; unset →
435
+ // local stdio worker; pure-TS graphs get none), and executor
436
+ // resolution (registry → Python bridge → throw).
437
+ const sessionOptions = {
438
+ graph,
439
+ registry,
440
+ // Registry alone hydrates node flags but not `propertyTypes`, and
441
+ // correlation analysis reads list-ness only from that map — so a
442
+ // saved workflow (which never carries it) had every list handle
443
+ // read as non-list, and legal fan-in into one was rejected before
444
+ // any node ran. The resolver fills it from node metadata.
445
+ resolveNodeType: createGraphNodeTypeResolver(registry).resolveNodeType,
446
+ jobId,
447
+ workflowId,
448
+ params,
449
+ context
450
+ };
451
+ if (triggerEvent) {
452
+ sessionOptions.triggerEvent = triggerEvent;
453
+ }
454
+ // Message capture is retention, so it stays off unless a supervised
455
+ // run needs the stream to print its `⛨` lines as they happen.
456
+ if (supervisor) {
457
+ sessionOptions.supervisor = supervisor.handle;
458
+ sessionOptions.captureMessages = true;
459
+ }
460
+ const nodeCountBucket = bucketNodeCount(graph.nodes.length);
461
+ trackCliEvent({
462
+ event: "workflow_run",
463
+ properties: { executionMode: "local", nodeCountBucket }
464
+ });
465
+ const runStartedAt = Date.now();
466
+ const session = await ExecutionSession.create(sessionOptions);
467
+ const interventionLines = supervisor
468
+ ? streamInterventionLines(session.messages)
469
+ : Promise.resolve();
470
+ const result = await session.result;
471
+ const durationBucket = bucketDuration(Date.now() - runStartedAt);
472
+ if (result.status === "completed") {
473
+ trackCliEvent({
474
+ event: "workflow_run_succeeded",
475
+ properties: {
476
+ executionMode: "local",
477
+ nodeCountBucket,
478
+ durationBucket
479
+ }
480
+ });
481
+ }
482
+ else {
483
+ trackCliEvent({
484
+ event: "workflow_run_failed",
485
+ properties: {
486
+ executionMode: "local",
487
+ nodeCountBucket,
488
+ durationBucket,
489
+ errorCategory: result.status === "cancelled"
490
+ ? "cancelled"
491
+ : categorizeError(result.error)
492
+ }
493
+ });
494
+ }
495
+ if (result.status === "failed" &&
496
+ (commandMissingRuntime(result) || commandMissingPack(result))) {
497
+ const exit = await installMissingPacksAndReexec(result, {
498
+ command: "workflows-run",
499
+ installMissing: opts.installMissing
500
+ });
501
+ if (exit !== null) {
502
+ process.exit(exit);
503
+ }
504
+ process.exit(printCommandError(result, opts.json));
505
+ }
506
+ await interventionLines;
507
+ supervisor?.handle.close();
508
+ const interventions = result.interventions ?? [];
509
+ if (supervisor && interventions.length > 0) {
510
+ await recordSupervisorCost({
511
+ interventions,
512
+ jobId,
513
+ workflowId,
514
+ userId: "1",
515
+ providerId: supervisor.providerId,
516
+ model: supervisor.model
517
+ });
518
+ }
519
+ const { printRunJson, previewJson } = await import("@nodetool-ai/cli-execution/run-json");
520
+ if (opts.json) {
521
+ await printRunJson(result, {
522
+ outputDir: resolve(process.cwd(), "nodetool-output", jobId)
523
+ });
524
+ }
525
+ else {
526
+ console.log(`Status: ${result.status}`);
527
+ if (result.error)
528
+ console.error(`Error: ${result.error}`);
529
+ for (const [nodeId, outputs] of Object.entries(result.outputs ?? {})) {
530
+ if (Array.isArray(outputs) && outputs.length > 0) {
531
+ console.log(`\nNode ${nodeId}:`);
532
+ for (const out of outputs) {
533
+ console.log(` ${previewJson(out, 200)}`);
534
+ }
535
+ }
536
+ }
537
+ printSupervisedSummary(interventions);
538
+ }
539
+ await finishCliTelemetry();
540
+ process.exit(result.status === "completed" ? 0 : 1);
541
+ }
542
+ catch (e) {
543
+ const exit = await installMissingPacksAndReexec(e, {
544
+ command: "workflows-run",
545
+ installMissing: opts.installMissing
546
+ });
547
+ if (exit !== null) {
548
+ process.exit(exit);
549
+ }
550
+ process.exit(printCommandError(e, opts.json));
551
+ }
552
+ });
553
+ workflows
554
+ .command("export-script <workflow_id_or_file>")
555
+ .description("Convert a workflow into a JavaScript script that calls each node in order (local DB by id, or a JSON file)")
556
+ .option("-t, --target <target>", "sandbox (a JS script document or Code node body) or node (a standalone Node.js .mjs)", "sandbox")
557
+ .option("-o, --output <file>", "Write the script to a file")
558
+ .action(async (idOrFile, opts) => {
559
+ try {
560
+ if (opts.target !== "sandbox" && opts.target !== "node") {
561
+ throw new Error(`Unknown target "${opts.target}": use "sandbox" or "node"`);
562
+ }
563
+ let graph;
564
+ let name;
565
+ if (idOrFile.endsWith(".json") ||
566
+ idOrFile.includes("/") ||
567
+ idOrFile.includes("\\")) {
568
+ const { readFileSync } = await import("node:fs");
569
+ const raw = JSON.parse(readFileSync(idOrFile, "utf8"));
570
+ graph = (raw.graph ?? raw);
571
+ name = isString(raw.name) ? raw.name : null;
572
+ }
573
+ else {
574
+ ensureDb();
575
+ const wf = await Workflow.find(LOCAL_USER_ID, idOrFile);
576
+ if (!wf)
577
+ throw new Error(`Workflow not found: ${idOrFile}`);
578
+ graph = wf.getGraph();
579
+ name = isString(wf.name) ? wf.name : null;
580
+ }
581
+ const script = workflowToScript(graph, {
582
+ target: opts.target,
583
+ workflowName: name
584
+ });
585
+ for (const issue of script.unsupported) {
586
+ console.error(`warning: ${issue.node_type} (${issue.node_id}) ${issue.message}`);
587
+ }
588
+ if (opts.output) {
589
+ const { writeFile } = await import("node:fs/promises");
590
+ await writeFile(opts.output, script.code, "utf8");
591
+ console.log(opts.output);
592
+ return;
593
+ }
594
+ console.log(script.code);
595
+ }
596
+ catch (e) {
597
+ process.exit(printCommandError(e));
598
+ }
599
+ });
600
+ workflows
601
+ .command("export-dsl <workflow_id_or_file>")
602
+ .description("Export a workflow as TypeScript DSL code (local DB by id; --api-url for remote)")
603
+ .option("--api-url <url>", "Fetch the workflow from a remote server instead of the local database", process.env["NODETOOL_API_URL"])
604
+ .option("-o, --output <file>", "Write the exported DSL to a file")
605
+ .action(async (idOrFile, opts) => {
606
+ try {
607
+ let source;
608
+ if (idOrFile.endsWith(".json") ||
609
+ idOrFile.includes("/") ||
610
+ idOrFile.includes("\\")) {
611
+ const { readFileSync } = await import("node:fs");
612
+ const raw = JSON.parse(readFileSync(idOrFile, "utf8"));
613
+ const graph = (raw.graph ?? raw);
614
+ source = workflowToDsl(graph, {
615
+ workflowName: isString(raw.name) ? raw.name : null
616
+ });
617
+ }
618
+ else if (opts.apiUrl) {
619
+ source = await apiGetText(opts.apiUrl, `/api/workflows/${idOrFile}/dsl-export`);
620
+ }
621
+ else {
622
+ ensureDb();
623
+ const wf = await Workflow.find(LOCAL_USER_ID, idOrFile);
624
+ if (!wf)
625
+ throw new Error(`Workflow not found: ${idOrFile}`);
626
+ const graph = wf.getGraph();
627
+ source = workflowToDsl(graph, {
628
+ workflowName: isString(wf.name) ? wf.name : null
629
+ });
630
+ }
631
+ if (opts.output) {
632
+ const { writeFile } = await import("node:fs/promises");
633
+ await writeFile(opts.output, source, "utf8");
634
+ console.log(opts.output);
635
+ return;
636
+ }
637
+ console.log(source);
638
+ }
639
+ catch (e) {
640
+ process.exit(printCommandError(e));
641
+ }
642
+ });
643
+ workflows
644
+ .command("export-example <workflow_id_or_file>")
645
+ .description("Export a workflow as a shipped template: materialize its assets into the " +
646
+ "package's constant asset dir (package:// refs) and write the example JSON")
647
+ .option("--api-url <url>", "Fetch the workflow and asset bytes from a remote server instead of the local DB/assets", process.env["NODETOOL_API_URL"])
648
+ .option("--package <name>", "Owning package", "nodetool-base")
649
+ .option("--assets-dir <dir>", "Root holding <package>/<file> constant assets (defaults to the monorepo base-nodes assets dir)")
650
+ .option("--examples-dir <dir>", "Directory the example JSON is written under, as <dir>/<package>/<name>.json")
651
+ .option("-o, --output <file>", "Write the example JSON to this exact path")
652
+ .option("--include-remote", "Also materialize http(s) and local-file refs (off by default)")
653
+ .action(async (idOrFile, opts) => {
654
+ try {
655
+ const { readFileSync } = await import("node:fs");
656
+ const { mkdir, writeFile } = await import("node:fs/promises");
657
+ const { join } = await import("node:path");
658
+ const { materializeWorkflowConstantAssets } = await import("@nodetool-ai/workflow-services");
659
+ let name = "";
660
+ let description = "";
661
+ let tags = [];
662
+ let graph;
663
+ const isFile = idOrFile.endsWith(".json") ||
664
+ idOrFile.includes("/") ||
665
+ idOrFile.includes("\\");
666
+ if (isFile) {
667
+ const raw = JSON.parse(readFileSync(idOrFile, "utf8"));
668
+ name = isString(raw.name) ? raw.name : "workflow";
669
+ description = isString(raw.description) ? raw.description : "";
670
+ tags = Array.isArray(raw.tags)
671
+ ? raw.tags.filter((t) => typeof t === "string")
672
+ : [];
673
+ graph = (raw.graph ?? raw);
674
+ }
675
+ else if (opts.apiUrl) {
676
+ const client = createApiClient(opts.apiUrl);
677
+ const wf = await client.workflows.get.query({
678
+ id: idOrFile
679
+ });
680
+ name = isString(wf.name) ? wf.name : "workflow";
681
+ description = isString(wf.description) ? wf.description : "";
682
+ tags = Array.isArray(wf.tags)
683
+ ? wf.tags.filter((t) => typeof t === "string")
684
+ : [];
685
+ graph = wf.graph;
686
+ }
687
+ else {
688
+ ensureDb();
689
+ const wf = await Workflow.find(LOCAL_USER_ID, idOrFile);
690
+ if (!wf)
691
+ throw new Error(`Workflow not found: ${idOrFile}`);
692
+ name = isString(wf.name) ? wf.name : "workflow";
693
+ description = isString(wf.description) ? wf.description : "";
694
+ tags = Array.isArray(wf.tags)
695
+ ? wf.tags.filter((t) => typeof t === "string")
696
+ : [];
697
+ graph = wf.getGraph();
698
+ }
699
+ if (!graph?.nodes) {
700
+ throw new Error("Workflow has no graph to export");
701
+ }
702
+ const repoRoot = resolve(__dirname, "..", "..", "..");
703
+ const baseNodesDir = join(repoRoot, "packages", "base-nodes", "nodetool");
704
+ const assetsRoot = opts.assetsDir ?? join(baseNodesDir, "assets");
705
+ const examplesDir = opts.examplesDir ?? join(baseNodesDir, "examples");
706
+ const fetchAssetBytes = opts.apiUrl
707
+ ? makeAssetFetcher(trimTrailingSlashes(opts.apiUrl))
708
+ : await makeLocalAssetFetcher();
709
+ const materializeOptions = {
710
+ packageName: opts.package,
711
+ assetsRoot,
712
+ fetchAssetBytes
713
+ };
714
+ if (opts.includeRemote) {
715
+ materializeOptions.includeRemote = true;
716
+ }
717
+ const result = await materializeWorkflowConstantAssets(graph, materializeOptions);
718
+ const slug = trimHyphens(name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
719
+ const example = {
720
+ id: slug || "workflow",
721
+ access: "public",
722
+ name,
723
+ description,
724
+ tags,
725
+ package_name: opts.package,
726
+ graph: result.graph
727
+ };
728
+ // Sanitize the on-disk filename separately from the display name so
729
+ // names with path separators can't escape the examples directory.
730
+ const safeFileName = name.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._]+/, "") ||
731
+ "workflow";
732
+ const outPath = opts.output ??
733
+ join(examplesDir, opts.package, `${safeFileName}.json`);
734
+ await mkdir(dirname(outPath), { recursive: true });
735
+ await writeFile(outPath, JSON.stringify(example, null, 2) + "\n", "utf8");
736
+ for (const a of result.exported) {
737
+ console.error(` asset ${a.source} → ${a.packageUri} (${a.byteLength} bytes)`);
738
+ }
739
+ for (const s of result.skipped) {
740
+ console.error(` warning: could not resolve asset ${s} (left as-is)`);
741
+ }
742
+ console.log(outPath);
743
+ }
744
+ catch (e) {
745
+ process.exit(printCommandError(e));
746
+ }
747
+ });
748
+ workflows
749
+ .command("export-bundle <workflow_id_or_file...>")
750
+ .description("Export one or more workflows as a portable .nodetool bundle (zip): the " +
751
+ "graphs plus the bytes of every asset they reference, in a single file")
752
+ .option("--api-url <url>", "Fetch the workflow and asset bytes from a remote server instead of the local DB/assets", process.env["NODETOOL_API_URL"])
753
+ .option("-o, --output <file>", "Output path (default: <name>.nodetool)")
754
+ .option("--include-remote", "Also embed http(s) and local-file refs (off by default)")
755
+ .action(async (idsOrFiles, opts) => {
756
+ try {
757
+ const { readFileSync } = await import("node:fs");
758
+ const { writeFile } = await import("node:fs/promises");
759
+ const { packWorkflowsBundle } = await import("@nodetool-ai/workflow-services");
760
+ const toBundleWf = (raw) => ({
761
+ name: isString(raw.name) ? raw.name : "workflow",
762
+ description: isString(raw.description) ? raw.description : "",
763
+ tags: Array.isArray(raw.tags)
764
+ ? raw.tags.filter((t) => typeof t === "string")
765
+ : [],
766
+ run_mode: raw.run_mode ?? null,
767
+ settings: raw.settings ?? null,
768
+ graph: (raw.graph ?? raw)
769
+ });
770
+ const useServer = Boolean(opts.apiUrl);
771
+ const apiUrl = opts.apiUrl ? trimTrailingSlashes(opts.apiUrl) : "";
772
+ if (!useServer)
773
+ ensureDb();
774
+ const workflowsToPack = [];
775
+ for (const idOrFile of idsOrFiles) {
776
+ const isFile = idOrFile.endsWith(".json") ||
777
+ idOrFile.includes("/") ||
778
+ idOrFile.includes("\\");
779
+ let raw;
780
+ if (isFile) {
781
+ raw = JSON.parse(readFileSync(idOrFile, "utf8"));
782
+ }
783
+ else if (useServer) {
784
+ raw = await createApiClient(apiUrl).workflows.get.query({
785
+ id: idOrFile
786
+ });
787
+ }
788
+ else {
789
+ const found = await Workflow.find(LOCAL_USER_ID, idOrFile);
790
+ if (!found)
791
+ throw new Error(`Workflow not found: ${idOrFile}`);
792
+ raw = { ...found, graph: found.getGraph() };
793
+ }
794
+ const wf = toBundleWf(raw);
795
+ if (!wf.graph?.nodes) {
796
+ throw new Error(`Workflow '${idOrFile}' has no graph to export`);
797
+ }
798
+ workflowsToPack.push(wf);
799
+ }
800
+ const packOptions = {
801
+ workflows: workflowsToPack,
802
+ fetchAssetBytes: useServer
803
+ ? makeAssetFetcher(apiUrl)
804
+ : await makeLocalAssetFetcher(),
805
+ nodetoolVersion: cliVersion()
806
+ };
807
+ if (opts.includeRemote) {
808
+ packOptions.includeRemote = true;
809
+ }
810
+ const { bytes, manifest, skipped } = await packWorkflowsBundle(packOptions);
811
+ const defaultBase = workflowsToPack.length === 1
812
+ ? workflowsToPack[0].name.replace(/[^A-Za-z0-9._-]+/g, "_")
813
+ : `${workflowsToPack.length}-workflows`;
814
+ const outPath = opts.output ?? `${defaultBase}.nodetool`;
815
+ await writeFile(outPath, bytes);
816
+ for (const a of manifest.assets) {
817
+ console.error(` embedded ${a.file} (${a.bytes} bytes)`);
818
+ }
819
+ for (const s of skipped) {
820
+ console.error(` warning: could not resolve asset ${s} (left as-is)`);
821
+ }
822
+ console.error(` bundled ${manifest.workflows.length} workflow(s), ${manifest.assets.length} asset(s)`);
823
+ console.log(outPath);
824
+ }
825
+ catch (e) {
826
+ process.exit(printCommandError(e));
827
+ }
828
+ });
829
+ workflows
830
+ .command("import-bundle <bundle_file>")
831
+ .description("Import a .nodetool bundle into the local library: store its assets and " +
832
+ "create the workflows with refs rewritten to the imported assets")
833
+ .option("--json", "Output the created workflows as JSON")
834
+ .action(async (bundleFile, opts) => {
835
+ try {
836
+ const { readFileSync } = await import("node:fs");
837
+ const { randomUUID } = await import("node:crypto");
838
+ const { extname } = await import("node:path");
839
+ const { importWorkflowBundle } = await import("@nodetool-ai/workflow-services");
840
+ await setupDb();
841
+ const storage = new FileStorageAdapter(getDefaultAssetsPath());
842
+ const zip = new Uint8Array(readFileSync(bundleFile));
843
+ const result = await importWorkflowBundle(zip, {
844
+ storeAsset: async ({ bytes, fileName, contentType }) => {
845
+ const id = randomUUID();
846
+ const key = `${id}${extname(fileName)}`;
847
+ await storage.store(key, bytes, contentType);
848
+ return { uri: `asset://${key}`, assetId: id };
849
+ }
850
+ });
851
+ const created = [];
852
+ for (const wf of result.workflows) {
853
+ created.push(await Workflow.create({
854
+ user_id: "1",
855
+ name: wf.name,
856
+ description: wf.description ?? "",
857
+ tags: wf.tags ?? [],
858
+ access: "private",
859
+ graph: wf.graph,
860
+ run_mode: wf.run_mode ?? "workflow",
861
+ settings: wf.settings ?? null
862
+ }));
863
+ }
864
+ for (const s of result.missing) {
865
+ console.error(` warning: bundle asset ${s} was missing from the archive`);
866
+ }
867
+ for (const m of result.checksumMismatches) {
868
+ console.error(` warning: ${m}`);
869
+ }
870
+ console.error(` imported ${created.length} workflow(s) and ${result.imported.length} asset(s) into local storage`);
871
+ if (opts.json) {
872
+ asJson(created);
873
+ }
874
+ else {
875
+ for (const c of created) {
876
+ console.log(String(c.id));
877
+ }
878
+ }
879
+ }
880
+ catch (e) {
881
+ process.exit(printCommandError(e, opts.json));
882
+ }
883
+ });
884
+ // ---------------------------------------------------------------------------
885
+ // jobs / assets — the read commands are registered from commands/resource-read.ts
886
+ // ---------------------------------------------------------------------------
887
+ const jobs = program.command("jobs").description("Job management");
888
+ const assets = program.command("assets").description("Asset management");
889
+ registerResourceReadCommands({ workflows, jobs, assets }, { ensureDb, localUserId: LOCAL_USER_ID });
890
+ // ---------------------------------------------------------------------------
891
+ // models
892
+ // ---------------------------------------------------------------------------
893
+ const secrets = program
894
+ .command("secrets")
895
+ .description("Manage encrypted secrets");
896
+ secrets
897
+ .command("list")
898
+ .description("List stored secret keys (values are never shown)")
899
+ .option("--user-id <id>", "User ID", "1")
900
+ .action(async (opts) => {
901
+ await setupDb();
902
+ try {
903
+ const [items] = await Secret.listForUser(opts.userId, 100);
904
+ if (items.length === 0) {
905
+ console.log("No secrets stored.");
906
+ return;
907
+ }
908
+ printTable(items.map((s) => ({ key: s.key, updated_at: s.updated_at ?? "" })));
909
+ }
910
+ catch (e) {
911
+ process.exit(printCommandError(e));
912
+ }
913
+ });
914
+ secrets
915
+ .command("store <key>")
916
+ .description("Store or update a secret (prompts for value)")
917
+ .option("--user-id <id>", "User ID", "1")
918
+ .option("--description <desc>", "Optional description")
919
+ .action(async (key, opts) => {
920
+ await setupDb();
921
+ try {
922
+ // Hidden on a TTY: an echoed value stays in the terminal scrollback.
923
+ const value = await readSecretValue(`Enter value for '${key}': `);
924
+ if (!value)
925
+ throw new Error(`No value given for '${key}'; nothing stored.`);
926
+ await Secret.upsert({
927
+ userId: opts.userId,
928
+ key,
929
+ value,
930
+ description: opts.description
931
+ });
932
+ console.log(`Secret '${key}' stored.`);
933
+ }
934
+ catch (e) {
935
+ process.exit(printCommandError(e));
936
+ }
937
+ });
938
+ secrets
939
+ .command("get <key>")
940
+ .description("Retrieve and print a secret value")
941
+ .option("--user-id <id>", "User ID", "1")
942
+ .action(async (key, opts) => {
943
+ await setupDb();
944
+ try {
945
+ const value = await getSecret(key, opts.userId);
946
+ if (value == null) {
947
+ console.error(`Secret '${key}' not found.`);
948
+ process.exit(1);
949
+ }
950
+ console.log(value);
951
+ }
952
+ catch (e) {
953
+ process.exit(printCommandError(e));
954
+ }
955
+ });
956
+ // ---------------------------------------------------------------------------
957
+ // settings
958
+ // ---------------------------------------------------------------------------
959
+ const settings = program
960
+ .command("settings")
961
+ .description("View configuration settings");
962
+ settings
963
+ .command("show")
964
+ .description("Show current settings from environment")
965
+ .option("--json", "Output as JSON")
966
+ .action((opts) => {
967
+ const vars = [
968
+ "ENV",
969
+ "LOG_LEVEL",
970
+ "PORT",
971
+ "HOST",
972
+ "DB_PATH",
973
+ "NODETOOL_API_URL",
974
+ "ANTHROPIC_API_KEY",
975
+ "OPENAI_API_KEY",
976
+ "GEMINI_API_KEY",
977
+ "MISTRAL_API_KEY",
978
+ "GROQ_API_KEY",
979
+ "OLLAMA_API_URL",
980
+ "SERPAPI_API_KEY",
981
+ "HF_TOKEN",
982
+ "VECTORSTORE_DB_PATH",
983
+ "ASSET_BUCKET",
984
+ "S3_ENDPOINT_URL"
985
+ ];
986
+ const data = Object.fromEntries(vars.map((k) => [
987
+ k,
988
+ process.env[k]
989
+ ? k.endsWith("KEY") || k.endsWith("TOKEN")
990
+ ? "***"
991
+ : process.env[k]
992
+ : ""
993
+ ]));
994
+ if (opts.json) {
995
+ asJson(data);
996
+ return;
997
+ }
998
+ printTable(Object.entries(data).map(([variable, value]) => ({ variable, value })));
999
+ });
1000
+ // ---------------------------------------------------------------------------
1001
+ // mcp
1002
+ registerProjectCommands(program);
1003
+ registerDebugCommands(program);
1004
+ registerValidateCommand(program);
1005
+ registerNodeCommands(program);
1006
+ registerGenerateCommand(program);
1007
+ registerCollectionCommands(program);
1008
+ registerCostsCommands(program);
1009
+ registerErrorsCommands(program);
1010
+ registerGenerationsCommands(program);
1011
+ registerRunsCommands(program, {
1012
+ ensureDb: setupDb,
1013
+ localUserId: LOCAL_USER_ID
1014
+ });
1015
+ return program;
1016
+ }
1017
+ export async function run(argv) {
1018
+ initializeCliRuntime(import.meta.url);
1019
+ configureCliPackHost(import.meta.url);
1020
+ await installLocalModelInterfaces();
1021
+ await (await createProgram()).parseAsync([...argv], { from: "user" });
1022
+ }
1023
+ export const feature = {
1024
+ descriptor: {
1025
+ id: "cli-workflow",
1026
+ apiVersion: 1,
1027
+ requires: ["core", "text"],
1028
+ capabilities: ["cli:workflow"]
1029
+ },
1030
+ run,
1031
+ resolveWorkflowPacks
1032
+ };
1033
+ export default feature;
1034
+ //# sourceMappingURL=index.js.map