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,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
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
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/table.ts — lightweight ASCII table renderer (Tier-3.1).
|
|
10
|
+
*
|
|
11
|
+
* Drop-in replacement for `Display.twoColumnBlock` style output at
|
|
12
|
+
* call sites that want full multi-column tables (`/skills`,
|
|
13
|
+
* `/cron list`, `/channel list`). No `cli-table3` dependency — the
|
|
14
|
+
* renderer is ~150 lines, ANSI-aware via `string-width`, and uses
|
|
15
|
+
* the same SkinEngine colour kinds as the rest of v4.
|
|
16
|
+
*
|
|
17
|
+
* Box drawing is sharp ASCII (`─ │ ┌ ┐ └ ┘ ├ ┤`) to stay aligned
|
|
18
|
+
* with the rest of the v4.1-tier3.1 box pass.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.renderTable = renderTable;
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
23
|
+
const stringWidth = require('string-width');
|
|
24
|
+
const skinEngine_1 = require("./skinEngine");
|
|
25
|
+
const box_1 = require("./box");
|
|
26
|
+
/**
|
|
27
|
+
* Visible (post-ANSI-strip) column width. Falls back to
|
|
28
|
+
* `visibleLength` from box.ts when string-width is unavailable
|
|
29
|
+
* (which would only happen if the dep was removed).
|
|
30
|
+
*/
|
|
31
|
+
function vWidth(s) {
|
|
32
|
+
try {
|
|
33
|
+
return stringWidth(s);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return (0, box_1.visibleLength)(s);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Pad `s` to `w` visible columns using `align`. ANSI-safe. */
|
|
40
|
+
function pad(s, w, align = 'left') {
|
|
41
|
+
const sw = vWidth(s);
|
|
42
|
+
if (sw >= w)
|
|
43
|
+
return s;
|
|
44
|
+
const gap = w - sw;
|
|
45
|
+
if (align === 'right')
|
|
46
|
+
return ' '.repeat(gap) + s;
|
|
47
|
+
if (align === 'center') {
|
|
48
|
+
const l = Math.floor(gap / 2);
|
|
49
|
+
return ' '.repeat(l) + s + ' '.repeat(gap - l);
|
|
50
|
+
}
|
|
51
|
+
return s + ' '.repeat(gap);
|
|
52
|
+
}
|
|
53
|
+
/** Truncate to `max` visible columns with a single `…` tail. */
|
|
54
|
+
function truncCell(s, max) {
|
|
55
|
+
if (vWidth(s) <= max)
|
|
56
|
+
return s;
|
|
57
|
+
if (max <= 1)
|
|
58
|
+
return '…';
|
|
59
|
+
return (0, box_1.truncateVisible)(s, max - 1) + '…';
|
|
60
|
+
}
|
|
61
|
+
/** Resolve a column's display string for one row. */
|
|
62
|
+
function cellValue(row, col) {
|
|
63
|
+
const raw = row[col.key];
|
|
64
|
+
const v = col.format ? col.format(raw, row) : (raw == null ? '' : String(raw));
|
|
65
|
+
if (col.truncate && vWidth(v) > col.truncate) {
|
|
66
|
+
return truncCell(v, col.truncate);
|
|
67
|
+
}
|
|
68
|
+
return v;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Tier-3.1b: word-boundary-aware truncate. Tries to cut at the last
|
|
72
|
+
* space inside `[max*0.5, max-1]` and append `…`. Falls back to the
|
|
73
|
+
* dumb mid-word cut when no space lives in that range. Never produces
|
|
74
|
+
* a result wider than `max`.
|
|
75
|
+
*/
|
|
76
|
+
function smartTrunc(s, max) {
|
|
77
|
+
if (vWidth(s) <= max)
|
|
78
|
+
return s;
|
|
79
|
+
if (max <= 1)
|
|
80
|
+
return '…';
|
|
81
|
+
const candidate = (0, box_1.truncateVisible)(s, max - 1);
|
|
82
|
+
// Word-boundary search — only honour spaces that leave at least
|
|
83
|
+
// half the column populated, otherwise the cell looks empty.
|
|
84
|
+
const lastSpace = candidate.lastIndexOf(' ');
|
|
85
|
+
if (lastSpace >= Math.floor(max * 0.5)) {
|
|
86
|
+
return candidate.slice(0, lastSpace) + '…';
|
|
87
|
+
}
|
|
88
|
+
return candidate + '…';
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Tier-3.1b: allocate per-column widths to fit `available` chars.
|
|
92
|
+
* Non-flex columns prefer their natural width; flex columns absorb
|
|
93
|
+
* the leftover space proportional to their natural sizes. When even
|
|
94
|
+
* fixed columns overflow, every column is shrunk proportionally with
|
|
95
|
+
* a hard floor of 8 chars per column.
|
|
96
|
+
*/
|
|
97
|
+
function allocateWidths(cols, natural, available) {
|
|
98
|
+
const numCols = cols.length;
|
|
99
|
+
const totalNatural = natural.reduce((a, b) => a + b, 0);
|
|
100
|
+
if (totalNatural <= available)
|
|
101
|
+
return natural.slice();
|
|
102
|
+
// If any column declared flex:true, treat those as flex; otherwise
|
|
103
|
+
// the last column carries the flex flag (description-most case).
|
|
104
|
+
const explicitFlex = cols.some((c) => c.flex === true);
|
|
105
|
+
const flexFlags = cols.map((c, i) => explicitFlex ? c.flex === true : i === numCols - 1);
|
|
106
|
+
const fixedSum = natural.reduce((s, w, i) => s + (flexFlags[i] ? 0 : w), 0);
|
|
107
|
+
const flexNaturalSum = natural.reduce((s, w, i) => s + (flexFlags[i] ? w : 0), 0);
|
|
108
|
+
if (fixedSum >= available || flexNaturalSum === 0) {
|
|
109
|
+
// Even fixed columns don't fit — proportional shrink everything.
|
|
110
|
+
const ratio = available / Math.max(1, totalNatural);
|
|
111
|
+
return natural.map((w) => Math.max(8, Math.floor(w * ratio)));
|
|
112
|
+
}
|
|
113
|
+
const remainingForFlex = available - fixedSum;
|
|
114
|
+
return natural.map((w, i) => {
|
|
115
|
+
if (!flexFlags[i])
|
|
116
|
+
return w;
|
|
117
|
+
return Math.max(8, Math.floor(remainingForFlex * (w / flexNaturalSum)));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Render `rows` as an ASCII table. Returns the multi-line string
|
|
122
|
+
* (with a trailing `\n`); caller writes it via the display.
|
|
123
|
+
*/
|
|
124
|
+
function renderTable(rows, cols, opts = {}) {
|
|
125
|
+
const skin = (0, skinEngine_1.getSkinEngine)();
|
|
126
|
+
const indent = opts.indent ?? 2;
|
|
127
|
+
const showRule = opts.showHeaderRule !== false;
|
|
128
|
+
// Pre-compute uncoloured cell values so width math sees exact text.
|
|
129
|
+
const valueGrid = rows.map((row) => cols.map((c) => cellValue(row, c)));
|
|
130
|
+
// Natural widths — max(header, longest cell, minWidth).
|
|
131
|
+
const naturalWidths = cols.map((c, i) => {
|
|
132
|
+
let w = vWidth(c.header);
|
|
133
|
+
for (const rowVals of valueGrid) {
|
|
134
|
+
const cw = vWidth(rowVals[i]);
|
|
135
|
+
if (cw > w)
|
|
136
|
+
w = cw;
|
|
137
|
+
}
|
|
138
|
+
if (c.minWidth && c.minWidth > w)
|
|
139
|
+
w = c.minWidth;
|
|
140
|
+
return w;
|
|
141
|
+
});
|
|
142
|
+
// Tier-3.1b: responsive width allocation. Total table chars =
|
|
143
|
+
// indent + 1 (left border) + sum(width+2) + (numCols-1) inner
|
|
144
|
+
// separators + 1 (right border). Solve for content budget given
|
|
145
|
+
// the caller-provided maxWidth (or terminal columns).
|
|
146
|
+
const numCols = cols.length;
|
|
147
|
+
const overhead = indent + 3 * numCols + 1;
|
|
148
|
+
// Honor an explicit override first, then the live TTY width, then
|
|
149
|
+
// the COLUMNS env var (set by `term`-aware shells and most spawned
|
|
150
|
+
// subprocess wrappers — process.stdout.columns is `undefined` when
|
|
151
|
+
// stdout is a pipe, so falling back to env keeps tables responsive
|
|
152
|
+
// for piped consumers like /ui dashboards). Final fallback: 100.
|
|
153
|
+
const envCols = process.env.COLUMNS ? parseInt(process.env.COLUMNS, 10) : 0;
|
|
154
|
+
const maxWidth = opts.maxWidth ??
|
|
155
|
+
process.stdout.columns ??
|
|
156
|
+
(envCols > 0 ? envCols : 100);
|
|
157
|
+
const availableForContent = Math.max(numCols * 8, maxWidth - overhead);
|
|
158
|
+
const widths = allocateWidths(cols, naturalWidths, availableForContent);
|
|
159
|
+
// Apply smart truncation to any cell whose content exceeds its
|
|
160
|
+
// allocated width. Non-flex columns at natural width never trigger
|
|
161
|
+
// this branch; flex columns may.
|
|
162
|
+
for (let i = 0; i < numCols; i += 1) {
|
|
163
|
+
const w = widths[i];
|
|
164
|
+
for (const rowVals of valueGrid) {
|
|
165
|
+
if (vWidth(rowVals[i]) > w) {
|
|
166
|
+
rowVals[i] = smartTrunc(rowVals[i], w);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Border characters (sharp ASCII).
|
|
171
|
+
const TL = '┌', TR = '┐', BL = '└', BR = '┘';
|
|
172
|
+
const T = '┬', B = '┴', L = '├', R = '┤';
|
|
173
|
+
const X = '┼', H = '─', V = '│';
|
|
174
|
+
const ind = ' '.repeat(indent);
|
|
175
|
+
// Top border.
|
|
176
|
+
const top = TL + widths.map((w) => H.repeat(w + 2)).join(T) + TR;
|
|
177
|
+
// Header row — heading colour, padded. Truncate first if the
|
|
178
|
+
// header itself is wider than the allocated width (rare, but
|
|
179
|
+
// keeps borders aligned under aggressive narrow-width pressure).
|
|
180
|
+
const headerCells = cols.map((c, i) => {
|
|
181
|
+
const w = widths[i];
|
|
182
|
+
const text = vWidth(c.header) > w ? smartTrunc(c.header, w) : c.header;
|
|
183
|
+
const padded = pad(text, w, c.align ?? 'left');
|
|
184
|
+
return ' ' + skin.applyColors(padded, 'heading') + ' ';
|
|
185
|
+
});
|
|
186
|
+
const headerRow = V + headerCells.join(V) + V;
|
|
187
|
+
// Header rule.
|
|
188
|
+
const rule = L + widths.map((w) => H.repeat(w + 2)).join(X) + R;
|
|
189
|
+
// Body rows.
|
|
190
|
+
const bodyLines = [];
|
|
191
|
+
const compact = opts.compact === true;
|
|
192
|
+
valueGrid.forEach((rowVals, rIdx) => {
|
|
193
|
+
if (!compact && rIdx > 0) {
|
|
194
|
+
// Tier-3.1a: inter-row separator using `├─…─┼─…─┤` glyphs.
|
|
195
|
+
bodyLines.push(L + widths.map((w) => H.repeat(w + 2)).join(X) + R);
|
|
196
|
+
}
|
|
197
|
+
const cells = cols.map((c, i) => {
|
|
198
|
+
const raw = rowVals[i];
|
|
199
|
+
const padded = pad(raw, widths[i], c.align ?? 'left');
|
|
200
|
+
const colorKind = c.color ? c.color(rows[rIdx][c.key], rows[rIdx]) : undefined;
|
|
201
|
+
const painted = colorKind ? skin.applyColors(padded, colorKind) : padded;
|
|
202
|
+
return ' ' + painted + ' ';
|
|
203
|
+
});
|
|
204
|
+
bodyLines.push(V + cells.join(V) + V);
|
|
205
|
+
});
|
|
206
|
+
// Bottom border.
|
|
207
|
+
const bot = BL + widths.map((w) => H.repeat(w + 2)).join(B) + BR;
|
|
208
|
+
const allLines = [
|
|
209
|
+
top,
|
|
210
|
+
headerRow,
|
|
211
|
+
...(showRule ? [rule] : []),
|
|
212
|
+
...bodyLines,
|
|
213
|
+
bot,
|
|
214
|
+
].map((l) => ind + l);
|
|
215
|
+
return allLines.join('\n') + '\n';
|
|
216
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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/themeDetect.ts — Phase v4.1-tier3-essentials
|
|
10
|
+
*
|
|
11
|
+
* Multi-signal auto theme detection. The skin engine consults this
|
|
12
|
+
* once at boot when the configured skin is `auto`; otherwise the
|
|
13
|
+
* explicitly-named skin wins.
|
|
14
|
+
*
|
|
15
|
+
* Priority order (first non-undefined hit wins):
|
|
16
|
+
*
|
|
17
|
+
* 1. AIDEN_THEME=light|dark — explicit override
|
|
18
|
+
* 2. AIDEN_THEME=auto / unset goes through 2..5
|
|
19
|
+
* 3. NO_COLOR set — forced monochrome
|
|
20
|
+
* 4. COLORFGBG="<fg>;<bg>" — slot 7 or 15 = light, others = dark
|
|
21
|
+
* 5. TERM_PROGRAM allow-list — Apple_Terminal default to light
|
|
22
|
+
* 6. Fallback: dark
|
|
23
|
+
*
|
|
24
|
+
* Returns 'light' / 'dark' / 'mono'. The skin engine maps:
|
|
25
|
+
* 'mono' → monochrome skin (no colour)
|
|
26
|
+
* 'light' → light skin
|
|
27
|
+
* 'dark' → default skin
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.detectTheme = detectTheme;
|
|
31
|
+
exports.detectedToSkinName = detectedToSkinName;
|
|
32
|
+
const LIGHT_DEFAULT_TERM_PROGRAMS = new Set([
|
|
33
|
+
// Apple Terminal default profile is on a light background.
|
|
34
|
+
'Apple_Terminal',
|
|
35
|
+
]);
|
|
36
|
+
/**
|
|
37
|
+
* Run the multi-signal detection. Pure function — env can be
|
|
38
|
+
* overridden for tests.
|
|
39
|
+
*/
|
|
40
|
+
function detectTheme(env = process.env) {
|
|
41
|
+
const explicit = (env.AIDEN_THEME ?? '').trim().toLowerCase();
|
|
42
|
+
if (explicit === 'light')
|
|
43
|
+
return 'light';
|
|
44
|
+
if (explicit === 'dark')
|
|
45
|
+
return 'dark';
|
|
46
|
+
if (explicit === 'mono' || explicit === 'monochrome')
|
|
47
|
+
return 'mono';
|
|
48
|
+
// NO_COLOR (https://no-color.org) — monochrome is its own theme,
|
|
49
|
+
// independent of light/dark, so it wins over the auto path.
|
|
50
|
+
if (env.NO_COLOR != null && env.NO_COLOR !== '')
|
|
51
|
+
return 'mono';
|
|
52
|
+
// COLORFGBG = "<fg>;<bg>" where slot 7 (light grey) or 15 (white)
|
|
53
|
+
// signals a light terminal background. Other slots = dark.
|
|
54
|
+
const colorfgbg = (env.COLORFGBG ?? '').trim();
|
|
55
|
+
if (colorfgbg) {
|
|
56
|
+
const parts = colorfgbg.split(';');
|
|
57
|
+
const lastField = parts[parts.length - 1] ?? '';
|
|
58
|
+
if (/^\d+$/.test(lastField)) {
|
|
59
|
+
const bg = Number(lastField);
|
|
60
|
+
if (bg === 7 || bg === 15)
|
|
61
|
+
return 'light';
|
|
62
|
+
if (bg >= 0 && bg < 16)
|
|
63
|
+
return 'dark';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// TERM_PROGRAM allow-list.
|
|
67
|
+
const termProgram = (env.TERM_PROGRAM ?? '').trim();
|
|
68
|
+
if (LIGHT_DEFAULT_TERM_PROGRAMS.has(termProgram))
|
|
69
|
+
return 'light';
|
|
70
|
+
// Fallback.
|
|
71
|
+
return 'dark';
|
|
72
|
+
}
|
|
73
|
+
/** Surfaced for skinEngine integration: maps DetectedTheme → skin name. */
|
|
74
|
+
function detectedToSkinName(theme) {
|
|
75
|
+
switch (theme) {
|
|
76
|
+
case 'light': return 'light';
|
|
77
|
+
case 'mono': return 'monochrome';
|
|
78
|
+
case 'dark':
|
|
79
|
+
default: return 'default';
|
|
80
|
+
}
|
|
81
|
+
}
|