aiden-runtime 4.9.0 → 4.9.2

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 (44) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/v4/aidenCLI.js +2 -2
  3. package/dist/cli/v4/aidenPrompt.js +12 -0
  4. package/dist/cli/v4/chatSession.js +43 -17
  5. package/dist/cli/v4/commands/channel.js +4 -6
  6. package/dist/cli/v4/commands/cron.js +6 -1
  7. package/dist/cli/v4/commands/daemon.js +6 -1
  8. package/dist/cli/v4/commands/daemonDoctor.js +6 -6
  9. package/dist/cli/v4/commands/daemonStatus.js +46 -27
  10. package/dist/cli/v4/commands/help.js +3 -0
  11. package/dist/cli/v4/commands/hooks.js +39 -1
  12. package/dist/cli/v4/commands/hooksSlash.js +33 -0
  13. package/dist/cli/v4/commands/index.js +9 -1
  14. package/dist/cli/v4/commands/mcp.js +2 -2
  15. package/dist/cli/v4/commands/memory.js +6 -1
  16. package/dist/cli/v4/commands/memorySlash.js +38 -0
  17. package/dist/cli/v4/commands/plugins.js +4 -6
  18. package/dist/cli/v4/commands/trigger.js +18 -18
  19. package/dist/cli/v4/confirmPrompt.js +67 -0
  20. package/dist/cli/v4/ui/progressBar.js +179 -0
  21. package/dist/cli/v4/util/closestAction.js +48 -0
  22. package/dist/core/v4/daemon/db/migrations.js +398 -398
  23. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +10 -10
  24. package/dist/core/v4/daemon/incarnationStore.js +9 -9
  25. package/dist/core/v4/daemon/runs/attemptStore.js +8 -8
  26. package/dist/core/v4/daemon/runs/reclaim.js +12 -12
  27. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +19 -19
  28. package/dist/core/v4/daemon/spans/spanStore.js +14 -14
  29. package/dist/core/v4/daemon/triggerBus.js +61 -61
  30. package/dist/core/v4/hooks/auditQuery.js +11 -11
  31. package/dist/core/v4/hooks/dispatcher.js +13 -13
  32. package/dist/core/v4/hooks/registry.js +8 -8
  33. package/dist/core/v4/mcp/transport.js +9 -9
  34. package/dist/core/v4/update/depWarningFilter.js +76 -0
  35. package/dist/core/v4/update/executeInstall.js +70 -53
  36. package/dist/core/v4/update/platformInstructions.js +128 -0
  37. package/dist/core/v4/update/recoveryScript.js +70 -0
  38. package/dist/core/v4/util/spawnCommand.js +151 -0
  39. package/package.json +1 -1
  40. package/themes/default.yaml +52 -52
  41. package/themes/dracula.yaml +32 -32
  42. package/themes/light.yaml +32 -32
  43. package/themes/monochrome.yaml +31 -31
  44. package/themes/tokyo-night.yaml +32 -32
@@ -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,20 @@
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");
57
+ const spawnCommand_1 = require("../util/spawnCommand");
58
+ const recoveryScript_1 = require("./recoveryScript");
50
59
  /** 90 s wall-clock cap. Generous on cold caches / slow networks. */
51
60
  exports.INSTALL_TIMEOUT_MS = 90000;
52
61
  const DEFAULT_PACKAGE_SPEC = 'aiden-runtime@latest';
