aiden-runtime 4.8.1 → 4.9.1

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 (100) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +37 -6
  3. package/dist/cli/v4/chatSession.js +53 -13
  4. package/dist/cli/v4/commands/daemon.js +53 -3
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +45 -26
  7. package/dist/cli/v4/commands/help.js +5 -0
  8. package/dist/cli/v4/commands/hooks.js +466 -0
  9. package/dist/cli/v4/commands/hooksSlash.js +33 -0
  10. package/dist/cli/v4/commands/index.js +13 -1
  11. package/dist/cli/v4/commands/mcp.js +89 -1
  12. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  13. package/dist/cli/v4/commands/memory.js +707 -0
  14. package/dist/cli/v4/commands/memorySlash.js +38 -0
  15. package/dist/cli/v4/commands/recovery.js +1 -1
  16. package/dist/cli/v4/commands/skin.js +7 -0
  17. package/dist/cli/v4/commands/theme.js +217 -0
  18. package/dist/cli/v4/commands/trigger.js +1 -1
  19. package/dist/cli/v4/design/tokens.js +52 -4
  20. package/dist/cli/v4/display.js +39 -26
  21. package/dist/cli/v4/replyRenderer.js +6 -5
  22. package/dist/cli/v4/skinEngine.js +67 -0
  23. package/dist/cli/v4/ui/progressBar.js +179 -0
  24. package/dist/cli/v4/util/closestAction.js +48 -0
  25. package/dist/core/v4/aidenAgent.js +45 -2
  26. package/dist/core/v4/daemon/api/runs.js +131 -0
  27. package/dist/core/v4/daemon/bootstrap.js +368 -13
  28. package/dist/core/v4/daemon/db/migrations.js +169 -0
  29. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  30. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  31. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  32. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  33. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  34. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  35. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  36. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  37. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  38. package/dist/core/v4/daemon/triggerBus.js +50 -19
  39. package/dist/core/v4/hooks/auditQuery.js +67 -0
  40. package/dist/core/v4/hooks/dispatcher.js +286 -0
  41. package/dist/core/v4/hooks/index.js +46 -0
  42. package/dist/core/v4/hooks/lifecycle.js +27 -0
  43. package/dist/core/v4/hooks/manifest.js +142 -0
  44. package/dist/core/v4/hooks/registry.js +149 -0
  45. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  46. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  47. package/dist/core/v4/hooks/trust.js +14 -0
  48. package/dist/core/v4/identity/contextManager.js +83 -0
  49. package/dist/core/v4/identity/daemonId.js +85 -0
  50. package/dist/core/v4/identity/enforcement.js +103 -0
  51. package/dist/core/v4/identity/executionContext.js +153 -0
  52. package/dist/core/v4/identity/hookExecution.js +62 -0
  53. package/dist/core/v4/identity/httpContext.js +68 -0
  54. package/dist/core/v4/identity/ids.js +185 -0
  55. package/dist/core/v4/identity/index.js +60 -0
  56. package/dist/core/v4/identity/subprocessContext.js +98 -0
  57. package/dist/core/v4/identity/traceparent.js +114 -0
  58. package/dist/core/v4/logger/index.js +3 -1
  59. package/dist/core/v4/logger/logger.js +28 -1
  60. package/dist/core/v4/logger/redact.js +149 -0
  61. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  62. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  63. package/dist/core/v4/mcp/install/backup.js +78 -0
  64. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  65. package/dist/core/v4/mcp/install/clients.js +203 -0
  66. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  67. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  68. package/dist/core/v4/mcp/install/profiles.js +109 -0
  69. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  70. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  71. package/dist/core/v4/memory/projectRoot.js +76 -0
  72. package/dist/core/v4/memory/reviewer/index.js +162 -0
  73. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  74. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  75. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  76. package/dist/core/v4/memoryManager.js +57 -10
  77. package/dist/core/v4/paths.js +2 -0
  78. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  79. package/dist/core/v4/theme/bundledThemes.js +106 -0
  80. package/dist/core/v4/theme/themeLoader.js +160 -0
  81. package/dist/core/v4/theme/themeRegistry.js +97 -0
  82. package/dist/core/v4/theme/themeWatcher.js +95 -0
  83. package/dist/core/v4/toolRegistry.js +71 -8
  84. package/dist/core/v4/update/depWarningFilter.js +76 -0
  85. package/dist/core/v4/update/executeInstall.js +41 -35
  86. package/dist/core/v4/update/platformInstructions.js +128 -0
  87. package/dist/moat/approvalEngine.js +4 -0
  88. package/dist/moat/memoryGuard.js +8 -1
  89. package/dist/providers/v4/anthropicAdapter.js +10 -4
  90. package/dist/tools/v4/backends/local.js +19 -2
  91. package/dist/tools/v4/sessions/recallSession.js +6 -1
  92. package/package.json +3 -1
  93. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  94. package/themes/default.yaml +52 -0
  95. package/themes/dracula.yaml +32 -0
  96. package/themes/light.yaml +32 -0
  97. package/themes/monochrome.yaml +31 -0
  98. package/themes/tokyo-night.yaml +32 -0
  99. package/dist/core/pluginSystem.js +0 -121
  100. package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/theme/themeWatcher.ts — v4.9.0 Slice 1a.
