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.
- package/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +421 -5
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +256 -55
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +297 -13
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- 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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
365
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
//
|
|
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.
|
|
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
|
|
491
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|