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,126 @@
|
|
|
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/providerPicker.ts — ONB1 slice 5.
|
|
10
|
+
*
|
|
11
|
+
* Rich provider picker for the redesigned first-run flow. Replaces
|
|
12
|
+
* the wizard's plain `prompts.choose(question, labels[])` call with
|
|
13
|
+
* an @inquirer/prompts `select` that renders:
|
|
14
|
+
*
|
|
15
|
+
* ❯ Claude (Anthropic) Best for code · API key
|
|
16
|
+
* ChatGPT (OpenAI) Most popular · API key
|
|
17
|
+
* Groq Free, fast · Free
|
|
18
|
+
* Gemini (Google) Free tier · Free
|
|
19
|
+
* Ollama Offline · Local
|
|
20
|
+
* Claude Pro Subscription · OAuth
|
|
21
|
+
* ChatGPT Plus Subscription · OAuth
|
|
22
|
+
* Other Custom URL · Custom
|
|
23
|
+
*
|
|
24
|
+
* Badge → colour:
|
|
25
|
+
* Free → success green
|
|
26
|
+
* API key → accent (light orange)
|
|
27
|
+
* OAuth → primary brand orange
|
|
28
|
+
* Local → muted
|
|
29
|
+
* Custom → muted
|
|
30
|
+
*
|
|
31
|
+
* Esc / Ctrl+C handling: inquirer raises a "force closed" Error; the
|
|
32
|
+
* wizard's outer loop already converts that to a graceful explore-mode
|
|
33
|
+
* exit, so we re-raise unchanged.
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.toRichChoice = toRichChoice;
|
|
37
|
+
exports.pickProvider = pickProvider;
|
|
38
|
+
const theme_1 = require("../../../core/v4/ui/theme");
|
|
39
|
+
/**
|
|
40
|
+
* Derive a RichChoice from a `ProviderOption`. The existing wizard
|
|
41
|
+
* labels are `<shortLabel> — <description>` strings — we split on the
|
|
42
|
+
* em-dash to get a clean description, and map `kind` to a badge.
|
|
43
|
+
*/
|
|
44
|
+
function toRichChoice(p) {
|
|
45
|
+
const parts = p.label.split(' — ');
|
|
46
|
+
const description = parts.length > 1 ? parts.slice(1).join(' — ') : p.shortLabel;
|
|
47
|
+
let badge;
|
|
48
|
+
if (p.kind === 'local')
|
|
49
|
+
badge = 'local';
|
|
50
|
+
else if (p.kind === 'custom')
|
|
51
|
+
badge = 'custom';
|
|
52
|
+
else if (p.kind === 'pro' || p.kind === 'oauth')
|
|
53
|
+
badge = 'oauth';
|
|
54
|
+
else if (/free/i.test(p.label))
|
|
55
|
+
badge = 'free';
|
|
56
|
+
else
|
|
57
|
+
badge = 'api';
|
|
58
|
+
return { id: p.id, title: p.shortLabel, description, badge };
|
|
59
|
+
}
|
|
60
|
+
const BADGE_LABEL = {
|
|
61
|
+
free: 'Free',
|
|
62
|
+
api: 'API key',
|
|
63
|
+
oauth: 'OAuth',
|
|
64
|
+
local: 'Local',
|
|
65
|
+
custom: 'Custom',
|
|
66
|
+
};
|
|
67
|
+
function paintBadge(b) {
|
|
68
|
+
switch (b) {
|
|
69
|
+
case 'free': return theme_1.c.success(BADGE_LABEL[b]);
|
|
70
|
+
case 'api': return theme_1.c.accent(BADGE_LABEL[b]);
|
|
71
|
+
case 'oauth': return theme_1.c.primary(BADGE_LABEL[b]);
|
|
72
|
+
case 'local': return theme_1.c.muted(BADGE_LABEL[b]);
|
|
73
|
+
case 'custom': return theme_1.c.muted(BADGE_LABEL[b]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function rpad(s, n) {
|
|
77
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format one choice row for the picker. Layout:
|
|
81
|
+
*
|
|
82
|
+
* <title pad to titleW> <description pad to descW> · <badge>
|
|
83
|
+
*
|
|
84
|
+
* inquirer's `select` highlights the entire row when hovered; the
|
|
85
|
+
* title gets emphasised colour, description stays muted.
|
|
86
|
+
*/
|
|
87
|
+
function formatChoiceRow(rc, titleW, descW) {
|
|
88
|
+
const title = theme_1.c.text(rpad(rc.title, titleW));
|
|
89
|
+
const desc = theme_1.c.muted(rpad(rc.description, descW));
|
|
90
|
+
const badge = paintBadge(rc.badge);
|
|
91
|
+
return `${title} ${desc} · ${badge}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Show the rich picker and return the selected provider. The wizard's
|
|
95
|
+
* outer loop converts thrown "force closed" errors into the skipped
|
|
96
|
+
* explore-mode exit, so we re-raise unchanged on Ctrl+C / Esc.
|
|
97
|
+
*/
|
|
98
|
+
async function pickProvider(opts) {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
100
|
+
const inq = opts.inquirerImpl ?? require('@inquirer/prompts');
|
|
101
|
+
const rich = opts.providers.map(toRichChoice);
|
|
102
|
+
const w = (0, theme_1.termWidth)();
|
|
103
|
+
const titleW = Math.min(22, Math.max(...rich.map((r) => r.title.length)) + 2);
|
|
104
|
+
const descAvail = Math.max(20, w - titleW - 14);
|
|
105
|
+
const descW = Math.min(40, descAvail);
|
|
106
|
+
const choices = rich.map((rc, i) => ({
|
|
107
|
+
name: formatChoiceRow(rc, titleW, descW),
|
|
108
|
+
value: String(i),
|
|
109
|
+
description: theme_1.c.muted(`Select ${rc.title} (${BADGE_LABEL[rc.badge].toLowerCase()})`),
|
|
110
|
+
}));
|
|
111
|
+
const defaultIdx = opts.defaultId
|
|
112
|
+
? Math.max(0, opts.providers.findIndex((p) => p.id === opts.defaultId))
|
|
113
|
+
: 0;
|
|
114
|
+
const answer = (await inq.select({
|
|
115
|
+
message: theme_1.c.text('Pick a provider:'),
|
|
116
|
+
choices,
|
|
117
|
+
default: String(defaultIdx),
|
|
118
|
+
loop: false,
|
|
119
|
+
}));
|
|
120
|
+
const idx = Number.parseInt(answer, 10);
|
|
121
|
+
return {
|
|
122
|
+
id: opts.providers[idx].id,
|
|
123
|
+
index: idx,
|
|
124
|
+
choice: rich[idx],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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/successScreen.ts — ONB1 slice 8.
|
|
10
|
+
*
|
|
11
|
+
* Replaces the wizard's prior "Setup Complete" box that told the
|
|
12
|
+
* user to re-run `aiden` to start chatting. We DO NOT exit — the
|
|
13
|
+
* wizard already returns to the boot path, which then drops into
|
|
14
|
+
* the REPL. The old message was a lie of omission. The new screen
|
|
15
|
+
* says exactly what happens next:
|
|
16
|
+
*
|
|
17
|
+
* ──────────────────────────────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* All set!
|
|
20
|
+
*
|
|
21
|
+
* Aiden is ready. Try these to start:
|
|
22
|
+
*
|
|
23
|
+
* ▸ summarize the files in this folder
|
|
24
|
+
* ▸ what's running on my computer right now
|
|
25
|
+
* ▸ research the latest in AI agents and save to notes.md
|
|
26
|
+
*
|
|
27
|
+
* Or just say hi.
|
|
28
|
+
*
|
|
29
|
+
* ──────────────────────────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* Width-responsive: collapses example bullets to a single line at
|
|
32
|
+
* <60 cols. Non-TTY callers see a plain `setup-complete` line so
|
|
33
|
+
* scripted setups have a deterministic post-condition marker.
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.renderSuccessScreen = renderSuccessScreen;
|
|
37
|
+
const theme_1 = require("../../../core/v4/ui/theme");
|
|
38
|
+
const DEFAULT_EXAMPLES = [
|
|
39
|
+
'summarize the files in this folder',
|
|
40
|
+
'what\'s running on my computer right now',
|
|
41
|
+
'research the latest in AI agents and save to notes.md',
|
|
42
|
+
];
|
|
43
|
+
function renderSuccessScreen(opts = {}) {
|
|
44
|
+
const out = opts.out ?? process.stdout;
|
|
45
|
+
const examples = opts.examples ?? DEFAULT_EXAMPLES;
|
|
46
|
+
if (!out.isTTY) {
|
|
47
|
+
out.write('setup-complete\n');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const w = (0, theme_1.termWidth)();
|
|
51
|
+
const sepW = Math.min(w - 4, 64);
|
|
52
|
+
const narrow = w < 60;
|
|
53
|
+
out.write('\n ' + (0, theme_1.separator)(sepW) + '\n');
|
|
54
|
+
out.write('\n ' + (0, theme_1.bold)(theme_1.c.primary('All set!')) + '\n');
|
|
55
|
+
out.write('\n ' + theme_1.c.text('Aiden is ready. Try these to start:') + '\n');
|
|
56
|
+
out.write('\n');
|
|
57
|
+
if (narrow) {
|
|
58
|
+
// Compact: a single suggestion line + the muted hello fallback.
|
|
59
|
+
out.write(' ' + theme_1.c.muted('▸ ') + theme_1.c.accent(examples[0]) + '\n');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
for (const ex of examples) {
|
|
63
|
+
out.write(' ' + theme_1.c.muted('▸ ') + theme_1.c.accent(ex) + '\n');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
out.write('\n ' + theme_1.c.muted('Or just say hi.') + '\n');
|
|
67
|
+
out.write('\n ' + (0, theme_1.separator)(sepW) + '\n\n');
|
|
68
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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/repl/firstRunHint.ts — ONB1 slice 9.
|
|
10
|
+
*
|
|
11
|
+
* One-time hint banner shown immediately below the standard boot
|
|
12
|
+
* card (status pills + source annotation) on the very first REPL
|
|
13
|
+
* session after a successful setup. Single muted line:
|
|
14
|
+
*
|
|
15
|
+
* Tip: try /walkthrough for a 60-second tour of what Aiden can do
|
|
16
|
+
*
|
|
17
|
+
* Dismissal is durable — once the user sends a first message OR
|
|
18
|
+
* runs `/dismiss`, we write a marker at `<paths.root>/.first-run-shown`
|
|
19
|
+
* so subsequent boots never re-show the line. The marker is plain
|
|
20
|
+
* text (single line: ISO timestamp) so an operator can `rm` it to
|
|
21
|
+
* see the hint again without other side effects.
|
|
22
|
+
*
|
|
23
|
+
* The hint is also suppressed on non-TTY callers (no point hinting
|
|
24
|
+
* at scripted callers that don't have a `/walkthrough` slash to run).
|
|
25
|
+
*/
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.isFirstRunHintShown = isFirstRunHintShown;
|
|
31
|
+
exports.renderFirstRunHint = renderFirstRunHint;
|
|
32
|
+
exports.markFirstRunHintDismissed = markFirstRunHintDismissed;
|
|
33
|
+
exports.resetFirstRunHint = resetFirstRunHint;
|
|
34
|
+
const node_fs_1 = require("node:fs");
|
|
35
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
36
|
+
const theme_1 = require("../../../core/v4/ui/theme");
|
|
37
|
+
const MARKER_NAME = '.first-run-shown';
|
|
38
|
+
function markerPath(paths) {
|
|
39
|
+
return node_path_1.default.join(paths.root, MARKER_NAME);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns true if the marker exists — caller should NOT render the
|
|
43
|
+
* hint. Returns false on any error (e.g. marker missing, fs read
|
|
44
|
+
* fails) so a corrupt state is treated as "show again" rather than
|
|
45
|
+
* silently hiding the hint forever.
|
|
46
|
+
*/
|
|
47
|
+
async function isFirstRunHintShown(paths) {
|
|
48
|
+
try {
|
|
49
|
+
await node_fs_1.promises.access(markerPath(paths));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Render the hint line if it hasn't been dismissed yet. Returns
|
|
58
|
+
* true when the line was painted (so the caller can adjust spacing).
|
|
59
|
+
* Mark-on-render: we write the dismissed-marker IMMEDIATELY after
|
|
60
|
+
* painting so the hint shows exactly once even if the user Ctrl+Cs
|
|
61
|
+
* before sending a first message. The "missed write" branch falls
|
|
62
|
+
* through silently — on the next boot the user may see the hint
|
|
63
|
+
* one more time, which is benign degradation.
|
|
64
|
+
*/
|
|
65
|
+
async function renderFirstRunHint(opts) {
|
|
66
|
+
const out = opts.out ?? process.stdout;
|
|
67
|
+
if (!out.isTTY)
|
|
68
|
+
return false;
|
|
69
|
+
if (await isFirstRunHintShown(opts.paths))
|
|
70
|
+
return false;
|
|
71
|
+
const line = ' ' + theme_1.c.muted('Tip:') + ' ' +
|
|
72
|
+
(0, theme_1.italic)(theme_1.c.muted('try ')) +
|
|
73
|
+
theme_1.c.accent('/walkthrough') +
|
|
74
|
+
(0, theme_1.italic)(theme_1.c.muted(' for a 60-second tour of what Aiden can do'));
|
|
75
|
+
out.write(line + '\n\n');
|
|
76
|
+
await markFirstRunHintDismissed(opts.paths);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Write the dismissed-marker. Idempotent. Caller should fire this
|
|
81
|
+
* once the user has either (a) sent their first message, or (b)
|
|
82
|
+
* invoked /dismiss. Failures are swallowed — a missed write just
|
|
83
|
+
* means the hint shows once more on the next boot, which is a
|
|
84
|
+
* benign degradation.
|
|
85
|
+
*/
|
|
86
|
+
async function markFirstRunHintDismissed(paths) {
|
|
87
|
+
try {
|
|
88
|
+
await node_fs_1.promises.mkdir(paths.root, { recursive: true });
|
|
89
|
+
await node_fs_1.promises.writeFile(markerPath(paths), new Date().toISOString() + '\n', { encoding: 'utf8' });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// best-effort — see jsdoc
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Test / debug helper. Removes the marker so the hint shows again on
|
|
97
|
+
* the next boot. Returns true when a marker was actually removed.
|
|
98
|
+
*/
|
|
99
|
+
async function resetFirstRunHint(paths) {
|
|
100
|
+
try {
|
|
101
|
+
await node_fs_1.promises.unlink(markerPath(paths));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -38,10 +38,17 @@ const kleur_1 = __importDefault(require("kleur"));
|
|
|
38
38
|
const paths_1 = require("../../core/v4/paths");
|
|
39
39
|
const config_1 = require("../../core/v4/config");
|
|
40
40
|
const display_1 = require("./display");
|
|
41
|
-
const keyValidator_1 = require("./keyValidator");
|
|
42
41
|
const providerAuth_1 = require("../../core/v4/auth/providerAuth");
|
|
43
42
|
const loadProvider_1 = require("./auth/loadProvider");
|
|
44
43
|
const box_1 = require("./box");
|
|
44
|
+
// ONB1-WIRE-2 — onboarding helpers consumed by the wizard. Static
|
|
45
|
+
// imports (not runtime require) so vitest's transpilation resolves
|
|
46
|
+
// the TS extension; the lazy-load benefit was marginal compared to
|
|
47
|
+
// the cost of broken unit tests under the test runtime.
|
|
48
|
+
const successScreen_1 = require("./onboarding/successScreen");
|
|
49
|
+
const providerPicker_1 = require("./onboarding/providerPicker");
|
|
50
|
+
const modelFetch_1 = require("../../core/v4/providers/modelFetch");
|
|
51
|
+
const probe_1 = require("../../core/v4/providers/probe");
|
|
45
52
|
// Phase 30.2.1 — provider order optimised for new-user time-to-first-chat.
|
|
46
53
|
// Free providers first (Groq → Gemini → OpenRouter → NVIDIA → Ollama),
|
|
47
54
|
// paid providers next (Anthropic, OpenAI, Together), subscription
|
|
@@ -518,7 +525,18 @@ async function runSetupWizard(opts = {}) {
|
|
|
518
525
|
};
|
|
519
526
|
}
|
|
520
527
|
await (0, paths_1.ensureAidenDirsExist)(paths);
|
|
521
|
-
|
|
528
|
+
// ONB1-WIRE-2 Slice A — drop the duplicate AIDEN banner in the
|
|
529
|
+
// real-terminal flow. The disclaimer screen (ONB1 slice 3) already
|
|
530
|
+
// paints the framed banner before the wizard runs in the
|
|
531
|
+
// fresh-install path, so a second printBanner() here produced a
|
|
532
|
+
// visually jarring double-banner. The test fixtures pre-date the
|
|
533
|
+
// disclaimer screen and assert on the banner's presence, so we
|
|
534
|
+
// keep printing it when a scripted `opts.prompts` is injected
|
|
535
|
+
// (only unit tests do that). For `aiden setup` / `/setup` re-runs
|
|
536
|
+
// the welcome line alone is enough.
|
|
537
|
+
if (opts.prompts) {
|
|
538
|
+
display.printBanner();
|
|
539
|
+
}
|
|
522
540
|
display.write('\nWelcome — let\'s pick a provider.\n');
|
|
523
541
|
display.write(`${kleur_1.default.dim('(Press Enter to accept Groq — free + fastest setup.)')}\n\n`);
|
|
524
542
|
// Phase 30.2.1 — Groq is the new recommended default for first-time
|
|
@@ -532,11 +550,27 @@ async function runSetupWizard(opts = {}) {
|
|
|
532
550
|
// the prompt") into the same "skipped" exit state as recovery [4]
|
|
533
551
|
// — the user clearly didn't want to finish, but we still want them
|
|
534
552
|
// to land in REPL "explore mode" rather than crash.
|
|
553
|
+
//
|
|
554
|
+
// ONB1 slice 5 — the picker now uses the rich provider-picker
|
|
555
|
+
// (description column + Free/API/OAuth badges) when the caller
|
|
556
|
+
// hasn't injected a custom `prompts` (which means we're in a real
|
|
557
|
+
// terminal, not a unit test). Stubbed-prompts callers fall through
|
|
558
|
+
// to the legacy `prompts.choose` path so existing fixtures keep
|
|
559
|
+
// working unchanged.
|
|
535
560
|
// eslint-disable-next-line no-constant-condition
|
|
536
561
|
outer: while (true) {
|
|
537
562
|
let providerIndex;
|
|
538
563
|
try {
|
|
539
|
-
|
|
564
|
+
if (!opts.prompts) {
|
|
565
|
+
const picked = await (0, providerPicker_1.pickProvider)({
|
|
566
|
+
providers: exports.PROVIDERS,
|
|
567
|
+
defaultId: 'groq',
|
|
568
|
+
});
|
|
569
|
+
providerIndex = picked.index + 1; // back to 1-based for the rest of the loop
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
providerIndex = await prompts.choose('Which provider would you like to use?', exports.PROVIDERS.map((p) => p.label), groqDefaultIdx > 0 ? groqDefaultIdx : undefined);
|
|
573
|
+
}
|
|
540
574
|
}
|
|
541
575
|
catch (err) {
|
|
542
576
|
const msg = err?.message ?? '';
|
|
@@ -634,27 +668,33 @@ async function runSetupWizard(opts = {}) {
|
|
|
634
668
|
}
|
|
635
669
|
display.write(` Tokens stored at: ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)}\n`);
|
|
636
670
|
display.write(` Expires: ${expIso}\n`);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
671
|
+
// ONB1 slice 7: encryption disclosure demoted from a paragraph to
|
|
672
|
+
// a one-line `?` hint. The full explainer remains available via
|
|
673
|
+
// `aiden doctor` — the wizard is the wrong moment for a security
|
|
674
|
+
// primer.
|
|
675
|
+
display.write(`${kleur_1.default.dim(' Tokens encrypted at rest · run `aiden doctor` for details')}\n`);
|
|
676
|
+
// ONB1 slice 8: success screen replaces the prior "Try: aiden" tail.
|
|
677
|
+
// The wizard already returns to the boot path, which then drops into
|
|
678
|
+
// the REPL — no process restart needed.
|
|
679
|
+
(0, successScreen_1.renderSuccessScreen)({ out: process.stdout });
|
|
641
680
|
return { status: 'configured', ran: true, config, envFile: paths.envFile };
|
|
642
681
|
}
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
//
|
|
682
|
+
// ONB1-WIRE-2 Slice B — flow reorder + live model fetch.
|
|
683
|
+
//
|
|
684
|
+
// Old order: pick model → ask key → validate. That worked because
|
|
685
|
+
// model selection came from the curated PROVIDERS.models array and
|
|
686
|
+
// didn't need the key. Live fetch from /models endpoints requires
|
|
687
|
+
// the key (Anthropic, OpenAI, Groq, Gemini all gate /models behind
|
|
688
|
+
// auth), so we now ask for credentials FIRST and pick the model
|
|
689
|
+
// FROM the live response. Falls back to the curated MODEL_CATALOG
|
|
690
|
+
// when the live endpoint is unreachable.
|
|
691
|
+
// Step 2: credentials (moved up from old step 3 for key/subscription).
|
|
692
|
+
//
|
|
693
|
+
// `custom` keeps the legacy "model id first, then baseUrl + apiKey"
|
|
694
|
+
// order — it has no live-fetch endpoint we could call with the key
|
|
695
|
+
// anyway, so the reorder bought nothing there. Existing test
|
|
696
|
+
// fixtures provide inputs in legacy order; preserving custom's
|
|
697
|
+
// order keeps them green.
|
|
658
698
|
let apiKey;
|
|
659
699
|
let baseUrl;
|
|
660
700
|
if (provider.kind === 'local') {
|
|
@@ -666,15 +706,83 @@ async function runSetupWizard(opts = {}) {
|
|
|
666
706
|
continue outer;
|
|
667
707
|
}
|
|
668
708
|
}
|
|
669
|
-
else if (provider.kind === 'custom') {
|
|
670
|
-
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)');
|
|
671
|
-
apiKey = await prompts.input('API key', { mask: true });
|
|
672
|
-
}
|
|
673
709
|
else if (provider.kind === 'key' || provider.kind === 'subscription') {
|
|
674
710
|
if (provider.envVar) {
|
|
675
711
|
apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
|
|
676
712
|
}
|
|
677
713
|
}
|
|
714
|
+
// provider.kind === 'custom' — defer credential prompts until AFTER
|
|
715
|
+
// the model picker below.
|
|
716
|
+
// Step 3: live model fetch + pick.
|
|
717
|
+
//
|
|
718
|
+
// Test-harness gate: when the caller injected `opts.prompts` (only
|
|
719
|
+
// unit tests do this), skip the live fetch and fall back to the
|
|
720
|
+
// curated PROVIDERS.models picker. Live fetch needs a runtime
|
|
721
|
+
// `require` of core/v4/providers/modelFetch which vitest can't
|
|
722
|
+
// resolve to .ts without a loader, and tests don't need a network
|
|
723
|
+
// round-trip anyway. Matches the picker-upgrade gate (slice 5).
|
|
724
|
+
let modelId = provider.defaultModel ?? '';
|
|
725
|
+
if (opts.prompts) {
|
|
726
|
+
// Legacy curated path — unchanged from pre-Slice-B behaviour.
|
|
727
|
+
if (provider.models && provider.models.length > 1) {
|
|
728
|
+
const modelIndex = await prompts.choose(`Pick a model for ${provider.shortLabel}`, provider.models);
|
|
729
|
+
modelId = provider.models[modelIndex - 1];
|
|
730
|
+
}
|
|
731
|
+
else if (provider.kind === 'local') {
|
|
732
|
+
modelId = await prompts.input('Ollama model id', {
|
|
733
|
+
default: provider.defaultModel ?? 'llama3.1:8b',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
else if (!modelId) {
|
|
737
|
+
modelId = await prompts.input('Model id', { default: '' });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
const spinner = display.startSpinner(`Fetching available models for ${provider.shortLabel}…`);
|
|
742
|
+
let fetchResult;
|
|
743
|
+
try {
|
|
744
|
+
fetchResult = await (0, modelFetch_1.fetchModels)({ providerId: provider.id, apiKey, baseUrl, fetchImpl });
|
|
745
|
+
}
|
|
746
|
+
finally {
|
|
747
|
+
spinner.stop();
|
|
748
|
+
}
|
|
749
|
+
if (fetchResult.source === 'fallback' && fetchResult.reason) {
|
|
750
|
+
display.write(`${kleur_1.default.dim(` Couldn't reach API — showing recommended models offline (${fetchResult.reason})`)}\n`);
|
|
751
|
+
}
|
|
752
|
+
else if (fetchResult.source === 'live') {
|
|
753
|
+
display.write(`${kleur_1.default.dim(` Live from ${provider.shortLabel} API · ${fetchResult.models.length} model${fetchResult.models.length === 1 ? '' : 's'}`)}\n`);
|
|
754
|
+
}
|
|
755
|
+
if (fetchResult.models.length === 0) {
|
|
756
|
+
// No models from live or static catalog — fall back to a free-text input.
|
|
757
|
+
if (provider.kind === 'local') {
|
|
758
|
+
modelId = await prompts.input('Ollama model id', {
|
|
759
|
+
default: provider.defaultModel ?? 'llama3.1:8b',
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
modelId = await prompts.input('Model id', { default: provider.defaultModel ?? '' });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else if (fetchResult.models.length === 1) {
|
|
767
|
+
modelId = fetchResult.models[0].id;
|
|
768
|
+
display.write(`${kleur_1.default.dim(` Only one model available — using ${modelId}.`)}\n`);
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
// Render picker. modelFetch already sorts recommended first; we
|
|
772
|
+
// append a `· recommended` marker so the user spots them visually.
|
|
773
|
+
const labels = fetchResult.models.map((m) => m.recommended ? `${m.displayName} · recommended` : m.displayName);
|
|
774
|
+
const recIdx = fetchResult.models.findIndex((m) => m.recommended);
|
|
775
|
+
const defaultIdx = recIdx >= 0 ? recIdx + 1 : 1;
|
|
776
|
+
const idx = await prompts.choose(`Pick a model for ${provider.shortLabel}`, labels, defaultIdx);
|
|
777
|
+
modelId = fetchResult.models[idx - 1].id;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// Custom-provider credentials: deferred from step 2 above so the
|
|
781
|
+
// legacy input order (model → baseUrl → apiKey) is preserved.
|
|
782
|
+
if (provider.kind === 'custom') {
|
|
783
|
+
baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)');
|
|
784
|
+
apiKey = await prompts.input('API key', { mask: true });
|
|
785
|
+
}
|
|
678
786
|
// Step 3.5: validate the API key against the provider endpoint.
|
|
679
787
|
// Bypassed when smokeTest or skipValidation is set, or when there's no key
|
|
680
788
|
// to validate (Ollama, or a subscription provider without an env var).
|
|
@@ -683,7 +791,49 @@ async function runSetupWizard(opts = {}) {
|
|
|
683
791
|
typeof apiKey === 'string' &&
|
|
684
792
|
apiKey.length > 0;
|
|
685
793
|
if (shouldValidate) {
|
|
686
|
-
|
|
794
|
+
// ONB1-WIRE-2 Slice C — three-step probe replaces the legacy
|
|
795
|
+
// single-shot validateProviderKey. The probe runs 3 internal
|
|
796
|
+
// round-trips (auth → model access → tool support) and returns a
|
|
797
|
+
// .steps[] trace; we render each step's outcome as a ✓/✗ row
|
|
798
|
+
// AFTER the spinner stops to avoid the spinner clobbering the row
|
|
799
|
+
// writes mid-render. Test injection via opts.validator falls
|
|
800
|
+
// through to the legacy validateProviderKey shape so existing
|
|
801
|
+
// unit-test fixtures keep working unchanged.
|
|
802
|
+
//
|
|
803
|
+
const STEP_LABELS = {
|
|
804
|
+
auth: 'Sending test request',
|
|
805
|
+
model: 'Verifying model access',
|
|
806
|
+
tools: 'Checking tool calls',
|
|
807
|
+
};
|
|
808
|
+
let lastProbe = null;
|
|
809
|
+
const probeAdapter = async (providerId, key, baseUrlArg, fetchImplArg) => {
|
|
810
|
+
const probe = await (0, probe_1.runProbe)({
|
|
811
|
+
providerId,
|
|
812
|
+
apiKey: key,
|
|
813
|
+
modelId,
|
|
814
|
+
baseUrl: baseUrlArg,
|
|
815
|
+
fetchImpl: fetchImplArg,
|
|
816
|
+
});
|
|
817
|
+
lastProbe = probe;
|
|
818
|
+
if (probe.ok)
|
|
819
|
+
return { valid: true };
|
|
820
|
+
const failed = probe.steps.find((s) => !s.ok);
|
|
821
|
+
if (!failed)
|
|
822
|
+
return { valid: false, reason: 'probe failed without details' };
|
|
823
|
+
// Unknown provider with no probe endpoint → soft skip (matches
|
|
824
|
+
// the legacy validateProviderKey 'skipped' semantics).
|
|
825
|
+
if (failed.category === 'unknown' && /No probe endpoint/i.test(failed.reason ?? '')) {
|
|
826
|
+
return { valid: true, skipped: true, skipReason: 'No probe endpoint for this provider' };
|
|
827
|
+
}
|
|
828
|
+
const retrySuffix = failed.category === 'rate-limit' && typeof failed.retryAfterSec === 'number'
|
|
829
|
+
? ` (retry in ${failed.retryAfterSec}s)`
|
|
830
|
+
: '';
|
|
831
|
+
return {
|
|
832
|
+
valid: false,
|
|
833
|
+
reason: `${failed.reason ?? failed.category ?? 'unknown'}${retrySuffix}`,
|
|
834
|
+
};
|
|
835
|
+
};
|
|
836
|
+
const validate = opts.validator ?? probeAdapter;
|
|
687
837
|
const maxAttempts = 3;
|
|
688
838
|
let attempt = 1;
|
|
689
839
|
let validated = false;
|
|
@@ -695,7 +845,8 @@ async function runSetupWizard(opts = {}) {
|
|
|
695
845
|
// can `continue validation` to retry with fresh attempts after
|
|
696
846
|
// option [2] "Get a key" opens the browser.
|
|
697
847
|
validation: while (attempt <= maxAttempts) {
|
|
698
|
-
|
|
848
|
+
lastProbe = null;
|
|
849
|
+
const spinner = display.startSpinner('Testing connection…');
|
|
699
850
|
let result;
|
|
700
851
|
try {
|
|
701
852
|
result = await validate(provider.id, apiKey, baseUrl, fetchImpl);
|
|
@@ -703,12 +854,28 @@ async function runSetupWizard(opts = {}) {
|
|
|
703
854
|
finally {
|
|
704
855
|
spinner.stop();
|
|
705
856
|
}
|
|
857
|
+
// Render the 3-row probe trace if we ran a probe (post-hoc, so
|
|
858
|
+
// the spinner doesn't clobber the rows). Skipped when a test
|
|
859
|
+
// injected opts.validator — lastProbe stays null.
|
|
860
|
+
if (lastProbe) {
|
|
861
|
+
const trace = lastProbe;
|
|
862
|
+
for (const s of trace.steps) {
|
|
863
|
+
const label = STEP_LABELS[s.step];
|
|
864
|
+
if (s.ok) {
|
|
865
|
+
display.write(` ${kleur_1.default.green('✓')} ${label}\n`);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
const tail = s.reason ? ` ${kleur_1.default.dim(s.reason)}` : '';
|
|
869
|
+
display.write(` ${kleur_1.default.red('✗')} ${label}${tail}\n`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
706
873
|
if (result.valid) {
|
|
707
874
|
if (result.skipped) {
|
|
708
875
|
display.write(`${kleur_1.default.dim(`Skipped validation: ${result.skipReason ?? 'no validation endpoint'}. The key will be tested on first call.`)}\n`);
|
|
709
876
|
}
|
|
710
877
|
else {
|
|
711
|
-
display.write(`${kleur_1.default.green(`✓ ${provider.shortLabel}
|
|
878
|
+
display.write(`${kleur_1.default.green(`✓ ${provider.shortLabel} connection validated`)}\n`);
|
|
712
879
|
}
|
|
713
880
|
validated = true;
|
|
714
881
|
break;
|
|
@@ -826,9 +993,12 @@ async function runSetupWizard(opts = {}) {
|
|
|
826
993
|
if (baseUrl && provider.kind === 'custom') {
|
|
827
994
|
await upsertEnvVar(paths.envFile, 'CUSTOM_BASE_URL', baseUrl);
|
|
828
995
|
}
|
|
829
|
-
// Step 6:
|
|
996
|
+
// Step 6: success — wizard drops straight into the REPL via the
|
|
997
|
+
// outer boot path. No "Try: aiden" advice needed; the user is
|
|
998
|
+
// already on their way to chat.
|
|
830
999
|
display.write(`\n${kleur_1.default.green(`✓ ${provider.shortLabel}`)} configured with model ${kleur_1.default.cyan(modelId)}.\n`);
|
|
831
|
-
|
|
1000
|
+
// ONB1 slice 8: success screen + REPL handoff.
|
|
1001
|
+
(0, successScreen_1.renderSuccessScreen)({ out: process.stdout });
|
|
832
1002
|
return { status: 'configured', ran: true, config, envFile: paths.envFile };
|
|
833
1003
|
} // end of outer: while (true) — every path inside either continues,
|
|
834
1004
|
// returns, or breaks. Reaching this `}` is impossible (guarded by
|