refactor(nodes): share run parsing helpers

This commit is contained in:
Peter Steinberger
2026-01-08 00:24:11 +00:00
parent e35845dd49
commit 67213e0fc6
4 changed files with 55 additions and 54 deletions

View File

@@ -118,6 +118,20 @@ Notes:
- Images return image blocks + `MEDIA:<path>`.
- Videos return `FILE:<path>` (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.

View File

@@ -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<string, string> | undefined =
Array.isArray(envRaw) && envRaw.length > 0
? (() => {
const parsed: Record<string, string> = {};
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

View File

@@ -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<string, string> = {};
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<string, unknown> = {
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;
}

30
src/cli/nodes-run.ts Normal file
View File

@@ -0,0 +1,30 @@
export function parseEnvPairs(
pairs: unknown,
): Record<string, string> | undefined {
if (!Array.isArray(pairs) || pairs.length === 0) return undefined;
const env: Record<string, string> = {};
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;
}