aiden-runtime 4.5.0 → 4.6.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 (50) hide show
  1. package/README.md +17 -2
  2. package/dist/cli/v4/aidenCLI.js +207 -100
  3. package/dist/cli/v4/chatSession.js +120 -0
  4. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
  5. package/dist/cli/v4/commands/fanout.js +42 -59
  6. package/dist/cli/v4/commands/help.js +8 -0
  7. package/dist/cli/v4/commands/index.js +21 -1
  8. package/dist/cli/v4/commands/mcp.js +80 -54
  9. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  10. package/dist/cli/v4/commands/recovery.js +122 -0
  11. package/dist/cli/v4/commands/runs.js +22 -2
  12. package/dist/cli/v4/commands/spawnPause.js +93 -0
  13. package/dist/cli/v4/commands/walkthrough.js +140 -0
  14. package/dist/cli/v4/daemonAgentBuilder.js +4 -1
  15. package/dist/cli/v4/defaultSoul.js +1 -1
  16. package/dist/cli/v4/onboarding/disclaimer.js +162 -0
  17. package/dist/cli/v4/onboarding/loading.js +208 -0
  18. package/dist/cli/v4/onboarding/providerPicker.js +126 -0
  19. package/dist/cli/v4/onboarding/successScreen.js +68 -0
  20. package/dist/cli/v4/repl/firstRunHint.js +107 -0
  21. package/dist/cli/v4/setupWizard.js +201 -31
  22. package/dist/core/v4/aidenAgent.js +219 -1
  23. package/dist/core/v4/daemon/bootstrap.js +47 -0
  24. package/dist/core/v4/daemon/db/migrations.js +66 -0
  25. package/dist/core/v4/daemon/runStore.js +33 -3
  26. package/dist/core/v4/providerFallback.js +35 -2
  27. package/dist/core/v4/providers/modelFetch.js +179 -0
  28. package/dist/core/v4/providers/probe.js +275 -0
  29. package/dist/core/v4/runtimeToggles.js +30 -3
  30. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  31. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  32. package/dist/core/v4/subagent/childBuilder.js +391 -0
  33. package/dist/core/v4/subagent/fanout.js +75 -51
  34. package/dist/core/v4/subagent/spawnPause.js +191 -0
  35. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  36. package/dist/core/v4/toolRegistry.js +19 -3
  37. package/dist/core/v4/ui/banner.js +133 -0
  38. package/dist/core/v4/ui/theme.js +164 -0
  39. package/dist/core/version.js +1 -1
  40. package/dist/moat/plannerGuard.js +29 -0
  41. package/dist/providers/v4/anthropicAdapter.js +31 -3
  42. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  43. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  44. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  45. package/dist/tools/v4/index.js +17 -3
  46. package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
  47. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  48. package/dist/tools/v4/subagent/subagentFanout.js +53 -1
  49. package/dist/tools/v4/ui/_uiSmokeTool.js +60 -0
  50. package/package.json +7 -3
