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,13 +18,15 @@
18
18
  *
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.Display = exports.SPINNER_PHRASES = exports.TOOL_ICONS = void 0;
21
+ exports.TOOL_ROW_ARG_CAP = exports.TOOL_ROW_NAME_PAD = exports.Display = exports.SPINNER_PHRASES = exports.TOOL_ICONS = void 0;
22
22
  exports.iconForTool = iconForTool;
23
23
  exports.detectConfiguredChannels = detectConfiguredChannels;
24
24
  exports.summarizeConfiguredChannels = summarizeConfiguredChannels;
25
25
  exports.summarizeChannelState = summarizeChannelState;
26
26
  exports.voiceIndicator = voiceIndicator;
27
27
  exports.previewToolArgs = previewToolArgs;
28
+ exports.countNewlines = countNewlines;
29
+ exports.splitAtUnclosedBold = splitAtUnclosedBold;
28
30
  exports.formatToolDuration = formatToolDuration;
29
31
  exports.formatCompactTokens = formatCompactTokens;
30
32
  exports.formatElapsedShort = formatElapsedShort;
@@ -36,77 +38,67 @@ const marked_1 = require("marked");
36
38
  const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
37
39
  const skinEngine_1 = require("./skinEngine");
38
40
  const box_1 = require("./box");
41
+ const toolTrail_1 = require("./display/toolTrail");
42
+ // v4.1.3-essentials — capability card renderer (auth/platform failures).
43
+ const capabilityCard_1 = require("./display/capabilityCard");
39
44
  // Phase v4.1-reply-formatting: skin-aware markdown renderer that
40
45
  // replaces marked-terminal's defaults with structured headers, lists,
41
46
  // code blocks, blockquotes, and links.
42
47
  const replyRenderer_1 = require("./replyRenderer");
43
48
  // Optional "Sources" footer when AIDEN_CITATIONS=1 (default off).
44
49
  const citationFooter_1 = require("./citationFooter");
50
+ const toolPreview_1 = require("./toolPreview");
45
51
  /**
46
- * Phase 26.2.7 — category emoji icons for the tool-row prefix when
47
- * `AIDEN_UI_ICONS=1` is set in the environment. Default OFF (the
48
- * row stays at `·`) because emoji width and font availability vary
49
- * across Windows Terminal / older Console hosts / SSH sessions.
52
+ * v4.1.3-repl-polish — category emoji icons for the tool-row trail.
53
+ * Icons are ON by default (AIDEN_UI_ICONS !== '0'). Set
54
+ * `AIDEN_UI_ICONS=0` to disable them (CI / dumb terminals).
50
55
  *
51
- * Categories tool name is matched against keys in this map:
52
- * 1. exact-match first (lowercased toolName)
53
- * 2. then substring match in insertion order
54
- * 3. fall back to `default` (·)
56
+ * Kept as a flat Record for backward-compat with smoke tests that
57
+ * import it directly. The canonical lookup now lives in
58
+ * `./display/toolTrail` (supports verb too); this map is derived
59
+ * from it for legacy reference.
55
60
  *
56
- * Keep the map small and category-based, NOT one-per-tool.
61
+ * Matching order: exact lowercased name, then substring, then 'default'.
57
62
  */
58
63
  exports.TOOL_ICONS = {
59
- // Observe / read / inspect
60
- observe: '👁',
61
- read: '👁',
62
- file_read: '👁',
63
- list: '👁',
64
- // Think / analyze / plan
65
- analyze: '🧠',
66
- think: '🧠',
67
- plan: '📋',
68
- skills_list: '📋',
69
- // Execute / write / run
70
- execute: '⚡',
71
- run: '⚡',
72
- bash: '⚡',
73
- powershell: '⚡',
74
- code: '⚡',
75
- skill_view: '⚡',
76
- write: '✏',
77
- edit: '✏',
64
+ // Observe / read
65
+ file_read: '👁', read_file: '👁', file_list: '👁', list_directory: '👁',
66
+ observe: '👁', read: '👁', list: '👁',
67
+ // Write / edit
68
+ file_write: '', write_file: '✏', edit_file: '✏',
69
+ write: '✏', edit: '✏', create: '✏',
70
+ // Execute / run
71
+ bash: '', powershell: '⚡', execute_code: '⚡', skill_view: '⚡',
72
+ execute: '', run: '⚡',
78
73
  // Web / browse
79
- web_search: '🌐',
80
- web_fetch: '🌐',
81
- open_url: '🌐',
82
- browser: '🌐',
83
- // Memory
84
- memory: '🧠',
85
- recall: '🧠',
74
+ web_search: '🌐', web_fetch: '🌐', fetch_url: '🌐', open_url: '🌐',
75
+ navigate: '🌐', browser: '🌐', fetch: '🌐', search: '🌐',
76
+ // Memory / recall
77
+ recall_session: '🧠', session_search: '🧠', memory: '🧠', recall: '🧠',
78
+ // Think
79
+ session_summary: '🧠', analyze: '🧠', think: '🧠',
80
+ // Skills / catalog
81
+ skills_list: '📋', skill: '📋',
82
+ // Screen / capture
83
+ screenshot: '🖥', computer: '🖥',
84
+ // Media / launch
85
+ now_playing: '▶', app_launch: '▶', media: '▶',
86
+ // Deploy / build
87
+ deploy: '📦', build: '📦', push: '📦',
88
+ // Message / send
89
+ send: '💬', message: '💬', notify: '💬',
86
90
  // Verify / test
87
- verify: '🛡',
88
- test: '🛡',
89
- // Default fallback (matches current behaviour).
91
+ verify: '🛡', test: '🛡', doctor: '🛡', health: '🛡',
92
+ // Default fallback
90
93
  default: '·',
91
94
  };
