@nowcrew/daemon 0.5.27 → 0.5.28
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/package.json +2 -2
- package/dist/attachments.js +0 -196
- package/dist/bound-im-decision.js +0 -22
- package/dist/completion-retransmitter.js +0 -77
- package/dist/computer-cli.js +0 -274
- package/dist/computer-profile-lock.js +0 -395
- package/dist/computer-profile.js +0 -364
- package/dist/computer-service.js +0 -358
- package/dist/config.js +0 -82
- package/dist/console-collapse.js +0 -13
- package/dist/console-formatter.js +0 -77
- package/dist/console-payload.js +0 -73
- package/dist/console.js +0 -329
- package/dist/daemon-startup-error.js +0 -30
- package/dist/execution-backend.js +0 -44
- package/dist/execution-event-limit.js +0 -64
- package/dist/execution-journal-lock.js +0 -421
- package/dist/execution-journal.js +0 -716
- package/dist/execution-protocol.js +0 -342
- package/dist/execution-recovery.js +0 -95
- package/dist/execution-runner.js +0 -659
- package/dist/execution-supervisor-child.js +0 -236
- package/dist/execution-supervisor.js +0 -316
- package/dist/execution-telemetry-journal.js +0 -71
- package/dist/external-output.js +0 -114
- package/dist/i18n.js +0 -64
- package/dist/json-result.js +0 -27
- package/dist/list-models.js +0 -92
- package/dist/local-executor.js +0 -439
- package/dist/log-format.js +0 -10
- package/dist/machine-info.js +0 -124
- package/dist/main.js +0 -118
- package/dist/normalize.js +0 -170
- package/dist/origin-decision.js +0 -44
- package/dist/platform.js +0 -8
- package/dist/prompt.js +0 -307
- package/dist/provider-env.js +0 -90
- package/dist/remote/claude-bridge.js +0 -402
- package/dist/remote/claude-channel.js +0 -164
- package/dist/remote/codex-client.js +0 -408
- package/dist/remote/codex-runtime.js +0 -77
- package/dist/remote/config.js +0 -83
- package/dist/remote/gateway.js +0 -572
- package/dist/remote/protocol.js +0 -178
- package/dist/remote/remote-cli.js +0 -233
- package/dist/remote/session-discovery.js +0 -249
- package/dist/remote/wrapper.js +0 -40
- package/dist/runner.js +0 -234
- package/dist/runtime-cancellation.js +0 -74
- package/dist/runtime-capabilities.js +0 -43
- package/dist/runtime-path.js +0 -60
- package/dist/runtimes/claude.js +0 -51
- package/dist/runtimes/codex-app-server-runner.js +0 -344
- package/dist/runtimes/codex-deepseek-catalog.js +0 -7
- package/dist/runtimes/codex-deepseek-config.js +0 -50
- package/dist/runtimes/codex.js +0 -53
- package/dist/runtimes/kimi-acp-runner.js +0 -364
- package/dist/runtimes/kimi.js +0 -45
- package/dist/runtimes/progress-watchdog.js +0 -26
- package/dist/scheduled-report.js +0 -51
- package/dist/scheduled-run-report.js +0 -57
- package/dist/serve-lifecycle.js +0 -82
- package/dist/serve.js +0 -868
- package/dist/session.js +0 -82
- package/dist/shared-execution-slots.js +0 -68
- package/dist/shutdown-deadline.js +0 -32
- package/dist/skill-preview.js +0 -21
- package/dist/skills.js +0 -56
- package/dist/slog.js +0 -228
- package/dist/supervised-runtime.js +0 -104
- package/dist/token.js +0 -24
- package/dist/unified-diff.js +0 -84
- package/dist/websocket-shutdown.js +0 -53
- package/dist/win32-job-object.js +0 -193
- package/dist/workspace-fs.js +0 -80
- package/dist/workspace-import.js +0 -127
- package/dist/workspace.js +0 -148
package/dist/unified-diff.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { WebSocket } from "ws";
|
|
2
|
-
export function closeWebSocketWithinDeadline(socket, signal) {
|
|
3
|
-
if (socket === null || socket.readyState === WebSocket.CLOSED)
|
|
4
|
-
return Promise.resolve();
|
|
5
|
-
return new Promise((resolve, reject) => {
|
|
6
|
-
let settled = false;
|
|
7
|
-
const cleanup = () => {
|
|
8
|
-
socket.removeListener("close", onClose);
|
|
9
|
-
socket.removeListener("error", onError);
|
|
10
|
-
signal.removeEventListener("abort", onAbort);
|
|
11
|
-
};
|
|
12
|
-
const settle = (outcome) => {
|
|
13
|
-
if (settled)
|
|
14
|
-
return;
|
|
15
|
-
settled = true;
|
|
16
|
-
cleanup();
|
|
17
|
-
outcome();
|
|
18
|
-
};
|
|
19
|
-
const onClose = () => settle(resolve);
|
|
20
|
-
const onError = () => {
|
|
21
|
-
if (socket.readyState === WebSocket.CLOSED)
|
|
22
|
-
onClose();
|
|
23
|
-
};
|
|
24
|
-
const onAbort = () => {
|
|
25
|
-
settle(() => {
|
|
26
|
-
if (socket.readyState !== WebSocket.CLOSED) {
|
|
27
|
-
try {
|
|
28
|
-
socket.terminate();
|
|
29
|
-
}
|
|
30
|
-
catch { /* deadline error remains authoritative */ }
|
|
31
|
-
}
|
|
32
|
-
reject(signal.reason);
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
socket.on("close", onClose);
|
|
36
|
-
socket.on("error", onError);
|
|
37
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
38
|
-
if (signal.aborted) {
|
|
39
|
-
onAbort();
|
|
40
|
-
}
|
|
41
|
-
else if (socket.readyState === WebSocket.CLOSED) {
|
|
42
|
-
onClose();
|
|
43
|
-
}
|
|
44
|
-
else if (socket.readyState !== WebSocket.CLOSING) {
|
|
45
|
-
try {
|
|
46
|
-
socket.close();
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
settle(() => reject(error));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
}
|
package/dist/win32-job-object.js
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Windows Job Object 执行后端的 FFI 封装(仅 win32 生效)。
|
|
3
|
-
*
|
|
4
|
-
* Node 不暴露 Job Object API,这里用 koffi 直接调 kernel32.dll。koffi 是可选依赖
|
|
5
|
-
* (optionalDependencies),用 createRequire 惰性同步加载 —— 加载失败/非 win32 一律抛错,
|
|
6
|
-
* 由上层降级到 fail-closed,不影响 Mac/Linux 路径。
|
|
7
|
-
*
|
|
8
|
-
* 关键语义:job 上设 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE 后,持有 job handle 的进程一旦退出
|
|
9
|
-
* (含崩溃),内核立即回收 Job 内所有进程 —— 等价于 POSIX「父死子亡 + 进程组整组清理」,且更强。
|
|
10
|
-
* 因此 job 必须由「离 runtime 最近、崩溃即代表该放弃所有权」的进程(supervisor child)创建并持有。
|
|
11
|
-
*/
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
|
-
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x0000_2000;
|
|
14
|
-
const JOBOBJECT_EXTENDED_LIMIT_INFORMATION = 9;
|
|
15
|
-
const JOBOBJECT_BASIC_ACCOUNTING_INFORMATION = 1;
|
|
16
|
-
const PROCESS_TERMINATE = 0x0001;
|
|
17
|
-
const PROCESS_SET_QUOTA = 0x0100;
|
|
18
|
-
const JOB_ASSIGN_ACCESS = PROCESS_TERMINATE | PROCESS_SET_QUOTA;
|
|
19
|
-
const JOB_EMPTY_POLL_MS = 10;
|
|
20
|
-
// koffi 的 struct 布局/对齐由类型描述自动推导,无需手工塞 padding。
|
|
21
|
-
// size_t/uintptr_t 在 x64/arm64 均为 8 字节,与 SIZE_T/ULONG_PTR 对齐。
|
|
22
|
-
const BASIC_LIMIT_INFORMATION = {
|
|
23
|
-
PerProcessUserTimeLimit: "int64",
|
|
24
|
-
PerJobUserTimeLimit: "int64",
|
|
25
|
-
LimitFlags: "uint32",
|
|
26
|
-
MinimumWorkingSetSize: "size_t",
|
|
27
|
-
MaximumWorkingSetSize: "size_t",
|
|
28
|
-
ActiveProcessLimit: "uint32",
|
|
29
|
-
Affinity: "uintptr_t",
|
|
30
|
-
PriorityClass: "uint32",
|
|
31
|
-
SchedulingClass: "uint32",
|
|
32
|
-
};
|
|
33
|
-
const IO_COUNTERS = {
|
|
34
|
-
ReadOperationCount: "uint64",
|
|
35
|
-
WriteOperationCount: "uint64",
|
|
36
|
-
OtherOperationCount: "uint64",
|
|
37
|
-
ReadTransferCount: "uint64",
|
|
38
|
-
WriteTransferCount: "uint64",
|
|
39
|
-
OtherTransferCount: "uint64",
|
|
40
|
-
};
|
|
41
|
-
let cachedApi = null;
|
|
42
|
-
let loadFailed = false;
|
|
43
|
-
/**
|
|
44
|
-
* 惰性加载并绑定 kernel32。非 win32 或 koffi 不可用时抛错。结果缓存;失败也缓存(不反复重试)。
|
|
45
|
-
*/
|
|
46
|
-
function loadKernel32() {
|
|
47
|
-
if (cachedApi !== null)
|
|
48
|
-
return cachedApi;
|
|
49
|
-
if (loadFailed)
|
|
50
|
-
throw new Error("Windows Job Object backend previously failed to load");
|
|
51
|
-
if (process.platform !== "win32") {
|
|
52
|
-
loadFailed = true;
|
|
53
|
-
throw new Error("Windows Job Object backend is only available on win32");
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
cachedApi = bindKernel32();
|
|
57
|
-
return cachedApi;
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
loadFailed = true;
|
|
61
|
-
throw error instanceof Error ? error : new Error(String(error));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function bindKernel32() {
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- koffi 无类型声明,单点收窄。
|
|
66
|
-
const koffi = createRequire(import.meta.url)("koffi");
|
|
67
|
-
const lib = koffi.load("kernel32.dll");
|
|
68
|
-
koffi.struct("JOBOBJECT_BASIC_LIMIT_INFORMATION", BASIC_LIMIT_INFORMATION);
|
|
69
|
-
koffi.struct("IO_COUNTERS", IO_COUNTERS);
|
|
70
|
-
koffi.struct("JOBOBJECT_EXTENDED_LIMIT_INFORMATION", {
|
|
71
|
-
BasicLimitInformation: "JOBOBJECT_BASIC_LIMIT_INFORMATION",
|
|
72
|
-
IoInfo: "IO_COUNTERS",
|
|
73
|
-
ProcessMemoryLimit: "size_t",
|
|
74
|
-
JobMemoryLimit: "size_t",
|
|
75
|
-
PeakProcessMemoryUsed: "size_t",
|
|
76
|
-
PeakJobMemoryUsed: "size_t",
|
|
77
|
-
});
|
|
78
|
-
koffi.struct("JOBOBJECT_BASIC_ACCOUNTING_INFORMATION", {
|
|
79
|
-
TotalUserTime: "int64",
|
|
80
|
-
TotalKernelTime: "int64",
|
|
81
|
-
ThisPeriodTotalUserTime: "int64",
|
|
82
|
-
ThisPeriodTotalKernelTime: "int64",
|
|
83
|
-
TotalPageFaultCount: "uint32",
|
|
84
|
-
TotalProcesses: "uint32",
|
|
85
|
-
ActiveProcesses: "uint32",
|
|
86
|
-
TotalTerminatedProcesses: "uint32",
|
|
87
|
-
});
|
|
88
|
-
const CreateJobObjectW = lib.func("void* __stdcall CreateJobObjectW(void* attrs, void* name)");
|
|
89
|
-
const SetInformationJobObject = lib.func("bool __stdcall SetInformationJobObject(void* job, int cls, void* info, uint32_t len)");
|
|
90
|
-
const OpenProcess = lib.func("void* __stdcall OpenProcess(uint32_t access, bool inherit, uint32_t pid)");
|
|
91
|
-
const AssignProcessToJobObject = lib.func("bool __stdcall AssignProcessToJobObject(void* job, void* process)");
|
|
92
|
-
const TerminateJobObject = lib.func("bool __stdcall TerminateJobObject(void* job, uint32_t code)");
|
|
93
|
-
const QueryInformationJobObject = lib.func("bool __stdcall QueryInformationJobObject(void* job, int cls, _Out_ void* info, uint32_t len, void* ret)");
|
|
94
|
-
const CloseHandle = lib.func("bool __stdcall CloseHandle(void* handle)");
|
|
95
|
-
const GetLastError = lib.func("uint32_t __stdcall GetLastError()");
|
|
96
|
-
return {
|
|
97
|
-
createJobObject: () => CreateJobObjectW(null, null),
|
|
98
|
-
setKillOnClose: (job) => {
|
|
99
|
-
// 只需把 LimitFlags 置上 KILL_ON_JOB_CLOSE,其余字段留 0。用 Buffer + offsetof 直写,
|
|
100
|
-
// 规避 koffi.encode 的入参数量怪癖;offset 由 koffi 从结构体推导,x64/arm64 一致。
|
|
101
|
-
const size = koffi.sizeof("JOBOBJECT_EXTENDED_LIMIT_INFORMATION");
|
|
102
|
-
const flagsOffset = koffi.offsetof("JOBOBJECT_BASIC_LIMIT_INFORMATION", "LimitFlags");
|
|
103
|
-
const buffer = Buffer.alloc(size);
|
|
104
|
-
buffer.writeUInt32LE(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, flagsOffset);
|
|
105
|
-
return SetInformationJobObject(job, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, buffer, size);
|
|
106
|
-
},
|
|
107
|
-
openProcess: (access, inherit, pid) => OpenProcess(access, inherit, pid),
|
|
108
|
-
assignProcessToJob: (job, process) => AssignProcessToJobObject(job, process),
|
|
109
|
-
terminateJobObject: (job, exitCode) => TerminateJobObject(job, exitCode),
|
|
110
|
-
queryActiveProcessCount: (job) => {
|
|
111
|
-
const size = koffi.sizeof("JOBOBJECT_BASIC_ACCOUNTING_INFORMATION");
|
|
112
|
-
const activeOffset = koffi.offsetof("JOBOBJECT_BASIC_ACCOUNTING_INFORMATION", "ActiveProcesses");
|
|
113
|
-
const buffer = Buffer.alloc(size);
|
|
114
|
-
const ok = QueryInformationJobObject(job, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, buffer, size, null);
|
|
115
|
-
if (!ok)
|
|
116
|
-
throw new Error(`QueryInformationJobObject failed (GetLastError=${GetLastError()})`);
|
|
117
|
-
return buffer.readUInt32LE(activeOffset);
|
|
118
|
-
},
|
|
119
|
-
closeHandle: (handle) => CloseHandle(handle),
|
|
120
|
-
getLastError: () => GetLastError(),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* 同步探测:能否创建带 KILL_ON_JOB_CLOSE 的 Job Object。用于 backend 能力判定。
|
|
125
|
-
* 非 win32 / koffi 缺失 / API 调用失败 → false(fail-closed 降级 legacy)。副作用为零(建后即关)。
|
|
126
|
-
*/
|
|
127
|
-
export function isJobObjectSupported() {
|
|
128
|
-
if (process.platform !== "win32")
|
|
129
|
-
return false;
|
|
130
|
-
try {
|
|
131
|
-
const handle = createKillOnCloseJob();
|
|
132
|
-
closeJob(handle);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/** 创建带 KILL_ON_JOB_CLOSE 的 Job Object。失败抛错。 */
|
|
140
|
-
export function createKillOnCloseJob() {
|
|
141
|
-
const api = loadKernel32();
|
|
142
|
-
const native = api.createJobObject();
|
|
143
|
-
if (!native)
|
|
144
|
-
throw new Error(`CreateJobObjectW failed (GetLastError=${api.getLastError()})`);
|
|
145
|
-
if (!api.setKillOnClose(native)) {
|
|
146
|
-
const code = api.getLastError();
|
|
147
|
-
api.closeHandle(native);
|
|
148
|
-
throw new Error(`SetInformationJobObject(KILL_ON_JOB_CLOSE) failed (GetLastError=${code})`);
|
|
149
|
-
}
|
|
150
|
-
return { native };
|
|
151
|
-
}
|
|
152
|
-
/** 把 pid 对应进程塞进 Job。进程随后 fork 的子孙默认继承 Job(未设 SILENT_BREAKAWAY,禁止逃逸)。 */
|
|
153
|
-
export function assignProcessToJob(handle, pid) {
|
|
154
|
-
const api = loadKernel32();
|
|
155
|
-
const process = api.openProcess(JOB_ASSIGN_ACCESS, false, pid);
|
|
156
|
-
if (!process)
|
|
157
|
-
throw new Error(`OpenProcess(${pid}) failed (GetLastError=${api.getLastError()})`);
|
|
158
|
-
try {
|
|
159
|
-
if (!api.assignProcessToJob(handle.native, process)) {
|
|
160
|
-
throw new Error(`AssignProcessToJobObject(${pid}) failed (GetLastError=${api.getLastError()})`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
finally {
|
|
164
|
-
api.closeHandle(process);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
/** 一次性杀光 Job 内所有进程(超时/取消/runtime 挂了 supervisor 还在)。 */
|
|
168
|
-
export function terminateJob(handle, exitCode = 1) {
|
|
169
|
-
const api = loadKernel32();
|
|
170
|
-
if (!api.terminateJobObject(handle.native, exitCode)) {
|
|
171
|
-
throw new Error(`TerminateJobObject failed (GetLastError=${api.getLastError()})`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/** 关闭 job handle。若为最后一个引用且设了 KILL_ON_JOB_CLOSE,内核连带杀光 Job 内进程。 */
|
|
175
|
-
export function closeJob(handle) {
|
|
176
|
-
const api = loadKernel32();
|
|
177
|
-
api.closeHandle(handle.native);
|
|
178
|
-
}
|
|
179
|
-
/** 当前 Job 内活跃进程数(BasicAccountingInformation.ActiveProcesses)。 */
|
|
180
|
-
export function activeProcessCount(handle) {
|
|
181
|
-
return loadKernel32().queryActiveProcessCount(handle.native);
|
|
182
|
-
}
|
|
183
|
-
/** 轮询等待 Job 清空(整组退出确认),复用 10ms 节奏。超时抛错。 */
|
|
184
|
-
export async function waitForJobEmpty(handle, timeoutMs, pollMs = JOB_EMPTY_POLL_MS) {
|
|
185
|
-
const deadline = Date.now() + timeoutMs;
|
|
186
|
-
while (true) {
|
|
187
|
-
if (activeProcessCount(handle) === 0)
|
|
188
|
-
return;
|
|
189
|
-
if (Date.now() >= deadline)
|
|
190
|
-
throw new Error(`Job Object did not empty within ${timeoutMs}ms`);
|
|
191
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
192
|
-
}
|
|
193
|
-
}
|
package/dist/workspace-fs.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 只读、沙箱化地暴露某 agent 工作区 (<agentsRoot>/<handle>/) 给控制面查看。
|
|
3
|
-
*
|
|
4
|
-
* 安全铁律:
|
|
5
|
-
* - 所有路径都 resolve 后必须仍落在 agent 根内 (防 `..` / 符号链接逃逸)。
|
|
6
|
-
* - 隐藏内部目录与点文件 (.crew / .git / 任何 . 开头)。
|
|
7
|
-
* - 只回文本文件,二进制拒绝;单文件大小上限,超出截断。
|
|
8
|
-
* - 仅 list / read,无写删。
|
|
9
|
-
*/
|
|
10
|
-
import { realpath, readdir, readFile, stat } from "node:fs/promises";
|
|
11
|
-
import { join, resolve, sep } from "node:path";
|
|
12
|
-
export class FsError extends Error {
|
|
13
|
-
}
|
|
14
|
-
const MAX_FILE_BYTES = 256 * 1024; // 单文件查看上限 256KB
|
|
15
|
-
const HIDDEN = new Set([".crew", ".git", ".slock"]);
|
|
16
|
-
const isHidden = (name) => name.startsWith(".") || HIDDEN.has(name);
|
|
17
|
-
/** 把相对路径安全解析到 root 内的绝对路径;越界抛错。realpath 兜底符号链接逃逸。 */
|
|
18
|
-
async function safeResolve(root, rel) {
|
|
19
|
-
const rootResolved = await realpath(root);
|
|
20
|
-
const target = resolve(rootResolved, rel.replace(/^\/+/, ""));
|
|
21
|
-
const withSep = rootResolved.endsWith(sep) ? rootResolved : rootResolved + sep;
|
|
22
|
-
if (target !== rootResolved && !target.startsWith(withSep)) {
|
|
23
|
-
throw new FsError("path escapes workspace");
|
|
24
|
-
}
|
|
25
|
-
// 解析真实路径再校验一次 (符号链接)
|
|
26
|
-
let real;
|
|
27
|
-
try {
|
|
28
|
-
real = await realpath(target);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return target; // 不存在的路径交给后续 stat 报错
|
|
32
|
-
}
|
|
33
|
-
if (real !== rootResolved && !real.startsWith(withSep)) {
|
|
34
|
-
throw new FsError("path escapes workspace (symlink)");
|
|
35
|
-
}
|
|
36
|
-
return real;
|
|
37
|
-
}
|
|
38
|
-
/** 列出某目录 (相对 root)。隐藏点文件/内部目录;目录在前、按名排序。 */
|
|
39
|
-
export async function listWorkspace(root, rel) {
|
|
40
|
-
// 工作区尚未创建 (agent 还没跑过):返回空,而不是报错
|
|
41
|
-
const rootExists = await stat(root).then((s) => s.isDirectory()).catch(() => false);
|
|
42
|
-
if (!rootExists)
|
|
43
|
-
return { root: resolve(root), path: rel.replace(/^\/+/, ""), entries: [] };
|
|
44
|
-
const rootResolved = await realpath(root);
|
|
45
|
-
const dir = await safeResolve(root, rel);
|
|
46
|
-
const st = await stat(dir).catch(() => null);
|
|
47
|
-
if (!st || !st.isDirectory())
|
|
48
|
-
throw new FsError("not a directory");
|
|
49
|
-
const names = await readdir(dir);
|
|
50
|
-
const entries = [];
|
|
51
|
-
for (const name of names) {
|
|
52
|
-
if (isHidden(name))
|
|
53
|
-
continue;
|
|
54
|
-
const full = join(dir, name);
|
|
55
|
-
const s = await stat(full).catch(() => null);
|
|
56
|
-
if (!s)
|
|
57
|
-
continue;
|
|
58
|
-
if (s.isDirectory())
|
|
59
|
-
entries.push({ name, type: "dir" });
|
|
60
|
-
else if (s.isFile())
|
|
61
|
-
entries.push({ name, type: "file", size: s.size });
|
|
62
|
-
}
|
|
63
|
-
entries.sort((a, b) => a.type !== b.type ? (a.type === "dir" ? -1 : 1) : a.name.localeCompare(b.name));
|
|
64
|
-
return { root: rootResolved, path: rel.replace(/^\/+/, ""), entries };
|
|
65
|
-
}
|
|
66
|
-
/** 读取某文本文件 (相对 root)。二进制/超大拒绝或截断。 */
|
|
67
|
-
export async function readWorkspaceFile(root, rel) {
|
|
68
|
-
const file = await safeResolve(root, rel);
|
|
69
|
-
const s = await stat(file).catch(() => null);
|
|
70
|
-
if (!s || !s.isFile())
|
|
71
|
-
throw new FsError("not a file");
|
|
72
|
-
const buf = await readFile(file);
|
|
73
|
-
// 二进制探测:含 NUL 即视为二进制
|
|
74
|
-
const sample = buf.subarray(0, Math.min(buf.length, 8192));
|
|
75
|
-
if (sample.includes(0))
|
|
76
|
-
throw new FsError("binary file not viewable");
|
|
77
|
-
const truncated = buf.length > MAX_FILE_BYTES;
|
|
78
|
-
const content = buf.subarray(0, MAX_FILE_BYTES).toString("utf8");
|
|
79
|
-
return { path: rel.replace(/^\/+/, ""), content, truncated, size: buf.length };
|
|
80
|
-
}
|
package/dist/workspace-import.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 导入一个已有的 raft agent 工作区到本机的 crew 工作区。
|
|
3
|
-
*
|
|
4
|
-
* raft 工作区目录(~/.slock/agents/<uuid>/)里:
|
|
5
|
-
* - 用户内容(复制):MEMORY.md、notes/、artifacts/、incoming/、*.html 等非隐藏文件
|
|
6
|
-
* - raft 内部(不复制,crew 自己重建):.git/、.slock/(系统提示词/mcp/CLI wrapper)
|
|
7
|
-
*
|
|
8
|
-
* crew 侧的唯一 id、隐藏的 .crew/(system-prompt + crew wrapper)由 prepareWorkspace 在
|
|
9
|
-
* 首次运行时生成,DB 行另起新 uuid —— 所以这里只搬运用户内容,内部一律重建。
|
|
10
|
-
*/
|
|
11
|
-
import { readFile, writeFile, readdir, stat, cp, mkdir } from "node:fs/promises";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
|
-
/** 一律跳过的隐藏/内部条目(其余 dotfile 也跳过)。 */
|
|
15
|
-
const SKIP = new Set([".git", ".slock", ".crew", ".DS_Store"]);
|
|
16
|
-
/** 展开开头的 `~` 为用户主目录(daemon 在被测机器上跑,路径里常带 ~)。 */
|
|
17
|
-
export function expandHome(p) {
|
|
18
|
-
if (p === "~")
|
|
19
|
-
return homedir();
|
|
20
|
-
if (p.startsWith("~/"))
|
|
21
|
-
return join(homedir(), p.slice(2));
|
|
22
|
-
return p;
|
|
23
|
-
}
|
|
24
|
-
/** 从 MEMORY.md 反填:H1 → name;`## Role` 段正文 → description(无 Role 则取 H1 后首段)。 */
|
|
25
|
-
export function parseMemory(md) {
|
|
26
|
-
const lines = md.split(/\r?\n/);
|
|
27
|
-
let name = "";
|
|
28
|
-
let h1Idx = -1;
|
|
29
|
-
for (let i = 0; i < lines.length; i++) {
|
|
30
|
-
const m = lines[i].match(/^#\s+(.+?)\s*$/);
|
|
31
|
-
if (m) {
|
|
32
|
-
name = m[1].trim();
|
|
33
|
-
h1Idx = i;
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const sectionBody = (startIdx) => {
|
|
38
|
-
const body = [];
|
|
39
|
-
for (let i = startIdx; i < lines.length; i++) {
|
|
40
|
-
if (/^#{1,6}\s+/.test(lines[i]))
|
|
41
|
-
break; // 下一个标题为止
|
|
42
|
-
body.push(lines[i]);
|
|
43
|
-
}
|
|
44
|
-
return body.join("\n").trim();
|
|
45
|
-
};
|
|
46
|
-
let description = "";
|
|
47
|
-
const roleIdx = lines.findIndex((l) => /^##\s+Role\b/i.test(l));
|
|
48
|
-
if (roleIdx >= 0)
|
|
49
|
-
description = sectionBody(roleIdx + 1);
|
|
50
|
-
if (!description && h1Idx >= 0)
|
|
51
|
-
description = sectionBody(h1Idx + 1); // 兜底:H1 后首段
|
|
52
|
-
return { name, description: description.slice(0, 3000) };
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* 导入时清空 MEMORY.md 的 `## Active Context` 段:其内容是 raft 里的在办任务进度(含任务 id),
|
|
56
|
-
* 这些 id 在新的 OpenSlock 里不一定存在,照搬会误导。保留标题,正文换成占位提示让 agent 重新记录。
|
|
57
|
-
* 其它段(Role/索引/领域知识)照常保留。
|
|
58
|
-
*/
|
|
59
|
-
export function stripActiveContext(md) {
|
|
60
|
-
const lines = md.split(/\r?\n/);
|
|
61
|
-
const start = lines.findIndex((l) => /^##\s+Active Context\b/i.test(l));
|
|
62
|
-
if (start < 0)
|
|
63
|
-
return md;
|
|
64
|
-
// 段落结束 = 下一个 # / ## 标题,或 `---` 分隔线,或文件末尾
|
|
65
|
-
let end = lines.length;
|
|
66
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
67
|
-
if (/^#{1,2}\s+/.test(lines[i]) || /^---\s*$/.test(lines[i])) {
|
|
68
|
-
end = i;
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
const replacement = [
|
|
73
|
-
lines[start], // 原 `## Active Context` 标题
|
|
74
|
-
"<!-- (导入时已清空:raft 的在办任务/任务 id 不属于本工作区。开工时在此重新记录当前进度。) -->",
|
|
75
|
-
"",
|
|
76
|
-
];
|
|
77
|
-
return [...lines.slice(0, start), ...replacement, ...lines.slice(end)].join("\n");
|
|
78
|
-
}
|
|
79
|
-
async function assertDir(srcPath) {
|
|
80
|
-
let st;
|
|
81
|
-
try {
|
|
82
|
-
st = await stat(srcPath);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
throw new Error(`workspace path not found: ${srcPath}`);
|
|
86
|
-
}
|
|
87
|
-
if (!st.isDirectory())
|
|
88
|
-
throw new Error(`not a directory: ${srcPath}`);
|
|
89
|
-
}
|
|
90
|
-
/** 读取 raft 工作区元信息(name/description/可复制条目),不写入任何文件。 */
|
|
91
|
-
export async function inspectRaftWorkspace(rawPath) {
|
|
92
|
-
const srcPath = expandHome(rawPath);
|
|
93
|
-
await assertDir(srcPath);
|
|
94
|
-
let memory;
|
|
95
|
-
try {
|
|
96
|
-
memory = await readFile(join(srcPath, "MEMORY.md"), "utf8");
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
throw new Error("no MEMORY.md in this folder — is it a raft agent workspace?");
|
|
100
|
-
}
|
|
101
|
-
const { name, description } = parseMemory(memory);
|
|
102
|
-
const all = await readdir(srcPath);
|
|
103
|
-
const entries = all.filter((e) => !SKIP.has(e) && !e.startsWith("."));
|
|
104
|
-
return { name, description, fileCount: entries.length, entries };
|
|
105
|
-
}
|
|
106
|
-
/** 复制 raft 工作区的用户内容到目标目录(隐藏/内部条目不复制)。 */
|
|
107
|
-
export async function importRaftWorkspace(rawPath, destDir) {
|
|
108
|
-
const srcPath = expandHome(rawPath);
|
|
109
|
-
await assertDir(srcPath);
|
|
110
|
-
await mkdir(destDir, { recursive: true });
|
|
111
|
-
const all = await readdir(srcPath);
|
|
112
|
-
const copied = [];
|
|
113
|
-
for (const e of all) {
|
|
114
|
-
if (SKIP.has(e) || e.startsWith("."))
|
|
115
|
-
continue;
|
|
116
|
-
if (e === "MEMORY.md") {
|
|
117
|
-
// MEMORY.md 特殊处理:剥离 Active Context(陈旧任务 id)后再落地,其余原样保留。
|
|
118
|
-
const cleaned = stripActiveContext(await readFile(join(srcPath, e), "utf8"));
|
|
119
|
-
await writeFile(join(destDir, e), cleaned, "utf8");
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
await cp(join(srcPath, e), join(destDir, e), { recursive: true });
|
|
123
|
-
}
|
|
124
|
-
copied.push(e);
|
|
125
|
-
}
|
|
126
|
-
return { copied, dir: destDir };
|
|
127
|
-
}
|