aiden-runtime 4.1.1 → 4.1.3

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 (68) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +169 -9
  3. package/dist/cli/v4/callbacks.js +20 -2
  4. package/dist/cli/v4/chatSession.js +644 -16
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/doctor.js +23 -27
  7. package/dist/cli/v4/commands/help.js +4 -0
  8. package/dist/cli/v4/commands/index.js +10 -1
  9. package/dist/cli/v4/commands/model.js +30 -1
  10. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  11. package/dist/cli/v4/commands/update.js +102 -0
  12. package/dist/cli/v4/defaultSoul.js +68 -2
  13. package/dist/cli/v4/display/capabilityCard.js +135 -0
  14. package/dist/cli/v4/display/sessionEndCard.js +127 -0
  15. package/dist/cli/v4/display/toolTrail.js +172 -0
  16. package/dist/cli/v4/display.js +492 -142
  17. package/dist/cli/v4/doctor.js +472 -58
  18. package/dist/cli/v4/doctorLiveness.js +65 -10
  19. package/dist/cli/v4/promotionPrompt.js +332 -0
  20. package/dist/cli/v4/providerBootSelector.js +144 -0
  21. package/dist/cli/v4/replyRenderer.js +311 -20
  22. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  23. package/dist/cli/v4/skinEngine.js +14 -3
  24. package/dist/cli/v4/toolPreview.js +153 -0
  25. package/dist/core/tools/nowPlaying.js +7 -15
  26. package/dist/core/v4/aidenAgent.js +91 -29
  27. package/dist/core/v4/capabilities.js +89 -0
  28. package/dist/core/v4/contextCompressor.js +25 -8
  29. package/dist/core/v4/distillationIndex.js +167 -0
  30. package/dist/core/v4/distillationStore.js +98 -0
  31. package/dist/core/v4/logger/logger.js +40 -9
  32. package/dist/core/v4/promotionCandidates.js +234 -0
  33. package/dist/core/v4/promptBuilder.js +145 -1
  34. package/dist/core/v4/sessionDistiller.js +452 -0
  35. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  36. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  37. package/dist/core/v4/subsystemHealth.js +143 -0
  38. package/dist/core/v4/toolRegistry.js +16 -1
  39. package/dist/core/v4/update/executeInstall.js +233 -0
  40. package/dist/core/version.js +1 -1
  41. package/dist/moat/memoryGuard.js +111 -0
  42. package/dist/moat/plannerGuard.js +19 -0
  43. package/dist/moat/skillTeacher.js +14 -5
  44. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  45. package/dist/providers/v4/errors.js +112 -4
  46. package/dist/providers/v4/modelDefaults.js +65 -0
  47. package/dist/providers/v4/registry.js +9 -2
  48. package/dist/providers/v4/runtimeResolver.js +6 -0
  49. package/dist/tools/v4/index.js +80 -1
  50. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  51. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  52. package/dist/tools/v4/sessions/recallSession.js +177 -0
  53. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  54. package/dist/tools/v4/system/_psHelpers.js +123 -0
  55. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  56. package/dist/tools/v4/system/appClose.js +79 -0
  57. package/dist/tools/v4/system/appInput.js +154 -0
  58. package/dist/tools/v4/system/appLaunch.js +218 -0
  59. package/dist/tools/v4/system/clipboardRead.js +54 -0
  60. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  61. package/dist/tools/v4/system/mediaKey.js +109 -0
  62. package/dist/tools/v4/system/mediaSessions.js +163 -0
  63. package/dist/tools/v4/system/mediaTransport.js +211 -0
  64. package/dist/tools/v4/system/osProcessList.js +99 -0
  65. package/dist/tools/v4/system/screenshot.js +106 -0
  66. package/dist/tools/v4/system/volumeSet.js +157 -0
  67. package/package.json +4 -1
  68. package/skills/system_control.md +185 -69
@@ -18,11 +18,12 @@
18
18
  * - `--providers` is opt-in. When the user types it we extend the
19
19
  * report with one liveness row per probe, then render a summary