92
95
  /**
93
- * Phase 26.2.7 — return the category emoji for `toolName` from
94
- * `TOOL_ICONS`, or `·` when nothing matches. Lowercases the input
95
- * and tries exact match first, then substring match in the map's
96
- * insertion order. Pure — exported for smoke testing.
96
+ * Return the category emoji for `toolName`, or '·' when nothing matches.
97
+ * Delegates to the canonical toolTrail lookup (returns icon only).
98
+ * Exported for backward-compat with existing smoke / unit tests.
97
99
  */
98
100
  function iconForTool(toolName) {
99
- const lc = toolName.toLowerCase();
100
- const exact = exports.TOOL_ICONS[lc];
101
- if (exact)
102
- return exact;
103
- for (const [key, glyph] of Object.entries(exports.TOOL_ICONS)) {
104
- if (key === 'default')
105
- continue;
106
- if (lc.includes(key))
107
- return glyph;
108
- }
109
- return exports.TOOL_ICONS.default;
101
+ return (0, toolTrail_1.iconForTool)(toolName).icon;
110
102
  }
111
103
  /**
112
104
  * Phase 26.2.6 — pool of fun spinner phrases that the chat REPL
@@ -240,6 +232,15 @@ class Display {
240
232
  // marked-terminal optional — markdown() falls back to raw text below
241
233
  }
242
234
  }
235
+ /**
236
+ * v4.1.3-repl-polish — public colour gate so callers (e.g. session-end
237
+ * card renderer) can colour text without importing SkinEngine directly.
238
+ * Delegates to the active skin's applyColors(). Monochrome mode is
239
+ * respected the same way as internal calls.
240
+ */
241
+ applyColors(text, kind) {
242
+ return this.skin.applyColors(text, kind);
243
+ }
243
244
  /**
244
245
  * Build the welcome banner string (does not write).
245
246
  *
@@ -393,13 +394,18 @@ class Display {
393
394
  const pill = (on, label, value) => `${dot(on)} ${lab(label)} ${val(value)}`;
394
395
  const providerOk = args.providerOk !== false;
395
396
  const modelValue = providerOk ? args.model : 'not configured';
396
- return (' ' +
397
- [
398
- pill(args.coreOnline, 'core', args.coreOnline ? 'online' : 'starting'),
399
- pill(true, 'mode', args.mode),
400
- pill(providerOk, 'model', modelValue),
401
- pill(args.memoryActive, 'memory', args.memoryActive ? 'active' : 'off'),
402
- ].join(' '));
397
+ const pills = [
398
+ pill(args.coreOnline, 'core', args.coreOnline ? 'online' : 'starting'),
399
+ pill(true, 'mode', args.mode),
400
+ pill(providerOk, 'model', modelValue),
401
+ pill(args.memoryActive, 'memory', args.memoryActive ? 'active' : 'off'),
402
+ ];
403
+ if (args.version) {
404
+ // Version pill: dot + value, no label (the `v` prefix is the label).
405
+ // Always-on dot — informational, not a health indicator.
406
+ pills.push(`${dot(true)} ${val(`v${args.version}`)}`);
407
+ }
408
+ return ' ' + pills.join(' ');
403
409
  }
404
410
  /**
405
411
  * Two-column block (Environment + Capabilities). Side-by-side when
@@ -732,55 +738,133 @@ class Display {
732
738
  // ANSI cursor games on a dumb sink.
733
739
  toolRow(name, args) {
734
740
  const sk = this.skin;
735
- const argStr = previewToolArgs(args);
736
- const padded = name.length > TOOL_ROW_NAME_PAD
737
- ? name.slice(0, TOOL_ROW_NAME_PAD)
738
- : name.padEnd(TOOL_ROW_NAME_PAD);
739
- // Phase 26.2.7 category emoji icon when AIDEN_UI_ICONS=1, else
740
- // the default muted middle-dot. Read at call-time so toggling
741
- // the env var doesn't require a restart. Emoji are rendered raw
742
- // (no SGR wrap) because most terminals paint emoji glyphs in
743
- // their native colour and ignore foreground ANSI anyway.
744
- const useIcons = process.env.AIDEN_UI_ICONS === '1';
745
- const glyph = useIcons ? iconForTool(name) : sk.applyColors('·', 'muted');
746
- const left = ` ${glyph} ` +
747
- `${sk.applyColors('tool', 'muted')} ` +
748
- `${sk.applyColors(padded, 'tool')} ` +
749
- `${sk.applyColors(argStr, 'muted')}`;
750
- const renderBracket = (text, kind) => {
751
- const colored = sk.applyColors(`[${text}]`, kind);
752
- return `${left} ${colored}\n`;
741
+ // ── Build the fixed left portion (icon + verb + detail) ────────────
742
+ // v4.1.3-repl-polish: icons default ON; set AIDEN_UI_ICONS=0 to
743
+ // disable (CI / dumb terminals / narrow SSH sessions).
744
+ // Read at call-time so env changes take effect without restart.
745
+ const useIcons = process.env.AIDEN_UI_ICONS !== '0';
746
+ const { icon, verb } = (0, toolTrail_1.iconForTool)(name);
747
+ const glyph = useIcons ? icon : sk.applyColors('·', 'muted');
748
+ // Detail field: v4.1.4-media consult `buildToolPreview` first so
749
+ // tools registered in `TOOL_PRIMARY_ARG` (media_transport 'target',
750
+ // media_key 'action', file_read 'path', etc.) get their
751
+ // meaningful primary-arg preview instead of a JSON blob. Fall back
752
+ // to the generic `previewToolArgs` scan for tools the map doesn't
753
+ // know about so unregistered MCP tools etc. still render readably.
754
+ const mapped = (0, toolPreview_1.buildToolPreview)(name, args);
755
+ const detail = (0, toolTrail_1.truncDetail)(mapped ?? previewToolArgs(args));
756
+ // v4.1.3-essentials: live tool indicator. Capture wall-clock start
757
+ // so the running-row renderer can append an elapsed-time suffix
758
+ // ("running 3s…") after the first second. Sub-second tools render
759
+ // without the suffix — no flash of `running 0s` for fast paths.
760
+ const startedAt = Date.now();
761
+ // Running row — muted pipe, raw icon, tool-colored verb, muted detail.
762
+ // The optional `running Ns…` tail appears once the tool crosses the
763
+ // 1-second mark; the tick interval below redraws this row every 1s.
764
+ const runningRow = () => {
765
+ const elapsed = Date.now() - startedAt;
766
+ const liveSuffix = elapsed >= 1000
767
+ ? ` ${sk.applyColors(`running ${formatToolDuration(elapsed)}…`, 'muted')}`
768
+ : '';
769
+ return `${sk.applyColors(toolTrail_1.TRAIL_PIPE, 'muted')} ${glyph} ` +
770
+ `${sk.applyColors((0, toolTrail_1.padVerb)(verb), 'tool')} ` +
771
+ `${sk.applyColors(detail, 'muted')}${liveSuffix}\n`;
753
772
  };
754
- const isTty = !!this.out.isTTY;
773
+ // Outcome row — entire line colored by outcome kind.
774
+ const outcomeRow = (suffix, kind) => {
775
+ const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail}` +
776
+ (suffix ? ` ${suffix}` : '');
777
+ return `${sk.applyColors(content, kind)}\n`;
778
+ };
779
+ // Capture stream reference so closures don't need `this`.
780
+ const out = this.out;
781
+ const isTty = !!out.isTTY;
755
782
  let printed = false;
756
- const writeFinal = (text, kind) => {
757
- if (isTty && printed) {
758
- // Move up one line, clear it, then write the final row.
759
- this.out.write('\x1b[1A\x1b[2K\r');
783
+ // v4.1.3-essentials: tick handle for the live-elapsed update. Set
784
+ // when we start the interval; cleared by every terminal method
785
+ // (ok / fail / degraded / blocked / emptyFail / emptyRetry) AND by
786
+ // `retry` (retry is a state announcement and should hold static
787
+ // until the next state change — race-free).
788
+ let tickTimer = null;
789
+ const stopTick = () => {
790
+ if (tickTimer !== null) {
791
+ clearInterval(tickTimer);
792
+ tickTimer = null;
760
793
  }
761
- this.out.write(renderBracket(text, kind));
794
+ };
795
+ // Erase the last printed line (TTY only).
796
+ const eraseLast = () => {
797
+ if (isTty && printed)
798
+ out.write('\x1b[1A\x1b[2K\r');
799
+ };
800
+ const writeFinal = (suffix, kind) => {
801
+ stopTick();
802
+ eraseLast();
803
+ out.write(outcomeRow(suffix, kind));
762
804
  printed = true;
763
805
  };
764
806
  if (isTty) {
765
- this.out.write(renderBracket('running', 'warn'));
807
+ // v4.1.3-essentials (replaces v4.1.3-repl-polish streamInterrupted
808
+ // flag pattern): if a stream is active, fence off the current
809
+ // chunk BEFORE the running row writes. `commitStreamChunk` does
810
+ // its own newline-fencing + in-place rerender of the just-streamed
811
+ // chunk so this row lands cleanly on its own line below.
812
+ this.commitStreamChunk();
813
+ out.write(runningRow());
766
814
  printed = true;
815
+ // v4.1.3-essentials: start the live-elapsed ticker. Fires every
816
+ // 1s; first tick at +1s, when `runningRow()` starts emitting the
817
+ // `running 1s…` suffix. Cleared by every terminal method via
818
+ // `stopTick()` — no leaked timers across the tool lifecycle.
819
+ // Tool dispatch in aidenAgent is sequential (one tool at a time
820
+ // per turn) so the assumption "running row is the last written
821
+ // line" holds for the whole tick lifetime; `eraseLast()` is safe.
822
+ tickTimer = setInterval(() => {
823
+ if (!printed)
824
+ return;
825
+ eraseLast();
826
+ out.write(runningRow());
827
+ }, 1000);
767
828
  }
768
- // On non-TTY we hold off entirely until the caller signals completion.
829
+ // Non-TTY: hold off until completion (log lines carry final state).
830
+ // No tick — non-TTY sinks (pipes, CI logs) get one line per call
831
+ // with the final state; live updates would be noise in scrollback.
769
832
  return {
770
833
  ok(durationMs, retries = 0) {
771
- const text = retries > 0
772
- ? `ok ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`
773
- : `ok ${formatToolDuration(durationMs)}`;
774
- writeFinal(text, 'success');
834
+ stopTick();
835
+ if (retries > 0) {
836
+ // Showed retries — surface the eventual success in warn so the
837
+ // user knows it took multiple attempts.
838
+ writeFinal(`ok ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`, 'warn');
839
+ }
840
+ else {
841
+ // Clean success — SILENT. Erase on TTY; emit nothing on non-TTY.
842
+ eraseLast();
843
+ }
775
844
  },
776
845
  fail(durationMs, retries = 0) {
777
- const text = retries > 0
846
+ const suffix = retries > 0
778
847
  ? `fail ${formatToolDuration(durationMs)} after ${retries} ${retries === 1 ? 'retry' : 'retries'}`
779
848
  : `fail ${formatToolDuration(durationMs)}`;
780
- writeFinal(text, 'error');
849
+ writeFinal(suffix, 'error');
850
+ },
851
+ degraded(durationMs, reason) {
852
+ const suffix = reason
853
+ ? `partial ${formatToolDuration(durationMs)} — ${reason}`
854
+ : `partial ${formatToolDuration(durationMs)}`;
855
+ writeFinal(suffix, 'degraded');
781
856
  },
782
857
  retry(n, m) {
783
- writeFinal(`retry ${n}/${m} …`, 'warn');
858
+ // v4.1.3-essentials: retry is a state-change announcement —
859
+ // freeze the row at the retry counter until next state change.
860
+ // Stopping the ticker prevents the next 1s tick from racing
861
+ // back over the retry counter with `running Ns…`.
862
+ stopTick();
863
+ // Update the running row with retry count.
864
+ eraseLast();
865
+ const content = `${toolTrail_1.TRAIL_PIPE} ${glyph} ${(0, toolTrail_1.padVerb)(verb)} ${detail} retry ${n}/${m} …`;
866
+ out.write(sk.applyColors(content, 'warn') + '\n');
867
+ printed = true;
784
868
  },
785
869
  blocked() {
786
870
  writeFinal('blocked', 'warn');
@@ -794,11 +878,24 @@ class Display {
794
878
  };
795
879
  }
796
880
  /**
797
- * Pretty-print a tool call before it executes. Args are JSON-stringified
798
- * with a 200-char hard cap so megabyte arguments don't flood the screen.
881
+ * Pretty-print a tool call before it executes. Phase v4.1.2 first
882
+ * consults the `TOOL_PRIMARY_ARG` map in `toolPreview.ts` to render
883
+ * just the meaningful argument (e.g. `terminal: npm test`); falls
884
+ * back to the legacy full-JSON stringification (200-char hard cap)
885
+ * for tools that aren't in the map.
799
886
  */
