aiden-runtime 4.0.1 → 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 (112) 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 +513 -14
  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 +269 -52
  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 +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. 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,13 +270,30 @@ 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 ─────────────────────────────────────
214
278
  async runAgentTurn(userInput) {
279
+ // Phase 30.2.1 — explore mode: short-circuit BEFORE building the
280
+ // turn-status spinner / agent call. The wizard skipped, so there's
281
+ // no real provider to talk to. Print a friendly redirect to /setup
282
+ // (or the env-var alternative) and return — REPL stays alive, user
283
+ // can run slash commands or hit /quit.
284
+ if (this.opts.unconfigured) {
285
+ void userInput; // silence unused-arg warning when this branch fires
286
+ this.opts.display.write('\n');
287
+ this.opts.display.printError('No AI provider configured yet.', 'Run /setup to configure a provider, or set an API key environment variable (e.g. GROQ_API_KEY).');
288
+ this.opts.display.write('\n');
289
+ return;
290
+ }
215
291
  // Phase 22 Task 4: status bar reflects the live phase. Set on
216
292
  // entry, cleared in both success and error paths below.
217
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`);
218
297
  // Phase 26.2.3 — blank line between the user-input echo and the
219
298
  // spinner / response so the eye sees user → agent as separate
220
299
  // beats instead of butting together.
@@ -301,6 +380,9 @@ class ChatSession {
301
380
  }
302
381
  this.setStatusState({ kind: 'ready' });
303
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`);
304
386
  this.renderStatusLine();
305
387
  }
