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,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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AIDEN_PRESHIP_BUILD = exports.AIDEN_CROSS_PLATFORM_BUILD = exports.AIDEN_REPLY_FORMAT_BUILD = exports.AIDEN_SKILL_MINING_BUILD = exports.AIDEN_UI_BUILD = void 0;
|
|
4
|
+
exports.citationsEnabled = citationsEnabled;
|
|
5
|
+
exports.isMcpServeMode = isMcpServeMode;
|
|
6
|
+
exports.isNoUiMode = isNoUiMode;
|
|
7
|
+
exports.uiIconsEnabled = uiIconsEnabled;
|
|
8
|
+
/**
|
|
9
|
+
* Copyright (c) 2026 Shiva Deore (Taracod). Licensed under AGPL-3.0.
|
|
10
|
+
*
|
|
11
|
+
* cli/v4/uiBuild.ts — Aiden v4.1 Tier-3 UI build fingerprint.
|
|
12
|
+
*
|
|
13
|
+
* A single source-of-truth string the smokes can `require()` from
|
|
14
|
+
* the built artifact. Bumped by hand at the start of each tier-3
|
|
15
|
+
* sub-phase so smoke harnesses can pin against the expected build.
|
|
16
|
+
*/
|
|
17
|
+
exports.AIDEN_UI_BUILD = 'v4.1-tier3-essentials';
|
|
18
|
+
/**
|
|
19
|
+
* Phase v4.1-skill-mining: build fingerprint for the auto-extract
|
|
20
|
+
* subsystem. Bumped per skill-mining sub-phase so smokes can pin
|
|
21
|
+
* against the expected build.
|
|
22
|
+
*/
|
|
23
|
+
exports.AIDEN_SKILL_MINING_BUILD = 'v4.1-skill-mining';
|
|
24
|
+
/**
|
|
25
|
+
* Phase v4.1-reply-formatting: build fingerprint for the structured
|
|
26
|
+
* markdown rendering / citation footer / streaming stable-prefix
|
|
27
|
+
* subsystem. Render-layer only — no agent prompts or behavior change.
|
|
28
|
+
*/
|
|
29
|
+
exports.AIDEN_REPLY_FORMAT_BUILD = 'v4.1-reply-formatting';
|
|
30
|
+
/**
|
|
31
|
+
* Phase v4.1-cross-platform: build fingerprint for the Linux / macOS
|
|
32
|
+
* compatibility pass — path helpers, audio backend detection, skill
|
|
33
|
+
* loader case-insensitive lookup, doctor checks per OS, CI matrix.
|
|
34
|
+
*/
|
|
35
|
+
exports.AIDEN_CROSS_PLATFORM_BUILD = 'v4.1-cross-platform';
|
|
36
|
+
/**
|
|
37
|
+
* Phase v4.1-preship-cleanup: build fingerprint for the day-one
|
|
38
|
+
* polish batch — vitest baseline goes from 37 fails to 0, telegram
|
|
39
|
+
* 409 path gains a local-machine polling lock to prevent same-box
|
|
40
|
+
* rivals from racing.
|
|
41
|
+
*/
|
|
42
|
+
exports.AIDEN_PRESHIP_BUILD = 'v4.1-preship-cleanup';
|
|
43
|
+
/** Predicate: is the citation footer enabled? Default off. */
|
|
44
|
+
function citationsEnabled() {
|
|
45
|
+
return process.env.AIDEN_CITATIONS === '1';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Predicate: are we running in MCP serve mode? When true, the
|
|
49
|
+
* stdout channel belongs to JSON-RPC and any UI write would corrupt
|
|
50
|
+
* the wire. Tier-3 UI helpers consult this before printing.
|
|
51
|
+
*
|
|
52
|
+
* The MCP server CLI (cli/v4/commands/mcp.ts) sets
|
|
53
|
+
* `process.env.AIDEN_MCP_SERVE = '1'` early in its boot path; that
|
|
54
|
+
* env-var check is intentionally cheap and safe to read often.
|
|
55
|
+
*/
|
|
56
|
+
function isMcpServeMode() {
|
|
57
|
+
return process.env.AIDEN_MCP_SERVE === '1';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Predicate: is the legacy/no-UI flag in effect? Disables tier-3
|
|
61
|
+
* polish (autosuggest ghost text, inline status line, etc.) and
|
|
62
|
+
* falls back to pre-tier3.1 rendering. Set by `aiden --no-ui`.
|
|
63
|
+
*/
|
|
64
|
+
function isNoUiMode() {
|
|
65
|
+
return process.env.AIDEN_NO_UI === '1';
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Predicate: should slash-command icons render? Default OFF; opt-in
|
|
69
|
+
* via `AIDEN_UI_ICONS=1`. Lets users with emoji-friendly terminals
|
|
70
|
+
* recover the previous icon column.
|
|
71
|
+
*/
|
|
72
|
+
function uiIconsEnabled() {
|
|
73
|
+
return process.env.AIDEN_UI_ICONS === '1';
|
|
74
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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/voiceCli.ts — Phase v4.1-voice-cli
|
|
10
|
+
*
|
|
11
|
+
* `aiden voice <action>` top-level CLI subcommand. Three actions:
|
|
12
|
+
*
|
|
13
|
+
* doctor — print diagnostics: build, TTY, mic backend,
|
|
14
|
+
* TTS providers, current config. No mic open.
|
|
15
|
+
* tts <text> — synthesise + play one short clip. Real
|
|
16
|
+
* provider call.
|
|
17
|
+
* transcribe <f> — STT one audio file. Reuses the v4.1-3
|
|
18
|
+
* `whisper-transcribe` channel pipeline.
|
|
19
|
+
*
|
|
20
|
+
* Distinct from the `/voice` slash command (which mutates session
|
|
21
|
+
* state from inside the REPL). This subcommand exists so users can
|
|
22
|
+
* verify mic + speaker setup BEFORE entering the REPL — useful for
|
|
23
|
+
* first-run mic-permission grants on Windows where the OS prompts
|
|
24
|
+
* the first time the device is opened.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.AIDEN_VOICE_CLI_BUILD = void 0;
|
|
28
|
+
exports.runVoiceSubcommand = runVoiceSubcommand;
|
|
29
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
30
|
+
const node_fs_1 = require("node:fs");
|
|
31
|
+
const diagnostics_1 = require("../../core/v4/voice/diagnostics");
|
|
32
|
+
Object.defineProperty(exports, "AIDEN_VOICE_CLI_BUILD", { enumerable: true, get: function () { return diagnostics_1.AIDEN_VOICE_CLI_BUILD; } });
|
|
33
|
+
const tts_1 = require("../../core/voice/tts");
|
|
34
|
+
const whisper_transcribe_1 = require("../../core/channels/whisper-transcribe");
|
|
35
|
+
async function runVoiceSubcommand(action, args, opts = {}) {
|
|
36
|
+
const writeOut = opts.writeOut ?? ((t) => process.stdout.write(t));
|
|
37
|
+
const writeErr = opts.writeErr ?? ((t) => process.stderr.write(t));
|
|
38
|
+
switch (action) {
|
|
39
|
+
case 'doctor': {
|
|
40
|
+
const diag = await (0, diagnostics_1.collectVoiceDiagnostics)();
|
|
41
|
+
writeOut(`Aiden voice — ${diagnostics_1.AIDEN_VOICE_CLI_BUILD}\n`);
|
|
42
|
+
writeOut(` tty: ${diag.isTty ? 'yes' : 'no'}\n`);
|
|
43
|
+
writeOut(` enabled: ${diag.enabled ? 'yes' : 'no (refused — non-TTY stdin)'}\n`);
|
|
44
|
+
writeOut(` mic backend: ${diag.audio.backend}\n`);
|
|
45
|
+
writeOut(` mic active: ${diag.audio.active ? 'yes' : 'no'}\n`);
|
|
46
|
+
writeOut(` sox on PATH: ${diag.audio.soxOnPath ? 'yes' : 'no'}\n`);
|
|
47
|
+
writeOut(` mode: ${diag.config.mode}\n`);
|
|
48
|
+
writeOut(` tts voice: ${diag.config.ttsVoice}\n`);
|
|
49
|
+
writeOut(` beeps: ${diag.config.beepsEnabled ? 'on' : 'off'}\n`);
|
|
50
|
+
writeOut(` tts providers:\n`);
|
|
51
|
+
for (const p of diag.ttsProviders) {
|
|
52
|
+
const tag = p.available ? '✓' : '✗';
|
|
53
|
+
writeOut(` ${tag} ${p.name.padEnd(12)} ${p.note ?? ''}\n`);
|
|
54
|
+
}
|
|
55
|
+
// Mic-backend hint when nothing is installed.
|
|
56
|
+
if (diag.audio.backend === 'unavailable') {
|
|
57
|
+
writeOut(`\n Hint: install \`decibri\` (npm i decibri) for prebuilt mic capture,\n`);
|
|
58
|
+
writeOut(` OR install sox (https://sox.sourceforge.io/) + node-record-lpcm16.\n`);
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
case 'tts': {
|
|
63
|
+
const text = args.join(' ').trim();
|
|
64
|
+
if (!text) {
|
|
65
|
+
writeErr(`Usage: aiden voice tts "<text>"\n`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
const cleaned = (0, tts_1.cleanForTTS)(text);
|
|
69
|
+
if (!cleaned) {
|
|
70
|
+
writeErr(`Empty after cleanForTTS — nothing to speak.\n`);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
writeOut(`Synthesising via TTS chain (${cleaned.length} chars)...\n`);
|
|
74
|
+
const r = await (0, tts_1.synthesize)({ text: cleaned });
|
|
75
|
+
if (r.error) {
|
|
76
|
+
writeErr(`TTS failed: ${r.error}\n`);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
writeOut(`TTS ok — provider: ${r.provider}, ${r.durationMs}ms\n`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
case 'transcribe': {
|
|
83
|
+
const filePath = args[0];
|
|
84
|
+
if (!filePath) {
|
|
85
|
+
writeErr(`Usage: aiden voice transcribe <audio-file>\n`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
await node_fs_1.promises.access(filePath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
writeErr(`File not found: ${filePath}\n`);
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
writeOut(`Transcribing ${filePath}...\n`);
|
|
96
|
+
const r = await (0, whisper_transcribe_1.transcribeForChannel)({ filePath });
|
|
97
|
+
if (!r.success) {
|
|
98
|
+
writeErr(`Transcribe failed: ${r.error ?? 'unknown'}\n`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const conf = typeof r.avgLogprob === 'number'
|
|
102
|
+
? ` (avgLogprob=${r.avgLogprob.toFixed(2)})`
|
|
103
|
+
: '';
|
|
104
|
+
writeOut(`Transcript${conf}:\n${r.text ?? ''}\n`);
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
default: {
|
|
108
|
+
writeErr(`Unknown 'aiden voice' action: ${action}\n`);
|
|
109
|
+
writeErr(`Actions: doctor | tts <text> | transcribe <file>\n`);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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/voicePromptApi.ts — Phase v4.1-voice-cli
|
|
10
|
+
*
|
|
11
|
+
* Wraps a default `ChatPromptApi` implementation with a raw-mode
|
|
12
|
+
* spacebar toggle for push-to-talk recording. When the user is at
|
|
13
|
+
* the prompt and presses Space:
|
|
14
|
+
*
|
|
15
|
+
* 1. Switch to raw mode + start `cliVoice.startRecording()`
|
|
16
|
+
* 2. Update the spinner: "🎤 recording (Space to stop, Esc to cancel)"
|
|
17
|
+
* 3. On second Space: stop and transcribe → return transcript
|
|
18
|
+
* 4. On Esc: cancel and return empty (caller falls back to text)
|
|
19
|
+
* 5. On any other character before the first Space: hand control
|
|
20
|
+
* back to the wrapped `inquirer` prompt so the user types
|
|
21
|
+
* normally
|
|
22
|
+
*
|
|
23
|
+
* Hard-refuses activation when `process.stdin.isTTY` is false. This
|
|
24
|
+
* is the MCP-stdio invariant — `aiden mcp serve` uses stdin as the
|
|
25
|
+
* JSON-RPC transport, and toggling raw mode there would corrupt
|
|
26
|
+
* every protocol frame. The refusal is silent in MCP context (the
|
|
27
|
+
* default `readLine` runs unchanged); explicit in REPL context (a
|
|
28
|
+
* stderr warning + fall-through).
|
|
29
|
+
*
|
|
30
|
+
* `selectSlashCommand` is delegated unchanged — slash commands
|
|
31
|
+
* still go through the inquirer dropdown.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.createVoicePromptApi = createVoicePromptApi;
|
|
35
|
+
exports.voiceModeAllowed = voiceModeAllowed;
|
|
36
|
+
const factory_1 = require("../../core/v4/logger/factory");
|
|
37
|
+
const KEY_SPACE = 0x20;
|
|
38
|
+
const KEY_ESC = 0x1b;
|
|
39
|
+
/** Build a prompt API that intercepts Space for push-to-talk and
|
|
40
|
+
* falls through to `inner` for normal text input. */
|
|
41
|
+
function createVoicePromptApi(opts) {
|
|
42
|
+
const logger = (opts.logger ?? (0, factory_1.noopLogger)()).child('voice-prompt');
|
|
43
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
44
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
45
|
+
return {
|
|
46
|
+
async readLine(prompt) {
|
|
47
|
+
// Hard refuse when stdin isn't a TTY. Voice mode requires raw
|
|
48
|
+
// mode; raw mode requires a TTY. MCP stdio mode hits this path
|
|
49
|
+
// when Claude Desktop spawns aiden — silently fall through.
|
|
50
|
+
if (!stdin.isTTY) {
|
|
51
|
+
return opts.inner.readLine(prompt);
|
|
52
|
+
}
|
|
53
|
+
const transcript = await waitForSpaceOrTypedInput({
|
|
54
|
+
prompt,
|
|
55
|
+
stdin,
|
|
56
|
+
stdout,
|
|
57
|
+
voice: opts.voice,
|
|
58
|
+
onStatus: opts.onStatus,
|
|
59
|
+
logger,
|
|
60
|
+
});
|
|
61
|
+
if (transcript === null) {
|
|
62
|
+
// User typed text — hand off to the regular prompt API. The
|
|
63
|
+
// first character is already in the typeahead via the buffer
|
|
64
|
+
// — `inner.readLine` reads from there.
|
|
65
|
+
return opts.inner.readLine(prompt);
|
|
66
|
+
}
|
|
67
|
+
if (transcript === '') {
|
|
68
|
+
// Cancelled — fall back to text prompt.
|
|
69
|
+
return opts.inner.readLine(prompt);
|
|
70
|
+
}
|
|
71
|
+
return transcript;
|
|
72
|
+
},
|
|
73
|
+
async selectSlashCommand(source) {
|
|
74
|
+
// Slash commands don't get voice intercept — they're a
|
|
75
|
+
// discrete dropdown.
|
|
76
|
+
return opts.inner.selectSlashCommand(source);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Wait for either Space (start recording) or any other char (fall
|
|
81
|
+
* through to text prompt). Returns:
|
|
82
|
+
* - the transcribed string when recording completes
|
|
83
|
+
* - '' when user cancels (Esc)
|
|
84
|
+
* - null when user types non-space (fall through to inner) */
|
|
85
|
+
async function waitForSpaceOrTypedInput(args) {
|
|
86
|
+
// Show a brief hint so users know voice mode is hot.
|
|
87
|
+
args.stdout.write(`${args.prompt}\x1b[2m(Space to talk)\x1b[0m `);
|
|
88
|
+
const stdin = args.stdin;
|
|
89
|
+
// Snapshot current raw mode state to restore on exit.
|
|
90
|
+
const wasRaw = !!stdin.isRaw;
|
|
91
|
+
if (!wasRaw)
|
|
92
|
+
stdin.setRawMode(true);
|
|
93
|
+
stdin.resume();
|
|
94
|
+
let result = undefined;
|
|
95
|
+
let recording = false;
|
|
96
|
+
let transcript = null;
|
|
97
|
+
let resolveOuter = null;
|
|
98
|
+
const cleanup = () => {
|
|
99
|
+
stdin.removeListener('data', onData);
|
|
100
|
+
stdin.removeListener('error', onError);
|
|
101
|
+
if (!wasRaw) {
|
|
102
|
+
try {
|
|
103
|
+
stdin.setRawMode(false);
|
|
104
|
+
}
|
|
105
|
+
catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
stdin.pause();
|
|
108
|
+
};
|
|
109
|
+
const onData = (chunk) => {
|
|
110
|
+
if (chunk.length === 0)
|
|
111
|
+
return;
|
|
112
|
+
const code = chunk[0];
|
|
113
|
+
if (!recording) {
|
|
114
|
+
if (code === KEY_SPACE) {
|
|
115
|
+
// Start recording.
|
|
116
|
+
recording = true;
|
|
117
|
+
args.voice.startRecording().catch((err) => {
|
|
118
|
+
args.logger.warn('startRecording threw', { error: err.message });
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else if (code === KEY_ESC) {
|
|
122
|
+
result = '';
|
|
123
|
+
cleanup();
|
|
124
|
+
resolveOuter?.('');
|
|
125
|
+
}
|
|
126
|
+
else if (code === 0x03) {
|
|
127
|
+
// Ctrl+C — propagate to inner via empty cancel.
|
|
128
|
+
result = '';
|
|
129
|
+
cleanup();
|
|
130
|
+
resolveOuter?.('');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// Any other key — fall through to inner prompt. Push the
|
|
134
|
+
// byte back so inner reads it (best-effort; on Windows
|
|
135
|
+
// the unread() trick isn't reliable, so we just signal
|
|
136
|
+
// null and inner re-prompts).
|
|
137
|
+
result = null;
|
|
138
|
+
cleanup();
|
|
139
|
+
resolveOuter?.(null);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Already recording. Space stops; Esc cancels.
|
|
144
|
+
if (code === KEY_SPACE) {
|
|
145
|
+
args.voice.stopRecording().catch((err) => {
|
|
146
|
+
args.logger.warn('stopRecording threw', { error: err.message });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else if (code === KEY_ESC || code === 0x03) {
|
|
150
|
+
args.voice.cancel();
|
|
151
|
+
result = '';
|
|
152
|
+
cleanup();
|
|
153
|
+
resolveOuter?.('');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const onError = (err) => {
|
|
158
|
+
args.logger.warn('stdin error during voice prompt', { error: err.message });
|
|
159
|
+
cleanup();
|
|
160
|
+
resolveOuter?.(null);
|
|
161
|
+
};
|
|
162
|
+
// Voice handle's onTranscript wins the race when recording succeeds.
|
|
163
|
+
// We register a one-shot subscription via the existing callback by
|
|
164
|
+
// taking advantage of the fact that handle.startRecording resolves
|
|
165
|
+
// when transcribe completes — at that point transcript will be set
|
|
166
|
+
// through the host's status callback. To keep this module narrowly
|
|
167
|
+
// scoped, we POLL voice.getStatus() between awaits via a watcher.
|
|
168
|
+
const watcher = setInterval(() => {
|
|
169
|
+
const s = args.voice.getStatus();
|
|
170
|
+
args.onStatus?.(s);
|
|
171
|
+
// Recording finished naturally OR errored.
|
|
172
|
+
if (recording && s === 'idle') {
|
|
173
|
+
// Drain stdin and resolve. The transcript was forwarded via
|
|
174
|
+
// the host's onTranscript callback (set up in the cliVoice
|
|
175
|
+
// constructor); the host stitches it into the conversation.
|
|
176
|
+
// For the prompt-API contract we resolve with empty so the
|
|
177
|
+
// outer loop spins to the next iteration.
|
|
178
|
+
cleanup();
|
|
179
|
+
clearInterval(watcher);
|
|
180
|
+
resolveOuter?.(transcript ?? '');
|
|
181
|
+
}
|
|
182
|
+
}, 50);
|
|
183
|
+
stdin.on('data', onData);
|
|
184
|
+
stdin.on('error', onError);
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
resolveOuter = (v) => {
|
|
187
|
+
clearInterval(watcher);
|
|
188
|
+
resolve(v);
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Test-only helper: enforce the TTY guard. Returns true when voice
|
|
193
|
+
* mode is allowed to activate in this process. */
|
|
194
|
+
function voiceModeAllowed(stdin = process.stdin) {
|
|
195
|
+
return !!stdin.isTTY;
|
|
196
|
+
}
|