@nowcrew/daemon 0.5.28 → 0.5.29
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/dist/attachments.js +196 -0
- package/dist/bound-im-decision.js +22 -0
- package/dist/completion-retransmitter.js +77 -0
- package/dist/computer-cli.js +274 -0
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +364 -0
- package/dist/computer-service.js +358 -0
- package/dist/config.js +82 -0
- package/dist/console-collapse.js +13 -0
- package/dist/console-formatter.js +77 -0
- package/dist/console-payload.js +73 -0
- package/dist/console.js +329 -0
- package/dist/daemon-startup-error.js +30 -0
- package/dist/execution-backend.js +44 -0
- package/dist/execution-event-limit.js +64 -0
- package/dist/execution-journal-lock.js +421 -0
- package/dist/execution-journal.js +716 -0
- package/dist/execution-protocol.js +342 -0
- package/dist/execution-recovery.js +95 -0
- package/dist/execution-runner.js +659 -0
- package/dist/execution-supervisor-child.js +236 -0
- package/dist/execution-supervisor.js +316 -0
- package/dist/execution-telemetry-journal.js +71 -0
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +64 -0
- package/dist/json-result.js +27 -0
- package/dist/list-models.js +92 -0
- package/dist/local-executor.js +439 -0
- package/dist/log-format.js +10 -0
- package/dist/machine-info.js +124 -0
- package/dist/main.js +118 -0
- package/dist/normalize.js +170 -0
- package/dist/origin-decision.js +44 -0
- package/dist/platform.js +8 -0
- package/dist/prompt.js +307 -0
- package/dist/provider-env.js +90 -0
- package/dist/runner.js +234 -0
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +43 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +51 -0
- package/dist/runtimes/codex-app-server-runner.js +344 -0
- package/dist/runtimes/codex-deepseek-catalog.js +7 -0
- package/dist/runtimes/codex-deepseek-config.js +50 -0
- package/dist/runtimes/codex.js +53 -0
- package/dist/runtimes/kimi-acp-runner.js +364 -0
- package/dist/runtimes/kimi.js +45 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/scheduled-report.js +51 -0
- package/dist/scheduled-run-report.js +57 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +868 -0
- package/dist/session.js +82 -0
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/skill-preview.js +21 -0
- package/dist/skills.js +56 -0
- package/dist/slog.js +228 -0
- package/dist/supervised-runtime.js +104 -0
- package/dist/token.js +24 -0
- package/dist/unified-diff.js +84 -0
- package/dist/websocket-shutdown.js +53 -0
- package/dist/win32-job-object.js +193 -0
- package/dist/workspace-fs.js +80 -0
- package/dist/workspace-import.js +127 -0
- package/dist/workspace.js +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { access, chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const WINDOWS_STOP_TIMEOUT_MS = 5_000;
|
|
9
|
+
const WINDOWS_STOP_POLL_MS = 100;
|
|
10
|
+
export const systemCommandRunner = async (command, args) => {
|
|
11
|
+
try {
|
|
12
|
+
const result = await execFileAsync(command, [...args], { encoding: "utf8" });
|
|
13
|
+
return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
const failure = error;
|
|
17
|
+
return {
|
|
18
|
+
exitCode: typeof failure.code === "number" ? failure.code : 127,
|
|
19
|
+
stdout: failure.stdout ?? "",
|
|
20
|
+
stderr: failure.stderr ?? failure.message,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function assertSingleLine(value, field) {
|
|
25
|
+
if (value.includes("\n") || value.includes("\r") || value.includes("\0")) {
|
|
26
|
+
throw new Error(`${field} must not contain control characters`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function xml(value) {
|
|
31
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
32
|
+
.replaceAll('"', """).replaceAll("'", "'");
|
|
33
|
+
}
|
|
34
|
+
function systemdArg(value) {
|
|
35
|
+
return `"${assertSingleLine(value, "service argument").replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
|
|
36
|
+
}
|
|
37
|
+
export function windowsCommandArg(value) {
|
|
38
|
+
const input = assertSingleLine(value, "service argument");
|
|
39
|
+
return `"${input.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\*)$/g, "$1$1")}"`;
|
|
40
|
+
}
|
|
41
|
+
export function buildServiceSpec(input) {
|
|
42
|
+
const { platform, profile, userHome } = input;
|
|
43
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
|
|
44
|
+
throw new Error(`Computer services are not supported on platform '${platform}'`);
|
|
45
|
+
}
|
|
46
|
+
const daemonCommand = [
|
|
47
|
+
assertSingleLine(input.nodePath, "node path"),
|
|
48
|
+
assertSingleLine(input.entryPath, "daemon entry path"),
|
|
49
|
+
"serve",
|
|
50
|
+
"--profile",
|
|
51
|
+
assertSingleLine(profile, "profile"),
|
|
52
|
+
...(input.profileHome ? ["--daemon-home", assertSingleLine(input.profileHome, "daemon home")] : []),
|
|
53
|
+
];
|
|
54
|
+
if (platform === "darwin") {
|
|
55
|
+
if (input.uid === undefined)
|
|
56
|
+
throw new Error("Cannot install launchd service without a numeric uid");
|
|
57
|
+
const id = `com.nowcrew.daemon.${profile}`;
|
|
58
|
+
const args = daemonCommand.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
|
|
59
|
+
return {
|
|
60
|
+
platform,
|
|
61
|
+
profile,
|
|
62
|
+
id,
|
|
63
|
+
descriptorPath: resolve(userHome, "Library", "LaunchAgents", `${id}.plist`),
|
|
64
|
+
daemonCommand,
|
|
65
|
+
managerDomain: `gui/${input.uid}`,
|
|
66
|
+
descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (platform === "linux") {
|
|
70
|
+
const id = `nowcrew-daemon-${profile}.service`;
|
|
71
|
+
return {
|
|
72
|
+
platform,
|
|
73
|
+
profile,
|
|
74
|
+
id,
|
|
75
|
+
descriptorPath: resolve(userHome, ".config", "systemd", "user", id),
|
|
76
|
+
daemonCommand,
|
|
77
|
+
managerDomain: null,
|
|
78
|
+
descriptor: `[Unit]\nDescription=NowCrew daemon (${profile})\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${daemonCommand.map(systemdArg).join(" ")}\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const id = `NowCrew Daemon (${profile})`;
|
|
82
|
+
return { platform, profile, id, descriptorPath: null, descriptor: null, daemonCommand, managerDomain: null };
|
|
83
|
+
}
|
|
84
|
+
async function pathExists(path) {
|
|
85
|
+
try {
|
|
86
|
+
await access(path, constants.F_OK);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error.code === "ENOENT")
|
|
91
|
+
return false;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function writePrivateAtomic(path, content) {
|
|
96
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
97
|
+
await chmod(dirname(path), 0o700);
|
|
98
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
101
|
+
await rename(temporary, path);
|
|
102
|
+
await chmod(path, 0o600);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await rm(temporary, { force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function checked(runner, command, args, action) {
|
|
109
|
+
const result = await runner(command, args);
|
|
110
|
+
if (result.exitCode !== 0) {
|
|
111
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`;
|
|
112
|
+
throw new Error(`${action} failed: ${detail}`);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
async function windowsTaskState(spec, runner) {
|
|
117
|
+
const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
|
|
118
|
+
const script = [
|
|
119
|
+
"$ErrorActionPreference='Stop';",
|
|
120
|
+
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
|
|
121
|
+
"$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
|
|
122
|
+
"if($tasks.Count -eq 0){'missing'}elseif($tasks.Count -eq 1){'state:'+([int]$tasks[0].State)}else{throw 'duplicate scheduled tasks'}",
|
|
123
|
+
].join("");
|
|
124
|
+
const result = await runner("powershell.exe", [
|
|
125
|
+
"-NoProfile", "-NonInteractive", "-Command", script, encodedName,
|
|
126
|
+
]);
|
|
127
|
+
if (result.exitCode !== 0) {
|
|
128
|
+
throw new Error(`Unable to query scheduled task: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
129
|
+
}
|
|
130
|
+
const value = result.stdout.trim();
|
|
131
|
+
if (value === "missing")
|
|
132
|
+
return { installed: false, state: null, result };
|
|
133
|
+
const match = /^state:([0-4])$/.exec(value);
|
|
134
|
+
if (!match)
|
|
135
|
+
throw new Error(`Scheduled task returned an unknown state '${value}'`);
|
|
136
|
+
return { installed: true, state: Number(match[1]), result };
|
|
137
|
+
}
|
|
138
|
+
async function windowsTaskExists(spec, runner) {
|
|
139
|
+
return (await windowsTaskState(spec, runner)).installed;
|
|
140
|
+
}
|
|
141
|
+
async function stopWindowsTask(spec, initial, runner) {
|
|
142
|
+
if (initial.state !== 2 && initial.state !== 4)
|
|
143
|
+
return initial.installed;
|
|
144
|
+
await checked(runner, "schtasks.exe", ["/End", "/TN", spec.id], "Scheduled task stop");
|
|
145
|
+
const deadline = Date.now() + WINDOWS_STOP_TIMEOUT_MS;
|
|
146
|
+
while (true) {
|
|
147
|
+
const current = await windowsTaskState(spec, runner);
|
|
148
|
+
if (!current.installed)
|
|
149
|
+
return false;
|
|
150
|
+
if (current.state !== 2 && current.state !== 4)
|
|
151
|
+
return true;
|
|
152
|
+
if (Date.now() >= deadline) {
|
|
153
|
+
throw new Error(`Scheduled task '${spec.id}' did not stop within ${WINDOWS_STOP_TIMEOUT_MS}ms`);
|
|
154
|
+
}
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, WINDOWS_STOP_POLL_MS));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function installService(spec, runner = systemCommandRunner) {
|
|
159
|
+
if (spec.platform === "win32") {
|
|
160
|
+
if (await windowsTaskExists(spec, runner))
|
|
161
|
+
throw new Error(`Service '${spec.id}' is already installed`);
|
|
162
|
+
const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
|
|
163
|
+
const encodedExecutable = Buffer.from(spec.daemonCommand[0], "utf16le").toString("base64");
|
|
164
|
+
const encodedArguments = Buffer.from(spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "), "utf16le").toString("base64");
|
|
165
|
+
const script = [
|
|
166
|
+
"$ErrorActionPreference='Stop';",
|
|
167
|
+
"$name=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
|
|
168
|
+
"$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
|
|
169
|
+
"$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
|
|
170
|
+
"$identity=[Security.Principal.WindowsIdentity]::GetCurrent().Name;",
|
|
171
|
+
"$action=New-ScheduledTaskAction -Execute $exe -Argument $argv;",
|
|
172
|
+
"$trigger=New-ScheduledTaskTrigger -AtLogOn -User $identity;",
|
|
173
|
+
"$settings=New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1);",
|
|
174
|
+
"$principal=New-ScheduledTaskPrincipal -UserId $identity -LogonType Interactive -RunLevel Limited;",
|
|
175
|
+
"Register-ScheduledTask -TaskName $name -TaskPath '\\' -Action $action -Trigger $trigger -Settings $settings -Principal $principal -ErrorAction Stop|Out-Null;",
|
|
176
|
+
].join("");
|
|
177
|
+
await checked(runner, "powershell.exe", [
|
|
178
|
+
"-NoProfile", "-NonInteractive", "-Command", script,
|
|
179
|
+
encodedName, encodedExecutable, encodedArguments,
|
|
180
|
+
], "Scheduled task installation");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (spec.descriptorPath === null || spec.descriptor === null)
|
|
184
|
+
throw new Error("Missing service descriptor");
|
|
185
|
+
if (await pathExists(spec.descriptorPath))
|
|
186
|
+
throw new Error(`Service '${spec.id}' is already installed`);
|
|
187
|
+
await writePrivateAtomic(spec.descriptorPath, spec.descriptor);
|
|
188
|
+
try {
|
|
189
|
+
if (spec.platform === "linux") {
|
|
190
|
+
await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
|
|
191
|
+
await checked(runner, "systemctl", ["--user", "enable", spec.id], "systemd enable");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
await rm(spec.descriptorPath, { force: true });
|
|
196
|
+
if (spec.platform === "linux") {
|
|
197
|
+
await runner("systemctl", ["--user", "disable", spec.id]);
|
|
198
|
+
await runner("systemctl", ["--user", "daemon-reload"]);
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
export async function uninstallService(spec, runner = systemCommandRunner) {
|
|
204
|
+
if (spec.platform === "win32") {
|
|
205
|
+
const task = await windowsTaskState(spec, runner);
|
|
206
|
+
if (!task.installed)
|
|
207
|
+
throw new Error(`Service '${spec.id}' is not installed`);
|
|
208
|
+
if (!await stopWindowsTask(spec, task, runner))
|
|
209
|
+
return;
|
|
210
|
+
await checked(runner, "schtasks.exe", ["/Delete", "/TN", spec.id, "/F"], "Scheduled task removal");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
|
|
214
|
+
throw new Error(`Service '${spec.id}' is not installed`);
|
|
215
|
+
}
|
|
216
|
+
if (spec.platform === "darwin") {
|
|
217
|
+
const status = await serviceStatus(spec, runner);
|
|
218
|
+
if (status.loaded) {
|
|
219
|
+
await checked(runner, "launchctl", ["bootout", `${spec.managerDomain}/${spec.id}`], "launchd bootout");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const disable = await runner("systemctl", ["--user", "disable", "--now", spec.id]);
|
|
224
|
+
if (disable.exitCode !== 0 && !/not loaded|does not exist|not found/i.test(`${disable.stdout}\n${disable.stderr}`)) {
|
|
225
|
+
throw new Error(`systemd disable failed: ${disable.stderr.trim() || disable.stdout.trim()}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
await rm(spec.descriptorPath);
|
|
229
|
+
if (spec.platform === "linux") {
|
|
230
|
+
await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function serviceAction(spec, action, runner = systemCommandRunner) {
|
|
234
|
+
if (spec.platform !== "win32" && (spec.descriptorPath === null || !await pathExists(spec.descriptorPath))) {
|
|
235
|
+
throw new Error(`Service '${spec.id}' is not installed`);
|
|
236
|
+
}
|
|
237
|
+
if (spec.platform === "darwin") {
|
|
238
|
+
const target = `${spec.managerDomain}/${spec.id}`;
|
|
239
|
+
const status = await serviceStatus(spec, runner);
|
|
240
|
+
if (action === "start") {
|
|
241
|
+
if (status.loaded)
|
|
242
|
+
await checked(runner, "launchctl", ["kickstart", "-k", target], "launchd start");
|
|
243
|
+
else
|
|
244
|
+
await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
|
|
245
|
+
}
|
|
246
|
+
else if (action === "stop") {
|
|
247
|
+
if (status.loaded)
|
|
248
|
+
await checked(runner, "launchctl", ["bootout", target], "launchd stop");
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
if (status.loaded)
|
|
252
|
+
await checked(runner, "launchctl", ["bootout", target], "launchd stop");
|
|
253
|
+
await checked(runner, "launchctl", ["bootstrap", spec.managerDomain, spec.descriptorPath], "launchd start");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (spec.platform === "linux") {
|
|
257
|
+
await checked(runner, "systemctl", ["--user", action, spec.id], `systemd ${action}`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const task = await windowsTaskState(spec, runner);
|
|
261
|
+
if (!task.installed)
|
|
262
|
+
throw new Error(`Service '${spec.id}' is not installed`);
|
|
263
|
+
if (action === "start")
|
|
264
|
+
await checked(runner, "schtasks.exe", ["/Run", "/TN", spec.id], "Scheduled task start");
|
|
265
|
+
else if (action === "stop") {
|
|
266
|
+
await stopWindowsTask(spec, task, runner);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (!await stopWindowsTask(spec, task, runner)) {
|
|
270
|
+
throw new Error(`Service '${spec.id}' disappeared while restarting`);
|
|
271
|
+
}
|
|
272
|
+
await checked(runner, "schtasks.exe", ["/Run", "/TN", spec.id], "Scheduled task start");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
export async function serviceStatus(spec, runner = systemCommandRunner) {
|
|
277
|
+
if (spec.platform === "win32") {
|
|
278
|
+
const task = await windowsTaskState(spec, runner);
|
|
279
|
+
return {
|
|
280
|
+
...task.result,
|
|
281
|
+
installed: task.installed,
|
|
282
|
+
loaded: task.installed,
|
|
283
|
+
running: task.state === 4,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
|
|
287
|
+
return { exitCode: 3, stdout: "", stderr: "service descriptor not found", installed: false, loaded: false, running: false };
|
|
288
|
+
}
|
|
289
|
+
const result = spec.platform === "darwin"
|
|
290
|
+
? await runner("launchctl", ["print", `${spec.managerDomain}/${spec.id}`])
|
|
291
|
+
: await runner("systemctl", ["--user", "is-active", spec.id]);
|
|
292
|
+
const running = spec.platform === "darwin"
|
|
293
|
+
? result.exitCode === 0 && /\bstate\s*=\s*running\b/.test(result.stdout)
|
|
294
|
+
: result.exitCode === 0;
|
|
295
|
+
return { ...result, installed: true, loaded: spec.platform === "darwin" ? result.exitCode === 0 : true, running };
|
|
296
|
+
}
|
|
297
|
+
export async function doctorService(spec, profilePrivate, runner = systemCommandRunner) {
|
|
298
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
299
|
+
const tool = spec.platform === "darwin" ? "launchctl" : spec.platform === "linux" ? "systemctl" : "schtasks.exe";
|
|
300
|
+
const probeArgs = spec.platform === "linux" ? ["--user", "--version"] : spec.platform === "darwin" ? ["help"] : ["/Query", "/?"];
|
|
301
|
+
const probe = await runner(tool, probeArgs);
|
|
302
|
+
const status = await serviceStatus(spec, runner);
|
|
303
|
+
return [
|
|
304
|
+
{ name: "node", ok: nodeMajor >= 22, detail: `Node ${process.versions.node} (requires >=22)` },
|
|
305
|
+
{ name: "profile-permissions", ok: profilePrivate, detail: profilePrivate ? "private" : "readable by other users" },
|
|
306
|
+
{ name: "service-manager", ok: probe.exitCode === 0, detail: probe.exitCode === 0 ? tool : (probe.stderr.trim() || `${tool} unavailable`) },
|
|
307
|
+
{ name: "service-installed", ok: status.installed, detail: status.installed ? spec.id : "not installed" },
|
|
308
|
+
{ name: "service-running", ok: status.running === true, detail: status.running ? "running" : "not running" },
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
export function buildUpgradeCommand(platform) {
|
|
312
|
+
return [platform === "win32" ? "npm.cmd" : "npm", ["install", "--global", "@nowcrew/daemon@latest"]];
|
|
313
|
+
}
|
|
314
|
+
export async function upgradeDaemon(platform, runner = systemCommandRunner) {
|
|
315
|
+
const [command, args] = buildUpgradeCommand(platform);
|
|
316
|
+
await checked(runner, command, args, "Daemon upgrade");
|
|
317
|
+
}
|
|
318
|
+
const WINDOWS_SID_SCRIPT = "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
|
|
319
|
+
export async function hardenWindowsProfile(path, runner = systemCommandRunner) {
|
|
320
|
+
const identity = await checked(runner, "powershell.exe", [
|
|
321
|
+
"-NoProfile", "-NonInteractive", "-Command", WINDOWS_SID_SCRIPT,
|
|
322
|
+
], "Windows identity lookup");
|
|
323
|
+
const sid = identity.stdout.trim();
|
|
324
|
+
if (!/^S-1-(?:\d+-)+\d+$/.test(sid))
|
|
325
|
+
throw new Error("Windows identity lookup returned an invalid SID");
|
|
326
|
+
await checked(runner, "icacls.exe", [path, "/inheritance:r", "/grant:r", `*${sid}:(F)`], "Profile ACL hardening");
|
|
327
|
+
}
|
|
328
|
+
export async function windowsProfileIsPrivate(path, runner = systemCommandRunner) {
|
|
329
|
+
const encodedPath = Buffer.from(path, "utf16le").toString("base64");
|
|
330
|
+
const script = [
|
|
331
|
+
"$p=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
|
|
332
|
+
"$me=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;",
|
|
333
|
+
"$safe=@($me,'S-1-5-18','S-1-5-32-544');",
|
|
334
|
+
"$rules=(Get-Acl -LiteralPath $p).Access;",
|
|
335
|
+
"$unsafe=$rules|Where-Object { $_.AccessControlType -eq 'Allow' -and $safe -notcontains $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value };",
|
|
336
|
+
"if($unsafe){'false'}else{'true'}",
|
|
337
|
+
].join("");
|
|
338
|
+
const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, encodedPath]);
|
|
339
|
+
if (result.exitCode !== 0)
|
|
340
|
+
throw new Error(`Profile ACL inspection failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
341
|
+
if (result.stdout.trim() === "true")
|
|
342
|
+
return true;
|
|
343
|
+
if (result.stdout.trim() === "false")
|
|
344
|
+
return false;
|
|
345
|
+
throw new Error("Profile ACL inspection returned an invalid result");
|
|
346
|
+
}
|
|
347
|
+
export async function readServiceDescriptor(spec) {
|
|
348
|
+
if (spec.descriptorPath === null)
|
|
349
|
+
return null;
|
|
350
|
+
try {
|
|
351
|
+
return await readFile(spec.descriptorPath, "utf8");
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (error.code === "ENOENT")
|
|
355
|
+
return null;
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** daemon 配置:从 env 读取,解析 crew CLI 路径 (注入给 agent 的 wrapper 用)。 */
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
7
|
+
import { resolveAgentsRoot } from "./computer-profile.js";
|
|
8
|
+
export const DEFAULT_EXECUTION_LIMITS = Object.freeze({
|
|
9
|
+
maxPromptBytes: 256_000,
|
|
10
|
+
maxTimeoutMs: 3_600_000,
|
|
11
|
+
maxEventBytes: 64_000,
|
|
12
|
+
maxParallelPerAgent: 4,
|
|
13
|
+
maxQueuedPerAgent: 32,
|
|
14
|
+
});
|
|
15
|
+
// Fits the largest mandatory v1 lifecycle envelope (UUID + timestamps + outcome facts) with margin.
|
|
16
|
+
export const MIN_EXECUTION_EVENT_BYTES = 512;
|
|
17
|
+
export const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
|
|
18
|
+
export class ConfigError extends Error {
|
|
19
|
+
}
|
|
20
|
+
function positiveIntegerEnv(env, field, fallback) {
|
|
21
|
+
const raw = env[field];
|
|
22
|
+
const value = raw === undefined ? fallback : Number(raw);
|
|
23
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
24
|
+
throw new ConfigError(`${field} must be a finite positive integer`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function integerEnvAtLeast(env, field, fallback, minimum) {
|
|
29
|
+
const value = positiveIntegerEnv(env, field, fallback);
|
|
30
|
+
if (value < minimum)
|
|
31
|
+
throw new ConfigError(`${field} must be at least ${minimum}`);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function integerEnvAtMost(env, field, fallback, maximum) {
|
|
35
|
+
const value = positiveIntegerEnv(env, field, fallback);
|
|
36
|
+
if (value > maximum)
|
|
37
|
+
throw new ConfigError(`${field} must be at most ${maximum}`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function defaultCliPath() {
|
|
41
|
+
// 已发布场景(npx):crew CLI 是 daemon 的依赖,从 node_modules 解析 @nowcrew/cli。
|
|
42
|
+
try {
|
|
43
|
+
return createRequire(import.meta.url).resolve("@nowcrew/cli/dist/main.js");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 开发场景(monorepo):daemon/src → 上两级到 crew/,再进 cli/dist/main.js
|
|
47
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
return resolve(here, "../../cli/dist/main.js");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export const DEFAULT_SERVER_URL = "http://127.0.0.1:3001";
|
|
52
|
+
export function loadConfig(env = process.env) {
|
|
53
|
+
const serverUrl = (env.CREW_SERVER_URL ?? DEFAULT_SERVER_URL).replace(/\/+$/, "");
|
|
54
|
+
const machineToken = env.CREW_MACHINE_TOKEN ?? "";
|
|
55
|
+
if (!machineToken) {
|
|
56
|
+
const lang = detectDaemonLang(env);
|
|
57
|
+
throw new ConfigError(translateDaemon(lang, "Missing CREW_MACHINE_TOKEN (sk_machine_*, printed by seed)"));
|
|
58
|
+
}
|
|
59
|
+
const executionLimits = Object.freeze({
|
|
60
|
+
maxPromptBytes: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_PROMPT_BYTES", DEFAULT_EXECUTION_LIMITS.maxPromptBytes),
|
|
61
|
+
maxTimeoutMs: integerEnvAtMost(env, "CREW_EXECUTION_MAX_TIMEOUT_MS", DEFAULT_EXECUTION_LIMITS.maxTimeoutMs, MAX_EXECUTION_TIMEOUT_MS),
|
|
62
|
+
maxEventBytes: integerEnvAtLeast(env, "CREW_EXECUTION_MAX_EVENT_BYTES", DEFAULT_EXECUTION_LIMITS.maxEventBytes, MIN_EXECUTION_EVENT_BYTES),
|
|
63
|
+
maxParallelPerAgent: positiveIntegerEnv(env, "CREW_MAX_PARALLEL", DEFAULT_EXECUTION_LIMITS.maxParallelPerAgent),
|
|
64
|
+
maxQueuedPerAgent: positiveIntegerEnv(env, "CREW_EXECUTION_MAX_QUEUED_PER_AGENT", DEFAULT_EXECUTION_LIMITS.maxQueuedPerAgent),
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
serverUrl,
|
|
68
|
+
machineToken,
|
|
69
|
+
agentsRoot: resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir()),
|
|
70
|
+
cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
|
|
71
|
+
runtimeBin: env.CREW_RUNTIME ?? "claude",
|
|
72
|
+
dangerous: env.CREW_RUNTIME_SAFE !== "1", // 默认开启 (headless agent 在自有 workspace 内运行)
|
|
73
|
+
resume: env.CREW_RESUME !== "off" && env.CREW_RESUME !== "0", // 默认开启;一键回退现状用 CREW_RESUME=off
|
|
74
|
+
resumeWarmMs: env.CREW_RESUME_WARM_MS != null ? Number(env.CREW_RESUME_WARM_MS) : 3_600_000, // 默认 1h
|
|
75
|
+
// 会话预算轮换:hard 120k 触发冷启动轮换;soft 90k 先提醒蒸馏 work-log;30 轮为 usage 缺失的兜底
|
|
76
|
+
sessionBudgetTokens: env.CREW_SESSION_BUDGET_TOKENS != null ? Number(env.CREW_SESSION_BUDGET_TOKENS) : 120_000,
|
|
77
|
+
sessionSoftTokens: env.CREW_SESSION_SOFT_TOKENS != null ? Number(env.CREW_SESSION_SOFT_TOKENS) : 90_000,
|
|
78
|
+
sessionMaxTurns: env.CREW_SESSION_MAX_TURNS != null ? Number(env.CREW_SESSION_MAX_TURNS) : 30,
|
|
79
|
+
productName: env.CREW_PRODUCT_NAME ?? "nowwork",
|
|
80
|
+
executionLimits,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MESSAGE_READ = /\bcrew\s+(?:message|thread)\s+read\b/i;
|
|
2
|
+
const TASK_LIST = /\bcrew\s+task\s+list\b/i;
|
|
3
|
+
export function buildCollapsedResult(command, output) {
|
|
4
|
+
if (MESSAGE_READ.test(command)) {
|
|
5
|
+
const count = output.split("\n").filter((line) => /^#\d+\s+\(msg=/.test(line.trim())).length;
|
|
6
|
+
return { kind: "collapsed_result", label: "CHANNEL HISTORY", count };
|
|
7
|
+
}
|
|
8
|
+
if (TASK_LIST.test(command)) {
|
|
9
|
+
const count = output.split("\n").filter((line) => /^#\d+\s+\[/.test(line.trim())).length;
|
|
10
|
+
return { kind: "collapsed_result", label: "TASK LIST", count };
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单次 runtime execution 的 console 格式化器。
|
|
3
|
+
* 用 tool_use id 关联 Read 结果,避免将整文件作为普通 tool_result 倾泻到终端。
|
|
4
|
+
*/
|
|
5
|
+
import { buildFilePreview } from "./console-payload.js";
|
|
6
|
+
import { buildScriptResult, toConsoleLines, TOOL_RESULT_CAP } from "./console.js";
|
|
7
|
+
import { parseUnifiedDiff } from "./unified-diff.js";
|
|
8
|
+
import { buildJsonResult } from "./json-result.js";
|
|
9
|
+
import { buildCollapsedResult } from "./console-collapse.js";
|
|
10
|
+
import { buildSkillPreview } from "./skill-preview.js";
|
|
11
|
+
const extract = (content) => {
|
|
12
|
+
if (typeof content === "string")
|
|
13
|
+
return content;
|
|
14
|
+
if (!Array.isArray(content))
|
|
15
|
+
return "";
|
|
16
|
+
return content.map((item) => item && typeof item === "object" && typeof item.text === "string"
|
|
17
|
+
? item.text : "").filter(Boolean).join("\n");
|
|
18
|
+
};
|
|
19
|
+
const clip = (text) => text.length > TOOL_RESULT_CAP
|
|
20
|
+
? `${text.slice(0, TOOL_RESULT_CAP)}… (+${text.length - TOOL_RESULT_CAP})`
|
|
21
|
+
: text;
|
|
22
|
+
const SCRIPT_TOOLS = new Set(["Bash", "PowerShell", "Shell"]);
|
|
23
|
+
export function createConsoleFormatter() {
|
|
24
|
+
const pending = new Map();
|
|
25
|
+
return {
|
|
26
|
+
format(event) {
|
|
27
|
+
const rec = event && typeof event === "object" ? event : {};
|
|
28
|
+
const message = rec.message && typeof rec.message === "object" ? rec.message : undefined;
|
|
29
|
+
if (rec.type === "assistant" && Array.isArray(message?.content)) {
|
|
30
|
+
for (const block of message.content) {
|
|
31
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
32
|
+
pending.set(block.id, { name: block.name, ...(block.input ? { input: block.input } : {}) });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (rec.type === "user" && Array.isArray(message?.content)) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const block of message.content) {
|
|
39
|
+
if (block.type !== "tool_result")
|
|
40
|
+
continue;
|
|
41
|
+
const text = extract(block.content);
|
|
42
|
+
if (!text)
|
|
43
|
+
continue;
|
|
44
|
+
const tool = block.tool_use_id ? pending.get(block.tool_use_id) : undefined;
|
|
45
|
+
if (block.tool_use_id)
|
|
46
|
+
pending.delete(block.tool_use_id);
|
|
47
|
+
if (!block.is_error && tool?.name === "Read" && typeof tool.input?.file_path === "string") {
|
|
48
|
+
const start = typeof tool.input.offset === "number" && Number.isSafeInteger(tool.input.offset)
|
|
49
|
+
? Math.max(1, tool.input.offset) : 1;
|
|
50
|
+
const preview = buildFilePreview("read", tool.input.file_path, text, start);
|
|
51
|
+
out.push({ stream: "tool_result", text: `Read ${preview.totalLines} line(s) from ${preview.path}`, payload: preview });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const patch = block.is_error ? null : parseUnifiedDiff(text);
|
|
55
|
+
const command = tool && SCRIPT_TOOLS.has(tool.name) && typeof tool.input?.command === "string" ? tool.input.command : "";
|
|
56
|
+
const skill = block.is_error || tool?.name !== "Skill" ? null : buildSkillPreview(text);
|
|
57
|
+
const collapsed = block.is_error || patch || skill || !command ? null : buildCollapsedResult(command, text);
|
|
58
|
+
const json = block.is_error || patch || skill || collapsed || !command ? null : buildJsonResult(command, text);
|
|
59
|
+
const script = patch || skill || collapsed || json || !command ? null : buildScriptResult(text);
|
|
60
|
+
out.push(patch
|
|
61
|
+
? { stream: "tool_result", text: `Changed ${patch.files.length} file(s): +${patch.additions} -${patch.deletions}`, payload: patch }
|
|
62
|
+
: skill
|
|
63
|
+
? { stream: "tool_result", text: "READ SKILL", payload: skill }
|
|
64
|
+
: collapsed
|
|
65
|
+
? { stream: "tool_result", text: collapsed.label, payload: collapsed }
|
|
66
|
+
: json
|
|
67
|
+
? { stream: "tool_result", text: json.label, payload: json }
|
|
68
|
+
: script
|
|
69
|
+
? { stream: "tool_result", text: clip(text), payload: script }
|
|
70
|
+
: { stream: "tool_result", text: clip(text) });
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
return toConsoleLines(event);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentConsole 的结构化展示协议与有界构造器。
|
|
3
|
+
* 所有代码/文件预览在 daemon 边界裁剪,避免 server/web 接收整文件。
|
|
4
|
+
*/
|
|
5
|
+
export const CONSOLE_PREVIEW_ROWS = 12;
|
|
6
|
+
export const CONSOLE_PREVIEW_HEAD_ROWS = 8;
|
|
7
|
+
export const CONSOLE_DIFF_ROWS = 40;
|
|
8
|
+
export const CONSOLE_ROW_CHARS = 240;
|
|
9
|
+
const clipRow = (text) => text.length > CONSOLE_ROW_CHARS
|
|
10
|
+
? `${text.slice(0, CONSOLE_ROW_CHARS)}…`
|
|
11
|
+
: text;
|
|
12
|
+
/** 头 8 + 尾 4,中间显式 omitted;小文件全部显示。 */
|
|
13
|
+
export function buildFilePreview(operation, path, content, startLine = 1) {
|
|
14
|
+
const lines = content ? content.split("\n") : [];
|
|
15
|
+
const visible = lines.length <= CONSOLE_PREVIEW_ROWS
|
|
16
|
+
? lines.map((text, index) => ({ type: "line", line: startLine + index, text: clipRow(text) }))
|
|
17
|
+
: [
|
|
18
|
+
...lines.slice(0, CONSOLE_PREVIEW_HEAD_ROWS).map((text, index) => ({
|
|
19
|
+
type: "line", line: startLine + index, text: clipRow(text),
|
|
20
|
+
})),
|
|
21
|
+
{ type: "omitted", count: lines.length - CONSOLE_PREVIEW_ROWS },
|
|
22
|
+
...lines.slice(-4).map((text, index) => ({
|
|
23
|
+
type: "line", line: startLine + lines.length - 4 + index, text: clipRow(text),
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
return {
|
|
27
|
+
kind: "file_preview",
|
|
28
|
+
operation,
|
|
29
|
+
path,
|
|
30
|
+
totalLines: lines.length,
|
|
31
|
+
totalBytes: Buffer.byteLength(content, "utf8"),
|
|
32
|
+
rows: visible,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** old/new 片段转紧凑带双行号 diff;变更中段超过预算时裁剪。 */
|
|
36
|
+
export function buildSnippetDiff(path, oldText, newText) {
|
|
37
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
38
|
+
const newLines = newText ? newText.split("\n") : [];
|
|
39
|
+
let prefix = 0;
|
|
40
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
|
|
41
|
+
prefix++;
|
|
42
|
+
let oldEnd = oldLines.length;
|
|
43
|
+
let newEnd = newLines.length;
|
|
44
|
+
while (oldEnd > prefix && newEnd > prefix && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
|
|
45
|
+
oldEnd--;
|
|
46
|
+
newEnd--;
|
|
47
|
+
}
|
|
48
|
+
const rows = [];
|
|
49
|
+
const contextStart = Math.max(0, prefix - 2);
|
|
50
|
+
for (let index = contextStart; index < prefix; index++) {
|
|
51
|
+
rows.push({ type: "context", oldLine: index + 1, newLine: index + 1, text: clipRow(oldLines[index]) });
|
|
52
|
+
}
|
|
53
|
+
for (let index = prefix; index < oldEnd; index++) {
|
|
54
|
+
rows.push({ type: "delete", oldLine: index + 1, newLine: null, text: clipRow(oldLines[index]) });
|
|
55
|
+
}
|
|
56
|
+
for (let index = prefix; index < newEnd; index++) {
|
|
57
|
+
rows.push({ type: "add", oldLine: null, newLine: index + 1, text: clipRow(newLines[index]) });
|
|
58
|
+
}
|
|
59
|
+
for (let offset = 0; offset < Math.min(2, oldLines.length - oldEnd); offset++) {
|
|
60
|
+
rows.push({ type: "context", oldLine: oldEnd + offset + 1, newLine: newEnd + offset + 1, text: clipRow(oldLines[oldEnd + offset]) });
|
|
61
|
+
}
|
|
62
|
+
const additions = Math.max(0, newEnd - prefix);
|
|
63
|
+
const deletions = Math.max(0, oldEnd - prefix);
|
|
64
|
+
if (rows.length <= CONSOLE_DIFF_ROWS)
|
|
65
|
+
return { kind: "diff_rows", files: [{ path, rows }], additions, deletions };
|
|
66
|
+
const head = rows.slice(0, 28);
|
|
67
|
+
const tail = rows.slice(-11);
|
|
68
|
+
return {
|
|
69
|
+
kind: "diff_rows",
|
|
70
|
+
files: [{ path, rows: [...head, { type: "omitted", oldCount: rows.length - 39, newCount: rows.length - 39 }, ...tail] }],
|
|
71
|
+
additions, deletions, truncated: true,
|
|
72
|
+
};
|
|
73
|
+
}
|