@nowcrew/daemon 0.5.18 → 0.5.20
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 +23 -0
- package/dist/attachments.js +196 -0
- package/dist/computer-cli.js +72 -12
- package/dist/computer-profile-lock.js +395 -0
- package/dist/computer-profile.js +189 -20
- package/dist/config.js +2 -1
- package/dist/console.js +175 -9
- package/dist/execution-event-limit.js +1 -1
- package/dist/execution-journal-lock.js +199 -40
- package/dist/execution-journal.js +42 -4
- package/dist/execution-protocol.js +21 -1
- package/dist/execution-recovery.js +71 -0
- package/dist/execution-runner.js +68 -77
- package/dist/execution-supervisor.js +79 -31
- package/dist/external-output.js +114 -0
- package/dist/i18n.js +5 -5
- package/dist/list-models.js +41 -5
- package/dist/local-executor.js +103 -14
- package/dist/machine-info.js +6 -1
- package/dist/main.js +23 -8
- package/dist/origin-decision.js +3 -1
- package/dist/prompt.js +4 -1
- package/dist/runner.js +14 -9
- package/dist/runtime-cancellation.js +74 -0
- package/dist/runtime-capabilities.js +38 -0
- package/dist/runtime-path.js +60 -0
- package/dist/runtimes/claude.js +9 -4
- package/dist/runtimes/codex-app-server-runner.js +340 -0
- package/dist/runtimes/codex.js +10 -4
- package/dist/runtimes/kimi-acp-runner.js +117 -17
- package/dist/runtimes/kimi.js +2 -0
- package/dist/runtimes/progress-watchdog.js +26 -0
- package/dist/serve-lifecycle.js +82 -0
- package/dist/serve.js +212 -212
- package/dist/session.js +1 -1
- package/dist/shared-execution-slots.js +68 -0
- package/dist/shutdown-deadline.js +32 -0
- package/dist/slog.js +34 -20
- package/dist/supervised-runtime.js +104 -0
- package/dist/websocket-shutdown.js +53 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -80,6 +80,25 @@ the local workspace/environment, and launches only a built-in runtime adapter (`
|
|
|
80
80
|
`kimi`). It does not decide collaboration rules, scheduled output policy, fallback delivery, or thread
|
|
81
81
|
behavior. Those are server responsibilities.
|
|
82
82
|
|
|
83
|
+
The stable adapters keep prompts out of provider argv wherever the provider protocol allows it:
|
|
84
|
+
|
|
85
|
+
| Runtime | Transport | System/wake input | Native resume |
|
|
86
|
+
| --- | --- | --- | --- |
|
|
87
|
+
| Claude | stream-json CLI | daemon-owned system prompt file | CLI session id |
|
|
88
|
+
| Codex | app-server JSON-RPC over stdio | `developerInstructions` + turn input | `thread/resume` |
|
|
89
|
+
| Kimi | ACP over stdio | `session/prompt` | `session/resume` (`session/load` compatibility fallback) |
|
|
90
|
+
|
|
91
|
+
Kimi ACP currently requires a Kimi account even when the CLI has a working custom provider. Only for
|
|
92
|
+
the explicit `Authentication required` response, the adapter retries once through Kimi's stream-json
|
|
93
|
+
CLI and carries the same native session id; unrelated ACP failures stay failures. The CLI fallback is
|
|
94
|
+
subject to the Windows UTF-16 argv guard, while protocol-v1 itself remains disabled on Windows until
|
|
95
|
+
durable Job Object ownership is available.
|
|
96
|
+
|
|
97
|
+
Codex and Kimi sessions use the same bounded per-task persistence and keyed lease as Claude when they
|
|
98
|
+
run through protocol v1. A first-progress watchdog covers a provider that accepts a turn but remains
|
|
99
|
+
semantically silent; after the first semantic event, the execution's configured total timeout remains
|
|
100
|
+
authoritative so a legitimate long-running tool is not killed for quiet output.
|
|
101
|
+
|
|
83
102
|
Protocol support and limits are advertised in `machine:hello`. `runtimes` reports every recognized CLI
|
|
84
103
|
found on `PATH`; `executionRuntimes` separately reports the installed CLIs backed by a complete built-in
|
|
85
104
|
adapter. The server must use the latter for admission and treats the former as diagnostic inventory only.
|
|
@@ -104,6 +123,10 @@ Before accepting work, the daemon writes a local journal entry under:
|
|
|
104
123
|
running supervisors are identity-checked and terminated before interruption is reported. A lock prevents
|
|
105
124
|
two daemon processes from sharing one journal.
|
|
106
125
|
|
|
126
|
+
Protocol v1 is advertised only when the platform has a durable process-tree backend. POSIX uses an
|
|
127
|
+
owned process group. Native Windows intentionally fails admission until a Job Object backend has passed
|
|
128
|
+
crash-cleanup tests; `taskkill /T` is not treated as equivalent ownership.
|
|
129
|
+
|
|
107
130
|
## Local Policy
|
|
108
131
|
|
|
109
132
|
Useful environment controls:
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, resolve, sep } from "node:path";
|
|
3
|
+
export const ATTACHMENT_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
4
|
+
export const ATTACHMENT_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
const throwIfAborted = (signal) => {
|
|
6
|
+
if (signal?.aborted)
|
|
7
|
+
throw signal.reason ?? new Error("Attachment materialization aborted");
|
|
8
|
+
};
|
|
9
|
+
function truncateUtf8(value, maxBytes) {
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
let output = "";
|
|
12
|
+
for (const character of value) {
|
|
13
|
+
const size = Buffer.byteLength(character, "utf8");
|
|
14
|
+
if (bytes + size > maxBytes)
|
|
15
|
+
break;
|
|
16
|
+
output += character;
|
|
17
|
+
bytes += size;
|
|
18
|
+
}
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
function safeFilename(value) {
|
|
22
|
+
const leaf = basename(value.replace(/\\/gu, "/"))
|
|
23
|
+
.replace(/[\u0000-\u001f\u007f]/gu, "_")
|
|
24
|
+
.replace(/^\.+/u, "")
|
|
25
|
+
.trim();
|
|
26
|
+
return truncateUtf8(leaf, 180) || "attachment";
|
|
27
|
+
}
|
|
28
|
+
function collisionName(filename, index) {
|
|
29
|
+
if (index === 1)
|
|
30
|
+
return filename;
|
|
31
|
+
const extension = extname(filename);
|
|
32
|
+
const stem = extension ? filename.slice(0, -extension.length) : filename;
|
|
33
|
+
return `${stem}-${index}${extension}`;
|
|
34
|
+
}
|
|
35
|
+
function containedPath(directory, filename) {
|
|
36
|
+
const target = resolve(directory, filename);
|
|
37
|
+
if (!target.startsWith(`${resolve(directory)}${sep}`)) {
|
|
38
|
+
throw new Error("Attachment path escapes the execution directory");
|
|
39
|
+
}
|
|
40
|
+
return target;
|
|
41
|
+
}
|
|
42
|
+
function checkedRedirect(value, base) {
|
|
43
|
+
const url = new URL(value, base);
|
|
44
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
45
|
+
throw new Error("Attachment redirects must use credential-free HTTPS URLs");
|
|
46
|
+
}
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
async function readBounded(response, maxBytes) {
|
|
50
|
+
const declared = Number(response.headers.get("content-length") ?? "");
|
|
51
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
52
|
+
throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
|
|
53
|
+
}
|
|
54
|
+
if (!response.body)
|
|
55
|
+
throw new Error("Attachment response body is empty");
|
|
56
|
+
const reader = response.body.getReader();
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let size = 0;
|
|
59
|
+
while (true) {
|
|
60
|
+
const part = await reader.read();
|
|
61
|
+
if (part.done)
|
|
62
|
+
break;
|
|
63
|
+
const chunk = Buffer.from(part.value);
|
|
64
|
+
size += chunk.length;
|
|
65
|
+
if (size > maxBytes) {
|
|
66
|
+
await reader.cancel();
|
|
67
|
+
throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
|
|
68
|
+
}
|
|
69
|
+
chunks.push(chunk);
|
|
70
|
+
}
|
|
71
|
+
return Buffer.concat(chunks, size);
|
|
72
|
+
}
|
|
73
|
+
async function downloadAttachment(input) {
|
|
74
|
+
const initial = new URL(`/agent/attachments/${encodeURIComponent(input.attachment.id)}/download`, input.serverUrl);
|
|
75
|
+
if (!["http:", "https:"].includes(initial.protocol)
|
|
76
|
+
|| initial.username || initial.password) {
|
|
77
|
+
throw new Error("Invalid NowWork attachment download URL");
|
|
78
|
+
}
|
|
79
|
+
let current = initial;
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
let timedOut = false;
|
|
82
|
+
const onAbort = () => controller.abort(input.signal?.reason ?? new Error("Attachment materialization aborted"));
|
|
83
|
+
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
84
|
+
if (input.signal?.aborted)
|
|
85
|
+
onAbort();
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
timedOut = true;
|
|
88
|
+
controller.abort(new Error(`Attachment download timed out after ${input.timeoutMs}ms`));
|
|
89
|
+
}, input.timeoutMs);
|
|
90
|
+
try {
|
|
91
|
+
let response = await input.fetchImpl(current, {
|
|
92
|
+
redirect: "manual",
|
|
93
|
+
headers: { authorization: `Bearer ${input.token}` },
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
});
|
|
96
|
+
for (let redirects = 0; [301, 302, 303, 307, 308].includes(response.status); redirects += 1) {
|
|
97
|
+
if (redirects >= input.maxRedirects)
|
|
98
|
+
throw new Error("Attachment redirect limit exceeded");
|
|
99
|
+
const location = response.headers.get("location");
|
|
100
|
+
if (!location)
|
|
101
|
+
throw new Error("Attachment redirect is missing a location");
|
|
102
|
+
current = checkedRedirect(location, current);
|
|
103
|
+
response = await input.fetchImpl(current, { redirect: "manual", signal: controller.signal });
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok)
|
|
106
|
+
throw new Error(`Attachment download failed with status ${response.status}`);
|
|
107
|
+
const data = await readBounded(response, input.maxFileBytes);
|
|
108
|
+
throwIfAborted(input.signal);
|
|
109
|
+
if (data.length !== input.attachment.sizeBytes) {
|
|
110
|
+
throw new Error(`Attachment size mismatch: expected ${input.attachment.sizeBytes}, received ${data.length}`);
|
|
111
|
+
}
|
|
112
|
+
return data;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (input.signal?.aborted) {
|
|
116
|
+
throw input.signal.reason ?? new Error("Attachment materialization aborted");
|
|
117
|
+
}
|
|
118
|
+
if (timedOut) {
|
|
119
|
+
throw new Error(`Attachment download timed out after ${input.timeoutMs}ms`);
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
input.signal?.removeEventListener("abort", onAbort);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export function executionAttachmentDirectory(runDir, executionId) {
|
|
129
|
+
const attachmentsRoot = resolve(runDir, "attachments");
|
|
130
|
+
const executionKey = executionId.replace(/[^A-Za-z0-9._-]/gu, "_");
|
|
131
|
+
const directory = resolve(attachmentsRoot, executionKey);
|
|
132
|
+
if (!directory.startsWith(`${attachmentsRoot}${sep}`)) {
|
|
133
|
+
throw new Error("Attachment directory escapes the run directory");
|
|
134
|
+
}
|
|
135
|
+
return directory;
|
|
136
|
+
}
|
|
137
|
+
export async function materializeAttachments(input) {
|
|
138
|
+
throwIfAborted(input.signal);
|
|
139
|
+
const maxFileBytes = input.maxFileBytes ?? ATTACHMENT_MAX_FILE_BYTES;
|
|
140
|
+
const maxTotalBytes = input.maxTotalBytes ?? ATTACHMENT_MAX_TOTAL_BYTES;
|
|
141
|
+
const total = input.attachments.reduce((sum, attachment) => sum + attachment.sizeBytes, 0);
|
|
142
|
+
if (input.attachments.some((attachment) => attachment.sizeBytes > maxFileBytes)
|
|
143
|
+
|| total > maxTotalBytes) {
|
|
144
|
+
throw new Error("Attachment metadata exceeds the configured byte limit");
|
|
145
|
+
}
|
|
146
|
+
const directory = executionAttachmentDirectory(input.runDir, input.executionId);
|
|
147
|
+
const used = new Set();
|
|
148
|
+
const materialized = [];
|
|
149
|
+
try {
|
|
150
|
+
await rm(directory, { recursive: true, force: true });
|
|
151
|
+
throwIfAborted(input.signal);
|
|
152
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
153
|
+
throwIfAborted(input.signal);
|
|
154
|
+
for (const attachment of input.attachments) {
|
|
155
|
+
const base = safeFilename(attachment.filename);
|
|
156
|
+
let collision = 1;
|
|
157
|
+
let filename = collisionName(base, collision);
|
|
158
|
+
while (used.has(filename.toLowerCase())) {
|
|
159
|
+
collision += 1;
|
|
160
|
+
filename = collisionName(base, collision);
|
|
161
|
+
}
|
|
162
|
+
used.add(filename.toLowerCase());
|
|
163
|
+
const path = containedPath(directory, filename);
|
|
164
|
+
const data = await downloadAttachment({
|
|
165
|
+
serverUrl: input.serverUrl,
|
|
166
|
+
token: input.token,
|
|
167
|
+
attachment,
|
|
168
|
+
fetchImpl: input.fetchImpl ?? fetch,
|
|
169
|
+
maxFileBytes,
|
|
170
|
+
maxRedirects: input.maxRedirects ?? 3,
|
|
171
|
+
timeoutMs: input.timeoutMs ?? 10_000,
|
|
172
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
173
|
+
});
|
|
174
|
+
throwIfAborted(input.signal);
|
|
175
|
+
await writeFile(path, data, {
|
|
176
|
+
flag: "wx",
|
|
177
|
+
mode: 0o600,
|
|
178
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
179
|
+
});
|
|
180
|
+
materialized.push({ ...attachment, filename, path });
|
|
181
|
+
}
|
|
182
|
+
return { directory, attachments: materialized };
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
try {
|
|
186
|
+
await rm(directory, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
catch (cleanupError) {
|
|
189
|
+
throw new AggregateError([error, cleanupError], "Attachment materialization failed and its execution directory could not be removed");
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export async function cleanupMaterializedAttachments(directory) {
|
|
195
|
+
await rm(directory, { recursive: true, force: true });
|
|
196
|
+
}
|
package/dist/computer-cli.js
CHANGED
|
@@ -2,8 +2,10 @@ import { parseArgs } from "node:util";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { daemonHome, listProfiles, loadProfile, profileIsPrivate, profilePath, publicProfile, removeProfile, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
|
|
5
|
+
import { assertProfileAgentsRootUnique, daemonHome, inspectProfileAgentsRoot, listProfiles, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, profileIsPrivate, profilePath, publicProfile, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
|
|
6
6
|
import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
|
|
7
|
+
import { defaultProcessController } from "./execution-journal.js";
|
|
8
|
+
import { inspectJournalLock } from "./execution-journal-lock.js";
|
|
7
9
|
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
8
10
|
const COMPUTER_COMMANDS = new Set(["profile", "doctor", "install", "uninstall", "start", "stop", "restart", "status", "upgrade"]);
|
|
9
11
|
export function builtDaemonEntry(moduleUrl = import.meta.url) {
|
|
@@ -60,6 +62,17 @@ function requireValue(value, name, td) {
|
|
|
60
62
|
throw new Error(td("Missing {{name}}", { name }));
|
|
61
63
|
return value;
|
|
62
64
|
}
|
|
65
|
+
function translatedError(error, td) {
|
|
66
|
+
if (error instanceof ProfileAgentsRootConflictError) {
|
|
67
|
+
return td(PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
|
|
68
|
+
profile: error.profile,
|
|
69
|
+
conflict: error.conflict,
|
|
70
|
+
agentsRoot: error.agentsRoot,
|
|
71
|
+
command: error.command,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return td(error.message);
|
|
75
|
+
}
|
|
63
76
|
export async function runComputerCommand(argv, overrides = {}) {
|
|
64
77
|
const command = argv[0];
|
|
65
78
|
if (!command || !COMPUTER_COMMANDS.has(command))
|
|
@@ -88,6 +101,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
88
101
|
const home = daemonHome(deps.env);
|
|
89
102
|
const profileStorage = {
|
|
90
103
|
platform: deps.platform,
|
|
104
|
+
userHome: deps.userHome,
|
|
91
105
|
...(deps.profileProtector ? { protector: deps.profileProtector } : {}),
|
|
92
106
|
...(deps.platform === "win32"
|
|
93
107
|
? { harden: (path) => hardenWindowsProfile(path, deps.runner) }
|
|
@@ -124,22 +138,28 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
124
138
|
const machineToken = (fromStdin ? await deps.readStdin() : deps.env.CREW_MACHINE_TOKEN)?.trim();
|
|
125
139
|
if (!machineToken)
|
|
126
140
|
throw new Error(td("Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin"));
|
|
127
|
-
|
|
141
|
+
const profile = {
|
|
128
142
|
name,
|
|
129
143
|
serverUrl: requireValue(parsed.values["server-url"], "--server-url", td),
|
|
130
144
|
machineToken,
|
|
131
|
-
...(parsed.values["agents-root"]
|
|
145
|
+
...(parsed.values["agents-root"]
|
|
146
|
+
? { agentsRoot: resolveAgentsRoot(parsed.values["agents-root"], deps.userHome, deps.platform) }
|
|
147
|
+
: {}),
|
|
132
148
|
...(deps.env.PATH ? { runtimePath: deps.env.PATH } : {}),
|
|
133
|
-
}
|
|
149
|
+
};
|
|
150
|
+
await saveProfile(profile, home, profileStorage);
|
|
134
151
|
deps.stdout(`${td("Saved profile '{{name}}' with private credentials.", { name })}\n`);
|
|
135
152
|
return 0;
|
|
136
153
|
}
|
|
137
154
|
const profileName = parsed.values.profile;
|
|
138
155
|
if (command === "upgrade") {
|
|
139
|
-
const
|
|
156
|
+
const restartProfile = profileName
|
|
157
|
+
? await loadProfile(profileName, home, profileStorage)
|
|
158
|
+
: null;
|
|
159
|
+
const spec = restartProfile
|
|
140
160
|
? buildServiceSpec({
|
|
141
161
|
...deps,
|
|
142
|
-
profile:
|
|
162
|
+
profile: restartProfile.name,
|
|
143
163
|
profileHome: home,
|
|
144
164
|
})
|
|
145
165
|
: null;
|
|
@@ -152,9 +172,18 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
152
172
|
throw new Error(td("Service '{{id}}' is not installed", { id: spec.id }));
|
|
153
173
|
}
|
|
154
174
|
await upgradeDaemon(deps.platform, deps.runner);
|
|
155
|
-
if (
|
|
175
|
+
if (restartProfile) {
|
|
176
|
+
try {
|
|
177
|
+
await assertProfileAgentsRootUnique(restartProfile, home, deps.userHome, profileStorage);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (!(error instanceof ProfileAgentsRootConflictError))
|
|
181
|
+
throw error;
|
|
182
|
+
deps.stdout(`${td("Upgraded daemon but skipped restart for '{{name}}': {{reason}}", { name: restartProfile.name, reason: translatedError(error, td) })}\n`);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
156
185
|
await serviceAction(spec, "restart", deps.runner);
|
|
157
|
-
deps.stdout(`${td("Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.", { name:
|
|
186
|
+
deps.stdout(`${td("Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.", { name: restartProfile.name })}\n`);
|
|
158
187
|
}
|
|
159
188
|
else {
|
|
160
189
|
deps.stdout(`${td("Upgraded daemon. Installed services were not restarted; pass --profile to restart one.")}\n`);
|
|
@@ -162,7 +191,10 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
162
191
|
return 0;
|
|
163
192
|
}
|
|
164
193
|
const name = requireValue(profileName, "--profile", td);
|
|
165
|
-
await loadProfile(name, home, profileStorage);
|
|
194
|
+
const profile = await loadProfile(name, home, profileStorage);
|
|
195
|
+
if (command === "install" || command === "start" || command === "restart") {
|
|
196
|
+
await assertProfileAgentsRootUnique(profile, home, deps.userHome, profileStorage);
|
|
197
|
+
}
|
|
166
198
|
if (!deps.entryPath.endsWith(".js")) {
|
|
167
199
|
throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
|
|
168
200
|
}
|
|
@@ -199,16 +231,44 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
199
231
|
const privateFile = deps.platform === "win32"
|
|
200
232
|
? await windowsProfileIsPrivate(profilePath(name, home), deps.runner)
|
|
201
233
|
: await profileIsPrivate(name, home, deps.platform);
|
|
202
|
-
const
|
|
234
|
+
const serviceChecks = await doctorService(spec, privateFile, deps.runner);
|
|
235
|
+
const rootInspection = await inspectProfileAgentsRoot(profile, home, deps.userHome, profileStorage);
|
|
236
|
+
const journalPath = resolve(rootInspection.agentsRoot, ".crew", "executions");
|
|
237
|
+
const journalLock = await inspectJournalLock({
|
|
238
|
+
directory: journalPath,
|
|
239
|
+
inspectIdentity: defaultProcessController.inspectIdentity,
|
|
240
|
+
});
|
|
241
|
+
const checks = [
|
|
242
|
+
...serviceChecks,
|
|
243
|
+
{
|
|
244
|
+
name: "agents-root-unique",
|
|
245
|
+
ok: rootInspection.duplicateProfiles.length === 0,
|
|
246
|
+
detail: rootInspection.duplicateProfiles.length === 0
|
|
247
|
+
? "unique"
|
|
248
|
+
: `also used by: ${rootInspection.duplicateProfiles.join(", ")}`,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "journal-lock-health",
|
|
252
|
+
ok: journalLock.status === "unlocked" || journalLock.status === "owned",
|
|
253
|
+
detail: journalLock.detail,
|
|
254
|
+
},
|
|
255
|
+
];
|
|
203
256
|
const registered = (await serviceStatus(spec, deps.runner)).installed;
|
|
204
|
-
deps.stdout(`${JSON.stringify({
|
|
257
|
+
deps.stdout(`${JSON.stringify({
|
|
258
|
+
agentsRoot: rootInspection.agentsRoot,
|
|
259
|
+
journalPath,
|
|
260
|
+
duplicateProfiles: rootInspection.duplicateProfiles,
|
|
261
|
+
journalLock,
|
|
262
|
+
checks,
|
|
263
|
+
registered,
|
|
264
|
+
}, null, 2)}\n`);
|
|
205
265
|
return checks.every((check) => check.ok) ? 0 : 4;
|
|
206
266
|
}
|
|
207
267
|
deps.stderr(usage(td));
|
|
208
268
|
return 2;
|
|
209
269
|
}
|
|
210
270
|
catch (error) {
|
|
211
|
-
deps.stderr(`crew-daemon: ${
|
|
271
|
+
deps.stderr(`crew-daemon: ${translatedError(error, td)}\n`);
|
|
212
272
|
return 1;
|
|
213
273
|
}
|
|
214
274
|
}
|