aiden-runtime 4.1.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +78 -26
  2. package/dist/cli/v4/aidenCLI.js +159 -9
  3. package/dist/cli/v4/callbacks.js +5 -2
  4. package/dist/cli/v4/chatSession.js +525 -15
  5. package/dist/cli/v4/commands/auth.js +6 -3
  6. package/dist/cli/v4/commands/help.js +4 -0
  7. package/dist/cli/v4/commands/index.js +10 -1
  8. package/dist/cli/v4/commands/reloadSoul.js +37 -0
  9. package/dist/cli/v4/commands/update.js +102 -0
  10. package/dist/cli/v4/defaultSoul.js +68 -2
  11. package/dist/cli/v4/display.js +28 -10
  12. package/dist/cli/v4/doctor.js +112 -0
  13. package/dist/cli/v4/doctorLiveness.js +65 -10
  14. package/dist/cli/v4/promotionPrompt.js +202 -0
  15. package/dist/cli/v4/providerBootSelector.js +144 -0
  16. package/dist/cli/v4/sessionSummaryGate.js +66 -0
  17. package/dist/cli/v4/toolPreview.js +139 -0
  18. package/dist/core/v4/aidenAgent.js +91 -29
  19. package/dist/core/v4/capabilities.js +89 -0
  20. package/dist/core/v4/contextCompressor.js +25 -8
  21. package/dist/core/v4/distillationIndex.js +167 -0
  22. package/dist/core/v4/distillationStore.js +98 -0
  23. package/dist/core/v4/logger/logger.js +40 -9
  24. package/dist/core/v4/promotionCandidates.js +234 -0
  25. package/dist/core/v4/promptBuilder.js +145 -1
  26. package/dist/core/v4/sessionDistiller.js +405 -0
  27. package/dist/core/v4/skillMining/skillMiner.js +43 -6
  28. package/dist/core/v4/skillOutcomeTracker.js +323 -0
  29. package/dist/core/v4/subsystemHealth.js +143 -0
  30. package/dist/core/v4/update/executeInstall.js +233 -0
  31. package/dist/core/version.js +1 -1
  32. package/dist/moat/memoryGuard.js +111 -0
  33. package/dist/moat/skillTeacher.js +14 -5
  34. package/dist/providers/v4/chatCompletionsAdapter.js +9 -0
  35. package/dist/providers/v4/errors.js +20 -4
  36. package/dist/providers/v4/modelDefaults.js +65 -0
  37. package/dist/providers/v4/registry.js +9 -2
  38. package/dist/providers/v4/runtimeResolver.js +6 -0
  39. package/dist/tools/v4/index.js +57 -1
  40. package/dist/tools/v4/memory/memoryRemove.js +57 -2
  41. package/dist/tools/v4/memory/sessionSummary.js +151 -0
  42. package/dist/tools/v4/sessions/recallSession.js +163 -0
  43. package/dist/tools/v4/sessions/sessionSearch.js +5 -1
  44. package/dist/tools/v4/system/_psHelpers.js +55 -0
  45. package/dist/tools/v4/system/aidenSelfUpdate.js +162 -0
  46. package/dist/tools/v4/system/appClose.js +79 -0
  47. package/dist/tools/v4/system/appLaunch.js +92 -0
  48. package/dist/tools/v4/system/clipboardRead.js +54 -0
  49. package/dist/tools/v4/system/clipboardWrite.js +84 -0
  50. package/dist/tools/v4/system/mediaKey.js +78 -0
  51. package/dist/tools/v4/system/osProcessList.js +99 -0
  52. package/dist/tools/v4/system/screenshot.js +106 -0
  53. package/dist/tools/v4/system/volumeSet.js +157 -0
  54. package/package.json +4 -1
  55. package/skills/system_control.md +135 -69
