@nowcrew/daemon 0.6.88-beta.6 → 0.6.88-beta.8

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.
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { containsLikelySecret } from "../agent-memory/policy.js";
3
+ const CategorySchema = z.enum([
4
+ "preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
5
+ ]);
6
+ export const LocalMemorySingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
7
+ export const LocalMemoryChangeSchema = z.object({
8
+ operation: z.enum(["create", "update", "supersede"]),
9
+ memoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
10
+ baseRevision: z.number().int().nonnegative().nullable(),
11
+ supersedesMemoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
12
+ title: LocalMemorySingleLineText.max(120),
13
+ kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
14
+ summary: LocalMemorySingleLineText,
15
+ triggers: z.array(LocalMemorySingleLineText.max(80)).min(1).max(16),
16
+ priority: z.enum(["high", "medium", "low"]),
17
+ detailCategory: CategorySchema,
18
+ detailBody: z.string().trim().min(1).max(12_000),
19
+ reviewAfter: LocalMemorySingleLineText.nullable(),
20
+ }).strict();
21
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
22
+ const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
23
+ export function validateLocalMemoryChange(change) {
24
+ const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
25
+ if (containsLikelySecret(text))
26
+ throw new Error("local_memory_secret_candidate");
27
+ if (CONTROL_CHARACTER_PATTERN.test(text))
28
+ throw new Error("local_memory_control_character");
29
+ if (RESERVED_MEMORY_MARKER.test(text))
30
+ throw new Error("local_memory_reserved_marker");
31
+ if (change.operation === "create"
32
+ && (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
33
+ throw new Error("local_memory_create_revision_invalid");
34
+ }
35
+ if (change.operation === "update"
36
+ && (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
37
+ throw new Error("local_memory_update_revision_missing");
38
+ }
39
+ if (change.operation === "supersede"
40
+ && (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
41
+ throw new Error("local_memory_supersede_revision_missing");
42
+ }
43
+ }
@@ -0,0 +1,29 @@
1
+ export function normalizePendingQuery(value) {
2
+ return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
3
+ }
4
+ function containsTerm(text, term) {
5
+ if (!term)
6
+ return false;
7
+ const startBoundary = /^[a-z0-9]/u.test(term);
8
+ const endBoundary = /[a-z0-9]$/u.test(term);
9
+ let position = text.indexOf(term);
10
+ while (position >= 0) {
11
+ const before = text[position - 1] ?? "";
12
+ const after = text[position + term.length] ?? "";
13
+ if ((!startBoundary || !/[a-z0-9_-]/u.test(before)) && (!endBoundary || !/[a-z0-9_-]/u.test(after)))
14
+ return true;
15
+ position = text.indexOf(term, position + 1);
16
+ }
17
+ return false;
18
+ }
19
+ export function pendingRecallScore(item, query) {
20
+ const normalized = normalizePendingQuery(query);
21
+ if (!normalized)
22
+ return 0;
23
+ const candidate = item.candidate;
24
+ const terms = normalized.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter(Boolean);
25
+ const text = normalizePendingQuery([candidate.title, candidate.summary, ...candidate.triggers, candidate.detailCategory, candidate.detailBody].join("\n"));
26
+ const explicit = [candidate.title, ...candidate.triggers].map(normalizePendingQuery);
27
+ const explicitScore = explicit.reduce((score, term) => score + (containsTerm(normalized, term) ? 10 : 0), 0);
28
+ return explicitScore + terms.reduce((score, term) => score + (containsTerm(text, term) ? 1 : 0), 0);
29
+ }
@@ -0,0 +1,100 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { isRecallablePending, PendingIdSchema } from "./pending-schema.js";
4
+ import { normalizePendingQuery, pendingRecallScore } from "./pending-match.js";
5
+ import { pendingDirectorySnapshot, pendingSourceError, PENDING_FILE_BYTES, readPendingSource } from "./pending-source.js";
6
+ const PAGE_FILES = 128;
7
+ const PAGE_BYTES = 1_000_000;
8
+ const OUTPUT_BYTES = 12 * 1024;
9
+ const CursorSchema = z.object({
10
+ v: z.literal(1), queryHash: z.string().regex(/^[a-f0-9]{64}$/u), version: z.string().regex(/^[a-f0-9]{64}$/u),
11
+ after: z.string().min(1).max(255), hadWarnings: z.boolean(),
12
+ }).strict();
13
+ const SearchSchema = z.object({
14
+ agentDir: z.string().min(1), query: z.string().trim().min(1).max(256),
15
+ cursor: z.string().max(2048).optional(), limit: z.number().int().min(1).max(40).default(10),
16
+ }).strict();
17
+ const excerpt = (text, cap) => Array.from(text).slice(0, cap).join("");
18
+ export async function searchPendingMemoryPage(input) {
19
+ const parsed = SearchSchema.safeParse(input);
20
+ if (!parsed.success)
21
+ throw new Error("memory_pending_arguments_invalid");
22
+ const args = parsed.data;
23
+ const queryHash = createHash("sha256").update(normalizePendingQuery(args.query)).digest("hex");
24
+ let cursor;
25
+ if (args.cursor !== undefined) {
26
+ try {
27
+ cursor = CursorSchema.parse(JSON.parse(Buffer.from(args.cursor, "base64url").toString("utf8")));
28
+ }
29
+ catch {
30
+ throw new Error("memory_pending_cursor_invalid");
31
+ }
32
+ if (cursor.queryHash !== queryHash)
33
+ throw new Error("memory_pending_cursor_invalid");
34
+ }
35
+ const snapshot = await pendingDirectorySnapshot(args.agentDir);
36
+ if (cursor && cursor.version !== snapshot.version)
37
+ throw new Error("memory_pending_source_changed");
38
+ const initial = cursor ? snapshot.names.indexOf(cursor.after) + 1 : 0;
39
+ if (cursor && initial === 0)
40
+ throw new Error("memory_pending_cursor_invalid");
41
+ const matches = [];
42
+ const warnings = [];
43
+ let position = initial;
44
+ let scannedFiles = 0;
45
+ let scannedBytes = 0;
46
+ // 为游标、错误与元数据预留空间;达到输出上限前停止消费来源,不跳过未输出的命中。
47
+ let outputBytes = 1600;
48
+ while (position < snapshot.names.length && scannedFiles < PAGE_FILES && scannedBytes + PENDING_FILE_BYTES <= PAGE_BYTES) {
49
+ const name = snapshot.names[position];
50
+ let source;
51
+ let warning;
52
+ try {
53
+ source = await readPendingSource(args.agentDir, name);
54
+ }
55
+ catch (error) {
56
+ const code = pendingSourceError(error);
57
+ if (code === "memory_pending_source_changed" || code === "memory_pending_not_found")
58
+ throw new Error("memory_pending_source_changed");
59
+ warning = { file: name, code };
60
+ }
61
+ const match = source && isRecallablePending(source.candidate) && pendingRecallScore(source.candidate, args.query) > 0
62
+ ? { pendingId: source.candidate.pendingId, sha256: source.sha256, title: source.candidate.candidate.title,
63
+ summary: excerpt(source.candidate.candidate.summary, 180), triggers: source.candidate.candidate.triggers.slice(0, 4),
64
+ operation: "create", state: "capacity_deferred" }
65
+ : null;
66
+ const addition = match ?? warning;
67
+ const size = addition ? Buffer.byteLength(JSON.stringify(addition), "utf8") + 2 : 0;
68
+ if (outputBytes + size > OUTPUT_BYTES)
69
+ break;
70
+ if (match)
71
+ matches.push(match);
72
+ if (warning)
73
+ warnings.push(warning);
74
+ outputBytes += size;
75
+ scannedFiles += 1;
76
+ scannedBytes += source?.bytes ?? PENDING_FILE_BYTES;
77
+ position += 1;
78
+ if (matches.length >= args.limit)
79
+ break;
80
+ }
81
+ if ((await pendingDirectorySnapshot(args.agentDir)).version !== snapshot.version)
82
+ throw new Error("memory_pending_source_changed");
83
+ const hasMore = position < snapshot.names.length;
84
+ const hadWarnings = cursor?.hadWarnings === true || warnings.length > 0;
85
+ const nextCursor = hasMore ? Buffer.from(JSON.stringify({ v: 1, queryHash, version: snapshot.version,
86
+ after: snapshot.names[position - 1], hadWarnings })).toString("base64url") : null;
87
+ return { matches, warnings, scannedFiles, scannedBytes, hasMore, nextCursor, complete: !hasMore && !hadWarnings };
88
+ }
89
+ export async function readPendingMemoryBody(input) {
90
+ if (!PendingIdSchema.safeParse(input.pendingId).success)
91
+ throw new Error("memory_pending_id_invalid");
92
+ const source = await readPendingSource(input.agentDir, `${input.pendingId}.json`);
93
+ if (input.ifHash !== undefined && input.ifHash !== source.sha256)
94
+ throw new Error("memory_pending_source_changed");
95
+ if (!isRecallablePending(source.candidate))
96
+ throw new Error("memory_pending_not_recallable");
97
+ const candidate = source.candidate.candidate;
98
+ return { pendingId: source.candidate.pendingId, sha256: source.sha256, title: candidate.title,
99
+ operation: "create", state: "capacity_deferred", content: candidate.detailBody };
100
+ }
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ import { LocalMemoryChangeSchema, validateLocalMemoryChange } from "./candidate.js";
3
+ // 索引会按分隔符展开触发词;16 个最多 80 UTF-16 单元的输入可产生 0..640 项。
4
+ const ArchivedMemoryChangeSchema = LocalMemoryChangeSchema.extend({
5
+ triggers: z.array(LocalMemoryChangeSchema.shape.triggers.element).max(640)
6
+ .refine((triggers) => triggers.reduce((length, trigger) => length + trigger.length, 0) <= 16 * 80),
7
+ });
8
+ export const PendingIdSchema = z.string().max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
9
+ export const PendingMemorySchema = z.object({
10
+ schemaVersion: z.literal(1), pendingId: PendingIdSchema,
11
+ createdAt: z.string().min(1).max(100), reason: z.literal("index_over_budget"),
12
+ source: z.object({ executionId: z.string().min(1).max(500), pruneTraceId: z.string().min(1).max(500).nullable() }).strict(),
13
+ candidate: ArchivedMemoryChangeSchema,
14
+ }).strict();
15
+ export function parsePendingMemoryCandidate(value) {
16
+ const parsed = PendingMemorySchema.safeParse(value);
17
+ if (!parsed.success)
18
+ throw new Error("local_memory_pending_invalid");
19
+ validateLocalMemoryChange(parsed.data.candidate);
20
+ return parsed.data;
21
+ }
22
+ export function isRecallablePending(value) {
23
+ try {
24
+ return parsePendingMemoryCandidate(value).candidate.operation === "create";
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
@@ -0,0 +1,126 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, opendir, realpath } from "node:fs/promises";
3
+ import { createHash } from "node:crypto";
4
+ import { join } from "node:path";
5
+ import { TextDecoder } from "node:util";
6
+ import { parsePendingMemoryCandidate, PendingIdSchema } from "./pending-schema.js";
7
+ export const PENDING_FILE_BYTES = 128 * 1024;
8
+ const DIRECTORY_ENTRY_CAP = 10_000;
9
+ export function pendingSourceError(error) {
10
+ if (error instanceof Error && /^memory_pending_[a-z_]+$/u.test(error.message))
11
+ return error.message;
12
+ return "memory_pending_file_invalid";
13
+ }
14
+ export async function pendingReadDirectory(agentDir) {
15
+ const root = await realpath(agentDir);
16
+ let directory = root;
17
+ for (const part of [".crew", "memory", "pending"]) {
18
+ directory = join(directory, part);
19
+ let stat;
20
+ try {
21
+ stat = await lstat(directory);
22
+ }
23
+ catch (error) {
24
+ if (error.code === "ENOENT")
25
+ return { root, directory: null };
26
+ throw new Error("memory_pending_directory_unreadable");
27
+ }
28
+ if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory) {
29
+ throw new Error("memory_pending_path_invalid");
30
+ }
31
+ }
32
+ return { root, directory };
33
+ }
34
+ export async function pendingFileNames(directory) {
35
+ if (directory === null)
36
+ return [];
37
+ const names = [];
38
+ let inspected = 0;
39
+ try {
40
+ for await (const item of await opendir(directory)) {
41
+ if (++inspected > DIRECTORY_ENTRY_CAP)
42
+ throw new Error("memory_pending_directory_over_budget");
43
+ if (item.name.endsWith(".json"))
44
+ names.push(item.name);
45
+ }
46
+ }
47
+ catch (error) {
48
+ if (error instanceof Error && error.message.startsWith("memory_pending_"))
49
+ throw error;
50
+ throw new Error("memory_pending_directory_unreadable");
51
+ }
52
+ return names.sort();
53
+ }
54
+ function version(stat) {
55
+ return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
56
+ }
57
+ export async function pendingDirectorySnapshot(agentDir) {
58
+ const location = await pendingReadDirectory(agentDir);
59
+ const names = await pendingFileNames(location.directory);
60
+ const digest = createHash("sha256").update(location.root);
61
+ if (location.directory !== null)
62
+ digest.update(version(await lstat(location.directory)));
63
+ for (const name of names) {
64
+ try {
65
+ digest.update(JSON.stringify([name, version(await lstat(join(location.directory, name)))]));
66
+ }
67
+ catch {
68
+ throw new Error("memory_pending_source_changed");
69
+ }
70
+ }
71
+ return { ...location, names, version: digest.digest("hex") };
72
+ }
73
+ export async function readPendingSource(agentDir, name, byteBudget = PENDING_FILE_BYTES) {
74
+ if (!name.endsWith(".json") || !PendingIdSchema.safeParse(name.slice(0, -5)).success) {
75
+ throw new Error("memory_pending_id_invalid");
76
+ }
77
+ const { directory } = await pendingReadDirectory(agentDir);
78
+ if (directory === null)
79
+ throw new Error("memory_pending_not_found");
80
+ const path = join(directory, name);
81
+ const before = await lstat(path).catch(() => { throw new Error("memory_pending_not_found"); });
82
+ if (!before.isFile() || before.isSymbolicLink())
83
+ throw new Error("memory_pending_path_invalid");
84
+ const limit = Math.min(byteBudget, PENDING_FILE_BYTES);
85
+ if (before.size > limit)
86
+ throw new Error("memory_pending_file_over_budget");
87
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
88
+ try {
89
+ const opened = await handle.stat();
90
+ if (!opened.isFile() || version(opened) !== version(before))
91
+ throw new Error("memory_pending_source_changed");
92
+ const chunks = [];
93
+ let bytes = 0;
94
+ for (;;) {
95
+ const chunk = Buffer.alloc(Math.min(16 * 1024, limit + 1 - bytes));
96
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
97
+ if (bytesRead === 0)
98
+ break;
99
+ bytes += bytesRead;
100
+ if (bytes > limit)
101
+ throw new Error("memory_pending_file_over_budget");
102
+ chunks.push(chunk.subarray(0, bytesRead));
103
+ }
104
+ const after = await handle.stat();
105
+ const currentDirectory = await pendingReadDirectory(agentDir);
106
+ const current = await lstat(path);
107
+ if (currentDirectory.directory !== directory || version(after) !== version(opened) || version(current) !== version(opened)) {
108
+ throw new Error("memory_pending_source_changed");
109
+ }
110
+ const buffer = Buffer.concat(chunks);
111
+ let parsed;
112
+ try {
113
+ const raw = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
114
+ parsed = parsePendingMemoryCandidate(JSON.parse(raw));
115
+ }
116
+ catch {
117
+ throw new Error("memory_pending_file_invalid");
118
+ }
119
+ if (`${parsed.pendingId}.json` !== name)
120
+ throw new Error("memory_pending_id_invalid");
121
+ return { candidate: parsed, bytes, sha256: createHash("sha256").update(buffer).digest("hex") };
122
+ }
123
+ finally {
124
+ await handle.close();
125
+ }
126
+ }
@@ -1,34 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { lstat, mkdir, readdir, realpath, rename, rm, writeFile, readFile } from "node:fs/promises";
2
+ import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
- import { z } from "zod";
4
+ import { isRecallablePending, parsePendingMemoryCandidate } from "./pending-schema.js";
5
+ import { pendingFileNames, pendingReadDirectory, PENDING_FILE_BYTES, readPendingSource } from "./pending-source.js";
6
+ import { pendingRecallScore } from "./pending-match.js";
5
7
  export const LOCAL_MEMORY_PENDING_SCAN_FILE_CAP = 128;
6
8
  export const LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP = 1_000_000;
7
- const CandidateSchema = z.object({
8
- operation: z.enum(["create", "update", "supersede"]),
9
- memoryId: z.string().nullable(),
10
- baseRevision: z.number().int().nonnegative().nullable(),
11
- supersedesMemoryId: z.string().nullable(),
12
- title: z.string().min(1),
13
- kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
14
- summary: z.string().min(1),
15
- triggers: z.array(z.string()).min(1),
16
- priority: z.enum(["high", "medium", "low"]),
17
- detailCategory: z.enum(["preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons"]),
18
- detailBody: z.string().min(1),
19
- reviewAfter: z.string().nullable(),
20
- }).strict();
21
- const PendingSchema = z.object({
22
- schemaVersion: z.literal(1),
23
- pendingId: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
24
- createdAt: z.string().min(1),
25
- reason: z.literal("index_over_budget"),
26
- source: z.object({
27
- executionId: z.string().min(1),
28
- pruneTraceId: z.string().min(1).nullable(),
29
- }).strict(),
30
- candidate: CandidateSchema,
31
- }).strict();
32
9
  function within(root, candidate) {
33
10
  const path = relative(root, candidate);
34
11
  return path === "" || (path !== ".." && !path.startsWith(`..${sep}`));
@@ -122,13 +99,11 @@ async function atomicWrite(path, content) {
122
99
  }
123
100
  }
124
101
  export async function persistPendingMemoryCandidate(input) {
125
- const parsed = PendingSchema.safeParse(input.candidate);
126
- if (!parsed.success)
127
- throw new Error("local_memory_pending_invalid");
102
+ const parsed = parsePendingMemoryCandidate(input.candidate);
128
103
  const directory = await pendingDirectory(input.agentDir);
129
- const target = join(directory, `${parsed.data.pendingId}.json`);
104
+ const target = join(directory, `${parsed.pendingId}.json`);
130
105
  await assertRegularFile(target);
131
- await atomicWrite(target, `${JSON.stringify(parsed.data, null, 2)}\n`);
106
+ await atomicWrite(target, `${JSON.stringify(parsed, null, 2)}\n`);
132
107
  return target;
133
108
  }
134
109
  export async function removePendingMemoryCandidate(input) {
@@ -140,73 +115,44 @@ export async function removePendingMemoryCandidate(input) {
140
115
  await rm(target, { force: true });
141
116
  }
142
117
  export async function readPendingMemoryCandidates(input) {
143
- const directory = await existingPendingDirectory(input.agentDir);
144
- if (directory === null)
145
- return [];
146
- const names = (await readdir(directory)).filter((name) => name.endsWith(".json")).sort();
118
+ const { directory } = await pendingReadDirectory(input.agentDir).catch((error) => {
119
+ if (error instanceof Error && error.message === "memory_pending_path_invalid") {
120
+ throw new Error("local_memory_pending_path_invalid");
121
+ }
122
+ throw error;
123
+ });
124
+ const names = await pendingFileNames(directory);
147
125
  const maxFiles = Math.max(0, input.maxFiles ?? LOCAL_MEMORY_PENDING_SCAN_FILE_CAP);
148
126
  const maxBytes = Math.max(0, input.maxBytes ?? LOCAL_MEMORY_PENDING_SCAN_BYTE_CAP);
149
127
  const result = [];
150
128
  let scannedBytes = 0;
151
- for (const name of names) {
152
- if (result.length >= maxFiles || scannedBytes >= maxBytes)
129
+ for (const name of names.slice(0, maxFiles)) {
130
+ if (scannedBytes >= maxBytes)
153
131
  break;
154
- const path = join(directory, name);
155
132
  try {
156
- await assertRegularFile(path);
157
- const stat = await lstat(path);
158
- if (!stat.isFile() || stat.isSymbolicLink())
159
- throw new Error("local_memory_pending_path_invalid");
160
- if (scannedBytes + stat.size > maxBytes)
161
- break;
162
- const raw = await readFile(path, "utf8");
163
- const size = Buffer.byteLength(raw, "utf8");
164
- if (scannedBytes + size > maxBytes)
165
- break;
166
- scannedBytes += size;
167
- const parsed = PendingSchema.safeParse(JSON.parse(raw));
168
- if (parsed.success && `${parsed.data.pendingId}.json` === name)
169
- result.push(parsed.data);
133
+ const source = await readPendingSource(input.agentDir, name, maxBytes - scannedBytes);
134
+ scannedBytes += source.bytes;
135
+ result.push(source.candidate);
170
136
  }
171
137
  catch {
172
- // A damaged pending item is diagnosable and must not hide other candidates.
138
+ // 失败读取也消耗预算,显式分页命令负责返回诊断和继续扫描。
139
+ scannedBytes += Math.min(PENDING_FILE_BYTES, maxBytes - scannedBytes);
173
140
  }
174
141
  }
175
142
  return result;
176
143
  }
177
- function normalize(value) {
178
- return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
179
- }
180
144
  function priorityRank(priority) {
181
145
  return priority === "high" ? 0 : priority === "medium" ? 1 : 2;
182
146
  }
183
- function candidateText(candidate) {
184
- return [
185
- candidate.candidate.title,
186
- candidate.candidate.summary,
187
- ...candidate.candidate.triggers,
188
- candidate.candidate.detailCategory,
189
- candidate.candidate.detailBody,
190
- ].join("\n");
191
- }
192
- function recallScore(candidate, query) {
193
- const normalizedQuery = normalize(query);
194
- if (normalizedQuery === "")
195
- return 1;
196
- const text = normalize(candidateText(candidate));
197
- if (text.includes(normalizedQuery))
198
- return 100;
199
- const terms = normalizedQuery.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter((term) => term.length > 0);
200
- return terms.reduce((score, term) => score + (text.includes(term) ? 1 : 0), 0);
201
- }
202
147
  export async function searchPendingMemoryCandidates(input) {
203
148
  const candidates = await readPendingMemoryCandidates({ agentDir: input.agentDir });
204
149
  const limit = Math.max(0, input.limit ?? 5);
205
150
  if (limit === 0)
206
151
  return [];
207
152
  return candidates
208
- .map((candidate) => ({ candidate, score: recallScore(candidate, input.query) }))
209
- .filter((item) => normalize(input.query) === "" || item.score > 0)
153
+ .filter(isRecallablePending)
154
+ .map((candidate) => ({ candidate, score: pendingRecallScore(candidate, input.query) }))
155
+ .filter((item) => item.score > 0)
210
156
  .sort((left, right) => right.score - left.score
211
157
  || priorityRank(left.candidate.candidate.priority) - priorityRank(right.candidate.candidate.priority)
212
158
  || right.candidate.createdAt.localeCompare(left.candidate.createdAt)
@@ -218,25 +164,29 @@ function truncateCodePoints(value, maxCodePoints) {
218
164
  return Array.from(value).slice(0, Math.max(0, maxCodePoints)).join("");
219
165
  }
220
166
  export function renderPendingMemoryContext(candidates, maxCodePoints = 2_000) {
221
- if (candidates.length === 0 || maxCodePoints <= 0)
167
+ const recallable = candidates.filter(isRecallablePending);
168
+ if (recallable.length === 0 || maxCodePoints <= 0)
222
169
  return "";
223
- const header = `\n\n## Archived memory candidates (daemon-managed)\n这些记忆已持久化,但尚未进入常驻热索引;只在当前请求相关时参考,必要时再由 memory-prune 提升。\n`;
170
+ const header = `\n\n## Archived memory candidates (daemon-managed)\n容量暂存的新增资料,仅为部分预览,不代表完整检索;可参考但不能覆盖现行规则,未包含内容请按需查询。\n`;
224
171
  let remaining = maxCodePoints - Array.from(header).length;
225
172
  if (remaining <= 0)
226
173
  return truncateCodePoints(header, maxCodePoints);
227
174
  const sections = [];
228
- for (const item of candidates) {
175
+ for (const item of recallable) {
229
176
  const change = item.candidate;
230
177
  const prefix = `\n### ${change.title}\n`
231
- + `- pending_file: .crew/memory/pending/${item.pendingId}.json\n`
178
+ + `- pending_id: ${item.pendingId}\n`
232
179
  + `- summary: ${change.summary}\n`
233
180
  + `- triggers: ${change.triggers.join(", ")}\n`
234
181
  + "- detail: ";
235
182
  const prefixLength = Array.from(prefix).length;
236
183
  if (prefixLength + 1 > remaining)
237
184
  break;
238
- const detail = truncateCodePoints(change.detailBody, remaining - prefixLength - 1);
239
- const section = `${prefix}${detail}\n`;
185
+ const notice = " [正文未完,请用 read-pending 分页读取]";
186
+ const available = remaining - prefixLength - 1;
187
+ const truncated = Array.from(change.detailBody).length > available;
188
+ const detail = truncateCodePoints(change.detailBody, truncated ? Math.max(0, available - Array.from(notice).length) : available);
189
+ const section = `${prefix}${detail}${truncated ? truncateCodePoints(notice, available - Array.from(detail).length) : ""}\n`;
240
190
  const length = Array.from(section).length;
241
191
  sections.push(section);
242
192
  remaining -= length;
@@ -1,6 +1,7 @@
1
1
  import { dslog } from "../slog.js";
2
2
  import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
3
3
  import { renderLocalMemoryResultInstructions } from "./runtime.js";
4
+ const PENDING_READ_GUIDANCE = "\n\nV2新增资料预览未命中不代表没有资料;先查有效索引,必要时用 crew-memory search-pending --query '关键词',按 nextCursor 续查;用 read-pending --id ID 分页读正文。容量暂存资料不能覆盖现行规则,未应用修改不作为记忆。用 crew-memory --help 查看参数。\n";
4
5
  export async function prepareLocalMemoryPrompt(input) {
5
6
  let pending = "";
6
7
  if (input.enabled) {
@@ -21,17 +22,18 @@ export async function prepareLocalMemoryPrompt(input) {
21
22
  }
22
23
  const original = input.renderBase(0);
23
24
  const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath);
24
- const wakeBytes = Buffer.byteLength(structured, "utf8");
25
- const fits = (prompt) => input.maxBytes === undefined || Buffer.byteLength(prompt, "utf8") + wakeBytes <= input.maxBytes;
26
- const render = (suffix) => (suffix === "" ? original : input.renderBase(Buffer.byteLength(suffix, "utf8") + wakeBytes)) + suffix;
27
- // 系统及当前消息各保留一份契约,避免 resume 沿用旧指令;两份均先占预算,再裁可选预览。
28
- const complete = render(pending + structured);
29
- if (fits(complete))
30
- return { systemPrompt: complete, resultPath: input.resultPath, wakePromptSuffix: structured };
31
- if (pending !== "") {
32
- const withoutPending = render(structured);
33
- if (fits(withoutPending))
34
- return { systemPrompt: withoutPending, resultPath: input.resultPath, wakePromptSuffix: structured };
25
+ const guidance = input.enabled ? PENDING_READ_GUIDANCE : "";
26
+ // 短命令指引放在当前轮,独立于可舍弃预览;收尾契约仍保持系统与当前轮两份。
27
+ for (const choice of [
28
+ { suffix: pending + structured, wake: structured + guidance },
29
+ { suffix: structured, wake: structured + guidance },
30
+ { suffix: structured, wake: structured },
31
+ ]) {
32
+ const reserved = Buffer.byteLength(choice.suffix + choice.wake, "utf8");
33
+ const systemPrompt = (reserved === 0 ? original : input.renderBase(reserved)) + choice.suffix;
34
+ if (input.maxBytes === undefined || Buffer.byteLength(systemPrompt + choice.wake, "utf8") <= input.maxBytes) {
35
+ return { systemPrompt, resultPath: input.resultPath, wakePromptSuffix: choice.wake };
36
+ }
35
37
  }
36
38
  // 强制基础指令与契约本身仍无法共存时,保留既有非结构化兼容回退,不裁基础指令。
37
39
  return { systemPrompt: original, resultPath: null, wakePromptSuffix: "" };
@@ -5,6 +5,7 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { parseArgs, TextDecoder } from "node:util";
7
7
  import { z } from "zod";
8
+ import { readPendingMemoryBody, searchPendingMemoryPage } from "./pending-query.js";
8
9
  const OUTPUT_BYTES = 12 * 1024;
9
10
  const FILE_BYTES = 32 * 1024 * 1024;
10
11
  const FileSchema = z.string().min(1).refine((file) => {
@@ -33,14 +34,33 @@ const CommandSchema = z.discriminatedUnion("command", [
33
34
  offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
34
35
  limit: z.coerce.number().int().min(1).max(4000).default(2000),
35
36
  }).strict(),
37
+ z.object({
38
+ command: z.literal("search-pending"),
39
+ query: z.string().trim().min(1).max(256),
40
+ cursor: z.string().min(1).max(4096).optional(),
41
+ limit: z.coerce.number().int().min(1).max(40).default(10),
42
+ }).strict(),
43
+ z.object({
44
+ command: z.literal("read-pending"),
45
+ id: z.string().min(1).max(250).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
46
+ ifHash: Common.ifHash,
47
+ offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
48
+ limit: z.coerce.number().int().min(1).max(4000).default(2000),
49
+ }).strict(),
36
50
  ]);
37
51
  const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
38
52
  crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
39
53
  crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
40
- 搜索关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
54
+ crew-memory search-pending --query <关键词> [--cursor <游标>] [--limit 10]
55
+ crew-memory read-pending --id <候选ID> [--offset 0] [--limit 2000] [--if-hash <sha256>]
56
+ search 关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
41
57
  读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
42
58
  后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
43
- 搜索结果是原始资料定位,不代表候选已生效;按当前有效索引读取明细,不扫描 pending。`;
59
+ search/read 读取 MEMORY.md 和 notes 原文,不过滤失效段落,也不扫描 pending。
60
+ search-pending 只查因容量延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
61
+ complete=true 才表示全部候选已检索完成且无遗漏;warnings 或非零退出码表示读取未完成,不能当作零命中。
62
+ read-pending 只返回候选正文;offset 非零时必须传 --if-hash,update/supersede 候选不可读取。
63
+ 候选只供相关问题参考,未生效,不能覆盖有效规则;先读取当前有效索引与明细。`;
44
64
  function inside(root, path) {
45
65
  const value = relative(root, path);
46
66
  return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
@@ -152,8 +172,8 @@ function searchSource(text, sha256, command) {
152
172
  nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null,
153
173
  };
154
174
  }
155
- function readPage(text, sha256, command) {
156
- const metadata = { file: command.file, sha256, offset: command.offset, nextOffset: Number.MAX_SAFE_INTEGER, eof: false, content: "" };
175
+ function readPage(text, source, command) {
176
+ const metadata = { ...source, offset: command.offset, nextOffset: Number.MAX_SAFE_INTEGER, eof: false, content: "" };
157
177
  let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify(metadata), "utf8");
158
178
  let position = 0;
159
179
  let offset = 0;
@@ -175,8 +195,16 @@ function readPage(text, sha256, command) {
175
195
  count += 1;
176
196
  }
177
197
  const eof = position === text.length;
198
+ if (!eof && count === 0)
199
+ throw new Error("memory_output_too_large");
178
200
  return { ...metadata, content, eof, nextOffset: eof ? null : offset };
179
201
  }
202
+ function writeResult(result, io) {
203
+ const output = JSON.stringify(result);
204
+ if (Buffer.byteLength(output, "utf8") > OUTPUT_BYTES)
205
+ throw new Error("memory_output_too_large");
206
+ io.out(output);
207
+ }
180
208
  export async function runMemoryReadCommand(argv, io) {
181
209
  let command;
182
210
  try {
@@ -186,6 +214,7 @@ export async function runMemoryReadCommand(argv, io) {
186
214
  query: { type: "string", short: "q" }, file: { type: "string" },
187
215
  limit: { type: "string" }, offset: { type: "string" },
188
216
  "after-line": { type: "string" }, "if-hash": { type: "string" },
217
+ id: { type: "string" }, cursor: { type: "string" },
189
218
  json: { type: "boolean" }, help: { type: "boolean", short: "h" },
190
219
  },
191
220
  });
@@ -201,24 +230,44 @@ export async function runMemoryReadCommand(argv, io) {
201
230
  ...(afterLine === undefined ? {} : { afterLine }),
202
231
  ...(ifHash === undefined ? {} : { ifHash }),
203
232
  });
233
+ if (command.command === "read-pending" && command.offset > 0 && command.ifHash === undefined) {
234
+ throw new Error("memory_hash_required");
235
+ }
204
236
  }
205
237
  catch {
206
- io.err(`memory_arguments_invalid: 参数不合法。\n${USAGE}`);
238
+ io.err(`memory_arguments_invalid: 参数不合法,记忆读取未完成。\n${USAGE}`);
207
239
  return 2;
208
240
  }
209
241
  try {
242
+ if (!io.env.CREW_HOME)
243
+ throw new Error("memory_home_missing");
244
+ if (command.command === "search-pending") {
245
+ const result = await searchPendingMemoryPage({ agentDir: io.env.CREW_HOME, query: command.query,
246
+ ...(command.cursor === undefined ? {} : { cursor: command.cursor }), limit: command.limit });
247
+ writeResult(result, io);
248
+ if (result.warnings.length > 0 || (!result.hasMore && !result.complete)) {
249
+ throw new Error("memory_pending_search_incomplete");
250
+ }
251
+ return 0;
252
+ }
253
+ if (command.command === "read-pending") {
254
+ const { content, ...metadata } = await readPendingMemoryBody({ agentDir: io.env.CREW_HOME,
255
+ pendingId: command.id, ...(command.ifHash === undefined ? {} : { ifHash: command.ifHash }) });
256
+ writeResult(readPage(content, metadata, command), io);
257
+ return 0;
258
+ }
210
259
  const source = await readSource(io.env.CREW_HOME, command.file);
211
260
  if (command.ifHash !== undefined && command.ifHash !== source.sha256)
212
261
  throw new Error("memory_file_changed");
213
262
  const result = command.command === "search"
214
263
  ? searchSource(source.text, source.sha256, command)
215
- : readPage(source.text, source.sha256, command);
216
- io.out(JSON.stringify(result));
264
+ : readPage(source.text, { file: command.file, sha256: source.sha256 }, command);
265
+ writeResult(result, io);
217
266
  return 0;
218
267
  }
219
268
  catch (error) {
220
269
  const code = error.code;
221
- const known = error instanceof Error && /^memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
270
+ const known = error instanceof Error && /^(?:local_)?memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
222
271
  io.err(JSON.stringify({ error: known, message: "记忆读取未完成;检查路径、文件版本或大小,不要将此错误当作没有记忆。" }));
223
272
  return 1;
224
273
  }
@@ -69,8 +69,8 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
69
69
  \`\`\`
70
70
 
71
71
  每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
72
- 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中;后续相关任务会按需召回,索引腾出空间后可自动提升。
73
- 写入候选文件不等于已经记住,只有 daemon 校验并提交成功后记忆才生效。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在提交成功后清理,失败时保留日志和候选用于排查或重试。
72
+ 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。
73
+ 写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
74
74
  `;
75
75
  }
76
76
  /** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
@@ -2,29 +2,12 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import { z } from "zod";
5
- import { containsLikelySecret } from "../agent-memory/policy.js";
5
+ import { dslog } from "../slog.js";
6
+ import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
6
7
  import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
7
8
  import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
8
- const CategorySchema = z.enum([
9
- "preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
10
- ]);
11
- const SingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
12
- const ChangeSchema = z.object({
13
- operation: z.enum(["create", "update", "supersede"]),
14
- memoryId: SingleLineText.regex(/^[^<>]+$/u).nullable(),
15
- baseRevision: z.number().int().nonnegative().nullable(),
16
- supersedesMemoryId: SingleLineText.regex(/^[^<>]+$/u).nullable(),
17
- title: SingleLineText.max(120),
18
- kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
19
- summary: SingleLineText,
20
- triggers: z.array(SingleLineText.max(80)).min(1).max(16),
21
- priority: z.enum(["high", "medium", "low"]),
22
- detailCategory: CategorySchema,
23
- detailBody: z.string().trim().min(1).max(12_000),
24
- reviewAfter: SingleLineText.nullable(),
25
- }).strict();
26
9
  const StatusChangeSchema = z.object({
27
- memoryId: SingleLineText,
10
+ memoryId: LocalMemorySingleLineText,
28
11
  expectedRevision: z.number().int().positive(),
29
12
  expectedStatus: z.enum(["candidate", "active", "pinned", "stale", "superseded", "archived"]),
30
13
  targetStatus: z.enum(["candidate", "active", "pinned", "stale", "archived"]),
@@ -35,7 +18,7 @@ const ResultSchema = z.object({
35
18
  purpose: z.literal("memory_prune"),
36
19
  baseCatalogRevision: z.number().int().nonnegative(),
37
20
  outcome: z.enum(["no_change", "proposed", "conflict", "failed"]),
38
- changes: z.array(ChangeSchema).max(50),
21
+ changes: z.array(LocalMemoryChangeSchema).max(50),
39
22
  statusChanges: z.array(StatusChangeSchema).max(50),
40
23
  }).strict();
41
24
  const GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
@@ -43,8 +26,6 @@ const INDEX_HEADING = /^##\s+Knowledge Index\s*$/mu;
43
26
  const NEXT_SECTION = /^##\s+(?!#)/mu;
44
27
  const CARD_HEADING = /^###\s+(.+?)\s*$/gmu;
45
28
  const FIELD_LINE = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/iu;
46
- const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
47
- const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
48
29
  function codePointLength(value) {
49
30
  return Array.from(value).length;
50
31
  }
@@ -148,34 +129,12 @@ function replaceNote(content, id, replacement) {
148
129
  const pattern = notePattern(id);
149
130
  return pattern.test(content) ? content.replace(pattern, replacement) : null;
150
131
  }
151
- function containsReservedText(change) {
152
- return RESERVED_MEMORY_MARKER.test([change.title, change.summary, ...change.triggers, change.detailBody].join("\n"));
153
- }
154
132
  function validateCandidate(result) {
155
133
  if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
156
134
  throw new Error("local_memory_no_change_has_operations");
157
135
  }
158
- for (const change of result.changes) {
159
- const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
160
- if (containsLikelySecret(text))
161
- throw new Error("local_memory_secret_candidate");
162
- if (CONTROL_CHARACTER_PATTERN.test(text))
163
- throw new Error("local_memory_control_character");
164
- if (containsReservedText(change))
165
- throw new Error("local_memory_reserved_marker");
166
- if (change.operation === "create"
167
- && (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
168
- throw new Error("local_memory_create_revision_invalid");
169
- }
170
- if (change.operation === "update"
171
- && (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
172
- throw new Error("local_memory_update_revision_missing");
173
- }
174
- if (change.operation === "supersede"
175
- && (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
176
- throw new Error("local_memory_supersede_revision_missing");
177
- }
178
- }
136
+ for (const change of result.changes)
137
+ validateLocalMemoryChange(change);
179
138
  }
180
139
  export function parseLocalMemoryResult(raw) {
181
140
  let value;
@@ -477,7 +436,16 @@ export async function applyLocalMemoryResult(input) {
477
436
  // Pending create candidates are durable overflow, not dead ends. Promote only
478
437
  // when the current catalog has room; supersede/update candidates stay pending
479
438
  // until a fresh revision-aware decision can safely replay them.
480
- const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir });
439
+ const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
440
+ if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
441
+ throw error;
442
+ // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
443
+ dslog("local_memory.pending_promotion_skipped", "pending目录超出扫描预算,本轮跳过自动提升", {
444
+ level: "WARN", execution_id: input.executionId, prune_trace_id: input.pruneTraceId,
445
+ reason: "memory_pending_directory_over_budget",
446
+ });
447
+ return [];
448
+ });
481
449
  for (const pending of pendingCandidatesOnDisk) {
482
450
  const change = pending.candidate;
483
451
  if (change.operation !== "create")
package/dist/main.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.88-beta.6",
3
+ "version": "0.6.88-beta.8",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",
@@ -16,9 +16,17 @@
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
+ "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
23
+ "prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
24
+ "test": "vitest run",
25
+ "typecheck": "tsc --noEmit"
26
+ },
19
27
  "dependencies": {
20
28
  "@agentclientprotocol/sdk": "1.2.1",
21
- "@nowcrew/cli": "0.4.13",
29
+ "@nowcrew/cli": "0.4.14",
22
30
  "cross-spawn": "^7.0.6",
23
31
  "semver": "7.8.5",
24
32
  "ws": "^8",
@@ -36,12 +44,5 @@
36
44
  "tsx": "^4.19.0",
37
45
  "typescript": "^5.6.0",
38
46
  "vitest": "^2.1.0"
39
- },
40
- "scripts": {
41
- "daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
42
- "build": "tsc -p tsconfig.json",
43
- "codex-home:migrate": "tsx src/runtimes/codex-home-migration-cli.ts",
44
- "test": "vitest run",
45
- "typecheck": "tsc --noEmit"
46
47
  }
47
- }
48
+ }