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.
- package/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +513 -14
- 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 +269 -52
- 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 +19 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/setup.js +34 -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 +300 -14
- 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/setupWizard.js +466 -232
- 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/firstRun/providerDetection.js +287 -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/providers/v4/nullAdapter.js +58 -0
- 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,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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
//
|
|
439
|
-
//
|
|
440
|
-
//
|
|
441
|
-
//
|
|
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.
|
|
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
|
|
475
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|