aiden-runtime 4.0.2 → 4.1.0

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 (108) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +421 -5
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +256 -55
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +41 -0
  28. package/dist/cli/v4/envSources.js +105 -0
  29. package/dist/cli/v4/ghostMatch.js +74 -0
  30. package/dist/cli/v4/historyStore.js +163 -0
  31. package/dist/cli/v4/pasteCompression.js +124 -0
  32. package/dist/cli/v4/pasteIntercept.js +203 -0
  33. package/dist/cli/v4/replyRenderer.js +209 -0
  34. package/dist/cli/v4/resizeGuard.js +92 -0
  35. package/dist/cli/v4/shellInterpolation.js +139 -0
  36. package/dist/cli/v4/skinEngine.js +21 -1
  37. package/dist/cli/v4/streamingPrefix.js +121 -0
  38. package/dist/cli/v4/syntaxHighlight.js +345 -0
  39. package/dist/cli/v4/table.js +216 -0
  40. package/dist/cli/v4/themeDetect.js +81 -0
  41. package/dist/cli/v4/uiBuild.js +74 -0
  42. package/dist/cli/v4/voiceCli.js +113 -0
  43. package/dist/cli/v4/voicePromptApi.js +196 -0
  44. package/dist/core/channels/discord.js +16 -10
  45. package/dist/core/channels/email.js +13 -9
  46. package/dist/core/channels/imessage.js +13 -9
  47. package/dist/core/channels/manager.js +25 -7
  48. package/dist/core/channels/pdf-extract.js +180 -0
  49. package/dist/core/channels/photo-vision.js +157 -0
  50. package/dist/core/channels/signal.js +11 -7
  51. package/dist/core/channels/slack.js +13 -10
  52. package/dist/core/channels/telegram-commands.js +154 -0
  53. package/dist/core/channels/telegram-groups.js +198 -0
  54. package/dist/core/channels/telegram-rate-limit.js +124 -0
  55. package/dist/core/channels/telegram.js +1980 -0
  56. package/dist/core/channels/twilio.js +11 -7
  57. package/dist/core/channels/webhook.js +9 -5
  58. package/dist/core/channels/whatsapp.js +15 -11
  59. package/dist/core/channels/whisper-transcribe.js +163 -0
  60. package/dist/core/cronManager.js +33 -294
  61. package/dist/core/gateway.js +29 -8
  62. package/dist/core/playwrightBridge.js +90 -0
  63. package/dist/core/v4/aidenAgent.js +35 -0
  64. package/dist/core/v4/auxiliaryClient.js +2 -2
  65. package/dist/core/v4/cron/atomicWrite.js +18 -4
  66. package/dist/core/v4/cron/cronExecute.js +300 -0
  67. package/dist/core/v4/cron/cronManager.js +502 -0
  68. package/dist/core/v4/cron/cronState.js +314 -0
  69. package/dist/core/v4/cron/cronTick.js +90 -0
  70. package/dist/core/v4/cron/diagnostics.js +104 -0
  71. package/dist/core/v4/cron/graceWindow.js +79 -0
  72. package/dist/core/v4/logger/factory.js +110 -0
  73. package/dist/core/v4/logger/index.js +22 -0
  74. package/dist/core/v4/logger/logger.js +101 -0
  75. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  76. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  77. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  78. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  79. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  80. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  81. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  82. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  83. package/dist/core/v4/platformPaths.js +105 -0
  84. package/dist/core/v4/providerFallback.js +25 -0
  85. package/dist/core/v4/skillLoader.js +21 -5
  86. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  87. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  88. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  89. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  90. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  91. package/dist/core/v4/subagent/budget.js +76 -0
  92. package/dist/core/v4/subagent/diagnostics.js +22 -0
  93. package/dist/core/v4/subagent/fanout.js +216 -0
  94. package/dist/core/v4/subagent/merger.js +148 -0
  95. package/dist/core/v4/subagent/providerRotation.js +54 -0
  96. package/dist/core/v4/voice/audioStream.js +373 -0
  97. package/dist/core/v4/voice/cliVoice.js +393 -0
  98. package/dist/core/v4/voice/diagnostics.js +66 -0
  99. package/dist/core/v4/voice/ttsStream.js +193 -0
  100. package/dist/core/version.js +1 -1
  101. package/dist/core/visionAnalyze.js +291 -90
  102. package/dist/core/voice/audio.js +61 -5
  103. package/dist/core/voice/audioBackend.js +134 -0
  104. package/dist/core/voice/stt.js +61 -6
  105. package/dist/core/voice/tts.js +19 -3
  106. package/dist/tools/v4/index.js +32 -1
  107. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  108. package/package.json +11 -2
