evolcore 0.0.23 → 0.0.24

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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
@@ -1,12 +1,197 @@
1
1
  import path from 'path';
2
- import { fileURLToPath } from 'url';
2
+ import { fileURLToPath, pathToFileURL } from 'url';
3
+ import { createRequire } from 'module';
3
4
  import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
4
5
  import { promisify } from 'util';
5
6
  import fs from 'fs';
6
7
  import { getProcessStartTime, parseCimDate } from './process-introspect.js';
7
8
  import { decodeWindowsOutput } from './windows-output.js';
8
9
  const execFileAsync = promisify(execFile);
10
+ const CLI_PROBE_FAILURE_BACKOFF_MS = 5_000;
11
+ const CLI_PROBE_PACKAGES = {
12
+ codex: '@openai/codex',
13
+ claude: '@anthropic-ai/claude-code',
14
+ gemini: '@google/gemini-cli',
15
+ };
16
+ const cliPathCache = new Map();
17
+ const cliInstallationCache = new Map();
18
+ const cliProbeCache = new Map();
9
19
  export const isWindows = process.platform === 'win32';
20
+ /** Resolve a CLI executable without spawning where/which. */
21
+ export function resolveProbePath(command) {
22
+ const pathKey = `${command}\0${process.cwd()}\0${process.env.PATH ?? ''}\0${process.env.PATHEXT ?? ''}`;
23
+ const cachedPath = cliPathCache.get(pathKey);
24
+ if (cachedPath && fs.existsSync(cachedPath))
25
+ return cachedPath;
26
+ cliPathCache.delete(pathKey);
27
+ const directories = (process.env.PATH ?? '').split(path.delimiter);
28
+ if (isWindows)
29
+ directories.unshift(process.cwd());
30
+ const extensions = isWindows
31
+ ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(ext => /\.(com|exe|bat|cmd)$/i.test(ext))
32
+ : [''];
33
+ for (const directory of directories) {
34
+ for (const extension of extensions) {
35
+ const candidate = path.resolve(directory.replace(/^"|"$/g, ''), command + extension);
36
+ try {
37
+ if (!fs.statSync(candidate).isFile())
38
+ continue;
39
+ fs.accessSync(candidate, isWindows ? fs.constants.F_OK : fs.constants.X_OK);
40
+ cliPathCache.set(pathKey, candidate);
41
+ return candidate;
42
+ }
43
+ catch { /* Continue PATH lookup. */ }
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+ function cliFileIdentity(file) {
49
+ try {
50
+ const real = fs.realpathSync(file);
51
+ const stat = fs.statSync(real);
52
+ return `${real}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
53
+ }
54
+ catch {
55
+ return `${file}:missing`;
56
+ }
57
+ }
58
+ /** Return an installation fingerprint so probes refresh after CLI upgrades. */
59
+ export function probeInstallationIdentity(command, executable) {
60
+ const cacheKey = `${command}\0${executable}`;
61
+ const cached = cliInstallationCache.get(cacheKey);
62
+ if (cached && cached.files.map(cliFileIdentity).join('|') === cached.identity)
63
+ return cached.identity;
64
+ const files = [executable];
65
+ const packageName = CLI_PROBE_PACKAGES[command];
66
+ if (packageName) {
67
+ files.push(path.join(path.dirname(executable), 'node_modules', packageName, 'package.json'));
68
+ try {
69
+ const real = fs.realpathSync(executable);
70
+ const require = createRequire(real);
71
+ const packageFile = require.resolve(`${packageName}/package.json`);
72
+ files.push(packageFile);
73
+ if (command === 'codex') {
74
+ const platformPackage = `@openai/codex-${process.platform}-${process.arch}`;
75
+ const platformFile = createRequire(packageFile).resolve(`${platformPackage}/package.json`);
76
+ files.push(platformFile);
77
+ const arch = process.arch === 'x64' ? 'x86_64' : 'aarch64';
78
+ const target = isWindows ? `${arch}-pc-windows-msvc`
79
+ : process.platform === 'darwin' ? `${arch}-apple-darwin` : `${arch}-unknown-linux-musl`;
80
+ for (const root of [path.dirname(platformFile), path.dirname(packageFile)]) {
81
+ files.push(path.join(root, 'vendor', target, 'bin', isWindows ? 'codex.exe' : 'codex'));
82
+ }
83
+ }
84
+ }
85
+ catch { /* Standalone executables have no npm package. */ }
86
+ }
87
+ const uniqueFiles = [...new Set(files)];
88
+ const identity = uniqueFiles.map(cliFileIdentity).join('|');
89
+ cliInstallationCache.set(cacheKey, { files: uniqueFiles, identity });
90
+ return identity;
91
+ }
92
+ /** Clear CLI probe caches after an in-process CLI installation. */
93
+ export function invalidateCliProbes() {
94
+ cliPathCache.clear();
95
+ cliInstallationCache.clear();
96
+ cliProbeCache.clear();
97
+ }
98
+ function cliProbeTimeouts(command, options) {
99
+ return options.timeouts?.length ? options.timeouts : command === 'codex' ? [3_000, 15_000] : [3_000];
100
+ }
101
+ function prepareCliProbe(command, args) {
102
+ const executable = resolveProbePath(command);
103
+ if (!executable)
104
+ throw Object.assign(new Error(`${command} CLI not found`), { code: 'ENOENT' });
105
+ const identity = `${probeInstallationIdentity(command, executable)}|${process.env.ComSpec ?? ''}`;
106
+ const key = JSON.stringify([command, args]);
107
+ let entry = cliProbeCache.get(key);
108
+ if (!entry || entry.identity !== identity || (entry.retryAt !== undefined && Date.now() >= entry.retryAt)) {
109
+ entry = { identity };
110
+ cliProbeCache.set(key, entry);
111
+ }
112
+ const shim = isWindows && /\.(cmd|bat)$/i.test(executable);
113
+ return { entry, command: shim ? process.env.ComSpec ?? 'cmd.exe' : executable,
114
+ args: shim ? ['/d', '/s', '/c', executable, ...args] : args };
115
+ }
116
+ function failCliProbe(entry, error) {
117
+ entry.error = error;
118
+ entry.retryAt = Date.now() + CLI_PROBE_FAILURE_BACKOFF_MS;
119
+ throw error;
120
+ }
121
+ export function probeCliSync(command, args, options = {}) {
122
+ const request = prepareCliProbe(command, args);
123
+ if (request.entry.output !== undefined)
124
+ return request.entry.output;
125
+ if (request.entry.error !== undefined)
126
+ throw request.entry.error;
127
+ for (const [index, timeout] of cliProbeTimeouts(command, options).entries()) {
128
+ const startedAt = Date.now();
129
+ try {
130
+ return request.entry.output = execFileSync(request.command, request.args, { encoding: 'utf8', timeout, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
131
+ }
132
+ catch (error) {
133
+ options.onFailure?.(args, startedAt, error, timeout);
134
+ if (error.code !== 'ETIMEDOUT' || index === cliProbeTimeouts(command, options).length - 1)
135
+ return failCliProbe(request.entry, error);
136
+ }
137
+ }
138
+ throw new Error('CLI probe exhausted');
139
+ }
140
+ export async function probeCli(command, args, options = {}) {
141
+ const request = prepareCliProbe(command, args);
142
+ if (request.entry.output !== undefined)
143
+ return request.entry.output;
144
+ if (request.entry.error !== undefined)
145
+ throw request.entry.error;
146
+ if (request.entry.pending)
147
+ return request.entry.pending;
148
+ const run = async () => {
149
+ const timeouts = cliProbeTimeouts(command, options);
150
+ for (const [index, timeout] of timeouts.entries()) {
151
+ const startedAt = Date.now();
152
+ try {
153
+ const output = await new Promise((resolve, reject) => {
154
+ const child = execFile(request.command, request.args, { encoding: 'utf8', timeout, windowsHide: true }, (error, stdout, stderr) => {
155
+ if (error) {
156
+ if (error.killed && error.signal === 'SIGTERM' && error.code == null)
157
+ error.code = 'ETIMEDOUT';
158
+ Object.assign(error, { stdout, stderr, path: request.command, spawnargs: request.args });
159
+ reject(error);
160
+ }
161
+ else
162
+ resolve(stdout);
163
+ });
164
+ child.stdin?.end();
165
+ });
166
+ return request.entry.output = output;
167
+ }
168
+ catch (error) {
169
+ options.onFailure?.(args, startedAt, error, timeout);
170
+ if (error.code !== 'ETIMEDOUT' || index === timeouts.length - 1)
171
+ return failCliProbe(request.entry, error);
172
+ }
173
+ }
174
+ throw new Error('CLI probe exhausted');
175
+ };
176
+ request.entry.pending = run();
177
+ try {
178
+ return await request.entry.pending;
179
+ }
180
+ finally {
181
+ request.entry.pending = undefined;
182
+ }
183
+ }
184
+ /** Non-blocking version lookup used by status menus. */
185
+ export function cachedCliVersion(command) {
186
+ try {
187
+ const { entry } = prepareCliProbe(command, ['--version']);
188
+ if (entry.output !== undefined)
189
+ return entry.output.match(/\b\d+\.\d+\.\d+(?:[-+][\w.-]+)?/)?.[0] ?? null;
190
+ void probeCli(command, ['--version']).catch(() => { });
191
+ }
192
+ catch { /* Missing CLI. */ }
193
+ return null;
194
+ }
10
195
  /**
11
196
  * Start a detached EvolCore Node process without creating a console window on
12
197
  * Windows. The child is unref'd here so callers cannot accidentally keep the
@@ -22,6 +207,256 @@ export function spawnDetachedNode(args, env = {}) {
22
207
  child.unref();
23
208
  return child;
24
209
  }
210
+ /**
211
+ * Spawn the restart monitor and wait until it proves it is running.
212
+ *
213
+ * Every caller stops (or kills) the daemon right after this returns, so a
214
+ * monitor that dies while loading leaves nothing running and nobody watching.
215
+ * In a checkout the monitor is executed through tsx, where a syntax error
216
+ * anywhere in its import graph is fatal at load time — and the usual
217
+ * `stdio: 'ignore'` would throw that diagnostic away. So stderr is captured,
218
+ * and success requires the child to still be alive after it had a chance to
219
+ * fail, not merely to have been spawned.
220
+ *
221
+ * `isAliveInRegistry` lets the caller define proof-of-life without dragging the
222
+ * instance registry into this module.
223
+ */
224
+ export async function launchMonitorAndConfirm(args, env, isAliveInRegistry, hasOtherLiveMonitor, timeoutMs = 15_000) {
225
+ const child = spawn(process.execPath, [...args], {
226
+ detached: true,
227
+ stdio: ['ignore', 'ignore', 'pipe'],
228
+ windowsHide: isWindows,
229
+ env: { ...process.env, ...env },
230
+ });
231
+ let stderrTail = '';
232
+ child.stderr?.on('data', (chunk) => {
233
+ stderrTail = (stderrTail + chunk.toString()).slice(-2000);
234
+ });
235
+ // Capturing stderr must not defeat detached-process semantics. A referenced
236
+ // pipe keeps the caller's event loop alive until the monitor exits, which
237
+ // makes restart commands wait for the monitor's full lifetime.
238
+ child.stderr?.unref?.();
239
+ let exitCode = null;
240
+ let spawnError = null;
241
+ child.on('exit', code => { exitCode = code; });
242
+ child.on('error', err => { spawnError = err.message; });
243
+ const childPid = child.pid;
244
+ child.unref();
245
+ if (!childPid)
246
+ return { ok: false, reason: 'spawn 未返回 PID' };
247
+ const deadline = Date.now() + timeoutMs;
248
+ while (Date.now() < deadline) {
249
+ await new Promise(resolve => setTimeout(resolve, 300));
250
+ if (spawnError)
251
+ return { ok: false, reason: spawnError, stderrTail };
252
+ if (exitCode !== null) {
253
+ // Exiting cleanly is legitimate when another monitor already owns this
254
+ // restart (single-instance election) — that one is alive and in charge.
255
+ if (exitCode === 0 && hasOtherLiveMonitor(childPid))
256
+ return { ok: true };
257
+ return { ok: false, reason: `进程已退出,exit=${exitCode}`, stderrTail };
258
+ }
259
+ // Registering means the module graph loaded and the monitor started work.
260
+ if (isAliveInRegistry(childPid))
261
+ return { ok: true };
262
+ }
263
+ // Still running but not yet registered: a slow start is not a failure.
264
+ if (isProcessRunning(childPid))
265
+ return { ok: true };
266
+ return { ok: false, reason: '进程未登记且已消失', stderrTail };
267
+ }
268
+ /**
269
+ * Copy the monitor out of the package directory so an upgrade cannot replace it
270
+ * mid-flight.
271
+ *
272
+ * This is the installed-package counterpart of running the monitor from `src/`
273
+ * in a checkout: `npm install -g` swaps the whole package directory, and the
274
+ * monitor has ~170 lazy `await import()` sites, so a module it has not loaded
275
+ * yet would come from the new version while the rest is still the old one.
276
+ * Staging `dist/` elsewhere keeps its module graph on one consistent version.
277
+ *
278
+ * Only `dist/` and `package.json` are copied (a few MB). `node_modules` is
279
+ * linked, because copying it is hundreds of MB. The link degrades in steps so
280
+ * that no platform is left without a path forward:
281
+ *
282
+ * 1. symlink — `junction` on Windows (works without elevation, unlike a
283
+ * `dir` symlink which needs admin or developer mode), plain dir symlink
284
+ * elsewhere. Junctions cannot span volumes, hence the fallbacks.
285
+ * 2. full copy — slow but always permitted.
286
+ * 3. skip staging — caller runs the monitor in place, exactly as before.
287
+ * Losing the upgrade isolation is much better than losing the restart.
288
+ */
289
+ export function prepareMonitorStage(packageRoot, stageParent) {
290
+ const distSource = path.join(packageRoot, 'dist');
291
+ const modulesSource = path.join(packageRoot, 'node_modules');
292
+ if (!fs.existsSync(distSource)) {
293
+ return { stageDir: null, linkMode: 'none', note: 'dist/ 不存在,跳过 stage' };
294
+ }
295
+ // Clear stages left behind by a monitor that was killed before self-cleanup.
296
+ try {
297
+ const cutoff = Date.now() - 60 * 60 * 1000;
298
+ for (const entry of fs.readdirSync(stageParent, { withFileTypes: true })) {
299
+ if (!entry.isDirectory())
300
+ continue;
301
+ const full = path.join(stageParent, entry.name);
302
+ if (fs.statSync(full).mtimeMs < cutoff)
303
+ fs.rmSync(full, { recursive: true, force: true });
304
+ }
305
+ }
306
+ catch { /* stageParent may not exist yet */ }
307
+ const stageDir = path.join(stageParent, `${process.pid}-${Date.now()}`);
308
+ try {
309
+ fs.mkdirSync(stageDir, { recursive: true });
310
+ fs.cpSync(distSource, path.join(stageDir, 'dist'), { recursive: true });
311
+ const pkgJson = path.join(packageRoot, 'package.json');
312
+ if (fs.existsSync(pkgJson))
313
+ fs.copyFileSync(pkgJson, path.join(stageDir, 'package.json'));
314
+ }
315
+ catch (error) {
316
+ try {
317
+ fs.rmSync(stageDir, { recursive: true, force: true });
318
+ }
319
+ catch { }
320
+ return { stageDir: null, linkMode: 'none', note: `复制 dist/ 失败:${error.message}` };
321
+ }
322
+ if (!fs.existsSync(modulesSource)) {
323
+ // Dependencies hoisted elsewhere; nothing to link, node resolves upward.
324
+ return { stageDir, linkMode: 'none' };
325
+ }
326
+ const stageModules = path.join(stageDir, 'node_modules');
327
+ try {
328
+ fs.symlinkSync(modulesSource, stageModules, isWindows ? 'junction' : 'dir');
329
+ // A junction to another volume is created but unusable, so prove it reads.
330
+ fs.readdirSync(stageModules);
331
+ return { stageDir, linkMode: 'symlink' };
332
+ }
333
+ catch (linkError) {
334
+ try {
335
+ fs.rmSync(stageModules, { recursive: true, force: true });
336
+ }
337
+ catch { }
338
+ try {
339
+ fs.cpSync(modulesSource, stageModules, { recursive: true });
340
+ return {
341
+ stageDir,
342
+ linkMode: 'copy',
343
+ note: `链接 node_modules 失败(${linkError.code ?? 'unknown'}),已改为完整复制`,
344
+ };
345
+ }
346
+ catch (copyError) {
347
+ try {
348
+ fs.rmSync(stageDir, { recursive: true, force: true });
349
+ }
350
+ catch { }
351
+ return {
352
+ stageDir: null,
353
+ linkMode: 'none',
354
+ note: `无法准备 stage(link: ${linkError.message}; copy: ${copyError.message})`,
355
+ };
356
+ }
357
+ }
358
+ }
359
+ /** Remove a staging directory created by prepareMonitorStage. */
360
+ export function cleanupMonitorStage(stageDir) {
361
+ try {
362
+ // Remove the link itself, never what it points at.
363
+ const modules = path.join(stageDir, 'node_modules');
364
+ const stat = fs.lstatSync(modules, { throwIfNoEntry: false });
365
+ if (stat?.isSymbolicLink())
366
+ fs.unlinkSync(modules);
367
+ }
368
+ catch { /* fall through to the recursive removal */ }
369
+ try {
370
+ fs.rmSync(stageDir, { recursive: true, force: true });
371
+ }
372
+ catch { }
373
+ }
374
+ /** True when the current process may write into `dir`. */
375
+ export function isDirWritable(dir) {
376
+ try {
377
+ fs.accessSync(dir, fs.constants.W_OK);
378
+ return true;
379
+ }
380
+ catch {
381
+ return false;
382
+ }
383
+ }
384
+ /**
385
+ * Resolve the launch argv for the restart monitor, staging it out of the
386
+ * package directory when that package can be replaced underneath it.
387
+ *
388
+ * Checkout: `resolveCliEntry` already points at `src/` via tsx, which no build
389
+ * can delete — nothing to stage.
390
+ *
391
+ * Installed: the monitor lives in the very directory `npm install -g` swaps,
392
+ * so it is copied to `stageParent` first and told where the real package is
393
+ * through `EVOLCORE_TARGET_PACKAGE_ROOT`. If staging cannot be done we fall
394
+ * back to launching in place: losing upgrade isolation is far better than
395
+ * losing the ability to restart.
396
+ */
397
+ export function resolveMonitorLaunch(packageRoot, stageParent) {
398
+ const resolved = resolveCliEntry(packageRoot);
399
+ if (resolved.usingSource) {
400
+ return { args: resolved.args, entry: resolved.entry, env: {}, stageDir: null };
401
+ }
402
+ const stage = prepareMonitorStage(packageRoot, stageParent);
403
+ if (!stage.stageDir) {
404
+ return {
405
+ args: resolved.args,
406
+ entry: resolved.entry,
407
+ env: {},
408
+ stageDir: null,
409
+ note: stage.note ? `${stage.note};本次未启用 stage 隔离` : undefined,
410
+ };
411
+ }
412
+ const stagedEntry = path.join(stage.stageDir, 'dist', 'cli', 'index.js');
413
+ if (!fs.existsSync(stagedEntry)) {
414
+ cleanupMonitorStage(stage.stageDir);
415
+ return { args: resolved.args, entry: resolved.entry, env: {}, stageDir: null, note: 'stage 缺少入口,已退回原地启动' };
416
+ }
417
+ return {
418
+ args: [stagedEntry],
419
+ entry: stagedEntry,
420
+ env: {
421
+ EVOLCORE_TARGET_PACKAGE_ROOT: packageRoot,
422
+ EVOLCORE_MONITOR_STAGE: stage.stageDir,
423
+ },
424
+ stageDir: stage.stageDir,
425
+ note: stage.note,
426
+ };
427
+ }
428
+ /**
429
+ * Resolve how to launch the EvolCore CLI (`restart`, `restart-monitor`, ...).
430
+ *
431
+ * In a development checkout the compiled `dist/` is deleted and rebuilt by
432
+ * `npm run build`, so anything spawned from `dist/cli/index.js` disappears
433
+ * exactly when a build is in flight. The restart monitor is the watchdog that
434
+ * has to outlive that window, so it must not live inside the artifact it
435
+ * supervises: when sources and tsx are both available we run the monitor
436
+ * straight from `src/`, mirroring what `bin/ec.js` already does for the CLI.
437
+ *
438
+ * An npm-installed package ships `dist/` without `src/` and without tsx (a
439
+ * devDependency), so both checks fail and the caller keeps the previous
440
+ * behaviour byte for byte.
441
+ */
442
+ export function resolveCliEntry(packageRoot) {
443
+ const sourceEntry = path.join(packageRoot, 'src', 'cli', 'index.ts');
444
+ const distEntry = path.join(packageRoot, 'dist', 'cli', 'index.js');
445
+ if (fs.existsSync(sourceEntry)) {
446
+ // tsx is a devDependency: present in a checkout, absent from the published
447
+ // package. Resolve it relative to the target root so `ec mode` switching
448
+ // between roots cannot borrow the wrong copy.
449
+ try {
450
+ const require = createRequire(path.join(packageRoot, 'package.json'));
451
+ const tsxUrl = pathToFileURL(require.resolve('tsx')).href;
452
+ return { args: ['--import', tsxUrl, sourceEntry], usingSource: true, entry: sourceEntry };
453
+ }
454
+ catch {
455
+ // tsx missing (e.g. `npm prune --production`): fall through to dist.
456
+ }
457
+ }
458
+ return { args: [distEntry], usingSource: false, entry: distEntry };
459
+ }
25
460
  const ENCODE_PATH_MAX = 200;
26
461
  function encodePathHash(s) {
27
462
  let h = 0;
@@ -38,6 +38,7 @@ export var ErrorType;
38
38
  ErrorType["STREAM_ERROR"] = "stream_error";
39
39
  ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
40
40
  ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
41
+ ErrorType["SESSION_UNAVAILABLE"] = "session_unavailable";
41
42
  ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
42
43
  ErrorType["CLAUDE_STARTUP_ARGUMENTS_TOO_LONG"] = "claude_startup_arguments_too_long";
43
44
  ErrorType["UNKNOWN"] = "unknown";
@@ -278,6 +279,9 @@ function hasRetryableHttpStatus(text) {
278
279
  }
279
280
  export function classifyError(error) {
280
281
  const msg = (error?.message || '').toLowerCase();
282
+ if (error?.code === 'TOPIC_BACKEND_UNAVAILABLE') {
283
+ return ErrorType.SESSION_UNAVAILABLE;
284
+ }
281
285
  // Startup-size failures are deterministic local failures and must win over
282
286
  // dictionary rules that would otherwise turn them into generic retries.
283
287
  if (isClaudeStartupArgumentsTooLong(error)) {
@@ -380,20 +384,23 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
380
384
  }
381
385
  // 回退到原有的错误消息匹配逻辑
382
386
  const msg = error?.message || String(error);
387
+ const warnPrefix = includeEmoji ? '⚠️ ' : '';
388
+ const errPrefix = includeEmoji ? '❌ ' : '';
383
389
  // Codex app-server reports this when another app-server still owns the
384
390
  // thread writer. Surface the handoff condition instead of hiding it behind
385
391
  // the generic "processing failed" message.
386
392
  if (error?.code === 'THREAD_ACTIVE_WRITER' || /already has an active writer/i.test(msg)) {
387
- const prefix = includeEmoji ? '⚠️ ' : '';
388
- return `${prefix}Codex 会话正在由另一个执行通道占用,请稍后重试(THREAD_ACTIVE_WRITER)`;
393
+ return `${warnPrefix}Codex 会话正在由另一个执行通道占用,请稍后重试(THREAD_ACTIVE_WRITER)`;
394
+ }
395
+ if (error?.code === 'TOPIC_BACKEND_UNAVAILABLE'
396
+ || /bound topic backend is unavailable/i.test(msg)) {
397
+ return `${warnPrefix}当前会话已失效,请发送 /renew。`;
389
398
  }
390
399
  // 字典优先 — 命中且有自定义消息则直接返回
391
400
  const rule = matchErrorRule(msg.toLowerCase());
392
401
  if (rule?.message)
393
402
  return rule.message;
394
403
  // 内置兜底规则(结构性错误)
395
- const warnPrefix = includeEmoji ? '⚠️ ' : '';
396
- const errPrefix = includeEmoji ? '❌ ' : '';
397
404
  if (msg.includes('CONTEXT_COMPACT_FAILED') || isContextTooLongText(msg)) {
398
405
  const detail = msg.match(/^CONTEXT_COMPACT_FAILED:\s*(.+)$/)?.[1]?.trim();
399
406
  return detail
@@ -1,20 +1,23 @@
1
- {"model":"claude-opus-4-8","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5}
2
- {"model":"claude-opus-4-7","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5}
3
- {"model":"claude-opus-4-6","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5}
4
- {"model":"claude-sonnet-4-6","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":3,"price_output":15,"price_cache_creation":3.75,"price_cache_read":0.3}
5
- {"model":"claude-haiku-4-5-20251001","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":0.8,"price_output":4,"price_cache_creation":1,"price_cache_read":0.08}
6
- {"model":"gpt-5.6-sol","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":5,"price_output":30,"price_cache_creation":6.25,"price_cache_read":0.5,"tiers":[{"up_to_input_tokens":272000,"price_input":5,"price_output":30,"price_cache_creation":6.25,"price_cache_read":0.5},{"up_to_input_tokens":null,"price_input":10,"price_output":45,"price_cache_creation":12.5,"price_cache_read":1}]}
7
- {"model":"gpt-5.6-terra","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":2.5,"price_output":15,"price_cache_creation":3.125,"price_cache_read":0.25,"tiers":[{"up_to_input_tokens":272000,"price_input":2.5,"price_output":15,"price_cache_creation":3.125,"price_cache_read":0.25},{"up_to_input_tokens":null,"price_input":5,"price_output":22.5,"price_cache_creation":6.25,"price_cache_read":0.5}]}
8
- {"model":"gpt-5.6-luna","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":1,"price_output":6,"price_cache_creation":1.25,"price_cache_read":0.1,"tiers":[{"up_to_input_tokens":272000,"price_input":1,"price_output":6,"price_cache_creation":1.25,"price_cache_read":0.1},{"up_to_input_tokens":null,"price_input":2,"price_output":9,"price_cache_creation":2.5,"price_cache_read":0.2}]}
9
- {"model":"gpt-5.5","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":5,"price_output":30,"price_cache_creation":0,"price_cache_read":0.5,"tiers":[{"up_to_input_tokens":272000,"price_input":5,"price_output":30,"price_cache_creation":0,"price_cache_read":0.5},{"up_to_input_tokens":null,"price_input":10,"price_output":45,"price_cache_creation":0,"price_cache_read":1}]}
10
- {"model":"gpt-5.4","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":2.5,"price_output":15,"price_cache_creation":0,"price_cache_read":0.25,"tiers":[{"up_to_input_tokens":272000,"price_input":2.5,"price_output":15,"price_cache_creation":0,"price_cache_read":0.25},{"up_to_input_tokens":null,"price_input":5,"price_output":22.5,"price_cache_creation":0,"price_cache_read":0.5}]}
11
- {"model":"gpt-5.3-codex","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":1.75,"price_output":14,"price_cache_creation":0,"price_cache_read":0.175}
12
- {"model":"deepseek-v4-pro","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.5,"price_cache_miss":2,"price_output":8}
13
- {"model":"deepseek-v4-flash","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.1,"price_cache_miss":0.5,"price_output":2}
14
- {"model":"deepseek-v3.2","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.5,"price_cache_miss":2,"price_output":8}
15
- {"model":"kimi-k2.6","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":2,"price_output":8,"price_cache_creation":0,"price_cache_read":0}
16
- {"model":"kimi-k2.5","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":2,"price_output":8,"price_cache_creation":0,"price_cache_read":0}
17
- {"model":"glm-5.1","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":5,"price_output":20,"price_cache_creation":0,"price_cache_read":0}
18
- {"model":"glm-5","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":5,"price_output":20,"price_cache_creation":0,"price_cache_read":0}
19
- {"model":"glm-4.7","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":1,"price_output":5,"price_cache_creation":0,"price_cache_read":0}
20
- {"model":"MiniMax-M2.7","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":1,"price_output":5,"price_cache_creation":0,"price_cache_read":0}
1
+ {"model":"claude-opus-4-8","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5,"context_window":1000000,"max_input_tokens":900000,"max_output_tokens":32768,"supports_cache":true,"supports_vision":true}
2
+ {"model":"claude-opus-4-7","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5,"context_window":1000000,"max_input_tokens":900000,"max_output_tokens":32768,"supports_cache":true,"supports_vision":true}
3
+ {"model":"claude-opus-4-6","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":15,"price_output":75,"price_cache_creation":18.75,"price_cache_read":1.5,"context_window":1000000,"max_input_tokens":900000,"max_output_tokens":32768,"supports_cache":true,"supports_vision":true}
4
+ {"model":"claude-sonnet-4-6","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":3,"price_output":15,"price_cache_creation":3.75,"price_cache_read":0.3,"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384,"supports_cache":true,"supports_vision":true}
5
+ {"model":"claude-haiku-4-5-20251001","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":0.8,"price_output":4,"price_cache_creation":1,"price_cache_read":0.08,"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384,"supports_cache":true,"supports_vision":false}
6
+ {"model":"gpt-5.6-sol","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":5,"price_output":30,"price_cache_creation":6.25,"price_cache_read":0.5,"tiers":[{"up_to_input_tokens":272000,"price_input":5,"price_output":30,"price_cache_creation":6.25,"price_cache_read":0.5},{"up_to_input_tokens":null,"price_input":10,"price_output":45,"price_cache_creation":12.5,"price_cache_read":1}],"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384}
7
+ {"model":"gpt-5.6-terra","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":2.5,"price_output":15,"price_cache_creation":3.125,"price_cache_read":0.25,"tiers":[{"up_to_input_tokens":272000,"price_input":2.5,"price_output":15,"price_cache_creation":3.125,"price_cache_read":0.25},{"up_to_input_tokens":null,"price_input":5,"price_output":22.5,"price_cache_creation":6.25,"price_cache_read":0.5}],"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384}
8
+ {"model":"gpt-5.6-luna","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":1,"price_output":6,"price_cache_creation":1.25,"price_cache_read":0.1,"tiers":[{"up_to_input_tokens":272000,"price_input":1,"price_output":6,"price_cache_creation":1.25,"price_cache_read":0.1},{"up_to_input_tokens":null,"price_input":2,"price_output":9,"price_cache_creation":2.5,"price_cache_read":0.2}],"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384}
9
+ {"model":"gpt-5.5","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":5,"price_output":30,"price_cache_creation":0,"price_cache_read":0.5,"tiers":[{"up_to_input_tokens":272000,"price_input":5,"price_output":30,"price_cache_creation":0,"price_cache_read":0.5},{"up_to_input_tokens":null,"price_input":10,"price_output":45,"price_cache_creation":0,"price_cache_read":1}],"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384}
10
+ {"model":"gpt-5.4","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":2.5,"price_output":15,"price_cache_creation":0,"price_cache_read":0.25,"tiers":[{"up_to_input_tokens":272000,"price_input":2.5,"price_output":15,"price_cache_creation":0,"price_cache_read":0.25},{"up_to_input_tokens":null,"price_input":5,"price_output":22.5,"price_cache_creation":0,"price_cache_read":0.5}],"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384}
11
+ {"model":"gpt-5.3-codex","effective_from":0,"billing_fn":"per_token_v1","currency":"USD","price_input":1.75,"price_output":14,"price_cache_creation":0,"price_cache_read":0.175,"context_window":200000,"max_input_tokens":180000,"max_output_tokens":16384}
12
+ {"model":"deepseek-v4-pro","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.5,"price_cache_miss":2,"price_output":8,"context_window":1000000,"max_input_tokens":950000,"max_output_tokens":8192,"supports_cache":true}
13
+ {"model":"deepseek-v4-flash","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.1,"price_cache_miss":0.5,"price_output":2,"context_window":1000000,"max_input_tokens":950000,"max_output_tokens":8192,"supports_cache":true}
14
+ {"model":"deepseek-v3.2","effective_from":0,"billing_fn":"per_token_deepseek_v1","currency":"CNY","price_cache_hit":0.5,"price_cache_miss":2,"price_output":8,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192,"supports_cache":true}
15
+ {"model":"kimi-k2.6","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":2,"price_output":8,"price_cache_creation":0,"price_cache_read":0,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192}
16
+ {"model":"kimi-k2.5","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":2,"price_output":8,"price_cache_creation":0,"price_cache_read":0,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192}
17
+ {"model":"glm-5.1","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":5,"price_output":20,"price_cache_creation":0,"price_cache_read":0,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192}
18
+ {"model":"glm-5","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":5,"price_output":20,"price_cache_creation":0,"price_cache_read":0,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192}
19
+ {"model":"glm-4.7","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":1,"price_output":5,"price_cache_creation":0,"price_cache_read":0,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192}
20
+ {"model":"MiniMax-M2.7","effective_from":0,"billing_fn":"per_token_v1","currency":"CNY","price_input":1,"price_output":5,"price_cache_creation":0,"price_cache_read":0,"context_window":128000,"max_input_tokens":120000,"max_output_tokens":8192}
21
+ {"model":"claude-opus-5","effective_from":0,"context_window":1000000,"max_input_tokens":900000,"max_output_tokens":131072,"supports_cache":true,"supports_vision":true}
22
+ {"model":"claude-sonnet-5","effective_from":0,"context_window":1000000,"max_input_tokens":900000,"max_output_tokens":131072,"supports_cache":true,"supports_vision":true}
23
+ {"model":"claude-fable-5","effective_from":0,"context_window":1000000,"max_input_tokens":900000,"max_output_tokens":131072,"supports_cache":true,"supports_vision":true}
@@ -14,6 +14,26 @@ import { getPackageRoot } from '../paths.js';
14
14
  import { isWindows } from './cross-platform.js';
15
15
  const execFileAsync = promisify(execFile);
16
16
  // ── npm install -g (shared) ────────────────────────────────────────────────
17
+ /**
18
+ * Can we run sudo without a password prompt?
19
+ *
20
+ * `sudo -n` never prompts: it exits non-zero instead when credentials are
21
+ * needed. That matters because upgrades run inside a detached monitor with no
22
+ * tty — a plain `sudo` there would block until the 180s timeout rather than
23
+ * fail. NOPASSWD rules and an unexpired credential cache both pass this check,
24
+ * so the cases where sudo can actually succeed are preserved.
25
+ */
26
+ async function canSudoNonInteractively() {
27
+ if (isWindows)
28
+ return false;
29
+ try {
30
+ await execFileAsync('sudo', ['-n', 'true'], { timeout: 5000 });
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
17
37
  export async function npmInstallGlobal(pkg, options = {}) {
18
38
  let cmd;
19
39
  let args;
@@ -41,7 +61,10 @@ export async function npmInstallGlobal(pkg, options = {}) {
41
61
  if (isWindows) {
42
62
  throw new Error('权限不足。请以管理员身份运行 PowerShell 或 CMD,然后重试');
43
63
  }
44
- await execFileAsync('sudo', ['npm', 'install', '-g', pkg], {
64
+ if (!await canSudoNonInteractively()) {
65
+ throw new Error(`权限不足,且 sudo 需要密码(当前进程无法交互输入)。请手动执行: sudo npm install -g ${pkg}`);
66
+ }
67
+ await execFileAsync('sudo', ['-n', 'npm', 'install', '-g', pkg], {
45
68
  timeout: options.timeoutMs ?? 180000,
46
69
  windowsHide: false,
47
70
  signal: options.signal,
@@ -693,7 +693,7 @@ export function configureWindowsAutostart(enabled, runtimeRoot) {
693
693
  '/TN', WINDOWS_AUTOSTART_TASK_NAME,
694
694
  '/SC', 'ONLOGON',
695
695
  '/TR', taskCommand(scriptPath),
696
- '/RL', 'LIMITED',
696
+ '/RL', 'HIGHEST',
697
697
  '/F',
698
698
  ];
699
699
  const result = runSchtasks(args);
@@ -58,6 +58,8 @@
58
58
  "scopes": ["agent"],
59
59
  "constraints": { "targetCurrentAgentOnly": true, "requireAgentAdmin": true }
60
60
  },
61
+ "ec.aid.lookup": { "allow": true, "scopes": ["agent"] },
62
+ "ec.aid.agentmd.get": { "allow": true, "scopes": ["agent"] },
61
63
  "ec.aid.agentmd.put": {
62
64
  "allow": true,
63
65
  "dangerous": true,