10
+ *
11
+ * chokidar-backed file watcher for `~/.aiden/theme.yaml`. On change
12
+ * (debounced 200ms to avoid mid-save partial reads), re-parses the
13
+ * file and applies it via the ThemeRegistry. Self-contained — caller
14
+ * just calls `startThemeWatcher(path)` once at REPL start and
15
+ * `stopThemeWatcher()` on quit.
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.startThemeWatcher = startThemeWatcher;
22
+ exports.stopThemeWatcher = stopThemeWatcher;
23
+ exports._isWatcherActive = _isWatcherActive;
24
+ const chokidar_1 = __importDefault(require("chokidar"));
25
+ const node_fs_1 = require("node:fs");
26
+ const themeLoader_1 = require("./themeLoader");
27
+ const themeRegistry_1 = require("./themeRegistry");
28
+ const DEBOUNCE_MS = 200;
29
+ let active = null;
30
+ /**
31
+ * Start watching the theme file. If the file already exists at start,
32
+ * applies it immediately (so the first paint reflects the user's
33
+ * choice). Idempotent — calling twice replaces the previous watcher.
34
+ */
35
+ function startThemeWatcher(themePath, opts = {}) {
36
+ stopThemeWatcher();
37
+ const debounceMs = opts.debounceMs ?? DEBOUNCE_MS;
38
+ const onWarn = opts.onWarn ?? ((m) => console.warn(`[theme] ${m}`));
39
+ // Apply on first-existence so the REPL boots into the user's theme.
40
+ if ((0, node_fs_1.existsSync)(themePath)) {
41
+ const { parsed, warnings } = (0, themeLoader_1.loadThemeFile)(themePath);
42
+ for (const w of warnings)
43
+ onWarn(w);
44
+ if (parsed)
45
+ (0, themeRegistry_1.applyTheme)(parsed, themePath);
46
+ }
47
+ const watcher = chokidar_1.default.watch(themePath, {
48
+ persistent: true,
49
+ awaitWriteFinish: { stabilityThreshold: debounceMs, pollInterval: 50 },
50
+ ignoreInitial: true,
51
+ });
52
+ const state = {
53
+ watcher,
54
+ debounceTimer: null,
55
+ themePath,
56
+ onWarn,
57
+ };
58
+ const reload = () => {
59
+ if (state.debounceTimer)
60
+ clearTimeout(state.debounceTimer);
61
+ state.debounceTimer = setTimeout(() => {
62
+ state.debounceTimer = null;
63
+ if (!(0, node_fs_1.existsSync)(themePath)) {
64
+ (0, themeRegistry_1.resetToDefault)();
65
+ return;
66
+ }
67
+ const { parsed, warnings } = (0, themeLoader_1.loadThemeFile)(themePath);
68
+ for (const w of warnings)
69
+ onWarn(w);
70
+ if (parsed)
71
+ (0, themeRegistry_1.applyTheme)(parsed, themePath);
72
+ }, debounceMs);
73
+ };
74
+ watcher.on('add', reload);
75
+ watcher.on('change', reload);
76
+ watcher.on('unlink', () => {
77
+ if (state.debounceTimer)
78
+ clearTimeout(state.debounceTimer);
79
+ state.debounceTimer = null;
80
+ (0, themeRegistry_1.resetToDefault)();
81
+ });
82
+ watcher.on('error', (err) => onWarn(`watcher error: ${err.message}`));
83
+ active = state;
84
+ }
85
+ /** Stop the active watcher. No-op when none is running. */
86
+ function stopThemeWatcher() {
87
+ if (!active)
88
+ return;
89
+ if (active.debounceTimer)
90
+ clearTimeout(active.debounceTimer);
91
+ void active.watcher.close();
92
+ active = null;
93
+ }
94
+ /** Test helper. */
95
+ function _isWatcherActive() { return active !== null; }
@@ -186,15 +186,40 @@ class ToolRegistry {
186
186
  };
187
187
  }
188
188
  }
