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.
Files changed (112) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +513 -14
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +269 -52
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. package/package.json +11 -2
@@ -0,0 +1,317 @@
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/aidenPrompt.ts — Tier-3.1.1 (v4.1-tier3.1.1)
10
+ *
11
+ * Custom @inquirer/core prompt component that combines:
12
+ * - Standard text input (inquirer.input parity)
13
+ * - Ghost-text overlay for matching slash commands / history
14
+ * - Slash dropdown with ↑/↓ nav, Esc dismiss, description column
15
+ * - History suggestions for non-slash text
16
+ * - Cooperation with pasteIntercept (ghost disabled when a paste
17
+ * label is in the buffer)
18
+ *
19
+ * The prompt returns the typed text on Enter (NOT the ghost). The
20
+ * user must explicitly accept the ghost via Right-arrow or Tab.
21
+ *
22
+ * MCP serve mode never reaches this path — the REPL is gated on
23
+ * `process.stdout.isTTY` and serve mode runs over JSON-RPC stdio.
24
+ *
25
+ * `--no-ui`: the chatSession owner consults `isNoUiMode()` and
26
+ * falls back to the legacy inquirer prompt path when the env-var
27
+ * is set.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ const core_1 = require("@inquirer/core");
31
+ const ghostMatch_1 = require("./ghostMatch");
32
+ const skinEngine_1 = require("./skinEngine");
33
+ const DEFAULT_DROPDOWN_LIMIT = 8;
34
+ /** Strip ANSI for width math. */
35
+ function stripAnsi(s) {
36
+ return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
37
+ }
38
+ /** Visible width — ignore ANSI escape sequences. */
39
+ function vWidth(s) {
40
+ return stripAnsi(s).length;
41
+ }
42
+ /** Build an SGR span using the active skin. */
43
+ function dim(s) {
44
+ return (0, skinEngine_1.getSkinEngine)().applyColors(s, 'muted');
45
+ }
46
+ /** Render a single dropdown row with right-aligned dim description. */
47
+ function renderDropdownRow(cmd, selected, width) {
48
+ const sk = (0, skinEngine_1.getSkinEngine)();
49
+ const marker = selected ? '▸ ' : ' ';
50
+ const nameCell = `/${cmd.name}`;
51
+ const desc = cmd.description ?? '';
52
+ // Reserve 2 chars for marker + 2-space pad before the desc column.
53
+ // The desc column is right-aligned and dim-coloured.
54
+ const lhs = `${marker}${nameCell}`;
55
+ const lhsWidth = vWidth(lhs);
56
+ const padBetween = Math.max(2, width - lhsWidth - vWidth(desc));
57
+ const truncatedDesc = vWidth(desc) > width - lhsWidth - 2
58
+ ? desc.slice(0, Math.max(0, width - lhsWidth - 3)) + '…'
59
+ : desc;
60
+ const dimDesc = sk.applyColors(truncatedDesc, 'muted');
61
+ const painted = selected
62
+ ? sk.applyColors(lhs, 'brand')
63
+ : lhs;
64
+ return painted + ' '.repeat(padBetween) + dimDesc;
65
+ }
66
+ /** Default 3-tier filter — matches commandRegistry.filter shape. */
67
+ function defaultFilter(cmds, input) {
68
+ const stem = input.startsWith('/') ? input.slice(1) : input;
69
+ const lower = stem.toLowerCase();
70
+ if (!lower)
71
+ return cmds.filter((c) => !c.hidden);
72
+ const visible = cmds.filter((c) => !c.hidden);
73
+ const prefix = visible.filter((c) => c.name.toLowerCase().startsWith(lower) ||
74
+ (c.aliases ?? []).some((a) => a.toLowerCase().startsWith(lower)));
75
+ if (prefix.length > 0)
76
+ return prefix;
77
+ const substring = visible.filter((c) => c.name.toLowerCase().includes(lower) ||
78
+ (c.aliases ?? []).some((a) => a.toLowerCase().includes(lower)));
79
+ if (substring.length > 0)
80
+ return substring;
81
+ return visible.filter((c) => (c.description ?? '').toLowerCase().includes(lower));
82
+ }
83
+ /**
84
+ * The prompt itself. Resolves with the literal user-typed text on
85
+ * Enter. If the dropdown is open and the user pressed Enter on a
86
+ * highlighted row, resolves with `/<row.name>` instead.
87
+ */
88
+ exports.default = (0, core_1.createPrompt)((config, done) => {
89
+ const theme = (0, core_1.makeTheme)({}, config.theme);
90
+ const [status, setStatus] = (0, core_1.useState)('idle');
91
+ const [value, setValue] = (0, core_1.useState)('');
92
+ const [ghost, setGhost] = (0, core_1.useState)(null);
93
+ const [dropdownOpen, setDropdownOpen] = (0, core_1.useState)(false);
94
+ const [selectedIdx, setSelectedIdx] = (0, core_1.useState)(0);
95
+ const [historyIdx, setHistoryIdx] = (0, core_1.useState)(null);
96
+ // Snapshot of the typed value when the user starts navigating
97
+ // history — restored when they reach the bottom of the stack.
98
+ const historyDraftRef = (0, core_1.useRef)('');
99
+ // Tier-3.1c: paste-burst guard. When bracketed paste mode isn't
100
+ // honoured by the terminal (no CSI 200~/201~ wrap) the paste
101
+ // arrives as raw bytes; the first internal `\n` becomes an Enter
102
+ // event and submits before the user can review. We can't always
103
+ // suppress that at the stdin level, so as a defence-in-depth the
104
+ // prompt records the timestamp of the last NON-Enter keypress and
105
+ // refuses to submit on Enter that arrives within
106
+ // PASTE_BURST_GUARD_MS of it. Real user Enter comes after a
107
+ // pause, so this only blocks the rapid Enter-from-paste path.
108
+ const lastNonEnterKeyMsRef = (0, core_1.useRef)(0);
109
+ const prefix = (0, core_1.usePrefix)({ status, theme });
110
+ const dropdownLimit = config.dropdownLimit ?? DEFAULT_DROPDOWN_LIMIT;
111
+ const filterFn = config.filter
112
+ ?? ((input) => defaultFilter(config.commands, input));
113
+ /** Recompute ghost + dropdown for a new value. */
114
+ function rederive(next) {
115
+ setValue(next);
116
+ setGhost((0, ghostMatch_1.findGhost)(next, {
117
+ slashNames: config.commands.map((c) => c.name),
118
+ slashAliases: config.commands.flatMap((c) => [...(c.aliases ?? [])]),
119
+ history: config.history,
120
+ }));
121
+ if (next.startsWith('/')) {
122
+ const matches = filterFn(next);
123
+ const open = matches.length > 0;
124
+ setDropdownOpen(open);
125
+ if (open)
126
+ setSelectedIdx(Math.min(selectedIdx, matches.length - 1));
127
+ }
128
+ else {
129
+ setDropdownOpen(false);
130
+ setSelectedIdx(0);
131
+ }
132
+ }
133
+ (0, core_1.useEffect)((rl) => {
134
+ // Initial sync — empty.
135
+ setValue('');
136
+ setGhost(null);
137
+ setDropdownOpen(false);
138
+ void rl;
139
+ }, []);
140
+ (0, core_1.useKeypress)((key, rl) => {
141
+ if (status !== 'idle')
142
+ return;
143
+ // ── Submit ──
144
+ if ((0, core_1.isEnterKey)(key)) {
145
+ // Tier-3.1c: paste-burst guard. If a non-Enter keystroke fired
146
+ // within the last 50ms, this Enter is almost certainly an
147
+ // internal `\n` from an unbracketed paste, not a deliberate
148
+ // user submit. Suppress and let readline keep accumulating
149
+ // bytes — the user will press Enter again once the paste
150
+ // settles.
151
+ const PASTE_BURST_GUARD_MS = 50;
152
+ const sinceLastKey = Date.now() - lastNonEnterKeyMsRef.current;
153
+ if (lastNonEnterKeyMsRef.current > 0 && sinceLastKey < PASTE_BURST_GUARD_MS) {
154
+ // Reset so subsequent rapid Enters are also caught while
155
+ // the burst continues.
156
+ lastNonEnterKeyMsRef.current = Date.now();
157
+ // Resync value in case readline already cleared the line
158
+ // on this Enter (it does — we can't fully prevent that, but
159
+ // we keep the buffered fragments in `value` for context).
160
+ rederive(rl.line);
161
+ return;
162
+ }
163
+ if (dropdownOpen) {
164
+ const matches = filterFn(value);
165
+ const picked = matches[selectedIdx];
166
+ if (picked) {
167
+ // Tier-3.1c: preserve typed args. If the user typed
168
+ // `/skills list` we must submit the literal value, not
169
+ // `/skills` alone — the row pick only short-circuits to
170
+ // the command name when the typed value is JUST the
171
+ // (partial) command without args. Detect args via a
172
+ // whitespace boundary inside `value`.
173
+ const hasArgs = /\s/.test(value);
174
+ const out = hasArgs ? value : `/${picked.name}`;
175
+ setStatus('done');
176
+ setValue(out);
177
+ done(out);
178
+ return;
179
+ }
180
+ }
181
+ // Normal submit — return literal typed text (NOT ghost).
182
+ setStatus('done');
183
+ done(value);
184
+ return;
185
+ }
186
+ // ── Esc — dismiss dropdown ──
187
+ if (key.name === 'escape') {
188
+ if (dropdownOpen) {
189
+ setDropdownOpen(false);
190
+ return;
191
+ }
192
+ // No dropdown — let inquirer's default Esc handling run
193
+ // (typically a no-op for an `input`-style prompt).
194
+ return;
195
+ }
196
+ // ── Right / Tab — accept ghost ──
197
+ if ((key.name === 'right' || (0, core_1.isTabKey)(key)) && ghost) {
198
+ // Tab unambiguously accepts. Right-arrow only accepts when the
199
+ // cursor is at the END of the line (otherwise the user is mid-
200
+ // edit and right-arrow should move the cursor normally). Cursor
201
+ // position isn't in @inquirer/type's InquirerReadline shape but
202
+ // is on the underlying node readline — read it via cast.
203
+ const cursorPos = rl.cursor ?? rl.line.length;
204
+ const atEnd = cursorPos === rl.line.length;
205
+ if (atEnd || (0, core_1.isTabKey)(key)) {
206
+ const accepted = value + ghost;
207
+ rl.clearLine(0);
208
+ rl.write(accepted);
209
+ rederive(accepted);
210
+ return;
211
+ }
212
+ }
213
+ // ── ↑/↓ ──
214
+ if (key.name === 'up' || key.name === 'down') {
215
+ if (dropdownOpen) {
216
+ const matches = filterFn(value);
217
+ if (matches.length > 0) {
218
+ if (key.name === 'up') {
219
+ setSelectedIdx((selectedIdx - 1 + matches.length) % matches.length);
220
+ }
221
+ else {
222
+ setSelectedIdx((selectedIdx + 1) % matches.length);
223
+ }
224
+ return;
225
+ }
226
+ }
227
+ // History nav (when dropdown closed). Up = older, Down = newer.
228
+ if (config.history.length > 0) {
229
+ if (key.name === 'up') {
230
+ if (historyIdx === null) {
231
+ historyDraftRef.current = value;
232
+ setHistoryIdx(0);
233
+ const next = config.history[0];
234
+ rl.clearLine(0);
235
+ rl.write(next);
236
+ rederive(next);
237
+ }
238
+ else if (historyIdx + 1 < config.history.length) {
239
+ const ni = historyIdx + 1;
240
+ setHistoryIdx(ni);
241
+ const next = config.history[ni];
242
+ rl.clearLine(0);
243
+ rl.write(next);
244
+ rederive(next);
245
+ }
246
+ }
247
+ else { // down
248
+ if (historyIdx !== null) {
249
+ if (historyIdx === 0) {
250
+ setHistoryIdx(null);
251
+ const draft = historyDraftRef.current;
252
+ rl.clearLine(0);
253
+ rl.write(draft);
254
+ rederive(draft);
255
+ }
256
+ else {
257
+ const ni = historyIdx - 1;
258
+ setHistoryIdx(ni);
259
+ const next = config.history[ni];
260
+ rl.clearLine(0);
261
+ rl.write(next);
262
+ rederive(next);
263
+ }
264
+ }
265
+ }
266
+ return;
267
+ }
268
+ return;
269
+ }
270
+ // ── Backspace fast-path so cursor sync stays clean ──
271
+ if ((0, core_1.isBackspaceKey)(key)) {
272
+ // rl.line already updated by the readline event before this
273
+ // handler runs, so we just resync.
274
+ lastNonEnterKeyMsRef.current = Date.now();
275
+ setHistoryIdx(null);
276
+ rederive(rl.line);
277
+ return;
278
+ }
279
+ // ── Default — sync from rl.line, recompute derived state ──
280
+ // Tier-3.1c: any non-Enter keystroke updates the burst-guard
281
+ // timestamp; the Enter handler reads it to decide whether the
282
+ // submit is a real user Enter or part of a paste burst.
283
+ lastNonEnterKeyMsRef.current = Date.now();
284
+ setHistoryIdx(null);
285
+ rederive(rl.line);
286
+ });
287
+ // ── Render ─────────────────────────────────────────────────────
288
+ const message = theme.style.message(config.message, status);
289
+ let line;
290
+ if (status === 'done') {
291
+ line = `${message} ${theme.style.answer(value)}`;
292
+ }
293
+ else {
294
+ const ghostStr = ghost ? dim(ghost) : '';
295
+ line = `${prefix} ${message}${value}${ghostStr}`;
296
+ }
297
+ // Footer (dropdown). Returning a tuple `[line, footer]` adds the
298
+ // footer below the input line; inquirer takes care of cursor
299
+ // positioning so the cursor stays on `line`.
300
+ let footer;
301
+ if (dropdownOpen && status === 'idle') {
302
+ const matches = filterFn(value);
303
+ if (matches.length > 0) {
304
+ const visibleCols = process.stdout.columns ?? 100;
305
+ const rowWidth = Math.max(40, Math.min(visibleCols - 4, 100));
306
+ const window = matches.slice(0, dropdownLimit);
307
+ // Clamp selectedIdx into the visible window.
308
+ const safeIdx = Math.min(selectedIdx, window.length - 1);
309
+ const rows = window.map((c, i) => renderDropdownRow(c, i === safeIdx, rowWidth));
310
+ const more = matches.length > window.length
311
+ ? ` ${dim(`… ${matches.length - window.length} more`)}`
312
+ : '';
313
+ footer = [...rows, more].filter((r) => r.length > 0).join('\n');
314
+ }
315
+ }
316
+ return footer ? [line, footer] : line;
317
+ });
@@ -6,47 +6,68 @@
6
6
  * Aiden — local-first agent.
