aiden-runtime 4.1.1 → 4.1.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 (55) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +159 -9
  3. package/dist/cli/v4/callbacks.js +5 -2
  4. package/dist/cli/v4/chatSession.js +525 -15
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/help.js +4 -0
  7. package/dist/cli/v4/commands/index.js +10 -1
  8. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  9. package/dist/cli/v4/commands/update.js +102 -0
  10. package/dist/cli/v4/defaultSoul.js +68 -2
  11. package/dist/cli/v4/display.js +28 -10
  12. package/dist/cli/v4/doctor.js +112 -0
  13. package/dist/cli/v4/doctorLiveness.js +65 -10
  14. package/dist/cli/v4/promotionPrompt.js +202 -0
  15. package/dist/cli/v4/providerBootSelector.js +144 -0
  16. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  17. package/dist/cli/v4/toolPreview.js +139 -0
  18. package/dist/core/v4/aidenAgent.js +91 -29
  19. package/dist/core/v4/capabilities.js +89 -0
  20. package/dist/core/v4/contextCompressor.js +25 -8
  21. package/dist/core/v4/distillationIndex.js +167 -0
  22. package/dist/core/v4/distillationStore.js +98 -0
  23. package/dist/core/v4/logger/logger.js +40 -9
  24. package/dist/core/v4/promotionCandidates.js +234 -0
  25. package/dist/core/v4/promptBuilder.js +145 -1
  26. package/dist/core/v4/sessionDistiller.js +405 -0
  27. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  28. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  29. package/dist/core/v4/subsystemHealth.js +143 -0
  30. package/dist/core/v4/update/executeInstall.js +233 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/moat/memoryGuard.js +111 -0
  33. package/dist/moat/skillTeacher.js +14 -5
  34. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  35. package/dist/providers/v4/errors.js +20 -4
  36. package/dist/providers/v4/modelDefaults.js +65 -0
  37. package/dist/providers/v4/registry.js +9 -2
  38. package/dist/providers/v4/runtimeResolver.js +6 -0
  39. package/dist/tools/v4/index.js +57 -1
  40. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  41. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  42. package/dist/tools/v4/sessions/recallSession.js +163 -0
  43. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  44. package/dist/tools/v4/system/_psHelpers.js +55 -0
  45. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  46. package/dist/tools/v4/system/appClose.js +79 -0
  47. package/dist/tools/v4/system/appLaunch.js +92 -0
  48. package/dist/tools/v4/system/clipboardRead.js +54 -0
  49. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  50. package/dist/tools/v4/system/mediaKey.js +78 -0
  51. package/dist/tools/v4/system/osProcessList.js +99 -0
  52. package/dist/tools/v4/system/screenshot.js +106 -0
  53. package/dist/tools/v4/system/volumeSet.js +157 -0
  54. package/package.json +4 -1
  55. package/skills/system_control.md +135 -69