20
20
  * line at the bottom.
21
- * - Tool-catalog validation is deliberately OUT of scope. Liveness
22
- * probes ship `tools: []` (see comment in checkProviderLiveness)
23
- * so one bad tool schema doesn't false-red every provider that
24
- * validates strictly. The eval-harness / registration-time schema
25
- * validator (v4.1.1 main) is the right home for that concern.
21
+ * - Tool-catalog validation is deliberately OUT of scope. The
22
+ * probe ships ONE hardcoded no-op tool (`probe_noop`) so the
23
+ * Codex backend accepts the request (it rejects empty `tools`),
24
+ * while user-registered tool schemas stay un-validated here. The
25
+ * eval-harness / registration-time schema validator (v4.1.1
26
+ * main) is the right home for that concern.
26
27
  *
27
28
  * Trust artifact:
28
29
  * - On failure we surface `err.message` VERBATIM (truncated to 200
@@ -31,6 +32,7 @@
31
32
  * prints the actual OpenAI reason, not a generic "provider failed."
32
33
  */
33
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.pickProbeModel = pickProbeModel;
34
36
  exports.enumerateConfiguredProviders = enumerateConfiguredProviders;
35
37
  exports.checkProviderLiveness = checkProviderLiveness;
36
38
  exports.runProviderLiveness = runProviderLiveness;
@@ -55,6 +57,28 @@ function truncate(s, max = ERROR_TRUNCATE_CHARS) {
55
57
  return s;
56
58
  return `${s.slice(0, max - 1)}…`;
57
59
  }
60
+ /**
61
+ * Phase v4.1.2-slice5: pick a probe-safe model id from the registry.
62
+ *
63
+ * Some providers list model slugs that only work for enterprise / CLI
64
+ * accounts. ChatGPT Plus is the canonical case: the registry's
65
+ * `modelIds[0]` is `gpt-5.1-codex-max`, which is rejected by the
66
+ * subscription-account Codex backend with
67
+ * `"The 'gpt-5.1-codex-max' model is not supported when using Codex
68
+ * with a ChatGPT account."` — even though real REPL chat on the same
69
+ * account works because the user has selected a non-Codex slug
70
+ * (`gpt-5.5`).
71
+ *
72
+ * Heuristic: skip any slug containing `-codex` (covers `-codex-max`,
73
+ * `-codex-mini`, plain `-codex` suffix variants). Falls back to
74
+ * `modelIds[0]` if every slug is Codex-flavoured. No provider id
75
+ * special-casing — the heuristic is shape-based so future-similar
76
+ * providers benefit too.
77
+ */
78
+ function pickProbeModel(entry) {
79
+ const safe = entry.modelIds.find((m) => !m.includes('-codex'));
80
+ return safe ?? entry.modelIds[0] ?? '';
81
+ }
58
82
  /**
59
83
  * Wrap a promise with a hard timeout. Resolves to the inner result on
60
84
  * success, throws a clearly-labelled `Error` on timeout. Cleans up the
@@ -91,7 +115,7 @@ async function enumerateConfiguredProviders(opts) {
91
115
  const out = [];
92
116
  for (const entry of Object.values(registry_1.PROVIDER_REGISTRY)) {
93
117
  // Every provider needs at least one model to probe against.
94
- const model = entry.modelIds[0];
118
+ const model = pickProbeModel(entry);
95
119
  if (!model) {
96
120
  out.push({
97
121
  entry,
@@ -199,11 +223,42 @@ async function checkProviderLiveness(provider, model, adapter, opts) {
199
223
  const start = Date.now();
200
224
  // Liveness probes "is this provider reachable + authenticated?".
201
225
  // Tool-catalog validation is a separate concern (eval harness,
202
- // v4.1.1 main). Sending tools: [] ensures one bad tool schema
203
- // doesn't false-red every provider that validates strictly.
226
+ // v4.1.1 main).
227
+ //
228
+ // Phase v4.1.2-slice5: the probe used to send `messages: [user]`
229
+ // only, with `tools: []`. That body 400s against the Codex backend
230
+ // for two reasons:
231
+ // 1. No system message → empty `instructions` field in the wire
232
+ // body. Codex rejects requests without `instructions` (same
233
+ // root cause as the eval-runner fix in 6535d531).
234
+ // 2. Empty tools array → the codex adapter omits `tools`,
235
+ // `tool_choice`, `parallel_tool_calls` from the wire body
236
+ // entirely. The Codex backend treats this as malformed.
237
+ //
238
+ // Fix: add a minimal one-line system message (collapses into
239
+ // `instructions`) and one hand-crafted no-op tool. The probe tool
240
+ // is hardcoded with a conservative JSON Schema
241
+ // (`additionalProperties: false`) so strict validators accept it.
242
+ // The "one bad tool schema false-reds everyone" concern from the
243
+ // pre-slice5 comment applied to USER tools; this tool is internal.
204
244
  const input = {
205
- messages: [{ role: 'user', content: 'ping' }],
206
- tools: [],
245
+ messages: [
246
+ {
247
+ role: 'system',
248
+ content: 'You are an availability probe. Respond with a single word.',
249
+ },
250
+ { role: 'user', content: 'ping' },
251
+ ],
252
+ tools: [
253
+ {
254
+ name: 'probe_noop',
255
+ description: 'Probe placeholder. Do not call — the probe ignores any tool calls.',
256
+ inputSchema: {
257
+ type: 'object',
258
+ properties: {},
259
+ },
260
+ },
261
+ ],
207
262
  maxTokens: PROBE_MAX_TOKENS,
208
263
  };
209
264
  try {
@@ -0,0 +1,332 @@
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/promotionPrompt.ts — Phase v4.1.2-memory-D.
10
+ *
11
+ * REPL-side glue for the durable-facts promotion flow:
12
+ * - `parsePromotionInput(raw, count)` — pure: parse user reply into
13
+ * a 0-indexed array of approved
14
+ * candidate indices.
15
+ * - `formatCandidateList(candidates)` — pure: render the prompt body
16
+ * the user sees.
17
+ * - `promptForApproval(api, ...)` — drives the prompt loop;
18
+ * re-prompts ONCE on garbage,
19
+ * then defaults to skip.
20
+ * - `writeApprovedDurableFacts(...)` — append approved candidates
21
+ * to MEMORY.md `## Durable facts`
22
+ * via MemoryGuard.replaceSection.
23
+ *
24
+ * Input grammar (per Phase D's Q3):
25
+ * - "all" → every shown candidate
26
+ * - "none" / "skip" / "" → none
27
+ * - "1,3" → 0-indexed 0 and 2
28
+ * - "1-3" → 0-indexed 0, 1, 2 (inclusive range)
29
+ * - "1, 3-5" → mixed; whitespace tolerated
30
+ * - Anything unparseable → re-prompt once, then default skip
31
+ *
32
+ * The function intentionally keeps the parser pure so unit tests
33
+ * don't have to drive a prompt API. The prompt-loop function wires
34
+ * the parser to the existing `ChatPromptApi.readLine`.
35
+ */
36
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
37
+ if (k2 === undefined) k2 = k;
38
+ var desc = Object.getOwnPropertyDescriptor(m, k);
39
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
40
+ desc = { enumerable: true, get: function() { return m[k]; } };
41
+ }
42
+ Object.defineProperty(o, k2, desc);
43
+ }) : (function(o, m, k, k2) {
44
+ if (k2 === undefined) k2 = k;
45
+ o[k2] = m[k];
46
+ }));
47
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
48
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
49
+ }) : function(o, v) {
50
+ o["default"] = v;
51
+ });
52
+ var __importStar = (this && this.__importStar) || (function () {
53
+ var ownKeys = function(o) {
54
+ ownKeys = Object.getOwnPropertyNames || function (o) {
55
+ var ar = [];
56
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
57
+ return ar;
58
+ };
59
+ return ownKeys(o);
60
+ };
61
+ return function (mod) {
62
+ if (mod && mod.__esModule) return mod;
63
+ var result = {};
64
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
65
+ __setModuleDefault(result, mod);
66
+ return result;
67
+ };
68
+ })();
69
+ Object.defineProperty(exports, "__esModule", { value: true });
70
+ exports.parsePromotionInput = parsePromotionInput;
71
+ exports.formatCandidateList = formatCandidateList;
72
+ exports.promptForApproval = promptForApproval;
73
+ exports.buildDurableFactsBody = buildDurableFactsBody;
74
+ exports.readExistingDurableFactsBody = readExistingDurableFactsBody;
75
+ exports.writeApprovedDurableFacts = writeApprovedDurableFacts;
76
+ // ── Parser ────────────────────────────────────────────────────────────────
77
+ /**
78
+ * Parse a user reply into the set of approved candidate indices
79
+ * (0-indexed). Returns `null` to signal "unparseable input — re-prompt
80
+ * once" so callers can distinguish "explicit skip" (empty array) from
81
+ * "garbage typed".
82
+ *
83
+ * Pure, deterministic; safe for unit tests.
84
+ */
85
+ function parsePromotionInput(raw, count) {
86
+ const trimmed = raw.trim().toLowerCase();
87
+ if (trimmed === '' || trimmed === 'none' || trimmed === 'skip')
88
+ return [];
89
+ if (trimmed === 'all') {
90
+ return Array.from({ length: count }, (_, i) => i);
91
+ }
92
+ const out = new Set();
93
+ let sawAnyValid = false;
94
+ // Tolerate "1, 3-5 ,7" with mixed whitespace.
95
+ for (const token of trimmed.split(',')) {
96
+ const piece = token.trim();
97
+ if (!piece)
98
+ continue;
99
+ const range = piece.match(/^(\d+)\s*-\s*(\d+)$/);
100
+ if (range) {
101
+ const start = Number.parseInt(range[1], 10);
102
+ const end = Number.parseInt(range[2], 10);
103
+ if (!Number.isFinite(start) || !Number.isFinite(end))
104
+ continue;
105
+ const [lo, hi] = start <= end ? [start, end] : [end, start];
106
+ for (let n = lo; n <= hi; n += 1) {
107
+ if (n >= 1 && n <= count) {
108
+ out.add(n - 1);
109
+ sawAnyValid = true;
110
+ }
111
+ }
112
+ continue;
113
+ }
114
+ const single = piece.match(/^\d+$/);
115
+ if (single) {
116
+ const n = Number.parseInt(piece, 10);
117
+ if (n >= 1 && n <= count) {
118
+ out.add(n - 1);
119
+ sawAnyValid = true;
120
+ }
121
+ continue;
122
+ }
123
+ // Non-numeric token alongside others — treat the WHOLE input as
124
+ // unparseable so the user gets one re-prompt instead of a silent
125
+ // partial selection.
126
+ return null;
127
+ }
128
+ if (!sawAnyValid)
129
+ return []; // numbers given but all out of range
130
+ return [...out].sort((a, b) => a - b);
131
+ }
132
+ // ── Renderer ──────────────────────────────────────────────────────────────
133
+ /**
134
+ * Build the text the user sees. Pure — caller writes this to display.
135
+ */
136
+ function formatCandidateList(candidates) {
137
+ const lines = [];
138
+ lines.push(`${candidates.length} thing${candidates.length === 1 ? '' : 's'} worth remembering this session. Promote which?`);
139
+ lines.push('');
140
+ for (let i = 0; i < candidates.length; i += 1) {
141
+ const c = candidates[i];
142
+ const sourceTag = c.source === 'explicit' ? '[user said]'
143
+ : c.source === 'decision' ? '[decision]'
144
+ : '[open item]';
145
+ lines.push(` [${i + 1}] ${sourceTag} ${c.text}`);
146
+ }
147
+ lines.push('');
148
+ lines.push('Reply: numbers to approve (e.g. "1,3" or "1-3"), "all", or skip.');
149
+ return lines.join('\n');
150
+ }
151
+ /**
152
+ * Drive the approval prompt. Two paths:
153
+ *
154
+ * 1. Interactive checkbox (TTY): @inquirer/prompts.checkbox, space
155
+ * to toggle, enter to confirm, esc/ctrl+c to skip. Default
156
+ * selection is NONE — the user explicitly opts in. v4.1.3-essentials
157
+ * promotion-ux replaces what used to be a text-input chore.
158
+ *
159
+ * 2. Text-input fallback (non-TTY / piped / CI): renders the
160
+ * numbered list and reads a single line. Parser handles "1,3"
161
+ * / "1-3" / "all" / "skip" / "". Re-prompts ONCE on garbage,
162
+ * then defaults to skip. The original Phase v4.1.2-memory-D
163
+ * behavior, preserved verbatim.
164
+ *
165
+ * Auto-routes via `process.stdout.isTTY`; tests override via opts.
166
+ *
167
+ * No mid-session state leakage — purely a session-end interaction.
168
+ */
169
+ async function promptForApproval(api, display, candidates, opts = {}) {
170
+ if (candidates.length === 0)
171
+ return [];
172
+ const useInteractive = opts.forceInteractive === true
173
+ ? true
174
+ : opts.forceTextInput === true
175
+ ? false
176
+ : !!process.stdout.isTTY;
177
+ if (useInteractive) {
178
+ return promptForApprovalInteractive(display, candidates);
179
+ }
180
+ return promptForApprovalText(api, display, candidates);
181
+ }
182
+ /**
183
+ * v4.1.3-essentials promotion-ux: interactive multi-select checkbox.
184
+ * Uses @inquirer/prompts.checkbox (already a runtime dep — same
185
+ * library as the model picker, setup wizard, approval prompts).
186
+ *
187
+ * Choices render with the source-type tag inline so the user sees
188
+ * "[decision] X" / "[open item] Y" / "[user said] Z" — matches the
189
+ * tag set the text-input renderer uses.
190
+ *
191
+ * Exit paths:
192
+ * - User confirms with at least one box checked → return selected
193
+ * - User confirms with zero boxes checked → dim note, return []
194
+ * - User hits Esc / Ctrl+C (inquirer throws) → dim note, return []
195
+ *
196
+ * All three "nothing selected" paths produce the same outcome — empty
197
+ * array — matching the user's Q5 default ("empty/skip/esc all
198
+ * equivalent").
199
+ *
200
+ * Lazy-require inquirer so test harnesses without a TTY don't crash
201
+ * importing the module. Same pattern setupWizard / callbacks /
202
+ * modelPicker already use.
203
+ */
204
+ async function promptForApprovalInteractive(display, candidates) {
205
+ // Dynamic ES import (not CommonJS require) so vitest's vi.mock can
206
+ // intercept the call in tests. The runtime behavior is identical
207
+ // for our purpose — single one-shot lazy load on first call.
208
+ const inq = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
209
+ const heading = `${candidates.length} thing${candidates.length === 1 ? '' : 's'} ` +
210
+ `worth remembering this session.`;
211
+ display.write('\n' + heading + '\n');
212
+ try {
213
+ const selected = await inq.checkbox({
214
+ message: 'Promote which to durable memory? (space toggles · enter confirms)',
215
+ choices: candidates.map((c, i) => ({
216
+ name: `${typeTag(c)} ${c.text}`,
217
+ value: i,
218
+ checked: false,
219
+ })),
220
+ loop: false,
221
+ pageSize: Math.min(10, candidates.length),
222
+ });
223
+ if (selected.length === 0) {
224
+ display.dim('Nothing promoted to durable facts.');
225
+ return [];
226
+ }
227
+ return selected.map((i) => candidates[i]);
228
+ }
229
+ catch {
230
+ // Inquirer throws on Ctrl+C / Esc — treat as skip.
231
+ display.dim('Skipped: nothing promoted to durable facts.');
232
+ return [];
233
+ }
234
+ }
235
+ /**
236
+ * Source-type tag matching the text-input renderer's format. Kept as
237
+ * a helper so the interactive choice labels stay in sync with the
238
+ * text path if a new `Candidate.source` value lands.
239
+ */
240
+ function typeTag(c) {
241
+ if (c.source === 'explicit')
242
+ return '[user said]';
243
+ if (c.source === 'decision')
244
+ return '[decision]';
245
+ return '[open item]';
246
+ }
247
+ /**
248
+ * Phase v4.1.2-memory-D text-input loop. Renders the candidate list,
249
+ * reads ONE line, parses, returns approved Candidate[]. On unparseable
250
+ * input re-prompts ONCE; second failure defaults to skip with a dim
251
+ * line explaining why nothing was promoted.
252
+ *
253
+ * Kept as the non-TTY fallback (pipes, CI, test harness) so the
254
+ * promotion-flow contract continues to work without an interactive
255
+ * shell. v4.1.3-essentials promotion-ux renamed this from
256
+ * `promptForApproval` so the public entry point can dispatch.
257
+ */
258
+ async function promptForApprovalText(api, display, candidates) {
259
+ display.write('\n' + formatCandidateList(candidates) + '\n');
260
+ for (let attempt = 0; attempt < 2; attempt += 1) {
261
+ const raw = await api.readLine('Promote > ');
262
+ const parsed = parsePromotionInput(raw, candidates.length);
263
+ if (parsed !== null) {
264
+ if (parsed.length === 0) {
265
+ display.dim('Nothing promoted to durable facts.');
266
+ return [];
267
+ }
268
+ return parsed.map((i) => candidates[i]);
269
+ }
270
+ if (attempt === 0) {
271
+ display.warn('Could not parse input. Use numbers ("1,3"), ranges ("1-3"), "all", or "skip".');
272
+ }
273
+ }
274
+ display.dim('Skipped: input still unparseable. Nothing promoted to durable facts.');
275
+ return [];
276
+ }
277
+ // ── Persistence ───────────────────────────────────────────────────────────
278
+ const DURABLE_FACTS_HEADER = '## Durable facts';
279
+ /**
280
+ * Render the section body for `## Durable facts` by combining existing
281
+ * entries with newly-approved candidates. Newest at the BOTTOM so
282
+ * read order reflects when each fact landed — matches how users scan
283
+ * MEMORY.md.
284
+ *
285
+ * Pure — caller passes existing body (extracted via the same regex
286
+ * pattern MemoryGuard uses in replaceSection).
287
+ */
288
+ function buildDurableFactsBody(existingBody, approved) {
289
+ const existingLines = existingBody
290
+ .split('\n')
291
+ .map((l) => l.trim())
292
+ .filter((l) => l.length > 0);
293
+ const newLines = approved.map((c) => `- ${c.text}`);
294
+ return [...existingLines, ...newLines].join('\n');
295
+ }
296
+ /**
297
+ * Read the current `## Durable facts` body from MEMORY.md (returns
298
+ * empty string when the section doesn't yet exist). Mirrors the
299
+ * regex pattern MemoryGuard.replaceSection uses.
300
+ */
301
+ async function readExistingDurableFactsBody(memoryManager) {
302
+ const snap = await memoryManager.loadSnapshot();
303
+ const md = snap.memoryMd ?? '';
304
+ const headerEscaped = DURABLE_FACTS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
305
+ const sectionRe = new RegExp(`${headerEscaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n## |$)`);
306
+ const m = md.match(sectionRe);
307
+ return m ? (m[1] ?? '').trim() : '';
308
+ }
309
+ /**
310
+ * Persist the approved candidates. Reads existing body (so a second
311
+ * session-end appends rather than overwrites), folds in new lines,
312
+ * and writes via MemoryGuard.replaceSection — which handles
313
+ * verify-on-disk + section auto-creation.
314
+ *
315
+ * Returns the GuardedResult so the caller can dim-log success or
316
+ * warn on a failed verify.
317
+ */
318
+ async function writeApprovedDurableFacts(memoryManager, memoryGuard, approved) {
319
+ if (approved.length === 0) {
320
+ return { ok: true, verified: true, entryCount: 0 };
321
+ }
322
+ const existingBody = await readExistingDurableFactsBody(memoryManager);
323
+ const newBody = buildDurableFactsBody(existingBody, approved);
324
+ const entryCount = newBody.split('\n').filter((l) => l.trim().length > 0).length;
325
+ const result = await memoryGuard.replaceSection('memory', DURABLE_FACTS_HEADER, newBody);
326
+ return {
327
+ ok: result.ok,
328
+ verified: result.verified,
329
+ reason: result.reason,
330
+ entryCount,
331
+ };
332
+ }
@@ -0,0 +1,144 @@
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/providerBootSelector.ts — Phase v4.1.2-bug1.
10
+ *
11
+ * Boot-time provider/model picker. Replaces the hardcoded
12
+ * `groq + llama-3.3-70b-versatile` fallback that bit new users:
13
+ * users authenticated with ChatGPT Plus OAuth (the post-v4.1.1
14
+ * onboarding default) booted into Groq anyway, and llama-3.3-70b's
15
+ * tool emission triggers Groq's first-party 400
16
+ * ("Failed to call a function. Please adjust your prompt.").
17
+ *
18
+ * Resolution precedence (caller in `aidenCLI.ts` enforces order):
19
+ * 1. Both CLI flags `--provider` + `--model` set → use as-is
20
+ * 2. One CLI flag set → use it, resolve other
21
+ * 3. Persisted config (model-selection.json) complete → use as-is
22
+ * 4. Persisted config partial → use it, resolve other
23
+ * 5. Neither → priority-list auto-pick → THIS MODULE
24
+ * 6. Nothing authed → hardcoded fallback
25
+ *
26
+ * `resolveBootProvider()` covers cases 1–5; the caller composes the
27
+ * input shape and falls back when this returns `null` (case 6).
28
+ *
29
+ * Test surface: the enumerator is injected so unit tests mock it.
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.BOOT_PRIORITY = void 0;
33
+ exports.findProviderForModel = findProviderForModel;
34
+ exports.resolveBootProvider = resolveBootProvider;
35
+ const doctorLiveness_1 = require("./doctorLiveness");
36
+ const registry_1 = require("../../providers/v4/registry");
37
+ /**
38
+ * Provider id ordering for auto-pick. Higher in the list = preferred.
39
+ * OAuth subscription flows lead because they have no API-key
40
+ * onboarding friction and are the default v4.1.1 install path.
41
+ */
42
+ exports.BOOT_PRIORITY = [
43
+ 'chatgpt-plus', // OAuth — primary onboarding flow
44
+ 'claude-pro', // OAuth — Anthropic equivalent
45
+ 'anthropic', // API key — power-user tier
46
+ 'openai', // API key — power-user tier
47
+ // Phase v4.1.2-deepseek: paid tier, strong tool-caller, ranked
48
+ // above groq for the same first-run-UX reason — groq's free-tier
49
+ // tool emission was the original bug1 (llama-3.3-70b 400s on
50
+ // tool calls).
51
+ 'deepseek', // API key — paid, strong tool-caller (V4 Pro)
52
+ 'groq', // free-tier API key — common but tool-emission flaky
53
+ 'ollama', // local — only if daemon up
54
+ ];
55
+ /**
56
+ * Walk every provider's `modelIds` and return the entry that lists
57
+ * `modelId`. Used by the `--model`-only path to validate the model
58
+ * is at least known to one provider before we accept it.
59
+ */
60
+ function findProviderForModel(modelId) {
61
+ for (const entry of Object.values(registry_1.PROVIDER_REGISTRY)) {
62
+ if (entry.modelIds.includes(modelId))
63
+ return entry;
64
+ }
65
+ return null;
66
+ }
67
+ /**
68
+ * Resolve the boot provider + model. Returns `null` when no choice
69
+ * could be inferred AND nothing is authed (the caller falls back to
70
+ * the hardcoded `groq + llama-3.3-70b-versatile` default in that
71
+ * one case).
72
+ *
73
+ * Throws (`Error`) when input is internally inconsistent — e.g. the
74
+ * user passed `--model foo` for a model no provider declares. Caller
75
+ * surfaces the message via the standard error path.
76
+ */
77
+ async function resolveBootProvider(input, enumerate) {
78
+ const { cliProviderId, cliModelId, cfgProviderId, cfgModelId } = input;
79
+ // Case 1: both CLI flags set.
80
+ if (cliProviderId && cliModelId) {
81
+ return { providerId: cliProviderId, modelId: cliModelId, source: 'cli-flag' };
82
+ }
83
+ // Case 2a: `--provider` only. Use that provider + its first
84
+ // non-codex model. The registry might not know this provider; we
85
+ // accept it as-is (the runtime resolver will throw a clearer error
86
+ // later if it's bogus).
87
+ if (cliProviderId && !cliModelId) {
88
+ const entry = registry_1.PROVIDER_REGISTRY[cliProviderId];
89
+ const modelId = entry ? (0, doctorLiveness_1.pickProbeModel)(entry) : '';
90
+ if (!modelId) {
91
+ throw new Error(`--provider '${cliProviderId}' set but no model could be inferred. ` +
92
+ `Pass --model explicitly.`);
93
+ }
94
+ return { providerId: cliProviderId, modelId, source: 'cli-flag-partial' };
95
+ }
96
+ // Case 2b: `--model` only. Verify the model is known to some
97
+ // provider; pick the matching provider.
98
+ if (!cliProviderId && cliModelId) {
99
+ const entry = findProviderForModel(cliModelId);
100
+ if (!entry) {
101
+ throw new Error(`--model '${cliModelId}' is not declared by any provider in the ` +
102
+ `registry. Run \`aiden model\` to see available models, or pass ` +
103
+ `--provider explicitly.`);
104
+ }
105
+ return { providerId: entry.id, modelId: cliModelId, source: 'cli-flag-partial' };
106
+ }
107
+ // Case 3: persisted config — both fields set.
108
+ if (cfgProviderId && cfgModelId) {
109
+ return { providerId: cfgProviderId, modelId: cfgModelId, source: 'persisted-config' };
110
+ }
111
+ // Case 4a: config has provider, no model — resolve via pickProbeModel.
112
+ if (cfgProviderId && !cfgModelId) {
113
+ const entry = registry_1.PROVIDER_REGISTRY[cfgProviderId];
114
+ const modelId = entry ? (0, doctorLiveness_1.pickProbeModel)(entry) : '';
115
+ if (modelId) {
116
+ return { providerId: cfgProviderId, modelId, source: 'config-partial' };
117
+ }
118
+ // Unknown provider in config — fall through to auto-pick rather
119
+ // than refusing to boot.
120
+ }
121
+ // Case 4b: config has model only — same shape as Case 2b but
122
+ // softer: fall through to auto-pick if the model isn't found in
123
+ // the registry (config can lag the codebase).
124
+ if (!cfgProviderId && cfgModelId) {
125
+ const entry = findProviderForModel(cfgModelId);
126
+ if (entry) {
127
+ return { providerId: entry.id, modelId: cfgModelId, source: 'config-partial' };
128
+ }
129
+ }
130
+ // Case 5: auto-pick from priority list.
131
+ const configured = await enumerate();
132
+ for (const id of exports.BOOT_PRIORITY) {
133
+ const hit = configured.find((c) => c.entry.id === id && c.configured);
134
+ if (hit) {
135
+ return {
136
+ providerId: id,
137
+ modelId: (0, doctorLiveness_1.pickProbeModel)(hit.entry),
138
+ source: 'auto-priority',
139
+ };
140
+ }
141
+ }
142
+ // Case 6: nothing authed — caller falls back to hardcoded default.
143
+ return null;
144
+ }