189
+ // v4.9.0 Slice 6 — wrap the handler call in a tool span when the
190
+ // daemon foundation is up AND an ExecutionContext is active. NOOP
191
+ // outside daemon mode or outside a runWithContext frame. Lazy
192
+ // require avoids pulling daemon code into the v4 core import
193
+ // graph at module load (would break headless / cli-test imports
194
+ // that don't open a DB).
195
+ //
196
+ // v4.9.0 Slice 12a Phase 3 — inside the tool span, fire
197
+ // `tool.call.pre` + `tool.call.post` hooks via `runToolWithHooks`.
198
+ // Mandatory pre-hook blocks surface as HookBlockedError, caught
199
+ // by the outer try/catch and mapped to a structured error result.
200
+ const dispatch = async (a) => handler.execute(a, context);
201
+ let result;
189
202
  try {
190
- const result = await handler.execute(args, context);
191
- // v4.1.3-repl-polish: lift `degraded` + `degradedReason` from the
192
- // handler's inner result to the outer ToolCallResult so the CLI
193
- // trail row can render the partial-yellow state. Tools opt in by
194
- // setting these on the object they return; without this lift the
195
- // flags would sit on `out.result.degraded` where callbacks.ts
196
- // can't see them. Strict typeof checks avoid promoting truthy-
197
- // but-wrong-shape junk (numbers, strings, nested objects).
203
+ const sliced = sliceSpanShim();
204
+ if (sliced && sliced.db && sliced.hasContext()) {
205
+ const sideEffect = sliced.classifySideEffect(handler);
206
+ const inputFp = sliced.fingerprint(args);
207
+ result = await sliced.withToolSpan(sliced.db, { toolName: call.name, inputFingerprint: inputFp, sideEffectClass: sideEffect }, async (childCtx) => sliced.runToolWithHooks({
208
+ db: sliced.db,
209
+ toolName: call.name,
210
+ toolCallId: call.id,
211
+ args,
212
+ ctx: {
213
+ runId: childCtx.runId,
214
+ traceId: childCtx.traceId,
215
+ spanId: childCtx.spanId,
216
+ parentSpanId: childCtx.parentSpanId,
217
+ },
218
+ }, dispatch));
219
+ }
220
+ else {
221
+ result = await dispatch(args);
222
+ }
198
223
  const inner = result;
199
224
  const out = { id: call.id, name: call.name, result };
200
225
  if (typeof inner?.degraded === 'boolean' && inner.degraded) {
@@ -206,6 +231,17 @@ class ToolRegistry {
206
231
  return out;
207
232
  }
208
233
  catch (err) {
234
+ // v4.9.0 Slice 12a — hook blocks surface as a structured
235
+ // rejection so the model gets the hook's `reason` / `model_message`
236
+ // verbatim instead of a bare exception string.
237
+ if (err instanceof toolHookGate_1.HookBlockedError) {
238
+ return {
239
+ id: call.id,
240
+ name: call.name,
241
+ result: null,
242
+ error: err.modelMessage ?? err.message,
243
+ };
244
+ }
209
245
  const message = err instanceof Error ? err.message : String(err);
210
246
  return { id: call.id, name: call.name, result: null, error: message };
211
247
  }
@@ -213,3 +249,30 @@ class ToolRegistry {
213
249
  }
214
250
  }
215
251
  exports.ToolRegistry = ToolRegistry;
252
+ // v4.9.0 Slice 6 — static imports for the span-shim bridge. Earlier
253
+ // attempts used lazy `require()` to keep daemon code out of the import
254
+ // graph when the test harness doesn't compile it; that path broke
255
+ // under vite-node which doesn't intercept CJS require for `.ts`
256
+ // targets. Static ESM imports work in both vitest + production builds.
257
+ const bootstrap_1 = require("./daemon/bootstrap");
258
+ const spanHelpers_1 = require("./daemon/spans/spanHelpers");
259
+ const identity_1 = require("./identity");
260
+ const toolHookGate_1 = require("./hooks/toolHookGate");
261
+ function classifySideEffectForHandler(h) {
262
+ if (h.riskTier === 'dangerous')
263
+ return 'destructive';
264
+ if (h.mutates === false)
265
+ return 'read';
266
+ if (h.mutates === true)
267
+ return 'mutating';
268
+ return 'read';
269
+ }
270
+ const _toolSpanShim = {
271
+ get db() { return (0, bootstrap_1.getCurrentDaemonDb)(); },
272
+ hasContext: () => (0, identity_1.currentContext)() !== undefined,
273
+ classifySideEffect: classifySideEffectForHandler,
274
+ fingerprint: spanHelpers_1.shortInputFingerprint,
275
+ withToolSpan: spanHelpers_1.withToolSpan,
276
+ runToolWithHooks: toolHookGate_1.runToolWithHooks,
277
+ };
278
+ function sliceSpanShim() { return _toolSpanShim; }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/update/depWarningFilter.ts — v4.9.1.
10
+ *
11
+ * Strip Node `DeprecationWarning` noise (DEP0190 and friends) from
12
+ * npm install stderr before it reaches the user. Filtered lines are
13
+ * preserved in `~/.aiden/logs/update.log` so diagnostics aren't lost.
14
+ *
15
+ * Conservative match — only filters lines that BOTH look like a Node
16
+ * deprecation header AND name a DEP code or the trace-deprecation hint.
17
+ * Legitimate npm errors (EACCES, ENOTFOUND, ENOENT, etc.) pass through.
18
+ */
19
+ var __importDefault = (this && this.__importDefault) || function (mod) {
20
+ return (mod && mod.__esModule) ? mod : { "default": mod };
21
+ };
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.isDeprecationLine = isDeprecationLine;
24
+ exports.splitStderr = splitStderr;
25
+ exports.logFilteredWarnings = logFilteredWarnings;
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = __importDefault(require("node:path"));
28
+ const node_os_1 = __importDefault(require("node:os"));
29
+ /** True iff the line is Node-deprecation chatter we should hide. */
30
+ function isDeprecationLine(line) {
31
+ // Node's deprecation banner header: "(node:NNNN) [DEP0190] DeprecationWarning: ..."
32
+ if (/^\s*\(node:\d+\)\s*(?:\[DEP\d+\]\s*)?DeprecationWarning:/.test(line))
33
+ return true;
34
+ // The follow-up hint Node emits underneath the header.
35
+ if (/Use `node --trace-deprecation/.test(line))
36
+ return true;
37
+ // Bare DEP code lines (some Node versions emit these stand-alone).
38
+ if (/^\s*\[DEP\d+\]/.test(line))
39
+ return true;
40
+ return false;
41
+ }
42
+ /**
43
+ * Split an stderr blob into `kept` (user-visible) and `filtered`
44
+ * (deprecation noise, routed to the diagnostic log).
45
+ */
46
+ function splitStderr(blob) {
47
+ if (!blob)
48
+ return { kept: '', filtered: '' };
49
+ const lines = blob.split(/\r?\n/);
50
+ const kept = [];
51
+ const filtered = [];
52
+ for (const ln of lines) {
53
+ if (isDeprecationLine(ln))
54
+ filtered.push(ln);
55
+ else
56
+ kept.push(ln);
57
+ }
58
+ return { kept: kept.join('\n'), filtered: filtered.join('\n') };
59
+ }
60
+ /**
61
+ * Append filtered lines to `~/.aiden/logs/update.log` with an ISO
62
+ * timestamp header. Fail-open: a log-write failure must NEVER crash
63
+ * the install path.
64
+ */
65
+ async function logFilteredWarnings(filtered, opts = {}) {
66
+ if (!filtered || !filtered.trim())
67
+ return;
68
+ try {
69
+ const root = opts.aidenRoot ?? node_path_1.default.join(node_os_1.default.homedir(), '.aiden');
70
+ const logDir = node_path_1.default.join(root, 'logs');
71
+ await node_fs_1.promises.mkdir(logDir, { recursive: true });
72
+ const entry = `[${new Date().toISOString()}] update.npm-deprecation:\n${filtered}\n\n`;
73
+ await node_fs_1.promises.appendFile(node_path_1.default.join(logDir, 'update.log'), entry, 'utf8');
74
+ }
75
+ catch { /* fail-open */ }
76
+ }
@@ -42,11 +42,18 @@
42
42
  * - No registry probe — call `checkForUpdate` first if you need to
43
43
  * know whether an install is warranted.
44
44
  */
45
+ var __importDefault = (this && this.__importDefault) || function (mod) {
46
+ return (mod && mod.__esModule) ? mod : { "default": mod };
47
+ };
45
48
  Object.defineProperty(exports, "__esModule", { value: true });
46
49
  exports.INSTALL_TIMEOUT_MS = void 0;
47
50
  exports.executeInstall = executeInstall;
48
51
  exports.parseInstalledVersion = parseInstalledVersion;
49
52
  const node_child_process_1 = require("node:child_process");
53
+ const node_os_1 = __importDefault(require("node:os"));
54
+ const depWarningFilter_1 = require("./depWarningFilter");
55
+ const platformInstructions_1 = require("./platformInstructions");
56
+ const progressBar_1 = require("../../../cli/v4/ui/progressBar");
50
57
  /** 90 s wall-clock cap. Generous on cold caches / slow networks. */
51
58
  exports.INSTALL_TIMEOUT_MS = 90000;
52
59
  const DEFAULT_PACKAGE_SPEC = 'aiden-runtime@latest';
@@ -64,6 +71,9 @@ async function executeInstall(opts = {}) {
64
71
  const timeoutMs = opts.timeoutMs ?? exports.INSTALL_TIMEOUT_MS;
65
72
  const packageSpec = opts.packageSpec ?? DEFAULT_PACKAGE_SPEC;
66
73
  const platform = opts.platform ?? process.platform;
74
+ const home = opts.home ?? node_os_1.default.homedir();
75
+ const env = opts.env ?? process.env;
76
+ const onPhase = opts.onPhase ?? ((_p) => { });
67
77
  return new Promise((resolve) => {
68
78
  const args = ['install', '-g', packageSpec];
69
79
  // v4.8.1 Slice 2 — drop `shell: true`. Node 20+ emits
@@ -78,6 +88,7 @@ async function executeInstall(opts = {}) {
78
88
  const spawnOpts = {
79
89
  stdio: ['ignore', 'pipe', 'pipe'],
80
90
  };
91
+ onPhase('spawning');
81
92
  let child;
82
93
  try {
83
94
  child = spawn(cmd, args, spawnOpts);
@@ -92,11 +103,23 @@ async function executeInstall(opts = {}) {
92
103
  }
93
104
  let stdoutBuf = '';
94
105
  let stderrBuf = '';
106
+ // v4.9.1 — parse phase signal off each chunk.
107
+ const tryEmitPhase = (chunk) => {
108
+ for (const ln of chunk.split(/\r?\n/)) {
109
+ const p = (0, progressBar_1.detectNpmPhase)(ln);
110
+ if (p)
111
+ onPhase(p);
112
+ }
113
+ };
95
114
  child.stdout?.on('data', (chunk) => {
96
- stdoutBuf += chunk.toString();
115
+ const s = chunk.toString();
116
+ stdoutBuf += s;
117
+ tryEmitPhase(s);
97
118
  });
98
119
  child.stderr?.on('data', (chunk) => {
99
- stderrBuf += chunk.toString();
120
+ const s = chunk.toString();
121
+ stderrBuf += s;
122
+ tryEmitPhase(s);
100
123
  });
101
124
  // Timeout — kill the child + resolve as a failure with the captured
102
125
  // output so the user sees what npm was doing.
@@ -121,7 +144,11 @@ async function executeInstall(opts = {}) {
121
144
  child.on('close', (code) => {
122
145
  clearTimeout(timer);
123
146
  const stdout = stdoutBuf;
124
- const stderr = stderrBuf;
147
+ // v4.9.1 — strip Node DEP* noise from stderr before any surfacing
148
+ // to the user. Filtered lines land in ~/.aiden/logs/update.log.
149
+ const { kept: stderr, filtered } = (0, depWarningFilter_1.splitStderr)(stderrBuf);
150
+ if (filtered)
151
+ void (0, depWarningFilter_1.logFilteredWarnings)(filtered);
125
152
  const exitCode = code ?? -1;
126
153
  if (timedOut) {
127
154
  resolve({
@@ -134,14 +161,16 @@ async function executeInstall(opts = {}) {
134
161
  }
135
162
  // Permission-denied: surface platform-specific remediations.
136
163
  if (isPermissionDenied(stdout, stderr, exitCode)) {
164
+ onPhase('failed');
137
165
  resolve({
138
166
  success: false,
139
- error: permissionDeniedMessage(platform),
167
+ error: permissionDeniedMessage(platform, home, env),
140
168
  stdout, stderr, exitCode,
141
169
  });
142
170
  return;
143
171
  }
144
172
  if (exitCode !== 0) {
173
+ onPhase('failed');
145
174
  resolve({
146
175
  success: false,
147
176
  error: `Install failed (npm exit ${exitCode}). ` +
@@ -153,6 +182,7 @@ async function executeInstall(opts = {}) {
153
182
  }
154
183
  // Success — parse installed version from npm output. Pattern:
155
184
  // "+ aiden-runtime@4.1.3" or "added 1 package ... aiden-runtime@4.1.3"
185
+ onPhase('installed');
156
186
  const installedVersion = parseInstalledVersion(stdout) ?? parseInstalledVersion(stderr) ?? undefined;
157
187
  resolve({
158
188
  success: true,
@@ -190,38 +220,14 @@ function isPermissionDenied(stdout, stderr, exitCode) {
190
220
  return false;
191
221
  }
192
222
  /**
193
- * Build the platform-specific copy-paste remediation. Provides three
194
- * distinct paths system-wide-with-elevation (Windows admin),
195
- * sudo (macOS/Linux), or user-local-prefix (cross-platform) so the
196
- * user has options without us trying to self-escalate to UAC/sudo
197
- * from inside the running REPL.
223
+ * v4.9.1 — Build the platform-specific copy-paste remediation. Delegates
224
+ * to `platformInstructions.ts` for the heavy lifting so the same builder
225
+ * powers both EPERM remediation + stale-prefix warnings + the future
226
+ * `aiden update --setup-user-prefix` helper.
198
227
  */
199
- function permissionDeniedMessage(platform) {
200
- const userLocal = 'Or use a user-local npm prefix to avoid privileges entirely:\n' +
201
- ' npm config set prefix ~/.npm-global\n' +
202
- ' export PATH=~/.npm-global/bin:$PATH # add to your shell profile\n' +
203
- ' npm install -g aiden-runtime@latest';
204
- if (platform === 'win32') {
205
- return [
206
- 'Install failed: permission denied (npm needs Administrator for global install).',
207
- '',
208
- 'To update manually:',
209
- ' Windows: Open PowerShell as Administrator, then:',
210
- ' npm install -g aiden-runtime@latest',
211
- '',
212
- userLocal,
213
- ].join('\n');
214
- }
215
- // darwin / linux / others — sudo path.
216
- return [
217
- 'Install failed: permission denied (npm needs sudo for global install).',
218
- '',
219
- 'To update manually:',
220
- ' macOS / Linux:',
221
- ' sudo npm install -g aiden-runtime@latest',
222
- '',
223
- userLocal,
224
- ].join('\n');
228
+ function permissionDeniedMessage(platform, home, env) {
229
+ const instr = (0, platformInstructions_1.permissionDeniedInstructions)({ platform, home, env });
230
+ return [instr.headline, '', ...instr.steps].join('\n');
225
231
  }
226
232
  /**
227
233
  * Find the installed version in npm output. Two common patterns:
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/update/platformInstructions.ts — v4.9.1.
10
+ *
11
+ * Per-platform copy-paste remediation text for:
12
+ * - EPERM / EACCES during global npm install
13
+ * - Stale / risky npm prefix detection
14
+ *
15
+ * Branches purely on `process.platform` (and `$SHELL` for unix shell-
16
+ * rc-file recommendations). Shell syntax MUST be correct per-platform:
17
+ * PowerShell on Windows, bash/zsh on Unix. Cross-contamination is a
18
+ * regression (the v4.9.0 bug we're hot-fixing).
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.detectShell = detectShell;
22
+ exports.permissionDeniedInstructions = permissionDeniedInstructions;
23
+ exports.detectStalePrefix = detectStalePrefix;
24
+ /**
25
+ * Detect the user's interactive shell on POSIX. Returns the basename
26
+ * (`zsh` / `bash` / `sh`) or null when undetectable. Pure — env is
27
+ * injected so tests pin a value.
28
+ */
29
+ function detectShell(env = process.env) {
30
+ const sh = env.SHELL;
31
+ if (!sh)
32
+ return null;
33
+ const last = sh.split(/[\\/]/).pop() || '';
34
+ return last.toLowerCase() || null;
35
+ }
36
+ /** rc-file path the user should edit, per shell. POSIX only. */
37
+ function rcFileFor(shell, home) {
38
+ if (shell === 'zsh')
39
+ return `${home}/.zshrc`;
40
+ if (shell === 'bash')
41
+ return `${home}/.bashrc (or ~/.bash_profile on macOS)`;
42
+ return `${home}/.profile`;
43
+ }
44
+ /**
45
+ * Build the EPERM/EACCES remediation. Two options per platform —
46
+ * elevation (one-time) and user-local prefix (permanent, no privs).
47
+ */
48
+ function permissionDeniedInstructions(opts) {
49
+ const env = opts.env ?? process.env;
50
+ if (opts.platform === 'win32') {
51
+ return {
52
+ headline: 'Install failed: permission denied. npm needs Administrator for a global install on Windows.',
53
+ shell: 'powershell',
54
+ steps: [
55
+ 'Option 1 — Run once with elevated privileges:',
56
+ ' • Open PowerShell as Administrator (right-click → "Run as administrator")',
57
+ ' • npm install -g aiden-runtime@latest',
58
+ '',
59
+ 'Option 2 — Permanent: switch npm to a user-local prefix (no admin needed ever again):',
60
+ ' • npm config set prefix "$env:USERPROFILE\\AppData\\Roaming\\npm"',
61
+ ' • [Environment]::SetEnvironmentVariable("Path", "$env:USERPROFILE\\AppData\\Roaming\\npm;" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")',
62
+ ' • Close + reopen PowerShell, then: npm install -g aiden-runtime@latest',
63
+ ],
64
+ };
65
+ }
66
+ // POSIX: darwin / linux / *bsd / etc.
67
+ const shell = detectShell(env);
68
+ const rcFile = rcFileFor(shell, opts.home);
69
+ return {
70
+ headline: `Install failed: permission denied. npm needs sudo for a global install on ${opts.platform}.`,
71
+ shell: shell ?? undefined,
72
+ rcFile,
73
+ steps: [
74
+ 'Option 1 — Run once with elevated privileges:',
75
+ ' • sudo npm install -g aiden-runtime@latest',
76
+ '',
77
+ 'Option 2 — Permanent: switch npm to a user-local prefix (no sudo needed ever again):',
78
+ ` • npm config set prefix "${opts.home}/.npm-global"`,
79
+ ` • echo 'export PATH="${opts.home}/.npm-global/bin:$PATH"' >> ${rcFile}`,
80
+ ` • source ${rcFile}`,
81
+ ' • npm install -g aiden-runtime@latest',
82
+ ],
83
+ };
84
+ }
85
+ /**
86
+ * Stale / risky prefix detection. Returns a warning when the npm
87
+ * `prefix` config points at a location that needs elevation OR is
88
+ * known to cause permission churn. `null` when the prefix is safe.
89
+ *
90
+ * `writable` is the result of a `fs.access` check the caller does
91
+ * before invoking us (we don't want to do filesystem I/O in a pure
92
+ * builder — caller controls the side effect).
93
+ */
94
+ function detectStalePrefix(opts) {
95
+ const env = opts.env ?? process.env;
96
+ const p = opts.prefix;
97
+ // Windows risk: Program Files.
98
+ if (opts.platform === 'win32') {
99
+ if (/^[a-zA-Z]:\\Program Files/i.test(p)) {
100
+ return {
101
+ warning: `npm prefix is "${p}" — global installs here require Administrator every time.`,
102
+ switchSteps: [
103
+ 'Switch to a user-local prefix to avoid the prompt forever:',
104
+ ' • npm config set prefix "$env:USERPROFILE\\AppData\\Roaming\\npm"',
105
+ ' • [Environment]::SetEnvironmentVariable("Path", "$env:USERPROFILE\\AppData\\Roaming\\npm;" + [Environment]::GetEnvironmentVariable("Path", "User"), "User")',
106
+ ' • Close + reopen PowerShell.',
107
+ ],
108
+ };
109
+ }
110
+ return null;
111
+ }
112
+ // POSIX risk: /usr or /usr/local without write access.
113
+ const risky = p === '/usr' || p === '/usr/local' || p.startsWith('/usr/');
114
+ if (risky && !opts.writable) {
115
+ const shell = detectShell(env);
116
+ const rcFile = rcFileFor(shell, opts.home);
117
+ return {
118
+ warning: `npm prefix is "${p}" — global installs here require sudo every time.`,
119
+ switchSteps: [
120
+ 'Switch to a user-local prefix to avoid sudo forever:',
121
+ ` • npm config set prefix "${opts.home}/.npm-global"`,
122
+ ` • echo 'export PATH="${opts.home}/.npm-global/bin:$PATH"' >> ${rcFile}`,
123
+ ` • source ${rcFile}`,
124
+ ],
125
+ };
126
+ }
127
+ return null;
128
+ }
@@ -180,6 +180,10 @@ class ApprovalEngine {
180
180
  * categories are always allowed without consulting any callback.
181
181
  */
182
182
  async checkApproval(req) {
183
+ // v4.9.0 Slice 12b — fire `approval.requested` for any gated call.
184
+ // Read-category short-circuits below; we still fire so observers
185
+ // see ALL approval requests (not just the ones that prompt).
186
+ this.callbacks.onRequested?.(req);
183
187
  if (req.category === 'read') {
184
188
  this.callbacks.onDecision?.(req, 'allow');
185
189
  return true;
@@ -212,7 +212,14 @@ class MemoryGuard {
212
212
  }
213
213
  exports.MemoryGuard = MemoryGuard;
214
214
  function pickFile(snap, file) {
215
- return file === 'user' ? snap.userMd : snap.memoryMd;
215
+ // v4.9.0 Slice 11 — legacy back-compat for 'memory' / 'user' direct
216
+ // reads (Phase 9 + Honesty Enforcement still consult these); new
217
+ // namespaces flow through the generalized `files` map.
218
+ if (file === 'user' && snap.userMd !== undefined)
219
+ return snap.userMd;
220
+ if (file === 'memory' && snap.memoryMd !== undefined)
221
+ return snap.memoryMd;
222
+ return snap.files?.[file]?.content ?? '';
216
223
  }
217
224
  /**
218
225
  * Phase v4.1.2-bug-X: section-aware containment check.
@@ -92,14 +92,14 @@ class AnthropicAdapter {
92
92
  // ── Public: non-streaming ────────────────────────────────────────────────
93
93
  async call(input) {
94
94
  const body = this.buildBody(input, /* streaming */ false);
95
- const reply = await this.dispatch(body, /* streaming */ false, input.signal);
95
+ const reply = await this.dispatch(body, /* streaming */ false, input.signal, input.headers);
96
96
  const json = (await reply.json());
97
97
  return decodeResponse(json);
98
98
  }
99
99
  // ── Public: streaming ────────────────────────────────────────────────────
100
100
  async *callStream(input) {
101
101
  const body = this.buildBody(input, /* streaming */ true);
102
- const reply = await this.dispatch(body, /* streaming */ true, input.signal);
102
+ const reply = await this.dispatch(body, /* streaming */ true, input.signal, input.headers);
103
103
  if (!reply.body) {
104
104
  // Server promised SSE but gave us nothing — fall through to a synthetic
105
105
  // empty done event so the agent loop terminates rather than hangs.
@@ -163,11 +163,17 @@ class AnthropicAdapter {
163
163
  // beta flags, or per-deployment routing tags without forking the adapter.
164
164
  return { ...headers, ...this.extraHeaders };
165
165
  }
166
- async dispatch(body, streaming, externalSignal) {
166
+ async dispatch(body, streaming, externalSignal, outboundHeaders) {
167
167
  // Resolved once per process via the userAgent module's cache, so paying
168
168
  // for the version detection here is cheap on every retry/turn.
169
169
  const userAgent = await (0, userAgent_1.getClaudeCliUserAgent)();
170
- const headers = this.buildHeaders(streaming, userAgent);
170
+ // v4.9.0 Slice 7 — merge caller-supplied outbound headers (e.g.
171
+ // `traceparent`, `X-Aiden-Run-Id`) below the adapter's defaults so
172
+ // they can't override `Authorization` / `anthropic-version` etc.,
173
+ // but above `extraHeaders` so a deliberate per-deployment override
174
+ // still wins.
175
+ const base = this.buildHeaders(streaming, userAgent);
176
+ const headers = outboundHeaders ? { ...outboundHeaders, ...base } : base;
171
177
  const serialised = JSON.stringify(body);
172
178
  const totalTries = this.maxRetries + 1;
173
179
  let lastErr = null;
@@ -20,6 +20,8 @@
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
21
  exports.localBackendExecute = localBackendExecute;
22
22
  const node_child_process_1 = require("node:child_process");
23
+ // v4.9.0 Slice 7 — propagate ExecutionContext into the child via env.
24
+ const identity_1 = require("../../../core/v4/identity");
23
25
  const DEFAULT_TIMEOUT = 30000;
24
26
  async function localBackendExecute(args, cb = {}) {
25
27
  const command = args.command.trim();
@@ -38,14 +40,29 @@ async function localBackendExecute(args, cb = {}) {
38
40
  const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT;
39
41
  const capture = args.captureOutput ?? true;
40
42
  return new Promise((resolve) => {
43
+ // v4.9.0 Slice 7 — when running inside a `runWithContext` frame,
44
+ // stamp AIDEN_* env vars so the child process can reconstitute
45
+ // the same daemon/incarnation/run/trace correlation chain via
46
+ // `readContextFromEnv(process.env)`. Outside a context frame, the
47
+ // env spread is unchanged.
48
+ const ambient = (0, identity_1.currentContext)();
49
+ let baseEnv;
50
+ if (ambient) {
51
+ baseEnv = (0, identity_1.spawnEnvWithContext)(ambient, process.env);
52
+ }
53
+ else {
54
+ // v4.9.0 Slice 8 — report through the enforcement layer.
55
+ (0, identity_1.reportMissingContext)('subprocess', 'shellExec');
56
+ baseEnv = process.env;
57
+ }
41
58
  const child = isWin
42
59
  ? (0, node_child_process_1.spawn)('powershell.exe', ['-NoProfile', '-Command', command], {
43
60
  cwd: args.cwd,
44
- env: { ...process.env, ...(args.env ?? {}) },
61
+ env: { ...baseEnv, ...(args.env ?? {}) },
45
62
  })
46
63
  : (0, node_child_process_1.spawn)('bash', ['-lc', command], {
47
64
  cwd: args.cwd,
48
- env: { ...process.env, ...(args.env ?? {}) },
65
+ env: { ...baseEnv, ...(args.env ?? {}) },
49
66
  });
50
67
  let stdout = '';
51
68
  let stderr = '';