7
7
  */
8
8
  /**
9
- * cli/v4/box.ts — rounded-corner box drawing helpers (Phase 22).
9
+ * cli/v4/box.ts — sharp + double-line box drawing helpers.
10
10
  *
11
- * Shared between the REPL boot card (chatSession.ts), the
12
- * setup-complete summary (setupWizard.ts), the /doctor health box
13
- * (doctor.ts), and the approval prompt box (callbacks.ts). Per the
14
- * Aiden uses the rounded set (╭╮╰╯) for box framing — rounded
15
- * reads softer at launch-card scale than square corners.
11
+ * Tier-3.1 (v4.1-tier3.1) replaced the rounded set (╭╮╰╯) with
12
+ * sharp corners (┌┐└┘) for the default box and added a second
13
+ * double-line variant (╔╗╚╝═║) for emphasis surfaces (e.g. the
14
+ * approval/escalation banner). The default `box*` exports continue
15
+ * to point at the sharp variant so existing callers compile
16
+ * unchanged; `boxSharp*` and `boxDouble*` are explicit aliases for
17
+ * call sites that want to declare intent.
16
18
  *
17
19
  * Width counts the inner cell only (between the verticals). Content
18
20
  * is padded to width-1 so a single leading space gives the box a
19
21
  * visual gutter.
20
22
  *
21
- * ANSI awareness (Phase 22 Group C smoke-fix): Group C's per-row
22
- * coloured content (orange icons, soft-cyan labels) inflated
23
- * `string.length` from ~50 visible chars to ~120 bytes per row, so
24
- * the prior byte-based padding under-filled and the closing `│`
25
- * drifted inside the visible box top/bottom borders. The helpers
23
+ * ANSI awareness: per-row coloured content (orange icons, soft-
24
+ * cyan labels) inflates `String.length` from ~50 visible chars to
25
+ * ~120 bytes per row, so byte-based padding under-fills and the
26
+ * closing vertical drifts inside the visible borders. The helpers
26
27
  * below measure / truncate against the visible (post-strip)
27
28
  * length, so coloured content frames identically to plain content.
28
29
  */
