@suwujs/king-ai 0.2.0
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 +96 -0
- package/dist/src/agent-config-validation.d.ts +9 -0
- package/dist/src/agent-config-validation.js +30 -0
- package/dist/src/api.d.ts +4 -0
- package/dist/src/api.js +48 -0
- package/dist/src/attachments.d.ts +45 -0
- package/dist/src/attachments.js +322 -0
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +1697 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +20 -0
- package/dist/src/cron.d.ts +11 -0
- package/dist/src/cron.js +65 -0
- package/dist/src/daemon.d.ts +36 -0
- package/dist/src/daemon.js +373 -0
- package/dist/src/engine.d.ts +32 -0
- package/dist/src/engine.js +1014 -0
- package/dist/src/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat.js +28 -0
- package/dist/src/host-api.d.ts +40 -0
- package/dist/src/host-api.js +59 -0
- package/dist/src/host-control.d.ts +48 -0
- package/dist/src/host-control.js +1279 -0
- package/dist/src/host-export.d.ts +50 -0
- package/dist/src/host-export.js +187 -0
- package/dist/src/host-feedback.d.ts +78 -0
- package/dist/src/host-feedback.js +178 -0
- package/dist/src/host-home.d.ts +13 -0
- package/dist/src/host-home.js +54 -0
- package/dist/src/host-ledger.d.ts +261 -0
- package/dist/src/host-ledger.js +554 -0
- package/dist/src/host-loop-events.d.ts +69 -0
- package/dist/src/host-loop-events.js +288 -0
- package/dist/src/host-permission.d.ts +36 -0
- package/dist/src/host-permission.js +180 -0
- package/dist/src/host-policy.d.ts +15 -0
- package/dist/src/host-policy.js +36 -0
- package/dist/src/host-run-executor.d.ts +13 -0
- package/dist/src/host-run-executor.js +221 -0
- package/dist/src/host-run-heartbeat.d.ts +40 -0
- package/dist/src/host-run-heartbeat.js +103 -0
- package/dist/src/host-run-layout.d.ts +17 -0
- package/dist/src/host-run-layout.js +387 -0
- package/dist/src/host-run-meta.d.ts +41 -0
- package/dist/src/host-run-meta.js +115 -0
- package/dist/src/host-run-spec.d.ts +149 -0
- package/dist/src/host-run-spec.js +465 -0
- package/dist/src/host-runs.d.ts +77 -0
- package/dist/src/host-runs.js +195 -0
- package/dist/src/host-sdk.d.ts +412 -0
- package/dist/src/host-sdk.js +628 -0
- package/dist/src/host-server.d.ts +26 -0
- package/dist/src/host-server.js +921 -0
- package/dist/src/host-timeline.d.ts +24 -0
- package/dist/src/host-timeline.js +161 -0
- package/dist/src/jsonl.d.ts +13 -0
- package/dist/src/jsonl.js +47 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +18 -0
- package/dist/src/message-routing.d.ts +32 -0
- package/dist/src/message-routing.js +119 -0
- package/dist/src/paths.d.ts +19 -0
- package/dist/src/paths.js +26 -0
- package/dist/src/project-profile.d.ts +49 -0
- package/dist/src/project-profile.js +356 -0
- package/dist/src/remediation.d.ts +14 -0
- package/dist/src/remediation.js +114 -0
- package/dist/src/remote-devices.d.ts +41 -0
- package/dist/src/remote-devices.js +156 -0
- package/dist/src/remote-diagnostics.d.ts +39 -0
- package/dist/src/remote-diagnostics.js +199 -0
- package/dist/src/remote-ssh.d.ts +39 -0
- package/dist/src/remote-ssh.js +129 -0
- package/dist/src/run-stream.d.ts +57 -0
- package/dist/src/run-stream.js +119 -0
- package/dist/src/runner.d.ts +131 -0
- package/dist/src/runner.js +1161 -0
- package/dist/src/runtime-data.d.ts +68 -0
- package/dist/src/runtime-data.js +172 -0
- package/dist/src/service.d.ts +114 -0
- package/dist/src/service.js +631 -0
- package/dist/src/shared-skills.d.ts +26 -0
- package/dist/src/shared-skills.js +85 -0
- package/dist/src/shim.d.ts +1 -0
- package/dist/src/shim.js +64 -0
- package/dist/src/skill-check.d.ts +17 -0
- package/dist/src/skill-check.js +158 -0
- package/dist/src/sse.d.ts +9 -0
- package/dist/src/sse.js +36 -0
- package/dist/src/team-routing.d.ts +55 -0
- package/dist/src/team-routing.js +131 -0
- package/dist/src/team-workflow.d.ts +78 -0
- package/dist/src/team-workflow.js +253 -0
- package/dist/src/text.d.ts +7 -0
- package/dist/src/text.js +27 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +1 -0
- package/dist/src/usage.d.ts +116 -0
- package/dist/src/usage.js +350 -0
- package/dist/src/workspace.d.ts +9 -0
- package/dist/src/workspace.js +56 -0
- package/dist/src/worktree.d.ts +47 -0
- package/dist/src/worktree.js +201 -0
- package/package.json +63 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RemoteDevicesConfig } from "./remote-devices.js";
|
|
2
|
+
import type { RemoteCommandExecutor, RemoteExecResult } from "./remote-ssh.js";
|
|
3
|
+
export interface RemoteCommandInput {
|
|
4
|
+
device?: unknown;
|
|
5
|
+
timeoutMs?: unknown;
|
|
6
|
+
maxOutputBytes?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface RemoteRunInput extends RemoteCommandInput {
|
|
9
|
+
cmd?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export interface RemoteLogsInput extends RemoteCommandInput {
|
|
12
|
+
app?: unknown;
|
|
13
|
+
path?: unknown;
|
|
14
|
+
tail?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface RemoteFindLogsInput extends RemoteLogsInput {
|
|
17
|
+
pattern?: unknown;
|
|
18
|
+
since?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface RemoteServiceInput extends RemoteCommandInput {
|
|
21
|
+
db?: unknown;
|
|
22
|
+
name?: unknown;
|
|
23
|
+
sql?: unknown;
|
|
24
|
+
cmd?: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface RemoteDeviceCommandDeps {
|
|
27
|
+
config: RemoteDevicesConfig;
|
|
28
|
+
env?: NodeJS.ProcessEnv;
|
|
29
|
+
executor?: RemoteCommandExecutor;
|
|
30
|
+
}
|
|
31
|
+
export declare function formatRemoteResult(result: RemoteExecResult): string;
|
|
32
|
+
export declare function formatRemoteDevices(config: RemoteDevicesConfig): string;
|
|
33
|
+
export declare function remoteProbe(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
34
|
+
export declare function remoteProfile(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
35
|
+
export declare function remoteRun(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
36
|
+
export declare function remoteLogs(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
37
|
+
export declare function remoteFindLogs(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
38
|
+
export declare function remotePg(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
39
|
+
export declare function remoteRedis(input: unknown, deps: RemoteDeviceCommandDeps): Promise<RemoteExecResult>;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { findRemoteDevice, listRemoteDeviceSummaries } from "./remote-devices.js";
|
|
2
|
+
import { sshExec } from "./remote-ssh.js";
|
|
3
|
+
export function formatRemoteResult(result) {
|
|
4
|
+
const status = result.ok ? "ok" : "failed";
|
|
5
|
+
const parts = [
|
|
6
|
+
`remote ${status}: ${result.device} ${result.command}`,
|
|
7
|
+
result.stdout.trim(),
|
|
8
|
+
result.stderr.trim() ? `stderr:\n${result.stderr.trim()}` : "",
|
|
9
|
+
result.truncated ? "(output truncated)" : "",
|
|
10
|
+
result.error ? `error: ${result.error}` : ""
|
|
11
|
+
].filter(Boolean);
|
|
12
|
+
return parts.join("\n");
|
|
13
|
+
}
|
|
14
|
+
export function formatRemoteDevices(config) {
|
|
15
|
+
const rows = listRemoteDeviceSummaries(config);
|
|
16
|
+
if (rows.length === 0)
|
|
17
|
+
return "no remote test devices configured";
|
|
18
|
+
return rows.map((device) => `${device.id}\t${device.user}@${device.host}:${device.port ?? 22}\t${device.name ?? ""}\t${device.auth}`).join("\n");
|
|
19
|
+
}
|
|
20
|
+
export async function remoteProbe(input, deps) {
|
|
21
|
+
const parsed = normalizeRemoteCommandInput(input);
|
|
22
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
23
|
+
return sshExec(device, "hostname && whoami && pwd && uptime", execOptions(parsed, deps));
|
|
24
|
+
}
|
|
25
|
+
export async function remoteProfile(input, deps) {
|
|
26
|
+
const parsed = normalizeRemoteCommandInput(input);
|
|
27
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
28
|
+
const markers = Object.values(device.apps ?? {}).flatMap((app) => app.installMarkers ?? []);
|
|
29
|
+
const markerScript = markers.length
|
|
30
|
+
? markers.map((marker) => `if [ -f ${sh(marker)} ]; then echo "marker ${marker}=$(cat ${sh(marker)} 2>/dev/null)"; fi`).join("\n")
|
|
31
|
+
: "true";
|
|
32
|
+
const logRoots = Object.values(device.apps ?? {}).flatMap((app) => app.logRoots ?? []);
|
|
33
|
+
const logScript = logRoots.length
|
|
34
|
+
? `for d in ${logRoots.map(sh).join(" ")}; do if [ -d "$d" ]; then echo "logroot $d"; find "$d" -type f -maxdepth 2 2>/dev/null | xargs ls -t 2>/dev/null | head -20; fi; done`
|
|
35
|
+
: "true";
|
|
36
|
+
const command = [
|
|
37
|
+
"echo '[system]'",
|
|
38
|
+
"hostname",
|
|
39
|
+
"whoami",
|
|
40
|
+
"uptime",
|
|
41
|
+
"echo '[install-markers]'",
|
|
42
|
+
markerScript,
|
|
43
|
+
"echo '[services]'",
|
|
44
|
+
"command -v systemctl >/dev/null 2>&1 && systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null | head -50 || ps -ef | head -30",
|
|
45
|
+
"echo '[logs]'",
|
|
46
|
+
logScript
|
|
47
|
+
].join("\n");
|
|
48
|
+
const result = await sshExec(device, command, execOptions(parsed, deps));
|
|
49
|
+
return { ...result, evidence: [{ kind: "profile", source: `${device.user}@${device.host}`, text: "remote environment profile" }] };
|
|
50
|
+
}
|
|
51
|
+
export async function remoteRun(input, deps) {
|
|
52
|
+
const parsed = normalizeRemoteRunInput(input);
|
|
53
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
54
|
+
return sshExec(device, parsed.cmd, execOptions(parsed, deps));
|
|
55
|
+
}
|
|
56
|
+
export async function remoteLogs(input, deps) {
|
|
57
|
+
const parsed = normalizeRemoteLogsInput(input);
|
|
58
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
59
|
+
const app = appConfig(device, parsed.app);
|
|
60
|
+
const target = parsed.path || newestLogCommand(app.logRoots ?? []);
|
|
61
|
+
const command = parsed.path
|
|
62
|
+
? `tail -n ${parsed.tail} ${sh(parsed.path)}`
|
|
63
|
+
: `f=$(${target}); if [ -n "$f" ]; then echo "==> $f <=="; tail -n ${parsed.tail} "$f"; else echo "no log file found"; exit 2; fi`;
|
|
64
|
+
const result = await sshExec(device, command, execOptions(parsed, deps));
|
|
65
|
+
return { ...result, evidence: [{ kind: "log", source: parsed.path || parsed.app || device.defaultApp || "logs", text: result.stdout.slice(0, 2000) }] };
|
|
66
|
+
}
|
|
67
|
+
export async function remoteFindLogs(input, deps) {
|
|
68
|
+
const parsed = normalizeRemoteFindLogsInput(input);
|
|
69
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
70
|
+
const app = appConfig(device, parsed.app);
|
|
71
|
+
const roots = app.logRoots ?? [];
|
|
72
|
+
if (roots.length === 0 && !parsed.path)
|
|
73
|
+
throw new Error("remote-find-logs requires path or app logRoots");
|
|
74
|
+
const sources = parsed.path ? [parsed.path] : roots;
|
|
75
|
+
const since = parsed.since ? ` since=${parsed.since}` : "";
|
|
76
|
+
const command = [
|
|
77
|
+
`echo "pattern=${parsed.pattern}${since}"`,
|
|
78
|
+
`for p in ${sources.map(sh).join(" ")}; do`,
|
|
79
|
+
" if [ -f \"$p\" ]; then grep -E -n --color=never " + sh(parsed.pattern) + " \"$p\" | tail -n " + parsed.tail + "; fi",
|
|
80
|
+
" if [ -d \"$p\" ]; then find \"$p\" -type f -maxdepth 2 2>/dev/null | xargs grep -E -n --color=never " + sh(parsed.pattern) + " 2>/dev/null | tail -n " + parsed.tail + "; fi",
|
|
81
|
+
"done"
|
|
82
|
+
].join("\n");
|
|
83
|
+
const result = await sshExec(device, command, execOptions(parsed, deps));
|
|
84
|
+
return { ...result, evidence: [{ kind: "log", source: sources.join(","), text: result.stdout.slice(0, 2000) }] };
|
|
85
|
+
}
|
|
86
|
+
export async function remotePg(input, deps) {
|
|
87
|
+
const parsed = normalizeRemoteServiceInput(input, "sql");
|
|
88
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
89
|
+
const name = parsed.db || "default";
|
|
90
|
+
const service = device.databases?.[name];
|
|
91
|
+
if (!service)
|
|
92
|
+
throw new Error(`remote database not configured: ${name}`);
|
|
93
|
+
const command = `${service.command} -c ${sh(parsed.sql)}`;
|
|
94
|
+
const result = await sshExec(device, command, execOptions(parsed, deps));
|
|
95
|
+
return { ...result, evidence: [{ kind: "database", source: name, text: parsed.sql }] };
|
|
96
|
+
}
|
|
97
|
+
export async function remoteRedis(input, deps) {
|
|
98
|
+
const parsed = normalizeRemoteServiceInput(input, "cmd");
|
|
99
|
+
const device = findRemoteDevice(deps.config, parsed.device);
|
|
100
|
+
const name = parsed.name || "default";
|
|
101
|
+
const service = device.redis?.[name];
|
|
102
|
+
if (!service)
|
|
103
|
+
throw new Error(`remote redis not configured: ${name}`);
|
|
104
|
+
const command = `${service.command} ${parsed.cmd}`;
|
|
105
|
+
const result = await sshExec(device, command, execOptions(parsed, deps));
|
|
106
|
+
return { ...result, evidence: [{ kind: "redis", source: name, text: parsed.cmd }] };
|
|
107
|
+
}
|
|
108
|
+
function execOptions(input, deps) {
|
|
109
|
+
return {
|
|
110
|
+
timeoutMs: input.timeoutMs,
|
|
111
|
+
maxOutputBytes: input.maxOutputBytes,
|
|
112
|
+
env: deps.env,
|
|
113
|
+
executor: deps.executor
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function normalizeRemoteCommandInput(input) {
|
|
117
|
+
const record = objectInput(input);
|
|
118
|
+
return {
|
|
119
|
+
device: optionalString(record.device),
|
|
120
|
+
timeoutMs: optionalPositiveInt(record.timeoutMs, "timeoutMs"),
|
|
121
|
+
maxOutputBytes: optionalPositiveInt(record.maxOutputBytes, "maxOutputBytes")
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function normalizeRemoteRunInput(input) {
|
|
125
|
+
const base = normalizeRemoteCommandInput(input);
|
|
126
|
+
const record = objectInput(input);
|
|
127
|
+
return { ...base, cmd: requiredString(record.cmd, "cmd") };
|
|
128
|
+
}
|
|
129
|
+
function normalizeRemoteLogsInput(input) {
|
|
130
|
+
const base = normalizeRemoteCommandInput(input);
|
|
131
|
+
const record = objectInput(input);
|
|
132
|
+
return {
|
|
133
|
+
...base,
|
|
134
|
+
app: optionalString(record.app),
|
|
135
|
+
path: optionalString(record.path),
|
|
136
|
+
tail: optionalPositiveInt(record.tail, "tail") ?? 200
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function normalizeRemoteFindLogsInput(input) {
|
|
140
|
+
const base = normalizeRemoteLogsInput(input);
|
|
141
|
+
const record = objectInput(input);
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
pattern: requiredString(record.pattern, "pattern"),
|
|
145
|
+
since: optionalString(record.since)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function normalizeRemoteServiceInput(input, field) {
|
|
149
|
+
const base = normalizeRemoteCommandInput(input);
|
|
150
|
+
const record = objectInput(input);
|
|
151
|
+
const value = requiredString(record[field], field);
|
|
152
|
+
return {
|
|
153
|
+
...base,
|
|
154
|
+
db: optionalString(record.db),
|
|
155
|
+
name: optionalString(record.name),
|
|
156
|
+
sql: field === "sql" ? value : "",
|
|
157
|
+
cmd: field === "cmd" ? value : ""
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function objectInput(input) {
|
|
161
|
+
if (!input)
|
|
162
|
+
return {};
|
|
163
|
+
if (typeof input !== "object")
|
|
164
|
+
throw new Error("remote command input must be an object");
|
|
165
|
+
return input;
|
|
166
|
+
}
|
|
167
|
+
function appConfig(device, appName) {
|
|
168
|
+
const name = appName || device.defaultApp;
|
|
169
|
+
if (!name)
|
|
170
|
+
return {};
|
|
171
|
+
const app = device.apps?.[name];
|
|
172
|
+
if (!app)
|
|
173
|
+
throw new Error(`remote app not configured: ${name}`);
|
|
174
|
+
return app;
|
|
175
|
+
}
|
|
176
|
+
function newestLogCommand(roots) {
|
|
177
|
+
if (roots.length === 0)
|
|
178
|
+
return "find /var/log -type f 2>/dev/null | xargs ls -t 2>/dev/null | head -1";
|
|
179
|
+
return `for d in ${roots.map(sh).join(" ")}; do [ -d "$d" ] && find "$d" -type f -maxdepth 2 2>/dev/null; done | xargs ls -t 2>/dev/null | head -1`;
|
|
180
|
+
}
|
|
181
|
+
function requiredString(raw, label) {
|
|
182
|
+
if (typeof raw !== "string" || !raw.trim())
|
|
183
|
+
throw new Error(`${label} is required`);
|
|
184
|
+
return raw.trim();
|
|
185
|
+
}
|
|
186
|
+
function optionalString(raw) {
|
|
187
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
|
|
188
|
+
}
|
|
189
|
+
function optionalPositiveInt(raw, label) {
|
|
190
|
+
if (raw === undefined || raw === null || raw === "")
|
|
191
|
+
return undefined;
|
|
192
|
+
const value = typeof raw === "number" ? raw : Number.parseInt(String(raw), 10);
|
|
193
|
+
if (!Number.isFinite(value) || value < 1)
|
|
194
|
+
throw new Error(`${label} must be a positive integer`);
|
|
195
|
+
return Math.floor(value);
|
|
196
|
+
}
|
|
197
|
+
function sh(value) {
|
|
198
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
199
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { RemoteDevice } from "./remote-devices.js";
|
|
2
|
+
export interface RemoteExecOptions {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
maxOutputBytes?: number;
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
executor?: RemoteCommandExecutor;
|
|
7
|
+
}
|
|
8
|
+
export interface RemoteExecResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
device: string;
|
|
11
|
+
host: string;
|
|
12
|
+
command: string;
|
|
13
|
+
exitCode: number | null;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
evidence: RemoteEvidence[];
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RemoteEvidence {
|
|
22
|
+
kind: "command" | "log" | "database" | "redis" | "profile";
|
|
23
|
+
source: string;
|
|
24
|
+
text: string;
|
|
25
|
+
time?: string;
|
|
26
|
+
}
|
|
27
|
+
export type RemoteCommandExecutor = (program: string, args: string[], options: {
|
|
28
|
+
env: NodeJS.ProcessEnv;
|
|
29
|
+
timeoutMs: number;
|
|
30
|
+
maxOutputBytes: number;
|
|
31
|
+
}) => Promise<Omit<RemoteExecResult, "device" | "host" | "command" | "evidence">>;
|
|
32
|
+
export declare function sshExec(device: RemoteDevice, command: string, options?: RemoteExecOptions): Promise<RemoteExecResult>;
|
|
33
|
+
export declare function buildSshCommand(device: RemoteDevice, command: string, env?: NodeJS.ProcessEnv): {
|
|
34
|
+
program: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
env: NodeJS.ProcessEnv;
|
|
37
|
+
};
|
|
38
|
+
export declare function remotePassword(device: RemoteDevice, env?: NodeJS.ProcessEnv): string | undefined;
|
|
39
|
+
export declare function redactRemoteSecrets(text: string, device: RemoteDevice, env?: NodeJS.ProcessEnv): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
3
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 200 * 1024;
|
|
4
|
+
export async function sshExec(device, command, options = {}) {
|
|
5
|
+
const timeoutMs = normalizePositiveInt(options.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
6
|
+
const maxOutputBytes = normalizePositiveInt(options.maxOutputBytes, DEFAULT_MAX_OUTPUT_BYTES);
|
|
7
|
+
const env = { ...process.env, ...(options.env ?? {}) };
|
|
8
|
+
const built = buildSshCommand(device, command, env);
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
const executor = options.executor ?? spawnCapture;
|
|
11
|
+
const result = await executor(built.program, built.args, { env: built.env, timeoutMs, maxOutputBytes });
|
|
12
|
+
return {
|
|
13
|
+
...result,
|
|
14
|
+
stdout: redactRemoteSecrets(result.stdout, device, env),
|
|
15
|
+
stderr: redactRemoteSecrets(result.stderr, device, env),
|
|
16
|
+
error: result.error ? redactRemoteSecrets(result.error, device, env) : undefined,
|
|
17
|
+
device: device.id,
|
|
18
|
+
host: device.host,
|
|
19
|
+
command,
|
|
20
|
+
durationMs: result.durationMs || Date.now() - started,
|
|
21
|
+
evidence: [{
|
|
22
|
+
kind: "command",
|
|
23
|
+
source: `${device.user}@${device.host}`,
|
|
24
|
+
text: command
|
|
25
|
+
}]
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function buildSshCommand(device, command, env = process.env) {
|
|
29
|
+
const port = String(device.port ?? 22);
|
|
30
|
+
const target = `${device.user}@${device.host}`;
|
|
31
|
+
const baseArgs = [
|
|
32
|
+
"-o", "StrictHostKeyChecking=no",
|
|
33
|
+
"-o", "UserKnownHostsFile=/dev/null",
|
|
34
|
+
"-o", "ConnectTimeout=5",
|
|
35
|
+
"-p", port
|
|
36
|
+
];
|
|
37
|
+
if (device.identityFile)
|
|
38
|
+
baseArgs.push("-i", device.identityFile);
|
|
39
|
+
const password = remotePassword(device, env);
|
|
40
|
+
if (password) {
|
|
41
|
+
return {
|
|
42
|
+
program: "sshpass",
|
|
43
|
+
args: ["-e", "ssh", ...baseArgs, target, command],
|
|
44
|
+
env: { ...env, SSHPASS: password }
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
program: "ssh",
|
|
49
|
+
args: [...baseArgs, target, command],
|
|
50
|
+
env
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function remotePassword(device, env = process.env) {
|
|
54
|
+
if (device.passwordEnv && env[device.passwordEnv])
|
|
55
|
+
return env[device.passwordEnv];
|
|
56
|
+
return device.password;
|
|
57
|
+
}
|
|
58
|
+
export function redactRemoteSecrets(text, device, env = process.env) {
|
|
59
|
+
let redacted = text;
|
|
60
|
+
for (const secret of [device.password, device.passwordEnv ? env[device.passwordEnv] : undefined].filter(Boolean)) {
|
|
61
|
+
redacted = redacted.split(secret).join("<redacted>");
|
|
62
|
+
}
|
|
63
|
+
return redacted;
|
|
64
|
+
}
|
|
65
|
+
async function spawnCapture(program, args, options) {
|
|
66
|
+
const started = Date.now();
|
|
67
|
+
return await new Promise((resolve) => {
|
|
68
|
+
const child = spawn(program, args, {
|
|
69
|
+
env: options.env,
|
|
70
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
71
|
+
});
|
|
72
|
+
let stdout = "";
|
|
73
|
+
let stderr = "";
|
|
74
|
+
let truncated = false;
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
child.kill("SIGTERM");
|
|
77
|
+
}, options.timeoutMs);
|
|
78
|
+
timer.unref?.();
|
|
79
|
+
const append = (chunk, target) => {
|
|
80
|
+
const text = chunk.toString("utf8");
|
|
81
|
+
if (Buffer.byteLength(stdout + stderr, "utf8") + Buffer.byteLength(text, "utf8") > options.maxOutputBytes) {
|
|
82
|
+
truncated = true;
|
|
83
|
+
const remaining = Math.max(0, options.maxOutputBytes - Buffer.byteLength(stdout + stderr, "utf8"));
|
|
84
|
+
const sliced = Buffer.from(text).subarray(0, remaining).toString("utf8");
|
|
85
|
+
if (target === "stdout")
|
|
86
|
+
stdout += sliced;
|
|
87
|
+
else
|
|
88
|
+
stderr += sliced;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (target === "stdout")
|
|
92
|
+
stdout += text;
|
|
93
|
+
else
|
|
94
|
+
stderr += text;
|
|
95
|
+
};
|
|
96
|
+
child.stdout?.on("data", (chunk) => append(chunk, "stdout"));
|
|
97
|
+
child.stderr?.on("data", (chunk) => append(chunk, "stderr"));
|
|
98
|
+
child.once("error", (err) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
resolve({
|
|
101
|
+
ok: false,
|
|
102
|
+
exitCode: 1,
|
|
103
|
+
stdout,
|
|
104
|
+
stderr,
|
|
105
|
+
truncated,
|
|
106
|
+
durationMs: Date.now() - started,
|
|
107
|
+
error: err.message
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
child.once("close", (code, signal) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
const timedOut = signal === "SIGTERM" && Date.now() - started >= options.timeoutMs;
|
|
113
|
+
resolve({
|
|
114
|
+
ok: code === 0 && !timedOut,
|
|
115
|
+
exitCode: code,
|
|
116
|
+
stdout,
|
|
117
|
+
stderr,
|
|
118
|
+
truncated,
|
|
119
|
+
durationMs: Date.now() - started,
|
|
120
|
+
...(timedOut ? { error: `remote command timed out after ${options.timeoutMs}ms` } : code === 0 ? {} : { error: `remote command exited with code ${code}` })
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function normalizePositiveInt(value, fallback) {
|
|
126
|
+
if (value === undefined)
|
|
127
|
+
return fallback;
|
|
128
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
|
|
129
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type RunStreamTerminal = "running" | "done" | "error" | "interrupted";
|
|
2
|
+
export type RunStreamFooter = "thinking" | "tool_running" | "streaming" | null;
|
|
3
|
+
export type RunStreamEvent = {
|
|
4
|
+
type: "reasoning_delta";
|
|
5
|
+
text: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: "message_delta";
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: "tool_started";
|
|
11
|
+
id?: string;
|
|
12
|
+
name: string;
|
|
13
|
+
input?: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "tool_delta";
|
|
16
|
+
id?: string;
|
|
17
|
+
text: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: "tool_done";
|
|
20
|
+
id?: string;
|
|
21
|
+
output?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
} | {
|
|
24
|
+
type: "done";
|
|
25
|
+
} | {
|
|
26
|
+
type: "error";
|
|
27
|
+
message: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: "interrupted";
|
|
30
|
+
};
|
|
31
|
+
export interface RunStreamTool {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
input?: string;
|
|
35
|
+
output?: string;
|
|
36
|
+
status: "running" | "done" | "error";
|
|
37
|
+
}
|
|
38
|
+
export interface RunStreamState {
|
|
39
|
+
reasoning: string;
|
|
40
|
+
message: string;
|
|
41
|
+
tools: RunStreamTool[];
|
|
42
|
+
terminal: RunStreamTerminal;
|
|
43
|
+
footer: RunStreamFooter;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function initialRunStreamState(): RunStreamState;
|
|
47
|
+
export declare function reduceRunStream(state: RunStreamState, event: RunStreamEvent): RunStreamState;
|
|
48
|
+
export declare function renderRunStreamText(state: RunStreamState): string;
|
|
49
|
+
export declare function renderRunStreamCard(state: RunStreamState): {
|
|
50
|
+
summary: string;
|
|
51
|
+
sections: Array<{
|
|
52
|
+
kind: "reasoning" | "tool" | "message" | "status";
|
|
53
|
+
title: string;
|
|
54
|
+
body: string;
|
|
55
|
+
collapsed: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export function initialRunStreamState() {
|
|
2
|
+
return {
|
|
3
|
+
reasoning: "",
|
|
4
|
+
message: "",
|
|
5
|
+
tools: [],
|
|
6
|
+
terminal: "running",
|
|
7
|
+
footer: "thinking"
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function reduceRunStream(state, event) {
|
|
11
|
+
if (state.terminal !== "running")
|
|
12
|
+
return state;
|
|
13
|
+
if (event.type === "reasoning_delta") {
|
|
14
|
+
return { ...state, reasoning: state.reasoning + event.text, footer: "thinking" };
|
|
15
|
+
}
|
|
16
|
+
if (event.type === "message_delta") {
|
|
17
|
+
return { ...state, message: state.message + event.text, footer: "streaming" };
|
|
18
|
+
}
|
|
19
|
+
if (event.type === "tool_started") {
|
|
20
|
+
return {
|
|
21
|
+
...state,
|
|
22
|
+
footer: "tool_running",
|
|
23
|
+
tools: [...state.tools, { id: event.id ?? `tool-${state.tools.length + 1}`, name: event.name, input: event.input, status: "running" }]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (event.type === "tool_delta") {
|
|
27
|
+
return updateTool(state, event.id, (tool) => ({
|
|
28
|
+
...tool,
|
|
29
|
+
output: `${tool.output ?? ""}${event.text}`,
|
|
30
|
+
status: "running"
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
if (event.type === "tool_done") {
|
|
34
|
+
return updateTool(state, event.id, (tool) => ({
|
|
35
|
+
...tool,
|
|
36
|
+
output: event.output ?? tool.output,
|
|
37
|
+
status: event.error ? "error" : "done"
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
if (event.type === "done")
|
|
41
|
+
return { ...state, terminal: "done", footer: null };
|
|
42
|
+
if (event.type === "error")
|
|
43
|
+
return { ...state, terminal: "error", footer: null, error: event.message };
|
|
44
|
+
return { ...state, terminal: "interrupted", footer: null };
|
|
45
|
+
}
|
|
46
|
+
export function renderRunStreamText(state) {
|
|
47
|
+
const lines = [];
|
|
48
|
+
if (state.reasoning)
|
|
49
|
+
lines.push(`reasoning: ${compact(state.reasoning, 500)}`);
|
|
50
|
+
for (const tool of state.tools) {
|
|
51
|
+
const status = tool.status === "running" ? "running" : tool.status;
|
|
52
|
+
const detail = tool.output ? ` - ${compact(tool.output, 300)}` : "";
|
|
53
|
+
lines.push(`tool ${status}: ${tool.name}${detail}`);
|
|
54
|
+
}
|
|
55
|
+
if (state.message)
|
|
56
|
+
lines.push(state.message.trimEnd());
|
|
57
|
+
if (state.terminal === "error" && state.error)
|
|
58
|
+
lines.push(`error: ${state.error}`);
|
|
59
|
+
if (state.terminal === "interrupted")
|
|
60
|
+
lines.push("interrupted");
|
|
61
|
+
return lines.join("\n").trim();
|
|
62
|
+
}
|
|
63
|
+
export function renderRunStreamCard(state) {
|
|
64
|
+
const sections = [];
|
|
65
|
+
if (state.reasoning) {
|
|
66
|
+
sections.push({ kind: "reasoning", title: state.footer === "thinking" ? "Thinking" : "Reasoning", body: compact(state.reasoning, 1500), collapsed: state.footer !== "thinking" });
|
|
67
|
+
}
|
|
68
|
+
const priorTools = state.tools.length > 2 && state.terminal === "running" ? state.tools.slice(0, -1) : [];
|
|
69
|
+
if (priorTools.length) {
|
|
70
|
+
sections.push({
|
|
71
|
+
kind: "tool",
|
|
72
|
+
title: `${priorTools.length} tool calls`,
|
|
73
|
+
body: priorTools.map((tool) => `${tool.status}: ${tool.name}`).join("\n"),
|
|
74
|
+
collapsed: true
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const visibleTools = priorTools.length ? state.tools.slice(-1) : state.tools;
|
|
78
|
+
for (const tool of visibleTools) {
|
|
79
|
+
sections.push({
|
|
80
|
+
kind: "tool",
|
|
81
|
+
title: `${tool.status}: ${tool.name}`,
|
|
82
|
+
body: compact([tool.input, tool.output].filter(Boolean).join("\n"), 1500) || "(no output)",
|
|
83
|
+
collapsed: tool.status !== "running"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (state.message)
|
|
87
|
+
sections.push({ kind: "message", title: "Reply", body: state.message, collapsed: false });
|
|
88
|
+
if (state.terminal !== "running") {
|
|
89
|
+
sections.push({ kind: "status", title: state.terminal, body: state.error ?? state.terminal, collapsed: false });
|
|
90
|
+
}
|
|
91
|
+
return { summary: summaryText(state), sections };
|
|
92
|
+
}
|
|
93
|
+
function updateTool(state, id, update) {
|
|
94
|
+
const target = id ?? state.tools.at(-1)?.id;
|
|
95
|
+
if (!target)
|
|
96
|
+
return state;
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
footer: "tool_running",
|
|
100
|
+
tools: state.tools.map((tool) => tool.id === target ? update(tool) : tool)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function summaryText(state) {
|
|
104
|
+
if (state.terminal === "done")
|
|
105
|
+
return "Completed";
|
|
106
|
+
if (state.terminal === "error")
|
|
107
|
+
return "Failed";
|
|
108
|
+
if (state.terminal === "interrupted")
|
|
109
|
+
return "Interrupted";
|
|
110
|
+
if (state.footer === "tool_running")
|
|
111
|
+
return "Calling tools";
|
|
112
|
+
if (state.footer === "streaming")
|
|
113
|
+
return "Streaming";
|
|
114
|
+
return "Thinking";
|
|
115
|
+
}
|
|
116
|
+
function compact(value, max) {
|
|
117
|
+
const text = value.replace(/\s+$/g, "");
|
|
118
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
119
|
+
}
|