306
388
  catch (err) {
@@ -339,57 +421,107 @@ class ChatSession {
339
421
  // collapses to plain 4-line credits below 75 cols.
340
422
  async renderStartupCard() {
341
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;
342
448
  display.write('\n');
343
- display.printBanner();
344
- display.write(` ${display.muted('Autonomous AI Engine')}\n`);
345
- display.write('\n');
346
- // Detection
347
- const toolsCount = this.opts.toolRegistry.list().length;
348
- let skillsLoaded = 0;
349
- try {
350
- 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`);
351
453
  }
352
- catch {
353
- 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');
354
463
  }
355
- // PIECE 1 — status pills row.
464
+ // Status pills.
356
465
  display.write(display.statusPillsRow({
357
466
  coreOnline: true,
358
467
  mode: 'auto',
359
468
  model: this.currentModelId,
360
469
  memoryActive: true,
470
+ providerOk: !this.opts.unconfigured,
361
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.
362
475
  display.write(` ${display.rule()}\n`);
363
- display.write('\n');
364
- // PIECE 2 Environment + Capabilities block.
365
- display.write(display.twoColumnBlock({
366
- title: 'Environment',
367
- rows: [
368
- { key: 'OS', value: detectOS() },
369
- { key: 'shell', value: detectShell() },
370
- { key: 'runtime', value: 'local-first' },
371
- { key: 'tools', value: `${toolsCount} loaded` },
372
- { key: 'skills', value: `${skillsLoaded} loaded` },
373
- ],
374
- }, {
375
- title: 'Capabilities',
376
- rows: [
377
- { key: 'web', value: 'research · extract' },
378
- { key: 'browser', value: 'navigate · automate' },
379
- { key: 'files', value: 'read · patch · organize' },
380
- { key: 'execution', value: 'shell · code · workflows' },
381
- { key: 'memory', value: 'persistent recall' },
382
- ],
383
- }) + '\n');
384
- display.write('\n');
385
- display.write(` ${display.rule()}\n`);
386
- display.write('\n');
387
- // 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).
388
521
  display.write(display.scrollFooter() + '\n');
522
+ // Bottom prompt hint — final line of the boot card.
389
523
  display.write('\n');
390
- // PIECE 4 — bottom prompt hint.
391
524
  display.write(display.bottomPromptHint() + '\n');
392
- display.write('\n');
393
525
  }
394
526
  /** Phase 22 Task 4: state transitions for the right-most segment. */
395
527
  setStatusState(state) {
@@ -435,14 +567,19 @@ class ChatSession {
435
567
  let raw = await api.readLine(promptText);
436
568
  if (raw == null)
437
569
  return '';
438
- // Bracketed paste polish (Phase 16): if the terminal sent paste markers,
439
- // strip them and accept the entire payload as one message. This replaces
440
- // Phase 15's timing heuristic when the terminal supports CSI 2004; the
441
- // 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.
442
579
  if ((0, bracketedPaste_1.hasPasteMarkers)(raw)) {
443
580
  const stripped = (0, bracketedPaste_1.stripPasteMarkers)(raw).replace(/\r/g, '');
444
581
  if ((0, bracketedPaste_1.isCompletePaste)(raw))
445
- return stripped;
582
+ return await this.maybeCompressVisiblePaste(stripped);
446
583
  // Unterminated paste — still return the stripped content so the user
447
584
  // doesn't see escape sequences in their prompt.
448
585
  raw = stripped;
@@ -453,7 +590,7 @@ class ChatSession {
453
590
  const inline = raw.slice(3);
454
591
  // Single-line `"""hello"""` shortcut.
455
592
  if (inline.endsWith('"""')) {
456
- return inline.slice(0, -3);
593
+ return await this.maybeCompressVisiblePaste(inline.slice(0, -3));
457
594
  }
458
595
  const buffer = [inline];
459
596
  while (true) {
@@ -466,13 +603,22 @@ class ChatSession {
466
603
  }
467
604
  buffer.push(next);
468
605
  }
469
- return buffer.join('\n').trim();
606
+ return await this.maybeCompressVisiblePaste(buffer.join('\n').trim());
470
607
  }
471
- // 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.
472
612
  if (raw.includes('\n'))
473
613
  return raw;
474
- // Slash command: invoke the autocomplete dropdown if registry has matches.
475
- 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('/')) {
476
622
  const matches = this.opts.commandRegistry.filter(raw);
477
623
  if (matches.length > 1) {
478
624
  try {
@@ -481,7 +627,7 @@ class ChatSession {
481
627
  return this.opts.commandRegistry
482
628
  .filter(filterStr)
483
629
  .map((cmd) => ({
484
- name: cmd.icon ? `${cmd.icon} /${cmd.name}` : `/${cmd.name}`,
630
+ name: renderCommandLabel(cmd),
485
631
  value: `/${cmd.name}`,
486
632
  description: cmd.description,
487
633
  }));
@@ -494,8 +640,48 @@ class ChatSession {
494
640
  }
495
641
  }
496
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
+ }
497
658
  return raw;
498
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
+ }
499
685
  }
500
686
  exports.ChatSession = ChatSession;
501
687
  // ── Helpers ──────────────────────────────────────────────────────────
@@ -654,7 +840,7 @@ function formatDuration(ms) {
654
840
  const remMin = min % 60;
655
841
  return remMin > 0 ? `${hr}h${remMin}m` : `${hr}h`;
656
842
  }
657
- function createDefaultPromptApi() {
843
+ function createDefaultPromptApi(opts = {}) {
658
844
  // Lazy-load @inquirer/prompts so test harnesses without a TTY don't break.
659
845
  // eslint-disable-next-line @typescript-eslint/no-var-requires
660
846
  const inq = require('@inquirer/prompts');
@@ -663,10 +849,37 @@ function createDefaultPromptApi() {
663
849
  // shows alone. We pass per-status entries so the answered echo also
664
850
  // stays clean; `loading` is intentionally untouched (spinner state).
665
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;
666
856
  return {
667
857
  async readLine(prompt) {
668
858
  try {
669
- 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 ?? '';
670
883
  }
671
884
  catch (err) {
672
885
  // Inquirer wraps Ctrl+C as ExitPromptError. Re-throw as plain Error
@@ -676,6 +889,10 @@ function createDefaultPromptApi() {
676
889
  }
677
890
  },
678
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.
679
896
  try {
680
897
  return (await inq.search({ message: '/', source, theme: promptTheme }));
681
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
+ }