@nowcrew/daemon 0.5.21 → 0.5.23
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/console-collapse.js +13 -0
- package/dist/console-formatter.js +73 -0
- package/dist/console-payload.js +73 -0
- package/dist/console.js +19 -5
- package/dist/execution-event-limit.js +5 -0
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +1 -0
- package/dist/json-result.js +27 -0
- package/dist/local-executor.js +4 -2
- package/dist/skill-preview.js +21 -0
- package/dist/unified-diff.js +84 -0
- package/package.json +2 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const MESSAGE_READ = /\bcrew\s+(?:message|thread)\s+read\b/i;
|
|
2
|
+
const TASK_LIST = /\bcrew\s+task\s+list\b/i;
|
|
3
|
+
export function buildCollapsedResult(command, output) {
|
|
4
|
+
if (MESSAGE_READ.test(command)) {
|
|
5
|
+
const count = output.split("\n").filter((line) => /^#\d+\s+\(msg=/.test(line.trim())).length;
|
|
6
|
+
return { kind: "collapsed_result", label: "CHANNEL HISTORY", count };
|
|
7
|
+
}
|
|
8
|
+
if (TASK_LIST.test(command)) {
|
|
9
|
+
const count = output.split("\n").filter((line) => /^#\d+\s+\[/.test(line.trim())).length;
|
|
10
|
+
return { kind: "collapsed_result", label: "TASK LIST", count };
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 单次 runtime execution 的 console 格式化器。
|
|
3
|
+
* 用 tool_use id 关联 Read 结果,避免将整文件作为普通 tool_result 倾泻到终端。
|
|
4
|
+
*/
|
|
5
|
+
import { buildFilePreview } from "./console-payload.js";
|
|
6
|
+
import { toConsoleLines, TOOL_RESULT_CAP } from "./console.js";
|
|
7
|
+
import { parseUnifiedDiff } from "./unified-diff.js";
|
|
8
|
+
import { buildJsonResult } from "./json-result.js";
|
|
9
|
+
import { buildCollapsedResult } from "./console-collapse.js";
|
|
10
|
+
import { buildSkillPreview } from "./skill-preview.js";
|
|
11
|
+
const extract = (content) => {
|
|
12
|
+
if (typeof content === "string")
|
|
13
|
+
return content;
|
|
14
|
+
if (!Array.isArray(content))
|
|
15
|
+
return "";
|
|
16
|
+
return content.map((item) => item && typeof item === "object" && typeof item.text === "string"
|
|
17
|
+
? item.text : "").filter(Boolean).join("\n");
|
|
18
|
+
};
|
|
19
|
+
const clip = (text) => text.length > TOOL_RESULT_CAP
|
|
20
|
+
? `${text.slice(0, TOOL_RESULT_CAP)}… (+${text.length - TOOL_RESULT_CAP})`
|
|
21
|
+
: text;
|
|
22
|
+
export function createConsoleFormatter() {
|
|
23
|
+
const pending = new Map();
|
|
24
|
+
return {
|
|
25
|
+
format(event) {
|
|
26
|
+
const rec = event && typeof event === "object" ? event : {};
|
|
27
|
+
const message = rec.message && typeof rec.message === "object" ? rec.message : undefined;
|
|
28
|
+
if (rec.type === "assistant" && Array.isArray(message?.content)) {
|
|
29
|
+
for (const block of message.content) {
|
|
30
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
31
|
+
pending.set(block.id, { name: block.name, ...(block.input ? { input: block.input } : {}) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (rec.type === "user" && Array.isArray(message?.content)) {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const block of message.content) {
|
|
38
|
+
if (block.type !== "tool_result")
|
|
39
|
+
continue;
|
|
40
|
+
const text = extract(block.content);
|
|
41
|
+
if (!text)
|
|
42
|
+
continue;
|
|
43
|
+
const tool = block.tool_use_id ? pending.get(block.tool_use_id) : undefined;
|
|
44
|
+
if (block.tool_use_id)
|
|
45
|
+
pending.delete(block.tool_use_id);
|
|
46
|
+
if (!block.is_error && tool?.name === "Read" && typeof tool.input?.file_path === "string") {
|
|
47
|
+
const start = typeof tool.input.offset === "number" && Number.isSafeInteger(tool.input.offset)
|
|
48
|
+
? Math.max(1, tool.input.offset) : 1;
|
|
49
|
+
const preview = buildFilePreview("read", tool.input.file_path, text, start);
|
|
50
|
+
out.push({ stream: "tool_result", text: `Read ${preview.totalLines} line(s) from ${preview.path}`, payload: preview });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const patch = block.is_error ? null : parseUnifiedDiff(text);
|
|
54
|
+
const command = tool?.name === "Bash" && typeof tool.input?.command === "string" ? tool.input.command : "";
|
|
55
|
+
const skill = block.is_error || tool?.name !== "Skill" ? null : buildSkillPreview(text);
|
|
56
|
+
const collapsed = block.is_error || patch || skill || !command ? null : buildCollapsedResult(command, text);
|
|
57
|
+
const json = block.is_error || patch || skill || collapsed || !command ? null : buildJsonResult(command, text);
|
|
58
|
+
out.push(patch
|
|
59
|
+
? { stream: "tool_result", text: `Changed ${patch.files.length} file(s): +${patch.additions} -${patch.deletions}`, payload: patch }
|
|
60
|
+
: skill
|
|
61
|
+
? { stream: "tool_result", text: "READ SKILL", payload: skill }
|
|
62
|
+
: collapsed
|
|
63
|
+
? { stream: "tool_result", text: collapsed.label, payload: collapsed }
|
|
64
|
+
: json
|
|
65
|
+
? { stream: "tool_result", text: json.label, payload: json }
|
|
66
|
+
: { stream: "tool_result", text: clip(text) });
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
return toConsoleLines(event);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentConsole 的结构化展示协议与有界构造器。
|
|
3
|
+
* 所有代码/文件预览在 daemon 边界裁剪,避免 server/web 接收整文件。
|
|
4
|
+
*/
|
|
5
|
+
export const CONSOLE_PREVIEW_ROWS = 12;
|
|
6
|
+
export const CONSOLE_PREVIEW_HEAD_ROWS = 8;
|
|
7
|
+
export const CONSOLE_DIFF_ROWS = 40;
|
|
8
|
+
export const CONSOLE_ROW_CHARS = 240;
|
|
9
|
+
const clipRow = (text) => text.length > CONSOLE_ROW_CHARS
|
|
10
|
+
? `${text.slice(0, CONSOLE_ROW_CHARS)}…`
|
|
11
|
+
: text;
|
|
12
|
+
/** 头 8 + 尾 4,中间显式 omitted;小文件全部显示。 */
|
|
13
|
+
export function buildFilePreview(operation, path, content, startLine = 1) {
|
|
14
|
+
const lines = content ? content.split("\n") : [];
|
|
15
|
+
const visible = lines.length <= CONSOLE_PREVIEW_ROWS
|
|
16
|
+
? lines.map((text, index) => ({ type: "line", line: startLine + index, text: clipRow(text) }))
|
|
17
|
+
: [
|
|
18
|
+
...lines.slice(0, CONSOLE_PREVIEW_HEAD_ROWS).map((text, index) => ({
|
|
19
|
+
type: "line", line: startLine + index, text: clipRow(text),
|
|
20
|
+
})),
|
|
21
|
+
{ type: "omitted", count: lines.length - CONSOLE_PREVIEW_ROWS },
|
|
22
|
+
...lines.slice(-4).map((text, index) => ({
|
|
23
|
+
type: "line", line: startLine + lines.length - 4 + index, text: clipRow(text),
|
|
24
|
+
})),
|
|
25
|
+
];
|
|
26
|
+
return {
|
|
27
|
+
kind: "file_preview",
|
|
28
|
+
operation,
|
|
29
|
+
path,
|
|
30
|
+
totalLines: lines.length,
|
|
31
|
+
totalBytes: Buffer.byteLength(content, "utf8"),
|
|
32
|
+
rows: visible,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** old/new 片段转紧凑带双行号 diff;变更中段超过预算时裁剪。 */
|
|
36
|
+
export function buildSnippetDiff(path, oldText, newText) {
|
|
37
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
38
|
+
const newLines = newText ? newText.split("\n") : [];
|
|
39
|
+
let prefix = 0;
|
|
40
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix])
|
|
41
|
+
prefix++;
|
|
42
|
+
let oldEnd = oldLines.length;
|
|
43
|
+
let newEnd = newLines.length;
|
|
44
|
+
while (oldEnd > prefix && newEnd > prefix && oldLines[oldEnd - 1] === newLines[newEnd - 1]) {
|
|
45
|
+
oldEnd--;
|
|
46
|
+
newEnd--;
|
|
47
|
+
}
|
|
48
|
+
const rows = [];
|
|
49
|
+
const contextStart = Math.max(0, prefix - 2);
|
|
50
|
+
for (let index = contextStart; index < prefix; index++) {
|
|
51
|
+
rows.push({ type: "context", oldLine: index + 1, newLine: index + 1, text: clipRow(oldLines[index]) });
|
|
52
|
+
}
|
|
53
|
+
for (let index = prefix; index < oldEnd; index++) {
|
|
54
|
+
rows.push({ type: "delete", oldLine: index + 1, newLine: null, text: clipRow(oldLines[index]) });
|
|
55
|
+
}
|
|
56
|
+
for (let index = prefix; index < newEnd; index++) {
|
|
57
|
+
rows.push({ type: "add", oldLine: null, newLine: index + 1, text: clipRow(newLines[index]) });
|
|
58
|
+
}
|
|
59
|
+
for (let offset = 0; offset < Math.min(2, oldLines.length - oldEnd); offset++) {
|
|
60
|
+
rows.push({ type: "context", oldLine: oldEnd + offset + 1, newLine: newEnd + offset + 1, text: clipRow(oldLines[oldEnd + offset]) });
|
|
61
|
+
}
|
|
62
|
+
const additions = Math.max(0, newEnd - prefix);
|
|
63
|
+
const deletions = Math.max(0, oldEnd - prefix);
|
|
64
|
+
if (rows.length <= CONSOLE_DIFF_ROWS)
|
|
65
|
+
return { kind: "diff_rows", files: [{ path, rows }], additions, deletions };
|
|
66
|
+
const head = rows.slice(0, 28);
|
|
67
|
+
const tail = rows.slice(-11);
|
|
68
|
+
return {
|
|
69
|
+
kind: "diff_rows",
|
|
70
|
+
files: [{ path, rows: [...head, { type: "omitted", oldCount: rows.length - 39, newCount: rows.length - 39 }, ...tail] }],
|
|
71
|
+
additions, deletions, truncated: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/console.js
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* kimi(OpenAI 消息风格行)。
|
|
14
14
|
*/
|
|
15
15
|
import { detectDaemonLang, translateDaemon } from "./i18n.js";
|
|
16
|
+
import { buildFilePreview, buildSnippetDiff } from "./console-payload.js";
|
|
17
|
+
import { parseUnifiedDiff } from "./unified-diff.js";
|
|
18
|
+
import { buildJsonResult } from "./json-result.js";
|
|
19
|
+
import { buildCollapsedResult } from "./console-collapse.js";
|
|
16
20
|
/** 单条工具返回正文上限(超出截断并标注),避免单条把终端/DB 撑爆。 */
|
|
17
21
|
export const TOOL_RESULT_CAP = 4000;
|
|
18
22
|
/** 工具输入摘要上限(标题行那一段)。 */
|
|
@@ -95,10 +99,10 @@ function toolPayload(name, input) {
|
|
|
95
99
|
return { kind: "command", command: clip(input.command, PAYLOAD_TEXT_CAP) };
|
|
96
100
|
}
|
|
97
101
|
if (name === "Edit" && typeof input.file_path === "string") {
|
|
98
|
-
return
|
|
102
|
+
return buildSnippetDiff(input.file_path, typeof input.old_string === "string" ? input.old_string : "", typeof input.new_string === "string" ? input.new_string : "");
|
|
99
103
|
}
|
|
100
104
|
if (name === "Write" && typeof input.file_path === "string" && typeof input.content === "string") {
|
|
101
|
-
return
|
|
105
|
+
return buildFilePreview("write", input.file_path, input.content);
|
|
102
106
|
}
|
|
103
107
|
if (name === "TodoWrite") {
|
|
104
108
|
const todos = normalizeTodos(input.todos);
|
|
@@ -125,7 +129,7 @@ function toolUseChunks(name, input) {
|
|
|
125
129
|
chunks.push({
|
|
126
130
|
stream: "tool",
|
|
127
131
|
text: `⏺ Edit(${clip(file, TOOL_INPUT_CAP)})`,
|
|
128
|
-
payload:
|
|
132
|
+
payload: buildSnippetDiff(file, typeof rec.old_string === "string" ? rec.old_string : "", typeof rec.new_string === "string" ? rec.new_string : ""),
|
|
129
133
|
});
|
|
130
134
|
}
|
|
131
135
|
if (chunks.length)
|
|
@@ -170,8 +174,18 @@ function codexItemChunks(item, td) {
|
|
|
170
174
|
},
|
|
171
175
|
}];
|
|
172
176
|
const output = item.aggregated_output?.trim();
|
|
173
|
-
if (output)
|
|
174
|
-
|
|
177
|
+
if (output) {
|
|
178
|
+
const patch = parseUnifiedDiff(output);
|
|
179
|
+
const collapsed = patch ? null : buildCollapsedResult(command, output);
|
|
180
|
+
const json = patch || collapsed ? null : buildJsonResult(command, output);
|
|
181
|
+
out.push(patch
|
|
182
|
+
? { stream: "tool_result", text: `Changed ${patch.files.length} file(s): +${patch.additions} -${patch.deletions}`, payload: patch }
|
|
183
|
+
: collapsed
|
|
184
|
+
? { stream: "tool_result", text: collapsed.label, payload: collapsed }
|
|
185
|
+
: json
|
|
186
|
+
? { stream: "tool_result", text: json.label, payload: json }
|
|
187
|
+
: { stream: "tool_result", text: clip(output, TOOL_RESULT_CAP) });
|
|
188
|
+
}
|
|
175
189
|
return out;
|
|
176
190
|
}
|
|
177
191
|
if (item.type === "file_change" && Array.isArray(item.changes)) {
|
|
@@ -38,6 +38,11 @@ export function boundExecutionFrame(input, maxBytes) {
|
|
|
38
38
|
}
|
|
39
39
|
else if (input.type === "execution:console" || input.type === "execution:output") {
|
|
40
40
|
frame = withBoundedString(frame, "text", maxBytes, false);
|
|
41
|
+
// 富 payload 是可选增强;事件超限时先丢 payload,保留必需 text 帧。
|
|
42
|
+
if (executionFrameBytes(frame) > maxBytes && "payload" in frame) {
|
|
43
|
+
const { payload: _payload, ...withoutPayload } = frame;
|
|
44
|
+
frame = withoutPayload;
|
|
45
|
+
}
|
|
41
46
|
}
|
|
42
47
|
else if (input.type === "execution:rejected") {
|
|
43
48
|
frame = withBoundedString(frame, "message", maxBytes, true);
|
|
@@ -191,6 +191,7 @@ export const ExecutionConsoleSchema = z.object({
|
|
|
191
191
|
executionId: ExecutionIdSchema,
|
|
192
192
|
stream: ConsoleStreamSchema,
|
|
193
193
|
text: z.string(),
|
|
194
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
194
195
|
seq: SequenceSchema,
|
|
195
196
|
at: TimestampSchema,
|
|
196
197
|
}).strict();
|
package/dist/execution-runner.js
CHANGED
|
@@ -422,6 +422,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
422
422
|
executionId: spec.executionId,
|
|
423
423
|
stream: chunk.stream,
|
|
424
424
|
text: chunk.text,
|
|
425
|
+
...(chunk.payload ? { payload: chunk.payload } : {}),
|
|
425
426
|
seq: consoleSequence++,
|
|
426
427
|
at: now().toISOString(),
|
|
427
428
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** 命令 JSON 输出识别与有界 pretty-print;供 AgentConsole 语法高亮,不引入重量级 highlighter。 */
|
|
2
|
+
const MAX_JSON_CHARS = 12_000;
|
|
3
|
+
const TASK_COMMAND = /\bcrew\s+task\s+(create|update|claim|assign|close)\b/i;
|
|
4
|
+
const MESSAGE_COMMAND = /\bcrew\s+message\s+(send|read)\b/i;
|
|
5
|
+
export function buildJsonResult(command, output) {
|
|
6
|
+
const trimmed = output.trim();
|
|
7
|
+
if (!trimmed || (trimmed[0] !== "{" && trimmed[0] !== "["))
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(trimmed);
|
|
11
|
+
const pretty = JSON.stringify(value, null, 2);
|
|
12
|
+
const commandType = command.match(TASK_COMMAND)?.[1]?.toUpperCase().replace("CLOSE", "UPDATE")
|
|
13
|
+
?? command.match(MESSAGE_COMMAND)?.[1]?.toUpperCase()
|
|
14
|
+
?? "RESULT";
|
|
15
|
+
const entity = TASK_COMMAND.test(command) ? "TASK" : MESSAGE_COMMAND.test(command) ? "MESSAGE" : "JSON";
|
|
16
|
+
const label = `${entity} ${commandType}`;
|
|
17
|
+
return {
|
|
18
|
+
kind: "json_result",
|
|
19
|
+
label,
|
|
20
|
+
json: pretty.length > MAX_JSON_CHARS ? `${pretty.slice(0, MAX_JSON_CHARS)}\n…` : pretty,
|
|
21
|
+
truncated: pretty.length > MAX_JSON_CHARS,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -9,7 +9,7 @@ import { applyProviderEnv, providerFingerprint } from "./provider-env.js";
|
|
|
9
9
|
import { augmentedPath } from "./runtime-path.js";
|
|
10
10
|
import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
|
|
11
11
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
12
|
-
import {
|
|
12
|
+
import { createConsoleFormatter } from "./console-formatter.js";
|
|
13
13
|
import { capMemoryForInject, capWorkLogForInject } from "./prompt.js";
|
|
14
14
|
import { decodeExternalOutputEvent, extractExternalAnswer, ExternalAnswerDecoder, stripExternalAnswerMarkers, } from "./external-output.js";
|
|
15
15
|
import { cleanupMaterializedAttachments as cleanupAttachments, executionAttachmentDirectory, materializeAttachments, } from "./attachments.js";
|
|
@@ -300,6 +300,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
300
300
|
let finalText = null;
|
|
301
301
|
let sentViaCrew = false;
|
|
302
302
|
const externalOutput = new ExternalAnswerDecoder();
|
|
303
|
+
// 每轮独立:tool_use/result 关联状态不能跨 execution 泄漏。
|
|
304
|
+
const consoleFormatter = createConsoleFormatter();
|
|
303
305
|
const readline = createInterface({ input: child.stdout });
|
|
304
306
|
readline.on("line", (line) => {
|
|
305
307
|
const event = parseLine(line);
|
|
@@ -327,7 +329,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
327
329
|
for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
|
|
328
330
|
callbacks.onExternalOutput?.(text);
|
|
329
331
|
}
|
|
330
|
-
for (const chunk of
|
|
332
|
+
for (const chunk of consoleFormatter.format(event))
|
|
331
333
|
callbacks.onConsole?.(chunk);
|
|
332
334
|
});
|
|
333
335
|
let stderrTail = "";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const FRONTMATTER = /(?:^|\n)---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
2
|
+
export function buildSkillPreview(output) {
|
|
3
|
+
const frontmatter = output.match(FRONTMATTER)?.[1];
|
|
4
|
+
if (!frontmatter)
|
|
5
|
+
return null;
|
|
6
|
+
const name = scalar(frontmatter, "name");
|
|
7
|
+
const description = scalar(frontmatter, "description");
|
|
8
|
+
if (!name && !description)
|
|
9
|
+
return null;
|
|
10
|
+
return {
|
|
11
|
+
kind: "skill_preview",
|
|
12
|
+
name: name || "skill",
|
|
13
|
+
description,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function scalar(frontmatter, key) {
|
|
17
|
+
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
18
|
+
if (!match)
|
|
19
|
+
return "";
|
|
20
|
+
return match[1].trim().replace(/^['"]|['"]$/g, "").slice(0, 500);
|
|
21
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** 严格、有限的 unified diff 解析器。只有完整文件头 + hunk 才识别,避免误染普通 +/- 日志。 */
|
|
2
|
+
import { CONSOLE_DIFF_ROWS, CONSOLE_ROW_CHARS, } from "./console-payload.js";
|
|
3
|
+
const HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
4
|
+
const clip = (text) => text.length > CONSOLE_ROW_CHARS ? `${text.slice(0, CONSOLE_ROW_CHARS)}…` : text;
|
|
5
|
+
const pathFromHeader = (line) => line.slice(4).split("\t", 1)[0].replace(/^[ab]\//, "");
|
|
6
|
+
export function parseUnifiedDiff(input) {
|
|
7
|
+
const lines = input.split("\n");
|
|
8
|
+
const files = [];
|
|
9
|
+
let additions = 0;
|
|
10
|
+
let deletions = 0;
|
|
11
|
+
let cursor = 0;
|
|
12
|
+
let totalRows = 0;
|
|
13
|
+
let truncated = false;
|
|
14
|
+
while (cursor < lines.length) {
|
|
15
|
+
while (cursor < lines.length && !isFileHeaderPair(lines, cursor))
|
|
16
|
+
cursor++;
|
|
17
|
+
if (cursor >= lines.length)
|
|
18
|
+
break;
|
|
19
|
+
const oldPath = pathFromHeader(lines[cursor]);
|
|
20
|
+
const newPath = pathFromHeader(lines[cursor + 1]);
|
|
21
|
+
const path = newPath === "/dev/null" ? oldPath : newPath;
|
|
22
|
+
cursor += 2;
|
|
23
|
+
const rows = [];
|
|
24
|
+
let sawHunk = false;
|
|
25
|
+
while (cursor < lines.length && !isFileHeaderPair(lines, cursor)) {
|
|
26
|
+
const match = lines[cursor].match(HUNK);
|
|
27
|
+
if (!match) {
|
|
28
|
+
cursor++;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
sawHunk = true;
|
|
32
|
+
let oldLine = Number(match[1]);
|
|
33
|
+
let oldRemaining = Number(match[2] ?? 1);
|
|
34
|
+
let newLine = Number(match[3]);
|
|
35
|
+
let newRemaining = Number(match[4] ?? 1);
|
|
36
|
+
cursor++;
|
|
37
|
+
while (cursor < lines.length && (oldRemaining > 0 || newRemaining > 0)) {
|
|
38
|
+
const line = lines[cursor];
|
|
39
|
+
cursor++;
|
|
40
|
+
if (line === "\")
|
|
41
|
+
continue;
|
|
42
|
+
let row;
|
|
43
|
+
if (line.startsWith("+")) {
|
|
44
|
+
row = { type: "add", oldLine: null, newLine, text: clip(line.slice(1)) };
|
|
45
|
+
newLine++;
|
|
46
|
+
newRemaining--;
|
|
47
|
+
additions++;
|
|
48
|
+
}
|
|
49
|
+
else if (line.startsWith("-")) {
|
|
50
|
+
row = { type: "delete", oldLine, newLine: null, text: clip(line.slice(1)) };
|
|
51
|
+
oldLine++;
|
|
52
|
+
oldRemaining--;
|
|
53
|
+
deletions++;
|
|
54
|
+
}
|
|
55
|
+
else if (line.startsWith(" ")) {
|
|
56
|
+
row = { type: "context", oldLine, newLine, text: clip(line.slice(1)) };
|
|
57
|
+
oldLine++;
|
|
58
|
+
newLine++;
|
|
59
|
+
oldRemaining--;
|
|
60
|
+
newRemaining--;
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
break;
|
|
64
|
+
if (totalRows < CONSOLE_DIFF_ROWS) {
|
|
65
|
+
rows.push(row);
|
|
66
|
+
totalRows++;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
truncated = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (sawHunk) {
|
|
73
|
+
if (truncated && !rows.some((row) => row.type === "omitted"))
|
|
74
|
+
rows.push({ type: "omitted", oldCount: 1, newCount: 1 });
|
|
75
|
+
files.push({ path, rows });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!files.length)
|
|
79
|
+
return null;
|
|
80
|
+
return { kind: "diff_rows", files, additions, deletions, ...(truncated ? { truncated: true } : {}) };
|
|
81
|
+
}
|
|
82
|
+
function isFileHeaderPair(lines, index) {
|
|
83
|
+
return lines[index]?.startsWith("--- ") === true && lines[index + 1]?.startsWith("+++ ") === true;
|
|
84
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"cross-spawn": "^7.0.6",
|
|
22
22
|
"ws": "^8",
|
|
23
23
|
"zod": "^3.23.0",
|
|
24
|
-
"@nowcrew/cli": "^0.4.
|
|
24
|
+
"@nowcrew/cli": "^0.4.6"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
27
|
"koffi": "^2.9.0"
|