@@ -19,8 +19,12 @@
19
19
  * 5. Re-renders the status line after every turn.
20
20
  *
21
21
  */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
22
25
  Object.defineProperty(exports, "__esModule", { value: true });
23
26
  exports.BOOT_TRY_HINT = exports.ChatSession = void 0;
27
+ exports.renderCommandLabel = renderCommandLabel;
24
28
  exports.detectOS = detectOS;
25
29
  exports.detectShell = detectShell;
26
30
  exports.formatStatusState = formatStatusState;
@@ -30,8 +34,38 @@ exports.renderProgressBar = renderProgressBar;
30
34
  exports.formatTokens = formatTokens;
31
35
  exports.formatDuration = formatDuration;
32
36
  exports.renderMemoryConfirmations = renderMemoryConfirmations;
37
+ const display_1 = require("./display");
38
+ const uiBuild_1 = require("./uiBuild");
39
+ const aidenPrompt_1 = __importDefault(require("./aidenPrompt"));
40
+ const historyStore_1 = require("./historyStore");
33
41
  const modelMetadata_1 = require("../../core/v4/modelMetadata");
34
42
  const bracketedPaste_1 = require("./bracketedPaste");
43
+ const pasteCompression_1 = require("./pasteCompression");
44
+ const pasteIntercept_1 = require("./pasteIntercept");
45
+ const shellInterpolation_1 = require("./shellInterpolation");
46
+ const resizeGuard_1 = require("./resizeGuard");
47
+ /**
48
+ * Tier-3.1 helper: render a slash-command label honouring the
49
+ * `AIDEN_UI_ICONS` opt-in. Default OFF — emoji icons are gated to
50
+ * keep the dropdown ASCII-clean for terminals without good emoji
51
+ * support. `AIDEN_UI_ICONS=1` recovers the previous icon column.
52
+ */
53
+ function renderCommandLabel(cmd) {
54
+ return cmd.icon && (0, uiBuild_1.uiIconsEnabled)()
55
+ ? `${cmd.icon} /${cmd.name}`
56
+ : `/${cmd.name}`;
57
+ }
58
+ /** Aiden version pulled from package.json at require-time; falls back
59
+ * to a static literal so TS compiles without a JSON resolution wobble. */
60
+ const AIDEN_VERSION = (() => {
61
+ try {
62
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
63
+ return require('../../package.json').version ?? '4.0.0';
64
+ }
65
+ catch {
66
+ return '4.0.0';
67
+ }
68
+ })();
35
69
  const STATUS_BAR_WIDTH = 10;