@@ -64,39 +73,64 @@ async function executeInstall(opts = {}) {
64
73
  const timeoutMs = opts.timeoutMs ?? exports.INSTALL_TIMEOUT_MS;
65
74
  const packageSpec = opts.packageSpec ?? DEFAULT_PACKAGE_SPEC;
66
75
  const platform = opts.platform ?? process.platform;
76
+ const home = opts.home ?? node_os_1.default.homedir();
77
+ const env = opts.env ?? process.env;
78
+ const onPhase = opts.onPhase ?? ((_p) => { });
67
79
  return new Promise((resolve) => {
68
80
  const args = ['install', '-g', packageSpec];
69
- // v4.8.1 Slice 2 — drop `shell: true`. Node 20+ emits
70
- // `DeprecationWarning: Passing args to a child process with shell
71
- // option true can lead to security vulnerabilities` whenever
72
- // shell:true is paired with an args array. We don't need the
73
- // shell either — on Windows we spawn `npm.cmd` explicitly (the
74
- // shim that PATHEXT would otherwise resolve to); on POSIX we
75
- // spawn `npm` directly. No user input flows into argv on either
76
- // path so the prior shell-resolution wasn't load-bearing.
77
- const cmd = platform === 'win32' ? 'npm.cmd' : 'npm';
78
- const spawnOpts = {
79
- stdio: ['ignore', 'pipe', 'pipe'],
80
- };
81
+ // v4.9.2 — route through the shared spawnCommand helper. On
82
+ // Windows it wraps `npm.cmd` through `cmd.exe /d /s /c` with
83
+ // escaped args (no shell:true no argument injection; no plain
84
+ // .cmd spawn no Node 20+ EINVAL). On Unix it's a direct spawn.
85
+ onPhase('spawning');
81
86
  let child;
82
87
  try {
83
- child = spawn(cmd, args, spawnOpts);
88
+ const r = (0, spawnCommand_1.spawnCommand)('npm', args, {
89
+ stdio: ['ignore', 'pipe', 'pipe'],
90
+ platform,
91
+ spawnImpl: spawn,
92
+ });
93
+ child = r.child;
84
94
  }
85
95
  catch (err) {
86
- resolve({
87
- success: false,
88
- error: `Could not launch npm: ${err.message}. ` +
89
- `Run \`npm install -g aiden-runtime@latest\` manually.`,
90
- });
96
+ // Synchronous spawn failure (helper crash, cmd.exe missing,
97
+ // invalid argv). Drop a recovery script the user can run by
98
+ // hand and report its path.
99
+ (async () => {
100
+ let recoveryPath = null;
101
+ try {
102
+ recoveryPath = await (0, recoveryScript_1.writeRecoveryScript)({ platform, home, packageSpec });
103
+ }
104
+ catch { /* best-effort */ }
105
+ resolve({
106
+ success: false,
107
+ error: `Could not launch npm: ${err.message}. ` +
108
+ (recoveryPath
109
+ ? `A recovery script was written to ${recoveryPath} — run it to complete the install.`
110
+ : `Run \`npm install -g ${packageSpec}\` manually.`),
111
+ });
112
+ })();
91
113
  return;
92
114
  }
93
115
  let stdoutBuf = '';
94
116
  let stderrBuf = '';
117
+ // v4.9.1 — parse phase signal off each chunk.
118
+ const tryEmitPhase = (chunk) => {
119
+ for (const ln of chunk.split(/\r?\n/)) {
120
+ const p = (0, progressBar_1.detectNpmPhase)(ln);
121
+ if (p)
122
+ onPhase(p);
123
+ }
124
+ };
95
125
  child.stdout?.on('data', (chunk) => {
96
- stdoutBuf += chunk.toString();
126
+ const s = chunk.toString();
127
+ stdoutBuf += s;
128
+ tryEmitPhase(s);
97
129
  });
98
130
  child.stderr?.on('data', (chunk) => {
99
- stderrBuf += chunk.toString();
131
+ const s = chunk.toString();
132
+ stderrBuf += s;
133
+ tryEmitPhase(s);
100
134
  });
101
135
  // Timeout — kill the child + resolve as a failure with the captured
102
136
  // output so the user sees what npm was doing.
@@ -121,7 +155,11 @@ async function executeInstall(opts = {}) {
121
155
  child.on('close', (code) => {
122
156
  clearTimeout(timer);
123
157
  const stdout = stdoutBuf;
124
- const stderr = stderrBuf;
158
+ // v4.9.1 — strip Node DEP* noise from stderr before any surfacing
159
+ // to the user. Filtered lines land in ~/.aiden/logs/update.log.
160
+ const { kept: stderr, filtered } = (0, depWarningFilter_1.splitStderr)(stderrBuf);
161
+ if (filtered)
162
+ void (0, depWarningFilter_1.logFilteredWarnings)(filtered);
125
163
  const exitCode = code ?? -1;
126
164
  if (timedOut) {
127
165
  resolve({
@@ -134,14 +172,16 @@ async function executeInstall(opts = {}) {
134
172
  }
135
173
  // Permission-denied: surface platform-specific remediations.
136
174
  if (isPermissionDenied(stdout, stderr, exitCode)) {
175
+ onPhase('failed');
137
176
  resolve({
138
177
  success: false,
139
- error: permissionDeniedMessage(platform),
178
+ error: permissionDeniedMessage(platform, home, env),
140
179
  stdout, stderr, exitCode,
141
180
  });
142
181
  return;
143
182
  }
144
183
  if (exitCode !== 0) {
184
+ onPhase('failed');
145
185
  resolve({
146
186
  success: false,
147
187
  error: `Install failed (npm exit ${exitCode}). ` +
@@ -153,6 +193,7 @@ async function executeInstall(opts = {}) {
153
193
  }
154
194
  // Success — parse installed version from npm output. Pattern:
155
195
  // "+ aiden-runtime@4.1.3" or "added 1 package ... aiden-runtime@4.1.3"
196
+ onPhase('installed');
156
197
  const installedVersion = parseInstalledVersion(stdout) ?? parseInstalledVersion(stderr) ?? undefined;
157
198
  resolve({
158
199
  success: true,
@@ -190,38 +231,14 @@ function isPermissionDenied(stdout, stderr, exitCode) {
190
231
  return false;
191
232
  }
192
233
  /**
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.
234
+ * v4.9.1 — Build the platform-specific copy-paste remediation. Delegates
235
+ * to `platformInstructions.ts` for the heavy lifting so the same builder
236
+ * powers both EPERM remediation + stale-prefix warnings + the future
237
+ * `aiden update --setup-user-prefix` helper.
198
238
  */
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');
239
+ function permissionDeniedMessage(platform, home, env) {
240
+ const instr = (0, platformInstructions_1.permissionDeniedInstructions)({ platform, home, env });
241
+ return [instr.headline, '', ...instr.steps].join('\n');
225
242
  }
226
243
  /**
227
244
  * 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
+ }
@@ -0,0 +1,70 @@
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/recoveryScript.ts — v4.9.2 SLICE 1.
10
+ *
11
+ * Fallback when spawnCommand() itself fails to launch npm (synchronous
12
+ * throw — invalid argv shape, cmd.exe missing, etc.). We write a small
13
+ * shell script the user can run by hand to complete the install and
14
+ * report its path in the failure message. Honest: we tried, we couldn't
15
+ * launch, here's the exact thing to type.
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.writeRecoveryScript = writeRecoveryScript;
22
+ const node_fs_1 = require("node:fs");
23
+ const node_path_1 = __importDefault(require("node:path"));
24
+ /**
25
+ * Write a recovery script under `~/.aiden/`:
26
+ * - Windows → update-recovery.ps1 (PowerShell)
27
+ * - Unix → update-recovery.sh (bash, +x)
28
+ *
29
+ * Returns the absolute path written. Creates `~/.aiden/` if missing.
30
+ */
31
+ async function writeRecoveryScript(input) {
32
+ const aidenDir = node_path_1.default.join(input.home, '.aiden');
33
+ await node_fs_1.promises.mkdir(aidenDir, { recursive: true });
34
+ if (input.platform === 'win32') {
35
+ const p = node_path_1.default.join(aidenDir, 'update-recovery.ps1');
36
+ const body = [
37
+ '# Aiden v4.9.2 — install-recovery fallback.',
38
+ '# Run from PowerShell (right-click → "Run with PowerShell" if associated).',
39
+ `Write-Host "Installing ${input.packageSpec} via npm..."`,
40
+ `npm install -g ${input.packageSpec}`,
41
+ 'if ($LASTEXITCODE -ne 0) {',
42
+ ' Write-Host "If you saw EPERM/EACCES, retry from an Administrator PowerShell."',
43
+ ' exit $LASTEXITCODE',
44
+ '}',
45
+ `Write-Host "Done. Type 'aiden' to launch the new version."`,
46
+ '',
47
+ ].join('\r\n');
48
+ await node_fs_1.promises.writeFile(p, body, 'utf8');
49
+ return p;
50
+ }
51
+ const p = node_path_1.default.join(aidenDir, 'update-recovery.sh');
52
+ const body = [
53
+ '#!/usr/bin/env bash',
54
+ '# Aiden v4.9.2 — install-recovery fallback.',
55
+ 'set -e',
56
+ `echo "Installing ${input.packageSpec} via npm..."`,
57
+ `if ! npm install -g ${input.packageSpec}; then`,
58
+ ' echo "Install failed. If you saw EACCES, retry with: sudo $0"',
59
+ ' exit 1',
60
+ 'fi',
61
+ 'echo "Done. Type \\"aiden\\" to launch the new version."',
62
+ '',
63
+ ].join('\n');
64
+ await node_fs_1.promises.writeFile(p, body, 'utf8');
65
+ try {
66
+ await node_fs_1.promises.chmod(p, 0o755);
67
+ }
68
+ catch { /* noop on platforms that don't support chmod */ }
69
+ return p;
70
+ }
@@ -0,0 +1,151 @@
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/util/spawnCommand.ts — v4.9.2 SLICE 1.
10
+ *
11
+ * Cross-platform spawn helper that survives the Node 18.20+ / 20+
12
+ * Windows EINVAL trap on `.cmd` / `.bat` shims. Two surfaces consume it:
13
+ * - core/v4/update/executeInstall.ts (spawns `npm install -g …`)
14
+ * - core/v4/mcp/transport.ts (spawns user-configured MCP servers,
15
+ * typically `npx -y <server>`)
16
+ *
17
+ * Both previously called `spawn(cmd, args, { shell: false })` directly,
18
+ * which throws EINVAL on Windows when `cmd` resolves to `.cmd` / `.bat`.
19
+ * The naive fix — `shell: true` — would silently permit argument
20
+ * injection through MCP server config (user-supplied command line),
21
+ * so we route Windows shims through `cmd.exe /d /s /c <quoted>` with
22
+ * `windowsVerbatimArguments: true` and manual cmd-meta escaping.
23
+ *
24
+ * Returns the raw ChildProcess (not a callback abstraction): both
25
+ * consumers need the full duplex surface (stdin write, stdout/stderr
26
+ * stream, exit + error events, SIGTERM → SIGKILL escalation). The
27
+ * helper's value is resolving the EINVAL trap, not abstracting I/O.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.resolveCommand = resolveCommand;
34
+ exports.escapeCmdArg = escapeCmdArg;
35
+ exports.spawnCommand = spawnCommand;
36
+ const node_child_process_1 = require("node:child_process");
37
+ const node_fs_1 = require("node:fs");
38
+ const node_path_1 = __importDefault(require("node:path"));
39
+ /**
40
+ * Resolve a bare command name to an absolute disk path via PATH lookup.
41
+ * On Windows tries PATHEXT order (.cmd, .exe, .bat, .ps1). If `command`
42
+ * is already absolute or contains a path separator, returns it verbatim
43
+ * (still detects shim suffix). Returns null when no match — caller can
44
+ * still spawn the bare name and let node:child_process emit ENOENT.
45
+ *
46
+ * Implemented in pure Node (no `where` subprocess — that would face
47
+ * the same spawn problem).
48
+ */
49
+ function resolveCommand(command, opts = {}) {
50
+ const platform = opts.platform ?? process.platform;
51
+ const env = opts.env ?? process.env;
52
+ const isWin = platform === 'win32';
53
+ // Already absolute or has a separator → trust the caller, just detect suffix.
54
+ if (node_path_1.default.isAbsolute(command) || command.includes('/') || command.includes('\\')) {
55
+ if (!(0, node_fs_1.existsSync)(command))
56
+ return null;
57
+ return { path: command, isShim: isWin && /\.(cmd|bat)$/i.test(command) };
58
+ }
59
+ const pathDirs = (env.PATH ?? env.Path ?? '').split(node_path_1.default.delimiter).filter(Boolean);
60
+ const pathExts = isWin
61
+ ? (env.PATHEXT ?? '.CMD;.EXE;.BAT;.COM').split(';').filter(Boolean)
62
+ : [''];
63
+ for (const dir of pathDirs) {
64
+ for (const ext of pathExts) {
65
+ const candidate = node_path_1.default.join(dir, command + ext);
66
+ try {
67
+ if ((0, node_fs_1.existsSync)(candidate) && (0, node_fs_1.statSync)(candidate).isFile()) {
68
+ return { path: candidate, isShim: isWin && /\.(cmd|bat)$/i.test(candidate) };
69
+ }
70
+ }
71
+ catch { /* permissions / ENOENT — keep walking */ }
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Escape an argv element for cmd.exe /c invocation. Wraps in double
78
+ * quotes if the value contains whitespace or any cmd metachar (& | <
79
+ * > ( ) @ ^ "), doubling any embedded quotes. Used together with
80
+ * `windowsVerbatimArguments: true` so Node passes our quoted string
81
+ * straight through to cmd.exe without its own (broken-for-.cmd)
82
+ * quoting heuristics.
83
+ *
84
+ * Reference: cross-spawn / npm-cli use the same pattern; Node 20+
85
+ * refuses to do this for us when spawning .cmd files because the
86
+ * heuristics were the source of CVE-2024-27980.
87
+ */
88
+ function escapeCmdArg(s) {
89
+ if (s.length === 0)
90
+ return '""';
91
+ // Conservative: any whitespace OR cmd metachar triggers quoting.
92
+ if (!/[\s&|<>()@^"]/.test(s))
93
+ return s;
94
+ // Double internal quotes (cmd.exe convention) and wrap.
95
+ return '"' + s.replace(/"/g, '""') + '"';
96
+ }
97
+ /**
98
+ * Cross-platform spawn. See file header for the why.
99
+ *
100
+ * - Unix → spawn(cmd, args, { shell:false, ...opts })
101
+ * - Win + .cmd/.bat → spawn('cmd.exe', ['/d','/s','/c', quoted], {
102
+ * shell:false,
103
+ * windowsVerbatimArguments: true, ...opts })
104
+ * - Win + .exe / abs → spawn(cmd, args, { shell:false, ...opts })
105
+ * - Win + bare name → resolveCommand() picks; bucket by suffix.
106
+ *
107
+ * Never throws synchronously for "not found" — emits 'error' on the
108
+ * returned ChildProcess like plain spawn() does. May throw synchronously
109
+ * for the same reasons spawn() itself does (e.g. invalid argv types).
110
+ */
111
+ function spawnCommand(command, args, opts = {}) {
112
+ const spawn = opts.spawnImpl ?? node_child_process_1.spawn;
113
+ const platform = opts.platform ?? process.platform;
114
+ const isWin = platform === 'win32';
115
+ const baseOpts = {
116
+ stdio: opts.stdio ?? ['pipe', 'pipe', 'pipe'],
117
+ env: opts.env,
118
+ cwd: opts.cwd,
119
+ shell: false,
120
+ };
121
+ if (!isWin) {
122
+ const child = spawn(command, args, baseOpts);
123
+ return { child, resolvedCmd: command, resolvedArgs: args, viaCmdExe: false };
124
+ }
125
+ // Windows: detect shim suffix.
126
+ const resolved = resolveCommand(command, { platform, env: opts.env });
127
+ const isShim = resolved
128
+ ? resolved.isShim
129
+ : /\.(cmd|bat)$/i.test(command); // unresolved but caller passed .cmd explicitly
130
+ if (!isShim) {
131
+ const child = spawn(command, args, baseOpts);
132
+ return { child, resolvedCmd: command, resolvedArgs: args, viaCmdExe: false };
133
+ }
134
+ // Wrap through cmd.exe /d /s /c. Pass resolved path if we have it so
135
+ // we don't re-walk PATH; otherwise let cmd.exe resolve the bare name.
136
+ //
137
+ // cmd.exe /s rule: it strips a single pair of outer quotes from the
138
+ // command line and parses the remainder. We therefore wrap the entire
139
+ // escaped argv sequence in an OUTER quote pair on top of each
140
+ // individual arg's escapeCmdArg quoting. Otherwise a command path
141
+ // containing whitespace (e.g. "C:\Program Files\nodejs\npm.CMD")
142
+ // gets its inner quotes stripped, breaking on the first space.
143
+ const target = resolved?.path ?? command;
144
+ const line = '"' + [target, ...args].map(escapeCmdArg).join(' ') + '"';
145
+ const cmdArgs = ['/d', '/s', '/c', line];
146
+ const child = spawn('cmd.exe', cmdArgs, {
147
+ ...baseOpts,
148
+ windowsVerbatimArguments: true,
149
+ });
150
+ return { child, resolvedCmd: 'cmd.exe', resolvedArgs: cmdArgs, viaCmdExe: true };
151
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiden-runtime",
3
- "version": "4.9.0",
3
+ "version": "4.9.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },