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
@@ -36,6 +36,48 @@ const TerminalRenderer = require('marked-terminal').default ?? require('marked-t
36
36
  function paint(kind) {
37
37
  return (text) => (0, skinEngine_1.getSkinEngine)().applyColors(text, kind);
38
38
  }
39
+ /**
40
+ * v4.1.3-essentials: bold (`**foo**`) markdown emphasis renders as
41
+ * ANSI bold + underline. Previously painted 'brand' (orange) which
42
+ * collided with the heading hierarchy. Briefly tried bold + bright-
43
+ * white; landed on bold + underline because underline carries
44
+ * emphasis without consuming a color slot — the palette stays
45
+ * available for state semantics (yellow=degraded, red=error, etc.).
46
+ *
47
+ * ANSI sequence: `\x1b[1m\x1b[4m{text}\x1b[24m\x1b[22m` — bold ON +
48
+ * underline ON, then underline OFF + bold OFF. Reset order matters
49
+ * (underline first, bold second) so the closing codes don't reorder
50
+ * styles surprisingly on terminals that batch SGR updates.
51
+ *
52
+ * Bypasses the skin system intentionally — bold-as-underline is an
53
+ * opinionated default for this slice. Same caveat as the prior bold-
54
+ * as-color iteration: nested markdown loses the outer style after
55
+ * close (pre-existing limitation of the painter-stack architecture).
56
+ *
57
+ * Honors `NO_COLOR=1` per the standard (skips the wrap entirely).
58
+ * Strictly speaking `NO_COLOR` is about color and underline isn't
59
+ * a color, but the wrap still emits ANSI escapes; honoring the env
60
+ * var keeps output paste-safe in scripted contexts.
61
+ */
62
+ function paintBoldUnderline(text) {
63
+ if (process.env.NO_COLOR && process.env.NO_COLOR !== '')
64
+ return text;
65
+ return `\x1b[1m\x1b[4m${text}\x1b[24m\x1b[22m`;
66
+ }
67
+ /**
68
+ * v4.1.3-essentials reply-polish: bold-on + skin paint + bold-off.
69
+ * Used by the 4-tier heading hierarchy so each level can pick its own
70
+ * color while sharing the bold weight. Emit order matches the rest of
71
+ * the painter stack: outer wrap is bold, inner wrap is fg color.
72
+ *
73
+ * Honors `NO_COLOR=1` via the skin engine's own gate; the bold ANSI
74
+ * still emits because bold is a weight, not a color (matches the
75
+ * paintBoldUnderline convention for `**bold**`).
76
+ */
77
+ function paintBold(kind) {
78
+ const colorize = paint(kind);
79
+ return (text) => `\x1b[1m${colorize(text)}\x1b[22m`;
80
+ }
39
81
  /**
40
82
  * Render a fenced code block: top divider with language label, body
41
83
  * with optional syntax highlighting, bottom divider.
@@ -51,10 +93,37 @@ function paint(kind) {
51
93
  * the renderer with; the older positional path is kept for
52
94
  * compatibility.
53
95
  */
96
+ /**
97
+ * v4.1.3-essentials reply-polish: 24-bit dark background applied per
98
+ * line so code stands out from prose.
99
+ *
100
+ * Color choice: `\x1b[48;2;50;50;60m` (#32323c, slightly bluish dark
101
+ * grey). The original `30,30,30` (#1e1e1e) was invisible against VS
102
+ * Code's integrated terminal default (also #1e1e1e) and barely
103
+ * distinct from Windows Terminal's One Half Dark. #32323c is visibly
104
+ * different from every common dark-terminal default (Campbell, One
105
+ * Half Dark, Solarized Dark, Monokai, VS Code) while staying subtle
106
+ * enough to read as "code chrome" rather than a jarring highlight.
107
+ *
108
+ * Used by BOTH the block path (fenced code blocks) and the inline
109
+ * path (`` `code` `` spans) so the two affordances visually agree —
110
+ * inline code reads as "this is code" via the same chrome as block
111
+ * code, just shorter.
112
+ *
113
+ * NOTE: \x1b[49m is "default background", terminating the per-line
114
+ * background scope cleanly. Each body line is wrapped individually
115
+ * rather than wrapping the whole block, so the background doesn't
116
+ * bleed across the closing horizontal rule (which already paints fg
117
+ * muted with its own reset).
118
+ */
119
+ const CODE_BG_ON = '\x1b[48;2;50;50;60m';
120
+ const CODE_BG_OFF = '\x1b[49m';
54
121
  function renderCodeBlock(code, lang) {
55
122
  const sk = (0, skinEngine_1.getSkinEngine)();
56
123
  const width = Math.min(process.stdout.columns ?? 80, 100) - 4;
57
124
  const langLabel = (lang ?? '').trim();
125
+ // v4.1.3-essentials reply-polish: language tag on the top rule
126
+ // already shipped; keep it. Bottom rule unlabeled (closing fence).
58
127
  const top = langLabel
59
128
  ? `── ${langLabel} ${'─'.repeat(Math.max(0, width - langLabel.length - 4))}`
60
129
  : '─'.repeat(width);
@@ -62,7 +131,22 @@ function renderCodeBlock(code, lang) {
62
131
  const body = (0, syntaxHighlight_1.isSupportedLang)(langLabel)
63
132
  ? (0, syntaxHighlight_1.highlightCode)(code, langLabel)
64
133
  : code;
65
- const indented = body.split('\n').map((ln) => ` ${ln}`).join('\n');
134
+ // v4.1.3-essentials reply-polish: each body line gets:
135
+ // - 2-space outer indent (existing reply container indent)
136
+ // - left rail `│ ` painted muted (mirrors blockquote's `┃ ` rail
137
+ // with a different glyph so they're visually distinct)
138
+ // - 24-bit dark background wrapping the rail + content (subtle
139
+ // "this is code" affordance without going full TUI box-frame)
140
+ //
141
+ // Strip the optional ANSI-only NO_COLOR gate by emitting bg codes
142
+ // unconditionally — the skin engine already short-circuits inner
143
+ // paint calls when NO_COLOR is set, and bare bg codes degrade
144
+ // gracefully on terminals that don't render them.
145
+ const rail = sk.applyColors('│', 'muted');
146
+ const indented = body
147
+ .split('\n')
148
+ .map((ln) => ` ${rail} ${CODE_BG_ON} ${ln} ${CODE_BG_OFF}`)
149
+ .join('\n');
66
150
  return [
67
151
  sk.applyColors(top, 'muted'),
68
152
  indented,
@@ -82,26 +166,61 @@ function renderBlockquote(quote) {
82
166
  .join('\n') + '\n';
83
167
  }
84
168
  /**
85
- * Marked-terminal heading callback gets the rendered heading text +
86
- * level. We paint h1 in brand-bold, h2 in brand, h3+ in heading.
169
+ * v4.1.3-essentials reply-polish: 4-tier heading hierarchy using the
170
+ * existing palette colors so visual weight differs per level even
171
+ * though we don't introduce a new ColorKind.
172
+ *
173
+ * H1 — brand + bold + UPPERCASE (major section heading)
174
+ * H2 — brand + bold (subsection — same hue as H1
175
+ * but sentence-case + no caps)
176
+ * H3 — agent + bold (off-white, lighter weight
177
+ * than brand)
178
+ * H4+ — muted + bold (quietest — same grey as the
179
+ * reply container's chrome)
180
+ *
181
+ * v4.1.3-essentials reply-polish: spacing tightened from `\n\n` to
182
+ * `\n` per level. marked-terminal contributes its own block
183
+ * separator (one more newline) → total `\n\n` between heading and
184
+ * next block = single blank line, matching paragraph rhythm.
185
+ * Previously this emitted `\n\n\n\n` (three blank lines) which made
186
+ * structured replies feel cramped at top and over-aired between
187
+ * sections.
87
188
  */
88
- function renderHeading(text, level, _raw) {
89
- if (level <= 1)
90
- return paint('brand')(text.toUpperCase()) + '\n\n';
91
- if (level === 2)
92
- return paint('brand')(text) + '\n\n';
93
- return paint('heading')(text) + '\n\n';
189
+ // 4-tier hierarchy. Called by the prototype-level `heading` override
190
+ // in getReplyRenderer() which extracts depth from the token first.
191
+ // Plain `(text, depth)` signature; the marked v15 / v14 / positional
192
+ // translation happens in the override.
193
+ //
194
+ // Each tier ends with `\n\n` to fence the heading from the next block
195
+ // with a blank line. Earlier we tried `\n` (single trailing newline)
196
+ // assuming marked-terminal's `section()` wrapper added its own
197
+ // padding — but the prototype-level override bypasses section(), so
198
+ // we own the spacing end-to-end. Result with `\n\n`: heading visible
199
+ // on its own line, blank line separates it from the next paragraph /
200
+ // heading / list. Matches the paragraph rhythm (`text\n\n`).
201
+ function renderHeading(text, depth) {
202
+ if (depth <= 1)
203
+ return paintBold('brand')(text.toUpperCase()) + '\n\n';
204
+ if (depth === 2)
205
+ return paintBold('brand')(text) + '\n\n';
206
+ if (depth === 3)
207
+ return paintBold('agent')(text) + '\n\n';
208
+ return paintBold('muted')(text) + '\n\n';
94
209
  }
95
210
  /**
96
- * List items get a `▸ ` glyph in muted; numbered lists keep their
97
- * numeric prefix (marked-terminal already prepends `N.` for ordered
98
- * lists, so we just paint the body).
211
+ * v4.1.3-essentials reply-polish: the `opts.listitem` callback used to
212
+ * own bullet rendering but marked-terminal's outer `list` method
213
+ * ALSO emits a `* ` prefix, producing visible double bullets
214
+ * (` * ▸ item`). The fix is a prototype-level override on BOTH
215
+ * `list` and `listitem` (mirrors the existing pattern for `code` and
216
+ * `link`). See the override block in getReplyRenderer().
217
+ *
218
+ * This callback now just returns the inner text unchanged so the
219
+ * prototype-level `list` override can do the bullet + indent work
220
+ * with full nesting-depth context.
99
221
  */
100
222
  function renderListItem(text) {
101
- // marked-terminal feeds us the rendered child text. Strip its
102
- // default tab prefix so our two-space indent stays consistent.
103
- const body = text.replace(/^\s+/, '');
104
- return ` ${paint('muted')('▸')} ${body}\n`;
223
+ return text;
105
224
  }
106
225
  /**
107
226
  * Singleton — caching is fine since options bind to the active skin
@@ -121,14 +240,33 @@ function getReplyRenderer() {
121
240
  // method directly below.
122
241
  const opts = {
123
242
  blockquote: renderBlockquote,
124
- heading: renderHeading,
125
- firstHeading: (text, _level, _raw) => paint('brand')(text.toUpperCase()) + '\n\n',
243
+ // v4.1.3-essentials reply-polish: `opts.heading` and `opts.firstHeading`
244
+ // both removed. marked-terminal calls `opts.heading(text)` with ONLY
245
+ // text (audit-confirmed via toString), dropping the depth info we
246
+ // need for the 4-tier hierarchy. The prototype-level `renderer.heading`
247
+ // override below owns the depth extraction + tier selection end-to-end.
248
+ // marked-terminal's stripped-args call path never reaches our callback.
126
249
  hr: () => paint('muted')('─'.repeat(Math.min(process.stdout.columns ?? 80, 100) - 4)) + '\n',
127
250
  listitem: renderListItem,
128
251
  paragraph: (text) => `${text}\n\n`,
129
- strong: paint('brand'),
252
+ // v4.1.3-essentials: bold renders as ANSI bold + underline
253
+ // (was 'brand' / orange, then bright-white; landed on underline
254
+ // so the color palette stays available for state semantics).
255
+ strong: paintBoldUnderline,
130
256
  em: paint('muted'),
131
- codespan: (text) => paint('accent')(`\`${text}\``),
257
+ // v4.1.3-essentials reply-polish: inline `` `code` `` — strip
258
+ // the literal backticks (used to leak into the visible output)
259
+ // and wrap with the same dark background as fenced code blocks.
260
+ // Visual consistency: inline code reads as "this is code" via the
261
+ // same chrome as block code, just shorter. One leading + trailing
262
+ // space inside the bg span gives the chrome a bit of padding so
263
+ // letters don't sit flush against the bg edge.
264
+ //
265
+ // Trade-off (accepted): if an inline-code span breaks across a
266
+ // line wrap, the bg painting may show a visual seam at the wrap
267
+ // point. Acceptable for v4.1.3 — revertable to Path A (no bg) if
268
+ // visual smoke surfaces a real problem.
269
+ codespan: (text) => `${CODE_BG_ON} ${paint('accent')(text)} ${CODE_BG_OFF}`,
132
270
  del: paint('muted'),
133
271
  // marked-terminal calls opts.link with the ASSEMBLED visual
134
272
  // (already OSC8-wrapped when the host terminal supports it),
@@ -185,6 +323,159 @@ function getReplyRenderer() {
185
323
  const painted = paint('accent')(label);
186
324
  return `\x1b]8;;${url}\x1b\\${painted}\x1b]8;;\x1b\\`;
187
325
  };
326
+ // v4.1.3-essentials reply-polish: prototype-level `heading` override.
327
+ //
328
+ // Why: marked-terminal's internal `heading` method extracts the
329
+ // token's depth, then calls `opts.heading(text)` with ONLY the
330
+ // text — dropping the level info on the floor. Our 4-tier hierarchy
331
+ // (H1 brand+caps, H2 brand, H3 agent, H4+ muted) needs level
332
+ // context, so we must own the whole method.
333
+ //
334
+ // The override accepts marked v15's token-object shape and falls
335
+ // through to v14 positional for unit tests that pass plain strings.
336
+ renderer.heading = function (textOrToken, levelArg, _raw) {
337
+ let text;
338
+ let depth;
339
+ if (typeof textOrToken === 'object' && textOrToken !== null) {
340
+ const tok = textOrToken;
341
+ depth = typeof tok.depth === 'number' ? tok.depth : 1;
342
+ // Prefer parseInline for rich heading content (e.g. `## H2 with **bold**`).
343
+ // Falls through to tok.text for the common plain-text case.
344
+ const parser = this.parser;
345
+ if (tok.tokens && parser?.parseInline) {
346
+ text = parser.parseInline(tok.tokens);
347
+ }
348
+ else {
349
+ text = String(tok.text ?? '');
350
+ }
351
+ }
352
+ else {
353
+ text = String(textOrToken ?? '');
354
+ depth = typeof levelArg === 'number' ? levelArg : 1;
355
+ }
356
+ return renderHeading(text, depth);
357
+ };
358
+ // v4.1.3-essentials reply-polish: prototype-level list overrides.
359
+ //
360
+ // Why two functions and a depth counter:
361
+ // - marked-terminal's default `list` injects a `* ` (or `N. `)
362
+ // prefix BEFORE calling our `opts.listitem` callback, producing
363
+ // visible double bullets — see audit. Owning `list` at the
364
+ // prototype level lets us suppress that and emit our own.
365
+ // - Nesting depth determines the bullet glyph: top-level gets `•`
366
+ // and any deeper level gets `▸`. marked doesn't pass depth to
367
+ // the renderer, so we track it on the renderer instance via a
368
+ // counter that increments on `list`-enter and decrements on
369
+ // exit. This works because marked walks the token tree
370
+ // synchronously: a nested list's `list` call always completes
371
+ // between its parent's `list`-enter and `list`-exit.
372
+ // - Items already had their child markdown rendered via the
373
+ // prototype's `listitem` (which we leave as a passthrough above
374
+ // in the opts block — it just returns the inner text). The
375
+ // body string we receive in `list` is the concatenated children;
376
+ // each child can itself be a nested list rendering, whose own
377
+ // `list` call already handled its bullets + indent.
378
+ //
379
+ // Numbered lists: `start` and `ordered` come from the token; we
380
+ // emit `N.` prefix in muted to keep the visual rhythm consistent
381
+ // with bulleted lists but preserve numeric semantics.
382
+ //
383
+ // Indent: 2 spaces per nesting level. Top-level items therefore
384
+ // sit at column 2 (matching the rest of the reply container's
385
+ // chrome); nested at column 4, 6, etc.
386
+ const proto = renderer;
387
+ proto._listDepth = 0;
388
+ renderer.listitem = function (text, _task, _checked) {
389
+ // marked v15 may pass a token object; the assembled-text fallback
390
+ // covers older signatures. Either way we want the inner text
391
+ // unchanged here — bullet + indent is owned by `list` below.
392
+ if (typeof text === 'object' && text !== null) {
393
+ const tok = text;
394
+ if (typeof tok.text === 'string')
395
+ return tok.text;
396
+ const parser = this.parser;
397
+ return parser?.parseInline?.(tok.tokens ?? []) ?? '';
398
+ }
399
+ return String(text ?? '');
400
+ };
401
+ renderer.list = function (body, ordered, start) {
402
+ // marked v15 token shape: { ordered, start, items: [token, ...] }
403
+ // Older positional shape: (body, ordered, start)
404
+ let isOrdered = false;
405
+ let startNum = 1;
406
+ let items;
407
+ // CRITICAL: increment depth BEFORE walking items. Item walking via
408
+ // `parser.parse(it.tokens)` recurses into our own override for any
409
+ // nested list tokens — those nested calls need to see the parent's
410
+ // incremented depth so they pick the deeper bullet glyph (▸) and
411
+ // indent. If we increment AFTER `parser.parse`, the nested call
412
+ // sees depth=0, renders at top-level styling, and the visible
413
+ // nesting collapses. Confirmed via runtime trace.
414
+ proto._listDepth = (proto._listDepth ?? 0) + 1;
415
+ const depth = proto._listDepth;
416
+ if (typeof body === 'object' && body !== null) {
417
+ const tok = body;
418
+ isOrdered = tok.ordered === true;
419
+ startNum = typeof tok.start === 'number' ? tok.start : 1;
420
+ // marked v15: renderer instance has a `parser` field pointing
421
+ // back to the Parser; `Parser.parse(tokens)` walks the token
422
+ // tree dispatching back to renderer methods (including this
423
+ // very `list` override for nested lists, which is what makes
424
+ // the depth counter increment properly).
425
+ const parser = this.parser;
426
+ items = (tok.items ?? []).map((it) => {
427
+ if (it.tokens && parser?.parse) {
428
+ return parser.parse(it.tokens);
429
+ }
430
+ return it.text ?? '';
431
+ });
432
+ }
433
+ else {
434
+ isOrdered = ordered === true;
435
+ startNum = typeof start === 'number' ? start : 1;
436
+ // Positional `body` is the already-concatenated rendered items.
437
+ // Split on newlines that introduce a fresh item; marked emits
438
+ // each item as its own logical line. Best-effort — marked v15
439
+ // path above is the production case.
440
+ const raw = String(body ?? '');
441
+ items = raw.split('\n').filter((ln) => ln.trim().length > 0);
442
+ }
443
+ const indent = ' '.repeat(depth);
444
+ // Top-level bullet `•` (filled); nested `▸` (arrow-like) for
445
+ // visual depth differentiation. Numbered lists override with
446
+ // `N.` regardless of depth.
447
+ const bulletGlyph = depth === 1 ? '•' : '▸';
448
+ const lines = [];
449
+ for (let i = 0; i < items.length; i += 1) {
450
+ const item = items[i];
451
+ // Each item may itself contain newlines (nested list output,
452
+ // multi-line paragraph). Indent every line of the rendered
453
+ // item AFTER the first — the first line takes the bullet, the
454
+ // continuation lines align under the bullet's content column.
455
+ const marker = isOrdered
456
+ ? paint('muted')(`${startNum + i}.`)
457
+ : paint('muted')(bulletGlyph);
458
+ const itemLines = item.split('\n');
459
+ const head = itemLines[0] ?? '';
460
+ const tail = itemLines.slice(1);
461
+ lines.push(`${indent}${marker} ${head}`);
462
+ // Continuation lines: if they already have content, align them
463
+ // under the bullet's text column (indent + marker-width + 1
464
+ // space). marked-terminal's nested lists arrive pre-indented so
465
+ // we pass them through.
466
+ for (const tailLine of tail) {
467
+ if (tailLine.length === 0)
468
+ continue;
469
+ lines.push(tailLine);
470
+ }
471
+ }
472
+ proto._listDepth -= 1;
473
+ // Top-level list closes with a trailing newline to separate from
474
+ // the next block; nested lists return without extra padding so
475
+ // they nest cleanly inside their parent item.
476
+ const out = lines.join('\n');
477
+ return proto._listDepth === 0 ? out + '\n' : out + '\n';
478
+ };
188
479
  cachedRenderer = {
189
480
  render(text) {
190
481
  try {
@@ -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
+ }
@@ -49,10 +49,17 @@ const DEFAULT_SKIN = {
49
49
  error: [0xf4, 0x47, 0x47],
50
50
  warn: [0xff, 0xc1, 0x07],
51
51
  success: [0x4c, 0xaf, 0x50],
52
- // Phase 22 colour discipline: soft cyan replaces grey for secondary
53
- // text (timestamps, hints, tips, dim diagnostics).
54
- muted: [0x6f, 0xb3, 0xd2],
52
+ // v4.1.3-repl-polish: muted is now true grey (#888) so it reads as
53
+ // genuinely secondary. The soft-cyan that was here moved to 'session'.
54
+ muted: [0x88, 0x88, 0x88],
55
55
  heading: BRAND_ORANGE,
56
+ // v4.1.3-repl-polish: session = soft cyan (ex-muted); used for IDs
57
+ // and the session-end card header labels.
58
+ session: [0x6f, 0xb3, 0xd2],
59
+ // v4.1.3-repl-polish: degraded = amber yellow; distinct from warn
60
+ // (which shares the colour) so callers can differentiate in code
61
+ // even though they render identically.
62
+ degraded: [0xff, 0xc1, 0x07],
56
63
  },
57
64
  glyphs: {
58
65
  bullet: '•',
@@ -79,6 +86,8 @@ const LIGHT_SKIN = {
79
86
  success: [0x1b, 0x5e, 0x20],
80
87
  muted: [0x60, 0x60, 0x60],
81
88
  heading: [0xc4, 0x42, 0x10],
89
+ session: [0x00, 0x55, 0x88],
90
+ degraded: [0x80, 0x60, 0x00],
82
91
  },
83
92
  glyphs: { ...DEFAULT_SKIN.glyphs },
84
93
  };
@@ -96,6 +105,8 @@ const MONOCHROME_SKIN = {
96
105
  success: null,
97
106
  muted: null,
98
107
  heading: null,
108
+ session: null,
109
+ degraded: null,
99
110
  },
100
111
  glyphs: {
101
112
  bullet: '*',
@@ -0,0 +1,153 @@
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
+ // ── v4.1.4-media — three-layer media-control bundle ──────────────────
90
+ // `media_sessions` has no args by schema; the empty-arg preview is
91
+ // suppressed by buildToolPreview returning ''.
92
+ // `media_transport` → preview by target ("spotify"), the actionable
93
+ // identifier the user typed. `action` is intentionally NOT chosen —
94
+ // GSMTC actions (play/pause/toggle) are short, the target is the
95
+ // discriminator.
96
+ // `media_key` is layer-3 fallback; show `action` since there's no
97
+ // target to surface (it's a blind keystroke).
98
+ // `app_input` shows `app` so the user sees which window got the keys.
99
+ media_sessions: '',
100
+ media_transport: 'target',
101
+ media_key: 'action',
102
+ app_input: 'app',
103
+ };
104
+ /**
105
+ * Maximum visible characters for the preview value. Long commands /
106
+ * full file contents get truncated with an ellipsis so a single tool
107
+ * row stays on one line at typical terminal widths.
108
+ */
109
+ const PREVIEW_MAX_CHARS = 120;
110
+ /**
111
+ * Build the per-tool preview string for `args`. Returns:
112
+ * - `null` when the tool isn't in the map (caller falls back to the
113
+ * legacy JSON.stringify path),
114
+ * - `''` (empty string) when the tool is in the map but has no
115
+ * meaningful primary arg (caller renders just the tool name),
116
+ * - the truncated string value of the primary arg otherwise.
117
+ *
118
+ * Exposed for unit tests. Pure function, no side effects.
119
+ */
120
+ function buildToolPreview(toolName, args) {
121
+ if (!Object.prototype.hasOwnProperty.call(exports.TOOL_PRIMARY_ARG, toolName)) {
122
+ return null;
123
+ }
124
+ const argKey = exports.TOOL_PRIMARY_ARG[toolName];
125
+ if (argKey === '')
126
+ return '';
127
+ if (!args || typeof args !== 'object')
128
+ return '';
129
+ const raw = args[argKey];
130
+ if (raw === undefined || raw === null)
131
+ return '';
132
+ let str;
133
+ if (typeof raw === 'string') {
134
+ str = raw;
135
+ }
136
+ else if (typeof raw === 'number' || typeof raw === 'boolean') {
137
+ str = String(raw);
138
+ }
139
+ else {
140
+ try {
141
+ str = JSON.stringify(raw);
142
+ }
143
+ catch {
144
+ str = String(raw);
145
+ }
146
+ }
147
+ // Collapse whitespace so multi-line commands stay on one preview row.
148
+ str = str.replace(/\s+/g, ' ').trim();
149
+ if (str.length > PREVIEW_MAX_CHARS) {
150
+ str = `${str.slice(0, PREVIEW_MAX_CHARS - 1)}…`;
151
+ }
152
+ return str;
153
+ }