@@ -0,0 +1,202 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.parsePromotionInput = parsePromotionInput;
38
+ exports.formatCandidateList = formatCandidateList;
39
+ exports.promptForApproval = promptForApproval;
40
+ exports.buildDurableFactsBody = buildDurableFactsBody;
41
+ exports.readExistingDurableFactsBody = readExistingDurableFactsBody;
42
+ exports.writeApprovedDurableFacts = writeApprovedDurableFacts;
43
+ // ── Parser ────────────────────────────────────────────────────────────────
44
+ /**
45
+ * Parse a user reply into the set of approved candidate indices
46
+ * (0-indexed). Returns `null` to signal "unparseable input — re-prompt
47
+ * once" so callers can distinguish "explicit skip" (empty array) from
48
+ * "garbage typed".
49
+ *
50
+ * Pure, deterministic; safe for unit tests.
51
+ */
52
+ function parsePromotionInput(raw, count) {
53
+ const trimmed = raw.trim().toLowerCase();
54
+ if (trimmed === '' || trimmed === 'none' || trimmed === 'skip')
55
+ return [];
56
+ if (trimmed === 'all') {
57
+ return Array.from({ length: count }, (_, i) => i);
58
+ }
59
+ const out = new Set();
60
+ let sawAnyValid = false;
61
+ // Tolerate "1, 3-5 ,7" with mixed whitespace.
62
+ for (const token of trimmed.split(',')) {
63
+ const piece = token.trim();
64
+ if (!piece)
65
+ continue;
66
+ const range = piece.match(/^(\d+)\s*-\s*(\d+)$/);
67
+ if (range) {
68
+ const start = Number.parseInt(range[1], 10);
69
+ const end = Number.parseInt(range[2], 10);
70
+ if (!Number.isFinite(start) || !Number.isFinite(end))
71
+ continue;
72
+ const [lo, hi] = start <= end ? [start, end] : [end, start];
73
+ for (let n = lo; n <= hi; n += 1) {
74
+ if (n >= 1 && n <= count) {
75
+ out.add(n - 1);
76
+ sawAnyValid = true;
77
+ }
78
+ }
79
+ continue;
80
+ }
81
+ const single = piece.match(/^\d+$/);
82
+ if (single) {
83
+ const n = Number.parseInt(piece, 10);
84
+ if (n >= 1 && n <= count) {
85
+ out.add(n - 1);
86
+ sawAnyValid = true;
87
+ }
88
+ continue;
89
+ }
90
+ // Non-numeric token alongside others — treat the WHOLE input as
91
+ // unparseable so the user gets one re-prompt instead of a silent
92
+ // partial selection.
93
+ return null;
94
+ }
95
+ if (!sawAnyValid)
96
+ return []; // numbers given but all out of range
97
+ return [...out].sort((a, b) => a - b);
98
+ }
99
+ // ── Renderer ──────────────────────────────────────────────────────────────
100
+ /**
101
+ * Build the text the user sees. Pure — caller writes this to display.
102
+ */
103
+ function formatCandidateList(candidates) {
104
+ const lines = [];
105
+ lines.push(`${candidates.length} thing${candidates.length === 1 ? '' : 's'} worth remembering this session. Promote which?`);
106
+ lines.push('');
107
+ for (let i = 0; i < candidates.length; i += 1) {
108
+ const c = candidates[i];
109
+ const sourceTag = c.source === 'explicit' ? '[user said]'
110
+ : c.source === 'decision' ? '[decision]'
111
+ : '[open item]';
112
+ lines.push(` [${i + 1}] ${sourceTag} ${c.text}`);
113
+ }
114
+ lines.push('');
115
+ lines.push('Reply: numbers to approve (e.g. "1,3" or "1-3"), "all", or skip.');
116
+ return lines.join('\n');
117
+ }
118
+ /**
119
+ * Drive the approval prompt. Renders the candidate list, reads ONE
120
+ * line, parses, returns approved Candidate[]. On unparseable input
121
+ * re-prompts ONCE; second failure defaults to skip with a dim line
122
+ * explaining why nothing was promoted.
123
+ *
124
+ * No mid-session state leakage — purely a session-end interaction.
125
+ */
126
+ async function promptForApproval(api, display, candidates) {
127
+ if (candidates.length === 0)
128
+ return [];
129
+ display.write('\n' + formatCandidateList(candidates) + '\n');
130
+ for (let attempt = 0; attempt < 2; attempt += 1) {
131
+ const raw = await api.readLine('Promote > ');
132
+ const parsed = parsePromotionInput(raw, candidates.length);
133
+ if (parsed !== null) {
134
+ if (parsed.length === 0) {
135
+ display.dim('Nothing promoted to durable facts.');
136
+ return [];
137
+ }
138
+ return parsed.map((i) => candidates[i]);
139
+ }
140
+ if (attempt === 0) {
141
+ display.warn('Could not parse input. Use numbers ("1,3"), ranges ("1-3"), "all", or "skip".');
142
+ }
143
+ }
144
+ display.dim('Skipped: input still unparseable. Nothing promoted to durable facts.');
145
+ return [];
146
+ }
147
+ // ── Persistence ───────────────────────────────────────────────────────────
148
+ const DURABLE_FACTS_HEADER = '## Durable facts';
149
+ /**
150
+ * Render the section body for `## Durable facts` by combining existing
151
+ * entries with newly-approved candidates. Newest at the BOTTOM so
152
+ * read order reflects when each fact landed — matches how users scan
153
+ * MEMORY.md.
154
+ *
155
+ * Pure — caller passes existing body (extracted via the same regex
156
+ * pattern MemoryGuard uses in replaceSection).
157
+ */
158
+ function buildDurableFactsBody(existingBody, approved) {
159
+ const existingLines = existingBody
160
+ .split('\n')
161
+ .map((l) => l.trim())
162
+ .filter((l) => l.length > 0);
163
+ const newLines = approved.map((c) => `- ${c.text}`);
164
+ return [...existingLines, ...newLines].join('\n');
165
+ }
166
+ /**
167
+ * Read the current `## Durable facts` body from MEMORY.md (returns
168
+ * empty string when the section doesn't yet exist). Mirrors the
169
+ * regex pattern MemoryGuard.replaceSection uses.
170
+ */
171
+ async function readExistingDurableFactsBody(memoryManager) {
172
+ const snap = await memoryManager.loadSnapshot();
173
+ const md = snap.memoryMd ?? '';
174
+ const headerEscaped = DURABLE_FACTS_HEADER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
175
+ const sectionRe = new RegExp(`${headerEscaped}[^\\n]*\\n([\\s\\S]*?)(?=\\n## |$)`);
176
+ const m = md.match(sectionRe);
177
+ return m ? (m[1] ?? '').trim() : '';
178
+ }
179
+ /**
180
+ * Persist the approved candidates. Reads existing body (so a second
181
+ * session-end appends rather than overwrites), folds in new lines,
182
+ * and writes via MemoryGuard.replaceSection — which handles
183
+ * verify-on-disk + section auto-creation.
184
+ *
185
+ * Returns the GuardedResult so the caller can dim-log success or
186
+ * warn on a failed verify.
187
+ */
188
+ async function writeApprovedDurableFacts(memoryManager, memoryGuard, approved) {
189
+ if (approved.length === 0) {
190
+ return { ok: true, verified: true, entryCount: 0 };
191
+ }
192
+ const existingBody = await readExistingDurableFactsBody(memoryManager);
193
+ const newBody = buildDurableFactsBody(existingBody, approved);
194
+ const entryCount = newBody.split('\n').filter((l) => l.trim().length > 0).length;
195
+ const result = await memoryGuard.replaceSection('memory', DURABLE_FACTS_HEADER, newBody);
196
+ return {
197
+ ok: result.ok,
198
+ verified: result.verified,
199
+ reason: result.reason,
200
+ entryCount,
201
+ };
202
+ }
@@ -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
+ }
@@ -0,0 +1,66 @@
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/sessionSummaryGate.ts — Phase v4.1.2-followup-2.
10
+ *
11
+ * Pure decision helpers extracted from `ChatSession.maybeAutoSummarize`
12
+ * so the threshold + mtime/size-grew logic is unit-testable without
13
+ * standing up a full ChatSession + mocked agent loop.
14
+ *
15
+ * shouldAutoSummarize → returns {fire: true} or
16
+ * {fire: false, reason: 'short'|'unconfigured'|'no-paths'}.
17
+ * ChatSession uses the reason tag to log the right
18
+ * user-visible message.
19
+ *
20
+ * memoryGrewBetween → strict size-or-mtime comparison so the caller can
21
+ * detect "the model actually fired session_summary"
22
+ * even when the tool wrote without growing the file
23
+ * length (e.g. replaced a previous same-length entry).
24
+ *
25
+ * No I/O here. ChatSession owns the fs.stat + display.warn / display.dim.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.SESSION_SUMMARY_MIN_TURNS = void 0;
29
+ exports.shouldAutoSummarize = shouldAutoSummarize;
30
+ exports.memoryGrewBetween = memoryGrewBetween;
31
+ /** Minimum user-message turns required before auto-summary triggers. */
32
+ exports.SESSION_SUMMARY_MIN_TURNS = 3;
33
+ /**
34
+ * Decide whether the /quit auto-summary should fire. Threshold lives
35
+ * here as the single source of truth; ChatSession imports the constant
36
+ * so the user-facing log message ("need 3+") cites the exact value.
37
+ */
38
+ function shouldAutoSummarize(input) {
39
+ if (input.userTurns < exports.SESSION_SUMMARY_MIN_TURNS) {
40
+ return { fire: false, reason: 'short' };
41
+ }
42
+ if (input.unconfigured) {
43
+ return { fire: false, reason: 'unconfigured' };
44
+ }
45
+ if (!input.memoryPath) {
46
+ return { fire: false, reason: 'no-paths' };
47
+ }
48
+ return { fire: true };
49
+ }
50
+ /**
51
+ * True iff MEMORY.md grew (longer) or was touched (newer mtime) between
52
+ * the two snapshots. Used to detect whether the agent actually fired
53
+ * the session_summary tool inside the synthetic turn — if not, the
54
+ * user sees a warning instead of a misleading "saved" message.
55
+ *
56
+ * The size-OR-mtime disjunction (not just size>before) covers the case
57
+ * where session_summary replaces an existing same-length entry: file
58
+ * size stays the same but mtime advances.
59
+ */
60
+ function memoryGrewBetween(before, after) {
61
+ if (after.size > before.size)
62
+ return true;
63
+ if (after.mtime > before.mtime)
64
+ return true;
65
+ return false;
66
+ }
@@ -0,0 +1,139 @@
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/toolPreview.ts — Phase v4.1.2 alive-core.
10
+ *
11
+ * Clean per-tool argument previews. Replaces the old
12
+ * `JSON.stringify(args)` blob in `display.toolPreview` with a
13
+ * tool-aware lookup that extracts the primary argument (the one
14
+ * actually useful at a glance — `command` for terminal, `path` for
15
+ * file ops, `query` for search, etc.).
16
+ *
17
+ * Falls back to the original full-JSON stringification when the tool
18
+ * isn't in the map or the primary arg is absent. This keeps unknown
19
+ * tools rendering exactly as before — additive only.
20
+ *
21
+ * Adding a new tool with a non-obvious primary arg? Add it here.
22
+ * Tools whose `args` shape is "the arg is meaningful at-a-glance"
23
+ * (a path, a query, a command, a URL, an id, a name) belong in this map.
24
+ * Tools whose args are a small flag bag (e.g. system_info has no args
25
+ * worth showing) can be omitted — the renderer hides the args block
26
+ * entirely when the map returns `null` and the arg object is empty.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.TOOL_PRIMARY_ARG = void 0;
30
+ exports.buildToolPreview = buildToolPreview;
31
+ /**
32
+ * Map of tool-name → name of the property in `args` that should render
33
+ * as the at-a-glance preview. Stable contract; tests assert specific
34
+ * entries.
35
+ */
36
+ exports.TOOL_PRIMARY_ARG = {
37
+ // ── terminal / execution ─────────────────────────────────────────────
38
+ shell_exec: 'command',
39
+ execute_code: 'code',
40
+ // ── file ops ─────────────────────────────────────────────────────────
41
+ file_read: 'path',
42
+ file_write: 'path',
43
+ file_patch: 'path',
44
+ file_list: 'path',
45
+ file_copy: 'source',
46
+ file_move: 'source',
47
+ file_delete: 'path',
48
+ // ── web ──────────────────────────────────────────────────────────────
49
+ web_search: 'query',
50
+ deep_research: 'query',
51
+ youtube_search: 'query',
52
+ fetch_url: 'url',
53
+ fetch_page: 'url',
54
+ open_url: 'url',
55
+ // ── browser ──────────────────────────────────────────────────────────
56
+ browser_navigate: 'url',
57
+ browser_click: 'selector',
58
+ browser_fill: 'selector',
59
+ browser_type: 'selector',
60
+ browser_scroll: 'selector',
61
+ browser_extract: 'selector',
62
+ browser_get_url: '', // no args — present so map lookup hits
63
+ browser_screenshot: 'path',
64
+ browser_close: '',
65
+ // ── memory ───────────────────────────────────────────────────────────
66
+ memory_add: 'content',
67
+ memory_remove: 'content',
68
+ memory_replace: 'old',
69
+ // ── skills ───────────────────────────────────────────────────────────
70
+ skill_view: 'name',
71
+ skill_manage: 'action',
72
+ skills_list: '',
73
+ // ── sessions ─────────────────────────────────────────────────────────
74
+ session_search: 'query',
75
+ session_list: '',
76
+ session_summary: 'trigger',
77
+ // ── process ──────────────────────────────────────────────────────────
78
+ process_spawn: 'command',
79
+ process_kill: 'pid',
80
+ process_list: '',
81
+ process_wait: 'pid',
82
+ process_log_read: 'pid',
83
+ // ── subagent ─────────────────────────────────────────────────────────
84
+ subagent_fanout: 'mode',
85
+ // ── system / misc ────────────────────────────────────────────────────
86
+ system_info: '',
87
+ now_playing: '',
88
+ get_natural_events: '',
89
+ };
90
+ /**
91
+ * Maximum visible characters for the preview value. Long commands /
92
+ * full file contents get truncated with an ellipsis so a single tool
93
+ * row stays on one line at typical terminal widths.
94
+ */
95
+ const PREVIEW_MAX_CHARS = 120;
96
+ /**
97
+ * Build the per-tool preview string for `args`. Returns:
98
+ * - `null` when the tool isn't in the map (caller falls back to the
99
+ * legacy JSON.stringify path),
100
+ * - `''` (empty string) when the tool is in the map but has no
101
+ * meaningful primary arg (caller renders just the tool name),
102
+ * - the truncated string value of the primary arg otherwise.
103
+ *
104
+ * Exposed for unit tests. Pure function, no side effects.
105
+ */
106
+ function buildToolPreview(toolName, args) {
107
+ if (!Object.prototype.hasOwnProperty.call(exports.TOOL_PRIMARY_ARG, toolName)) {
108
+ return null;
109
+ }
110
+ const argKey = exports.TOOL_PRIMARY_ARG[toolName];
111
+ if (argKey === '')
112
+ return '';
113
+ if (!args || typeof args !== 'object')
114
+ return '';
115
+ const raw = args[argKey];
116
+ if (raw === undefined || raw === null)
117
+ return '';
118
+ let str;
119
+ if (typeof raw === 'string') {
120
+ str = raw;
121
+ }
122
+ else if (typeof raw === 'number' || typeof raw === 'boolean') {
123
+ str = String(raw);
124
+ }
125
+ else {
126
+ try {
127
+ str = JSON.stringify(raw);
128
+ }
129
+ catch {
130
+ str = String(raw);
131
+ }
132
+ }
133
+ // Collapse whitespace so multi-line commands stay on one preview row.
134
+ str = str.replace(/\s+/g, ' ').trim();
135
+ if (str.length > PREVIEW_MAX_CHARS) {
136
+ str = `${str.slice(0, PREVIEW_MAX_CHARS - 1)}…`;
137
+ }
138
+ return str;
139
+ }