refactor(nodes): share run parsing helpers
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
30
src/cli/nodes-run.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user