@@ -0,0 +1,93 @@
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
+ * cli/v4/commands/spawnPause.ts — v4.6 Phase 3A.
10
+ *
11
+ * `/spawn-pause on|off|status [reason...]` — operator kill-switch
12
+ * for sub-agent spawning. Backed by a file marker at
13
+ * `$aidenHome/spawn.paused` (see `core/v4/subagent/spawnPause.ts`)
14
+ * so REPL + daemon + MCP server all coordinate via the same state.
15
+ *
16
+ * /spawn-pause on — pause, no reason
17
+ * /spawn-pause on runaway-fanout — pause, reason="runaway-fanout"
18
+ * /spawn-pause on deploy window — pause, reason="deploy window"
19
+ * /spawn-pause off — resume
20
+ * /spawn-pause status — current state + reason + duration
21
+ *
22
+ * Unlike `/planner-guard`, `/sandbox`, etc., this command does NOT
23
+ * route through `runtimeToggles` — pause state is file-marker-
24
+ * backed (cross-process visibility) with first-class
25
+ * reason/pausedAt/pausedBy metadata that the boolean toggle surface
26
+ * can't carry. Mirrors plannerGuard.ts's command shape; diverges
27
+ * from `_runtimeToggleHelpers` because the storage backend is
28
+ * different.
29
+ *
30
+ * Hard contract: in-flight children are NEVER cancelled by this
31
+ * command. Pause affects only NEW spawns. Operators who want to
32
+ * stop in-flight runs use `aiden runs interrupt <runId>` (the
33
+ * existing per-run cancellation surface from v4.5 Phase 6).
34
+ */
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.spawnPause = void 0;
37
+ const spawnPause_1 = require("../../../core/v4/subagent/spawnPause");
38
+ /** Format a duration in ms as a compact `Xs` / `Xm` / `Xh` string. */
39
+ function formatDuration(ms) {
40
+ if (ms < 1000)
41
+ return `${ms}ms`;
42
+ if (ms < 60000)
43
+ return `${Math.round(ms / 1000)}s`;
44
+ if (ms < 3600000)
45
+ return `${Math.round(ms / 60000)}m`;
46
+ return `${Math.round(ms / 3600000)}h`;
47
+ }
48
+ exports.spawnPause = {
49
+ name: 'spawn-pause',
50
+ description: 'Pause/resume sub-agent spawning (in-flight children continue).',
51
+ category: 'system',
52
+ icon: '⏸',
53
+ handler: async (ctx) => {
54
+ const action = (ctx.args[0] ?? 'status').toLowerCase();
55
+ const reasonArg = ctx.args.slice(1).join(' ').trim() || null;
56
+ let state;
57
+ try {
58
+ state = (0, spawnPause_1.getSpawnPause)();
59
+ }
60
+ catch (e) {
61
+ ctx.display.printError('spawn-pause: not initialized — REPL boot did not wire the singleton.', e instanceof Error ? e.message : String(e));
62
+ return {};
63
+ }
64
+ if (action === 'on' || action === 'enable' || action === 'true' || action === '1') {
65
+ state.pause({ reason: reasonArg, pausedBy: 'repl' });
66
+ const s = state.status();
67
+ const reasonLine = s.reason ? ` reason: ${s.reason}\n` : '';
68
+ ctx.display.write(`spawn-pause: ON\n${reasonLine}`);
69
+ ctx.display.dim(' in-flight children continue. New spawn_sub_agent / subagent_fanout calls will reject.');
70
+ return {};
71
+ }
72
+ if (action === 'off' || action === 'disable' || action === 'false' || action === '0' || action === 'resume') {
73
+ state.resume();
74
+ ctx.display.write('spawn-pause: OFF (resumed)\n');
75
+ return {};
76
+ }
77
+ if (action === 'status' || action === '') {
78
+ const s = state.status();
79
+ if (!s.paused) {
80
+ ctx.display.write('spawn-pause: OFF\n');
81
+ return {};
82
+ }
83
+ const reasonLine = s.reason ? ` reason: ${s.reason}\n` : '';
84
+ const durationLine = s.durationMs !== undefined ? ` duration: ${formatDuration(s.durationMs)}\n` : '';
85
+ const pausedAtLine = s.pausedAt ? ` pausedAt: ${new Date(s.pausedAt).toISOString()}\n` : '';
86
+ const pausedByLine = s.pausedBy ? ` pausedBy: ${s.pausedBy}\n` : '';
87
+ ctx.display.write(`spawn-pause: ON\n${reasonLine}${durationLine}${pausedAtLine}${pausedByLine}`);
88
+ return {};
89
+ }
90
+ ctx.display.printError('Usage: /spawn-pause on [reason...] | off | status');
91
+ return {};
92
+ },
93
+ };
@@ -0,0 +1,140 @@
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
+ * cli/v4/commands/walkthrough.ts — ONB1 slice 10.
10
+ *
11
+ * `/walkthrough` — 5-screen guided tour of what Aiden can do. Pointed
12
+ * at by the first-run hint banner (slice 9) but available any time.
13
+ *
14
+ * Each screen:
15
+ * 1. A short title + one paragraph describing a capability area.
16
+ * 2. A "Try this now" example prompt the user can run verbatim.
17
+ * 3. A muted line on what tools/skills will fire.
18
+ *
19
+ * Screens:
20
+ * 1. Files & shell
21
+ * 2. Browser & web
22
+ * 3. Memory & SOUL
23
+ * 4. Skills & MCP
24
+ * 5. Operator controls
25
+ *
26
+ * Navigation: the command renders all five screens in order with
27
+ * separator lines between, then drops back to the REPL. No interactive
28
+ * paging — the user can scroll the buffer if they want to revisit
29
+ * something. Re-running `/walkthrough` is cheap.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.walkthrough = void 0;
33
+ const theme_1 = require("../../../core/v4/ui/theme");
34
+ const SCREENS = [
35
+ {
36
+ title: 'Files & shell',
37
+ body: [
38
+ 'Aiden can read, patch, and organise files on your machine, and run',
39
+ 'shell commands when a task needs the OS. Patches go through a diff',
40
+ 'preview before anything writes to disk.',
41
+ ],
42
+ tryThis: 'summarize the files in this folder and flag anything stale',
43
+ fires: 'file_read · shell · diff_preview',
44
+ },
45
+ {
46
+ title: 'Browser & web',
47
+ body: [
48
+ 'Persistent Chromium session for navigation, screenshots, form fill,',
49
+ 'and content extraction. The browser stays warm across turns so the',
50
+ 'agent can chain research steps without re-logging in.',
51
+ ],
52
+ tryThis: 'research the top three local-first AI tools and save a comparison to notes.md',
53
+ fires: 'open_browser · browser_extract · file_write',
54
+ },
55
+ {
56
+ title: 'Memory & SOUL',
57
+ body: [
58
+ 'Aiden remembers across sessions via SOUL.md (identity), MEMORY.md',
59
+ '(curated notes), and the sessions database. `/identity` shows what',
60
+ 'persona is loaded; edit SOUL.md to shape it.',
61
+ ],
62
+ tryThis: 'remember that I prefer concise replies and Python over TypeScript',
63
+ fires: 'memory_write · session_log',
64
+ },
65
+ {
66
+ title: 'Skills & MCP',
67
+ body: [
68
+ 'Skills are bundled workflows (graphify, voice, kanban, more). MCP',
69
+ 'servers extend the toolset further — each MCP exposes its own tools',
70
+ 'as if they were native. `/skills` lists what is loaded; `/tools` lists',
71
+ 'every active tool.',
72
+ ],
73
+ tryThis: '/skills',
74
+ fires: '(no agent call — local registry walk)',
75
+ },
76
+ {
77
+ title: 'Operator controls',
78
+ body: [
79
+ 'You stay in charge. `/spawn-pause on` halts sub-agent spawning;',
80
+ '`/recovery list` shows what Aiden has learned from past failures;',
81
+ '`/doctor` prints a health snapshot; `/quit` exits cleanly.',
82
+ ],
83
+ tryThis: '/doctor',
84
+ fires: '(no agent call — local self-check)',
85
+ },
86
+ ];
87
+ function wrap(text, width) {
88
+ const words = text.split(/\s+/);
89
+ const lines = [];
90
+ let cur = '';
91
+ for (const w of words) {
92
+ if (!cur) {
93
+ cur = w;
94
+ continue;
95
+ }
96
+ if (cur.length + 1 + w.length > width) {
97
+ lines.push(cur);
98
+ cur = w;
99
+ }
100
+ else
101
+ cur += ' ' + w;
102
+ }
103
+ if (cur)
104
+ lines.push(cur);
105
+ return lines;
106
+ }
107
+ function renderScreen(ctx, screen, idx, total) {
108
+ const out = ctx.display;
109
+ const w = (0, theme_1.termWidth)();
110
+ const bodyW = Math.min(w - 6, 72);
111
+ out.write('\n ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n\n');
112
+ out.write(' ' + theme_1.c.muted(`(${idx + 1}/${total})`) + ' ' + (0, theme_1.bold)(theme_1.c.primary(screen.title)) + '\n\n');
113
+ const joined = screen.body.join(' ');
114
+ for (const line of wrap(joined, bodyW)) {
115
+ out.write(' ' + theme_1.c.text(line) + '\n');
116
+ }
117
+ out.write('\n');
118
+ out.write(' ' + theme_1.c.muted('Try this now:') + '\n');
119
+ out.write(' ' + theme_1.c.accent('▸ ') + theme_1.c.accent(screen.tryThis) + '\n');
120
+ out.write(' ' + (0, theme_1.italic)(theme_1.c.muted(`fires: ${screen.fires}`)) + '\n');
121
+ }
122
+ exports.walkthrough = {
123
+ name: 'walkthrough',
124
+ description: '5-screen guided tour of what Aiden can do.',
125
+ category: 'system',
126
+ icon: '🧭',
127
+ aliases: ['tour', 'tutorial'],
128
+ handler: async (ctx) => {
129
+ const w = (0, theme_1.termWidth)();
130
+ ctx.display.write('\n');
131
+ ctx.display.write(' ' + (0, theme_1.bold)(theme_1.c.primary('Aiden walkthrough')) + '\n');
132
+ ctx.display.write(' ' + theme_1.c.muted('Five short screens. Each ends with a prompt you can copy-paste into chat.') + '\n');
133
+ for (let i = 0; i < SCREENS.length; i++) {
134
+ renderScreen(ctx, SCREENS[i], i, SCREENS.length);
135
+ }
136
+ ctx.display.write('\n ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n');
137
+ ctx.display.write(' ' + theme_1.c.muted('That\'s the tour. Type something — Aiden is listening.') + '\n\n');
138
+ return {};
139
+ },
140
+ };
@@ -87,7 +87,10 @@ function buildDaemonAgentBuilder(deps) {
87
87
  };