36
70
  class ChatSession {
37
71
  constructor(opts) {
@@ -129,7 +163,15 @@ class ChatSession {
129
163
  process.on('SIGINT', sigintHandler);
130
164
  }
131
165
  // 4. Main loop.
132
- const promptApi = this.opts.promptApi ?? createDefaultPromptApi();
166
+ // Tier-3.1.1: feed the new aidenPrompt with live slash commands +
167
+ // recent history so ghost-text + dropdown work out of the box.
168
+ // The legacy inquirer path runs when `--no-ui` (AIDEN_NO_UI=1) is
169
+ // set or when a caller injects its own `promptApi`.
170
+ const promptApi = this.opts.promptApi ??
171
+ createDefaultPromptApi({
172
+ commands: this.opts.commandRegistry.list(),
173
+ loadHistory: () => (0, historyStore_1.loadRecent)(500),
174
+ });
133
175
  const max = this.opts.maxIterations ?? Number.POSITIVE_INFINITY;
134
176
  let iter = 0;
135
177
  // Phase 16: enable bracketed paste for the duration of the REPL when
@@ -139,6 +181,25 @@ class ChatSession {
139
181
  const pasteEnabled = stdout?.isTTY && !this.opts.promptApi
140
182
  ? (0, bracketedPaste_1.enableBracketedPaste)(stdout)
141
183
  : false;
184
+ // Tier-3.1a: install stdin pre-tap so bracketed paste payloads are
185
+ // captured and replaced with `[paste #N: …]` labels BEFORE inquirer
186
+ // sees them. Without this, modern @inquirer/prompts treats internal
187
+ // `\n` as Enter and auto-submits the first line of a multi-line paste.
188
+ // Tier-3.1c: install regardless of TTY status. Bracketed-paste
189
+ // sequences can arrive on a piped stdin too (CI harnesses, the
190
+ // runtime smoke), and the interceptor's wrap is a no-op on
191
+ // non-paste data — there's no cost to installing always. The
192
+ // promptApi opt-out remains so callers that supply their own
193
+ // input plumbing aren't surprised.
194
+ const restorePasteInterceptor = this.opts.promptApi
195
+ ? () => { }
196
+ : (0, pasteIntercept_1.installPasteInterceptor)(process.stdin);
197
+ // Tier-3-essentials: hard-clear the screen on terminal resize so
198
+ // dropdown re-renders + previous prompt frames don't ghost into
199
+ // the new viewport. No-op on non-TTY / MCP serve mode.
200
+ const restoreResizeGuard = this.opts.promptApi
201
+ ? () => { }
202
+ : (0, resizeGuard_1.installResizeGuard)();
142
203
  try {
143
204
  while (iter < max) {
144
205
  iter += 1;
@@ -178,6 +239,7 @@ class ChatSession {
178
239
  personalityManager: this.opts.personalityManager,
179
240
  agent: this.opts.agent,
180
241
  pluginLoader: this.opts.pluginLoader,
242
+ channelManager: this.opts.channelManager,
181
243
  confirm: async (msg) => {
182
244
  // Phase 17.1: bug — was reading `this.opts.promptApi?` which is
183
245
  // undefined when no override is passed; the chain silently
@@ -208,6 +270,8 @@ class ChatSession {
208
270
  process.off('SIGINT', sigintHandler);
209
271
  if (pasteEnabled)
210
272
  (0, bracketedPaste_1.disableBracketedPaste)(stdout);
273
+ restorePasteInterceptor();
274
+ restoreResizeGuard();
211
275
  }
212
276
  }
213
277
  // ── Inner: a single agent turn ─────────────────────────────────────
@@ -227,6 +291,9 @@ class ChatSession {
227
291
  // Phase 22 Task 4: status bar reflects the live phase. Set on
228
292
  // entry, cleared in both success and error paths below.
229
293
  this.setStatusState({ kind: 'generating', sinceMs: Date.now() });
294
+ // Tier-3.1a: dim full-width rule between the user input echo and
295
+ // the agent reply for clean visual rhythm.
296
+ this.opts.display.write(` ${this.opts.display.rule()}\n`);
230
297
  // Phase 26.2.3 — blank line between the user-input echo and the
231
298
  // spinner / response so the eye sees user → agent as separate
232
299
  // beats instead of butting together.
@@ -313,6 +380,9 @@ class ChatSession {
313
380
  }
314
381
  this.setStatusState({ kind: 'ready' });
315
382
  this.lastTurnElapsedMs = Date.now() - turnStartedAt;
383
+ // Tier-3.1a: dim full-width rule between the agent reply and the
384
+ // post-turn status footer.
385
+ this.opts.display.write(` ${this.opts.display.rule()}\n`);
316
386
  this.renderStatusLine();
317
387
  }
318
388
  catch (err) {
@@ -351,23 +421,47 @@ class ChatSession {
351
421
  // collapses to plain 4-line credits below 75 cols.
352
422
  async renderStartupCard() {
353
423
  const display = this.opts.display;
424
+ // Tier-3.1a: skip entirely on non-TTY so piped/scripted callers
425
+ // don't get scrollback chatter on stdout.
426
+ if (!process.stdout.isTTY)
427
+ return;
428
+ // Channel summary — observable, not banner-essential, but kept so
429
+ // status pills aren't the only place a user sees telegram health.
430
+ const cm = this.opts.channelManager;
431
+ if (cm) {
432
+ const adapterStatuses = cm.getStatus().map((s) => {
433
+ const adapter = cm.get(s.name);
434
+ const tg = adapter;
435
+ const botHandle = typeof tg?.getBotUsername === 'function' ? tg.getBotUsername() : null;
436
+ const state = typeof tg?.getState === 'function' ? tg.getState() : undefined;
437
+ return { id: s.name, healthy: s.healthy, botHandle, state };
438
+ });
439
+ void (0, display_1.summarizeChannelState)({ adapters: adapterStatuses });
440
+ }
441
+ else {
442
+ void (0, display_1.summarizeChannelState)(null);
443
+ }
444
+ const cols = display.cols();
445
+ const isNarrow = cols < 60;
446
+ const showEnvCapBlock = cols >= 70;
447
+ const version = AIDEN_VERSION;
354
448
  display.write('\n');
355
- display.printBanner();
356
- display.write(` ${display.muted('Autonomous AI Engine')}\n`);
357
- display.write('\n');
358
- // Detection
359
- const toolsCount = this.opts.toolRegistry.list().length;
360
- let skillsLoaded = 0;
361
- try {
362
- skillsLoaded = (await this.opts.skillLoader.list()).length;
449
+ if (isNarrow) {
450
+ // Compact — single-line text logo + one-line capability summary.
451
+ display.write(` ${display.brand('AIDEN')} ${display.muted(`v${version}`)}\n`);
452
+ display.write(` ${display.muted('Local AI · controls your computer · never forgets')}\n`);
363
453
  }
364
- catch {
365
- skillsLoaded = 0;
454
+ else {
455
+ // Wide — full ASCII art + subtitle. Tier-3.1c: dropped the
456
+ // tagline + sponsor lines from the top section because they
457
+ // duplicate the credits already inside the scrollFooter at
458
+ // the bottom of the boot card. Subtitle stays — it's the only
459
+ // brand anchor between the ASCII art and the pills row.
460
+ display.printBanner(version);
461
+ display.write(` ${display.muted('Autonomous AI Engine')}\n`);
462
+ display.write('\n');
366
463
  }
367
- // PIECE 1 — status pills row.
368
- // Phase 30.2.1: in explore mode the model pill renders "not
369
- // configured" instead of the DEFAULT_CONFIG fallback, so a fresh
370
- // user who skipped the wizard isn't misled by a stale model name.
464
+ // Status pills.
371
465
  display.write(display.statusPillsRow({
372
466
  coreOnline: true,
373
467
  mode: 'auto',
@@ -375,37 +469,59 @@ class ChatSession {
375
469
  memoryActive: true,
376
470
  providerOk: !this.opts.unconfigured,
377
471
  }) + '\n');
472
+ // Tier-3.1b: rule + environment/capabilities block + rule + scroll
473
+ // + bottom prompt hint. Skipped at <70 cols to keep the narrow
474
+ // boot card from wrapping into noise.
378
475
  display.write(` ${display.rule()}\n`);
379
- display.write('\n');
380
- // PIECE 2 Environment + Capabilities block.
381
- display.write(display.twoColumnBlock({
382
- title: 'Environment',
383
- rows: [
384
- { key: 'OS', value: detectOS() },
385
- { key: 'shell', value: detectShell() },
386
- { key: 'runtime', value: 'local-first' },
387
- { key: 'tools', value: `${toolsCount} loaded` },
388
- { key: 'skills', value: `${skillsLoaded} loaded` },
389
- ],
390
- }, {
391
- title: 'Capabilities',
392
- rows: [
393
- { key: 'web', value: 'research · extract' },
394
- { key: 'browser', value: 'navigate · automate' },
395
- { key: 'files', value: 'read · patch · organize' },
396
- { key: 'execution', value: 'shell · code · workflows' },
397
- { key: 'memory', value: 'persistent recall' },
398
- ],
399
- }) + '\n');
400
- display.write('\n');
401
- display.write(` ${display.rule()}\n`);
402
- display.write('\n');
403
- // PIECE 3 — scroll footer with credits.
476
+ if (showEnvCapBlock) {
477
+ // Detect environment lazily (cheap on every boot — no caching
478
+ // needed; tools/skills counts are already loaded by this point).
479
+ const toolsCount = this.opts.toolRegistry.list().length;
480
+ let skillsLoaded = 0;
481
+ try {
482
+ skillsLoaded = (await this.opts.skillLoader.list()).length;
483
+ }
484
+ catch {
485
+ skillsLoaded = 0;
486
+ }
487
+ display.write('\n');
488
+ // Pass sideBySideThreshold=120 so 70-119 cols stack vertically
489
+ // (per the tier3.1b dispatch's width-tier policy) and only
490
+ // ≥120 renders the full side-by-side block.
491
+ display.write(display.twoColumnBlock({
492
+ title: 'Environment',
493
+ rows: [
494
+ { key: 'OS', value: detectOS() },
495
+ { key: 'shell', value: detectShell() },
496
+ { key: 'runtime', value: 'local-first' },
497
+ { key: 'tools', value: `${toolsCount} loaded` },
498
+ { key: 'skills', value: `${skillsLoaded} loaded` },
499
+ ],
500
+ }, {
501
+ title: 'Capabilities',
502
+ rows: [
503
+ { key: 'web', value: 'research · extract' },
504
+ { key: 'browser', value: 'navigate · automate' },
505
+ { key: 'files', value: 'read · patch · organize' },
506
+ { key: 'execution', value: 'shell · code · workflows' },
507
+ { key: 'memory', value: 'persistent recall' },
508
+ ],
509
+ },
510
+ // Tier-3.1c: lowered from 120 → 100 so wide-but-not-huge
511
+ // terminals (laptop screens, default Windows Terminal) get
512
+ // the side-by-side block instead of the stacked fallback.
513
+ // Each column at ~38 chars + 4-char separator + 2-char
514
+ // indent fits in 82 chars; 100 leaves 18 chars headroom.
515
+ { sideBySideThreshold: 100 }) + '\n');
516
+ display.write('\n');
517
+ display.write(` ${display.rule()}\n`);
518
+ display.write('\n');
519
+ }
520
+ // Scroll footer (parchment at ≥80 cols, single-line credits below).
404
521
  display.write(display.scrollFooter() + '\n');
522
+ // Bottom prompt hint — final line of the boot card.
405
523
  display.write('\n');
406
- // PIECE 4 — bottom prompt hint.
407
524
  display.write(display.bottomPromptHint() + '\n');
408
- display.write('\n');
409
525
  }
410
526
  /** Phase 22 Task 4: state transitions for the right-most segment. */
411
527
  setStatusState(state) {
@@ -451,14 +567,19 @@ class ChatSession {
451
567
  let raw = await api.readLine(promptText);
452
568
  if (raw == null)
453
569
  return '';
454
- // Bracketed paste polish (Phase 16): if the terminal sent paste markers,
455
- // strip them and accept the entire payload as one message. This replaces
456
- // Phase 15's timing heuristic when the terminal supports CSI 2004; the
457
- // timing fallback remains for older Console hosts that don't.
570
+ // Tier-3.1a: stdin pre-tap (pasteIntercept) already converted any
571
+ // bracketed-paste payload into a `[paste #N: …]` label before
572
+ // inquirer saw it. Swap the label back for the original here so
573
+ // the agent receives full content. User-typed labels with unknown
574
+ // ids are left untouched.
575
+ raw = (0, pasteIntercept_1.expandPasteLabels)(raw);
576
+ // Bracketed paste polish (Phase 16): if the terminal still sent
577
+ // paste markers (interceptor disabled — non-TTY or test promptApi),
578
+ // strip them and accept the entire payload as one message.
458
579
  if ((0, bracketedPaste_1.hasPasteMarkers)(raw)) {
459
580
  const stripped = (0, bracketedPaste_1.stripPasteMarkers)(raw).replace(/\r/g, '');
460
581
  if ((0, bracketedPaste_1.isCompletePaste)(raw))
461
- return stripped;
582
+ return await this.maybeCompressVisiblePaste(stripped);
462
583
  // Unterminated paste — still return the stripped content so the user
463
584
  // doesn't see escape sequences in their prompt.
464
585
  raw = stripped;
@@ -469,7 +590,7 @@ class ChatSession {
469
590
  const inline = raw.slice(3);
470
591
  // Single-line `"""hello"""` shortcut.
471
592
  if (inline.endsWith('"""')) {
472
- return inline.slice(0, -3);
593
+ return await this.maybeCompressVisiblePaste(inline.slice(0, -3));
473
594
  }
474
595
  const buffer = [inline];
475
596
  while (true) {
@@ -482,13 +603,22 @@ class ChatSession {
482
603
  }
483
604
  buffer.push(next);
484
605
  }
485
- return buffer.join('\n').trim();
606
+ return await this.maybeCompressVisiblePaste(buffer.join('\n').trim());
486
607
  }
487
- // Paste detection: multiple lines arrived in a single chunk. Accept verbatim.
608
+ // Paste detection: multiple lines arrived in a single chunk. The
609
+ // interceptor + expandPasteLabels path already produced the original
610
+ // text — no extra echo needed since the user saw the `[paste #N: …]`
611
+ // label in the input buffer. Pass the original through unchanged.
488
612
  if (raw.includes('\n'))
489
613
  return raw;
490
- // Slash command: invoke the autocomplete dropdown if registry has matches.
491
- if (raw.startsWith('/')) {
614
+ // Slash command: invoke the legacy autocomplete dropdown only
615
+ // when --no-ui (AIDEN_NO_UI=1) is set. The new aidenPrompt
616
+ // handles the dropdown inline as the user types, so re-opening
617
+ // a second prompt here would double-prompt — once in
618
+ // aidenPrompt, once in inq.search. Tier-3.1.1 routes everything
619
+ // through aidenPrompt unless the legacy path is explicitly
620
+ // requested.
621
+ if ((0, uiBuild_1.isNoUiMode)() && raw.startsWith('/')) {
492
622
  const matches = this.opts.commandRegistry.filter(raw);
493
623
  if (matches.length > 1) {
494
624
  try {
@@ -497,7 +627,7 @@ class ChatSession {
497
627
  return this.opts.commandRegistry
498
628
  .filter(filterStr)
499
629
  .map((cmd) => ({
500
- name: cmd.icon ? `${cmd.icon} /${cmd.name}` : `/${cmd.name}`,
630
+ name: renderCommandLabel(cmd),
501
631
  value: `/${cmd.name}`,
502
632
  description: cmd.description,
503
633
  }));
@@ -510,8 +640,48 @@ class ChatSession {
510
640
  }
511
641
  }
512
642
  }
643
+ // Tier-3-essentials: inline shell interpolation. If the prompt
644
+ // contains `{!cmd}` spans, run each in parallel (5s timeout per
645
+ // span, 500-char output cap) and splice the output back in. The
646
+ // rewritten prompt is what reaches the agent — visible feedback
647
+ // is a single dim line so the user sees that the work happened.
648
+ if ((0, shellInterpolation_1.hasInterpolation)(raw)) {
649
+ const spans = (0, shellInterpolation_1.countSpans)(raw);
650
+ this.opts.display.dim(`[shell] running ${spans} interpolation${spans === 1 ? '' : 's'}…`);
651
+ try {
652
+ raw = await (0, shellInterpolation_1.expand)(raw);
653
+ }
654
+ catch {
655
+ // expand() never rejects, but defence-in-depth.
656
+ }
657
+ }
513
658
  return raw;
514
659
  }
660
+ /**
661
+ * Tier-3.1: when a paste is large (>5 lines OR >500 chars), echo a
662
+ * compact `[paste #<id>: …]` label to the user and persist the
663
+ * original to disk so `/show <id>` can recall it later. The agent
664
+ * still receives the full original text — only the visible echo is
665
+ * compressed.
666
+ *
667
+ * MCP serve mode never reaches this path (REPL doesn't run there),
668
+ * so the display.write here is safe.
669
+ */
670
+ async maybeCompressVisiblePaste(text) {
671
+ try {
672
+ const result = await (0, pasteCompression_1.compressPaste)(text);
673
+ if (result.compressed && result.label) {
674
+ // Echo the label only — newline-terminated for cleanliness.
675
+ this.opts.display.write(` ${this.opts.display.muted(result.label)}\n`);
676
+ }
677
+ }
678
+ catch {
679
+ // Paste compression is a polish feature; if disk write fails we
680
+ // silently fall through to the original text rather than crash
681
+ // the prompt loop.
682
+ }
683
+ return text;
684
+ }
515
685
  }
516
686
  exports.ChatSession = ChatSession;
517
687
  // ── Helpers ──────────────────────────────────────────────────────────
@@ -670,7 +840,7 @@ function formatDuration(ms) {
670
840
  const remMin = min % 60;
671
841
  return remMin > 0 ? `${hr}h${remMin}m` : `${hr}h`;
672
842
  }
673
- function createDefaultPromptApi() {
843
+ function createDefaultPromptApi(opts = {}) {
674
844
  // Lazy-load @inquirer/prompts so test harnesses without a TTY don't break.
675
845
  // eslint-disable-next-line @typescript-eslint/no-var-requires
676
846
  const inq = require('@inquirer/prompts');
@@ -679,10 +849,37 @@ function createDefaultPromptApi() {
679
849
  // shows alone. We pass per-status entries so the answered echo also
680
850
  // stays clean; `loading` is intentionally untouched (spinner state).
681
851
  const promptTheme = { prefix: { idle: '', done: '' } };
852
+ // Tier-3.1.1: when `--no-ui` (AIDEN_NO_UI=1) is set, fall back to
853
+ // the legacy inquirer prompt path. Otherwise use the new
854
+ // aidenPrompt component (ghost text + slash dropdown + history nav).
855
+ const useLegacyPrompt = (0, uiBuild_1.isNoUiMode)() || !opts.commands;
682
856
  return {
683
857
  async readLine(prompt) {
684
858
  try {
685
- return (await inq.input({ message: prompt, theme: promptTheme })) ?? '';
859
+ if (useLegacyPrompt) {
860
+ return (await inq.input({ message: prompt, theme: promptTheme })) ?? '';
861
+ }
862
+ // Fetch history just-in-time so each read sees the latest
863
+ // (the user's previous turn was just appended).
864
+ const history = opts.loadHistory ? await opts.loadHistory() : [];
865
+ const value = await (0, aidenPrompt_1.default)({
866
+ message: prompt,
867
+ commands: opts.commands ?? [],
868
+ history,
869
+ theme: promptTheme,
870
+ });
871
+ const trimmed = (value ?? '').trim();
872
+ // Append to disk history. Awaited so the write flushes before
873
+ // the agent loop progresses — `/quit` exits the process and
874
+ // a fire-and-forget write would race the exit. The latency
875
+ // on a single appended line is negligible (~ms).
876
+ if (trimmed.length > 0) {
877
+ try {
878
+ await (0, historyStore_1.appendHistory)(trimmed);
879
+ }
880
+ catch { /* best-effort */ }
881
+ }
882
+ return value ?? '';
686
883
  }
687
884
  catch (err) {
688
885
  // Inquirer wraps Ctrl+C as ExitPromptError. Re-throw as plain Error
@@ -692,6 +889,10 @@ function createDefaultPromptApi() {
692
889
  }
693
890
  },
694
891
  async selectSlashCommand(source) {
892
+ // Tier-3.1.1: aidenPrompt handles the slash dropdown inline so
893
+ // this hook is rarely invoked. The legacy path stays available
894
+ // for `--no-ui` callers + any external promptApi shim that
895
+ // doesn't wrap aidenPrompt directly.
695
896
  try {
696
897
  return (await inq.search({ message: '/', source, theme: promptTheme }));
697
898
  }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod). Licensed under AGPL-3.0.
4
+ *
5
+ * Aiden — local-first agent.
6
+ */
7
+ /**
8
+ * cli/v4/citationFooter.ts — Phase v4.1-reply-formatting
9
+ *
10
+ * Optional post-reply "Sources" footer. Detects URLs in recent
11
+ * tool-call results (fetch_url, web_fetch, web_search, open_url,
12
+ * fetch_page) and renders a numbered list at the end of an agent
13
+ * turn. Default OFF — gated on `AIDEN_CITATIONS=1`.
14
+ *
15
+ * ──────
16
+ * Sources
17
+ * [1] forbes.com/sites/craigsmith/...
18
+ * [2] safe.ai/newsletter
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.extractSources = extractSources;
22
+ exports.buildCitationFooter = buildCitationFooter;
23
+ exports.renderCitationFooter = renderCitationFooter;
24
+ const skinEngine_1 = require("./skinEngine");
25
+ const SOURCE_TOOL_RE = /^(fetch_url|fetch_page|web_search|web_fetch|open_url|browser_get_url|browser_extract|deep_research)$/i;
26
+ const URL_RE = /\bhttps?:\/\/[^\s<>"'\\)\]]+/g;
27
+ const MAX_DISPLAY_LEN = 80;
28
+ /** Strip `https?://` and trailing slashes for compact display. */
29
+ function shortenUrl(url) {
30
+ let s = url.replace(/^https?:\/\//i, '').replace(/\/+$/, '');
31
+ if (s.length > MAX_DISPLAY_LEN)
32
+ s = s.slice(0, MAX_DISPLAY_LEN - 1) + '…';
33
+ return s;
34
+ }
35
+ /**
36
+ * Walk a trace, emit deduplicated URLs in first-seen order. Only
37
+ * traces whose tool name matches `SOURCE_TOOL_RE` contribute. Both
38
+ * args and result are scanned — args catch the `url:` arg, result
39
+ * catches URLs returned in extracted text.
40
+ */
41
+ function extractSources(trace) {
42
+ const seen = new Set();
43
+ const out = [];
44
+ for (const entry of trace) {
45
+ if (!SOURCE_TOOL_RE.test(entry.name))
46
+ continue;
47
+ const blob = JSON.stringify({ args: entry.args ?? null, result: entry.result ?? null });
48
+ const matches = blob.match(URL_RE);
49
+ if (!matches)
50
+ continue;
51
+ for (const url of matches) {
52
+ // Strip trailing punctuation that the regex sometimes catches.
53
+ const clean = url.replace(/[.,;:!?]+$/, '');
54
+ if (!seen.has(clean)) {
55
+ seen.add(clean);
56
+ out.push(clean);
57
+ }
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * Build the rendered footer string. Returns an empty string when
64
+ * there are no sources to surface (caller can skip the line entirely).
65
+ *
66
+ * The OSC8 wrapper makes URLs clickable in modern terminals; the
67
+ * visible label is the shortened form for compact display.
68
+ */
69
+ function buildCitationFooter(sources) {
70
+ if (sources.length === 0)
71
+ return '';
72
+ const sk = (0, skinEngine_1.getSkinEngine)();
73
+ const m = (s) => sk.applyColors(s, 'muted');
74
+ const lab = (s) => sk.applyColors(s, 'brand');
75
+ const val = (s) => sk.applyColors(s, 'accent');
76
+ const rule = m('──────');
77
+ const header = lab('Sources');
78
+ const lines = sources.map((url, i) => {
79
+ const idx = m(`[${i + 1}]`);
80
+ const display = shortenUrl(url);
81
+ // OSC8 hyperlink — the visible text is `display`, the link target
82
+ // is the full URL.
83
+ const linked = `\x1b]8;;${url}\x1b\\${val(display)}\x1b]8;;\x1b\\`;
84
+ return ` ${idx} ${linked}`;
85
+ });
86
+ return [rule, header, ...lines, ''].join('\n') + '\n';
87
+ }
88
+ /**
89
+ * Convenience: extract + build in one go. Returns '' when the env
90
+ * gate is off or no sources surface.
91
+ */
92
+ function renderCitationFooter(trace) {
93
+ if (process.env.AIDEN_CITATIONS !== '1')
94
+ return '';
95
+ const sources = extractSources(trace);
96
+ return buildCitationFooter(sources);
97
+ }