@nowcrew/daemon 0.6.16 → 0.6.18
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/agent-ability/runtime-context.js +7 -1
- package/dist/atomic-private-write.js +54 -1
- package/dist/automatic-install-target.js +40 -11
- package/dist/console.js +9 -0
- package/dist/control-plane-url.js +2 -2
- package/dist/daemon-migration-controller.js +198 -0
- package/dist/daemon-migration-wiring.js +22 -0
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/directory-projection-identity.js +32 -0
- package/dist/directory-projection.js +922 -0
- package/dist/execution-protocol.js +78 -11
- package/dist/execution-runner.js +50 -2
- package/dist/i18n.js +1 -0
- package/dist/local-execution-prompt.js +57 -0
- package/dist/local-executor.js +99 -40
- package/dist/machine-info.js +45 -9
- package/dist/main.js +0 -0
- package/dist/normalize.js +5 -0
- package/dist/profile-layout.js +41 -0
- package/dist/project-skills/controller.js +74 -14
- package/dist/project-skills/execution-adapter.js +11 -0
- package/dist/project-skills/initialized-reconciler.js +20 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +153 -0
- package/dist/project-skills/projection-state-store.js +841 -0
- package/dist/project-skills/projection-state-transaction.js +318 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/reconciler.js +299 -68
- package/dist/project-skills/runtime-warning.js +6 -0
- package/dist/project-skills/scanner.js +30 -1
- package/dist/project-skills/types.js +9 -0
- package/dist/project-workspaces/resolver.js +179 -0
- package/dist/project-workspaces/types.js +1 -0
- package/dist/prompt.js +40 -0
- package/dist/runtimes/claude.js +235 -4
- package/dist/runtimes/codex-app-server-runner.js +100 -25
- package/dist/runtimes/codex-contract.js +123 -0
- package/dist/runtimes/codex.js +2 -0
- package/dist/serve.js +31 -17
- package/dist/session.js +3 -0
- package/dist/supervised-runtime.js +12 -4
- package/dist/workspace.js +14 -5
- package/package.json +10 -9
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { createInterface } from "node:readline";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import spawn from "cross-spawn";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { signalSupervisorTree } from "../execution-supervisor.js";
|
|
9
|
+
import { assertCodexNativeWorkspaceVersion, assertCodexProjectWorkspaceResponse, assertCodexSkillsListResponse, boundedCodexPlan, codexProjectWorkspaceUnsupported, } from "./codex-contract.js";
|
|
8
10
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
9
11
|
const RunnerInputSchema = z.object({
|
|
10
12
|
systemPrompt: z.string().min(1),
|
|
@@ -15,6 +17,9 @@ const RunnerInputSchema = z.object({
|
|
|
15
17
|
sessionId: z.string().min(1).optional(),
|
|
16
18
|
imagePaths: z.array(z.string().min(1)).optional(),
|
|
17
19
|
projectRootMarkers: z.array(z.string().min(1).max(64).regex(/^[A-Za-z0-9._-]+$/u)).max(16).optional(),
|
|
20
|
+
cwd: z.string().min(1).optional(),
|
|
21
|
+
workspaceRoots: z.array(z.string().min(1)).optional(),
|
|
22
|
+
skillRoots: z.array(z.string().min(1)).optional(),
|
|
18
23
|
resume: z.boolean(),
|
|
19
24
|
}).strict();
|
|
20
25
|
const RPC_TIMEOUT_MS = 30_000;
|
|
@@ -32,6 +37,7 @@ const MAX_TRANSIENT_TURN_RETRIES = 2;
|
|
|
32
37
|
const TRANSIENT_TURN_RETRY_DELAY_MS = 15_000;
|
|
33
38
|
const TRANSIENT_TURN_RETRY_BACKOFF_FACTOR = 3;
|
|
34
39
|
const PROCESS_TREE_STOP_TIMEOUT_MS = 1_000;
|
|
40
|
+
const USER_INPUT_UNAVAILABLE_MESSAGE = "NowWork cannot collect interactive user input in this headless run; ask the user in the final response instead of assuming an answer.";
|
|
35
41
|
export function codexAppServerArgs(projectRootMarkers) {
|
|
36
42
|
return [
|
|
37
43
|
...(projectRootMarkers === undefined
|
|
@@ -65,12 +71,8 @@ export function codexRpcTimeoutMs(method) {
|
|
|
65
71
|
return method === "initialize" ? INITIALIZE_RPC_TIMEOUT_MS : RPC_TIMEOUT_MS;
|
|
66
72
|
}
|
|
67
73
|
function safeErrorMessage(error, secrets) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (secret)
|
|
71
|
-
message = message.replaceAll(secret, "[prompt redacted]");
|
|
72
|
-
}
|
|
73
|
-
return message.slice(0, ERROR_MESSAGE_CAP);
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
return sanitizeCodexDiagnostic(message, secrets).slice(0, ERROR_MESSAGE_CAP);
|
|
74
76
|
}
|
|
75
77
|
function sensitiveEnvironmentValues(env) {
|
|
76
78
|
return Object.entries(env)
|
|
@@ -79,13 +81,34 @@ function sensitiveEnvironmentValues(env) {
|
|
|
79
81
|
.map(([, value]) => value);
|
|
80
82
|
}
|
|
81
83
|
function redactionSecrets(input, env) {
|
|
82
|
-
const
|
|
84
|
+
const pathValues = [
|
|
85
|
+
process.cwd(),
|
|
86
|
+
...(input.imagePaths ?? []),
|
|
87
|
+
...(input.cwd === undefined ? [] : [input.cwd]),
|
|
88
|
+
...(input.workspaceRoots ?? []),
|
|
89
|
+
...(input.skillRoots ?? []),
|
|
90
|
+
];
|
|
91
|
+
const realpathAliases = pathValues.flatMap((value) => {
|
|
92
|
+
try {
|
|
93
|
+
return [realpathSync.native(value)];
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const values = [
|
|
100
|
+
input.systemPrompt,
|
|
101
|
+
input.wakePrompt,
|
|
102
|
+
...pathValues,
|
|
103
|
+
...realpathAliases,
|
|
104
|
+
...sensitiveEnvironmentValues(env),
|
|
105
|
+
];
|
|
83
106
|
return [...new Set(values.flatMap((value) => [
|
|
84
107
|
value,
|
|
85
108
|
...value.split(/\r?\n/).map((line) => line.trim()),
|
|
86
109
|
]).filter(Boolean))];
|
|
87
110
|
}
|
|
88
|
-
export function
|
|
111
|
+
export function sanitizeCodexDiagnostic(text, secrets) {
|
|
89
112
|
let redacted = text;
|
|
90
113
|
for (const secret of [...secrets].sort((left, right) => right.length - left.length)) {
|
|
91
114
|
if (secret)
|
|
@@ -93,9 +116,13 @@ export function redactCodexStderr(text, secrets) {
|
|
|
93
116
|
}
|
|
94
117
|
return redacted
|
|
95
118
|
.replace(/(Bearer\s+)[^\s"']+/gi, "$1[redacted]")
|
|
96
|
-
.replace(/\bsk-[A-Za-z0-9_-]{6,}\b/
|
|
97
|
-
.replace(/((?:api[_-]?key|access[_-]?token|auth(?:orization)?|password|secret)\s*[=:]\s*)[^\s"']+/gi, "$1[redacted]")
|
|
119
|
+
.replace(/\bsk[-_][A-Za-z0-9_-]{6,}\b/gi, "[redacted]")
|
|
120
|
+
.replace(/((?:api[_-]?key|access[_-]?token|auth[_-]?token|token|auth(?:orization)?|password|secret)\s*[=:]\s*)[^\s"']+/gi, "$1[redacted]")
|
|
121
|
+
.replace(/(^|[\s"'(=:[{])(?:[A-Za-z]:[\\/]|\\\\)[^\s"'`<>|]+/g, "$1[redacted-path]")
|
|
122
|
+
.replace(/(^|[\s"'(=:[{])\/[^\s"'`<>|]+/g, "$1[redacted-path]");
|
|
98
123
|
}
|
|
124
|
+
/** @deprecated Use the shared diagnostic sanitizer for every runner-owned error surface. */
|
|
125
|
+
export const redactCodexStderr = sanitizeCodexDiagnostic;
|
|
99
126
|
class StderrCapture {
|
|
100
127
|
secrets;
|
|
101
128
|
tail = "";
|
|
@@ -110,7 +137,7 @@ class StderrCapture {
|
|
|
110
137
|
redactedTail() {
|
|
111
138
|
const pending = this.omittingLine
|
|
112
139
|
? STDERR_LINE_OMITTED
|
|
113
|
-
:
|
|
140
|
+
: sanitizeCodexDiagnostic(this.pendingLine, this.secrets);
|
|
114
141
|
return `${this.tail}${pending}`.slice(-STDERR_TAIL_CAP).trim();
|
|
115
142
|
}
|
|
116
143
|
append(chunk) {
|
|
@@ -143,7 +170,7 @@ class StderrCapture {
|
|
|
143
170
|
this.omittingLine = false;
|
|
144
171
|
}
|
|
145
172
|
emit(text) {
|
|
146
|
-
const redacted =
|
|
173
|
+
const redacted = sanitizeCodexDiagnostic(text, this.secrets);
|
|
147
174
|
process.stderr.write(redacted);
|
|
148
175
|
this.tail = `${this.tail}${redacted}`.slice(-STDERR_TAIL_CAP);
|
|
149
176
|
}
|
|
@@ -171,16 +198,26 @@ export function codexApprovalResponse(method, permission) {
|
|
|
171
198
|
|| method === "item/fileChange/requestApproval") {
|
|
172
199
|
return { decision: permission === "full_access" ? "accept" : "decline" };
|
|
173
200
|
}
|
|
174
|
-
if (method === "item/tool/requestUserInput")
|
|
175
|
-
return {
|
|
201
|
+
if (method === "item/tool/requestUserInput") {
|
|
202
|
+
return { error: { code: -32002, message: USER_INPUT_UNAVAILABLE_MESSAGE } };
|
|
203
|
+
}
|
|
176
204
|
return null;
|
|
177
205
|
}
|
|
178
206
|
/** Translate app-server v2 notifications into the daemon's existing runtime event contract. */
|
|
179
207
|
export function mapCodexNotification(method, params) {
|
|
180
|
-
const value = params;
|
|
208
|
+
const value = (params !== null && typeof params === "object" ? params : {});
|
|
181
209
|
if (method === "thread/started" && value.thread?.id) {
|
|
182
210
|
return [{ type: "thread.started", thread_id: value.thread.id }];
|
|
183
211
|
}
|
|
212
|
+
const plan = method === "turn/plan/updated" ? boundedCodexPlan(value.plan) : null;
|
|
213
|
+
if (plan !== null && typeof value.threadId === "string" && typeof value.turnId === "string") {
|
|
214
|
+
return [{
|
|
215
|
+
type: "turn.plan.updated",
|
|
216
|
+
thread_id: value.threadId,
|
|
217
|
+
turn_id: value.turnId,
|
|
218
|
+
plan,
|
|
219
|
+
}];
|
|
220
|
+
}
|
|
184
221
|
if (method !== "item/completed" || value.item === undefined)
|
|
185
222
|
return [];
|
|
186
223
|
if (value.item.type === "agentMessage" && value.item.text) {
|
|
@@ -221,14 +258,16 @@ class CodexRpcClient {
|
|
|
221
258
|
permission;
|
|
222
259
|
onNotification;
|
|
223
260
|
onFatal;
|
|
261
|
+
diagnosticSecrets;
|
|
224
262
|
nextId = 1;
|
|
225
263
|
pending = new Map();
|
|
226
264
|
closedError = null;
|
|
227
|
-
constructor(child, permission, onNotification, onFatal) {
|
|
265
|
+
constructor(child, permission, onNotification, onFatal, diagnosticSecrets) {
|
|
228
266
|
this.child = child;
|
|
229
267
|
this.permission = permission;
|
|
230
268
|
this.onNotification = onNotification;
|
|
231
269
|
this.onFatal = onFatal;
|
|
270
|
+
this.diagnosticSecrets = diagnosticSecrets;
|
|
232
271
|
if (child.stdin === null || child.stdout === null) {
|
|
233
272
|
throw new Error("Codex app-server did not expose stdio");
|
|
234
273
|
}
|
|
@@ -272,7 +311,7 @@ class CodexRpcClient {
|
|
|
272
311
|
message = JSON.parse(line);
|
|
273
312
|
}
|
|
274
313
|
catch {
|
|
275
|
-
process.stderr.write(`Codex app-server emitted invalid JSON: ${line.slice(0, 500)}\n
|
|
314
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex app-server emitted invalid JSON: ${line.slice(0, 500)}\n`, this.diagnosticSecrets));
|
|
276
315
|
return;
|
|
277
316
|
}
|
|
278
317
|
if (message.id !== undefined && message.method !== undefined) {
|
|
@@ -284,6 +323,10 @@ class CodexRpcClient {
|
|
|
284
323
|
error: { code: -32001, message: `NowCrew denied unsupported request ${message.method}` },
|
|
285
324
|
});
|
|
286
325
|
}
|
|
326
|
+
else if (result.error !== undefined && typeof result.error === "object" && result.error !== null) {
|
|
327
|
+
process.stderr.write("[codex-app-server] interactive user input is unavailable; returned a truthful RPC error\n");
|
|
328
|
+
this.write({ jsonrpc: "2.0", id: message.id, error: result.error });
|
|
329
|
+
}
|
|
287
330
|
else {
|
|
288
331
|
this.write({ jsonrpc: "2.0", id: message.id, result });
|
|
289
332
|
}
|
|
@@ -416,9 +459,9 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
416
459
|
if (method === "error" && params.willRetry === false) {
|
|
417
460
|
const detail = params.error?.message;
|
|
418
461
|
if (detail)
|
|
419
|
-
process.stderr.write(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n
|
|
462
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn error: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
|
|
420
463
|
}
|
|
421
|
-
}, completionReject);
|
|
464
|
+
}, completionReject, secrets);
|
|
422
465
|
let treeStopPromise = null;
|
|
423
466
|
const ensureChildTreeStopped = () => {
|
|
424
467
|
treeStopPromise ??= stopChildTree(child);
|
|
@@ -440,30 +483,62 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
440
483
|
let activeStage = "initialize";
|
|
441
484
|
let stageStartedAt = Date.now();
|
|
442
485
|
try {
|
|
443
|
-
await rpc.request("initialize", {
|
|
486
|
+
const initialized = await rpc.request("initialize", {
|
|
444
487
|
clientInfo: { name: "nowcrew-daemon", version: "1" },
|
|
445
488
|
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
446
489
|
}, initializeTimeoutMs);
|
|
490
|
+
if (input.workspaceRoots !== undefined || input.skillRoots !== undefined) {
|
|
491
|
+
assertCodexNativeWorkspaceVersion(initialized);
|
|
492
|
+
}
|
|
447
493
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
448
494
|
rpc.notify("initialized");
|
|
449
495
|
activeStage = input.resume ? "thread_resume" : "thread_start";
|
|
450
496
|
stageStartedAt = Date.now();
|
|
497
|
+
const threadCwd = input.cwd ?? process.cwd();
|
|
451
498
|
const threadParams = {
|
|
452
|
-
cwd:
|
|
499
|
+
cwd: threadCwd,
|
|
500
|
+
...(input.workspaceRoots === undefined
|
|
501
|
+
? {}
|
|
502
|
+
: { runtimeWorkspaceRoots: input.workspaceRoots }),
|
|
453
503
|
approvalPolicy: "never",
|
|
454
504
|
sandbox: sandboxMode(input.effectivePermission),
|
|
455
505
|
developerInstructions: input.systemPrompt,
|
|
456
506
|
...(input.model === undefined ? {} : { model: input.model }),
|
|
457
507
|
};
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
508
|
+
let thread;
|
|
509
|
+
const nativeContractRequired = input.workspaceRoots !== undefined || input.skillRoots !== undefined;
|
|
510
|
+
try {
|
|
511
|
+
thread = input.resume && input.sessionId !== undefined
|
|
512
|
+
? await rpc.request("thread/resume", { threadId: input.sessionId, ...threadParams })
|
|
513
|
+
: await rpc.request("thread/start", threadParams);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
if (nativeContractRequired)
|
|
517
|
+
throw codexProjectWorkspaceUnsupported();
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
if (nativeContractRequired) {
|
|
521
|
+
thread = assertCodexProjectWorkspaceResponse(thread, {
|
|
522
|
+
cwd: threadCwd,
|
|
523
|
+
workspaceRoots: input.workspaceRoots ?? [],
|
|
524
|
+
});
|
|
525
|
+
}
|
|
461
526
|
logStage(activeStage, "ok", attempt, stageStartedAt);
|
|
462
527
|
threadId = thread.thread.id;
|
|
463
528
|
if (announcedThreadId !== threadId) {
|
|
464
529
|
announcedThreadId = threadId;
|
|
465
530
|
await jsonLine({ type: "thread.started", thread_id: threadId });
|
|
466
531
|
}
|
|
532
|
+
if (input.skillRoots !== undefined) {
|
|
533
|
+
try {
|
|
534
|
+
await rpc.request("skills/extraRoots/set", { extraRoots: input.skillRoots });
|
|
535
|
+
const listed = await rpc.request("skills/list", { cwds: [threadCwd], forceReload: true });
|
|
536
|
+
assertCodexSkillsListResponse(listed, threadCwd);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
throw codexProjectWorkspaceUnsupported();
|
|
540
|
+
}
|
|
541
|
+
}
|
|
467
542
|
firstProgress = startFirstProgressWatchdog(() => {
|
|
468
543
|
completionReject(new Error("Codex produced no semantic progress within the startup window"));
|
|
469
544
|
void cancel();
|
|
@@ -501,7 +576,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
|
|
|
501
576
|
if (completed.turn?.status === "completed")
|
|
502
577
|
return { code: 0, initializeTimedOut: false };
|
|
503
578
|
const detail = completed.turn?.error?.message ?? `turn status ${completed.turn?.status ?? "unknown"}`;
|
|
504
|
-
process.stderr.write(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n
|
|
579
|
+
process.stderr.write(sanitizeCodexDiagnostic(`Codex turn failed: ${detail.slice(0, ERROR_MESSAGE_CAP)}\n`, secrets));
|
|
505
580
|
return {
|
|
506
581
|
code: completed.turn?.status === "interrupted" ? 130 : 1,
|
|
507
582
|
initializeTimedOut: false,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { CODEX_NATIVE_WORKSPACE_MIN_VERSION } from "./codex.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const CODEX_PROJECT_WORKSPACE_UNSUPPORTED = "codex_project_workspace_unsupported";
|
|
4
|
+
export const CODEX_PLAN_ENTRY_LIMIT = 16;
|
|
5
|
+
// 16 steps remain inside the server's 4,000 UTF-16-unit plan payload cap even
|
|
6
|
+
// when every retained code point is a surrogate pair (for example, emoji).
|
|
7
|
+
export const CODEX_PLAN_STEP_TEXT_LIMIT = 120;
|
|
8
|
+
const NativeInitializeResponseSchema = z.object({
|
|
9
|
+
userAgent: z.string().min(1),
|
|
10
|
+
codexHome: z.string().min(1),
|
|
11
|
+
platformFamily: z.string().min(1),
|
|
12
|
+
platformOs: z.string().min(1),
|
|
13
|
+
}).passthrough();
|
|
14
|
+
const NativeThreadWorkspaceSchema = z.object({
|
|
15
|
+
thread: z.object({ id: z.string().min(1) }).passthrough(),
|
|
16
|
+
cwd: z.string().min(1),
|
|
17
|
+
runtimeWorkspaceRoots: z.array(z.string().min(1)),
|
|
18
|
+
instructionSources: z.array(z.string().min(1)),
|
|
19
|
+
}).strict();
|
|
20
|
+
const SkillMetadataSchema = z.object({
|
|
21
|
+
name: z.string().min(1),
|
|
22
|
+
description: z.string(),
|
|
23
|
+
shortDescription: z.string().optional(),
|
|
24
|
+
interface: z.unknown().optional(),
|
|
25
|
+
dependencies: z.unknown().optional(),
|
|
26
|
+
path: z.string().min(1),
|
|
27
|
+
scope: z.string().min(1),
|
|
28
|
+
enabled: z.boolean(),
|
|
29
|
+
}).passthrough();
|
|
30
|
+
const SkillsListResponseSchema = z.object({
|
|
31
|
+
data: z.array(z.object({
|
|
32
|
+
cwd: z.string().min(1),
|
|
33
|
+
skills: z.array(SkillMetadataSchema),
|
|
34
|
+
errors: z.array(z.object({
|
|
35
|
+
path: z.string(),
|
|
36
|
+
message: z.string(),
|
|
37
|
+
}).passthrough()),
|
|
38
|
+
}).passthrough()),
|
|
39
|
+
}).passthrough();
|
|
40
|
+
const stringArrayEquals = (value, expected) => Array.isArray(value)
|
|
41
|
+
&& value.length === expected.length
|
|
42
|
+
&& value.every((entry, index) => typeof entry === "string" && entry === expected[index]);
|
|
43
|
+
const versionTuple = (version) => {
|
|
44
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/u.exec(version);
|
|
45
|
+
if (match === null)
|
|
46
|
+
return null;
|
|
47
|
+
const [, major = "0", minor = "0", patch = "0"] = match;
|
|
48
|
+
const tuple = [Number(major), Number(minor), Number(patch)];
|
|
49
|
+
return tuple.every(Number.isSafeInteger) ? tuple : null;
|
|
50
|
+
};
|
|
51
|
+
const versionAtLeast = (actual, minimum) => actual[0] > minimum[0]
|
|
52
|
+
|| (actual[0] === minimum[0] && (actual[1] > minimum[1]
|
|
53
|
+
|| (actual[1] === minimum[1] && actual[2] >= minimum[2])));
|
|
54
|
+
/** Native roots require the app-server version returned by initialize, not a separate shell probe. */
|
|
55
|
+
export function assertCodexNativeWorkspaceVersion(response) {
|
|
56
|
+
const parsed = NativeInitializeResponseSchema.safeParse(response);
|
|
57
|
+
if (!parsed.success)
|
|
58
|
+
throw codexProjectWorkspaceUnsupported();
|
|
59
|
+
const match = /(?:^|[/\s])(\d+\.\d+\.\d+)(?=$|[\s(])/u.exec(parsed.data.userAgent);
|
|
60
|
+
const actual = match === null ? null : versionTuple(match[1] ?? "");
|
|
61
|
+
const minimum = versionTuple(CODEX_NATIVE_WORKSPACE_MIN_VERSION);
|
|
62
|
+
if (actual === null || minimum === null || !versionAtLeast(actual, minimum)) {
|
|
63
|
+
throw codexProjectWorkspaceUnsupported();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const PLAN_STATUSES = new Set(["pending", "inProgress", "completed"]);
|
|
67
|
+
const clipCharacters = (value, limit) => [...value.trim().replace(/\s+/gu, " ")].slice(0, limit).join("");
|
|
68
|
+
/** Treat plan updates as bounded user-visible data; any malformed retained step rejects the event. */
|
|
69
|
+
export function boundedCodexPlan(raw) {
|
|
70
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
const plan = [];
|
|
73
|
+
for (const entry of raw.slice(0, CODEX_PLAN_ENTRY_LIMIT)) {
|
|
74
|
+
if (entry === null || typeof entry !== "object")
|
|
75
|
+
return null;
|
|
76
|
+
const value = entry;
|
|
77
|
+
if (typeof value.step !== "string"
|
|
78
|
+
|| typeof value.status !== "string"
|
|
79
|
+
|| !PLAN_STATUSES.has(value.status))
|
|
80
|
+
return null;
|
|
81
|
+
const step = clipCharacters(value.step, CODEX_PLAN_STEP_TEXT_LIMIT);
|
|
82
|
+
if (step === "")
|
|
83
|
+
return null;
|
|
84
|
+
plan.push(Object.freeze({ step, status: value.status }));
|
|
85
|
+
}
|
|
86
|
+
return Object.freeze(plan);
|
|
87
|
+
}
|
|
88
|
+
export function codexPlanText(plan) {
|
|
89
|
+
const marker = {
|
|
90
|
+
pending: " ", inProgress: "~", completed: "x",
|
|
91
|
+
};
|
|
92
|
+
return plan.map(({ step, status }) => `- [${marker[status]}] ${step}`).join("\n");
|
|
93
|
+
}
|
|
94
|
+
/** Bound launches require the experimental workspace fields to round-trip before any model turn. */
|
|
95
|
+
export function assertCodexProjectWorkspaceResponse(response, expected) {
|
|
96
|
+
const raw = response !== null && typeof response === "object"
|
|
97
|
+
? response
|
|
98
|
+
: {};
|
|
99
|
+
// Validate a strict projection so additions to the upstream response remain forward-compatible.
|
|
100
|
+
const parsed = NativeThreadWorkspaceSchema.safeParse({
|
|
101
|
+
thread: raw.thread,
|
|
102
|
+
cwd: raw.cwd,
|
|
103
|
+
runtimeWorkspaceRoots: raw.runtimeWorkspaceRoots,
|
|
104
|
+
instructionSources: raw.instructionSources,
|
|
105
|
+
});
|
|
106
|
+
if (!parsed.success
|
|
107
|
+
|| parsed.data.cwd !== expected.cwd
|
|
108
|
+
|| !stringArrayEquals(parsed.data.runtimeWorkspaceRoots, expected.workspaceRoots)) {
|
|
109
|
+
throw codexProjectWorkspaceUnsupported();
|
|
110
|
+
}
|
|
111
|
+
return parsed.data;
|
|
112
|
+
}
|
|
113
|
+
export function assertCodexSkillsListResponse(response, requestedCwd) {
|
|
114
|
+
const parsed = SkillsListResponseSchema.safeParse(response);
|
|
115
|
+
if (!parsed.success
|
|
116
|
+
|| !parsed.data.data.some((entry) => entry.cwd === requestedCwd)
|
|
117
|
+
|| parsed.data.data.some((entry) => entry.errors.length > 0)) {
|
|
118
|
+
throw codexProjectWorkspaceUnsupported();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function codexProjectWorkspaceUnsupported() {
|
|
122
|
+
return new Error(CODEX_PROJECT_WORKSPACE_UNSUPPORTED);
|
|
123
|
+
}
|
package/dist/runtimes/codex.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
// cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
|
|
5
5
|
import spawn from "cross-spawn";
|
|
6
|
+
/** Minimum CLI whose real app-server contract supports NowCrew native project workspaces. */
|
|
7
|
+
export const CODEX_NATIVE_WORKSPACE_MIN_VERSION = "0.148.0";
|
|
6
8
|
// Codex CLI 原生 model_reasoning_effort 档位(codex 0.135.0 实测:非法值时 config 解析报错枚举这六档)。
|
|
7
9
|
// 注意:codex 对非法值是硬失败(进程直接退出),所以必须白名单过滤;白名单外(含 "default"、
|
|
8
10
|
// claude 专属的 "max")回落 CODEX_DEFAULT_EFFORT。
|
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import { initSlog, dslog, setSlogDefaults, drainSpool, flushSlog } from "./slog.js";
|
|
9
9
|
import { mergeRunAgentResults, reportScheduledStartFailure, runAgent } from "./runner.js";
|
|
10
10
|
import { buildOriginDecisionRetryPrompt, buildScheduledPrompt } from "./prompt.js";
|
|
11
|
-
import { collectMachineHello, cliVersion, daemonVersion, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
|
|
11
|
+
import { collectMachineHello, cliVersion, daemonCapabilityBindings, daemonVersion, detectExecutionRuntimesWithSignal, } from "./machine-info.js";
|
|
12
12
|
import { conservativeExecutionRuntimes, createRuntimeProbeCoordinator, } from "./runtime-probe.js";
|
|
13
13
|
import { listWorkspace, readWorkspaceFile } from "./workspace-fs.js";
|
|
14
14
|
import { listSkills } from "./skills.js";
|
|
@@ -21,7 +21,7 @@ import { runWithOriginDecisionGuard } from "./origin-decision.js";
|
|
|
21
21
|
import { createExecutionJournal } from "./execution-journal.js";
|
|
22
22
|
import { createExecutionTelemetryJournal } from "./execution-telemetry-journal.js";
|
|
23
23
|
import { ExecutionRejectedSchema, ExecutionSnapshotSchema, LegacyAgentStartSchema, ServerToDaemonExecutionFrameSchema, } from "./execution-protocol.js";
|
|
24
|
-
import { hashExecutionSpec, runExecution, } from "./execution-runner.js";
|
|
24
|
+
import { hashExecutionSpec, projectWorkspaceCapabilityRejection, runExecution, } from "./execution-runner.js";
|
|
25
25
|
import { awaitWithCancellation, createRuntimeCancellation, RuntimeCancelledError, } from "./runtime-cancellation.js";
|
|
26
26
|
import { createShutdownDeadline, readTestShutdownConfiguration } from "./shutdown-deadline.js";
|
|
27
27
|
import { closeWebSocketWithinDeadline } from "./websocket-shutdown.js";
|
|
@@ -40,10 +40,12 @@ import { createProjectSkillsController, } from "./project-skills/controller.js";
|
|
|
40
40
|
import { PROJECT_SKILLS_CAPABILITY } from "./project-skills/types.js";
|
|
41
41
|
import { createAgentProjectionCoordinator } from "./project-skills/agent-projection-coordinator.js";
|
|
42
42
|
import { createAgentAbilityRuntime } from "./agent-ability/runtime.js";
|
|
43
|
-
import { createProjectSkillsReconciler,
|
|
43
|
+
import { createProjectSkillsReconciler, } from "./project-skills/reconciler.js";
|
|
44
|
+
import { initializedProjectSkillsReconciler } from "./project-skills/initialized-reconciler.js";
|
|
44
45
|
import { parseMemoryPruneTraceId } from "./memory-prune-diagnostics.js";
|
|
45
46
|
import { handleRuntimeProbeFrame, probeRuntimeHealth } from "./runtime-health.js";
|
|
46
47
|
import { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
48
|
+
import { createServeMigrationController, handleMigrationControlMessage } from "./daemon-migration-wiring.js";
|
|
47
49
|
export { buildControlPlaneUrl } from "./control-plane-url.js";
|
|
48
50
|
// normalize.ts 的活动种类 → activity 枚举
|
|
49
51
|
const ACTIVITY_MAP = {
|
|
@@ -104,6 +106,16 @@ export function serve(config, opts = {}) {
|
|
|
104
106
|
catch { /* reconnect/timeout reconciliation handles a lost status frame */ }
|
|
105
107
|
},
|
|
106
108
|
});
|
|
109
|
+
const migrationController = createServeMigrationController({
|
|
110
|
+
config,
|
|
111
|
+
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
112
|
+
isBusy: () => executionRuns.size > 0 || legacyRuns.size > 0,
|
|
113
|
+
sendStatus: (frame) => { try {
|
|
114
|
+
if (ws?.readyState === WebSocket.OPEN)
|
|
115
|
+
ws.send(JSON.stringify(frame));
|
|
116
|
+
}
|
|
117
|
+
catch { /* reconnect */ } },
|
|
118
|
+
});
|
|
107
119
|
const projectionCoordinator = createAgentProjectionCoordinator();
|
|
108
120
|
const agentAbilityRuntime = createAgentAbilityRuntime(config.agentsRoot, {
|
|
109
121
|
...opts.agentAbility,
|
|
@@ -127,6 +139,7 @@ export function serve(config, opts = {}) {
|
|
|
127
139
|
catch { /* 下一次 ready 或项目操作会重新发送完整快照 */ }
|
|
128
140
|
},
|
|
129
141
|
reconcile: (handle, bindings) => projectSkillsReconciler.reconcile(handle, bindings),
|
|
142
|
+
ensureSnapshot: (handle, snapshot) => projectSkillsReconciler.ensureSnapshot(handle, snapshot),
|
|
130
143
|
});
|
|
131
144
|
let projectSkillsStatus = "initializing";
|
|
132
145
|
let projectSkillsInitialization = null;
|
|
@@ -172,18 +185,7 @@ export function serve(config, opts = {}) {
|
|
|
172
185
|
projectSkillsInitialization = attempt;
|
|
173
186
|
return attempt;
|
|
174
187
|
};
|
|
175
|
-
const
|
|
176
|
-
async reconcile(handle, bindings) {
|
|
177
|
-
return await ensureProjectSkillsInitialized()
|
|
178
|
-
? projectSkillsReconciler.reconcile(handle, bindings)
|
|
179
|
-
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
180
|
-
},
|
|
181
|
-
async prepareAndLaunch(agentsRoot, handle, bindings, launch) {
|
|
182
|
-
return await ensureProjectSkillsInitialized()
|
|
183
|
-
? projectSkillsReconciler.prepareAndLaunch(agentsRoot, handle, bindings, launch)
|
|
184
|
-
: Promise.reject(new ProjectProjectionError("skill_projection_failed"));
|
|
185
|
-
},
|
|
186
|
-
};
|
|
188
|
+
const initializedProjectSkills = initializedProjectSkillsReconciler(ensureProjectSkillsInitialized, projectSkillsReconciler);
|
|
187
189
|
const safeExecutionSend = (frame) => {
|
|
188
190
|
try {
|
|
189
191
|
if (ws?.readyState !== WebSocket.OPEN)
|
|
@@ -281,6 +283,7 @@ export function serve(config, opts = {}) {
|
|
|
281
283
|
reservation.release();
|
|
282
284
|
};
|
|
283
285
|
let connectedAt = 0; // 本次 WS 连接建立时刻(断开日志算在线时长用)
|
|
286
|
+
const capabilities = daemonCapabilityBindings(process.platform, opts.machineInfo?.capabilities);
|
|
284
287
|
initSlog(config.serverUrl, config.machineToken, { daemonVersion: daemonVersion(), cliVersion: cliVersion(), ...(opts.profileName === undefined ? {} : { profileName: opts.profileName }), agentsRoot: config.agentsRoot });
|
|
285
288
|
dslog("daemon.start", "daemon 常驻模式启动", { server_url: config.serverUrl, runtime: config.runtimeBin });
|
|
286
289
|
// 并行调度:同一 agent 可并行处理多个【不同任务】(线程/频道),每任务隔离 cwd+work-log。
|
|
@@ -293,7 +296,7 @@ export function serve(config, opts = {}) {
|
|
|
293
296
|
function connect() {
|
|
294
297
|
if (stopped)
|
|
295
298
|
return;
|
|
296
|
-
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready");
|
|
299
|
+
const wsUrl = buildControlPlaneUrl(config.serverUrl, config.machineToken, process.platform, undefined, projectSkillsStatus === "ready", capabilities.controlPlaneUrl);
|
|
297
300
|
ws = createWebSocket(wsUrl);
|
|
298
301
|
ws.on("open", () => {
|
|
299
302
|
const openedSocket = ws;
|
|
@@ -307,9 +310,11 @@ export function serve(config, opts = {}) {
|
|
|
307
310
|
const { detectInstalled, detectExecutable = detectExecutionRuntimesWithSignal } = opts.machineInfo ?? {};
|
|
308
311
|
const helloPromise = collectMachineHello(config.agentsRoot, config.executionLimits, process.platform, {
|
|
309
312
|
...(detectInstalled ? { detectInstalled } : {}),
|
|
313
|
+
...(opts.profileName === undefined ? {} : { profileName: opts.profileName }),
|
|
310
314
|
// First hello must not wait for third-party handshakes; optional transports arrive in the refresh.
|
|
311
315
|
detectExecutable: async (installed) => conservativeExecutionRuntimes(installed),
|
|
312
316
|
additionalCapabilities: () => managedDaemonCapabilities(updateEligibility),
|
|
317
|
+
capabilities: capabilities.machineHello,
|
|
313
318
|
});
|
|
314
319
|
runtimeFacts = helloPromise
|
|
315
320
|
.then(async (hello) => {
|
|
@@ -390,6 +395,10 @@ export function serve(config, opts = {}) {
|
|
|
390
395
|
});
|
|
391
396
|
return;
|
|
392
397
|
}
|
|
398
|
+
if (rawType === "daemon:migrate") {
|
|
399
|
+
void handleMigrationControlMessage(migrationController, decoded).catch((error) => dslog("daemon.migration_failed", "daemon 布局迁移失败", { level: "ERROR", error_message: error.message }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
393
402
|
if (rawType.startsWith("execution:")) {
|
|
394
403
|
const parsedExecution = ServerToDaemonExecutionFrameSchema.safeParse(decoded);
|
|
395
404
|
if (!parsedExecution.success) {
|
|
@@ -451,6 +460,9 @@ export function serve(config, opts = {}) {
|
|
|
451
460
|
return;
|
|
452
461
|
}
|
|
453
462
|
const spec = frame;
|
|
463
|
+
const workspaceRejection = projectWorkspaceCapabilityRejection(spec, capabilities.executionRunner, new Date().toISOString());
|
|
464
|
+
if (workspaceRejection !== null)
|
|
465
|
+
return void safeExecutionSend(workspaceRejection);
|
|
454
466
|
const hash = hashExecutionSpec(spec);
|
|
455
467
|
const knownHash = knownExecutionHashes.get(spec.executionId);
|
|
456
468
|
if (knownHash !== undefined) {
|
|
@@ -587,7 +599,8 @@ export function serve(config, opts = {}) {
|
|
|
587
599
|
}
|
|
588
600
|
const execution = executeProtocol(config, spec, {
|
|
589
601
|
...opts.execution?.dependencies,
|
|
590
|
-
|
|
602
|
+
capabilities: capabilities.executionRunner,
|
|
603
|
+
projectSkills: opts.execution?.dependencies?.projectSkills ?? initializedProjectSkills,
|
|
591
604
|
abilityRelease: opts.execution?.dependencies?.abilityRelease ?? agentAbilityRuntime.materializer,
|
|
592
605
|
...(agentMemory === undefined ? {} : { agentMemory }),
|
|
593
606
|
journal: executionJournal,
|
|
@@ -1154,6 +1167,7 @@ export function serve(config, opts = {}) {
|
|
|
1154
1167
|
const pending = [
|
|
1155
1168
|
webSocketClosed,
|
|
1156
1169
|
updateController.drain(),
|
|
1170
|
+
...(migrationController === null ? [] : [migrationController.drain()]),
|
|
1157
1171
|
executionFrameQueue,
|
|
1158
1172
|
...executionRuns.values(),
|
|
1159
1173
|
...[...legacyRuns.values()].map((run) => run.done),
|
package/dist/session.js
CHANGED
|
@@ -29,6 +29,9 @@ export async function readSession(runDir) {
|
|
|
29
29
|
turns: typeof o.turns === "number" ? o.turns : 0,
|
|
30
30
|
model: typeof o.model === "string" ? o.model : null,
|
|
31
31
|
providerFingerprint: typeof o.providerFingerprint === "string" ? o.providerFingerprint : null,
|
|
32
|
+
...(typeof o.sessionContextFingerprint === "string"
|
|
33
|
+
? { sessionContextFingerprint: o.sessionContextFingerprint }
|
|
34
|
+
: {}),
|
|
32
35
|
...(typeof o.contextTokens === "number" && o.contextTokens >= 0 ? { contextTokens: o.contextTokens } : {}),
|
|
33
36
|
...(typeof o.lastExitOk === "boolean" ? { lastExitOk: o.lastExitOk } : {}),
|
|
34
37
|
};
|
|
@@ -13,6 +13,10 @@ export function supervisorLaunch(request) {
|
|
|
13
13
|
...(request.reasoning === undefined ? {} : { reasoning: request.reasoning }),
|
|
14
14
|
};
|
|
15
15
|
if (request.runtime === "claude") {
|
|
16
|
+
const additionalDirectories = request.claudeAdditionalDirectories
|
|
17
|
+
?? (request.agentRoot === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: [join(request.agentRoot, ".crew", "claude-skills"), request.agentRoot]);
|
|
16
20
|
return {
|
|
17
21
|
command: request.bin,
|
|
18
22
|
args: buildClaudeArgs({
|
|
@@ -21,10 +25,7 @@ export function supervisorLaunch(request) {
|
|
|
21
25
|
cwd: request.cwd,
|
|
22
26
|
env: request.env,
|
|
23
27
|
systemPromptPath: request.systemPromptPath,
|
|
24
|
-
...(
|
|
25
|
-
projectSkillsDirectory: join(request.agentRoot, ".crew", "claude-skills"),
|
|
26
|
-
agentRootDirectory: request.agentRoot,
|
|
27
|
-
}),
|
|
28
|
+
...(additionalDirectories === undefined ? {} : { additionalDirectories }),
|
|
28
29
|
...(request.sessionId === undefined ? {} : {
|
|
29
30
|
sessionId: request.sessionId,
|
|
30
31
|
resume: request.resume,
|
|
@@ -54,6 +55,13 @@ export function supervisorLaunch(request) {
|
|
|
54
55
|
...(request.agentRoot === undefined ? {} : {
|
|
55
56
|
projectRootMarkers: [".git", ".nowwork-root"],
|
|
56
57
|
}),
|
|
58
|
+
...(request.workspaceRoots === undefined ? {} : {
|
|
59
|
+
cwd: request.cwd,
|
|
60
|
+
workspaceRoots: request.workspaceRoots,
|
|
61
|
+
}),
|
|
62
|
+
...(request.codexSkillRoots === undefined ? {} : {
|
|
63
|
+
skillRoots: request.codexSkillRoots,
|
|
64
|
+
}),
|
|
57
65
|
resume: request.resume,
|
|
58
66
|
}),
|
|
59
67
|
};
|
package/dist/workspace.js
CHANGED
|
@@ -88,14 +88,23 @@ export async function prepareWorkspace(input) {
|
|
|
88
88
|
}
|
|
89
89
|
const workLog = (await exists(workLogPath)) ? await readFile(workLogPath, "utf8") : "";
|
|
90
90
|
// resumeKey 可跨不同 task cwd 维持同一底层会话;协议键用 opaque 映射,legacy 保持 safeKey。
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// 未绑定项目时保留原有分支,避免改变旧 sessionDir 与 import 语义。
|
|
92
|
+
let sessionDir;
|
|
93
|
+
if (input.sessionContextFingerprint === undefined) {
|
|
94
|
+
sessionDir = input.resumeKey
|
|
95
|
+
? join(crewDir, "sessions", workspaceKey(input.resumeKey, input.keyMode))
|
|
96
|
+
: runDir;
|
|
97
|
+
if (input.resumeKey)
|
|
98
|
+
await mkdir(sessionDir, { recursive: true });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const effectiveSessionKey = `${input.resumeKey ?? input.taskKey}\0${input.sessionContextFingerprint}`;
|
|
102
|
+
sessionDir = join(crewDir, "sessions", workspaceKey(effectiveSessionKey, input.keyMode));
|
|
95
103
|
await mkdir(sessionDir, { recursive: true });
|
|
104
|
+
}
|
|
96
105
|
let agentSessionId = null;
|
|
97
106
|
let sessionResume = false;
|
|
98
|
-
if (input.taskKey || input.resumeKey) {
|
|
107
|
+
if (input.taskKey || input.resumeKey || input.sessionContextFingerprint !== undefined) {
|
|
99
108
|
const sessionPath = join(sessionDir, ".session");
|
|
100
109
|
if (await exists(sessionPath)) {
|
|
101
110
|
agentSessionId = (await readFile(sessionPath, "utf8")).trim() || null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,9 +16,16 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
19
26
|
"dependencies": {
|
|
20
27
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
21
|
-
"@nowcrew/cli": "^0.4.
|
|
28
|
+
"@nowcrew/cli": "^0.4.14",
|
|
22
29
|
"cross-spawn": "^7.0.6",
|
|
23
30
|
"ws": "^8",
|
|
24
31
|
"yaml": "^2.8.1",
|
|
@@ -34,11 +41,5 @@
|
|
|
34
41
|
"tsx": "^4.19.0",
|
|
35
42
|
"typescript": "^5.6.0",
|
|
36
43
|
"vitest": "^2.1.0"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
40
|
-
"build": "tsc -p tsconfig.json",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"typecheck": "tsc --noEmit"
|
|
43
44
|
}
|
|
44
|
-
}
|
|
45
|
+
}
|