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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/historyStore.ts — Tier-3.1.1 (v4.1-tier3.1.1)
|
|
10
|
+
*
|
|
11
|
+
* Persistent input history for the chat REPL. Each user prompt is
|
|
12
|
+
* appended to `<aidenHome>/.aiden_history`, one entry per line,
|
|
13
|
+
* multiline-encoded so a prompt with embedded newlines round-trips
|
|
14
|
+
* faithfully:
|
|
15
|
+
* - `\n` inside a prompt is encoded as `\\n` on disk
|
|
16
|
+
* - `\\` inside a prompt is encoded as `\\\\` on disk
|
|
17
|
+
*
|
|
18
|
+
* The store filters out:
|
|
19
|
+
* - blank entries
|
|
20
|
+
* - duplicates of the most recent entry
|
|
21
|
+
* - very-short entries (<3 chars after trim) — too noisy to suggest
|
|
22
|
+
* - paste-labelled entries (`[paste #N: …]`) — privacy. The user's
|
|
23
|
+
* real prompt was the label-substituted text; storing the label
|
|
24
|
+
* would leak nothing but storing the expanded text would leak the
|
|
25
|
+
* entire pasted block into a plain-text history file.
|
|
26
|
+
*
|
|
27
|
+
* On startup, `loadRecent()` returns the last `limit` entries (newest
|
|
28
|
+
* first) for the autosuggest history fallback.
|
|
29
|
+
*
|
|
30
|
+
* Atomic write: each `append` writes a temp sibling then renames over
|
|
31
|
+
* the live file (Windows rename is atomic on the same volume), so a
|
|
32
|
+
* crash mid-write can never corrupt the history.
|
|
33
|
+
*/
|
|
34
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
35
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
|
+
};
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.HISTORY_MAX_ENTRIES = void 0;
|
|
39
|
+
exports.appendHistory = appendHistory;
|
|
40
|
+
exports.loadRecent = loadRecent;
|
|
41
|
+
exports._resetForTests = _resetForTests;
|
|
42
|
+
const node_fs_1 = require("node:fs");
|
|
43
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const paths_1 = require("../../core/v4/paths");
|
|
45
|
+
const HISTORY_FILENAME = '.aiden_history';
|
|
46
|
+
const PASTE_LABEL_RE = /\[paste #\d+:[^\]]*\]/;
|
|
47
|
+
let writeLatch = Promise.resolve();
|
|
48
|
+
function historyPath() {
|
|
49
|
+
return node_path_1.default.join((0, paths_1.resolveAidenPaths)().root, HISTORY_FILENAME);
|
|
50
|
+
}
|
|
51
|
+
/** Encode a prompt for one-line storage on disk. */
|
|
52
|
+
function encode(s) {
|
|
53
|
+
return s.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
|
|
54
|
+
}
|
|
55
|
+
/** Reverse of `encode`. */
|
|
56
|
+
function decode(s) {
|
|
57
|
+
// Walk the string so `\\\\n` decodes to `\\n` (literal backslash + n)
|
|
58
|
+
// rather than the placeholder for newline.
|
|
59
|
+
let out = '';
|
|
60
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
61
|
+
const c = s[i];
|
|
62
|
+
if (c === '\\' && i + 1 < s.length) {
|
|
63
|
+
const next = s[i + 1];
|
|
64
|
+
if (next === 'n') {
|
|
65
|
+
out += '\n';
|
|
66
|
+
i += 1;
|
|
67
|
+
}
|
|
68
|
+
else if (next === '\\') {
|
|
69
|
+
out += '\\';
|
|
70
|
+
i += 1;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
out += c;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
out += c;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Append `entry` to the history file. Filters per the rules above.
|
|
84
|
+
* Best-effort — disk failures are swallowed so a crashed history
|
|
85
|
+
* write never breaks the agent loop.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* Tier-3-essentials: cap the live file at this many entries. When an
|
|
89
|
+
* append would push the count above, we rotate the oldest out before
|
|
90
|
+
* writing the new line. 5000 = ~250 KB at typical prompt sizes,
|
|
91
|
+
* trivial to keep on disk and load on every prompt.
|
|
92
|
+
*/
|
|
93
|
+
exports.HISTORY_MAX_ENTRIES = 5000;
|
|
94
|
+
async function appendHistory(entry) {
|
|
95
|
+
const trimmed = entry.trim();
|
|
96
|
+
if (trimmed.length < 3)
|
|
97
|
+
return;
|
|
98
|
+
if (PASTE_LABEL_RE.test(trimmed))
|
|
99
|
+
return;
|
|
100
|
+
await (writeLatch = writeLatch.then(async () => {
|
|
101
|
+
try {
|
|
102
|
+
const p = historyPath();
|
|
103
|
+
const dir = node_path_1.default.dirname(p);
|
|
104
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
105
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
106
|
+
// Read current contents once — we use them for both dup-suppress
|
|
107
|
+
// and rotation.
|
|
108
|
+
let priorLines = [];
|
|
109
|
+
try {
|
|
110
|
+
const cur = await node_fs_1.promises.readFile(p, 'utf8');
|
|
111
|
+
priorLines = cur.split('\n').filter((l) => l.length > 0);
|
|
112
|
+
}
|
|
113
|
+
catch { /* file may not exist yet */ }
|
|
114
|
+
// Skip if equal to the last entry on disk (cheap dup-suppress).
|
|
115
|
+
const lastDecoded = priorLines.length > 0 ? decode(priorLines[priorLines.length - 1]) : '';
|
|
116
|
+
if (lastDecoded === entry)
|
|
117
|
+
return;
|
|
118
|
+
// Rotation: cap at HISTORY_MAX_ENTRIES. The new line will push
|
|
119
|
+
// total to len+1; if that exceeds the cap, drop the oldest
|
|
120
|
+
// (len+1 - cap) entries from the front so we land EXACTLY at
|
|
121
|
+
// the cap.
|
|
122
|
+
const wantTotal = priorLines.length + 1;
|
|
123
|
+
if (wantTotal > exports.HISTORY_MAX_ENTRIES) {
|
|
124
|
+
const dropFront = wantTotal - exports.HISTORY_MAX_ENTRIES;
|
|
125
|
+
priorLines = priorLines.slice(dropFront);
|
|
126
|
+
}
|
|
127
|
+
const tmp = `${p}.tmp`;
|
|
128
|
+
const nextContent = priorLines.join('\n')
|
|
129
|
+
+ (priorLines.length > 0 ? '\n' : '')
|
|
130
|
+
+ `${encode(entry)}\n`;
|
|
131
|
+
await node_fs_1.promises.writeFile(tmp, nextContent, 'utf8');
|
|
132
|
+
await node_fs_1.promises.rename(tmp, p);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// History write failure must not bubble up.
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Return the last `limit` entries (newest first). Decoded — caller
|
|
141
|
+
* sees the original prompt verbatim including any embedded newlines.
|
|
142
|
+
*
|
|
143
|
+
* Tier-3-essentials: default raised 100 → 500 so the autosuggest
|
|
144
|
+
* history-mode reaches further back. The on-disk cap is independently
|
|
145
|
+
* controlled by `HISTORY_MAX_ENTRIES`.
|
|
146
|
+
*/
|
|
147
|
+
async function loadRecent(limit = 500) {
|
|
148
|
+
try {
|
|
149
|
+
const p = historyPath();
|
|
150
|
+
const raw = await node_fs_1.promises.readFile(p, 'utf8');
|
|
151
|
+
const lines = raw.split('\n').filter((l) => l.length > 0);
|
|
152
|
+
const decoded = lines.map(decode);
|
|
153
|
+
const sliced = decoded.slice(Math.max(0, decoded.length - limit));
|
|
154
|
+
return sliced.reverse();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Test/reset hook: drop in-process state. Disk untouched. */
|
|
161
|
+
function _resetForTests() {
|
|
162
|
+
writeLatch = Promise.resolve();
|
|
163
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/pasteCompression.ts — Tier-3.1 (v4.1-tier3.1)
|
|
10
|
+
*
|
|
11
|
+
* When a user pastes a large block (>5 lines OR >500 chars), the
|
|
12
|
+
* REPL replaces the visible echo with a compact label
|
|
13
|
+
* `[paste #<id>: <N> lines, <KB>]`
|
|
14
|
+
* and stores the original at `<aidenRoot>/pastes/paste_<id>.txt`.
|
|
15
|
+
* The agent receives the original text as input — only the visible
|
|
16
|
+
* echo is compressed, so the LLM still sees full content.
|
|
17
|
+
*
|
|
18
|
+
* The id counter is persisted in `<aidenRoot>/pastes/manifest.json`
|
|
19
|
+
* so it increments across sessions. Concurrent writes from the same
|
|
20
|
+
* process are serialised through an in-process latch; cross-process
|
|
21
|
+
* concurrency is best-effort (the manifest is read+rewritten atomic-
|
|
22
|
+
* ally enough for a single-user CLI).
|
|
23
|
+
*
|
|
24
|
+
* `expandPaste(id)` reads the original back from disk, used by the
|
|
25
|
+
* `/show <id>` slash command.
|
|
26
|
+
*/
|
|
27
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.PASTE_COMPRESS_CHARS = exports.PASTE_COMPRESS_LINES = void 0;
|
|
32
|
+
exports.compressPaste = compressPaste;
|
|
33
|
+
exports.expandPaste = expandPaste;
|
|
34
|
+
exports._resetLatchForTests = _resetLatchForTests;
|
|
35
|
+
const node_fs_1 = require("node:fs");
|
|
36
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
37
|
+
const paths_1 = require("../../core/v4/paths");
|
|
38
|
+
/** Heuristic threshold — copy-paste a code block of >5 lines or >500
|
|
39
|
+
* chars and we compress; smaller pastes echo verbatim. */
|
|
40
|
+
exports.PASTE_COMPRESS_LINES = 5;
|
|
41
|
+
exports.PASTE_COMPRESS_CHARS = 500;
|
|
42
|
+
/** Per-process write latch so concurrent compresses don't race the
|
|
43
|
+
* manifest. Cross-process safety is non-goal for the single-user CLI. */
|
|
44
|
+
let writeLatch = Promise.resolve();
|
|
45
|
+
function pastesDir() {
|
|
46
|
+
const paths = (0, paths_1.resolveAidenPaths)();
|
|
47
|
+
return node_path_1.default.join(paths.root, 'pastes');
|
|
48
|
+
}
|
|
49
|
+
function manifestPath() {
|
|
50
|
+
return node_path_1.default.join(pastesDir(), 'manifest.json');
|
|
51
|
+
}
|
|
52
|
+
async function readNextId() {
|
|
53
|
+
try {
|
|
54
|
+
const raw = await node_fs_1.promises.readFile(manifestPath(), 'utf8');
|
|
55
|
+
const j = JSON.parse(raw);
|
|
56
|
+
if (typeof j.nextId === 'number' && j.nextId >= 1)
|
|
57
|
+
return j.nextId;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// missing or malformed — start at 1
|
|
61
|
+
}
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
async function writeNextId(next) {
|
|
65
|
+
const dir = pastesDir();
|
|
66
|
+
await node_fs_1.promises.mkdir(dir, { recursive: true });
|
|
67
|
+
await node_fs_1.promises.writeFile(manifestPath(), JSON.stringify({ nextId: next }, null, 2), 'utf8');
|
|
68
|
+
}
|
|
69
|
+
function formatBytes(text) {
|
|
70
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
71
|
+
if (bytes < 1024)
|
|
72
|
+
return `${bytes}B`;
|
|
73
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Decide whether `text` should be compressed and (if so) persist the
|
|
77
|
+
* original. Always returns the original text for the agent path; only
|
|
78
|
+
* the echo path consults `compressed`/`label`.
|
|
79
|
+
*/
|
|
80
|
+
async function compressPaste(text) {
|
|
81
|
+
const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
82
|
+
const big = lineCount > exports.PASTE_COMPRESS_LINES || text.length > exports.PASTE_COMPRESS_CHARS;
|
|
83
|
+
if (!big) {
|
|
84
|
+
return { compressed: false, original: text };
|
|
85
|
+
}
|
|
86
|
+
// Atomic ID allocation under in-process latch.
|
|
87
|
+
let id = '';
|
|
88
|
+
let label = '';
|
|
89
|
+
await (writeLatch = writeLatch.then(async () => {
|
|
90
|
+
const next = await readNextId();
|
|
91
|
+
id = String(next);
|
|
92
|
+
const dir = pastesDir();
|
|
93
|
+
await node_fs_1.promises.mkdir(dir, { recursive: true });
|
|
94
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(dir, `paste_${id}.txt`), text, 'utf8');
|
|
95
|
+
await writeNextId(next + 1);
|
|
96
|
+
label = `[paste #${id}: ${lineCount} lines, ${formatBytes(text)}]`;
|
|
97
|
+
}));
|
|
98
|
+
return { compressed: true, id, label, original: text };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read back a previously stored paste by id. Returns `null` if the
|
|
102
|
+
* id is unknown or the file vanished.
|
|
103
|
+
*/
|
|
104
|
+
async function expandPaste(id) {
|
|
105
|
+
// Defence-in-depth: only allow numeric ids — keeps the path-join
|
|
106
|
+
// from straying outside the pastes directory if someone ever
|
|
107
|
+
// wires an untrusted argument here.
|
|
108
|
+
if (!/^\d+$/.test(id))
|
|
109
|
+
return null;
|
|
110
|
+
try {
|
|
111
|
+
const file = node_path_1.default.join(pastesDir(), `paste_${id}.txt`);
|
|
112
|
+
return await node_fs_1.promises.readFile(file, 'utf8');
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Test/reset hook: drop the in-process latch so a fresh test run
|
|
120
|
+
* starts with a clean serialiser. Disk state untouched.
|
|
121
|
+
*/
|
|
122
|
+
function _resetLatchForTests() {
|
|
123
|
+
writeLatch = Promise.resolve();
|
|
124
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/pasteIntercept.ts — Tier-3.1a (v4.1-tier3.1a)
|
|
10
|
+
*
|
|
11
|
+
* Stdin pre-tap that handles bracketed paste sequences before
|
|
12
|
+
* @inquirer/prompts sees them. Modern inquirer treats any internal
|
|
13
|
+
* `\n` as Enter and resolves early, so a multi-line paste auto-
|
|
14
|
+
* submits before the user has a chance to review. This module
|
|
15
|
+
* intercepts paste boundaries (CSI 2004), captures the content,
|
|
16
|
+
* persists it via the existing pasteCompression manifest, and
|
|
17
|
+
* substitutes a `[paste #<id>: <N> lines, <KB>]` label on stdin.
|
|
18
|
+
*
|
|
19
|
+
* The user sees the label in inquirer's input buffer, presses Enter
|
|
20
|
+
* to submit, and chatSession.readUserInput swaps the label for the
|
|
21
|
+
* original via getPasteOriginal(id) before handing to the agent.
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.getPasteOriginal = getPasteOriginal;
|
|
28
|
+
exports.expandPasteLabels = expandPasteLabels;
|
|
29
|
+
exports.installPasteInterceptor = installPasteInterceptor;
|
|
30
|
+
exports._resetForTests = _resetForTests;
|
|
31
|
+
const node_fs_1 = require("node:fs");
|
|
32
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
33
|
+
const paths_1 = require("../../core/v4/paths");
|
|
34
|
+
const PASTE_BEGIN = '\x1b[200~';
|
|
35
|
+
const PASTE_END = '\x1b[201~';
|
|
36
|
+
/** id → original text (in-memory swap table). */
|
|
37
|
+
const originals = new Map();
|
|
38
|
+
function pastesDir() {
|
|
39
|
+
return node_path_1.default.join((0, paths_1.resolveAidenPaths)().root, 'pastes');
|
|
40
|
+
}
|
|
41
|
+
function manifestPath() {
|
|
42
|
+
return node_path_1.default.join(pastesDir(), 'manifest.json');
|
|
43
|
+
}
|
|
44
|
+
function readNextIdSync() {
|
|
45
|
+
try {
|
|
46
|
+
const raw = (0, node_fs_1.readFileSync)(manifestPath(), 'utf8');
|
|
47
|
+
const j = JSON.parse(raw);
|
|
48
|
+
if (typeof j.nextId === 'number' && j.nextId >= 1)
|
|
49
|
+
return j.nextId;
|
|
50
|
+
}
|
|
51
|
+
catch { /* missing or malformed */ }
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
function writeNextIdSync(next) {
|
|
55
|
+
const dir = pastesDir();
|
|
56
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
57
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
58
|
+
(0, node_fs_1.writeFileSync)(manifestPath(), JSON.stringify({ nextId: next }, null, 2), 'utf8');
|
|
59
|
+
}
|
|
60
|
+
function formatBytes(text) {
|
|
61
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
62
|
+
return bytes < 1024 ? `${bytes}B` : `${(bytes / 1024).toFixed(1)}KB`;
|
|
63
|
+
}
|
|
64
|
+
function compressSync(text) {
|
|
65
|
+
const dir = pastesDir();
|
|
66
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
67
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
68
|
+
const next = readNextIdSync();
|
|
69
|
+
const id = String(next);
|
|
70
|
+
(0, node_fs_1.writeFileSync)(node_path_1.default.join(dir, `paste_${id}.txt`), text, 'utf8');
|
|
71
|
+
writeNextIdSync(next + 1);
|
|
72
|
+
const lineCount = (text.match(/\n/g)?.length ?? 0) + 1;
|
|
73
|
+
return { id, label: `[paste #${id}: ${lineCount} lines, ${formatBytes(text)}]` };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Look up the original text for a paste id. Returns undefined if the
|
|
77
|
+
* id was never seen by this process (e.g. the user typed a label by
|
|
78
|
+
* hand). Disk is the source of truth for /show <id>; this map is the
|
|
79
|
+
* fast path for the in-flight prompt swap.
|
|
80
|
+
*/
|
|
81
|
+
function getPasteOriginal(id) {
|
|
82
|
+
return originals.get(id);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Replace `[paste #N: …]` patterns in `input` with the corresponding
|
|
86
|
+
* original text from the in-process map. Patterns whose id we don't
|
|
87
|
+
* know are left intact (might be user-typed). Returns the swapped
|
|
88
|
+
* string.
|
|
89
|
+
*/
|
|
90
|
+
function expandPasteLabels(input) {
|
|
91
|
+
return input.replace(/\[paste #(\d+):[^\]]*\]/g, (m, id) => {
|
|
92
|
+
const orig = originals.get(id);
|
|
93
|
+
return orig !== undefined ? orig : m;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
let installed = null;
|
|
97
|
+
/**
|
|
98
|
+
* Install the stdin pre-tap. Wraps `process.stdin.emit('data', …)`
|
|
99
|
+
* so paste payloads are captured + replaced with labels before any
|
|
100
|
+
* downstream listener (inquirer) sees them. Idempotent. Returns an
|
|
101
|
+
* uninstall function.
|
|
102
|
+
*
|
|
103
|
+
* MCP serve mode: never call this — `aiden mcp serve` doesn't run
|
|
104
|
+
* the REPL.
|
|
105
|
+
*/
|
|
106
|
+
function installPasteInterceptor(stdin) {
|
|
107
|
+
if (installed)
|
|
108
|
+
return installed.restore;
|
|
109
|
+
const origEmit = stdin.emit.bind(stdin);
|
|
110
|
+
const state = { inPaste: false, buf: '' };
|
|
111
|
+
function processChunk(text) {
|
|
112
|
+
let out = '';
|
|
113
|
+
let cursor = 0;
|
|
114
|
+
while (cursor < text.length) {
|
|
115
|
+
if (state.inPaste) {
|
|
116
|
+
const endIdx = text.indexOf(PASTE_END, cursor);
|
|
117
|
+
if (endIdx === -1) {
|
|
118
|
+
state.buf += text.slice(cursor);
|
|
119
|
+
cursor = text.length;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
state.buf += text.slice(cursor, endIdx);
|
|
123
|
+
cursor = endIdx + PASTE_END.length;
|
|
124
|
+
// Tier-3.1c: terminals (and some clipboard payloads) emit a
|
|
125
|
+
// trailing CR/LF immediately after PASTE_END. Without this
|
|
126
|
+
// swallow the bytes pass through to readline, where they
|
|
127
|
+
// become an Enter event and auto-submit the prompt before
|
|
128
|
+
// the user has reviewed the paste. Eat at most one CR + one
|
|
129
|
+
// LF (in either order) right after PASTE_END.
|
|
130
|
+
if (text[cursor] === '\r')
|
|
131
|
+
cursor += 1;
|
|
132
|
+
if (text[cursor] === '\n')
|
|
133
|
+
cursor += 1;
|
|
134
|
+
state.inPaste = false;
|
|
135
|
+
const original = state.buf.replace(/\r\n/g, '\n');
|
|
136
|
+
state.buf = '';
|
|
137
|
+
// Strip a single trailing newline (Enter at end of paste).
|
|
138
|
+
const trimmed = original.replace(/\n+$/, '');
|
|
139
|
+
if (!trimmed.includes('\n') && trimmed.length <= 500) {
|
|
140
|
+
// Single-line, small — emit as-is so user can edit.
|
|
141
|
+
out += trimmed;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// Multi-line or large — disk-back + emit label.
|
|
145
|
+
try {
|
|
146
|
+
const { id, label } = compressSync(trimmed);
|
|
147
|
+
originals.set(id, trimmed);
|
|
148
|
+
out += label;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Disk failure: fall back to a single-space substitute
|
|
152
|
+
// so internal newlines don't trigger auto-submit.
|
|
153
|
+
out += trimmed.replace(/\n/g, ' ');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const beginIdx = text.indexOf(PASTE_BEGIN, cursor);
|
|
160
|
+
if (beginIdx === -1) {
|
|
161
|
+
out += text.slice(cursor);
|
|
162
|
+
cursor = text.length;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
out += text.slice(cursor, beginIdx);
|
|
166
|
+
cursor = beginIdx + PASTE_BEGIN.length;
|
|
167
|
+
state.inPaste = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
const wrappedEmit = function (event, ...args) {
|
|
174
|
+
if (event !== 'data')
|
|
175
|
+
return origEmit(event, ...args);
|
|
176
|
+
const chunk = args[0];
|
|
177
|
+
if (chunk == null)
|
|
178
|
+
return origEmit(event, ...args);
|
|
179
|
+
const text = Buffer.isBuffer(chunk)
|
|
180
|
+
? chunk.toString('utf8')
|
|
181
|
+
: (typeof chunk === 'string' ? chunk : String(chunk));
|
|
182
|
+
const processed = processChunk(text);
|
|
183
|
+
if (processed.length === 0)
|
|
184
|
+
return true; // suppress entirely
|
|
185
|
+
const nextArgs = [Buffer.from(processed, 'utf8'), ...args.slice(1)];
|
|
186
|
+
return origEmit(event, ...nextArgs);
|
|
187
|
+
};
|
|
188
|
+
stdin.emit = wrappedEmit;
|
|
189
|
+
const restore = () => {
|
|
190
|
+
if (!installed)
|
|
191
|
+
return;
|
|
192
|
+
stdin.emit = origEmit;
|
|
193
|
+
installed = null;
|
|
194
|
+
};
|
|
195
|
+
installed = { restore };
|
|
196
|
+
return restore;
|
|
197
|
+
}
|
|
198
|
+
/** Test helper: clear the in-memory map (does not touch disk). */
|
|
199
|
+
function _resetForTests() {
|
|
200
|
+
originals.clear();
|
|
201
|
+
if (installed)
|
|
202
|
+
installed.restore();
|
|
203
|
+
}
|