refactor(nodes): share run parsing helpers
This commit is contained in:
@@ -118,6 +118,20 @@ Notes:
|
|||||||
- Images return image blocks + `MEDIA:<path>`.
|
- Images return image blocks + `MEDIA:<path>`.
|
||||||
- Videos return `FILE:<path>` (mp4).
|
- Videos return `FILE:<path>` (mp4).
|
||||||
- Location returns a JSON payload (lat/lon/accuracy/timestamp).
|
- 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`
|
### `image`
|
||||||
Analyze an image with the configured image model.
|
Analyze an image with the configured image model.
|
||||||
@@ -260,11 +274,11 @@ Canvas render:
|
|||||||
Node targeting:
|
Node targeting:
|
||||||
1) `nodes` → `status`
|
1) `nodes` → `status`
|
||||||
2) `describe` on the chosen node
|
2) `describe` on the chosen node
|
||||||
3) `notify` / `camera_snap` / `screen_record`
|
3) `notify` / `run` / `camera_snap` / `screen_record`
|
||||||
|
|
||||||
## Safety
|
## 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.
|
- Respect user consent for camera/screen capture.
|
||||||
- Use `status/describe` to ensure permissions before invoking media commands.
|
- Use `status/describe` to ensure permissions before invoking media commands.
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
parseCameraSnapPayload,
|
parseCameraSnapPayload,
|
||||||
writeBase64ToFile,
|
writeBase64ToFile,
|
||||||
} from "../../cli/nodes-camera.js";
|
} from "../../cli/nodes-camera.js";
|
||||||
|
import { parseEnvPairs, parseTimeoutMs } from "../../cli/nodes-run.js";
|
||||||
import {
|
import {
|
||||||
parseScreenRecordPayload,
|
parseScreenRecordPayload,
|
||||||
screenRecordTempPath,
|
screenRecordTempPath,
|
||||||
@@ -513,33 +514,9 @@ export function createNodesTool(): AnyAgentTool {
|
|||||||
typeof params.cwd === "string" && params.cwd.trim()
|
typeof params.cwd === "string" && params.cwd.trim()
|
||||||
? params.cwd.trim()
|
? params.cwd.trim()
|
||||||
: undefined;
|
: undefined;
|
||||||
const envRaw = params.env;
|
const env = parseEnvPairs(params.env);
|
||||||
const env: Record<string, string> | undefined =
|
const commandTimeoutMs = parseTimeoutMs(params.commandTimeoutMs);
|
||||||
Array.isArray(envRaw) && envRaw.length > 0
|
const invokeTimeoutMs = parseTimeoutMs(params.invokeTimeoutMs);
|
||||||
? (() => {
|
|
||||||
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 needsScreenRecording =
|
const needsScreenRecording =
|
||||||
typeof params.needsScreenRecording === "boolean"
|
typeof params.needsScreenRecording === "boolean"
|
||||||
? params.needsScreenRecording
|
? params.needsScreenRecording
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
canvasSnapshotTempPath,
|
canvasSnapshotTempPath,
|
||||||
parseCanvasSnapshotPayload,
|
parseCanvasSnapshotPayload,
|
||||||
} from "./nodes-canvas.js";
|
} from "./nodes-canvas.js";
|
||||||
|
import { parseEnvPairs, parseTimeoutMs } from "./nodes-run.js";
|
||||||
import {
|
import {
|
||||||
parseScreenRecordPayload,
|
parseScreenRecordPayload,
|
||||||
screenRecordTempPath,
|
screenRecordTempPath,
|
||||||
@@ -194,20 +195,6 @@ function normalizeNodeKey(value: string) {
|
|||||||
.replace(/-+$/, "");
|
.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) {
|
async function resolveNodeId(opts: NodesRpcOpts, query: string) {
|
||||||
const q = String(query ?? "").trim();
|
const q = String(query ?? "").trim();
|
||||||
if (!q) throw new Error("node required");
|
if (!q) throw new Error("node required");
|
||||||
@@ -598,12 +585,8 @@ export function registerNodesCli(program: Command) {
|
|||||||
throw new Error("command required");
|
throw new Error("command required");
|
||||||
}
|
}
|
||||||
const env = parseEnvPairs(opts.env);
|
const env = parseEnvPairs(opts.env);
|
||||||
const timeoutMs = opts.commandTimeout
|
const timeoutMs = parseTimeoutMs(opts.commandTimeout);
|
||||||
? Number.parseInt(String(opts.commandTimeout), 10)
|
const invokeTimeout = parseTimeoutMs(opts.invokeTimeout);
|
||||||
: undefined;
|
|
||||||
const invokeTimeout = opts.invokeTimeout
|
|
||||||
? Number.parseInt(String(opts.invokeTimeout), 10)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const invokeParams: Record<string, unknown> = {
|
const invokeParams: Record<string, unknown> = {
|
||||||
nodeId,
|
nodeId,
|
||||||
@@ -612,17 +595,14 @@ export function registerNodesCli(program: Command) {
|
|||||||
command,
|
command,
|
||||||
cwd: opts.cwd,
|
cwd: opts.cwd,
|
||||||
env,
|
env,
|
||||||
timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : undefined,
|
timeoutMs,
|
||||||
needsScreenRecording: opts.needsScreenRecording === true,
|
needsScreenRecording: opts.needsScreenRecording === true,
|
||||||
},
|
},
|
||||||
idempotencyKey: String(
|
idempotencyKey: String(
|
||||||
opts.idempotencyKey ?? randomIdempotencyKey(),
|
opts.idempotencyKey ?? randomIdempotencyKey(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
if (
|
if (invokeTimeout !== undefined) {
|
||||||
typeof invokeTimeout === "number" &&
|
|
||||||
Number.isFinite(invokeTimeout)
|
|
||||||
) {
|
|
||||||
invokeParams.timeoutMs = invokeTimeout;
|
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