@@ -0,0 +1,323 @@
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/skillOutcomeTracker.ts — Phase v4.1.2-slice4.
10
+ *
11
+ * Track whether skills actually succeed when loaded. The mining-time
12
+ * confidence score (skillMining/skillMiner.ts:computeConfidence) is
13
+ * set once and never updated — skills that consistently produce bad
14
+ * tool-call traces stay confident; skills that consistently work well
15
+ * never accumulate evidence of that.
16
+ *
17
+ * Mechanism:
18
+ * - When `skill_view` fires (the model just received a skill body),
19
+ * open an attribution WINDOW for that skill: the next N tool calls
20
+ * are attributed as that skill's downstream outcomes.
21
+ * - Tool successes / failures attributed to the skill (counter-bump).
22
+ * - Another `skill_view` supersedes the window (last-write-wins).
23
+ * - Window closes after N tool calls or when superseded.
24
+ *
25
+ * What this is NOT:
26
+ * - Not a quality judge. We don't ask an LLM "did that skill help?".
27
+ * Tool success is a proxy — a noisy one — but it's deterministic
28
+ * and free. Per slice4 Phase 3 decision tree: Option A.
29
+ * - Not a promotion engine. Surfaced via `aiden doctor`; the existing
30
+ * SkillTeacher.flaggedSkillNames() flagging path stays dead (it
31
+ * would change SkillLoader behavior — separate decision).
32
+ *
33
+ * Persistence:
34
+ * `<skillsDir>/.skill-outcomes.json` — sidecar, atomic write
35
+ * (tmp + rename), best-effort failure handling via slice3
36
+ * SubsystemHealthTracker. Lazy hydrate on first `onTool` call so
37
+ * sessions that never load a skill pay zero disk I/O.
38
+ *
39
+ * Status: PHASE v4.1.2-slice4.
40
+ */
41
+ var __importDefault = (this && this.__importDefault) || function (mod) {
42
+ return (mod && mod.__esModule) ? mod : { "default": mod };
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.SkillOutcomeTracker = exports.ATTRIBUTION_WINDOW = void 0;
46
+ exports.isFailure = isFailure;
47
+ const node_fs_1 = require("node:fs");
48
+ const node_path_1 = __importDefault(require("node:path"));
49
+ /**
50
+ * Attribution window size — number of non-skill_view tool calls
51
+ * following a `skill_view` whose outcomes are attributed to that
52
+ * skill. Hard-coded per slice4 Phase 3 Q1: don't add config knobs
53
+ * we won't tune. If empirical signal shows 5 is wrong, change it
54
+ * here.
55
+ */
56
+ exports.ATTRIBUTION_WINDOW = 5;
57
+ /** Cap for `lastError.message` — keep snapshots small. */
58
+ const ERROR_MESSAGE_CAP = 200;
59
+ class SkillOutcomeTracker {
60
+ /**
61
+ * @param persistPath Absolute path to the sidecar JSON file.
62
+ * @param healthTracker Optional slice3 tracker for persist failures.
63
+ */
64
+ constructor(persistPath, healthTracker) {
65
+ this.persistPath = persistPath;
66
+ this.healthTracker = healthTracker;
67
+ /** Currently-loaded skill (last skill_view, while its window is open). */
68
+ this.currentSkill = null;
69
+ /** Tool calls remaining in the current attribution window. */
70
+ this.remaining = 0;
71
+ /** In-memory outcomes, keyed by skill name. Hydrated lazily. */
72
+ this.outcomes = new Map();
73
+ /** True once we've attempted hydration from disk. */
74
+ this.hydrated = false;
75
+ /** Pending persist requested while one is in flight. */
76
+ this.persistQueued = false;
77
+ }
78
+ /**
79
+ * Unified hook compatible with `AidenAgentOptions.onToolCall`.
80
+ * The agent fires it as `(call, 'before')` then `(call, 'after', result)`.
81
+ */
82
+ onTool(call, phase, result) {
83
+ if (phase === 'before')
84
+ this.onToolBefore(call);
85
+ else
86
+ this.onToolAfter(call, result);
87
+ }
88
+ /** Called before each tool. Opens / supersedes the attribution window. */
89
+ onToolBefore(call) {
90
+ if (call.name !== 'skill_view')
91
+ return;
92
+ const name = extractSkillName(call.arguments);
93
+ if (!name)
94
+ return;
95
+ // Hydrate synchronously so the bump below merges with any prior
96
+ // persisted state. The file is small (one row per ever-loaded
97
+ // skill), so the one-time sync read is cheap and avoids the
98
+ // ordering hazard of awaiting in an inherently sync hook.
99
+ this.ensureHydratedSync();
100
+ this.currentSkill = name;
101
+ this.remaining = exports.ATTRIBUTION_WINDOW;
102
+ this.bump(name, (o) => {
103
+ o.loaded += 1;
104
+ o.lastUsed = new Date().toISOString();
105
+ });
106
+ void this.queuePersist();
107
+ }
108
+ /**
109
+ * Called after each tool. Attributes success/failure to the currently
110
+ * open window. `skill_view` itself does NOT attribute back to itself
111
+ * (the window's purpose is to grade DOWNSTREAM tools).
112
+ */
113
+ onToolAfter(call, result) {
114
+ if (call.name === 'skill_view')
115
+ return;
116
+ if (!this.currentSkill || this.remaining <= 0)
117
+ return;
118
+ const skill = this.currentSkill;
119
+ const failed = isFailure(result);
120
+ this.bump(skill, (o) => {
121
+ if (failed) {
122
+ o.toolFailures += 1;
123
+ const msg = extractErrorMessage(result);
124
+ if (msg) {
125
+ o.lastError = {
126
+ message: truncate(msg, ERROR_MESSAGE_CAP),
127
+ at: new Date().toISOString(),
128
+ };
129
+ }
130
+ }
131
+ else {
132
+ o.toolSuccesses += 1;
133
+ }
134
+ });
135
+ this.remaining -= 1;
136
+ if (this.remaining === 0)
137
+ this.currentSkill = null;
138
+ void this.queuePersist();
139
+ }
140
+ /**
141
+ * Read-only snapshot for `aiden doctor`. Sorted by `loaded` descending
142
+ * so the most-used skills surface first.
143
+ */
144
+ snapshot() {
145
+ const arr = Array.from(this.outcomes.values());
146
+ arr.sort((a, b) => b.loaded - a.loaded);
147
+ return arr;
148
+ }
149
+ /** Total skills with at least one observation. */
150
+ size() {
151
+ return this.outcomes.size;
152
+ }
153
+ // ── private ───────────────────────────────────────────────────────
154
+ bump(skillName, mutator) {
155
+ const cur = this.outcomes.get(skillName) ?? {
156
+ skillName,
157
+ loaded: 0,
158
+ toolSuccesses: 0,
159
+ toolFailures: 0,
160
+ };
161
+ mutator(cur);
162
+ this.outcomes.set(skillName, cur);
163
+ }
164
+ /**
165
+ * Synchronous disk-hydration. Called once per instance lifetime on
166
+ * the first `skill_view` observation. The sidecar is small (one row
167
+ * per ever-loaded skill) so a sync read is cheap and removes the
168
+ * race between async hydration and immediately-following bumps.
169
+ *
170
+ * Failures (parse, EACCES) get recorded into the health tracker —
171
+ * doctor surfaces them. ENOENT (no file yet) is the common case on
172
+ * first run and stays silent.
173
+ */
174
+ ensureHydratedSync() {
175
+ if (this.hydrated)
176
+ return;
177
+ this.hydrated = true;
178
+ try {
179
+ if (!(0, node_fs_1.existsSync)(this.persistPath))
180
+ return;
181
+ const raw = (0, node_fs_1.readFileSync)(this.persistPath, 'utf-8');
182
+ const parsed = JSON.parse(raw);
183
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
184
+ for (const [name, val] of Object.entries(parsed)) {
185
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
186
+ const v = val;
187
+ this.outcomes.set(name, {
188
+ skillName: v.skillName ?? name,
189
+ loaded: Number(v.loaded ?? 0),
190
+ toolSuccesses: Number(v.toolSuccesses ?? 0),
191
+ toolFailures: Number(v.toolFailures ?? 0),
192
+ ...(v.lastUsed ? { lastUsed: v.lastUsed } : {}),
193
+ ...(v.lastError ? { lastError: v.lastError } : {}),
194
+ });
195
+ }
196
+ }
197
+ }
198
+ }
199
+ catch (err) {
200
+ this.healthTracker?.recordFailure(err);
201
+ }
202
+ }
203
+ /**
204
+ * Test/shutdown seam. Awaits any in-flight or queued persist so the
205
+ * caller knows the sidecar is on disk. The agent runtime doesn't
206
+ * need to call this (writes are durable enough via the coalescing
207
+ * queue); tests use it to deterministically wait for I/O.
208
+ */
209
+ async flush() {
210
+ while (this.persisting) {
211
+ await this.persisting;
212
+ }
213
+ }
214
+ /**
215
+ * Coalescing persist. If a write is in flight, queue exactly one
216
+ * follow-up; further requests collapse into that single follow-up.
217
+ * Keeps disk I/O cheap when many tool calls happen in a burst.
218
+ */
219
+ queuePersist() {
220
+ if (this.persisting) {
221
+ this.persistQueued = true;
222
+ return this.persisting;
223
+ }
224
+ this.persisting = this.persist()
225
+ .finally(() => {
226
+ const wasQueued = this.persistQueued;
227
+ this.persistQueued = false;
228
+ this.persisting = undefined;
229
+ if (wasQueued) {
230
+ // Fire-and-forget the queued follow-up.
231
+ void this.queuePersist();
232
+ }
233
+ });
234
+ return this.persisting;
235
+ }
236
+ async persist() {
237
+ try {
238
+ await node_fs_1.promises.mkdir(node_path_1.default.dirname(this.persistPath), { recursive: true });
239
+ const payload = {};
240
+ for (const [k, v] of this.outcomes)
241
+ payload[k] = v;
242
+ const tmp = `${this.persistPath}.tmp`;
243
+ await node_fs_1.promises.writeFile(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
244
+ await node_fs_1.promises.rename(tmp, this.persistPath);
245
+ this.healthTracker?.recordSuccess();
246
+ }
247
+ catch (err) {
248
+ this.healthTracker?.recordFailure(err);
249
+ // Best-effort: clean up tmp file if it exists. Ignore errors.
250
+ try {
251
+ await node_fs_1.promises.unlink(`${this.persistPath}.tmp`);
252
+ }
253
+ catch { /* ignore */ }
254
+ }
255
+ }
256
+ }
257
+ exports.SkillOutcomeTracker = SkillOutcomeTracker;
258
+ // ── private helpers ───────────────────────────────────────────────────
259
+ function extractSkillName(args) {
260
+ if (!args || typeof args !== 'object')
261
+ return '';
262
+ const v = args.name;
263
+ return typeof v === 'string' ? v.trim() : '';
264
+ }
265
+ /**
266
+ * Failure classification rules (per slice4 Phase 3 explicit decision):
267
+ * - result.success === false → failure
268
+ * - result.error truthy → failure
269
+ * - everything else → success
270
+ *
271
+ * "Tool succeeded but result was wrong" is NOT classifiable without an
272
+ * LLM judge and is intentionally out of scope.
273
+ */
274
+ function isFailure(result) {
275
+ if (!result)
276
+ return false;
277
+ // The ToolCallResult shape from providers/v4/types is { id, name, result }.
278
+ // Tool implementations conventionally return `{ success: boolean, error?, ... }`
279
+ // inside the `result` payload — both are surveyed.
280
+ const top = result;
281
+ if (top.error)
282
+ return true;
283
+ if (top.success === false)
284
+ return true;
285
+ const inner = result.result;
286
+ if (inner && typeof inner === 'object') {
287
+ const i = inner;
288
+ if (i.error)
289
+ return true;
290
+ if (i.success === false)
291
+ return true;
292
+ }
293
+ return false;
294
+ }
295
+ function extractErrorMessage(result) {
296
+ if (!result)
297
+ return '';
298
+ const top = result;
299
+ if (typeof top.error === 'string')
300
+ return top.error;
301
+ if (top.error && typeof top.error === 'object') {
302
+ const m = top.error.message;
303
+ if (typeof m === 'string')
304
+ return m;
305
+ }
306
+ const inner = result.result;
307
+ if (inner && typeof inner === 'object') {
308
+ const i = inner;
309
+ if (typeof i.error === 'string')
310
+ return i.error;
311
+ if (i.error && typeof i.error === 'object') {
312
+ const m = i.error.message;
313
+ if (typeof m === 'string')
314
+ return m;
315
+ }
316
+ }
317
+ return '';
318
+ }
319
+ function truncate(s, max) {
320
+ if (s.length <= max)
321
+ return s;
322
+ return s.slice(0, max - 3) + '...';
323
+ }
@@ -0,0 +1,143 @@
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/subsystemHealth.ts — Phase v4.1.2-slice3.
10
+ *
11
+ * Lightweight in-process telemetry for the silent-failure layers.
12
+ * Four subsystems (ContextCompressor, SkillTeacher, SkillMiner,
13
+ * Logger) historically caught errors and continued without
14
+ * surfacing them — masking real bugs that were diagnosable only
15
+ * after manual instrumentation. This module is the surface.
16
+ *
17
+ * Design (decision tree from slice3 Phase 3):
18
+ * Option C — subsystem-owned state object, optionally registered
19
+ * with a shared registry. The registry is constructor-injected
20
+ * (no singleton — singletons leak state between parallel tests),
21
+ * and every record op is O(1) and side-effect-free (no I/O, no
22
+ * log writes, no recursion through the Logger we are tracking).
23
+ *
24
+ * Surface:
25
+ * - `SubsystemHealth` — read-only snapshot shape doctor renders
26
+ * - `SubsystemHealthTracker` — per-subsystem owned counter
27
+ * - `SubsystemHealthRegistry`— optional aggregator AidenAgent owns
28
+ *
29
+ * Subsystems may operate without a tracker (back-compat); when a
30
+ * tracker is wired they call `recordSuccess()` / `recordFailure(err)`
31
+ * at the appropriate points. The registry is read by `aiden doctor`
32
+ * via the AidenAgent public field.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.SubsystemHealthTracker = void 0;
36
+ exports.createSubsystemHealthRegistry = createSubsystemHealthRegistry;
37
+ /**
38
+ * Per-subsystem health counter. One instance per subsystem; cheap
39
+ * to construct (no I/O, no allocations beyond the counter object).
40
+ *
41
+ * Subsystems hold a private tracker (or undefined for back-compat)
42
+ * and call `recordSuccess()` / `recordFailure(err)` from their
43
+ * critical paths. The tracker is registered with the registry at
44
+ * construction; doctor reads the snapshot lazily.
45
+ */
46
+ class SubsystemHealthTracker {
47
+ /**
48
+ * @param subsystem Stable id rendered by doctor. Prefer kebab-case
49
+ * ('compressor', 'skill-teacher', 'logger:file-sink').
50
+ */
51
+ constructor(subsystem) {
52
+ this.subsystem = subsystem;
53
+ this._totalCalls = 0;
54
+ this._totalErrors = 0;
55
+ this._consecutive = 0;
56
+ }
57
+ /** O(1): bump call counter, reset consecutive-failure streak. */
58
+ recordSuccess() {
59
+ this._totalCalls += 1;
60
+ this._consecutive = 0;
61
+ }
62
+ /**
63
+ * O(1): bump call + error counters, update lastError with a
64
+ * length-capped message. Never logs (would recurse through the
65
+ * Logger we are tracking) and never writes to disk.
66
+ */
67
+ recordFailure(err) {
68
+ this._totalCalls += 1;
69
+ this._totalErrors += 1;
70
+ this._consecutive += 1;
71
+ const raw = err instanceof Error ? err.message
72
+ : typeof err === 'string' ? err
73
+ : safeStringify(err);
74
+ this._lastError = {
75
+ message: truncate(raw, 200),
76
+ at: new Date(),
77
+ };
78
+ }
79
+ /** Render the current state. Doctor invokes this on demand. */
80
+ snapshot() {
81
+ const snap = {
82
+ subsystem: this.subsystem,
83
+ totalCalls: this._totalCalls,
84
+ totalErrors: this._totalErrors,
85
+ };
86
+ if (this._lastError) {
87
+ snap.lastError = {
88
+ message: this._lastError.message,
89
+ at: this._lastError.at,
90
+ consecutive: this._consecutive,
91
+ };
92
+ }
93
+ return snap;
94
+ }
95
+ }
96
+ exports.SubsystemHealthTracker = SubsystemHealthTracker;
97
+ /** Build a fresh registry. No I/O; cheap. */
98
+ function createSubsystemHealthRegistry() {
99
+ const readers = new Map();
100
+ return {
101
+ register(subsystem, reader) {
102
+ readers.set(subsystem, reader);
103
+ },
104
+ snapshot() {
105
+ const out = [];
106
+ for (const reader of readers.values()) {
107
+ try {
108
+ const v = reader();
109
+ if (Array.isArray(v))
110
+ out.push(...v);
111
+ else
112
+ out.push(v);
113
+ }
114
+ catch {
115
+ // Reader threw — skip it. Telemetry must never break doctor.
116
+ }
117
+ }
118
+ return out;
119
+ },
120
+ reset() {
121
+ readers.clear();
122
+ },
123
+ };
124
+ }
125
+ // ── private helpers ───────────────────────────────────────────────────
126
+ function truncate(s, max) {
127
+ if (s.length <= max)
128
+ return s;
129
+ return s.slice(0, max - 3) + '...';
130
+ }
131
+ function safeStringify(v) {
132
+ // `JSON.stringify(undefined)` returns the value `undefined`, not the
133
+ // string "undefined" — guard so the downstream length-cap doesn't
134
+ // crash. Symbols, functions, and circular objects also need a
135
+ // String() fallback.
136
+ try {
137
+ const out = JSON.stringify(v);
138
+ return typeof out === 'string' ? out : String(v);
139
+ }
140
+ catch {
141
+ return String(v);
142
+ }
143
+ }
@@ -0,0 +1,233 @@
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/executeInstall.ts — Phase v4.1.2-update.
10
+ *
11
+ * Shared in-process installer for `npm install -g aiden-runtime@latest`.
12
+ * Used by two surfaces:
13
+ * - `/update install` slash command (cli/v4/commands/update.ts)
14
+ * - `aiden_self_update` tool (tools/v4/system/aidenSelfUpdate.ts)
15
+ *
16
+ * Both call this single executor so install behavior — timeout,
17
+ * permission-denied fallback, version detection — has ONE source of
18
+ * truth. Future v4.1.3+ rollback / package-manager-swap work edits
19
+ * one file.
20
+ *
21
+ * Behavior:
22
+ * - Spawns `npm install -g aiden-runtime@latest` with INSTALL_TIMEOUT_MS
23
+ * wall-clock cap.
24
+ * - Captures stdout/stderr; returns both for diagnostics regardless
25
+ * of outcome.
26
+ * - Detects the installed version from npm's `+ aiden-runtime@x.y.z`
27
+ * output line; null when not parseable.
28
+ * - On permission-denied (EACCES / "EACCES" / Windows ENOPRIV /
29
+ * "operation not permitted"): returns structured failure with
30
+ * platform-specific copy-paste commands so the user can run the
31
+ * install manually with proper privileges.
32
+ *
33
+ * Honest about what it doesn't do:
34
+ * - No auto-restart of the running REPL. The currently-running
35
+ * process keeps running the OLD version regardless of what npm
36
+ * just installed globally — claiming otherwise would lie to the
37
+ * user. Caller prints the "type /quit and rerun aiden" hint
38
+ * instead so the user knows exactly when the new version takes
39
+ * effect.
40
+ * - No self-escalation to UAC/sudo. We try once; on permission
41
+ * failure we surface the right copy-paste, not silent escalation.
42
+ * - No registry probe — call `checkForUpdate` first if you need to
43
+ * know whether an install is warranted.
44
+ */
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.INSTALL_TIMEOUT_MS = void 0;
47
+ exports.executeInstall = executeInstall;
48
+ exports.parseInstalledVersion = parseInstalledVersion;
49
+ const node_child_process_1 = require("node:child_process");
50
+ /** 90 s wall-clock cap. Generous on cold caches / slow networks. */
51
+ exports.INSTALL_TIMEOUT_MS = 90000;
52
+ const DEFAULT_PACKAGE_SPEC = 'aiden-runtime@latest';
53
+ /**
54
+ * Run the install. Returns a structured result; NEVER throws — the
55
+ * outer surface (slash command / tool) renders the result to the user.
56
+ *
57
+ * Error path is intentionally string-typed (single user-visible
58
+ * paragraph). The structured fields (stdout/stderr/exitCode) are for
59
+ * diagnostics; callers that want to surface them to the user can
60
+ * compose their own message from those.
61
+ */
62
+ async function executeInstall(opts = {}) {
63
+ const spawn = opts.spawnImpl ?? node_child_process_1.spawn;
64
+ const timeoutMs = opts.timeoutMs ?? exports.INSTALL_TIMEOUT_MS;
65
+ const packageSpec = opts.packageSpec ?? DEFAULT_PACKAGE_SPEC;
66
+ const platform = opts.platform ?? process.platform;
67
+ return new Promise((resolve) => {
68
+ const args = ['install', '-g', packageSpec];
69
+ // shell: true on Windows so npm.cmd is found via PATHEXT; on
70
+ // POSIX we spawn npm directly. Either way the args are validated
71
+ // (only npm + install + a hardcoded spec by default) — no user
72
+ // input flows into argv.
73
+ const spawnOpts = {
74
+ shell: platform === 'win32',
75
+ stdio: ['ignore', 'pipe', 'pipe'],
76
+ };
77
+ let child;
78
+ try {
79
+ child = spawn('npm', args, spawnOpts);
80
+ }
81
+ catch (err) {
82
+ resolve({
83
+ success: false,
84
+ error: `Could not launch npm: ${err.message}. ` +
85
+ `Run \`npm install -g aiden-runtime@latest\` manually.`,
86
+ });
87
+ return;
88
+ }
89
+ let stdoutBuf = '';
90
+ let stderrBuf = '';
91
+ child.stdout?.on('data', (chunk) => {
92
+ stdoutBuf += chunk.toString();
93
+ });
94
+ child.stderr?.on('data', (chunk) => {
95
+ stderrBuf += chunk.toString();
96
+ });
97
+ // Timeout — kill the child + resolve as a failure with the captured
98
+ // output so the user sees what npm was doing.
99
+ let timedOut = false;
100
+ const timer = setTimeout(() => {
101
+ timedOut = true;
102
+ try {
103
+ child.kill('SIGTERM');
104
+ }
105
+ catch { /* ignore */ }
106
+ }, timeoutMs);
107
+ child.on('error', (err) => {
108
+ clearTimeout(timer);
109
+ // Spawn-level error (ENOENT — npm not on PATH).
110
+ resolve({
111
+ success: false,
112
+ error: `npm spawn failed: ${err.message}. Is npm installed and on PATH?`,
113
+ stderr: stderrBuf,
114
+ stdout: stdoutBuf,
115
+ });
116
+ });
117
+ child.on('close', (code) => {
118
+ clearTimeout(timer);
119
+ const stdout = stdoutBuf;
120
+ const stderr = stderrBuf;
121
+ const exitCode = code ?? -1;
122
+ if (timedOut) {
123
+ resolve({
124
+ success: false,
125
+ error: `Install timed out after ${timeoutMs}ms. ` +
126
+ `Try \`npm install -g aiden-runtime@latest\` manually.`,
127
+ stdout, stderr, exitCode: -1,
128
+ });
129
+ return;
130
+ }
131
+ // Permission-denied: surface platform-specific remediations.
132
+ if (isPermissionDenied(stdout, stderr, exitCode)) {
133
+ resolve({
134
+ success: false,
135
+ error: permissionDeniedMessage(platform),
136
+ stdout, stderr, exitCode,
137
+ });
138
+ return;
139
+ }
140
+ if (exitCode !== 0) {
141
+ resolve({
142
+ success: false,
143
+ error: `Install failed (npm exit ${exitCode}). ` +
144
+ (stderr.trim().slice(0, 200) ||
145
+ 'See stderr/stdout for details. Try `npm install -g aiden-runtime@latest` manually.'),
146
+ stdout, stderr, exitCode,
147
+ });
148
+ return;
149
+ }
150
+ // Success — parse installed version from npm output. Pattern:
151
+ // "+ aiden-runtime@4.1.3" or "added 1 package ... aiden-runtime@4.1.3"
152
+ const installedVersion = parseInstalledVersion(stdout) ?? parseInstalledVersion(stderr) ?? undefined;
153
+ resolve({
154
+ success: true,
155
+ installedVersion,
156
+ stdout, stderr, exitCode,
157
+ });
158
+ });
159
+ });
160
+ }
161
+ // ── Helpers ───────────────────────────────────────────────────────────────
162
+ /**
163
+ * Did npm fail because of a permission error? Heuristics across the
164
+ * three platforms — npm doesn't return a single canonical exit code
165
+ * for this, so we sniff the captured streams.
166
+ */
167
+ function isPermissionDenied(stdout, stderr, exitCode) {
168
+ const haystack = `${stderr}\n${stdout}`.toLowerCase();
169
+ // POSIX: "EACCES", "permission denied", "operation not permitted"
170
+ if (haystack.includes('eacces'))
171
+ return true;
172
+ if (haystack.includes('permission denied'))
173
+ return true;
174
+ if (haystack.includes('operation not permitted'))
175
+ return true;
176
+ // Windows: usually exit 1 with stderr containing "EPERM" or
177
+ // "operation not permitted" or "access is denied"
178
+ if (haystack.includes('eperm'))
179
+ return true;
180
+ if (haystack.includes('access is denied'))
181
+ return true;
182
+ // exit 243 is the npm conventional "permission" code on some setups;
183
+ // we don't gate on exit code alone (too noisy) but combined with
184
+ // any of the above strings it's a clear signal.
185
+ void exitCode;
186
+ return false;
187
+ }
188
+ /**
189
+ * Build the platform-specific copy-paste remediation. Provides three
190
+ * distinct paths — system-wide-with-elevation (Windows admin),
191
+ * sudo (macOS/Linux), or user-local-prefix (cross-platform) — so the
192
+ * user has options without us trying to self-escalate to UAC/sudo
193
+ * from inside the running REPL.
194
+ */
195
+ function permissionDeniedMessage(platform) {
196
+ const userLocal = 'Or use a user-local npm prefix to avoid privileges entirely:\n' +
197
+ ' npm config set prefix ~/.npm-global\n' +
198
+ ' export PATH=~/.npm-global/bin:$PATH # add to your shell profile\n' +
199
+ ' npm install -g aiden-runtime@latest';
200
+ if (platform === 'win32') {
201
+ return [
202
+ 'Install failed: permission denied (npm needs Administrator for global install).',
203
+ '',
204
+ 'To update manually:',
205
+ ' Windows: Open PowerShell as Administrator, then:',
206
+ ' npm install -g aiden-runtime@latest',
207
+ '',
208
+ userLocal,
209
+ ].join('\n');
210
+ }
211
+ // darwin / linux / others — sudo path.
212
+ return [
213
+ 'Install failed: permission denied (npm needs sudo for global install).',
214
+ '',
215
+ 'To update manually:',
216
+ ' macOS / Linux:',
217
+ ' sudo npm install -g aiden-runtime@latest',
218
+ '',
219
+ userLocal,
220
+ ].join('\n');
221
+ }
222
+ /**
223
+ * Find the installed version in npm output. Two common patterns:
224
+ * "+ aiden-runtime@4.1.3"
225
+ * "added 1 package in 12s ... aiden-runtime@4.1.3"
226
+ * Returns the bare version string (no `v` prefix) or null.
227
+ */
228
+ function parseInstalledVersion(out) {
229
+ if (!out)
230
+ return null;
231
+ const m = out.match(/aiden-runtime@(\d+\.\d+\.\d+(?:-[a-z0-9.]+)?)/i);
232
+ return m ? m[1] : null;
233
+ }