@yuandc/aica 0.1.0

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.
Files changed (49) hide show
  1. package/README.md +9 -0
  2. package/dist/acp/agent.js +54 -0
  3. package/dist/acp/client/acp-client.js +102 -0
  4. package/dist/acp/client/acp-content.js +13 -0
  5. package/dist/acp/client/acp-events.js +106 -0
  6. package/dist/acp/client/acp-process.js +34 -0
  7. package/dist/acp/client/acp-runtime-pool.js +248 -0
  8. package/dist/acp/client/context-usage.js +29 -0
  9. package/dist/acp/client/json-rpc.js +128 -0
  10. package/dist/acp/provider-types.js +1 -0
  11. package/dist/acp/providers/codex/codex-process.js +51 -0
  12. package/dist/acp/providers/codex/events.js +1473 -0
  13. package/dist/acp/providers/codex/permissions.js +49 -0
  14. package/dist/acp/providers/codex/provider.js +376 -0
  15. package/dist/acp/providers/codex-acp/adapter.js +947 -0
  16. package/dist/acp/providers/codex-acp/context-maintenance.js +148 -0
  17. package/dist/acp/providers/codex-acp/launch.js +35 -0
  18. package/dist/acp/providers/codex-acp/provider.js +486 -0
  19. package/dist/acp/providers/mimo/provider.js +448 -0
  20. package/dist/acp/providers/opencode/provider.js +489 -0
  21. package/dist/acp/providers/registry.js +23 -0
  22. package/dist/acp/standard-events.js +167 -0
  23. package/dist/commands/start.js +137 -0
  24. package/dist/commands/worker-auth.js +100 -0
  25. package/dist/commands/worker-project.js +57 -0
  26. package/dist/core/aca-config.js +74 -0
  27. package/dist/core/aca-server-client.js +57 -0
  28. package/dist/core/acp-event-coalescer.js +108 -0
  29. package/dist/core/acp-event-upload-filter.js +16 -0
  30. package/dist/core/acp-orphan-cleanup.js +91 -0
  31. package/dist/core/affected-files.js +268 -0
  32. package/dist/core/auth.js +36 -0
  33. package/dist/core/file-transfer-worker.js +169 -0
  34. package/dist/core/fs.js +28 -0
  35. package/dist/core/heartbeat.js +578 -0
  36. package/dist/core/job-permission-policy.js +42 -0
  37. package/dist/core/job-worker.js +749 -0
  38. package/dist/core/logger.js +42 -0
  39. package/dist/core/long-poll-worker.js +26 -0
  40. package/dist/core/machine-filesystem-worker.js +352 -0
  41. package/dist/core/paths.js +26 -0
  42. package/dist/core/process-identity.js +34 -0
  43. package/dist/core/process.js +33 -0
  44. package/dist/core/provider-health.js +54 -0
  45. package/dist/core/runtime-options.js +38 -0
  46. package/dist/core/worktree.js +95 -0
  47. package/dist/worker-cli.js +27 -0
  48. package/dist/worker-single-cli.js +17 -0
  49. package/package.json +35 -0
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ensureDir } from "./fs.js";
4
+ import { getLogDir } from "./paths.js";
5
+ const levelRank = {
6
+ debug: 10,
7
+ info: 20,
8
+ warn: 30,
9
+ error: 40
10
+ };
11
+ export class Logger {
12
+ scope;
13
+ minLevel;
14
+ file;
15
+ constructor(scope, minLevel = "info") {
16
+ this.scope = scope;
17
+ this.minLevel = minLevel;
18
+ const date = new Date().toISOString().slice(0, 10);
19
+ this.file = path.join(getLogDir(), `${date}.log`);
20
+ ensureDir(path.dirname(this.file));
21
+ }
22
+ debug(message) {
23
+ this.write("debug", message);
24
+ }
25
+ info(message) {
26
+ this.write("info", message);
27
+ }
28
+ warn(message) {
29
+ this.write("warn", message);
30
+ }
31
+ error(message) {
32
+ this.write("error", message);
33
+ }
34
+ write(level, message) {
35
+ const line = `${new Date().toISOString()} [${level.toUpperCase()}] [${this.scope}] ${message}`;
36
+ fs.appendFileSync(this.file, `${line}\n`, "utf8");
37
+ if (levelRank[level] >= levelRank[this.minLevel]) {
38
+ const writer = level === "error" ? process.stderr : process.stdout;
39
+ writer.write(`${line}\n`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,26 @@
1
+ export function startLongPollWorker(input) {
2
+ // 正常长轮询超时后立即续接;只有网络或服务错误才指数退避,防止重启时集中重连。
3
+ let stopped = false;
4
+ let failures = 0;
5
+ const loop = async () => {
6
+ while (!stopped) {
7
+ try {
8
+ await input.run();
9
+ failures = 0;
10
+ }
11
+ catch (error) {
12
+ if (stopped)
13
+ break;
14
+ failures += 1;
15
+ const retryMs = Math.min(10_000, 500 * 2 ** Math.min(5, failures - 1)) + Math.floor(Math.random() * 250);
16
+ input.onError(error, retryMs);
17
+ await delay(retryMs);
18
+ }
19
+ }
20
+ };
21
+ void loop();
22
+ return { stop: () => { stopped = true; } };
23
+ }
24
+ function delay(ms) {
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
@@ -0,0 +1,352 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { acaServerRequest } from "./aca-server-client.js";
7
+ import { loadAcaConfig, upsertLocalProject } from "./aca-config.js";
8
+ import { startLongPollWorker } from "./long-poll-worker.js";
9
+ const LONG_POLL_WAIT_MS = 25_000;
10
+ const MAX_DIRECTORY_ITEMS = 300;
11
+ const FILESYSTEM_ROOTS_CACHE_MS = 30_000;
12
+ let filesystemRootsCache = null;
13
+ export function startMachineFilesystemWorkerLoop(logger) {
14
+ return startLongPollWorker({
15
+ run: async () => {
16
+ const config = loadAcaConfig();
17
+ if (!config.token)
18
+ throw new Error("Worker 尚未配置认证令牌");
19
+ const response = await acaServerRequest("GET", `/api/client/machine-operations/claim?machineId=${encodeURIComponent(config.machineId)}&waitMs=${LONG_POLL_WAIT_MS}`);
20
+ if (!response.item)
21
+ return;
22
+ try {
23
+ const result = await executeMachineOperation(response.item);
24
+ await acaServerRequest("POST", `/api/client/machine-operations/${encodeURIComponent(response.item.requestId)}/complete`, { result });
25
+ }
26
+ catch (error) {
27
+ const message = error instanceof Error ? error.message : String(error);
28
+ logger.warn(`machine filesystem request failed request=${response.item.requestId}: ${message}`);
29
+ await acaServerRequest("POST", `/api/client/machine-operations/${encodeURIComponent(response.item.requestId)}/fail`, { message }).catch(() => undefined);
30
+ }
31
+ },
32
+ onError: (error, retryMs) => logger.warn(`machine filesystem worker failed, retry in ${retryMs}ms: ${error instanceof Error ? error.message : String(error)}`)
33
+ });
34
+ }
35
+ async function executeMachineOperation(request) {
36
+ if (request.operation === "filesystem_roots")
37
+ return listMachineFilesystemRoots();
38
+ if (request.operation === "list_directory") {
39
+ return listMachineDirectory({
40
+ directoryPath: String(request.payload.path || ""),
41
+ includeHidden: request.payload.includeHidden === true
42
+ });
43
+ }
44
+ if (request.operation === "register_project")
45
+ return registerMachineProject(request.payload);
46
+ if (request.operation === "list_project_directory")
47
+ return listProjectDirectory(request.payload);
48
+ throw new Error("不支持的 Machine 文件系统操作。 ");
49
+ }
50
+ export function listProjectDirectory(payload) {
51
+ const rootPath = canonicalProjectRoot(String(payload.rootPath || ""));
52
+ const relativePath = normalizeProjectRelativePath(String(payload.relativePath || ""));
53
+ const directoryPath = resolveProjectEntry(rootPath, relativePath, true);
54
+ const offset = Math.max(0, Number(payload.offset || 0));
55
+ const limit = Math.min(300, Math.max(20, Number(payload.limit || 200)));
56
+ const entries = fs.readdirSync(directoryPath, { withFileTypes: true })
57
+ .filter((entry) => !entry.isSymbolicLink())
58
+ .map((entry) => ({
59
+ name: entry.name,
60
+ path: projectRelativePath(rootPath, path.join(directoryPath, entry.name)),
61
+ type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other",
62
+ hidden: entry.name.startsWith("."),
63
+ extension: entry.isFile() ? path.extname(entry.name).slice(1).toLowerCase() : ""
64
+ }))
65
+ .filter((entry) => entry.type !== "other")
66
+ .sort((left, right) => {
67
+ if (left.type !== right.type)
68
+ return left.type === "directory" ? -1 : 1;
69
+ return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" });
70
+ });
71
+ const items = entries.slice(offset, offset + limit);
72
+ return {
73
+ path: relativePath,
74
+ rootKind: payload.rootKind === "worktree" ? "worktree" : "project",
75
+ items,
76
+ total: entries.length,
77
+ nextOffset: offset + items.length < entries.length ? offset + items.length : null
78
+ };
79
+ }
80
+ export function listMachineFilesystemRoots() {
81
+ const platform = process.platform;
82
+ const roots = cachedFilesystemRoots(platform);
83
+ return { platform, roots };
84
+ }
85
+ export function listMachineDirectory(input) {
86
+ if (!input.directoryPath.trim())
87
+ throw new Error("目录路径不能为空。");
88
+ return process.platform === "win32"
89
+ ? listWindowsDirectory(input.directoryPath, Boolean(input.includeHidden))
90
+ : listPosixDirectory(input.directoryPath, Boolean(input.includeHidden));
91
+ }
92
+ export function comparableMachinePath(value, platform = process.platform) {
93
+ const api = platform === "win32" ? path.win32 : path.posix;
94
+ const normalizedValue = api.normalize(value);
95
+ const root = api.parse(normalizedValue).root;
96
+ const normalized = normalizedValue.length > root.length ? normalizedValue.replace(/[\\/]+$/, "") : normalizedValue;
97
+ return platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
98
+ }
99
+ function posixFilesystemRoots() {
100
+ const home = fs.realpathSync(os.homedir());
101
+ const roots = [{ id: "home", label: "用户目录", path: home, kind: "home", available: true }];
102
+ if (home !== path.parse(home).root)
103
+ roots.push({ id: "root", label: "文件系统", path: "/", kind: "root", available: true });
104
+ return roots;
105
+ }
106
+ function cachedFilesystemRoots(platform) {
107
+ if (filesystemRootsCache?.platform === platform && filesystemRootsCache.expiresAtMs > Date.now())
108
+ return filesystemRootsCache.roots;
109
+ const roots = platform === "win32" ? windowsFilesystemRoots() : posixFilesystemRoots();
110
+ filesystemRootsCache = { platform, roots, expiresAtMs: Date.now() + FILESYSTEM_ROOTS_CACHE_MS };
111
+ return roots;
112
+ }
113
+ function windowsFilesystemRoots() {
114
+ const home = fs.realpathSync.native(os.homedir());
115
+ const roots = new Map();
116
+ roots.set(comparableMachinePath(home, "win32"), { id: "home", label: "用户目录", path: home, kind: "home", available: true });
117
+ try {
118
+ const script = [
119
+ "$items = Get-CimInstance Win32_LogicalDisk | ForEach-Object {",
120
+ " [PSCustomObject]@{ Name = $_.DeviceID; Root = ($_.DeviceID + '\\\\'); DriveType = $_.DriveType; VolumeName = $_.VolumeName }",
121
+ "}",
122
+ "@($items) | ConvertTo-Json -Compress"
123
+ ].join("\n");
124
+ const values = parsePowerShellJson(execPowerShell(script));
125
+ for (const value of values) {
126
+ const rootPath = String(value.Root || "");
127
+ if (!rootPath)
128
+ continue;
129
+ const driveType = Number(value.DriveType || 0);
130
+ const name = String(value.Name || rootPath.replace(/[\\/]+$/, ""));
131
+ const volume = String(value.VolumeName || "").trim();
132
+ roots.set(comparableMachinePath(rootPath, "win32"), {
133
+ id: `drive-${name.replace(/[^a-z0-9]+/gi, "").toLowerCase()}`,
134
+ label: volume ? `${volume} (${name})` : driveLabel(name, driveType),
135
+ path: rootPath,
136
+ kind: driveKind(driveType),
137
+ available: true
138
+ });
139
+ }
140
+ }
141
+ catch {
142
+ const rootPath = path.win32.parse(home).root;
143
+ roots.set(comparableMachinePath(rootPath, "win32"), { id: "drive-system", label: `系统盘 (${rootPath.replace(/\\$/, "")})`, path: rootPath, kind: "fixed", available: true });
144
+ }
145
+ return [...roots.values()];
146
+ }
147
+ function listPosixDirectory(directoryPath, includeHidden) {
148
+ const canonicalPath = canonicalDirectory(directoryPath);
149
+ const entries = fs.readdirSync(canonicalPath, { withFileTypes: true });
150
+ const directories = entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink() && (includeHidden || !entry.name.startsWith(".")));
151
+ const items = directories
152
+ .slice(0, MAX_DIRECTORY_ITEMS)
153
+ .map((entry) => ({ name: entry.name, path: path.join(canonicalPath, entry.name), kind: "directory", hidden: entry.name.startsWith("."), hasChildren: true }));
154
+ items.sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
155
+ return directoryListing(canonicalPath, items, directories.length > MAX_DIRECTORY_ITEMS, "linux");
156
+ }
157
+ function listWindowsDirectory(directoryPath, includeHidden) {
158
+ const canonicalPath = canonicalDirectory(directoryPath);
159
+ const script = [
160
+ "$items = Get-ChildItem -LiteralPath $env:ACA_MACHINE_FS_PATH -Directory -Force -ErrorAction Stop | ForEach-Object {",
161
+ " [PSCustomObject]@{ Name = $_.Name; FullName = $_.FullName; Attributes = $_.Attributes.ToString() }",
162
+ "}",
163
+ "@($items) | ConvertTo-Json -Compress"
164
+ ].join("\n");
165
+ const values = parsePowerShellJson(execPowerShell(script, { ACA_MACHINE_FS_PATH: canonicalPath }));
166
+ const items = values
167
+ .map((value) => {
168
+ const attributes = String(value.Attributes || "");
169
+ return {
170
+ name: String(value.Name || ""),
171
+ path: String(value.FullName || ""),
172
+ kind: "directory",
173
+ hidden: /Hidden|System/i.test(attributes),
174
+ hasChildren: true,
175
+ reparsePoint: /ReparsePoint/i.test(attributes)
176
+ };
177
+ })
178
+ .filter((entry) => entry.name && entry.path && !entry.reparsePoint && (includeHidden || !entry.hidden))
179
+ .slice(0, MAX_DIRECTORY_ITEMS)
180
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
181
+ return directoryListing(canonicalPath, items, values.length > MAX_DIRECTORY_ITEMS, "win32");
182
+ }
183
+ function directoryListing(canonicalPath, items, truncated, platform) {
184
+ const api = platform === "win32" ? path.win32 : path.posix;
185
+ const root = api.parse(canonicalPath).root;
186
+ const parent = comparableMachinePath(canonicalPath, platform) === comparableMachinePath(root, platform)
187
+ ? null
188
+ : api.dirname(canonicalPath);
189
+ return {
190
+ platform,
191
+ path: canonicalPath,
192
+ parentPath: parent,
193
+ breadcrumbs: pathBreadcrumbs(canonicalPath, platform),
194
+ readable: canAccess(canonicalPath, fs.constants.R_OK),
195
+ writable: canAccess(canonicalPath, fs.constants.W_OK),
196
+ items,
197
+ truncated
198
+ };
199
+ }
200
+ function pathBreadcrumbs(value, platform) {
201
+ const api = platform === "win32" ? path.win32 : path.posix;
202
+ const parsed = api.parse(value);
203
+ const relative = value.slice(parsed.root.length).split(/[\\/]+/).filter(Boolean);
204
+ const result = [{ label: parsed.root.replace(/[\\/]+$/, "") || parsed.root, path: parsed.root }];
205
+ let current = parsed.root;
206
+ for (const segment of relative) {
207
+ current = api.join(current, segment);
208
+ result.push({ label: segment, path: current });
209
+ }
210
+ return result;
211
+ }
212
+ async function registerMachineProject(payload) {
213
+ const workspaceId = String(payload.workspaceId || "").trim();
214
+ const name = String(payload.name || "").trim();
215
+ const projectType = payload.projectType === "chat_room" ? "chat_room" : "project";
216
+ if (!workspaceId || !name)
217
+ throw new Error("项目名称和工作区不能为空。");
218
+ const rootPath = canonicalDirectory(String(payload.path || ""));
219
+ fs.accessSync(rootPath, fs.constants.R_OK | fs.constants.W_OK);
220
+ const projects = await acaServerRequest("GET", `/api/projects?workspaceId=${encodeURIComponent(workspaceId)}`);
221
+ const existing = projects.items.find((item) => comparableMachinePath(item.root_path) === comparableMachinePath(rootPath));
222
+ const item = existing || (await acaServerRequest("POST", "/api/projects", {
223
+ projectId: randomUUID(),
224
+ workspaceId,
225
+ name,
226
+ rootPath,
227
+ source: "aica",
228
+ projectType
229
+ })).item;
230
+ upsertLocalProject(localProject(item));
231
+ return { project: item, duplicate: Boolean(existing) };
232
+ }
233
+ function localProject(item) {
234
+ return {
235
+ projectId: item.project_id,
236
+ workspaceId: item.workspace_id,
237
+ name: item.name,
238
+ rootPath: item.root_path
239
+ };
240
+ }
241
+ function canonicalDirectory(value) {
242
+ if (!value.trim() || !path.isAbsolute(value))
243
+ throw new Error("必须选择绝对目录路径。");
244
+ assertRequestedPathAllowed(value);
245
+ const stat = fs.statSync(value);
246
+ if (!stat.isDirectory())
247
+ throw new Error("选择的路径不是目录。");
248
+ const canonicalPath = fs.realpathSync.native(value);
249
+ assertBrowsePathAllowed(canonicalPath);
250
+ return canonicalPath;
251
+ }
252
+ function canonicalProjectRoot(value) {
253
+ if (!value.trim() || !path.isAbsolute(value))
254
+ throw new Error("项目根目录无效。");
255
+ const root = fs.realpathSync.native(value);
256
+ if (!fs.statSync(root).isDirectory())
257
+ throw new Error("项目根目录不存在。");
258
+ return root;
259
+ }
260
+ function normalizeProjectRelativePath(value) {
261
+ const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
262
+ if (!normalized)
263
+ return "";
264
+ if (normalized.includes("\0") || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
265
+ throw new Error("项目相对路径无效。");
266
+ }
267
+ return normalized;
268
+ }
269
+ function resolveProjectEntry(rootPath, relativePath, requireDirectory) {
270
+ const candidate = relativePath ? path.join(rootPath, ...relativePath.split("/")) : rootPath;
271
+ const target = fs.realpathSync.native(candidate);
272
+ const relative = path.relative(rootPath, target);
273
+ if (relative.startsWith("..") || path.isAbsolute(relative))
274
+ throw new Error("目录超出项目范围。");
275
+ const stat = fs.statSync(target);
276
+ if (requireDirectory && !stat.isDirectory())
277
+ throw new Error("请求路径不是目录。");
278
+ return target;
279
+ }
280
+ function projectRelativePath(rootPath, targetPath) {
281
+ return path.relative(rootPath, targetPath).split(path.sep).join("/");
282
+ }
283
+ function assertRequestedPathAllowed(candidate) {
284
+ const api = process.platform === "win32" ? path.win32 : path.posix;
285
+ const allowed = cachedFilesystemRoots(process.platform).some((root) => {
286
+ const rootPath = String(root.path || "");
287
+ if (!rootPath)
288
+ return false;
289
+ const relative = api.relative(rootPath, candidate);
290
+ return relative === "" || (!relative.startsWith("..") && !api.isAbsolute(relative));
291
+ });
292
+ if (!allowed)
293
+ throw new Error("目录不在允许浏览的位置内。");
294
+ }
295
+ function assertBrowsePathAllowed(candidate) {
296
+ const api = process.platform === "win32" ? path.win32 : path.posix;
297
+ const allowed = cachedFilesystemRoots(process.platform).some((root) => {
298
+ const rootPath = String(root.path || "");
299
+ if (!rootPath)
300
+ return false;
301
+ const relative = api.relative(rootPath, candidate);
302
+ return relative === "" || (!relative.startsWith("..") && !api.isAbsolute(relative));
303
+ });
304
+ if (!allowed)
305
+ throw new Error("目录不在允许浏览的位置内。");
306
+ }
307
+ function canAccess(value, mode) {
308
+ try {
309
+ fs.accessSync(value, mode);
310
+ return true;
311
+ }
312
+ catch {
313
+ return false;
314
+ }
315
+ }
316
+ function execPowerShell(script, extraEnv = {}) {
317
+ return execFileSync("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
318
+ encoding: "utf8",
319
+ windowsHide: true,
320
+ timeout: 8_000,
321
+ maxBuffer: 4 * 1024 * 1024,
322
+ env: { ...process.env, ...extraEnv }
323
+ }).replace(/^\uFEFF/, "").trim();
324
+ }
325
+ function parsePowerShellJson(value) {
326
+ if (!value)
327
+ return [];
328
+ const parsed = JSON.parse(value);
329
+ if (Array.isArray(parsed))
330
+ return parsed.filter((item) => Boolean(item && typeof item === "object"));
331
+ return parsed && typeof parsed === "object" ? [parsed] : [];
332
+ }
333
+ function driveKind(value) {
334
+ if (value === 2)
335
+ return "removable";
336
+ if (value === 3)
337
+ return "fixed";
338
+ if (value === 4)
339
+ return "network";
340
+ if (value === 5)
341
+ return "optical";
342
+ return "drive";
343
+ }
344
+ function driveLabel(name, type) {
345
+ if (type === 2)
346
+ return `移动磁盘 (${name})`;
347
+ if (type === 4)
348
+ return `网络驱动器 (${name})`;
349
+ if (type === 5)
350
+ return `光驱 (${name})`;
351
+ return `本地磁盘 (${name})`;
352
+ }
@@ -0,0 +1,26 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function getAcaHome() {
4
+ return process.env.AICA_HOME || process.env.ACA_HOME || path.join(os.homedir(), ".aica");
5
+ }
6
+ export function getPidFile() {
7
+ return path.join(getAcaHome(), "daemon.pid");
8
+ }
9
+ export function getLogDir() {
10
+ return path.join(getAcaHome(), "logs");
11
+ }
12
+ export function getStateDir() {
13
+ return path.join(getAcaHome(), "state");
14
+ }
15
+ export function getSessionDbPath() {
16
+ return path.join(getStateDir(), "sessions.sqlite3");
17
+ }
18
+ export function getRuntimeStatePath() {
19
+ return path.join(getAcaHome(), "runtime-state.json");
20
+ }
21
+ export function getAcaConfigPath() {
22
+ return process.env.AICA_CONFIG || process.env.ACA_CONFIG || path.join(getAcaHome(), "config.json");
23
+ }
24
+ export function getCredentialsPath() {
25
+ return process.env.AICA_CREDENTIALS || process.env.ACA_CREDENTIALS || path.join(getAcaHome(), "credentials.json");
26
+ }
@@ -0,0 +1,34 @@
1
+ import fs from "node:fs";
2
+ export const ACA_WORKER_INSTANCE_ENV = "ACA_WORKER_INSTANCE_ID";
3
+ export const ACA_WORKER_PID_ENV = "ACA_WORKER_PID";
4
+ export const ACA_WORKER_START_TICKS_ENV = "ACA_WORKER_START_TICKS";
5
+ export const ACA_ACP_CHILD_ENV = "ACA_ACP_RUNTIME_CHILD";
6
+ export const ACA_ACP_OWNER_PID_ENV = "ACA_ACP_RUNTIME_OWNER_PID";
7
+ export const ACA_ACP_OWNER_START_TICKS_ENV = "ACA_ACP_RUNTIME_OWNER_START_TICKS";
8
+ export const ACA_ACP_WORKER_INSTANCE_ENV = "ACA_ACP_RUNTIME_WORKER_INSTANCE_ID";
9
+ export function readProcessStartTicks(pid) {
10
+ try {
11
+ const value = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
12
+ const closingParen = value.lastIndexOf(")");
13
+ if (closingParen < 0)
14
+ return null;
15
+ const fields = value.slice(closingParen + 2).trim().split(/\s+/);
16
+ return fields[19] || null;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function acpChildEnvironment(extra = {}) {
23
+ const ownerPid = process.env[ACA_WORKER_PID_ENV] || String(process.pid);
24
+ const ownerStartTicks = process.env[ACA_WORKER_START_TICKS_ENV] || readProcessStartTicks(Number(ownerPid)) || "";
25
+ const workerInstanceId = process.env[ACA_WORKER_INSTANCE_ENV] || "";
26
+ return {
27
+ ...process.env,
28
+ ...extra,
29
+ [ACA_ACP_CHILD_ENV]: "1",
30
+ [ACA_ACP_OWNER_PID_ENV]: ownerPid,
31
+ [ACA_ACP_OWNER_START_TICKS_ENV]: ownerStartTicks,
32
+ [ACA_ACP_WORKER_INSTANCE_ENV]: workerInstanceId
33
+ };
34
+ }
@@ -0,0 +1,33 @@
1
+ import fs from "node:fs";
2
+ import { getPidFile } from "./paths.js";
3
+ import { ensureDir, removeFileIfExists } from "./fs.js";
4
+ import path from "node:path";
5
+ export function isProcessAlive(pid) {
6
+ try {
7
+ process.kill(pid, 0);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ export function readPidFile() {
15
+ try {
16
+ const raw = fs.readFileSync(getPidFile(), "utf8").trim();
17
+ const pid = Number(raw);
18
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return null;
23
+ throw error;
24
+ }
25
+ }
26
+ export function writePidFile(pid) {
27
+ const file = getPidFile();
28
+ ensureDir(path.dirname(file));
29
+ fs.writeFileSync(file, `${pid}\n`, "utf8");
30
+ }
31
+ export function removePidFile() {
32
+ removeFileIfExists(getPidFile());
33
+ }
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { resolveCodexPath } from "../acp/providers/codex/codex-process.js";
3
+ const PROVIDER_HEALTH_TTL_MS = 5 * 60 * 1000;
4
+ let cache = null;
5
+ export function providerHealthSnapshot() {
6
+ const now = Date.now();
7
+ if (cache && cache.expiresAtMs > now)
8
+ return cache.items;
9
+ const items = {
10
+ codex: probeProvider(resolveCodexPath()),
11
+ mimo: probeProvider(process.env.ACA_MIMO_COMMAND || "mimo"),
12
+ opencode: probeProvider(process.env.ACA_OPENCODE_COMMAND || "opencode")
13
+ };
14
+ cache = { expiresAtMs: now + PROVIDER_HEALTH_TTL_MS, items };
15
+ return items;
16
+ }
17
+ function probeProvider(command) {
18
+ const checkedAtMs = Date.now();
19
+ try {
20
+ const output = execFileSync(command, ["--version"], {
21
+ encoding: "utf8",
22
+ timeout: 8_000,
23
+ maxBuffer: 256 * 1024,
24
+ stdio: ["ignore", "pipe", "pipe"]
25
+ });
26
+ return {
27
+ available: true,
28
+ status: "ready",
29
+ version: firstOutputLine(output),
30
+ checkedAtMs,
31
+ errorCode: null
32
+ };
33
+ }
34
+ catch (error) {
35
+ const code = errorCode(error);
36
+ return {
37
+ available: false,
38
+ status: code === "ENOENT" ? "unavailable" : "error",
39
+ version: null,
40
+ checkedAtMs,
41
+ errorCode: code === "ENOENT" ? "not_installed" : "probe_failed"
42
+ };
43
+ }
44
+ }
45
+ function firstOutputLine(value) {
46
+ const line = value.split(/\r?\n/).map((item) => item.trim()).find(Boolean);
47
+ return line ? line.slice(0, 120) : null;
48
+ }
49
+ function errorCode(error) {
50
+ if (!error || typeof error !== "object")
51
+ return null;
52
+ const code = error.code;
53
+ return typeof code === "string" ? code : null;
54
+ }
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { getAcaConfigPath, getAcaHome, getCredentialsPath } from "./paths.js";
5
+ export function applyRuntimePathOptions(options) {
6
+ const runtimeHome = options.aicaHome || options.acaHome;
7
+ if (runtimeHome) {
8
+ process.env.AICA_HOME = path.resolve(runtimeHome);
9
+ }
10
+ const configPath = options.aicaConfig || options.acaConfig;
11
+ if (configPath) {
12
+ process.env.AICA_CONFIG = path.resolve(configPath);
13
+ }
14
+ migrateLegacyWorkerFiles();
15
+ }
16
+ /**
17
+ * AICA 与 ACA Server 使用不同运行目录。升级时只迁移 Worker 登录所需文件,
18
+ * 不复制 Server 数据库、证书、日志、PID、聊天目录或 worktree。
19
+ */
20
+ function migrateLegacyWorkerFiles() {
21
+ const legacyHome = path.resolve(process.env.AICA_LEGACY_HOME || path.join(os.homedir(), ".aca"));
22
+ const targetHome = path.resolve(getAcaHome());
23
+ const defaultAicaHome = path.resolve(path.join(os.homedir(), ".aica"));
24
+ if (legacyHome === targetHome || targetHome !== defaultAicaHome)
25
+ return;
26
+ if (!process.env.AICA_CONFIG && !process.env.ACA_CONFIG) {
27
+ copyIfMissing(path.join(legacyHome, "config.json"), getAcaConfigPath());
28
+ }
29
+ if (!process.env.AICA_CREDENTIALS && !process.env.ACA_CREDENTIALS) {
30
+ copyIfMissing(path.join(legacyHome, "credentials.json"), getCredentialsPath());
31
+ }
32
+ }
33
+ function copyIfMissing(source, target) {
34
+ if (fs.existsSync(target) || !fs.existsSync(source))
35
+ return;
36
+ fs.mkdirSync(path.dirname(target), { recursive: true });
37
+ fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL);
38
+ }