aiden-runtime 4.0.2 → 4.1.1
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 +19 -11
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +424 -7
- 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 +102 -1
- package/dist/cli/v4/doctorLiveness.js +329 -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 +118 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +140 -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/moat/dangerousPatterns.js +1 -1
- package/dist/providers/v4/codexResponsesAdapter.js +7 -2
- package/dist/providers/v4/errors.js +51 -1
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +190 -0
- package/package.json +11 -2
|
@@ -0,0 +1,139 @@
|
|
|
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/shellInterpolation.ts — Phase v4.1-tier3-essentials
|
|
10
|
+
*
|
|
11
|
+
* Inline shell expansion. When a user prompt contains one or more
|
|
12
|
+
* `{!cmd}` spans, we run each command, splice the output back in,
|
|
13
|
+
* and submit the rewritten prompt to the agent.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - Each `{!cmd}` runs via `child_process.exec` with a 5s wallclock
|
|
17
|
+
* timeout (kill on overrun).
|
|
18
|
+
* - Output is stdout (or stderr if stdout empty), trimmed, capped
|
|
19
|
+
* at 500 visible chars. Multi-line output is collapsed to the
|
|
20
|
+
* first 500 chars verbatim — newlines preserved.
|
|
21
|
+
* - On non-zero exit / timeout / spawn failure, the span is
|
|
22
|
+
* replaced with `[shell:error]` so the rest of the prompt still
|
|
23
|
+
* submits.
|
|
24
|
+
* - Every span runs in parallel; total wait bounded by the slowest
|
|
25
|
+
* single command.
|
|
26
|
+
*
|
|
27
|
+
* MCP serve mode never reaches this path (REPL doesn't run there).
|
|
28
|
+
*
|
|
29
|
+
* Security: this expands BEFORE the agent loop, so the same
|
|
30
|
+
* `approvalEngine` gate the user has on the in-agent `shell_exec`
|
|
31
|
+
* tool does NOT apply here. To prevent an unattended REPL from
|
|
32
|
+
* exfiltrating arbitrary command output, callers SHOULD only invoke
|
|
33
|
+
* this on user-typed prompts (never on tool-emitted text). chatSession
|
|
34
|
+
* applies it to `readUserInput`'s return value, which is exactly that.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.INTERPOLATION_RE = void 0;
|
|
38
|
+
exports.hasInterpolation = hasInterpolation;
|
|
39
|
+
exports.expand = expand;
|
|
40
|
+
exports.countSpans = countSpans;
|
|
41
|
+
const node_child_process_1 = require("node:child_process");
|
|
42
|
+
/** Matches `{!cmd}` spans non-greedily so `{!a} {!b}` produces two matches. */
|
|
43
|
+
exports.INTERPOLATION_RE = /\{!(.+?)\}/g;
|
|
44
|
+
/** Truthy check used by callers that want to skip the work entirely. */
|
|
45
|
+
function hasInterpolation(text) {
|
|
46
|
+
return /\{![^}]+\}/.test(text);
|
|
47
|
+
}
|
|
48
|
+
/** Default output cap (visible chars per span). */
|
|
49
|
+
const OUTPUT_CAP = 500;
|
|
50
|
+
/** Default wallclock timeout per span (ms). */
|
|
51
|
+
const TIMEOUT_MS = 5000;
|
|
52
|
+
/**
|
|
53
|
+
* Run a single `cmd` and return the trimmed output (or `[shell:error]`).
|
|
54
|
+
* Always resolves; never rejects.
|
|
55
|
+
*/
|
|
56
|
+
async function runOne(cmd, opts) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
let settled = false;
|
|
59
|
+
const child = (0, node_child_process_1.exec)(cmd, { timeout: opts.timeoutMs, windowsHide: true }, (err, stdout, stderr) => {
|
|
60
|
+
if (settled)
|
|
61
|
+
return;
|
|
62
|
+
settled = true;
|
|
63
|
+
if (err) {
|
|
64
|
+
// exec sets err.killed=true on timeout. Either way:
|
|
65
|
+
// surface a marker rather than a partial command output.
|
|
66
|
+
resolve('[shell:error]');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const out = (stdout && stdout.length > 0 ? stdout : stderr ?? '').trim();
|
|
70
|
+
if (out.length === 0) {
|
|
71
|
+
resolve('');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (out.length <= opts.outputCap) {
|
|
75
|
+
resolve(out);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
resolve(out.slice(0, opts.outputCap) + '…');
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// Defensive: kill on timeout in case `exec`'s built-in timeout
|
|
82
|
+
// misses the window (Windows shell quirks). The exec callback
|
|
83
|
+
// above will still fire.
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
if (!settled) {
|
|
86
|
+
try {
|
|
87
|
+
child.kill('SIGKILL');
|
|
88
|
+
}
|
|
89
|
+
catch { /* */ }
|
|
90
|
+
}
|
|
91
|
+
}, opts.timeoutMs + 500);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Expand every `{!cmd}` in `text`, returning the rewritten string.
|
|
96
|
+
* If `text` contains no spans, returned verbatim with no work done.
|
|
97
|
+
*/
|
|
98
|
+
async function expand(text, optsIn = {}) {
|
|
99
|
+
if (!hasInterpolation(text))
|
|
100
|
+
return text;
|
|
101
|
+
const opts = {
|
|
102
|
+
timeoutMs: optsIn.timeoutMs ?? TIMEOUT_MS,
|
|
103
|
+
outputCap: optsIn.outputCap ?? OUTPUT_CAP,
|
|
104
|
+
};
|
|
105
|
+
// Collect all matches up front so we can splice in order.
|
|
106
|
+
const matches = [];
|
|
107
|
+
for (const m of text.matchAll(exports.INTERPOLATION_RE)) {
|
|
108
|
+
if (m.index === undefined)
|
|
109
|
+
continue;
|
|
110
|
+
matches.push({
|
|
111
|
+
start: m.index,
|
|
112
|
+
end: m.index + m[0].length,
|
|
113
|
+
cmd: (m[1] ?? '').trim(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Run all spans in parallel.
|
|
117
|
+
const replacements = await Promise.all(matches.map(async (m) => ({
|
|
118
|
+
start: m.start,
|
|
119
|
+
end: m.end,
|
|
120
|
+
text: m.cmd.length > 0 ? await runOne(m.cmd, opts) : '',
|
|
121
|
+
})));
|
|
122
|
+
// Splice from right-to-left so earlier spans' positions stay valid.
|
|
123
|
+
let out = text;
|
|
124
|
+
for (let i = replacements.length - 1; i >= 0; i -= 1) {
|
|
125
|
+
const r = replacements[i];
|
|
126
|
+
out = out.slice(0, r.start) + r.text + out.slice(r.end);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Cheap surface count for the pre-submit "[shell] running N
|
|
132
|
+
* interpolations…" status line.
|
|
133
|
+
*/
|
|
134
|
+
function countSpans(text) {
|
|
135
|
+
let n = 0;
|
|
136
|
+
for (const _ of text.matchAll(exports.INTERPOLATION_RE))
|
|
137
|
+
n += 1;
|
|
138
|
+
return n;
|
|
139
|
+
}
|
|
@@ -57,7 +57,12 @@ const DEFAULT_SKIN = {
|
|
|
57
57
|
glyphs: {
|
|
58
58
|
bullet: '•',
|
|
59
59
|
arrow: '›',
|
|
60
|
-
|
|
60
|
+
// Tier-3.1 (v4.1-tier3.1): replaced the generic braille spinner
|
|
61
|
+
// with a custom Aiden frame set derived from the ▲ prompt glyph.
|
|
62
|
+
// Six-frame rotating-triangle cadence reads as motion at the
|
|
63
|
+
// standard ~80ms tick without depending on colour, so it works
|
|
64
|
+
// identically under monochrome forks of this skin.
|
|
65
|
+
spinner: ['▲', '△', '▴', '▵', '▴', '△'],
|
|
61
66
|
},
|
|
62
67
|
};
|
|
63
68
|
const LIGHT_SKIN = {
|
|
@@ -150,6 +155,21 @@ class SkinEngine {
|
|
|
150
155
|
* The loaded skin becomes the active skin.
|
|
151
156
|
*/
|
|
152
157
|
async loadSkin(name) {
|
|
158
|
+
// Tier-3-essentials: 'auto' resolves to a concrete skin via the
|
|
159
|
+
// multi-signal detector (AIDEN_THEME / NO_COLOR / COLORFGBG /
|
|
160
|
+
// TERM_PROGRAM, falling back to 'default'). Resolves once per
|
|
161
|
+
// call — caller can re-invoke loadSkin('auto') if env changes.
|
|
162
|
+
if (name === 'auto') {
|
|
163
|
+
// Lazy import keeps the skin loader free of detector deps in
|
|
164
|
+
// synchronous test paths that bypass loadSkin.
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
166
|
+
const { detectTheme, detectedToSkinName } = require('./themeDetect');
|
|
167
|
+
const detected = detectTheme();
|
|
168
|
+
const resolved = detectedToSkinName(detected);
|
|
169
|
+
// Re-enter loadSkin with the resolved name (no infinite loop —
|
|
170
|
+
// the detector never returns 'auto').
|
|
171
|
+
return this.loadSkin(resolved);
|
|
172
|
+
}
|
|
153
173
|
if (this.cache.has(name)) {
|
|
154
174
|
this.current = this.cache.get(name);
|
|
155
175
|
return this.current;
|
|
@@ -0,0 +1,121 @@
|
|
|
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/streamingPrefix.ts — Phase v4.1-reply-formatting
|
|
9
|
+
*
|
|
10
|
+
* Stable-prefix split for streaming markdown. Given the running
|
|
11
|
+
* buffered text, return the index of the last "safe" boundary —
|
|
12
|
+
* a `\n\n` that lies OUTSIDE any open code fence. Content above
|
|
13
|
+
* the boundary is locked (already rendered, won't redraw); content
|
|
14
|
+
* below the boundary is the suffix the caller may re-render.
|
|
15
|
+
*
|
|
16
|
+
* safePrefixBoundary("# H\n\nbody\n\nmore") === <index after 2nd \n\n>
|
|
17
|
+
* safePrefixBoundary("```ts\nstill open") === 0 // inside fence
|
|
18
|
+
*
|
|
19
|
+
* Pure function. Used by display.streamComplete to decide whether to
|
|
20
|
+
* re-render the whole stream as markdown or only the trailing chunk.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.safePrefixBoundary = safePrefixBoundary;
|
|
24
|
+
exports.splitAtBoundary = splitAtBoundary;
|
|
25
|
+
exports.endsInsideFence = endsInsideFence;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the index in `text` immediately AFTER the last `\n\n` that
|
|
28
|
+
* lies outside an open code fence. Returns 0 when no safe boundary
|
|
29
|
+
* exists (e.g. when the text is entirely inside an open fence).
|
|
30
|
+
*/
|
|
31
|
+
function safePrefixBoundary(text) {
|
|
32
|
+
let inFence = false;
|
|
33
|
+
let fenceMarker = null;
|
|
34
|
+
let lastBoundary = 0;
|
|
35
|
+
let i = 0;
|
|
36
|
+
const n = text.length;
|
|
37
|
+
while (i < n) {
|
|
38
|
+
if (!inFence) {
|
|
39
|
+
// Open fence?
|
|
40
|
+
if (text.startsWith('```', i)) {
|
|
41
|
+
inFence = true;
|
|
42
|
+
fenceMarker = '```';
|
|
43
|
+
i += 3;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (text.startsWith('~~~', i)) {
|
|
47
|
+
inFence = true;
|
|
48
|
+
fenceMarker = '~~~';
|
|
49
|
+
i += 3;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Paragraph break — `\n\n` (or `\n \n` with whitespace gap).
|
|
53
|
+
if (text[i] === '\n' && /\n[ \t]*\n/.test(text.slice(i, i + 4))) {
|
|
54
|
+
// Advance past the consecutive whitespace+newlines.
|
|
55
|
+
let j = i;
|
|
56
|
+
while (j < n && (text[j] === '\n' || text[j] === ' ' || text[j] === '\t')) {
|
|
57
|
+
j += 1;
|
|
58
|
+
}
|
|
59
|
+
// Boundary is the START of the post-break content.
|
|
60
|
+
lastBoundary = j;
|
|
61
|
+
i = j;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Close fence?
|
|
67
|
+
if (fenceMarker && text.startsWith(fenceMarker, i)) {
|
|
68
|
+
inFence = false;
|
|
69
|
+
fenceMarker = null;
|
|
70
|
+
i += 3;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
return lastBoundary;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Split `text` at the safe boundary. `prefix` is locked content
|
|
80
|
+
* (already rendered), `suffix` is the unstable tail the caller
|
|
81
|
+
* should re-render on the next pass.
|
|
82
|
+
*/
|
|
83
|
+
function splitAtBoundary(text) {
|
|
84
|
+
const idx = safePrefixBoundary(text);
|
|
85
|
+
return { prefix: text.slice(0, idx), suffix: text.slice(idx) };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Helper: detect whether `text` ends inside an open code fence.
|
|
89
|
+
* The streaming renderer uses this to decide whether to defer
|
|
90
|
+
* markdown rendering until the fence closes.
|
|
91
|
+
*/
|
|
92
|
+
function endsInsideFence(text) {
|
|
93
|
+
let inFence = false;
|
|
94
|
+
let fenceMarker = null;
|
|
95
|
+
let i = 0;
|
|
96
|
+
const n = text.length;
|
|
97
|
+
while (i < n) {
|
|
98
|
+
if (!inFence) {
|
|
99
|
+
if (text.startsWith('```', i)) {
|
|
100
|
+
inFence = true;
|
|
101
|
+
fenceMarker = '```';
|
|
102
|
+
i += 3;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (text.startsWith('~~~', i)) {
|
|
106
|
+
inFence = true;
|
|
107
|
+
fenceMarker = '~~~';
|
|
108
|
+
i += 3;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (fenceMarker && text.startsWith(fenceMarker, i)) {
|
|
113
|
+
inFence = false;
|
|
114
|
+
fenceMarker = null;
|
|
115
|
+
i += 3;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
return inFence;
|
|
121
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
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/syntaxHighlight.ts — Phase v4.1-reply-formatting
|
|
10
|
+
*
|
|
11
|
+
* Lightweight regex-based syntax highlighter for fenced code blocks.
|
|
12
|
+
* No external dependency. Languages with first-class support:
|
|
13
|
+
* - typescript / javascript / ts / js
|
|
14
|
+
* - python / py
|
|
15
|
+
* - shell / bash / sh / zsh
|
|
16
|
+
* - json
|
|
17
|
+
* - yaml / yml
|
|
18
|
+
*
|
|
19
|
+
* Unknown languages fall through with no highlighting (returns the
|
|
20
|
+
* input verbatim) so the renderer never breaks on exotic fences.
|
|
21
|
+
*
|
|
22
|
+
* Tokenization is order-sensitive: comments/strings first (so a
|
|
23
|
+
* keyword inside a string isn't recoloured), then numbers, then
|
|
24
|
+
* keywords. The regex passes are conservative — they paint by
|
|
25
|
+
* token shape, not by full grammar — but they handle the common
|
|
26
|
+
* 90% case well enough for terminal display.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.highlightCode = highlightCode;
|
|
30
|
+
exports.isSupportedLang = isSupportedLang;
|
|
31
|
+
const skinEngine_1 = require("./skinEngine");
|
|
32
|
+
function normalizeLang(raw) {
|
|
33
|
+
const s = (raw ?? '').toLowerCase().trim();
|
|
34
|
+
if (s === 'ts' || s === 'tsx' || s === 'typescript')
|
|
35
|
+
return 'typescript';
|
|
36
|
+
if (s === 'js' || s === 'jsx' || s === 'javascript')
|
|
37
|
+
return 'javascript';
|
|
38
|
+
if (s === 'py' || s === 'python')
|
|
39
|
+
return 'python';
|
|
40
|
+
if (s === 'sh' || s === 'bash' || s === 'zsh' || s === 'shell')
|
|
41
|
+
return 'shell';
|
|
42
|
+
if (s === 'json')
|
|
43
|
+
return 'json';
|
|
44
|
+
if (s === 'yaml' || s === 'yml')
|
|
45
|
+
return 'yaml';
|
|
46
|
+
return 'unknown';
|
|
47
|
+
}
|
|
48
|
+
const TS_KEYWORDS = new Set([
|
|
49
|
+
'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
|
|
50
|
+
'break', 'continue', 'switch', 'case', 'default', 'async', 'await', 'class',
|
|
51
|
+
'extends', 'implements', 'new', 'this', 'super', 'import', 'export', 'from',
|
|
52
|
+
'as', 'typeof', 'instanceof', 'interface', 'type', 'enum', 'public', 'private',
|
|
53
|
+
'protected', 'readonly', 'static', 'void', 'never', 'any', 'unknown', 'true',
|
|
54
|
+
'false', 'null', 'undefined', 'throw', 'try', 'catch', 'finally', 'yield',
|
|
55
|
+
'in', 'of',
|
|
56
|
+
]);
|
|
57
|
+
const PY_KEYWORDS = new Set([
|
|
58
|
+
'def', 'return', 'if', 'elif', 'else', 'for', 'while', 'break', 'continue',
|
|
59
|
+
'import', 'from', 'as', 'class', 'pass', 'raise', 'try', 'except', 'finally',
|
|
60
|
+
'with', 'lambda', 'yield', 'global', 'nonlocal', 'True', 'False', 'None',
|
|
61
|
+
'and', 'or', 'not', 'is', 'in', 'async', 'await', 'assert', 'del',
|
|
62
|
+
]);
|
|
63
|
+
const SH_KEYWORDS = new Set([
|
|
64
|
+
'if', 'then', 'else', 'elif', 'fi', 'for', 'do', 'done', 'while', 'until', 'case',
|
|
65
|
+
'esac', 'function', 'return', 'export', 'local', 'readonly', 'in', 'select',
|
|
66
|
+
]);
|
|
67
|
+
/** ANSI-paint a token with the active skin's colour kind. */
|
|
68
|
+
function paint(text, kind) {
|
|
69
|
+
return (0, skinEngine_1.getSkinEngine)().applyColors(text, kind);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Walk `code` building an output string. Identifies regions by a
|
|
73
|
+
* simple state machine: STRING → COMMENT → NUMBER → IDENT (which
|
|
74
|
+
* may be a keyword). Anything else is emitted verbatim.
|
|
75
|
+
*
|
|
76
|
+
* Returns the painted string. Pure — no side effects.
|
|
77
|
+
*/
|
|
78
|
+
function highlightTsJs(code, kw) {
|
|
79
|
+
let out = '';
|
|
80
|
+
let i = 0;
|
|
81
|
+
const n = code.length;
|
|
82
|
+
while (i < n) {
|
|
83
|
+
const c = code[i];
|
|
84
|
+
// Single-line comment
|
|
85
|
+
if (c === '/' && code[i + 1] === '/') {
|
|
86
|
+
const end = code.indexOf('\n', i);
|
|
87
|
+
const stop = end === -1 ? n : end;
|
|
88
|
+
out += paint(code.slice(i, stop), 'muted');
|
|
89
|
+
i = stop;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// Block comment
|
|
93
|
+
if (c === '/' && code[i + 1] === '*') {
|
|
94
|
+
const end = code.indexOf('*/', i);
|
|
95
|
+
const stop = end === -1 ? n : end + 2;
|
|
96
|
+
out += paint(code.slice(i, stop), 'muted');
|
|
97
|
+
i = stop;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Strings (', ", `)
|
|
101
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
102
|
+
const quote = c;
|
|
103
|
+
let j = i + 1;
|
|
104
|
+
while (j < n && code[j] !== quote) {
|
|
105
|
+
if (code[j] === '\\' && j + 1 < n) {
|
|
106
|
+
j += 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
j += 1;
|
|
110
|
+
}
|
|
111
|
+
const stop = j < n ? j + 1 : n;
|
|
112
|
+
out += paint(code.slice(i, stop), 'success');
|
|
113
|
+
i = stop;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Numbers
|
|
117
|
+
if (/[0-9]/.test(c) && (i === 0 || !/[a-zA-Z_$]/.test(code[i - 1] ?? ''))) {
|
|
118
|
+
let j = i;
|
|
119
|
+
while (j < n && /[0-9.eExX_a-fA-F]/.test(code[j] ?? ''))
|
|
120
|
+
j += 1;
|
|
121
|
+
out += paint(code.slice(i, j), 'accent');
|
|
122
|
+
i = j;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Identifiers / keywords
|
|
126
|
+
if (/[a-zA-Z_$]/.test(c)) {
|
|
127
|
+
let j = i;
|
|
128
|
+
while (j < n && /[a-zA-Z0-9_$]/.test(code[j] ?? ''))
|
|
129
|
+
j += 1;
|
|
130
|
+
const word = code.slice(i, j);
|
|
131
|
+
out += kw.has(word) ? paint(word, 'brand') : word;
|
|
132
|
+
i = j;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out += c;
|
|
136
|
+
i += 1;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function highlightPython(code) {
|
|
141
|
+
// Python: # comments, '/" strings, numbers, keywords. Same engine
|
|
142
|
+
// as TS/JS but with a `#` comment and Python keyword set.
|
|
143
|
+
let out = '';
|
|
144
|
+
let i = 0;
|
|
145
|
+
const n = code.length;
|
|
146
|
+
while (i < n) {
|
|
147
|
+
const c = code[i];
|
|
148
|
+
if (c === '#') {
|
|
149
|
+
const end = code.indexOf('\n', i);
|
|
150
|
+
const stop = end === -1 ? n : end;
|
|
151
|
+
out += paint(code.slice(i, stop), 'muted');
|
|
152
|
+
i = stop;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (c === '"' || c === "'") {
|
|
156
|
+
const quote = c;
|
|
157
|
+
// Triple-quote support
|
|
158
|
+
const triple = code[i + 1] === quote && code[i + 2] === quote;
|
|
159
|
+
let j = triple ? i + 3 : i + 1;
|
|
160
|
+
const closer = triple ? quote.repeat(3) : quote;
|
|
161
|
+
while (j < n) {
|
|
162
|
+
if (code.startsWith(closer, j)) {
|
|
163
|
+
j += closer.length;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
if (code[j] === '\\' && j + 1 < n) {
|
|
167
|
+
j += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
j += 1;
|
|
171
|
+
}
|
|
172
|
+
out += paint(code.slice(i, j), 'success');
|
|
173
|
+
i = j;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/[0-9]/.test(c) && (i === 0 || !/[a-zA-Z_]/.test(code[i - 1] ?? ''))) {
|
|
177
|
+
let j = i;
|
|
178
|
+
while (j < n && /[0-9._jJ]/.test(code[j] ?? ''))
|
|
179
|
+
j += 1;
|
|
180
|
+
out += paint(code.slice(i, j), 'accent');
|
|
181
|
+
i = j;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
185
|
+
let j = i;
|
|
186
|
+
while (j < n && /[a-zA-Z0-9_]/.test(code[j] ?? ''))
|
|
187
|
+
j += 1;
|
|
188
|
+
const word = code.slice(i, j);
|
|
189
|
+
out += PY_KEYWORDS.has(word) ? paint(word, 'brand') : word;
|
|
190
|
+
i = j;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
out += c;
|
|
194
|
+
i += 1;
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function highlightShell(code) {
|
|
199
|
+
let out = '';
|
|
200
|
+
let i = 0;
|
|
201
|
+
const n = code.length;
|
|
202
|
+
while (i < n) {
|
|
203
|
+
const c = code[i];
|
|
204
|
+
if (c === '#' && (i === 0 || code[i - 1] === '\n' || code[i - 1] === ' ')) {
|
|
205
|
+
const end = code.indexOf('\n', i);
|
|
206
|
+
const stop = end === -1 ? n : end;
|
|
207
|
+
out += paint(code.slice(i, stop), 'muted');
|
|
208
|
+
i = stop;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (c === '"' || c === "'") {
|
|
212
|
+
const quote = c;
|
|
213
|
+
let j = i + 1;
|
|
214
|
+
while (j < n && code[j] !== quote) {
|
|
215
|
+
if (code[j] === '\\' && j + 1 < n) {
|
|
216
|
+
j += 2;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
j += 1;
|
|
220
|
+
}
|
|
221
|
+
const stop = j < n ? j + 1 : n;
|
|
222
|
+
out += paint(code.slice(i, stop), 'success');
|
|
223
|
+
i = stop;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (/[a-zA-Z_]/.test(c)) {
|
|
227
|
+
let j = i;
|
|
228
|
+
while (j < n && /[a-zA-Z0-9_]/.test(code[j] ?? ''))
|
|
229
|
+
j += 1;
|
|
230
|
+
const word = code.slice(i, j);
|
|
231
|
+
out += SH_KEYWORDS.has(word) ? paint(word, 'brand') : word;
|
|
232
|
+
i = j;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out += c;
|
|
236
|
+
i += 1;
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
function highlightJson(code) {
|
|
241
|
+
// JSON has only strings, numbers, true/false/null, and structure.
|
|
242
|
+
let out = '';
|
|
243
|
+
let i = 0;
|
|
244
|
+
const n = code.length;
|
|
245
|
+
while (i < n) {
|
|
246
|
+
const c = code[i];
|
|
247
|
+
if (c === '"') {
|
|
248
|
+
let j = i + 1;
|
|
249
|
+
while (j < n && code[j] !== '"') {
|
|
250
|
+
if (code[j] === '\\' && j + 1 < n) {
|
|
251
|
+
j += 2;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
j += 1;
|
|
255
|
+
}
|
|
256
|
+
const stop = j < n ? j + 1 : n;
|
|
257
|
+
// Treat key (followed by ':') vs value distinct.
|
|
258
|
+
// Skip whitespace after to peek at the next non-space char.
|
|
259
|
+
let k = stop;
|
|
260
|
+
while (k < n && /\s/.test(code[k] ?? ''))
|
|
261
|
+
k += 1;
|
|
262
|
+
const isKey = code[k] === ':';
|
|
263
|
+
out += paint(code.slice(i, stop), isKey ? 'heading' : 'success');
|
|
264
|
+
i = stop;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (/[0-9-]/.test(c) && /[0-9]/.test(code[i + 1] ?? c)) {
|
|
268
|
+
let j = i;
|
|
269
|
+
while (j < n && /[0-9.eE+-]/.test(code[j] ?? ''))
|
|
270
|
+
j += 1;
|
|
271
|
+
out += paint(code.slice(i, j), 'accent');
|
|
272
|
+
i = j;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (/[a-zA-Z]/.test(c)) {
|
|
276
|
+
let j = i;
|
|
277
|
+
while (j < n && /[a-zA-Z]/.test(code[j] ?? ''))
|
|
278
|
+
j += 1;
|
|
279
|
+
const word = code.slice(i, j);
|
|
280
|
+
if (word === 'true' || word === 'false' || word === 'null') {
|
|
281
|
+
out += paint(word, 'brand');
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
out += word;
|
|
285
|
+
}
|
|
286
|
+
i = j;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
out += c;
|
|
290
|
+
i += 1;
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
function highlightYaml(code) {
|
|
295
|
+
// Simple per-line: comments to '#', `key:` keys, strings.
|
|
296
|
+
return code
|
|
297
|
+
.split('\n')
|
|
298
|
+
.map((line) => {
|
|
299
|
+
const trimStart = line.match(/^\s*/)?.[0] ?? '';
|
|
300
|
+
const rest = line.slice(trimStart.length);
|
|
301
|
+
if (rest.startsWith('#'))
|
|
302
|
+
return trimStart + paint(rest, 'muted');
|
|
303
|
+
// key:
|
|
304
|
+
const m = /^([A-Za-z_][\w-]*)(\s*):(\s*)(.*)$/.exec(rest);
|
|
305
|
+
if (m) {
|
|
306
|
+
const [, key, padBeforeColon, padAfter, value] = m;
|
|
307
|
+
const valOut = /^['"]/.test(value)
|
|
308
|
+
? paint(value, 'success')
|
|
309
|
+
: /^-?\d/.test(value)
|
|
310
|
+
? paint(value, 'accent')
|
|
311
|
+
: value === 'true' || value === 'false' || value === 'null'
|
|
312
|
+
? paint(value, 'brand')
|
|
313
|
+
: value;
|
|
314
|
+
return `${trimStart}${paint(key, 'heading')}${padBeforeColon}:${padAfter}${valOut}`;
|
|
315
|
+
}
|
|
316
|
+
// Bullet lists
|
|
317
|
+
if (rest.startsWith('- ')) {
|
|
318
|
+
return trimStart + paint('-', 'muted') + ' ' + rest.slice(2);
|
|
319
|
+
}
|
|
320
|
+
return line;
|
|
321
|
+
})
|
|
322
|
+
.join('\n');
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Highlight `code` according to `lang`. Returns the painted string.
|
|
326
|
+
* If `lang` is unknown or empty, returns the input verbatim.
|
|
327
|
+
*/
|
|
328
|
+
function highlightCode(code, lang) {
|
|
329
|
+
const norm = normalizeLang(lang);
|
|
330
|
+
switch (norm) {
|
|
331
|
+
case 'typescript':
|
|
332
|
+
case 'javascript':
|
|
333
|
+
return highlightTsJs(code, TS_KEYWORDS);
|
|
334
|
+
case 'python': return highlightPython(code);
|
|
335
|
+
case 'shell': return highlightShell(code);
|
|
336
|
+
case 'json': return highlightJson(code);
|
|
337
|
+
case 'yaml': return highlightYaml(code);
|
|
338
|
+
case 'unknown':
|
|
339
|
+
default: return code;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** Tiny helper for tests. */
|
|
343
|
+
function isSupportedLang(lang) {
|
|
344
|
+
return normalizeLang(lang) !== 'unknown';
|
|
345
|
+
}
|