@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.
Files changed (67) hide show
  1. package/dist/attachments.js +196 -0
  2. package/dist/bound-im-decision.js +22 -0
  3. package/dist/completion-retransmitter.js +77 -0
  4. package/dist/computer-cli.js +274 -0
  5. package/dist/computer-profile-lock.js +395 -0
  6. package/dist/computer-profile.js +364 -0
  7. package/dist/computer-service.js +358 -0
  8. package/dist/config.js +82 -0
  9. package/dist/console-collapse.js +13 -0
  10. package/dist/console-formatter.js +77 -0
  11. package/dist/console-payload.js +73 -0
  12. package/dist/console.js +329 -0
  13. package/dist/daemon-startup-error.js +30 -0
  14. package/dist/execution-backend.js +44 -0
  15. package/dist/execution-event-limit.js +64 -0
  16. package/dist/execution-journal-lock.js +421 -0
  17. package/dist/execution-journal.js +716 -0
  18. package/dist/execution-protocol.js +342 -0
  19. package/dist/execution-recovery.js +95 -0
  20. package/dist/execution-runner.js +659 -0
  21. package/dist/execution-supervisor-child.js +236 -0
  22. package/dist/execution-supervisor.js +316 -0
  23. package/dist/execution-telemetry-journal.js +71 -0
  24. package/dist/external-output.js +114 -0
  25. package/dist/i18n.js +64 -0
  26. package/dist/json-result.js +27 -0
  27. package/dist/list-models.js +92 -0
  28. package/dist/local-executor.js +439 -0
  29. package/dist/log-format.js +10 -0
  30. package/dist/machine-info.js +124 -0
  31. package/dist/main.js +118 -0
  32. package/dist/normalize.js +170 -0
  33. package/dist/origin-decision.js +44 -0
  34. package/dist/platform.js +8 -0
  35. package/dist/prompt.js +307 -0
  36. package/dist/provider-env.js +90 -0
  37. package/dist/runner.js +234 -0
  38. package/dist/runtime-cancellation.js +74 -0
  39. package/dist/runtime-capabilities.js +43 -0
  40. package/dist/runtime-path.js +60 -0
  41. package/dist/runtimes/claude.js +51 -0
  42. package/dist/runtimes/codex-app-server-runner.js +344 -0
  43. package/dist/runtimes/codex-deepseek-catalog.js +7 -0
  44. package/dist/runtimes/codex-deepseek-config.js +50 -0
  45. package/dist/runtimes/codex.js +53 -0
  46. package/dist/runtimes/kimi-acp-runner.js +364 -0
  47. package/dist/runtimes/kimi.js +45 -0
  48. package/dist/runtimes/progress-watchdog.js +26 -0
  49. package/dist/scheduled-report.js +51 -0
  50. package/dist/scheduled-run-report.js +57 -0
  51. package/dist/serve-lifecycle.js +82 -0
  52. package/dist/serve.js +868 -0
  53. package/dist/session.js +82 -0
  54. package/dist/shared-execution-slots.js +68 -0
  55. package/dist/shutdown-deadline.js +32 -0
  56. package/dist/skill-preview.js +21 -0
  57. package/dist/skills.js +56 -0
  58. package/dist/slog.js +228 -0
  59. package/dist/supervised-runtime.js +104 -0
  60. package/dist/token.js +24 -0
  61. package/dist/unified-diff.js +84 -0
  62. package/dist/websocket-shutdown.js +53 -0
  63. package/dist/win32-job-object.js +193 -0
  64. package/dist/workspace-fs.js +80 -0
  65. package/dist/workspace-import.js +127 -0
  66. package/dist/workspace.js +148 -0
  67. package/package.json +1 -1
