From 67213e0fc6d541c1ff0a2c937b94f96111ca351b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 8 Jan 2026 00:24:11 +0000 Subject: [PATCH] refactor(nodes): share run parsing helpers --- docs/tools/index.md | 18 ++++++++++++++++-- src/agents/tools/nodes-tool.ts | 31 ++++--------------------------- src/cli/nodes-cli.ts | 30 +++++------------------------- src/cli/nodes-run.ts | 30 ++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 54 deletions(-) create mode 100644 src/cli/nodes-run.ts diff --git a/docs/tools/index.md b/docs/tools/index.md index 75c5bdb67..408051f2f 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -118,6 +118,20 @@ Notes: - Images return image blocks + `MEDIA:`. - Videos return `FILE:` (mp4). - Location returns a JSON payload (lat/lon/accuracy/timestamp). +- `run` params: `command` argv array; optional `cwd`, `env` (`KEY=VAL`), `commandTimeoutMs`, `invokeTimeoutMs`, `needsScreenRecording`. + +Example (`run`): +```json +{ + "action": "run", + "node": "office-mac", + "command": ["echo", "Hello"], + "env": ["FOO=bar"], + "commandTimeoutMs": 12000, + "invokeTimeoutMs": 45000, + "needsScreenRecording": false +} +``` ### `image` Analyze an image with the configured image model. @@ -260,11 +274,11 @@ Canvas render: Node targeting: 1) `nodes` → `status` 2) `describe` on the chosen node -3) `notify` / `camera_snap` / `screen_record` +3) `notify` / `run` / `camera_snap` / `screen_record` ## Safety -- Avoid `system.run` (not exposed as a tool). +- Avoid direct `system.run`; use `nodes` → `run` only with explicit user consent. - Respect user consent for camera/screen capture. - Use `status/describe` to ensure permissions before invoking media commands. diff --git a/src/agents/tools/nodes-tool.ts b/src/agents/tools/nodes-tool.ts index c8c648581..b3c47dfa2 100644 --- a/src/agents/tools/nodes-tool.ts +++ b/src/agents/tools/nodes-tool.ts @@ -10,6 +10,7 @@ import { parseCameraSnapPayload, writeBase64ToFile, } from "../../cli/nodes-camera.js"; +import { parseEnvPairs, parseTimeoutMs } from "../../cli/nodes-run.js"; import { parseScreenRecordPayload, screenRecordTempPath, @@ -513,33 +514,9 @@ export function createNodesTool(): AnyAgentTool { typeof params.cwd === "string" && params.cwd.trim() ? params.cwd.trim() : undefined; - const envRaw = params.env; - const env: Record | undefined = - Array.isArray(envRaw) && envRaw.length > 0 - ? (() => { - const parsed: Record = {}; - for (const pair of envRaw) { - if (typeof pair !== "string") continue; - const idx = pair.indexOf("="); - if (idx <= 0) continue; - const key = pair.slice(0, idx).trim(); - const value = pair.slice(idx + 1); - if (!key) continue; - parsed[key] = value; - } - return Object.keys(parsed).length > 0 ? parsed : undefined; - })() - : undefined; - const commandTimeoutMs = - typeof params.commandTimeoutMs === "number" && - Number.isFinite(params.commandTimeoutMs) - ? params.commandTimeoutMs - : undefined; - const invokeTimeoutMs = - typeof params.invokeTimeoutMs === "number" && - Number.isFinite(params.invokeTimeoutMs) - ? params.invokeTimeoutMs - : undefined; + const env = parseEnvPairs(params.env); + const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs); + const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs); const needsScreenRecording = typeof params.needsScreenRecording === "boolean" ? params.needsScreenRecording diff --git a/src/cli/nodes-cli.ts b/src/cli/nodes-cli.ts index 0e446bbe4..b19fc9c1d 100644 --- a/src/cli/nodes-cli.ts +++ b/src/cli/nodes-cli.ts @@ -12,6 +12,7 @@ import { canvasSnapshotTempPath, parseCanvasSnapshotPayload, } from "./nodes-canvas.js"; +import { parseEnvPairs, parseTimeoutMs } from "./nodes-run.js"; import { parseScreenRecordPayload, screenRecordTempPath, @@ -194,20 +195,6 @@ function normalizeNodeKey(value: string) { .replace(/-+$/, ""); } -function parseEnvPairs(pairs: string[] | undefined) { - if (!Array.isArray(pairs) || pairs.length === 0) return undefined; - const env: Record = {}; - for (const pair of pairs) { - const idx = pair.indexOf("="); - if (idx <= 0) continue; - const key = pair.slice(0, idx).trim(); - const value = pair.slice(idx + 1); - if (!key) continue; - env[key] = value; - } - return Object.keys(env).length > 0 ? env : undefined; -} - async function resolveNodeId(opts: NodesRpcOpts, query: string) { const q = String(query ?? "").trim(); if (!q) throw new Error("node required"); @@ -598,12 +585,8 @@ export function registerNodesCli(program: Command) { throw new Error("command required"); } const env = parseEnvPairs(opts.env); - const timeoutMs = opts.commandTimeout - ? Number.parseInt(String(opts.commandTimeout), 10) - : undefined; - const invokeTimeout = opts.invokeTimeout - ? Number.parseInt(String(opts.invokeTimeout), 10) - : undefined; + const timeoutMs = parseTimeoutMs(opts.commandTimeout); + const invokeTimeout = parseTimeoutMs(opts.invokeTimeout); const invokeParams: Record = { nodeId, @@ -612,17 +595,14 @@ export function registerNodesCli(program: Command) { command, cwd: opts.cwd, env, - timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : undefined, + timeoutMs, needsScreenRecording: opts.needsScreenRecording === true, }, idempotencyKey: String( opts.idempotencyKey ?? randomIdempotencyKey(), ), }; - if ( - typeof invokeTimeout === "number" && - Number.isFinite(invokeTimeout) - ) { + if (invokeTimeout !== undefined) { invokeParams.timeoutMs = invokeTimeout; } diff --git a/src/cli/nodes-run.ts b/src/cli/nodes-run.ts new file mode 100644 index 000000000..7d375e97f --- /dev/null +++ b/src/cli/nodes-run.ts @@ -0,0 +1,30 @@ +export function parseEnvPairs( + pairs: unknown, +): Record | undefined { + if (!Array.isArray(pairs) || pairs.length === 0) return undefined; + const env: Record = {}; + for (const pair of pairs) { + if (typeof pair !== "string") continue; + const idx = pair.indexOf("="); + if (idx <= 0) continue; + const key = pair.slice(0, idx).trim(); + if (!key) continue; + env[key] = pair.slice(idx + 1); + } + return Object.keys(env).length > 0 ? env : undefined; +} + +export function parseTimeoutMs(raw: unknown): number | undefined { + if (raw === undefined || raw === null) return undefined; + let value = Number.NaN; + if (typeof raw === "number") { + value = raw; + } else if (typeof raw === "bigint") { + value = Number(raw); + } else if (typeof raw === "string") { + const trimmed = raw.trim(); + if (!trimmed) return undefined; + value = Number.parseInt(trimmed, 10); + } + return Number.isFinite(value) ? value : undefined; +}