88
88
  const agent = new aidenAgent_1.AidenAgent({
89
89
  provider: adapter,
90
- tools: deps.toolRegistry.getSchemas(),
90
+ // v4.6 Phase 1 — 'daemon' context filter excludes REPL-only
91
+ // tools (`spawn_sub_agent` per Q6). Tools without an explicit
92
+ // `contexts` field stay visible to both REPL and daemon.
93
+ tools: deps.toolRegistry.getSchemas(undefined, 'daemon'),
91
94
  toolExecutor: deps.toolExecutor,
92
95
  maxTurns,
93
96
  auxiliaryClient: deps.auxiliaryClient,
@@ -30,7 +30,7 @@ exports.PREVIOUS_BUNDLED_SOULS = exports.DEFAULT_SOUL_MD = exports.BUNDLED_SOUL_
30
30
  // <act_dont_ask>. ensureSoulMdSeeded compares this against the user's
31
31
  // on-disk SOUL.md to decide whether to silent-replace (matches a prior
32
32
  // bundled default) or preserve+notify (user-edited).
33
- exports.BUNDLED_SOUL_VERSION = 'v4.5.0';
33
+ exports.BUNDLED_SOUL_VERSION = 'v4.6.0';
34
34
  exports.DEFAULT_SOUL_MD = `You are Aiden — a local-first AI agent built by Shiva Deore at Taracod.
35
35
 
36
36
  Identity:
@@ -0,0 +1,162 @@
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
+ * cli/v4/onboarding/disclaimer.ts — ONB1 slice 3.
10
+ *
11
+ * First screen of the redesigned first-run experience. Renders the
12
+ * framed AIDEN banner + tagline + credits + a single-paragraph
13
+ * disclaimer paragraph + Y/n prompt. Default Y. Typing 'n' or 'N'
14
+ * exits with a friendly goodbye line; ENTER (or 'y'/'Y') advances.
15
+ *
16
+ * Caller is responsible for branching on the returned `ok` boolean.
17
+ * This function NEVER calls process.exit — keeps it testable, and
18
+ * the parent (aidenCLI) controls the exit code centrally.
19
+ *
20
+ * TTY guard: non-TTY callers (systemd / launchd / CI / pipe) return
21
+ * `{ ok: true, skipped: true }` immediately so the wider boot path
22
+ * falls through to explore-mode wiring as today.
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.showDisclaimer = showDisclaimer;
59
+ const readline = __importStar(require("node:readline"));
60
+ const banner_1 = require("../../../core/v4/ui/banner");
61
+ const theme_1 = require("../../../core/v4/ui/theme");
62
+ const version_1 = require("../../../core/version");
63
+ const DISCLAIMER_PARA = 'Aiden is a semi-autonomous AI agent that can touch your files, ' +
64
+ 'browser, and shell. Open source — read the code if you want. ' +
65
+ 'Started as a hobby project, built solo, still rough in spots.';
66
+ /**
67
+ * Word-wrap `text` to `width` columns. Preserves single spaces; does
68
+ * not handle ANSI codes (callers pass plain text here).
69
+ */
70
+ function wrap(text, width) {
71
+ const words = text.split(/\s+/);
72
+ const lines = [];
73
+ let current = '';
74
+ for (const w of words) {
75
+ if (!current) {
76
+ current = w;
77
+ continue;
78
+ }
79
+ if (current.length + 1 + w.length > width) {
80
+ lines.push(current);
81
+ current = w;
82
+ }
83
+ else {
84
+ current += ' ' + w;
85
+ }
86
+ }
87
+ if (current)
88
+ lines.push(current);
89
+ return lines;
90
+ }
91
+ /**
92
+ * Clear the screen if stdout is a TTY. No-op otherwise.
93
+ */
94
+ function clearScreen(out) {
95
+ if (!out.isTTY)
96
+ return;
97
+ out.write('\x1b[2J\x1b[H');
98
+ }
99
+ /**
100
+ * Render the disclaimer body — banner + separator + wrapped paragraph.
101
+ * Pure renderer; caller owns the write.
102
+ */
103
+ function renderDisclaimerBody(version) {
104
+ const w = (0, theme_1.termWidth)();
105
+ const body = [];
106
+ body.push((0, banner_1.renderBanner)({ version }));
107
+ body.push(' ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n');
108
+ body.push('\n');
109
+ const indent = ' ';
110
+ for (const line of wrap(DISCLAIMER_PARA, Math.min(w - 4, 70))) {
111
+ body.push(indent + theme_1.c.text(line) + '\n');
112
+ }
113
+ body.push('\n');
114
+ return body.join('');
115
+ }
116
+ /**
117
+ * Read a single line from stdin. Resolves with the trimmed input.
118
+ * Rejects on close-without-input (typical when stdin is closed under us).
119
+ */
120
+ function promptYesNo(inStream, outStream, question) {
121
+ return new Promise((resolve) => {
122
+ const rl = readline.createInterface({ input: inStream, output: outStream });
123
+ rl.question(question, (answer) => {
124
+ rl.close();
125
+ resolve((answer ?? '').trim());
126
+ });
127
+ });
128
+ }
129
+ /**
130
+ * Show the disclaimer screen and collect Y/n. ENTER == accept; 'n'/'N'
131
+ * == decline. Anything else also counts as accept (matches Claude
132
+ * Code / Codex defaults — users hammer enter and expect to advance).
133
+ */
134
+ async function showDisclaimer(opts = {}) {
135
+ const out = opts.out ?? process.stdout;
136
+ const inStream = opts.in ?? process.stdin;
137
+ const version = opts.version ?? version_1.VERSION;
138
+ // Non-TTY: skip the prompt entirely. Caller falls through to whatever
139
+ // non-interactive path is appropriate (explore mode in aidenCLI today).
140
+ if (!out.isTTY || !inStream.isTTY) {
141
+ return { ok: true, skipped: true };
142
+ }
143
+ clearScreen(out);
144
+ out.write(renderDisclaimerBody(version));
145
+ const question = ' ' + theme_1.c.accent('Continue?') + ' ' + theme_1.c.muted('[Y/n] ');
146
+ const answer = await promptYesNo(inStream, out, question);
147
+ if (answer === 'n' || answer === 'N' || answer.toLowerCase() === 'no') {
148
+ out.write('\n ' + theme_1.c.muted('No problem — run `aiden` anytime to come back.') + '\n\n');
149
+ return { ok: false, reason: 'user-declined' };
150
+ }
151
+ return { ok: true };
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Direct invocation for visual smoke test:
155
+ // npx ts-node cli/v4/onboarding/disclaimer.ts
156
+ // ---------------------------------------------------------------------------
157
+ if (require.main === module) {
158
+ showDisclaimer().then((r) => {
159
+ process.stdout.write(`\n[result] ${JSON.stringify(r)}\n`);
160
+ process.exit(r.ok ? 0 : 1);
161
+ });
162
+ }
@@ -0,0 +1,208 @@
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
+ * cli/v4/onboarding/loading.ts — ONB1 slice 4.
10
+ *
11
+ * Animated multi-step loading screen rendered AFTER the disclaimer
12
+ * accept and BEFORE the provider picker. Each step does real work
13
+ * (no sleep-only fakery); the spinner shows for at least 300ms per
14
+ * step so the user actually perceives motion. Spinner replaces with
15
+ * a green ✓ on success or red ✗ on failure; right-aligned status
16
+ * text gives the answer (e.g. "Node v22 · Windows 11", "74 loaded").
17
+ *
18
+ * Setting up Aiden...
19
+ *
20
+ * ✓ Checking system Node v22 · Windows 11 [180ms]
21
+ * ✓ Loading skills 74 loaded [640ms]
22
+ * ✓ Initializing tools 60 tools [310ms]
23
+ * ✓ Configuring memory ~/.aiden/ created [420ms]
24
+ *
25
+ * ────────────────────────────────────────────────────────────
26
+ *
27
+ * Step contract: each step is a sync-or-async function returning
28
+ * `{ status: string }` (the right-aligned answer). Throwing converts
29
+ * to ✗ with the error message as status; the runner continues to
30
+ * subsequent steps so a single failure doesn't strand the user.
31
+ */
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runLoadingSequence = runLoadingSequence;
37
+ exports.defaultLoadingSteps = defaultLoadingSteps;
38
+ const node_os_1 = __importDefault(require("node:os"));
39
+ const node_fs_1 = require("node:fs");
40
+ const node_path_1 = __importDefault(require("node:path"));
41
+ const theme_1 = require("../../../core/v4/ui/theme");
42
+ const paths_1 = require("../../../core/v4/paths");
43
+ const MIN_SPINNER_MS = 300;
44
+ const SPIN_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
45
+ const SPIN_INTERVAL = 80;
46
+ function rowGeom(width) {
47
+ const inner = Math.min(width - 4, 70);
48
+ const labelCol = Math.floor(inner * 0.45);
49
+ const statusCol = inner - labelCol;
50
+ return { labelCol, statusCol };
51
+ }
52
+ function rpad(s, n) {
53
+ return s.length >= n ? s : s + ' '.repeat(n - s.length);
54
+ }
55
+ function lpad(s, n) {
56
+ return s.length >= n ? s : ' '.repeat(n - s.length) + s;
57
+ }
58
+ /**
59
+ * Run all steps sequentially and render the live multi-row pane.
60
+ * Non-TTY: emits one line per step on completion with no spinner.
61
+ */
62
+ async function runLoadingSequence(steps, opts = {}) {
63
+ const out = opts.out ?? process.stdout;
64
+ const heading = opts.heading ?? 'Setting up Aiden...';
65
+ const w = (0, theme_1.termWidth)();
66
+ const { labelCol, statusCol } = rowGeom(w);
67
+ const isTty = !!out.isTTY;
68
+ const results = [];
69
+ if (!isTty) {
70
+ out.write(`${heading}\n`);
71
+ for (const step of steps) {
72
+ const t0 = Date.now();
73
+ try {
74
+ const r = await step.run();
75
+ const ms = Date.now() - t0;
76
+ results.push({ label: step.label, ok: true, status: r.status, ms });
77
+ out.write(` ok ${step.label} ${r.status} [${ms}ms]\n`);
78
+ }
79
+ catch (err) {
80
+ const ms = Date.now() - t0;
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ results.push({ label: step.label, ok: false, status: msg, ms });
83
+ out.write(` err ${step.label} ${msg} [${ms}ms]\n`);
84
+ }
85
+ }
86
+ return { ok: results.every((r) => r.ok), steps: results };
87
+ }
88
+ out.write('\n ' + theme_1.c.text(heading) + '\n\n');
89
+ // Pre-paint placeholder rows so the spinner overwrites in place.
90
+ for (const step of steps) {
91
+ const line = ' ' + theme_1.c.muted('·') + ' ' + rpad(step.label, labelCol) +
92
+ ' ' + theme_1.c.muted(lpad('—', statusCol));
93
+ out.write(line + '\n');
94
+ }
95
+ // Walk back up to the top of the block.
96
+ out.write(`\x1b[${steps.length}A`);
97
+ for (let i = 0; i < steps.length; i++) {
98
+ const step = steps[i];
99
+ const t0 = Date.now();
100
+ let frame = 0;
101
+ const spinner = setInterval(() => {
102
+ const glyph = theme_1.c.primary(SPIN_FRAMES[frame % SPIN_FRAMES.length]);
103
+ out.write('\x1b[2K\r ' + glyph + ' ' + theme_1.c.text(rpad(step.label, labelCol)));
104
+ frame += 1;
105
+ }, SPIN_INTERVAL);
106
+ let ok = true;
107
+ let status = '';
108
+ try {
109
+ const r = await step.run();
110
+ status = r.status;
111
+ }
112
+ catch (err) {
113
+ ok = false;
114
+ status = err instanceof Error ? err.message : String(err);
115
+ }
116
+ // Enforce a minimum perceptible spinner duration.
117
+ const elapsed = Date.now() - t0;
118
+ if (elapsed < MIN_SPINNER_MS) {
119
+ await new Promise((r) => setTimeout(r, MIN_SPINNER_MS - elapsed));
120
+ }
121
+ clearInterval(spinner);
122
+ const ms = Date.now() - t0;
123
+ const glyph = ok ? theme_1.c.success('✓') : theme_1.c.error('✗');
124
+ const statusText = ok ? theme_1.c.muted(status) : theme_1.c.error(status);
125
+ const timing = theme_1.c.muted(`[${ms}ms]`);
126
+ const row = ' ' + glyph + ' ' + theme_1.c.text(rpad(step.label, labelCol)) +
127
+ ' ' + lpad(statusText, statusCol) + ' ' + timing;
128
+ out.write('\x1b[2K\r' + row + '\n');
129
+ results.push({ label: step.label, ok, status, ms });
130
+ }
131
+ out.write('\n ' + (0, theme_1.separator)(Math.min(w - 4, 64)) + '\n');
132
+ return { ok: results.every((r) => r.ok), steps: results };
133
+ }
134
+ // ---------------------------------------------------------------------------
135
+ // Default step set — the real-work pipeline used by the wizard entry point.
136
+ // Exported so the wizard composer can extend / reorder; the smoke test
137
+ // also reuses these to verify the real numbers shown match disk state.
138
+ // ---------------------------------------------------------------------------
139
+ function defaultLoadingSteps(paths) {
140
+ const resolved = paths ?? (0, paths_1.resolveAidenPaths)();
141
+ return [
142
+ {
143
+ label: 'Checking system',
144
+ run: () => {
145
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
146
+ if (!Number.isFinite(nodeMajor) || nodeMajor < 18) {
147
+ throw new Error(`Node ${process.versions.node} (need 18+)`);
148
+ }
149
+ const plat = process.platform === 'win32'
150
+ ? `Windows ${node_os_1.default.release().split('.')[0]}`
151
+ : process.platform === 'darwin'
152
+ ? `macOS ${node_os_1.default.release()}`
153
+ : `${process.platform} ${node_os_1.default.release()}`;
154
+ return { status: `Node v${nodeMajor} · ${plat}` };
155
+ },
156
+ },
157
+ {
158
+ label: 'Loading skills',
159
+ run: async () => {
160
+ // Walk skills/ at repo root + user dir; count SKILL.md files.
161
+ let count = 0;
162
+ const dirs = [
163
+ node_path_1.default.join(process.cwd(), 'skills'),
164
+ resolved.skillsDir,
165
+ ];
166
+ for (const d of dirs) {
167
+ try {
168
+ const entries = await node_fs_1.promises.readdir(d, { withFileTypes: true });
169
+ for (const e of entries) {
170
+ if (!e.isDirectory())
171
+ continue;
172
+ try {
173
+ await node_fs_1.promises.access(node_path_1.default.join(d, e.name, 'SKILL.md'));
174
+ count += 1;
175
+ }
176
+ catch { /* not a skill — skip */ }
177
+ }
178
+ }
179
+ catch { /* directory missing — skip */ }
180
+ }
181
+ return { status: `${count} available` };
182
+ },
183
+ },
184
+ {
185
+ label: 'Initializing tools',
186
+ run: async () => {
187
+ // Best-effort tool count: read the tools/v4/index source as a
188
+ // text count of registrations. Cheap and good enough for the
189
+ // "60 tools" status — avoids loading the tool registry itself.
190
+ try {
191
+ const src = await node_fs_1.promises.readFile(node_path_1.default.join(process.cwd(), 'tools', 'v4', 'index.ts'), 'utf8');
192
+ const matches = src.match(/registerTool\s*\(/g) ?? [];
193
+ return { status: `${matches.length || 0} registered` };
194
+ }
195
+ catch {
196
+ return { status: 'registry ready' };
197
+ }
198
+ },
199
+ },
200
+ {
201
+ label: 'Configuring memory',
202
+ run: async () => {
203
+ await (0, paths_1.ensureAidenDirsExist)(resolved);
204
+ return { status: `${resolved.root} ready` };
205
+ },
206
+ },
207
+ ];
208
+ }