@@ -0,0 +1,364 @@
1
+ import { access, chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { realpathSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, posix, resolve, win32 } from "node:path";
6
+ import { randomUUID } from "node:crypto";
7
+ import { spawn } from "node:child_process";
8
+ import { z } from "zod";
9
+ import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
10
+ import { formatDaemonText } from "./i18n.js";
11
+ const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
12
+ const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
13
+ const url = new URL(value);
14
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
15
+ }, "serverUrl must be an HTTP(S) URL without embedded credentials");
16
+ const ProfileSchema = z.object({
17
+ version: z.literal(1),
18
+ name: z.string().regex(PROFILE_NAME),
19
+ serverUrl: ServerUrlSchema,
20
+ machineToken: z.string().startsWith("sk_machine_").max(4096),
21
+ agentsRoot: z.string().min(1).optional(),
22
+ runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
23
+ }).strict();
24
+ const ProfileAgentsRootCandidateSchema = ProfileSchema.pick({
25
+ name: true,
26
+ serverUrl: true,
27
+ agentsRoot: true,
28
+ }).strict();
29
+ const StoredPlainProfileSchema = ProfileSchema;
30
+ const StoredProtectedProfileSchema = ProfileSchema.omit({ machineToken: true }).extend({
31
+ machineTokenProtected: z.object({
32
+ scheme: z.literal("dpapi-current-user"),
33
+ ciphertext: z.string().min(1).max(16384),
34
+ }).strict(),
35
+ }).strict();
36
+ const StoredProfileSchema = z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]);
37
+ export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
38
+ export class ProfileAgentsRootConflictError extends Error {
39
+ profile;
40
+ conflict;
41
+ agentsRoot;
42
+ command;
43
+ constructor(input) {
44
+ const quote = input.platform === "win32" ? quotePowerShellArgument : quotePosixArgument;
45
+ const pathApi = input.platform === "win32" ? win32 : posix;
46
+ const suggestedRoot = resolveAgentsRoot(pathApi.join(input.userHome, ".crew", `agents-${input.profile}`), input.userHome, input.platform);
47
+ const command = [
48
+ "crew-daemon profile save",
49
+ quote(input.profile),
50
+ "--server-url",
51
+ quote(input.serverUrl),
52
+ "--agents-root",
53
+ quote(suggestedRoot),
54
+ "--token-stdin",
55
+ ].join(" ");
56
+ super(formatDaemonText("en", PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
57
+ profile: input.profile,
58
+ conflict: input.conflict,
59
+ agentsRoot: input.agentsRoot,
60
+ command,
61
+ }));
62
+ this.name = "ProfileAgentsRootConflictError";
63
+ this.profile = input.profile;
64
+ this.conflict = input.conflict;
65
+ this.agentsRoot = input.agentsRoot;
66
+ this.command = command;
67
+ }
68
+ }
69
+ const quotePosixArgument = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
70
+ const quotePowerShellArgument = (value) => `'${value.replaceAll("'", "''")}'`;
71
+ export function daemonHome(env = process.env) {
72
+ return resolve(env.CREW_DAEMON_HOME ?? resolve(homedir(), ".crew/daemon"));
73
+ }
74
+ export function resolveAgentsRoot(configured, userHome = homedir(), platform = process.platform) {
75
+ const pathApi = platform === "win32" ? win32 : posix;
76
+ const normalizedHome = pathApi.resolve(userHome);
77
+ if (configured === undefined)
78
+ return canonicalizeRoot(pathApi.resolve(normalizedHome, ".crew", "agents"), platform);
79
+ const expanded = configured === "~"
80
+ ? normalizedHome
81
+ : configured.startsWith("~/") || (platform === "win32" && configured.startsWith("~\\"))
82
+ ? pathApi.resolve(normalizedHome, configured.slice(2))
83
+ : configured;
84
+ return canonicalizeRoot(pathApi.resolve(expanded), platform);
85
+ }
86
+ function canonicalizeRoot(path, platform) {
87
+ const compatibleHost = (platform === "win32") === (process.platform === "win32");
88
+ if (!compatibleHost)
89
+ return path;
90
+ try {
91
+ return realpathSync.native(path);
92
+ }
93
+ catch (error) {
94
+ const code = error.code;
95
+ if (code === "ENOTDIR")
96
+ return path;
97
+ if (code !== "ENOENT")
98
+ throw error;
99
+ }
100
+ const pathApi = platform === "win32" ? win32 : posix;
101
+ const missingSegments = [];
102
+ let ancestor = path;
103
+ for (;;) {
104
+ const parent = pathApi.dirname(ancestor);
105
+ if (parent === ancestor)
106
+ return path;
107
+ missingSegments.unshift(pathApi.basename(ancestor));
108
+ ancestor = parent;
109
+ try {
110
+ return pathApi.resolve(realpathSync.native(ancestor), ...missingSegments);
111
+ }
112
+ catch (error) {
113
+ const code = error.code;
114
+ if (code === "ENOTDIR")
115
+ return path;
116
+ if (code !== "ENOENT")
117
+ throw error;
118
+ }
119
+ }
120
+ }
121
+ function comparableAgentsRoot(path, platform) {
122
+ return platform === "win32" ? win32.normalize(path).toLocaleLowerCase("en-US") : path;
123
+ }
124
+ export function validateProfileName(name) {
125
+ if (!PROFILE_NAME.test(name)) {
126
+ throw new Error("profile name must match [a-z0-9][a-z0-9_-]{0,47}");
127
+ }
128
+ return name;
129
+ }
130
+ export function profilePath(name, home = daemonHome()) {
131
+ return resolve(home, "profiles", `${validateProfileName(name)}.json`);
132
+ }
133
+ async function atomicPrivateWrite(path, content, beforeCommit) {
134
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
135
+ await chmod(dirname(path), 0o700);
136
+ const temporary = `${path}.${randomUUID()}.tmp`;
137
+ try {
138
+ await writeFile(temporary, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
139
+ await beforeCommit?.(temporary);
140
+ await rename(temporary, path);
141
+ await chmod(path, 0o600);
142
+ }
143
+ finally {
144
+ await rm(temporary, { force: true });
145
+ }
146
+ }
147
+ const DPAPI_PROTECT = "$p=[Console]::In.ReadToEnd();$b=[Text.Encoding]::UTF8.GetBytes($p);$e=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Convert]::ToBase64String($e)";
148
+ const DPAPI_UNPROTECT = "$p=[Console]::In.ReadToEnd();$b=[Convert]::FromBase64String($p);$d=[Security.Cryptography.ProtectedData]::Unprotect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser);[Console]::Out.Write([Text.Encoding]::UTF8.GetString($d))";
149
+ async function powershellStdin(script, input) {
150
+ return new Promise((resolvePromise, reject) => {
151
+ const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
152
+ stdio: ["pipe", "pipe", "pipe"],
153
+ windowsHide: true,
154
+ });
155
+ const stdout = [];
156
+ const stderr = [];
157
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
158
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
159
+ child.once("error", reject);
160
+ child.once("close", (code) => {
161
+ if (code !== 0) {
162
+ reject(new Error(`Windows DPAPI operation failed: ${Buffer.concat(stderr).toString("utf8").trim() || `exit ${code}`}`));
163
+ return;
164
+ }
165
+ resolvePromise(Buffer.concat(stdout).toString("utf8").trim());
166
+ });
167
+ child.stdin.end(input);
168
+ });
169
+ }
170
+ export const windowsDpapiProtector = {
171
+ protect: (plaintext) => powershellStdin(DPAPI_PROTECT, plaintext),
172
+ unprotect: (ciphertext) => powershellStdin(DPAPI_UNPROTECT, ciphertext),
173
+ };
174
+ function storageOptions(options) {
175
+ const platform = options.platform ?? process.platform;
176
+ return {
177
+ platform,
178
+ ...(options.protector ? { protector: options.protector } : platform === "win32" ? { protector: windowsDpapiProtector } : {}),
179
+ ...(options.harden ? { harden: options.harden } : {}),
180
+ };
181
+ }
182
+ function attachCleanupError(primaryError, cleanupError) {
183
+ try {
184
+ if (!(primaryError instanceof Error)
185
+ || !Object.isExtensible(primaryError)
186
+ || Object.prototype.hasOwnProperty.call(primaryError, "cleanupError"))
187
+ return;
188
+ Object.defineProperty(primaryError, "cleanupError", {
189
+ configurable: true,
190
+ value: cleanupError,
191
+ });
192
+ }
193
+ catch {
194
+ // Preserve the primary error even when it cannot accept diagnostics.
195
+ }
196
+ }
197
+ export async function saveProfile(input, home = daemonHome(), options = {}) {
198
+ const profile = ProfileSchema.parse({ version: 1, ...input });
199
+ const configured = storageOptions(options);
200
+ const release = await acquireProfileSaveLock(resolve(home, "profiles"), {
201
+ ...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
202
+ ...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
203
+ ...(options.lockNow === undefined ? {} : { now: options.lockNow }),
204
+ ...(options.lockWait === undefined ? {} : { wait: options.lockWait }),
205
+ ...(options.lockProcessController === undefined
206
+ ? {}
207
+ : { processController: options.lockProcessController }),
208
+ ...(options.lockHooks === undefined ? {} : { hooks: options.lockHooks }),
209
+ });
210
+ let bodyFailed = false;
211
+ let bodyError;
212
+ try {
213
+ await assertProfileAgentsRootUnique(profile, home, options.userHome ?? homedir(), options);
214
+ if (configured.platform === "win32") {
215
+ if (!configured.protector)
216
+ throw new Error("Windows profile storage requires CurrentUser DPAPI");
217
+ if (!configured.harden)
218
+ throw new Error("Windows profile storage requires ACL hardening");
219
+ const ciphertext = await configured.protector.protect(profile.machineToken);
220
+ if (!ciphertext)
221
+ throw new Error("Windows DPAPI returned an empty ciphertext");
222
+ const stored = StoredProtectedProfileSchema.parse({
223
+ version: 1,
224
+ name: profile.name,
225
+ serverUrl: profile.serverUrl,
226
+ ...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
227
+ ...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
228
+ machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
229
+ });
230
+ await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(stored, null, 2)}\n`, configured.harden);
231
+ }
232
+ else {
233
+ await atomicPrivateWrite(profilePath(profile.name, home), `${JSON.stringify(profile, null, 2)}\n`);
234
+ }
235
+ }
236
+ catch (error) {
237
+ bodyFailed = true;
238
+ bodyError = error;
239
+ throw error;
240
+ }
241
+ finally {
242
+ try {
243
+ await release();
244
+ }
245
+ catch (cleanupError) {
246
+ if (!bodyFailed)
247
+ throw cleanupError;
248
+ attachCleanupError(bodyError, cleanupError);
249
+ }
250
+ }
251
+ return profile;
252
+ }
253
+ export async function loadProfile(name, home = daemonHome(), options = {}) {
254
+ const path = profilePath(name, home);
255
+ try {
256
+ const stored = StoredProfileSchema.parse(JSON.parse(await readFile(path, "utf8")));
257
+ const configured = storageOptions(options);
258
+ if (configured.platform === "win32") {
259
+ if (!("machineTokenProtected" in stored)) {
260
+ throw new Error(`Computer profile '${name}' contains a plaintext token and is refused on Windows`);
261
+ }
262
+ if (!configured.protector)
263
+ throw new Error("Windows profile storage requires CurrentUser DPAPI");
264
+ return ProfileSchema.parse({
265
+ version: stored.version,
266
+ name: stored.name,
267
+ serverUrl: stored.serverUrl,
268
+ ...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
269
+ ...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
270
+ machineToken: await configured.protector.unprotect(stored.machineTokenProtected.ciphertext),
271
+ });
272
+ }
273
+ if (!("machineToken" in stored)) {
274
+ throw new Error(`Computer profile '${name}' is DPAPI-protected and can only be loaded by its Windows user`);
275
+ }
276
+ return stored;
277
+ }
278
+ catch (error) {
279
+ if (error.code === "ENOENT") {
280
+ throw new Error(`Computer profile '${name}' does not exist`);
281
+ }
282
+ throw error;
283
+ }
284
+ }
285
+ export async function listProfiles(home = daemonHome()) {
286
+ const root = resolve(home, "profiles");
287
+ try {
288
+ const entries = await readdir(root, { withFileTypes: true });
289
+ return entries
290
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
291
+ .map((entry) => entry.name.slice(0, -5))
292
+ .filter((name) => PROFILE_NAME.test(name))
293
+ .sort();
294
+ }
295
+ catch (error) {
296
+ if (error.code === "ENOENT")
297
+ return [];
298
+ throw error;
299
+ }
300
+ }
301
+ export async function inspectProfileAgentsRoot(profile, home = daemonHome(), userHome = homedir(), options = {}) {
302
+ const platform = storageOptions(options).platform;
303
+ const agentsRoot = resolveAgentsRoot(profile.agentsRoot, userHome, platform);
304
+ const comparableRoot = comparableAgentsRoot(agentsRoot, platform);
305
+ const names = (await listProfiles(home)).filter((name) => name !== profile.name);
306
+ const profiles = await Promise.all(names.map((name) => loadProfile(name, home, options)));
307
+ return {
308
+ agentsRoot,
309
+ duplicateProfiles: profiles
310
+ .filter((candidate) => comparableAgentsRoot(resolveAgentsRoot(candidate.agentsRoot, userHome, platform), platform) === comparableRoot)
311
+ .map((candidate) => candidate.name),
312
+ };
313
+ }
314
+ export async function assertProfileAgentsRootUnique(profile, home = daemonHome(), userHome = homedir(), options = {}) {
315
+ const candidate = ProfileAgentsRootCandidateSchema.parse({
316
+ name: profile.name,
317
+ serverUrl: profile.serverUrl,
318
+ ...(profile.agentsRoot === undefined ? {} : { agentsRoot: profile.agentsRoot }),
319
+ });
320
+ const inspection = await inspectProfileAgentsRoot(candidate, home, userHome, options);
321
+ const conflict = inspection.duplicateProfiles[0];
322
+ if (conflict === undefined)
323
+ return;
324
+ throw new ProfileAgentsRootConflictError({
325
+ profile: candidate.name,
326
+ conflict,
327
+ agentsRoot: inspection.agentsRoot,
328
+ serverUrl: candidate.serverUrl,
329
+ userHome,
330
+ platform: storageOptions(options).platform,
331
+ });
332
+ }
333
+ export async function removeProfile(name, home = daemonHome()) {
334
+ await rm(profilePath(name, home), { force: true });
335
+ }
336
+ export async function profileIsPrivate(name, home = daemonHome(), platform = process.platform) {
337
+ const path = profilePath(name, home);
338
+ await access(path, constants.R_OK);
339
+ if (platform === "win32") {
340
+ const stored = StoredProfileSchema.parse(JSON.parse(await readFile(path, "utf8")));
341
+ return "machineTokenProtected" in stored && stored.machineTokenProtected.scheme === "dpapi-current-user";
342
+ }
343
+ const { mode } = await stat(path);
344
+ return (mode & 0o077) === 0;
345
+ }
346
+ export function publicProfile(profile) {
347
+ return {
348
+ version: profile.version,
349
+ name: profile.name,
350
+ serverUrl: profile.serverUrl,
351
+ machineTokenConfigured: true,
352
+ ...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
353
+ };
354
+ }
355
+ export function applyProfileToEnv(profile, env) {
356
+ env.CREW_SERVER_URL = profile.serverUrl;
357
+ env.CREW_MACHINE_TOKEN = profile.machineToken;
358
+ if (profile.agentsRoot)
359
+ env.CREW_AGENTS_ROOT = profile.agentsRoot;
360
+ else
361
+ delete env.CREW_AGENTS_ROOT;
362
+ if (profile.runtimePath)
363
+ env.PATH = profile.runtimePath;
364
+ }