29
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.boxSharpTopTitled = exports.boxSharpLine = exports.boxSharpBottom = exports.boxSharpTop = void 0;
30
32
  exports.visibleLength = visibleLength;
31
33
  exports.truncateVisible = truncateVisible;
32
34
  exports.boxTop = boxTop;
33
35
  exports.boxBottom = boxBottom;
34
36
  exports.boxLine = boxLine;
35
37
  exports.boxTopTitled = boxTopTitled;
36
- const TL = '╭';
37
- const TR = '╮';
38
- const BL = '╰';
39
- const BR = '╯';
40
- const H = '─';
41
- const V = '│';
38
+ exports.boxDoubleTop = boxDoubleTop;
39
+ exports.boxDoubleBottom = boxDoubleBottom;
40
+ exports.boxDoubleLine = boxDoubleLine;
41
+ exports.boxDoubleTopTitled = boxDoubleTopTitled;
42
+ exports.boxDouble = boxDouble;
43
+ exports.boxSharp = boxSharp;
44
+ // ── Sharp (default) ──────────────────────────────────────────────
45
+ const SHARP = {
46
+ TL: '┌',
47
+ TR: '┐',
48
+ BL: '└',
49
+ BR: '┘',
50
+ H: '─',
51
+ V: '│',
52
+ };
53
+ // ── Double-line (emphasis) ───────────────────────────────────────
54
+ const DOUBLE = {
55
+ TL: '╔',
56
+ TR: '╗',
57
+ BL: '╚',
58
+ BR: '╝',
59
+ H: '═',
60
+ V: '║',
61
+ };
42
62
  /**
43
63
  * Strip ANSI CSI escape sequences and return the visible length in
44
64
  * Unicode code units (`String.length`). Sufficient for all colour
45
65
  * codes we emit (`\x1b[38;2;r;g;bm`, `\x1b[39m`, `\x1b[0m`, etc.).
46
66
  *
47
67
  * Doesn't try to handle East Asian wide chars / emoji-with-VS16 — we
48
- * use only single-cell glyphs in box content (✓ ⚠ ✗ ⏵ ▶ ⊕). If the
49
- * skill expands in v4.1 to cover wide chars, swap to `string-width`.
68
+ * use only single-cell glyphs in box content (✓ ⚠ ✗ ⏵ ▶ ⊕). Wide-
69
+ * char-aware width is available in `cli/v4/table.ts` via
70
+ * `string-width`, used only by the table renderer.
50
71
  */