800
887
  toolPreview(name, args) {
801
888
  const sk = this.skin;
889
+ const arrow = sk.getActive().glyphs?.arrow ?? '>';
890
+ // Phase v4.1.2: per-tool primary-arg preview.
891
+ const preview = (0, toolPreview_1.buildToolPreview)(name, args);
892
+ if (preview !== null) {
893
+ if (preview === '') {
894
+ return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')}`;
895
+ }
896
+ return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')} ${sk.applyColors(preview, 'muted')}`;
897
+ }
898
+ // Unknown tool — original behaviour (full JSON, 200-char cap).
802
899
  let serialized;
803
900
  try {
804
901
  serialized = JSON.stringify(args);
@@ -808,7 +905,6 @@ class Display {
808
905
  }
809
906
  if (serialized.length > 200)
810
907
  serialized = `${serialized.slice(0, 197)}...`;
811
- const arrow = sk.getActive().glyphs?.arrow ?? '>';
812
908
  return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')} ${sk.applyColors(serialized, 'muted')}`;
813
909
  }
814
910
  /**
@@ -955,6 +1051,30 @@ class Display {
955
1051
  printError(message, suggestion) {
956
1052
  this.out.write(this.error(message, suggestion));
957
1053
  }
1054
+ /**
1055
+ * v4.1.3-essentials: render a structured capability card. Used when
1056
+ * a tool fails because a capability is missing (platform unsupported,
1057
+ * auth not present, key env-var unset) and a generic error wouldn't
1058
+ * give the user enough signal. The card lists what the user CAN
1059
+ * still do, what's blocked, and a one-line fix.
1060
+ *
1061
+ * Delegates the layout to the pure renderer in `./display/capability-
1062
+ * Card.ts`. This method is the I/O boundary — caller passes data, we
1063
+ * write lines to the configured stdout. Trailing newline ensures the
1064
+ * card sits clean above whatever renders next (typically the prompt).
1065
+ */
1066
+ capabilityCard(data) {
1067
+ // v4.1.3-essentials: fence off any active stream chunk before the
1068
+ // card writes so the chunk gets rerendered as markdown and the
1069
+ // card lands below it on its own line. Same pattern as toolRow /
1070
+ // streamToolIndicator.
1071
+ this.commitStreamChunk();
1072
+ const lines = (0, capabilityCard_1.renderCapabilityCard)(data, (t, k) => this.applyColors(t, k));
1073
+ for (const line of lines) {
1074
+ this.out.write(line + '\n');
1075
+ }
1076
+ this.out.write('\n');
1077
+ }
958
1078
  /**
959
1079
  * Append a streamed text fragment. Writes a styled "Aiden" header on
960
1080
  * the first call of a turn, then writes raw text directly via the
@@ -987,51 +1107,59 @@ class Display {
987
1107
  this.streamLineCount += 1;
988
1108
  }
989
1109
  /**
990
- * Mark the end of a streaming turn. Adds a trailing newline if the
991
- * stream didn't end with one so the next CLI line doesn't visually
992
- * butt up against the model's last token. Resets the per-turn state
993
- * so the next `streamPartial` re-emits the header.
1110
+ * v4.1.3-essentials: rerender a buffered stream chunk in place. Walks
1111
+ * the cursor back `lines` rows, erases to end-of-screen, and reprints
1112
+ * the chunk as skin-aware markdown.
1113
+ *
1114
+ * Pure side-effect; returns nothing. Used by:
1115
+ * - `commitStreamChunk()` — when a tool row interrupts the
1116
+ * stream, render the pre-tool
1117
+ * chunk before the row writes.
1118
+ * - `streamComplete()` — final chunk at end of turn.
1119
+ *
1120
+ * Heuristic gate avoids flicker on plain prose (no structure → no
1121
+ * rerender, no eraser fires). The catch block writes the RAW buffered
1122
+ * text as a fallback if `marked` throws — without this the eraser
1123
+ * would already have run and the body would silently vanish.
1124
+ * v4.1.3-essentials raw-text fallback per "make state legible" thesis.
994
1125
  */
995
- streamComplete() {
996
- if (!this.streamHeaderShown)
997
- return;
998
- if (!this.streamLastEndedNewline)
999
- this.out.write('\n');
1000
- // Phase v4.1-reply-formatting: re-render the buffered stream as
1001
- // structured markdown — but ONLY when stdout is a TTY and the
1002
- // buffer actually contains markdown structure worth rendering.
1003
- // Plain prose with no headers / lists / fences gets left alone
1004
- // (no flicker, identical output). Otherwise we erase the raw
1005
- // streamed body via cursor-up + erase-line and reprint via the
1006
- // skin-aware renderer.
1007
- const buffered = this.streamBuffer;
1008
- const lines = this.streamLineCount;
1009
- this.streamBuffer = '';
1010
- this.streamLineCount = 0;
1011
- this.streamHeaderShown = false;
1012
- this.streamLastEndedNewline = false;
1126
+ tryRerenderInPlace(buffered, lines) {
1013
1127
  if (!this.out.isTTY)
1014
1128
  return;
1015
1129
  if (process.env.AIDEN_NO_REFORMAT === '1')
1016
1130
  return;
1017
- // Cheap heuristic: only re-render when there's structure that
1018
- // benefits from formatting. Avoids flicker on short prose replies.
1131
+ if (lines === 0)
1132
+ return;
1133
+ // Cheap structural heuristic — only re-render when formatting
1134
+ // actually helps. Plain prose chunks stay raw (no flicker).
1135
+ //
1136
+ // v4.1.3-essentials post-ship: inline `**bold**` and `` `code` ``
1137
+ // added to the heuristic. Before this, a chunk that contained ONLY
1138
+ // inline markdown (no headings / lists / code blocks) skipped
1139
+ // rerender entirely, leaving the literal `**bold**` asterisks in
1140
+ // user-visible output. The `paintBoldWhite` strong renderer was
1141
+ // never invoked for those chunks.
1142
+ //
1143
+ // Patterns:
1144
+ // - `**bold**`: requires non-space immediately after the opening
1145
+ // `**` so `2 ** 3` math expressions don't false-positive.
1146
+ // Tolerates multi-line bold via `[\s\S]*?`.
1147
+ // - `` `code` ``: negative-lookarounds for the triple-backtick
1148
+ // fence so we don't double-trigger when ``` lines are present
1149
+ // (those already match the fence pattern above).
1019
1150
  const hasStructure = /^#{1,6}\s/m.test(buffered) ||
1020
1151
  /^\s*[-*+]\s/m.test(buffered) ||
1021
1152
  /^\s*\d+\.\s/m.test(buffered) ||
1022
1153
  /^>\s/m.test(buffered) ||
1023
- /```/.test(buffered);
1154
+ /```/.test(buffered) ||
1155
+ /\*\*\S[\s\S]*?\*\*/.test(buffered) ||
1156
+ /(?<![`])`[^`\n]+`(?![`])/.test(buffered);
1024
1157
  if (!hasStructure)
1025
1158
  return;
1026
1159
  try {
1027
- // Erase the raw streamed body in place. We wrote `lines + 1`
1028
- // rows (header + body) the header (`┃ Aiden`) stays, so we
1029
- // walk back `lines` rows and clear each.
1030
- // `\x1b[<n>F` = cursor-up-and-to-column-0 N times.
1031
- // `\x1b[J` = erase from cursor to end of screen.
1032
- if (lines > 0) {
1033
- this.out.write(`\x1b[${lines}F\x1b[J`);
1034
- }
1160
+ // \x1b[<n>F = cursor-up-and-to-column-0 N times.
1161
+ // \x1b[J = erase from cursor to end of screen.
1162
+ this.out.write(`\x1b[${lines}F\x1b[J`);
1035
1163
  const formatted = this.markdown(buffered).trimEnd();
1036
1164
  const indented = formatted
1037
1165
  .split('\n')
@@ -1040,11 +1168,127 @@ class Display {
1040
1168
  this.out.write(indented + '\n');
1041
1169
  }
1042
1170
  catch {
1043
- // If anything goes wrong with the re-render, leave the raw
1044
- // streamed text in place graceful degradation beats flicker
1045
- // + corrupted output.
1171
+ // Eraser already ran. v4.1.3-essentials: write the raw buffered
1172
+ // text back so the body doesn't vanish silently. The user sees
1173
+ // unformatted markdown rather than a missing reply — the honest
1174
+ // failure mode.
1175
+ this.out.write(buffered);
1176
+ if (!buffered.endsWith('\n'))
1177
+ this.out.write('\n');
1046
1178
  }
1047
1179
  }
1180
+ /**
1181
+ * v4.1.3-essentials: fence off the current stream chunk before a
1182
+ * non-stream write (tool row, tool indicator, capability card) lands.
1183
+ *
1184
+ * Replaces the v4.1.3-repl-polish `streamInterrupted` flag pattern.
1185
+ * Old pattern: set flag mid-stream → on streamComplete, check flag
1186
+ * and SKIP rerender entirely (lost markdown on every tool-using
1187
+ * turn). New pattern: at each interrupt point, eagerly rerender THIS
1188
+ * chunk in place, then reset the per-chunk window so the next
1189
+ * streamPartial starts a fresh count. The cursor is at the end of
1190
+ * this chunk when commit fires, so `streamLineCount` is correct for
1191
+ * the eraser — tool rows write below without being clobbered.
1192
+ *
1193
+ * Multi-chunk turns (model says X, calls tool, says Y, calls tool,
1194
+ * says Z) get all three chunks rerendered as markdown.
1195
+ *
1196
+ * Idempotent: no-op when no stream cycle is active or when the buffer
1197
+ * is empty (consecutive tool calls). Always ensures the cursor sits
1198
+ * at start-of-line before returning so the caller can write its own
1199
+ * row cleanly.
1200
+ */
1201
+ commitStreamChunk() {
1202
+ if (!this.streamHeaderShown)
1203
+ return;
1204
+ // Ensure the streamed chunk ends with a newline so the interrupt
1205
+ // row doesn't stick to mid-token text from the prior delta.
1206
+ if (!this.streamLastEndedNewline) {
1207
+ this.out.write('\n');
1208
+ this.streamLastEndedNewline = true;
1209
+ // The trailing newline we just wrote DOES bump the cursor's row,
1210
+ // but only by 1 — and `streamLineCount` should reflect physical
1211
+ // rows of the chunk. Add it so the eraser walks back the right
1212
+ // amount.
1213
+ this.streamLineCount += 1;
1214
+ }
1215
+ // v4.1.3-essentials boldwrap-fix: if the chunk ends mid-bold-pair
1216
+ // (e.g. tool fired between the model emitting `**` and the closing
1217
+ // `**`), splitting here would leave literal asterisks in the
1218
+ // rerendered output and a matching orphan in the next chunk.
1219
+ // `splitAtUnclosedBold` finds the last unmatched `**` and carves
1220
+ // the buffer into two parts: the closed-bold prefix we CAN
1221
+ // rerender now, and the carry tail that we keep for the next
1222
+ // chunk (where the closing `**` will eventually arrive).
1223
+ //
1224
+ // Code-fence safety (carried in the helper): if the would-be
1225
+ // unmatched `**` is inside an open ``` fence, we defer the whole
1226
+ // chunk — bold-syntax inside code blocks isn't markdown bold and
1227
+ // splitting there would corrupt the fence.
1228
+ const split = splitAtUnclosedBold(this.streamBuffer);
1229
+ if (split.carry === '') {
1230
+ // Common case: buffer is balanced (or has no `**` at all).
1231
+ // Same behavior as before — rerender the whole chunk in place
1232
+ // and reset the per-chunk window.
1233
+ this.tryRerenderInPlace(this.streamBuffer, this.streamLineCount);
1234
+ this.streamBuffer = '';
1235
+ this.streamLineCount = 0;
1236
+ return;
1237
+ }
1238
+ // Split path: erase the WHOLE chunk (because the cursor is at the
1239
+ // end of the full buffer), rerender the closed prefix, then
1240
+ // re-emit the carry as raw text. The carry visibly stays on
1241
+ // screen as raw `**Live tool indi`-style text — ugly for the
1242
+ // ~milliseconds until the next streamPartial extends it past the
1243
+ // closing `**`, at which point the next commit will rerender
1244
+ // cleanly.
1245
+ const rerenderableLines = countNewlines(split.rerenderable);
1246
+ const carryLines = this.streamLineCount - rerenderableLines;
1247
+ if (this.out.isTTY && this.streamLineCount > 0) {
1248
+ // \x1b[<n>F = cursor-up-and-to-column-0 N times.
1249
+ // \x1b[J = erase from cursor to end of screen.
1250
+ this.out.write(`\x1b[${this.streamLineCount}F\x1b[J`);
1251
+ }
1252
+ // Rerender the closed prefix (handles its own heuristic gate
1253
+ // internally — a prefix without structure stays raw, which is
1254
+ // identical to the pre-split behavior).
1255
+ this.tryRerenderInPlace(split.rerenderable, rerenderableLines);
1256
+ // Re-emit the carry verbatim. It's intentionally raw because the
1257
+ // unmatched `**` can't be rendered without its closing pair.
1258
+ this.out.write(split.carry);
1259
+ // Reset the per-chunk window to the carry only. Next streamPartial
1260
+ // extends it; when the closing `**` lands, the next commit (or
1261
+ // streamComplete) rerenders cleanly.
1262
+ this.streamBuffer = split.carry;
1263
+ this.streamLineCount = carryLines;
1264
+ this.streamLastEndedNewline = split.carry.endsWith('\n');
1265
+ }
1266
+ /**
1267
+ * Mark the end of a streaming turn. Adds a trailing newline if the
1268
+ * stream didn't end with one so the next CLI line doesn't visually
1269
+ * butt up against the model's last token. Rerenders the FINAL chunk
1270
+ * (post-last-tool prose, or the whole body if no tools fired this
1271
+ * turn) and resets the per-turn state so the next `streamPartial`
1272
+ * re-emits the header.
1273
+ */
1274
+ streamComplete() {
1275
+ if (!this.streamHeaderShown)
1276
+ return;
1277
+ if (!this.streamLastEndedNewline) {
1278
+ this.out.write('\n');
1279
+ this.streamLineCount += 1;
1280
+ }
1281
+ // Final chunk: same in-place rerender path as commitStreamChunk
1282
+ // (factored shared helper). Tool-row interrupts have already
1283
+ // committed their preceding chunks; what's left in the buffer here
1284
+ // is the post-final-tool prose — typically the bulk of the
1285
+ // user-visible body in well-behaved turns.
1286
+ this.tryRerenderInPlace(this.streamBuffer, this.streamLineCount);
1287
+ this.streamBuffer = '';
1288
+ this.streamLineCount = 0;
1289
+ this.streamHeaderShown = false;
1290
+ this.streamLastEndedNewline = false;
1291
+ }
1048
1292
  /**
1049
1293
  * Phase v4.1-reply-formatting: render the optional "Sources"
1050
1294
  * footer when AIDEN_CITATIONS=1 and the trace has fetch-class
@@ -1067,10 +1311,15 @@ class Display {
1067
1311
  * newline if the prior delta ran past column N without one.
1068
1312
  */
1069
1313
  streamToolIndicator(name) {
1314
+ // v4.1.3-essentials (replaces v4.1.3-repl-polish streamInterrupted
1315
+ // flag pattern): fence off the streamed chunk before the indicator
1316
+ // writes. `commitStreamChunk` handles the newline-or-not and
1317
+ // in-place rerenders the pre-indicator chunk so this row lands
1318
+ // below well-formed markdown rather than below raw streamed text.
1319
+ this.commitStreamChunk();
1070
1320
  const sk = this.skin;
1071
1321
  const arrow = sk.getActive().glyphs?.arrow ?? '>';
1072
- const prefix = this.streamLastEndedNewline ? '' : '\n';
1073
- this.out.write(`${prefix}${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
1322
+ this.out.write(`${sk.applyColors(`${arrow} ${name}…`, 'tool')}\n`);
1074
1323
  this.streamLastEndedNewline = true;
1075
1324
  }
1076
1325
  }
@@ -1126,11 +1375,29 @@ function renderRmsBar(rms) {
1126
1375
  const filled = Math.round((safe / BAR_FULL_RMS) * BAR_WIDTH);
1127
1376
  return '▌'.repeat(filled) + ' '.repeat(BAR_WIDTH - filled);
1128
1377
  }
1129
- // ── Phase 23.5 — tool row helpers ─────────────────────────────────────
1130
- /** Width the tool name is padded to so brackets line up across rows. */
1131
- const TOOL_ROW_NAME_PAD = 16;
1132
- /** Args preview cap. Args longer than this get truncated with "…". */
1133
- const TOOL_ROW_ARG_CAP = 40;
1378
+ // ── Phase 23.5 / v4.1.3-repl-polish — tool row helpers ────────────────
1379
+ //
1380
+ // v4.1.3 changes the row format from:
1381
+ // " {glyph} tool {name:16} {args} [{state}]"
1382
+ // to the compact trail format:
1383
+ // "┊ {icon} {verb:12} {detail:40}"
1384
+ //
1385
+ // Outcome semantics:
1386
+ // ok() → SILENT — running row is erased; nothing persists.
1387
+ // fail() → row persists in error (red).
1388
+ // degraded() → row persists in degraded (yellow). NEW in v4.1.3.
1389
+ // blocked() → row persists in warn.
1390
+ // retry() → running row is updated with retry counter.
1391
+ // emptyFail() → treated as fail (error colour).
1392
+ // emptyRetry()→ treated as retry.
1393
+ /**
1394
+ * Kept for reference / smoke-test backward compat. New code should use
1395
+ * TRAIL_VERB_PAD / TRAIL_DETAIL_CAP from toolTrail.ts instead.
1396
+ * @deprecated
1397
+ */
1398
+ exports.TOOL_ROW_NAME_PAD = toolTrail_1.TRAIL_VERB_PAD;
1399
+ /** @deprecated Use TRAIL_DETAIL_CAP. */
1400
+ exports.TOOL_ROW_ARG_CAP = toolTrail_1.TRAIL_DETAIL_CAP;
1134
1401
  /**
1135
1402
  * Build a compact, single-line preview of the tool's arguments. Picks
1136
1403
  * the most informative scalar fields when the args are an object, then
@@ -1166,13 +1433,96 @@ function previewToolArgs(args) {
1166
1433
  catch {
1167
1434
  serialized = String(obj);
1168
1435
  }
1436
+ // v4.1.4-media: an empty object serializes to '{}'. Rendering that
1437
+ // literal in the trail row is honest but ugly and reads as "buggy
1438
+ // empty args". When the model legitimately passes an empty args
1439
+ // object (e.g. `media_sessions({})`, `system_info()`), show nothing
1440
+ // rather than the braces — `buildToolPreview` already does this for
1441
+ // tools mapped in `TOOL_PRIMARY_ARG`; here we extend the same UX to
1442
+ // any unmapped-tool fallback that bottoms out at `{}`.
1443
+ if (serialized === '{}')
1444
+ return '';
1169
1445
  return truncToolArg(serialized);
1170
1446
  }
1171
1447
  function truncToolArg(s) {
1172
1448
  const flat = s.replace(/\s+/g, ' ').trim();
1173
- if (flat.length <= TOOL_ROW_ARG_CAP)
1449
+ if (flat.length <= exports.TOOL_ROW_ARG_CAP)
1174
1450
  return flat;
1175
- return flat.slice(0, TOOL_ROW_ARG_CAP - 1) + '…';
1451
+ return flat.slice(0, exports.TOOL_ROW_ARG_CAP - 1) + '…';
1452
+ }
1453
+ /**
1454
+ * v4.1.3-essentials boldwrap-fix: count `\n` occurrences in `s`.
1455
+ * Used by `commitStreamChunk` to recompute `streamLineCount` after
1456
+ * splitting a buffer at an unclosed-bold boundary. Pure helper —
1457
+ * exported for unit-test access.
1458
+ */
1459
+ function countNewlines(s) {
1460
+ let n = 0;
1461
+ for (let i = 0; i < s.length; i += 1)
1462
+ if (s[i] === '\n')
1463
+ n += 1;
1464
+ return n;
1465
+ }
1466
+ /**
1467
+ * v4.1.3-essentials boldwrap-fix: split a streamed-chunk buffer at the
1468
+ * last unmatched `**` so the closed-bold prefix can be rerendered now
1469
+ * and the open-bold tail can be carried into the next chunk.
1470
+ *
1471
+ * Returns `{ rerenderable, carry }`:
1472
+ * - rerenderable: the prefix with all `**` pairs balanced
1473
+ * - carry: the suffix starting at the last unmatched `**`
1474
+ *
1475
+ * `carry === ''` signals "balanced — render the whole buffer". Caller
1476
+ * uses this as the fast-path discriminator.
1477
+ *
1478
+ * Code-fence safety: if the buffer contains an UNCLOSED fenced code
1479
+ * block (` ``` ` count is odd), defer the entire chunk by returning
1480
+ * `{ rerenderable: '', carry: buffer }`. Bold-syntax inside code
1481
+ * blocks is literal text — splitting there would corrupt the fence
1482
+ * AND likely produce nonsensical rerender output. Trade-off: a chunk
1483
+ * that ends mid-code-block doesn't rerender at all until the closing
1484
+ * ``` arrives; acceptable because code blocks have their own
1485
+ * styling (dark bg + left rail) that doesn't depend on the markdown
1486
+ * rerender step.
1487
+ *
1488
+ * Pure function. Tested via `tests/v4/cli/display.test.ts`.
1489
+ */
1490
+ function splitAtUnclosedBold(buffer) {
1491
+ // Fast path: no `**` at all → balanced.
1492
+ if (!buffer.includes('**'))
1493
+ return { rerenderable: buffer, carry: '' };
1494
+ // Code-fence safety: count triple-backtick fences. Odd = open fence,
1495
+ // defer the whole buffer.
1496
+ const fenceMatches = buffer.match(/```/g);
1497
+ if (fenceMatches && fenceMatches.length % 2 === 1) {
1498
+ return { rerenderable: '', carry: buffer };
1499
+ }
1500
+ // Count `**` occurrences. Even → balanced. Odd → there's an
1501
+ // unmatched `**` — find the LAST one (the open).
1502
+ const positions = [];
1503
+ for (let i = 0; i < buffer.length - 1; i += 1) {
1504
+ if (buffer[i] === '*' && buffer[i + 1] === '*') {
1505
+ positions.push(i);
1506
+ i += 1; // skip the second `*` so `***` doesn't double-count
1507
+ }
1508
+ }
1509
+ if (positions.length % 2 === 0) {
1510
+ return { rerenderable: buffer, carry: '' };
1511
+ }
1512
+ const lastUnmatched = positions[positions.length - 1];
1513
+ // Inline-backtick safety: if the unmatched `**` sits inside an
1514
+ // open single-backtick span on the same line, the `**` is literal
1515
+ // code, not a bold marker. Defer the whole chunk.
1516
+ const lineStart = buffer.lastIndexOf('\n', lastUnmatched) + 1;
1517
+ const lineUpToBold = buffer.slice(lineStart, lastUnmatched);
1518
+ const backticksOnLine = (lineUpToBold.match(/`/g) ?? []).length;
1519
+ if (backticksOnLine % 2 === 1) {
1520
+ return { rerenderable: '', carry: buffer };
1521
+ }
1522
+ return {
1523
+ rerenderable: buffer.slice(0, lastUnmatched),
1524
+ carry: buffer.slice(lastUnmatched),
1525
+ };
1176
1526
  }
1177
1527
  /**
1178
1528
  * Render a tool-call duration in the bracket cluster. Sub-second