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.
- package/README.md +17 -2
- package/dist/cli/v4/aidenCLI.js +207 -100
- package/dist/cli/v4/chatSession.js +120 -0
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +2 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +8 -0
- package/dist/cli/v4/commands/index.js +21 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +22 -2
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/walkthrough.js +140 -0
- package/dist/cli/v4/daemonAgentBuilder.js +4 -1
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/onboarding/disclaimer.js +162 -0
- package/dist/cli/v4/onboarding/loading.js +208 -0
- package/dist/cli/v4/onboarding/providerPicker.js +126 -0
- package/dist/cli/v4/onboarding/successScreen.js +68 -0
- package/dist/cli/v4/repl/firstRunHint.js +107 -0
- package/dist/cli/v4/setupWizard.js +201 -31
- package/dist/core/v4/aidenAgent.js +219 -1
- package/dist/core/v4/daemon/bootstrap.js +47 -0
- package/dist/core/v4/daemon/db/migrations.js +66 -0
- package/dist/core/v4/daemon/runStore.js +33 -3
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/providers/modelFetch.js +179 -0
- package/dist/core/v4/providers/probe.js +275 -0
- package/dist/core/v4/runtimeToggles.js +30 -3
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/toolRegistry.js +19 -3
- package/dist/core/v4/ui/banner.js +133 -0
- package/dist/core/v4/ui/theme.js +164 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/index.js +17 -3
- package/dist/tools/v4/skills/lookupToolSchema.js +6 -1
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +53 -1
- package/dist/tools/v4/ui/_uiSmokeTool.js +60 -0
- 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
|
-
|
|
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.
|
|
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
|
+
}
|