@nodetool-ai/cli 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/.nodetool-build-stamp +1 -1
- package/dist/affected/affected.d.ts +11 -13
- package/dist/affected/affected.d.ts.map +1 -1
- package/dist/affected/affected.js +51 -2
- package/dist/affected/affected.js.map +1 -1
- package/dist/app-debug/app-target.d.ts +1 -1
- package/dist/app-debug/app-target.d.ts.map +1 -1
- package/dist/app-debug/harness.d.ts +3 -1
- package/dist/app-debug/harness.d.ts.map +1 -1
- package/dist/app-debug/harness.js +23 -0
- package/dist/app-debug/harness.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +2 -1
- package/dist/app.js.map +1 -1
- package/dist/chat-codeact.d.ts +8 -0
- package/dist/chat-codeact.d.ts.map +1 -1
- package/dist/chat-codeact.js +59 -21
- package/dist/chat-codeact.js.map +1 -1
- package/dist/chat-context.d.ts.map +1 -1
- package/dist/chat-context.js +7 -2
- package/dist/chat-context.js.map +1 -1
- package/dist/child-exit.d.ts +3 -0
- package/dist/child-exit.d.ts.map +1 -0
- package/dist/child-exit.js +17 -0
- package/dist/child-exit.js.map +1 -0
- package/dist/commands/__tests__/timeline-render-audio.test.js +1 -1
- package/dist/commands/__tests__/timeline-render-audio.test.js.map +1 -1
- package/dist/commands/__tests__/timeline-render-completeness.test.d.ts +2 -0
- package/dist/commands/__tests__/timeline-render-completeness.test.d.ts.map +1 -0
- package/dist/commands/__tests__/timeline-render-completeness.test.js +117 -0
- package/dist/commands/__tests__/timeline-render-completeness.test.js.map +1 -0
- package/dist/commands/__tests__/timeline-score.test.d.ts +2 -0
- package/dist/commands/__tests__/timeline-score.test.d.ts.map +1 -0
- package/dist/commands/__tests__/timeline-score.test.js +100 -0
- package/dist/commands/__tests__/timeline-score.test.js.map +1 -0
- package/dist/commands/affected.d.ts.map +1 -1
- package/dist/commands/affected.js +10 -5
- package/dist/commands/affected.js.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +12 -2
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/app.d.ts.map +1 -1
- package/dist/commands/app.js +29 -5
- package/dist/commands/app.js.map +1 -1
- package/dist/commands/apps.d.ts.map +1 -1
- package/dist/commands/apps.js +126 -32
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/debug-summary-output.d.ts +22 -0
- package/dist/commands/debug-summary-output.d.ts.map +1 -0
- package/dist/commands/debug-summary-output.js +28 -0
- package/dist/commands/debug-summary-output.js.map +1 -0
- package/dist/commands/deploy-helpers.d.ts +2 -1
- package/dist/commands/deploy-helpers.d.ts.map +1 -1
- package/dist/commands/errors.d.ts +11 -0
- package/dist/commands/errors.d.ts.map +1 -0
- package/dist/commands/errors.js +177 -0
- package/dist/commands/errors.js.map +1 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +4 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/extension.d.ts +12 -0
- package/dist/commands/extension.d.ts.map +1 -0
- package/dist/commands/extension.js +30 -0
- package/dist/commands/extension.js.map +1 -0
- package/dist/commands/game-benchmark.d.ts +34 -0
- package/dist/commands/game-benchmark.d.ts.map +1 -0
- package/dist/commands/game-benchmark.js +95 -0
- package/dist/commands/game-benchmark.js.map +1 -0
- package/dist/commands/game-smoke.d.ts +3 -0
- package/dist/commands/game-smoke.d.ts.map +1 -0
- package/dist/commands/game-smoke.js +23 -0
- package/dist/commands/game-smoke.js.map +1 -0
- package/dist/commands/game.d.ts.map +1 -1
- package/dist/commands/game.js +291 -20
- package/dist/commands/game.js.map +1 -1
- package/dist/commands/harness.d.ts +50 -0
- package/dist/commands/harness.d.ts.map +1 -1
- package/dist/commands/harness.js +289 -73
- package/dist/commands/harness.js.map +1 -1
- package/dist/commands/js-script.d.ts.map +1 -1
- package/dist/commands/js-script.js +6 -22
- package/dist/commands/js-script.js.map +1 -1
- package/dist/commands/mcp.d.ts +21 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +166 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/output.d.ts +8 -0
- package/dist/commands/output.d.ts.map +1 -1
- package/dist/commands/output.js +18 -0
- package/dist/commands/output.js.map +1 -1
- package/dist/commands/package.d.ts.map +1 -1
- package/dist/commands/package.js +12 -12
- package/dist/commands/package.js.map +1 -1
- package/dist/commands/resource-read.d.ts.map +1 -1
- package/dist/commands/resource-read.js +5 -1
- package/dist/commands/resource-read.js.map +1 -1
- package/dist/commands/runs.d.ts +7 -0
- package/dist/commands/runs.d.ts.map +1 -0
- package/dist/commands/runs.js +302 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/sketch.d.ts.map +1 -1
- package/dist/commands/sketch.js +6 -22
- package/dist/commands/sketch.js.map +1 -1
- package/dist/commands/timeline-render.d.ts +6 -1
- package/dist/commands/timeline-render.d.ts.map +1 -1
- package/dist/commands/timeline-render.js +15 -25
- package/dist/commands/timeline-render.js.map +1 -1
- package/dist/commands/timeline-score.d.ts +16 -0
- package/dist/commands/timeline-score.d.ts.map +1 -0
- package/dist/commands/timeline-score.js +90 -0
- package/dist/commands/timeline-score.js.map +1 -0
- package/dist/commands/timeline.d.ts.map +1 -1
- package/dist/commands/timeline.js +8 -22
- package/dist/commands/timeline.js.map +1 -1
- package/dist/commands/version-commands.d.ts +2 -5
- package/dist/commands/version-commands.d.ts.map +1 -1
- package/dist/commands/version-commands.js.map +1 -1
- package/dist/commands/worker.js +1 -1
- package/dist/commands/worker.js.map +1 -1
- package/dist/debug/server-runner.d.ts +2 -0
- package/dist/debug/server-runner.d.ts.map +1 -1
- package/dist/debug/server-runner.js +45 -4
- package/dist/debug/server-runner.js.map +1 -1
- package/dist/debug-bundle.d.ts +12 -0
- package/dist/debug-bundle.d.ts.map +1 -0
- package/dist/debug-bundle.js +23 -0
- package/dist/debug-bundle.js.map +1 -0
- package/dist/document-json.d.ts +11 -0
- package/dist/document-json.d.ts.map +1 -0
- package/dist/document-json.js +27 -0
- package/dist/document-json.js.map +1 -0
- package/dist/harness/capability-coverage.d.ts +1 -1
- package/dist/harness/capability-coverage.d.ts.map +1 -1
- package/dist/harness/capability-coverage.js +1 -1
- package/dist/harness/capability-coverage.js.map +1 -1
- package/dist/harness/capability-table.d.ts.map +1 -1
- package/dist/harness/capability-table.js +365 -35
- package/dist/harness/capability-table.js.map +1 -1
- package/dist/harness/changed-files.d.ts +2 -0
- package/dist/harness/changed-files.d.ts.map +1 -1
- package/dist/harness/changed-files.js +14 -0
- package/dist/harness/changed-files.js.map +1 -1
- package/dist/harness/registry.d.ts +32 -4
- package/dist/harness/registry.d.ts.map +1 -1
- package/dist/harness/registry.js +377 -33
- package/dist/harness/registry.js.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/interaction-script.d.ts +23 -0
- package/dist/interaction-script.d.ts.map +1 -1
- package/dist/interaction-script.js +50 -1
- package/dist/interaction-script.js.map +1 -1
- package/dist/js-script-debug/harness.d.ts +29 -1
- package/dist/js-script-debug/harness.d.ts.map +1 -1
- package/dist/js-script-debug/harness.js +29 -72
- package/dist/js-script-debug/harness.js.map +1 -1
- package/dist/js-script-debug/target.d.ts.map +1 -1
- package/dist/js-script-debug/target.js +1 -24
- package/dist/js-script-debug/target.js.map +1 -1
- package/dist/local-model-interfaces.d.ts.map +1 -1
- package/dist/local-model-interfaces.js +1 -24
- package/dist/local-model-interfaces.js.map +1 -1
- package/dist/node-registry.d.ts +14 -0
- package/dist/node-registry.d.ts.map +1 -1
- package/dist/node-registry.js +15 -18
- package/dist/node-registry.js.map +1 -1
- package/dist/nodetool-main.d.ts +15 -0
- package/dist/nodetool-main.d.ts.map +1 -0
- package/dist/nodetool-main.js +1411 -0
- package/dist/nodetool-main.js.map +1 -0
- package/dist/nodetool.d.ts +0 -13
- package/dist/nodetool.d.ts.map +1 -1
- package/dist/nodetool.js +17 -1683
- package/dist/nodetool.js.map +1 -1
- package/dist/package-assets.d.ts +8 -0
- package/dist/package-assets.d.ts.map +1 -0
- package/dist/package-assets.js +21 -0
- package/dist/package-assets.js.map +1 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.d.ts.map +1 -1
- package/dist/providers.js +15 -1
- package/dist/providers.js.map +1 -1
- package/dist/run-dsl.d.ts +6 -0
- package/dist/run-dsl.d.ts.map +1 -1
- package/dist/run-dsl.js +4 -1
- package/dist/run-dsl.js.map +1 -1
- package/dist/sandbox-catalog.d.ts +15 -1
- package/dist/sandbox-catalog.d.ts.map +1 -1
- package/dist/sandbox-catalog.js +58 -2
- package/dist/sandbox-catalog.js.map +1 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +2 -0
- package/dist/settings.js.map +1 -1
- package/dist/sketch-debug/harness.d.ts +14 -0
- package/dist/sketch-debug/harness.d.ts.map +1 -1
- package/dist/sketch-debug/harness.js +11 -66
- package/dist/sketch-debug/harness.js.map +1 -1
- package/dist/sketch-debug/target.d.ts.map +1 -1
- package/dist/sketch-debug/target.js +1 -24
- package/dist/sketch-debug/target.js.map +1 -1
- package/dist/stdin.d.ts +6 -1
- package/dist/stdin.d.ts.map +1 -1
- package/dist/stdin.js +10 -1
- package/dist/stdin.js.map +1 -1
- package/dist/timeline-debug/harness.d.ts +14 -0
- package/dist/timeline-debug/harness.d.ts.map +1 -1
- package/dist/timeline-debug/harness.js +11 -66
- package/dist/timeline-debug/harness.js.map +1 -1
- package/dist/timeline-debug/target.d.ts.map +1 -1
- package/dist/timeline-debug/target.js +1 -24
- package/dist/timeline-debug/target.js.map +1 -1
- package/package.json +39 -35
package/dist/nodetool.js
CHANGED
|
@@ -1,1690 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* nodetool —
|
|
3
|
+
* nodetool — CLI entry.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* nodetool secrets list/store <key>
|
|
13
|
-
* nodetool settings show
|
|
5
|
+
* The full command tree lives in ./nodetool-main.ts and statically imports
|
|
6
|
+
* every workspace package, so it fails with ERR_MODULE_NOT_FOUND on a tree
|
|
7
|
+
* whose `dist/` has not been built. `harness` (list, audit, gate planning)
|
|
8
|
+
* is the tool that tells you what to build and check, so it must work first:
|
|
9
|
+
* it is routed to a program holding only that command, and its commands import
|
|
10
|
+
* built packages lazily (capabilities) or not at all. Every other command goes
|
|
11
|
+
* to the full tree unchanged.
|
|
14
12
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import { workflowToDsl } from "@nodetool-ai/dsl";
|
|
22
|
-
import { initDb, Workflow, Asset, Secret, getSecret } from "@nodetool-ai/models";
|
|
23
|
-
import { readCachedHfModels, searchCachedHfModels } from "@nodetool-ai/huggingface";
|
|
24
|
-
import { initMasterKey } from "@nodetool-ai/security";
|
|
25
|
-
import { installLocalModelInterfaces } from "./local-model-interfaces.js";
|
|
26
|
-
import { getDefaultDbPath, getDefaultAssetsPath } from "@nodetool-ai/config";
|
|
27
|
-
import { ExecutionSession, isExecutionPreflightError } from "@nodetool-ai/execution";
|
|
28
|
-
import { NodeRegistry, createGraphNodeTypeResolver } from "@nodetool-ai/node-sdk";
|
|
29
|
-
import { registerBaseNodes } from "@nodetool-ai/base-nodes";
|
|
30
|
-
import { registerElevenLabsNodes } from "@nodetool-ai/elevenlabs-nodes";
|
|
31
|
-
import { registerMinimaxNodes } from "@nodetool-ai/minimax-nodes";
|
|
32
|
-
import { registerTransformersJsNodes } from "@nodetool-ai/transformers-js-nodes";
|
|
33
|
-
import { registerFalNodes } from "@nodetool-ai/fal-nodes";
|
|
34
|
-
import { registerReplicateNodes } from "@nodetool-ai/replicate-nodes";
|
|
35
|
-
import { registerReveNodes } from "@nodetool-ai/reve-nodes";
|
|
36
|
-
import { registerHuggingFaceNodes } from "@nodetool-ai/huggingface-nodes";
|
|
37
|
-
import { ProcessingContext, createLocalWorkspace, initTelemetry, onPythonBridgeCreated } from "@nodetool-ai/runtime";
|
|
38
|
-
import { FileStorageAdapter } from "@nodetool-ai/storage";
|
|
39
|
-
import { mkdirSync } from "node:fs";
|
|
40
|
-
import { addSupervisorOptions, createSupervisorHandle, parseSupervisorFlags, printSupervisedSummary, recordSupervisorCost, streamInterventionLines } from "./supervisor.js";
|
|
41
|
-
import { registerPackageCommands } from "./commands/package.js";
|
|
42
|
-
import { registerPackCommands } from "./commands/packs.js";
|
|
43
|
-
import { registerDeployCommands } from "./commands/deploy.js";
|
|
44
|
-
import { registerHfCommands } from "./commands/models-hf.js";
|
|
45
|
-
import { registerWorkerCommands } from "./commands/worker.js";
|
|
46
|
-
import { installWorkerActivityHeartbeat } from "@nodetool-ai/compute";
|
|
47
|
-
import { registerRecommendedCommand } from "./commands/models-recommended.js";
|
|
48
|
-
import { registerAgentCommands } from "./commands/agent.js";
|
|
49
|
-
import { registerDbCommands } from "./commands/db.js";
|
|
50
|
-
import { registerDebugCommands } from "./commands/debug.js";
|
|
51
|
-
import { registerAppCommands } from "./commands/app.js";
|
|
52
|
-
import { registerAppsCommands } from "./commands/apps.js";
|
|
53
|
-
import { registerValidateCommand } from "./commands/validate.js";
|
|
54
|
-
import { registerTimelineCommands } from "./commands/timeline.js";
|
|
55
|
-
import { registerSketchCommands } from "./commands/sketch.js";
|
|
56
|
-
import { registerJsScriptCommands } from "./commands/js-script.js";
|
|
57
|
-
import { registerReliabilityCommands } from "./commands/reliability.js";
|
|
58
|
-
import { registerNodeCommands } from "./commands/node.js";
|
|
59
|
-
import { registerGenerateCommand } from "./commands/generate.js";
|
|
60
|
-
import { registerEvalCommand } from "./commands/eval.js";
|
|
61
|
-
import { registerJtbdCommand } from "./commands/jtbd.js";
|
|
62
|
-
import { registerAffectedCommand } from "./commands/affected.js";
|
|
63
|
-
import { registerHarnessCommands } from "./commands/harness.js";
|
|
64
|
-
import { registerCollectionCommands } from "./commands/collections.js";
|
|
65
|
-
import { registerCostsCommands } from "./commands/costs.js";
|
|
66
|
-
import { registerGenerationsCommands } from "./commands/generations.js";
|
|
67
|
-
import { registerGameCommands } from "./commands/game.js";
|
|
68
|
-
import { registerAuthCommands } from "./commands/auth.js";
|
|
69
|
-
import { registerTelegramCommands } from "./commands/telegram.js";
|
|
70
|
-
import { registerResourceReadCommands } from "./commands/resource-read.js";
|
|
71
|
-
import { createApiClient } from "./api-client.js";
|
|
72
|
-
import { listAllModels, listConfiguredProviderInfo, listOllamaModels, listProviderModels } from "./providers.js";
|
|
73
|
-
import { isString } from "./predicates.js";
|
|
74
|
-
import { printTable, asJson } from "./commands/output.js";
|
|
75
|
-
import { printCommandError } from "./command-errors.js";
|
|
76
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
77
|
-
const __dirname = dirname(__filename);
|
|
78
|
-
/**
|
|
79
|
-
* The CLI's own version, read from its package.json rather than repeated here.
|
|
80
|
-
* A literal drifts silently: it sat at "0.1.0" across every 0.7.0-rc release,
|
|
81
|
-
* so `nodetool --version` told users nothing about what they had installed.
|
|
82
|
-
*/
|
|
83
|
-
const CLI_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// DB setup (for secrets commands)
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// The local single-user id every direct-DB command runs as, matching the
|
|
88
|
-
// `workflows run` / `secrets` commands and the stdio MCP server.
|
|
89
|
-
import { resolveLocalSecret } from "./local-secrets.js";
|
|
90
|
-
const LOCAL_USER_ID = "1";
|
|
91
|
-
// Open the local SQLite database for a read-only command. Unlike setupDb() this
|
|
92
|
-
// skips master-key unlocking (no secret decryption needed to list/read rows),
|
|
93
|
-
// so it never emits a keychain warning. Idempotent — initDb() is a no-op once
|
|
94
|
-
// a connection is open.
|
|
95
|
-
function ensureDb() {
|
|
96
|
-
initDb(getDefaultDbPath());
|
|
97
|
-
}
|
|
98
|
-
async function setupDb() {
|
|
99
|
-
ensureDb();
|
|
100
|
-
// Resolve the master encryption key from keychain / env / AWS so that
|
|
101
|
-
// both encryption (Secret.upsert) and decryption (Secret.getDecryptedValue)
|
|
102
|
-
// use the same persistent key. Without this, a fresh process would
|
|
103
|
-
// auto-generate a one-shot key inside `getMasterKey()`, encrypt with it,
|
|
104
|
-
// then lose it on exit — and the next launch could not decrypt.
|
|
105
|
-
try {
|
|
106
|
-
await initMasterKey();
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
-
process.stderr.write(`Could not unlock the secret store: ${msg}\n` +
|
|
111
|
-
`Tip: set SECRETS_MASTER_KEY or grant keychain access.\n`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
// API clients
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// REST GET (text) — used for the DSL export endpoint which stays REST
|
|
118
|
-
// because it returns a TypeScript source string rather than JSON.
|
|
119
|
-
async function apiGetText(apiUrl, path) {
|
|
120
|
-
const res = await fetch(`${apiUrl}${path}`);
|
|
121
|
-
if (!res.ok)
|
|
122
|
-
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
|
123
|
-
return res.text();
|
|
124
|
-
}
|
|
125
|
-
// This CLI's package version, stamped into exported bundle manifests.
|
|
126
|
-
function cliVersion() {
|
|
127
|
-
try {
|
|
128
|
-
const req = createRequire(import.meta.url);
|
|
129
|
-
return req("../package.json").version;
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
// Version is best-effort metadata; omit it if the file can't be read.
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Download asset bytes from a running server, used by the export commands to
|
|
137
|
-
// resolve `asset://` / `/api/storage/` (and optionally http) refs into bytes.
|
|
138
|
-
function makeAssetFetcher(apiUrl) {
|
|
139
|
-
return async (ref) => {
|
|
140
|
-
let url = null;
|
|
141
|
-
if (ref.startsWith("asset://")) {
|
|
142
|
-
const id = ref.slice("asset://".length);
|
|
143
|
-
const bareId = id.replace(/\.[^.]+$/, "");
|
|
144
|
-
try {
|
|
145
|
-
const meta = await fetch(`${apiUrl}/api/assets/${encodeURIComponent(bareId)}`);
|
|
146
|
-
if (meta.ok) {
|
|
147
|
-
const j = (await meta.json());
|
|
148
|
-
if (j.get_url) {
|
|
149
|
-
url = j.get_url.startsWith("/")
|
|
150
|
-
? `${apiUrl}${j.get_url}`
|
|
151
|
-
: j.get_url;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
// Metadata lookup failed — fall back to the storage route below.
|
|
157
|
-
}
|
|
158
|
-
url ??= `${apiUrl}/api/storage/${encodeURIComponent(id)}`;
|
|
159
|
-
}
|
|
160
|
-
else if (ref.includes("/api/storage/")) {
|
|
161
|
-
url = /^https?:\/\//.test(ref)
|
|
162
|
-
? ref
|
|
163
|
-
: `${apiUrl}${ref.startsWith("/") ? ref : `/${ref}`}`;
|
|
164
|
-
}
|
|
165
|
-
else if (/^https?:\/\//.test(ref)) {
|
|
166
|
-
url = ref;
|
|
167
|
-
}
|
|
168
|
-
if (!url)
|
|
169
|
-
return null;
|
|
170
|
-
const res = await fetch(url);
|
|
171
|
-
if (!res.ok)
|
|
172
|
-
return null;
|
|
173
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
// Resolve asset bytes from the local assets directory, so the export commands
|
|
177
|
-
// work by id without a running server. `asset://<key>` is tried verbatim as a
|
|
178
|
-
// storage key first (that's how import-bundle writes refs), then via the DB
|
|
179
|
-
// record to derive `<id>.<ext>` from the content type. Remote http(s) refs
|
|
180
|
-
// still fetch over the network.
|
|
181
|
-
async function makeLocalAssetFetcher() {
|
|
182
|
-
const { getAssetFileName } = await import("@nodetool-ai/websocket");
|
|
183
|
-
const storage = new FileStorageAdapter(getDefaultAssetsPath());
|
|
184
|
-
return async (ref) => {
|
|
185
|
-
if (ref.startsWith("asset://")) {
|
|
186
|
-
const raw = ref.slice("asset://".length);
|
|
187
|
-
const direct = await storage.retrieve(`/api/storage/${raw}`);
|
|
188
|
-
if (direct)
|
|
189
|
-
return direct;
|
|
190
|
-
const bareId = raw.replace(/\.[^.]+$/, "");
|
|
191
|
-
const asset = await Asset.find(LOCAL_USER_ID, bareId);
|
|
192
|
-
if (!asset)
|
|
193
|
-
return null;
|
|
194
|
-
const key = getAssetFileName(asset.id, asset.content_type);
|
|
195
|
-
return storage.retrieve(`/api/storage/${key}`);
|
|
196
|
-
}
|
|
197
|
-
if (ref.includes("/api/storage/")) {
|
|
198
|
-
const local = await storage.retrieve(ref);
|
|
199
|
-
if (local)
|
|
200
|
-
return local;
|
|
201
|
-
}
|
|
202
|
-
if (/^https?:\/\//.test(ref)) {
|
|
203
|
-
const res = await fetch(ref);
|
|
204
|
-
if (!res.ok)
|
|
205
|
-
return null;
|
|
206
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
// Asset and timeline persistence for every context any command builds, here
|
|
212
|
-
// rather than per-entrance: `debug` wired none while `workflows run` wired
|
|
213
|
-
// four, so one workflow saved an image under one command and threw under the
|
|
214
|
-
// other. The interfaces touch the database only when a node calls them, so
|
|
215
|
-
// installing them before `setupDb()` is safe.
|
|
216
|
-
await installLocalModelInterfaces();
|
|
217
|
-
/**
|
|
218
|
-
* What to print when a run command fails. A preflight refusal already reads as
|
|
219
|
-
* a complete explanation — printing the error class in front of it only buries
|
|
220
|
-
* the secret name the user has to go set.
|
|
221
|
-
*/
|
|
222
|
-
function describeRunFailure(error) {
|
|
223
|
-
return isExecutionPreflightError(error) ? error.message : String(error);
|
|
224
|
-
}
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
// info
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
program
|
|
229
|
-
.name("nodetool")
|
|
230
|
-
.description("NodeTool CLI")
|
|
231
|
-
.version(CLI_VERSION)
|
|
232
|
-
.option("--trace-file <path>", "Append every LLM/agent/workflow span as JSONL to <path>")
|
|
233
|
-
.option("--trace-stdout [format]", "Stream spans to stdout: 'pretty' (default) or 'json' (JSONL)")
|
|
234
|
-
.option("--no-trace-stdout", "Disable stdout span output (overrides NODETOOL_TRACE_STDOUT)")
|
|
235
|
-
.hook("preAction", async (thisCommand, actionCommand) => {
|
|
236
|
-
// With `--trace`, the `debug` command owns telemetry: it routes spans into
|
|
237
|
-
// its bundle's trace.jsonl via its own initTelemetry call. Initializing here
|
|
238
|
-
// first would win the one-shot init and drop that file sink, so skip it.
|
|
239
|
-
// Without `--trace`, fall through so the global --trace-file/--trace-stdout
|
|
240
|
-
// flags still work for a debug run.
|
|
241
|
-
if (actionCommand?.name() === "debug" && actionCommand.opts()["trace"])
|
|
242
|
-
return;
|
|
243
|
-
const opts = thisCommand.opts();
|
|
244
|
-
await initTelemetry({
|
|
245
|
-
...(opts.traceFile && { traceFile: opts.traceFile }),
|
|
246
|
-
...(opts.traceStdout !== undefined && {
|
|
247
|
-
stdout: parseTraceStdout(opts.traceStdout)
|
|
248
|
-
})
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
function parseTraceStdout(v) {
|
|
252
|
-
if (v === false)
|
|
253
|
-
return false;
|
|
254
|
-
if (v === true)
|
|
255
|
-
return "pretty";
|
|
256
|
-
if (isString(v)) {
|
|
257
|
-
const lower = v.toLowerCase();
|
|
258
|
-
if (lower === "false" || lower === "0" || lower === "no")
|
|
259
|
-
return false;
|
|
260
|
-
if (lower === "json")
|
|
261
|
-
return "json";
|
|
262
|
-
if (lower === "pretty" || lower === "true" || lower === "1")
|
|
263
|
-
return "pretty";
|
|
264
|
-
}
|
|
265
|
-
throw new Error(`--trace-stdout must be 'pretty' or 'json' (got ${JSON.stringify(v)})`);
|
|
13
|
+
if (process.argv[2] === "harness") {
|
|
14
|
+
const { program } = await import("commander");
|
|
15
|
+
const { registerHarnessCommands } = await import("./commands/harness.js");
|
|
16
|
+
program.name("nodetool").description("NodeTool CLI");
|
|
17
|
+
registerHarnessCommands(program);
|
|
18
|
+
await program.parseAsync();
|
|
266
19
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
addSupervisorOptions(program
|
|
271
|
-
.command("run <dsl-file>")
|
|
272
|
-
.description("Run a TypeScript/JavaScript DSL workflow file")
|
|
273
|
-
.option("--json", "Output results as JSON (default: pretty-print)")).action(async (dslFile, opts) => {
|
|
274
|
-
try {
|
|
275
|
-
const { resolve } = await import("node:path");
|
|
276
|
-
const { runDslFile } = await import("./run-dsl.js");
|
|
277
|
-
const { registerBaseNodes } = await import("@nodetool-ai/base-nodes");
|
|
278
|
-
const { NodeRegistry } = await import("@nodetool-ai/node-sdk");
|
|
279
|
-
registerBaseNodes(NodeRegistry.global);
|
|
280
|
-
const supervisorConfig = parseSupervisorFlags(opts);
|
|
281
|
-
const absolutePath = resolve(dslFile);
|
|
282
|
-
let results;
|
|
283
|
-
let interventions = [];
|
|
284
|
-
if (supervisorConfig) {
|
|
285
|
-
// The supervisor's provider key comes from the secret store, and its
|
|
286
|
-
// spend is written to the local ledger — both need the DB.
|
|
287
|
-
await setupDb();
|
|
288
|
-
const { runDslFileSupervised } = await import("./run-dsl-supervised.js");
|
|
289
|
-
const run = await runDslFileSupervised(absolutePath, supervisorConfig, opts.json === true);
|
|
290
|
-
results = run.results;
|
|
291
|
-
interventions = run.interventions;
|
|
292
|
-
}
|
|
293
|
-
else {
|
|
294
|
-
results = await runDslFile(absolutePath);
|
|
295
|
-
}
|
|
296
|
-
const { printRunJson, previewJson } = await import("./run-json.js");
|
|
297
|
-
if (opts.json) {
|
|
298
|
-
// A supervised run reports what the supervisor did alongside the
|
|
299
|
-
// outputs; an unsupervised one keeps the bare results shape.
|
|
300
|
-
await printRunJson(supervisorConfig ? { results, interventions } : results);
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
for (const [workflowName, outputs] of Object.entries(results)) {
|
|
304
|
-
console.log(`\n${workflowName}:`);
|
|
305
|
-
for (const [key, value] of Object.entries(outputs)) {
|
|
306
|
-
console.log(` ${key}: ${previewJson(value)}`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
catch (e) {
|
|
312
|
-
console.error(describeRunFailure(e));
|
|
313
|
-
process.exit(1);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
program
|
|
317
|
-
.command("info")
|
|
318
|
-
.description("Display system and environment information")
|
|
319
|
-
.option("--json", "Output as JSON")
|
|
320
|
-
.action((opts) => {
|
|
321
|
-
const apiKeys = [
|
|
322
|
-
"ANTHROPIC_API_KEY",
|
|
323
|
-
"OPENAI_API_KEY",
|
|
324
|
-
"GEMINI_API_KEY",
|
|
325
|
-
"MISTRAL_API_KEY",
|
|
326
|
-
"GROQ_API_KEY",
|
|
327
|
-
"OLLAMA_API_URL",
|
|
328
|
-
"SERPAPI_API_KEY",
|
|
329
|
-
"HF_TOKEN"
|
|
330
|
-
];
|
|
331
|
-
const data = {
|
|
332
|
-
version: CLI_VERSION,
|
|
333
|
-
node_version: process.version,
|
|
334
|
-
platform: process.platform,
|
|
335
|
-
arch: process.arch,
|
|
336
|
-
api_keys: Object.fromEntries(apiKeys.map((k) => [k, process.env[k] ? "configured" : "not set"])),
|
|
337
|
-
environment: {
|
|
338
|
-
ENV: process.env["ENV"] ?? "development",
|
|
339
|
-
LOG_LEVEL: process.env["LOG_LEVEL"] ?? "INFO",
|
|
340
|
-
PORT: process.env["PORT"] ?? "7777"
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
if (opts.json) {
|
|
344
|
-
asJson(data);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
console.log("\nNodeTool System Information\n");
|
|
348
|
-
printTable([
|
|
349
|
-
{ property: "Version", value: data.version },
|
|
350
|
-
{ property: "Node.js", value: data.node_version },
|
|
351
|
-
{ property: "Platform", value: `${data.platform}/${data.arch}` }
|
|
352
|
-
]);
|
|
353
|
-
console.log("\nAPI Keys:");
|
|
354
|
-
printTable(Object.entries(data.api_keys).map(([k, v]) => ({ key: k, status: v })));
|
|
355
|
-
console.log("\nEnvironment:");
|
|
356
|
-
printTable(Object.entries(data.environment).map(([k, v]) => ({
|
|
357
|
-
variable: k,
|
|
358
|
-
value: v
|
|
359
|
-
})));
|
|
360
|
-
console.log();
|
|
361
|
-
});
|
|
362
|
-
// ---------------------------------------------------------------------------
|
|
363
|
-
// serve
|
|
364
|
-
// ---------------------------------------------------------------------------
|
|
365
|
-
program
|
|
366
|
-
.command("serve")
|
|
367
|
-
.description("Start the NodeTool WebSocket + HTTP server")
|
|
368
|
-
.option("--host <host>", "Host to bind", "127.0.0.1")
|
|
369
|
-
.option("--port <port>", "Port to listen on", "7777")
|
|
370
|
-
.action((opts) => {
|
|
371
|
-
const serverPath = resolve(__dirname, "../../websocket/dist/server.js");
|
|
372
|
-
process.env["HOST"] = opts.host;
|
|
373
|
-
process.env["PORT"] = opts.port;
|
|
374
|
-
const result = spawnSync("node", [serverPath], {
|
|
375
|
-
stdio: "inherit",
|
|
376
|
-
env: { ...process.env }
|
|
377
|
-
});
|
|
378
|
-
process.exit(result.status ?? 0);
|
|
379
|
-
});
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
// chat
|
|
382
|
-
// ---------------------------------------------------------------------------
|
|
383
|
-
program
|
|
384
|
-
.command("chat")
|
|
385
|
-
.description("Start fullscreen chat")
|
|
386
|
-
.helpOption(false)
|
|
387
|
-
.option("-p, --provider <provider>", "LLM provider")
|
|
388
|
-
.option("-m, --model <model>", "Model ID")
|
|
389
|
-
.option("-a, --agent", "Agent mode")
|
|
390
|
-
.option("-u, --url <url>", "WebSocket server URL")
|
|
391
|
-
.option("-w, --workspace <path>", "Workspace directory")
|
|
392
|
-
.option("--tools <tools>", "Comma-separated enabled tools")
|
|
393
|
-
.allowUnknownOption()
|
|
394
|
-
.action((_opts, cmd) => {
|
|
395
|
-
const chatPath = resolve(__dirname, import.meta.url.endsWith(".ts") ? "index.ts" : "index.js");
|
|
396
|
-
const args = cmd.args;
|
|
397
|
-
const rawArgs = process.argv;
|
|
398
|
-
// Re-run with the chat entry point, forwarding all flags after "chat"
|
|
399
|
-
const chatIdx = rawArgs.indexOf("chat");
|
|
400
|
-
const forwarded = chatIdx >= 0 ? rawArgs.slice(chatIdx + 1) : args;
|
|
401
|
-
const result = spawnSync(process.execPath, [...process.execArgv, chatPath, ...forwarded], {
|
|
402
|
-
stdio: "inherit",
|
|
403
|
-
env: { ...process.env }
|
|
404
|
-
});
|
|
405
|
-
process.exit(result.status ?? 1);
|
|
406
|
-
});
|
|
407
|
-
// ---------------------------------------------------------------------------
|
|
408
|
-
// workflows
|
|
409
|
-
// ---------------------------------------------------------------------------
|
|
410
|
-
const workflows = program
|
|
411
|
-
.command("workflows")
|
|
412
|
-
.description("Workflow management");
|
|
413
|
-
addSupervisorOptions(workflows
|
|
414
|
-
.command("run <workflow_id_or_file>")
|
|
415
|
-
.description("Run a workflow by ID, JSON file, or TypeScript DSL file")
|
|
416
|
-
.option("--params <json>", "JSON params string")
|
|
417
|
-
.option("--json", "Output as JSON")
|
|
418
|
-
.option("--asset-output-mode <mode>", "How media outputs are materialized: native | raw | data_uri | workspace | storage_url | temp_url", "native")
|
|
419
|
-
.option("--workspace <dir>", "Workspace directory for workspace-mode asset output (default: ./nodetool-output)")
|
|
420
|
-
.option("--trigger-event <json>", 'Wake a trigger node as the scheduler/webhook adapter would: \'{"node_id":"watch","payload":{"path":"/tmp/a.csv","event":"created"}}\'. ' +
|
|
421
|
-
"Without it a webhook, file-watch or manual trigger has no event to " +
|
|
422
|
-
"emit and the run stalls until its timeout — so this is the only way " +
|
|
423
|
-
"to exercise a trigger-driven workflow from the CLI.")).action(async (idOrFile, opts) => {
|
|
424
|
-
try {
|
|
425
|
-
const supervisorConfig = parseSupervisorFlags(opts);
|
|
426
|
-
await setupDb();
|
|
427
|
-
let graph;
|
|
428
|
-
let workflowId = null;
|
|
429
|
-
const params = opts.params
|
|
430
|
-
? JSON.parse(opts.params)
|
|
431
|
-
: {};
|
|
432
|
-
// A trigger node emits from `emitTriggerEvent`, not `genProcess`, and
|
|
433
|
-
// only when the run carries an event addressed to it. The kernel has
|
|
434
|
-
// always accepted one (`RunJobRequest.trigger_event`); nothing on the
|
|
435
|
-
// CLI could supply it, so a webhook or file-watch workflow could be
|
|
436
|
-
// validated but never executed. `input_id` defaults because every
|
|
437
|
-
// caller would otherwise invent the same throwaway id.
|
|
438
|
-
const triggerEvent = opts.triggerEvent
|
|
439
|
-
? (() => {
|
|
440
|
-
const parsed = JSON.parse(opts.triggerEvent);
|
|
441
|
-
if (!parsed.node_id) {
|
|
442
|
-
throw new Error('--trigger-event needs a "node_id" naming the trigger node to wake.');
|
|
443
|
-
}
|
|
444
|
-
return {
|
|
445
|
-
node_id: parsed.node_id,
|
|
446
|
-
payload: parsed.payload ?? {},
|
|
447
|
-
input_id: parsed.input_id ?? `cli-${Date.now()}`
|
|
448
|
-
};
|
|
449
|
-
})()
|
|
450
|
-
: null;
|
|
451
|
-
// Determine if argument is a file path or workflow ID
|
|
452
|
-
if (idOrFile.endsWith(".json") ||
|
|
453
|
-
idOrFile.endsWith(".ts") ||
|
|
454
|
-
idOrFile.endsWith(".tsx") ||
|
|
455
|
-
idOrFile.includes("/") ||
|
|
456
|
-
idOrFile.includes("\\")) {
|
|
457
|
-
/** A DSL/JSON workflow file: the graph, optionally wrapped. */
|
|
458
|
-
let raw;
|
|
459
|
-
if (idOrFile.endsWith(".ts") || idOrFile.endsWith(".tsx")) {
|
|
460
|
-
// Execute DSL file via tsx and capture JSON output
|
|
461
|
-
const { execFileSync } = await import("node:child_process");
|
|
462
|
-
const output = execFileSync("npx", ["tsx", resolve(idOrFile)], {
|
|
463
|
-
encoding: "utf8",
|
|
464
|
-
cwd: dirname(resolve(idOrFile)),
|
|
465
|
-
timeout: 30000
|
|
466
|
-
});
|
|
467
|
-
raw = JSON.parse(output.trim());
|
|
468
|
-
}
|
|
469
|
-
else {
|
|
470
|
-
const { readFileSync } = await import("node:fs");
|
|
471
|
-
raw = JSON.parse(readFileSync(idOrFile, "utf8"));
|
|
472
|
-
}
|
|
473
|
-
graph = raw.graph ?? raw;
|
|
474
|
-
workflowId = raw.workflow_id ?? raw.id ?? null;
|
|
475
|
-
if (raw.params)
|
|
476
|
-
Object.assign(params, raw.params);
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
const wf = await Workflow.get(idOrFile);
|
|
480
|
-
if (!wf)
|
|
481
|
-
throw new Error(`Workflow not found: ${idOrFile}`);
|
|
482
|
-
graph = wf.graph;
|
|
483
|
-
workflowId = idOrFile;
|
|
484
|
-
}
|
|
485
|
-
if (!graph?.nodes || !graph?.edges) {
|
|
486
|
-
throw new Error("Invalid workflow: missing nodes or edges");
|
|
487
|
-
}
|
|
488
|
-
// Editor-only nodes (Comment, Group, Reroute) and the data→properties
|
|
489
|
-
// rename are handled by `ExecutionSession.create` below, which runs
|
|
490
|
-
// the graph through `normalizeGraph` (@nodetool-ai/execution) — no
|
|
491
|
-
// need to duplicate that here.
|
|
492
|
-
const registry = new NodeRegistry();
|
|
493
|
-
registerBaseNodes(registry);
|
|
494
|
-
registerElevenLabsNodes(registry);
|
|
495
|
-
registerMinimaxNodes(registry);
|
|
496
|
-
registerTransformersJsNodes(registry);
|
|
497
|
-
registerFalNodes(registry);
|
|
498
|
-
registerReplicateNodes(registry);
|
|
499
|
-
registerReveNodes(registry);
|
|
500
|
-
registerHuggingFaceNodes(registry);
|
|
501
|
-
const jobId = `job-${Date.now()}`;
|
|
502
|
-
// Resolve asset URIs (e.g. /api/storage/<key>) against the local
|
|
503
|
-
// assets directory, so workflows referencing stored assets run the
|
|
504
|
-
// same decode path the server does.
|
|
505
|
-
// Apply the workflow's assigned workspace (if any) so file ops and
|
|
506
|
-
// workspace-driving nodes land in the same directory as a server run.
|
|
507
|
-
const { resolveWorkflowWorkspace } = await import("@nodetool-ai/websocket");
|
|
508
|
-
const assignedWorkspace = workflowId
|
|
509
|
-
? await resolveWorkflowWorkspace(workflowId, "1")
|
|
510
|
-
: null;
|
|
511
|
-
const ASSET_OUTPUT_MODES = [
|
|
512
|
-
"native",
|
|
513
|
-
"raw",
|
|
514
|
-
"data_uri",
|
|
515
|
-
"workspace",
|
|
516
|
-
"storage_url",
|
|
517
|
-
"temp_url"
|
|
518
|
-
];
|
|
519
|
-
const assetOutputMode = (opts.assetOutputMode ??
|
|
520
|
-
"native");
|
|
521
|
-
if (!ASSET_OUTPUT_MODES.includes(assetOutputMode)) {
|
|
522
|
-
throw new Error(`Unknown --asset-output-mode "${assetOutputMode}". ` +
|
|
523
|
-
`Expected one of: ${ASSET_OUTPUT_MODES.join(", ")}`);
|
|
524
|
-
}
|
|
525
|
-
// `--workspace <dir>` wins, then whatever the workflow is assigned.
|
|
526
|
-
// `workspace` asset mode writes each output into <workspace>/assets, so
|
|
527
|
-
// it needs somewhere to write even when the run has no saved workflow (a
|
|
528
|
-
// JSON or DSL file target). Fall back to ./nodetool-output so a bare
|
|
529
|
-
// `workflows run file.json --asset-output-mode workspace` produces files
|
|
530
|
-
// in the current directory rather than failing.
|
|
531
|
-
const overrideWorkspaceDir = opts.workspace ??
|
|
532
|
-
(assignedWorkspace
|
|
533
|
-
? null
|
|
534
|
-
: assetOutputMode === "workspace"
|
|
535
|
-
? resolve(process.cwd(), "nodetool-output")
|
|
536
|
-
: null);
|
|
537
|
-
if (overrideWorkspaceDir) {
|
|
538
|
-
mkdirSync(overrideWorkspaceDir, { recursive: true });
|
|
539
|
-
}
|
|
540
|
-
const runWorkspace = overrideWorkspaceDir
|
|
541
|
-
? createLocalWorkspace(overrideWorkspaceDir)
|
|
542
|
-
: assignedWorkspace;
|
|
543
|
-
const assetStorage = new FileStorageAdapter(getDefaultAssetsPath());
|
|
544
|
-
const context = new ProcessingContext({
|
|
545
|
-
jobId,
|
|
546
|
-
workflowId,
|
|
547
|
-
userId: "1",
|
|
548
|
-
secretResolver: resolveLocalSecret,
|
|
549
|
-
storage: assetStorage,
|
|
550
|
-
assetOutputMode,
|
|
551
|
-
workspace: runWorkspace
|
|
552
|
-
});
|
|
553
|
-
const supervisor = supervisorConfig
|
|
554
|
-
? await createSupervisorHandle({
|
|
555
|
-
config: supervisorConfig,
|
|
556
|
-
context
|
|
557
|
-
})
|
|
558
|
-
: null;
|
|
559
|
-
console.error(`Running workflow${workflowId ? ` ${workflowId}` : ""}` +
|
|
560
|
-
(supervisor
|
|
561
|
-
? ` supervised by ${supervisor.providerId}/${supervisor.model}`
|
|
562
|
-
: "") +
|
|
563
|
-
"...");
|
|
564
|
-
// ExecutionSession owns graph hydration, Python-bridge connection
|
|
565
|
-
// (NODETOOL_WORKER_URL/NODETOOL_WORKER_TOKEN → remote worker; unset →
|
|
566
|
-
// local stdio worker; pure-TS graphs get none), and executor
|
|
567
|
-
// resolution (registry → Python bridge → throw).
|
|
568
|
-
const sessionOptions = {
|
|
569
|
-
graph,
|
|
570
|
-
registry,
|
|
571
|
-
// Registry alone hydrates node flags but not `propertyTypes`, and
|
|
572
|
-
// correlation analysis reads list-ness only from that map — so a
|
|
573
|
-
// saved workflow (which never carries it) had every list handle
|
|
574
|
-
// read as non-list, and legal fan-in into one was rejected before
|
|
575
|
-
// any node ran. The resolver fills it from node metadata.
|
|
576
|
-
resolveNodeType: createGraphNodeTypeResolver(registry).resolveNodeType,
|
|
577
|
-
jobId,
|
|
578
|
-
workflowId,
|
|
579
|
-
params,
|
|
580
|
-
context
|
|
581
|
-
};
|
|
582
|
-
if (triggerEvent) {
|
|
583
|
-
sessionOptions.triggerEvent = triggerEvent;
|
|
584
|
-
}
|
|
585
|
-
// Message capture is retention, so it stays off unless a supervised
|
|
586
|
-
// run needs the stream to print its `⛨` lines as they happen.
|
|
587
|
-
if (supervisor) {
|
|
588
|
-
sessionOptions.supervisor = supervisor.handle;
|
|
589
|
-
sessionOptions.captureMessages = true;
|
|
590
|
-
}
|
|
591
|
-
const session = await ExecutionSession.create(sessionOptions);
|
|
592
|
-
const interventionLines = supervisor
|
|
593
|
-
? streamInterventionLines(session.messages)
|
|
594
|
-
: Promise.resolve();
|
|
595
|
-
const result = await session.result;
|
|
596
|
-
await interventionLines;
|
|
597
|
-
supervisor?.handle.close();
|
|
598
|
-
const interventions = result.interventions ?? [];
|
|
599
|
-
if (supervisor && interventions.length > 0) {
|
|
600
|
-
await recordSupervisorCost({
|
|
601
|
-
interventions,
|
|
602
|
-
jobId,
|
|
603
|
-
workflowId,
|
|
604
|
-
userId: "1",
|
|
605
|
-
providerId: supervisor.providerId,
|
|
606
|
-
model: supervisor.model
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
const { printRunJson, previewJson } = await import("./run-json.js");
|
|
610
|
-
if (opts.json) {
|
|
611
|
-
await printRunJson(result, {
|
|
612
|
-
outputDir: resolve(process.cwd(), "nodetool-output", jobId)
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
else {
|
|
616
|
-
console.log(`Status: ${result.status}`);
|
|
617
|
-
if (result.error)
|
|
618
|
-
console.error(`Error: ${result.error}`);
|
|
619
|
-
for (const [nodeId, outputs] of Object.entries(result.outputs ?? {})) {
|
|
620
|
-
if (Array.isArray(outputs) && outputs.length > 0) {
|
|
621
|
-
console.log(`\nNode ${nodeId}:`);
|
|
622
|
-
for (const out of outputs) {
|
|
623
|
-
console.log(` ${previewJson(out, 200)}`);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
printSupervisedSummary(interventions);
|
|
628
|
-
}
|
|
629
|
-
process.exit(result.status === "completed" ? 0 : 1);
|
|
630
|
-
}
|
|
631
|
-
catch (e) {
|
|
632
|
-
console.error(describeRunFailure(e));
|
|
633
|
-
process.exit(1);
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
workflows
|
|
637
|
-
.command("export-dsl <workflow_id_or_file>")
|
|
638
|
-
.description("Export a workflow as TypeScript DSL code (local DB by id; --api-url for remote)")
|
|
639
|
-
.option("--api-url <url>", "Fetch the workflow from a remote server instead of the local database", process.env["NODETOOL_API_URL"])
|
|
640
|
-
.option("-o, --output <file>", "Write the exported DSL to a file")
|
|
641
|
-
.action(async (idOrFile, opts) => {
|
|
642
|
-
try {
|
|
643
|
-
let source;
|
|
644
|
-
if (idOrFile.endsWith(".json") ||
|
|
645
|
-
idOrFile.includes("/") ||
|
|
646
|
-
idOrFile.includes("\\")) {
|
|
647
|
-
const { readFileSync } = await import("node:fs");
|
|
648
|
-
const raw = JSON.parse(readFileSync(idOrFile, "utf8"));
|
|
649
|
-
const graph = (raw.graph ?? raw);
|
|
650
|
-
source = workflowToDsl(graph, {
|
|
651
|
-
workflowName: isString(raw.name) ? raw.name : null
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
else if (opts.apiUrl) {
|
|
655
|
-
source = await apiGetText(opts.apiUrl, `/api/workflows/${idOrFile}/dsl-export`);
|
|
656
|
-
}
|
|
657
|
-
else {
|
|
658
|
-
ensureDb();
|
|
659
|
-
const wf = await Workflow.find(LOCAL_USER_ID, idOrFile);
|
|
660
|
-
if (!wf)
|
|
661
|
-
throw new Error(`Workflow not found: ${idOrFile}`);
|
|
662
|
-
const graph = wf.getGraph();
|
|
663
|
-
source = workflowToDsl(graph, {
|
|
664
|
-
workflowName: isString(wf.name) ? wf.name : null
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
if (opts.output) {
|
|
668
|
-
const { writeFile } = await import("node:fs/promises");
|
|
669
|
-
await writeFile(opts.output, source, "utf8");
|
|
670
|
-
console.log(opts.output);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
console.log(source);
|
|
674
|
-
}
|
|
675
|
-
catch (e) {
|
|
676
|
-
printCommandError(e);
|
|
677
|
-
process.exit(1);
|
|
678
|
-
}
|
|
679
|
-
});
|
|
680
|
-
workflows
|
|
681
|
-
.command("export-example <workflow_id_or_file>")
|
|
682
|
-
.description("Export a workflow as a shipped template: materialize its assets into the " +
|
|
683
|
-
"package's constant asset dir (package:// refs) and write the example JSON")
|
|
684
|
-
.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"])
|
|
685
|
-
.option("--package <name>", "Owning package", "nodetool-base")
|
|
686
|
-
.option("--assets-dir <dir>", "Root holding <package>/<file> constant assets (defaults to the monorepo base-nodes assets dir)")
|
|
687
|
-
.option("--examples-dir <dir>", "Directory the example JSON is written under, as <dir>/<package>/<name>.json")
|
|
688
|
-
.option("-o, --output <file>", "Write the example JSON to this exact path")
|
|
689
|
-
.option("--include-remote", "Also materialize http(s) and local-file refs (off by default)")
|
|
690
|
-
.action(async (idOrFile, opts) => {
|
|
691
|
-
try {
|
|
692
|
-
const { readFileSync } = await import("node:fs");
|
|
693
|
-
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
694
|
-
const { join } = await import("node:path");
|
|
695
|
-
const { materializeWorkflowConstantAssets } = await import("@nodetool-ai/websocket");
|
|
696
|
-
let name = "";
|
|
697
|
-
let description = "";
|
|
698
|
-
let tags = [];
|
|
699
|
-
let graph;
|
|
700
|
-
const isFile = idOrFile.endsWith(".json") ||
|
|
701
|
-
idOrFile.includes("/") ||
|
|
702
|
-
idOrFile.includes("\\");
|
|
703
|
-
if (isFile) {
|
|
704
|
-
const raw = JSON.parse(readFileSync(idOrFile, "utf8"));
|
|
705
|
-
name = isString(raw.name) ? raw.name : "workflow";
|
|
706
|
-
description =
|
|
707
|
-
isString(raw.description) ? raw.description : "";
|
|
708
|
-
tags = Array.isArray(raw.tags)
|
|
709
|
-
? raw.tags.filter((t) => typeof t === "string")
|
|
710
|
-
: [];
|
|
711
|
-
graph = (raw.graph ?? raw);
|
|
712
|
-
}
|
|
713
|
-
else if (opts.apiUrl) {
|
|
714
|
-
const client = createApiClient(opts.apiUrl);
|
|
715
|
-
const wf = (await client.workflows.get.query({
|
|
716
|
-
id: idOrFile
|
|
717
|
-
}));
|
|
718
|
-
name = isString(wf.name) ? wf.name : "workflow";
|
|
719
|
-
description =
|
|
720
|
-
isString(wf.description) ? wf.description : "";
|
|
721
|
-
tags = Array.isArray(wf.tags)
|
|
722
|
-
? wf.tags.filter((t) => typeof t === "string")
|
|
723
|
-
: [];
|
|
724
|
-
graph = wf.graph;
|
|
725
|
-
}
|
|
726
|
-
else {
|
|
727
|
-
ensureDb();
|
|
728
|
-
const wf = await Workflow.find(LOCAL_USER_ID, idOrFile);
|
|
729
|
-
if (!wf)
|
|
730
|
-
throw new Error(`Workflow not found: ${idOrFile}`);
|
|
731
|
-
name = isString(wf.name) ? wf.name : "workflow";
|
|
732
|
-
description =
|
|
733
|
-
isString(wf.description) ? wf.description : "";
|
|
734
|
-
tags = Array.isArray(wf.tags)
|
|
735
|
-
? wf.tags.filter((t) => typeof t === "string")
|
|
736
|
-
: [];
|
|
737
|
-
graph = wf.getGraph();
|
|
738
|
-
}
|
|
739
|
-
if (!graph?.nodes) {
|
|
740
|
-
throw new Error("Workflow has no graph to export");
|
|
741
|
-
}
|
|
742
|
-
const repoRoot = resolve(__dirname, "..", "..", "..");
|
|
743
|
-
const baseNodesDir = join(repoRoot, "packages", "base-nodes", "nodetool");
|
|
744
|
-
const assetsRoot = opts.assetsDir ?? join(baseNodesDir, "assets");
|
|
745
|
-
const examplesDir = opts.examplesDir ?? join(baseNodesDir, "examples");
|
|
746
|
-
const fetchAssetBytes = opts.apiUrl
|
|
747
|
-
? makeAssetFetcher(opts.apiUrl.replace(/\/+$/, ""))
|
|
748
|
-
: await makeLocalAssetFetcher();
|
|
749
|
-
const materializeOptions = {
|
|
750
|
-
packageName: opts.package,
|
|
751
|
-
assetsRoot,
|
|
752
|
-
fetchAssetBytes
|
|
753
|
-
};
|
|
754
|
-
if (opts.includeRemote) {
|
|
755
|
-
materializeOptions.includeRemote = true;
|
|
756
|
-
}
|
|
757
|
-
const result = await materializeWorkflowConstantAssets(graph, materializeOptions);
|
|
758
|
-
const slug = name
|
|
759
|
-
.toLowerCase()
|
|
760
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
761
|
-
.replace(/^-+|-+$/g, "");
|
|
762
|
-
const example = {
|
|
763
|
-
id: slug || "workflow",
|
|
764
|
-
access: "public",
|
|
765
|
-
name,
|
|
766
|
-
description,
|
|
767
|
-
tags,
|
|
768
|
-
package_name: opts.package,
|
|
769
|
-
graph: result.graph
|
|
770
|
-
};
|
|
771
|
-
// Sanitize the on-disk filename separately from the display name so
|
|
772
|
-
// names with path separators can't escape the examples directory.
|
|
773
|
-
const safeFileName = name.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^[._]+/, "") ||
|
|
774
|
-
"workflow";
|
|
775
|
-
const outPath = opts.output ??
|
|
776
|
-
join(examplesDir, opts.package, `${safeFileName}.json`);
|
|
777
|
-
await mkdir(dirname(outPath), { recursive: true });
|
|
778
|
-
await writeFile(outPath, JSON.stringify(example, null, 2) + "\n", "utf8");
|
|
779
|
-
for (const a of result.exported) {
|
|
780
|
-
console.error(` asset ${a.source} → ${a.packageUri} (${a.byteLength} bytes)`);
|
|
781
|
-
}
|
|
782
|
-
for (const s of result.skipped) {
|
|
783
|
-
console.error(` warning: could not resolve asset ${s} (left as-is)`);
|
|
784
|
-
}
|
|
785
|
-
console.log(outPath);
|
|
786
|
-
}
|
|
787
|
-
catch (e) {
|
|
788
|
-
printCommandError(e);
|
|
789
|
-
process.exit(1);
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
workflows
|
|
793
|
-
.command("export-bundle <workflow_id_or_file...>")
|
|
794
|
-
.description("Export one or more workflows as a portable .nodetool bundle (zip): the " +
|
|
795
|
-
"graphs plus the bytes of every asset they reference, in a single file")
|
|
796
|
-
.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"])
|
|
797
|
-
.option("-o, --output <file>", "Output path (default: <name>.nodetool)")
|
|
798
|
-
.option("--include-remote", "Also embed http(s) and local-file refs (off by default)")
|
|
799
|
-
.action(async (idsOrFiles, opts) => {
|
|
800
|
-
try {
|
|
801
|
-
const { readFileSync } = await import("node:fs");
|
|
802
|
-
const { writeFile } = await import("node:fs/promises");
|
|
803
|
-
const { packWorkflowsBundle } = await import("@nodetool-ai/websocket");
|
|
804
|
-
const toBundleWf = (raw) => ({
|
|
805
|
-
name: isString(raw.name) ? raw.name : "workflow",
|
|
806
|
-
description: isString(raw.description) ? raw.description : "",
|
|
807
|
-
tags: Array.isArray(raw.tags)
|
|
808
|
-
? raw.tags.filter((t) => typeof t === "string")
|
|
809
|
-
: [],
|
|
810
|
-
run_mode: raw.run_mode ?? null,
|
|
811
|
-
settings: raw.settings ?? null,
|
|
812
|
-
graph: (raw.graph ?? raw)
|
|
813
|
-
});
|
|
814
|
-
const useServer = Boolean(opts.apiUrl);
|
|
815
|
-
const apiUrl = opts.apiUrl?.replace(/\/+$/, "") ?? "";
|
|
816
|
-
if (!useServer)
|
|
817
|
-
ensureDb();
|
|
818
|
-
const workflowsToPack = [];
|
|
819
|
-
for (const idOrFile of idsOrFiles) {
|
|
820
|
-
const isFile = idOrFile.endsWith(".json") ||
|
|
821
|
-
idOrFile.includes("/") ||
|
|
822
|
-
idOrFile.includes("\\");
|
|
823
|
-
let raw;
|
|
824
|
-
if (isFile) {
|
|
825
|
-
raw = JSON.parse(readFileSync(idOrFile, "utf8"));
|
|
826
|
-
}
|
|
827
|
-
else if (useServer) {
|
|
828
|
-
raw = (await createApiClient(apiUrl).workflows.get.query({
|
|
829
|
-
id: idOrFile
|
|
830
|
-
}));
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
|
-
const found = await Workflow.find(LOCAL_USER_ID, idOrFile);
|
|
834
|
-
if (!found)
|
|
835
|
-
throw new Error(`Workflow not found: ${idOrFile}`);
|
|
836
|
-
raw = { ...found, graph: found.getGraph() };
|
|
837
|
-
}
|
|
838
|
-
const wf = toBundleWf(raw);
|
|
839
|
-
if (!wf.graph?.nodes) {
|
|
840
|
-
throw new Error(`Workflow '${idOrFile}' has no graph to export`);
|
|
841
|
-
}
|
|
842
|
-
workflowsToPack.push(wf);
|
|
843
|
-
}
|
|
844
|
-
const packOptions = {
|
|
845
|
-
workflows: workflowsToPack,
|
|
846
|
-
fetchAssetBytes: useServer
|
|
847
|
-
? makeAssetFetcher(apiUrl)
|
|
848
|
-
: await makeLocalAssetFetcher(),
|
|
849
|
-
nodetoolVersion: cliVersion()
|
|
850
|
-
};
|
|
851
|
-
if (opts.includeRemote) {
|
|
852
|
-
packOptions.includeRemote = true;
|
|
853
|
-
}
|
|
854
|
-
const { bytes, manifest, skipped } = await packWorkflowsBundle(packOptions);
|
|
855
|
-
const defaultBase = workflowsToPack.length === 1
|
|
856
|
-
? workflowsToPack[0].name.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
857
|
-
: `${workflowsToPack.length}-workflows`;
|
|
858
|
-
const outPath = opts.output ?? `${defaultBase}.nodetool`;
|
|
859
|
-
await writeFile(outPath, bytes);
|
|
860
|
-
for (const a of manifest.assets) {
|
|
861
|
-
console.error(` embedded ${a.file} (${a.bytes} bytes)`);
|
|
862
|
-
}
|
|
863
|
-
for (const s of skipped) {
|
|
864
|
-
console.error(` warning: could not resolve asset ${s} (left as-is)`);
|
|
865
|
-
}
|
|
866
|
-
console.error(` bundled ${manifest.workflows.length} workflow(s), ${manifest.assets.length} asset(s)`);
|
|
867
|
-
console.log(outPath);
|
|
868
|
-
}
|
|
869
|
-
catch (e) {
|
|
870
|
-
printCommandError(e);
|
|
871
|
-
process.exit(1);
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
workflows
|
|
875
|
-
.command("import-bundle <bundle_file>")
|
|
876
|
-
.description("Import a .nodetool bundle into the local library: store its assets and " +
|
|
877
|
-
"create the workflows with refs rewritten to the imported assets")
|
|
878
|
-
.option("--json", "Output the created workflows as JSON")
|
|
879
|
-
.action(async (bundleFile, opts) => {
|
|
880
|
-
try {
|
|
881
|
-
const { readFileSync } = await import("node:fs");
|
|
882
|
-
const { randomUUID } = await import("node:crypto");
|
|
883
|
-
const { extname } = await import("node:path");
|
|
884
|
-
const { importWorkflowBundle } = await import("@nodetool-ai/websocket");
|
|
885
|
-
await setupDb();
|
|
886
|
-
const storage = new FileStorageAdapter(getDefaultAssetsPath());
|
|
887
|
-
const zip = new Uint8Array(readFileSync(bundleFile));
|
|
888
|
-
const result = await importWorkflowBundle(zip, {
|
|
889
|
-
storeAsset: async ({ bytes, fileName, contentType }) => {
|
|
890
|
-
const id = randomUUID();
|
|
891
|
-
const key = `${id}${extname(fileName)}`;
|
|
892
|
-
await storage.store(key, bytes, contentType);
|
|
893
|
-
return { uri: `asset://${key}`, assetId: id };
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
const created = [];
|
|
897
|
-
for (const wf of result.workflows) {
|
|
898
|
-
created.push(await Workflow.create({
|
|
899
|
-
user_id: "1",
|
|
900
|
-
name: wf.name,
|
|
901
|
-
description: wf.description ?? "",
|
|
902
|
-
tags: wf.tags ?? [],
|
|
903
|
-
access: "private",
|
|
904
|
-
graph: wf.graph,
|
|
905
|
-
run_mode: wf.run_mode ?? "workflow",
|
|
906
|
-
settings: wf.settings ?? null
|
|
907
|
-
}));
|
|
908
|
-
}
|
|
909
|
-
for (const s of result.missing) {
|
|
910
|
-
console.error(` warning: bundle asset ${s} was missing from the archive`);
|
|
911
|
-
}
|
|
912
|
-
for (const m of result.checksumMismatches) {
|
|
913
|
-
console.error(` warning: ${m}`);
|
|
914
|
-
}
|
|
915
|
-
console.error(` imported ${created.length} workflow(s) and ${result.imported.length} asset(s) into local storage`);
|
|
916
|
-
if (opts.json) {
|
|
917
|
-
asJson(created);
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
920
|
-
for (const c of created) {
|
|
921
|
-
console.log(String(c.id));
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
catch (e) {
|
|
926
|
-
printCommandError(e, opts.json);
|
|
927
|
-
process.exit(1);
|
|
928
|
-
}
|
|
929
|
-
});
|
|
930
|
-
// ---------------------------------------------------------------------------
|
|
931
|
-
// jobs / assets — the read commands are registered from commands/resource-read.ts
|
|
932
|
-
// ---------------------------------------------------------------------------
|
|
933
|
-
const jobs = program.command("jobs").description("Job management");
|
|
934
|
-
const assets = program.command("assets").description("Asset management");
|
|
935
|
-
registerResourceReadCommands({ workflows, jobs, assets }, { ensureDb, localUserId: LOCAL_USER_ID });
|
|
936
|
-
const models = program.command("models").description("Model management");
|
|
937
|
-
const modelKinds = [
|
|
938
|
-
"llm",
|
|
939
|
-
"image",
|
|
940
|
-
"tts",
|
|
941
|
-
"asr",
|
|
942
|
-
"video",
|
|
943
|
-
"embedding"
|
|
944
|
-
];
|
|
945
|
-
function modelRow(m) {
|
|
946
|
-
return {
|
|
947
|
-
id: m["id"],
|
|
948
|
-
name: m["name"],
|
|
949
|
-
provider: m["provider"] ?? "",
|
|
950
|
-
type: m["type"] ?? "",
|
|
951
|
-
repo_id: m["repo_id"] ?? ""
|
|
952
|
-
};
|
|
20
|
+
else {
|
|
21
|
+
await import("./nodetool-main.js");
|
|
953
22
|
}
|
|
954
|
-
|
|
955
|
-
.command("list")
|
|
956
|
-
.description("List all models (recommended + provider + HF cached); local by default, --api-url for remote")
|
|
957
|
-
.option("--api-url <url>", "Query a remote server instead of local providers/cache", process.env["NODETOOL_API_URL"])
|
|
958
|
-
.option("--json", "Output as JSON")
|
|
959
|
-
.action(async (opts) => {
|
|
960
|
-
try {
|
|
961
|
-
let rows;
|
|
962
|
-
if (opts.apiUrl) {
|
|
963
|
-
const client = createApiClient(opts.apiUrl);
|
|
964
|
-
rows = await client.models.all.query();
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
await setupDb();
|
|
968
|
-
rows = await listAllModels();
|
|
969
|
-
}
|
|
970
|
-
if (opts.json) {
|
|
971
|
-
asJson(rows);
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
printTable(rows.map(modelRow));
|
|
975
|
-
}
|
|
976
|
-
catch (e) {
|
|
977
|
-
printCommandError(e, opts.json);
|
|
978
|
-
process.exit(1);
|
|
979
|
-
}
|
|
980
|
-
});
|
|
981
|
-
models
|
|
982
|
-
.command("providers")
|
|
983
|
-
.description("List configured providers and their capabilities")
|
|
984
|
-
.option("--json", "Output as JSON")
|
|
985
|
-
.action(async (opts) => {
|
|
986
|
-
await setupDb();
|
|
987
|
-
try {
|
|
988
|
-
const data = await listConfiguredProviderInfo();
|
|
989
|
-
if (opts.json) {
|
|
990
|
-
asJson(data);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
printTable(data.map((p) => ({
|
|
994
|
-
provider: p.provider,
|
|
995
|
-
capabilities: p.capabilities.join(", ")
|
|
996
|
-
})));
|
|
997
|
-
}
|
|
998
|
-
catch (e) {
|
|
999
|
-
printCommandError(e, opts.json);
|
|
1000
|
-
process.exit(1);
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
registerRecommendedCommand(models);
|
|
1004
|
-
registerHfCommands(models);
|
|
1005
|
-
models
|
|
1006
|
-
.command("ollama")
|
|
1007
|
-
.description("List Ollama models (queries the local daemon; --api-url for remote)")
|
|
1008
|
-
.option("--api-url <url>", "Query a remote server instead of the local Ollama daemon", process.env["NODETOOL_API_URL"])
|
|
1009
|
-
.option("--json", "Output as JSON")
|
|
1010
|
-
.action(async (opts) => {
|
|
1011
|
-
try {
|
|
1012
|
-
let rows;
|
|
1013
|
-
if (opts.apiUrl) {
|
|
1014
|
-
const client = createApiClient(opts.apiUrl);
|
|
1015
|
-
rows = await client.models.ollama.query();
|
|
1016
|
-
}
|
|
1017
|
-
else {
|
|
1018
|
-
await setupDb();
|
|
1019
|
-
rows = await listOllamaModels();
|
|
1020
|
-
}
|
|
1021
|
-
if (opts.json) {
|
|
1022
|
-
asJson(rows);
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
printTable(rows.map(modelRow));
|
|
1026
|
-
}
|
|
1027
|
-
catch (e) {
|
|
1028
|
-
printCommandError(e, opts.json);
|
|
1029
|
-
process.exit(1);
|
|
1030
|
-
}
|
|
1031
|
-
});
|
|
1032
|
-
models
|
|
1033
|
-
.command("huggingface")
|
|
1034
|
-
.description("List HuggingFace cached models (scans the local cache; --api-url for remote)")
|
|
1035
|
-
.option("--api-url <url>", "Query a remote server instead of the local HuggingFace cache", process.env["NODETOOL_API_URL"])
|
|
1036
|
-
.option("--query <q>", "Search query")
|
|
1037
|
-
.option("--type <t>", "Filter by HF model type")
|
|
1038
|
-
.option("--json", "Output as JSON")
|
|
1039
|
-
.action(async (opts) => {
|
|
1040
|
-
try {
|
|
1041
|
-
let rows;
|
|
1042
|
-
if (opts.apiUrl) {
|
|
1043
|
-
const client = createApiClient(opts.apiUrl);
|
|
1044
|
-
if (opts.query || opts.type) {
|
|
1045
|
-
const search = {};
|
|
1046
|
-
if (opts.query) {
|
|
1047
|
-
search.query = String(opts.query);
|
|
1048
|
-
}
|
|
1049
|
-
if (opts.type) {
|
|
1050
|
-
search.type = String(opts.type);
|
|
1051
|
-
}
|
|
1052
|
-
rows = await client.models.huggingfaceSearch.query(search);
|
|
1053
|
-
}
|
|
1054
|
-
else {
|
|
1055
|
-
rows = await client.models.huggingfaceList.query();
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
else if (opts.query || opts.type) {
|
|
1059
|
-
// Wrap a bare query in wildcards so it matches as a substring, the same
|
|
1060
|
-
// normalization the server's huggingfaceSearch applies.
|
|
1061
|
-
const rawQuery = opts.query ? String(opts.query) : undefined;
|
|
1062
|
-
const query = rawQuery && !rawQuery.includes("*") ? `*${rawQuery}*` : rawQuery;
|
|
1063
|
-
rows = await searchCachedHfModels(query ? [query] : undefined, opts.type ? [String(opts.type)] : undefined);
|
|
1064
|
-
}
|
|
1065
|
-
else {
|
|
1066
|
-
rows = await readCachedHfModels();
|
|
1067
|
-
}
|
|
1068
|
-
if (opts.json) {
|
|
1069
|
-
asJson(rows);
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
printTable(rows.map(modelRow));
|
|
1073
|
-
}
|
|
1074
|
-
catch (e) {
|
|
1075
|
-
printCommandError(e, opts.json);
|
|
1076
|
-
process.exit(1);
|
|
1077
|
-
}
|
|
1078
|
-
});
|
|
1079
|
-
models
|
|
1080
|
-
.command("by-provider <provider>")
|
|
1081
|
-
.description(`List models for a provider. --kind one of: ${modelKinds.join(", ")}`)
|
|
1082
|
-
.option("--kind <kind>", "Model kind", "llm")
|
|
1083
|
-
.option("--json", "Output as JSON")
|
|
1084
|
-
.action(async (provider, opts) => {
|
|
1085
|
-
const kind = opts.kind;
|
|
1086
|
-
if (!modelKinds.includes(kind)) {
|
|
1087
|
-
console.error(`Invalid --kind '${opts.kind}'. Must be one of: ${modelKinds.join(", ")}`);
|
|
1088
|
-
process.exit(1);
|
|
1089
|
-
}
|
|
1090
|
-
await setupDb();
|
|
1091
|
-
try {
|
|
1092
|
-
const rows = await listProviderModels(provider, kind);
|
|
1093
|
-
if (opts.json) {
|
|
1094
|
-
asJson(rows);
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
printTable(rows.map(modelRow));
|
|
1098
|
-
}
|
|
1099
|
-
catch (e) {
|
|
1100
|
-
printCommandError(e, opts.json);
|
|
1101
|
-
process.exit(1);
|
|
1102
|
-
}
|
|
1103
|
-
});
|
|
1104
|
-
// ---------------------------------------------------------------------------
|
|
1105
|
-
// storage
|
|
1106
|
-
// ---------------------------------------------------------------------------
|
|
1107
|
-
const storage = program
|
|
1108
|
-
.command("storage")
|
|
1109
|
-
.description("Maintain the asset storage backend");
|
|
1110
|
-
storage
|
|
1111
|
-
.command("migrate-keys")
|
|
1112
|
-
.description("Move asset objects under their owner's prefix (<userId>/<assetId>.<ext>)")
|
|
1113
|
-
.option("--dry-run", "Report what would move without writing anything")
|
|
1114
|
-
.option("--user-id <id>", "Migrate only this user's objects")
|
|
1115
|
-
.option("--json", "Output the report as JSON")
|
|
1116
|
-
.action(async (opts) => {
|
|
1117
|
-
await setupDb();
|
|
1118
|
-
const { migrateStorageKeys, formatMigrateKeysReport } = await import("./commands/storage.js");
|
|
1119
|
-
try {
|
|
1120
|
-
const migrateOptions = {
|
|
1121
|
-
dryRun: Boolean(opts.dryRun)
|
|
1122
|
-
};
|
|
1123
|
-
if (opts.userId) {
|
|
1124
|
-
migrateOptions.userId = opts.userId;
|
|
1125
|
-
}
|
|
1126
|
-
const report = await migrateStorageKeys(migrateOptions);
|
|
1127
|
-
if (opts.json) {
|
|
1128
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1129
|
-
}
|
|
1130
|
-
else {
|
|
1131
|
-
console.log(formatMigrateKeysReport(report, Boolean(opts.dryRun)));
|
|
1132
|
-
}
|
|
1133
|
-
if (report.failed > 0)
|
|
1134
|
-
process.exit(1);
|
|
1135
|
-
}
|
|
1136
|
-
catch (e) {
|
|
1137
|
-
printCommandError(e, opts.json);
|
|
1138
|
-
process.exit(1);
|
|
1139
|
-
}
|
|
1140
|
-
});
|
|
1141
|
-
workflows
|
|
1142
|
-
.command("migrate-code-inputs")
|
|
1143
|
-
.description("Rewrite saved Code node bodies to read declared inputs off `inputs`")
|
|
1144
|
-
.option("--dry-run", "Report what would change without writing anything")
|
|
1145
|
-
.option("--user-id <id>", "Migrate only this user's workflows")
|
|
1146
|
-
.option("--json", "Output the report as JSON")
|
|
1147
|
-
.action(async (opts) => {
|
|
1148
|
-
await setupDb();
|
|
1149
|
-
const { migrateCodeInputs, formatMigrateCodeInputsReport } = await import("./commands/migrate-code-inputs.js");
|
|
1150
|
-
try {
|
|
1151
|
-
const migrateOptions = {
|
|
1152
|
-
dryRun: Boolean(opts.dryRun)
|
|
1153
|
-
};
|
|
1154
|
-
if (opts.userId) {
|
|
1155
|
-
migrateOptions.userId = opts.userId;
|
|
1156
|
-
}
|
|
1157
|
-
const report = await migrateCodeInputs(migrateOptions);
|
|
1158
|
-
if (opts.json) {
|
|
1159
|
-
console.log(JSON.stringify(report, null, 2));
|
|
1160
|
-
}
|
|
1161
|
-
else {
|
|
1162
|
-
console.log(formatMigrateCodeInputsReport(report, Boolean(opts.dryRun)));
|
|
1163
|
-
}
|
|
1164
|
-
if (report.failed > 0)
|
|
1165
|
-
process.exit(1);
|
|
1166
|
-
}
|
|
1167
|
-
catch (e) {
|
|
1168
|
-
printCommandError(e, opts.json);
|
|
1169
|
-
process.exit(1);
|
|
1170
|
-
}
|
|
1171
|
-
});
|
|
1172
|
-
// ---------------------------------------------------------------------------
|
|
1173
|
-
// secrets
|
|
1174
|
-
// ---------------------------------------------------------------------------
|
|
1175
|
-
const secrets = program
|
|
1176
|
-
.command("secrets")
|
|
1177
|
-
.description("Manage encrypted secrets");
|
|
1178
|
-
secrets
|
|
1179
|
-
.command("list")
|
|
1180
|
-
.description("List stored secret keys (values are never shown)")
|
|
1181
|
-
.option("--user-id <id>", "User ID", "1")
|
|
1182
|
-
.action(async (opts) => {
|
|
1183
|
-
await setupDb();
|
|
1184
|
-
try {
|
|
1185
|
-
const [items] = await Secret.listForUser(opts.userId, 100);
|
|
1186
|
-
if (items.length === 0) {
|
|
1187
|
-
console.log("No secrets stored.");
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
printTable(items.map((s) => ({ key: s.key, updated_at: s.updated_at ?? "" })));
|
|
1191
|
-
}
|
|
1192
|
-
catch (e) {
|
|
1193
|
-
printCommandError(e);
|
|
1194
|
-
process.exit(1);
|
|
1195
|
-
}
|
|
1196
|
-
});
|
|
1197
|
-
secrets
|
|
1198
|
-
.command("store <key>")
|
|
1199
|
-
.description("Store or update a secret (prompts for value)")
|
|
1200
|
-
.option("--user-id <id>", "User ID", "1")
|
|
1201
|
-
.option("--description <desc>", "Optional description")
|
|
1202
|
-
.action(async (key, opts) => {
|
|
1203
|
-
await setupDb();
|
|
1204
|
-
const rl = createInterface({
|
|
1205
|
-
input: process.stdin,
|
|
1206
|
-
output: process.stderr
|
|
1207
|
-
});
|
|
1208
|
-
const value = await new Promise((resolve) => {
|
|
1209
|
-
process.stderr.write(`Enter value for '${key}': `);
|
|
1210
|
-
rl.question("", (ans) => {
|
|
1211
|
-
rl.close();
|
|
1212
|
-
resolve(ans);
|
|
1213
|
-
});
|
|
1214
|
-
});
|
|
1215
|
-
try {
|
|
1216
|
-
await Secret.upsert({
|
|
1217
|
-
userId: opts.userId,
|
|
1218
|
-
key,
|
|
1219
|
-
value,
|
|
1220
|
-
description: opts.description
|
|
1221
|
-
});
|
|
1222
|
-
console.log(`Secret '${key}' stored.`);
|
|
1223
|
-
}
|
|
1224
|
-
catch (e) {
|
|
1225
|
-
printCommandError(e);
|
|
1226
|
-
process.exit(1);
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1229
|
-
secrets
|
|
1230
|
-
.command("get <key>")
|
|
1231
|
-
.description("Retrieve and print a secret value")
|
|
1232
|
-
.option("--user-id <id>", "User ID", "1")
|
|
1233
|
-
.action(async (key, opts) => {
|
|
1234
|
-
await setupDb();
|
|
1235
|
-
try {
|
|
1236
|
-
const value = await getSecret(key, opts.userId);
|
|
1237
|
-
if (value == null) {
|
|
1238
|
-
console.error(`Secret '${key}' not found.`);
|
|
1239
|
-
process.exit(1);
|
|
1240
|
-
}
|
|
1241
|
-
console.log(value);
|
|
1242
|
-
}
|
|
1243
|
-
catch (e) {
|
|
1244
|
-
printCommandError(e);
|
|
1245
|
-
process.exit(1);
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
// ---------------------------------------------------------------------------
|
|
1249
|
-
// settings
|
|
1250
|
-
// ---------------------------------------------------------------------------
|
|
1251
|
-
const settings = program
|
|
1252
|
-
.command("settings")
|
|
1253
|
-
.description("View configuration settings");
|
|
1254
|
-
settings
|
|
1255
|
-
.command("show")
|
|
1256
|
-
.description("Show current settings from environment")
|
|
1257
|
-
.option("--json", "Output as JSON")
|
|
1258
|
-
.action((opts) => {
|
|
1259
|
-
const vars = [
|
|
1260
|
-
"ENV",
|
|
1261
|
-
"LOG_LEVEL",
|
|
1262
|
-
"PORT",
|
|
1263
|
-
"HOST",
|
|
1264
|
-
"DB_PATH",
|
|
1265
|
-
"NODETOOL_API_URL",
|
|
1266
|
-
"ANTHROPIC_API_KEY",
|
|
1267
|
-
"OPENAI_API_KEY",
|
|
1268
|
-
"GEMINI_API_KEY",
|
|
1269
|
-
"MISTRAL_API_KEY",
|
|
1270
|
-
"GROQ_API_KEY",
|
|
1271
|
-
"OLLAMA_API_URL",
|
|
1272
|
-
"SERPAPI_API_KEY",
|
|
1273
|
-
"HF_TOKEN",
|
|
1274
|
-
"VECTORSTORE_DB_PATH",
|
|
1275
|
-
"ASSET_BUCKET",
|
|
1276
|
-
"S3_ENDPOINT_URL"
|
|
1277
|
-
];
|
|
1278
|
-
const data = Object.fromEntries(vars.map((k) => [
|
|
1279
|
-
k,
|
|
1280
|
-
process.env[k]
|
|
1281
|
-
? k.endsWith("KEY") || k.endsWith("TOKEN")
|
|
1282
|
-
? "***"
|
|
1283
|
-
: process.env[k]
|
|
1284
|
-
: ""
|
|
1285
|
-
]));
|
|
1286
|
-
if (opts.json) {
|
|
1287
|
-
asJson(data);
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
printTable(Object.entries(data).map(([variable, value]) => ({ variable, value })));
|
|
1291
|
-
});
|
|
1292
|
-
// ---------------------------------------------------------------------------
|
|
1293
|
-
// mcp
|
|
1294
|
-
// ---------------------------------------------------------------------------
|
|
1295
|
-
const mcp = program
|
|
1296
|
-
.command("mcp")
|
|
1297
|
-
.description("MCP server integration for AI coding assistants");
|
|
1298
|
-
mcp
|
|
1299
|
-
.command("install")
|
|
1300
|
-
.description("Install NodeTool MCP server config for Claude Code, Codex, and/or OpenCode")
|
|
1301
|
-
.option("--url <url>", "MCP server URL", "http://127.0.0.1:7777/mcp")
|
|
1302
|
-
.option("--claude", "Install for Claude Code only")
|
|
1303
|
-
.option("--codex", "Install for Codex only")
|
|
1304
|
-
.option("--opencode", "Install for OpenCode only")
|
|
1305
|
-
.action(async (opts) => {
|
|
1306
|
-
const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import("node:fs");
|
|
1307
|
-
const { join } = await import("node:path");
|
|
1308
|
-
const { homedir } = await import("node:os");
|
|
1309
|
-
const home = homedir();
|
|
1310
|
-
const mcpUrl = opts.url;
|
|
1311
|
-
const installAll = !opts.claude && !opts.codex && !opts.opencode;
|
|
1312
|
-
const results = [];
|
|
1313
|
-
// ── Claude Code ──────────────────────────────────────────────
|
|
1314
|
-
if (installAll || opts.claude) {
|
|
1315
|
-
try {
|
|
1316
|
-
const claudeConfigPath = join(home, ".claude.json");
|
|
1317
|
-
let config = {};
|
|
1318
|
-
if (existsSync(claudeConfigPath)) {
|
|
1319
|
-
config = JSON.parse(readFileSync(claudeConfigPath, "utf8"));
|
|
1320
|
-
}
|
|
1321
|
-
// Install globally under projects."~" which applies to all projects
|
|
1322
|
-
const projects = (config["projects"] ?? {});
|
|
1323
|
-
const globalProject = projects[home] ?? {};
|
|
1324
|
-
const mcpServers = (globalProject["mcpServers"] ?? {});
|
|
1325
|
-
mcpServers["nodetool"] = {
|
|
1326
|
-
type: "http",
|
|
1327
|
-
url: mcpUrl
|
|
1328
|
-
};
|
|
1329
|
-
globalProject["mcpServers"] = mcpServers;
|
|
1330
|
-
projects[home] = globalProject;
|
|
1331
|
-
config["projects"] = projects;
|
|
1332
|
-
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
1333
|
-
results.push({
|
|
1334
|
-
target: "Claude Code",
|
|
1335
|
-
status: `Installed → ${claudeConfigPath}`
|
|
1336
|
-
});
|
|
1337
|
-
}
|
|
1338
|
-
catch (e) {
|
|
1339
|
-
results.push({ target: "Claude Code", status: `Error: ${e}` });
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
// ── Codex ────────────────────────────────────────────────────
|
|
1343
|
-
if (installAll || opts.codex) {
|
|
1344
|
-
try {
|
|
1345
|
-
const codexDir = join(home, ".codex");
|
|
1346
|
-
const codexConfigPath = join(codexDir, "config.toml");
|
|
1347
|
-
mkdirSync(codexDir, { recursive: true });
|
|
1348
|
-
const NODETOOL_MCP_BEGIN = "# BEGIN NODETOOL MCP";
|
|
1349
|
-
const NODETOOL_MCP_END = "# END NODETOOL MCP";
|
|
1350
|
-
const mcpBlock = [
|
|
1351
|
-
NODETOOL_MCP_BEGIN,
|
|
1352
|
-
"[mcp_servers.nodetool]",
|
|
1353
|
-
`url = "${mcpUrl}"`,
|
|
1354
|
-
"startup_timeout_sec = 20",
|
|
1355
|
-
"tool_timeout_sec = 60",
|
|
1356
|
-
"enabled = true",
|
|
1357
|
-
"required = true",
|
|
1358
|
-
NODETOOL_MCP_END
|
|
1359
|
-
].join("\n");
|
|
1360
|
-
let content = "";
|
|
1361
|
-
if (existsSync(codexConfigPath)) {
|
|
1362
|
-
content = readFileSync(codexConfigPath, "utf8");
|
|
1363
|
-
const re = new RegExp(`${NODETOOL_MCP_BEGIN}[\\s\\S]*?${NODETOOL_MCP_END}\\n?`);
|
|
1364
|
-
if (re.test(content)) {
|
|
1365
|
-
content = content.replace(re, mcpBlock + "\n");
|
|
1366
|
-
}
|
|
1367
|
-
else {
|
|
1368
|
-
content = content.trimEnd() + "\n\n" + mcpBlock + "\n";
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
else {
|
|
1372
|
-
content = mcpBlock + "\n";
|
|
1373
|
-
}
|
|
1374
|
-
writeFileSync(codexConfigPath, content);
|
|
1375
|
-
results.push({
|
|
1376
|
-
target: "Codex",
|
|
1377
|
-
status: `Installed → ${codexConfigPath}`
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
catch (e) {
|
|
1381
|
-
results.push({ target: "Codex", status: `Error: ${e}` });
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
// ── OpenCode ─────────────────────────────────────────────────
|
|
1385
|
-
if (installAll || opts.opencode) {
|
|
1386
|
-
try {
|
|
1387
|
-
const opencodeDir = join(home, ".config", "opencode");
|
|
1388
|
-
const opencodeConfigPath = join(opencodeDir, "opencode.json");
|
|
1389
|
-
mkdirSync(opencodeDir, { recursive: true });
|
|
1390
|
-
let config = {};
|
|
1391
|
-
if (existsSync(opencodeConfigPath)) {
|
|
1392
|
-
config = JSON.parse(readFileSync(opencodeConfigPath, "utf8"));
|
|
1393
|
-
}
|
|
1394
|
-
const mcpConfig = (config["mcp"] ?? {});
|
|
1395
|
-
mcpConfig["nodetool"] = {
|
|
1396
|
-
type: "remote",
|
|
1397
|
-
url: mcpUrl
|
|
1398
|
-
};
|
|
1399
|
-
config["mcp"] = mcpConfig;
|
|
1400
|
-
writeFileSync(opencodeConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
1401
|
-
results.push({
|
|
1402
|
-
target: "OpenCode",
|
|
1403
|
-
status: `Installed → ${opencodeConfigPath}`
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
catch (e) {
|
|
1407
|
-
results.push({ target: "OpenCode", status: `Error: ${e}` });
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
console.log(`\nNodeTool MCP Server: ${mcpUrl}\n`);
|
|
1411
|
-
printTable(results);
|
|
1412
|
-
console.log("\nMake sure the NodeTool server is running (nodetool serve) for MCP to work.\n");
|
|
1413
|
-
});
|
|
1414
|
-
mcp
|
|
1415
|
-
.command("uninstall")
|
|
1416
|
-
.description("Remove NodeTool MCP server config from all AI coding assistants")
|
|
1417
|
-
.option("--claude", "Uninstall from Claude Code only")
|
|
1418
|
-
.option("--codex", "Uninstall from Codex only")
|
|
1419
|
-
.option("--opencode", "Uninstall from OpenCode only")
|
|
1420
|
-
.action(async (opts) => {
|
|
1421
|
-
const { readFileSync, writeFileSync, existsSync } = await import("node:fs");
|
|
1422
|
-
const { join } = await import("node:path");
|
|
1423
|
-
const { homedir } = await import("node:os");
|
|
1424
|
-
const home = homedir();
|
|
1425
|
-
const uninstallAll = !opts.claude && !opts.codex && !opts.opencode;
|
|
1426
|
-
const results = [];
|
|
1427
|
-
// ── Claude Code ──────────────────────────────────────────────
|
|
1428
|
-
if (uninstallAll || opts.claude) {
|
|
1429
|
-
try {
|
|
1430
|
-
const claudeConfigPath = join(home, ".claude.json");
|
|
1431
|
-
if (existsSync(claudeConfigPath)) {
|
|
1432
|
-
const config = JSON.parse(readFileSync(claudeConfigPath, "utf8"));
|
|
1433
|
-
const projects = config["projects"] ?? {};
|
|
1434
|
-
const globalProject = projects[home] ?? {};
|
|
1435
|
-
const mcpServers = globalProject["mcpServers"] ?? {};
|
|
1436
|
-
if ("nodetool" in mcpServers) {
|
|
1437
|
-
delete mcpServers["nodetool"];
|
|
1438
|
-
globalProject["mcpServers"] = mcpServers;
|
|
1439
|
-
projects[home] = globalProject;
|
|
1440
|
-
config["projects"] = projects;
|
|
1441
|
-
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
1442
|
-
results.push({ target: "Claude Code", status: "Removed" });
|
|
1443
|
-
}
|
|
1444
|
-
else {
|
|
1445
|
-
results.push({ target: "Claude Code", status: "Not installed" });
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
else {
|
|
1449
|
-
results.push({ target: "Claude Code", status: "Config not found" });
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
catch (e) {
|
|
1453
|
-
results.push({ target: "Claude Code", status: `Error: ${e}` });
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
// ── Codex ────────────────────────────────────────────────────
|
|
1457
|
-
if (uninstallAll || opts.codex) {
|
|
1458
|
-
try {
|
|
1459
|
-
const codexConfigPath = join(home, ".codex", "config.toml");
|
|
1460
|
-
if (existsSync(codexConfigPath)) {
|
|
1461
|
-
let content = readFileSync(codexConfigPath, "utf8");
|
|
1462
|
-
const re = /# BEGIN NODETOOL MCP[\s\S]*?# END NODETOOL MCP\n?/;
|
|
1463
|
-
if (re.test(content)) {
|
|
1464
|
-
content = content.replace(re, "").trimEnd() + "\n";
|
|
1465
|
-
writeFileSync(codexConfigPath, content);
|
|
1466
|
-
results.push({ target: "Codex", status: "Removed" });
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
results.push({ target: "Codex", status: "Not installed" });
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
else {
|
|
1473
|
-
results.push({ target: "Codex", status: "Config not found" });
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
catch (e) {
|
|
1477
|
-
results.push({ target: "Codex", status: `Error: ${e}` });
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
// ── OpenCode ─────────────────────────────────────────────────
|
|
1481
|
-
if (uninstallAll || opts.opencode) {
|
|
1482
|
-
try {
|
|
1483
|
-
const opencodeConfigPath = join(home, ".config", "opencode", "opencode.json");
|
|
1484
|
-
if (existsSync(opencodeConfigPath)) {
|
|
1485
|
-
const config = JSON.parse(readFileSync(opencodeConfigPath, "utf8"));
|
|
1486
|
-
const mcpConfig = config["mcp"] ?? {};
|
|
1487
|
-
if ("nodetool" in mcpConfig) {
|
|
1488
|
-
delete mcpConfig["nodetool"];
|
|
1489
|
-
config["mcp"] = mcpConfig;
|
|
1490
|
-
writeFileSync(opencodeConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
1491
|
-
results.push({ target: "OpenCode", status: "Removed" });
|
|
1492
|
-
}
|
|
1493
|
-
else {
|
|
1494
|
-
results.push({ target: "OpenCode", status: "Not installed" });
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
else {
|
|
1498
|
-
results.push({ target: "OpenCode", status: "Config not found" });
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
catch (e) {
|
|
1502
|
-
results.push({ target: "OpenCode", status: `Error: ${e}` });
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
console.log();
|
|
1506
|
-
printTable(results);
|
|
1507
|
-
console.log();
|
|
1508
|
-
});
|
|
1509
|
-
mcp
|
|
1510
|
-
.command("status")
|
|
1511
|
-
.description("Show NodeTool MCP installation status for all AI coding assistants")
|
|
1512
|
-
.action(async () => {
|
|
1513
|
-
const { readFileSync, existsSync } = await import("node:fs");
|
|
1514
|
-
const { join } = await import("node:path");
|
|
1515
|
-
const { homedir } = await import("node:os");
|
|
1516
|
-
const home = homedir();
|
|
1517
|
-
const results = [];
|
|
1518
|
-
// Claude Code
|
|
1519
|
-
try {
|
|
1520
|
-
const claudeConfigPath = join(home, ".claude.json");
|
|
1521
|
-
if (existsSync(claudeConfigPath)) {
|
|
1522
|
-
const config = JSON.parse(readFileSync(claudeConfigPath, "utf8"));
|
|
1523
|
-
const mcpServers = config?.projects?.[home]?.mcpServers ?? {};
|
|
1524
|
-
if (mcpServers["nodetool"]) {
|
|
1525
|
-
results.push({
|
|
1526
|
-
target: "Claude Code",
|
|
1527
|
-
status: "Installed",
|
|
1528
|
-
url: mcpServers["nodetool"].url ?? ""
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
else {
|
|
1532
|
-
results.push({
|
|
1533
|
-
target: "Claude Code",
|
|
1534
|
-
status: "Not installed",
|
|
1535
|
-
url: ""
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
else {
|
|
1540
|
-
results.push({ target: "Claude Code", status: "No config", url: "" });
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
catch {
|
|
1544
|
-
results.push({
|
|
1545
|
-
target: "Claude Code",
|
|
1546
|
-
status: "Error reading config",
|
|
1547
|
-
url: ""
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
// Codex
|
|
1551
|
-
try {
|
|
1552
|
-
const codexConfigPath = join(home, ".codex", "config.toml");
|
|
1553
|
-
if (existsSync(codexConfigPath)) {
|
|
1554
|
-
const content = readFileSync(codexConfigPath, "utf8");
|
|
1555
|
-
const urlMatch = /# BEGIN NODETOOL MCP[\s\S]*?url\s*=\s*"([^"]*)"/.exec(content);
|
|
1556
|
-
if (urlMatch) {
|
|
1557
|
-
results.push({
|
|
1558
|
-
target: "Codex",
|
|
1559
|
-
status: "Installed",
|
|
1560
|
-
url: urlMatch[1] ?? ""
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
else {
|
|
1564
|
-
results.push({ target: "Codex", status: "Not installed", url: "" });
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
else {
|
|
1568
|
-
results.push({ target: "Codex", status: "No config", url: "" });
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
catch {
|
|
1572
|
-
results.push({
|
|
1573
|
-
target: "Codex",
|
|
1574
|
-
status: "Error reading config",
|
|
1575
|
-
url: ""
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
// OpenCode
|
|
1579
|
-
try {
|
|
1580
|
-
const opencodeConfigPath = join(home, ".config", "opencode", "opencode.json");
|
|
1581
|
-
if (existsSync(opencodeConfigPath)) {
|
|
1582
|
-
const config = JSON.parse(readFileSync(opencodeConfigPath, "utf8"));
|
|
1583
|
-
if (config?.mcp?.nodetool) {
|
|
1584
|
-
results.push({
|
|
1585
|
-
target: "OpenCode",
|
|
1586
|
-
status: "Installed",
|
|
1587
|
-
url: config.mcp.nodetool.url ?? ""
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
else {
|
|
1591
|
-
results.push({
|
|
1592
|
-
target: "OpenCode",
|
|
1593
|
-
status: "Not installed",
|
|
1594
|
-
url: ""
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
else {
|
|
1599
|
-
results.push({ target: "OpenCode", status: "No config", url: "" });
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
catch {
|
|
1603
|
-
results.push({
|
|
1604
|
-
target: "OpenCode",
|
|
1605
|
-
status: "Error reading config",
|
|
1606
|
-
url: ""
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
console.log("\nNodeTool MCP Integration Status\n");
|
|
1610
|
-
printTable(results);
|
|
1611
|
-
console.log();
|
|
1612
|
-
});
|
|
1613
|
-
mcp
|
|
1614
|
-
.command("serve")
|
|
1615
|
-
.description("Run the NodeTool MCP server over stdio for an external agent " +
|
|
1616
|
-
"(Claude Code, Codex, …). All logging goes to stderr; stdout is the " +
|
|
1617
|
-
"MCP protocol channel.")
|
|
1618
|
-
.action(async () => {
|
|
1619
|
-
try {
|
|
1620
|
-
const { createMcpServer, createMcpStdioTransport, forwardMcpStdioToLocalServer } = await import("@nodetool-ai/websocket");
|
|
1621
|
-
if (await forwardMcpStdioToLocalServer()) {
|
|
1622
|
-
process.stderr.write("NodeTool MCP forwarding to the local API server.\n");
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
await setupDb();
|
|
1626
|
-
// A local TS-node registry powers list_nodes / search_nodes /
|
|
1627
|
-
// validate_workflow without spinning up a Python worker.
|
|
1628
|
-
const registry = new NodeRegistry();
|
|
1629
|
-
registerBaseNodes(registry);
|
|
1630
|
-
registerElevenLabsNodes(registry);
|
|
1631
|
-
registerMinimaxNodes(registry);
|
|
1632
|
-
registerTransformersJsNodes(registry);
|
|
1633
|
-
registerFalNodes(registry);
|
|
1634
|
-
registerReplicateNodes(registry);
|
|
1635
|
-
registerReveNodes(registry);
|
|
1636
|
-
registerHuggingFaceNodes(registry);
|
|
1637
|
-
// stdio serves exactly one local user; "1" is the local single-user id.
|
|
1638
|
-
const server = createMcpServer({
|
|
1639
|
-
registry,
|
|
1640
|
-
agentToolsScope: { userId: "1", source: "stdio-local" }
|
|
1641
|
-
});
|
|
1642
|
-
const transport = createMcpStdioTransport();
|
|
1643
|
-
await server.connect(transport);
|
|
1644
|
-
process.stderr.write("NodeTool MCP server ready on stdio.\n");
|
|
1645
|
-
// The stdio transport keeps the event loop alive until the client
|
|
1646
|
-
// closes the connection.
|
|
1647
|
-
}
|
|
1648
|
-
catch (e) {
|
|
1649
|
-
printCommandError(e);
|
|
1650
|
-
process.exit(1);
|
|
1651
|
-
}
|
|
1652
|
-
});
|
|
1653
|
-
// ---------------------------------------------------------------------------
|
|
1654
|
-
// package
|
|
1655
|
-
// ---------------------------------------------------------------------------
|
|
1656
|
-
registerPackageCommands(program);
|
|
1657
|
-
registerPackCommands(program);
|
|
1658
|
-
registerWorkerCommands(program);
|
|
1659
|
-
// Any command that opens a remote Python bridge keeps the attached worker's
|
|
1660
|
-
// idle clock fresh, so the reaper doesn't pause a GPU that is mid-job.
|
|
1661
|
-
installWorkerActivityHeartbeat(onPythonBridgeCreated);
|
|
1662
|
-
// ---------------------------------------------------------------------------
|
|
1663
|
-
// deploy — registered from commands/deploy.ts
|
|
1664
|
-
// ---------------------------------------------------------------------------
|
|
1665
|
-
registerDeployCommands(program);
|
|
1666
|
-
registerAgentCommands(program);
|
|
1667
|
-
registerDbCommands(program);
|
|
1668
|
-
registerDebugCommands(program);
|
|
1669
|
-
registerAppCommands(program);
|
|
1670
|
-
registerAppsCommands(program);
|
|
1671
|
-
registerValidateCommand(program);
|
|
1672
|
-
registerTimelineCommands(program);
|
|
1673
|
-
registerSketchCommands(program);
|
|
1674
|
-
registerGameCommands(program);
|
|
1675
|
-
registerJsScriptCommands(program);
|
|
1676
|
-
registerReliabilityCommands(program);
|
|
1677
|
-
registerNodeCommands(program);
|
|
1678
|
-
registerGenerateCommand(program);
|
|
1679
|
-
registerEvalCommand(program);
|
|
1680
|
-
registerJtbdCommand(program, setupDb);
|
|
1681
|
-
registerAffectedCommand(program);
|
|
1682
|
-
registerHarnessCommands(program);
|
|
1683
|
-
registerCollectionCommands(program);
|
|
1684
|
-
registerCostsCommands(program);
|
|
1685
|
-
registerGenerationsCommands(program);
|
|
1686
|
-
registerAuthCommands(program);
|
|
1687
|
-
registerTelegramCommands(program);
|
|
1688
|
-
// ---------------------------------------------------------------------------
|
|
1689
|
-
program.parse();
|
|
23
|
+
export {};
|
|
1690
24
|
//# sourceMappingURL=nodetool.js.map
|