51
72
  const ANSI_REGEX = /\x1b\[[0-9;]*[A-Za-z]/g;
52
73
  function visibleLength(s) {
@@ -55,10 +76,8 @@ function visibleLength(s) {
55
76
  /**
56
77
  * Truncate `s` to `maxVisible` visible columns, preserving any ANSI
57
78
  * sequences encountered along the way. When the input contained ANSI
58
- * codes, an SGR reset is appended so the closing `│` doesn't inherit
59
- * the truncated content's colour. Plain-text input is unchanged
60
- * beyond the truncation, so callers building plain rows still see
61
- * exactly `maxVisible` characters back.
79
+ * codes, an SGR reset is appended so the closing vertical doesn't
80
+ * inherit the truncated content's colour.
62
81
  */
63
82
  function truncateVisible(s, maxVisible) {
64
83
  if (visibleLength(s) <= maxVisible)
@@ -84,29 +103,76 @@ function truncateVisible(s, maxVisible) {
84
103
  }
85
104
  return sawAnsi ? out + '\x1b[0m' : out;
86
105
  }
87
- function boxTop(width) {
88
- return TL + H.repeat(width) + TR;
106
+ // ── Generic primitives ───────────────────────────────────────────
107
+ function renderTop(g, width) {
108
+ return g.TL + g.H.repeat(width) + g.TR;
89
109
  }
90
- function boxBottom(width) {
91
- return BL + H.repeat(width) + BR;
110
+ function renderBottom(g, width) {
111
+ return g.BL + g.H.repeat(width) + g.BR;
92
112
  }
93
- function boxLine(content, width) {
113
+ function renderLine(g, content, width) {
94
114
  const inner = ' ' + content;
95
115
  const visible = visibleLength(inner);
96
116
  if (visible >= width) {
97
- return V + truncateVisible(inner, width) + V;
117
+ return g.V + truncateVisible(inner, width) + g.V;
98
118
  }
99
- return V + inner + ' '.repeat(width - visible) + V;
119
+ return g.V + inner + ' '.repeat(width - visible) + g.V;
120
+ }
121
+ function renderTopTitled(g, title, width) {
122
+ const lhs = `${g.TL}${g.H}${g.H} ${title} `;
123
+ const visibleLhs = 2 + 1 + visibleLength(title) + 1;
124
+ const remaining = Math.max(0, width - visibleLhs);
125
+ return `${lhs}${g.H.repeat(remaining)}${g.TR}`;
126
+ }
127
+ // ── Sharp variant (default) ──────────────────────────────────────
128
+ function boxTop(width) {
129
+ return renderTop(SHARP, width);
130
+ }
131
+ function boxBottom(width) {
132
+ return renderBottom(SHARP, width);
133
+ }
134
+ function boxLine(content, width) {
135
+ return renderLine(SHARP, content, width);
136
+ }
137
+ function boxTopTitled(title, width) {
138
+ return renderTopTitled(SHARP, title, width);
139
+ }
140
+ // Explicit sharp aliases (for call sites that want to declare intent).
141
+ exports.boxSharpTop = boxTop;
142
+ exports.boxSharpBottom = boxBottom;
143
+ exports.boxSharpLine = boxLine;
144
+ exports.boxSharpTopTitled = boxTopTitled;
145
+ // ── Double-line variant ──────────────────────────────────────────
146
+ function boxDoubleTop(width) {
147
+ return renderTop(DOUBLE, width);
148
+ }
149
+ function boxDoubleBottom(width) {
150
+ return renderBottom(DOUBLE, width);
151
+ }
152
+ function boxDoubleLine(content, width) {
153
+ return renderLine(DOUBLE, content, width);
154
+ }
155
+ function boxDoubleTopTitled(title, width) {
156
+ return renderTopTitled(DOUBLE, title, width);
100
157
  }
101
158
  /**
102
- * Render a titled box header top border with the title injected just
103
- * after the left corner, e.g. `╭─ Setup Complete ─────╮`. Used for the
104
- * setup-complete summary and the /doctor + approval boxes.
159
+ * Convenience: wrap an array of content rows with double-line
160
+ * borders and an optional title. Returns the full multi-line box
161
+ * as a single string with `\n` separators.
105
162
  */
106
- function boxTopTitled(title, width) {
107
- // Two leading dashes, space, title, space, then fill remaining dashes.
108
- const lhs = `${TL}${H}${H} ${title} `;
109
- const visibleLhs = 2 + 1 + visibleLength(title) + 1; // dashes + space + title + space
110
- const remaining = Math.max(0, width - visibleLhs);
111
- return `${lhs}${H.repeat(remaining)}${TR}`;
163
+ function boxDouble(rows, width, title) {
164
+ const top = title ? boxDoubleTopTitled(title, width) : boxDoubleTop(width);
165
+ const body = rows.map((r) => boxDoubleLine(r, width)).join('\n');
166
+ const bottom = boxDoubleBottom(width);
167
+ return [top, body, bottom].filter(Boolean).join('\n');
168
+ }
169
+ /**
170
+ * Convenience: wrap an array of content rows with sharp borders
171
+ * and an optional title.
172
+ */
173
+ function boxSharp(rows, width, title) {
174
+ const top = title ? boxTopTitled(title, width) : boxTop(width);
175
+ const body = rows.map((r) => boxLine(r, width)).join('\n');
176
+ const bottom = boxBottom(width);
177
+ return [top, body, bottom].filter(Boolean).join('\n');
112
178
  }
@@ -33,10 +33,15 @@ async function defaultPrompts() {
33
33
  },
34
34
  };
35
35
  }
36
+ // Tier-3-essentials: terse 4-state ladder labels per dispatch.
37
+ // Once / Session / Always / Deny — same underlying ApprovalDecision
38
+ // values, friendlier wording. Persistence to <aidenHome>/approvals.json
39
+ // is wired in aidenCLI.ts via approvalEngine.callbacks.persistAllow
40
+ // (Phase 16f); session-scope cache in approvalEngine.allowForSession.
36
41
  const DECISION_CHOICES = [
37
- { name: 'Allow once', value: 'allow' },
38
- { name: 'Allow this session', value: 'allow_session' },
39
- { name: 'Allow always', value: 'allow_always' },
42
+ { name: 'Once', value: 'allow' },
43
+ { name: 'Session', value: 'allow_session' },
44
+ { name: 'Always', value: 'allow_always' },
40
45
  { name: 'Deny', value: 'deny' },
41
46
  ];
42
47
  const KNOWN_TIERS = new Set(['safe', 'caution', 'dangerous']);
@@ -184,6 +189,26 @@ Reply with ONE word: safe, caution, or dangerous.`;
184
189
  this.display.dim(`[planner] kept ${decision.selectedTools.length} tools (${decision.reason})`);
185
190
  }
186
191
  };
192
+ /**
193
+ * Phase v4.1-skill-mining — post-turn cue when the miner has
194
+ * staged a candidate for `/skills review`. Single dim line, no
195
+ * modal. Pulls the skill name + confidence from the candidate's
196
+ * own SKILL.md (best-effort parse; falls back to id slice).
197
+ */
198
+ this.onSkillCandidate = (candidate) => {
199
+ let name = candidate.id.slice(0, 8);
200
+ try {
201
+ // Tier-3.1c sweep: do not import here — chatSession's display
202
+ // wraps strings, and the SKILL.md frontmatter is plain enough
203
+ // that a quick regex is fine for the cue line.
204
+ const m = /\bname\s*:\s*([^\n]+)/.exec(candidate.skillContent);
205
+ if (m)
206
+ name = m[1].trim();
207
+ }
208
+ catch { /* fall through */ }
209
+ const conf = candidate.candidateConfidence.toFixed(2);
210
+ this.display.dim(`[skill] candidate '${name}' queued (conf ${conf}) — run /skills review`);
211
+ };
187
212
  /** ContextCompressor sink — always shows. */
188
213
  this.onCompression = (result) => {
189
214
  if (result.refused) {
@@ -239,14 +264,22 @@ Reply with ONE word: safe, caution, or dangerous.`;
239
264
  }
240
265
  }
241
266
  exports.CliCallbacks = CliCallbacks;
267
+ // Tier-3.1 (v4.1-tier3.1): replaced 🟢/🟡/🔴 emoji badges with
268
+ // text-state badges. Each badge is 7 visible chars (pad-aligned) so
269
+ // approval-prompt rows align across tiers. Plain ANSI SGR colour to
270
+ // keep this file dependency-free.
271
+ const ANSI_GREEN = '\x1b[32m';
272
+ const ANSI_YELLOW = '\x1b[33m';
273
+ const ANSI_RED = '\x1b[31m';
274
+ const ANSI_RESET = '\x1b[0m';
242
275
  function badgeForTier(tier) {
243
276
  switch (tier) {
244
277
  case 'safe':
245
- return '🟢 safe';
278
+ return `${ANSI_GREEN}[ALLOW]${ANSI_RESET} safe`;
246
279
  case 'caution':
247
- return '🟡 caution';
280
+ return `${ANSI_YELLOW}[WARN] ${ANSI_RESET} caution`;
248
281
  case 'dangerous':
249
- return '🔴 dangerous';
282
+ return `${ANSI_RED}[DENY] ${ANSI_RESET} dangerous`;
250
283
  default:
251
284
  return '';
252
285
  }