compact-agent 1.4.0 → 1.6.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/dist/accessibility.d.ts +50 -0
- package/dist/accessibility.js +0 -0
- package/dist/accessibility.js.map +1 -0
- package/dist/audio.d.ts +50 -0
- package/dist/audio.js +382 -0
- package/dist/audio.js.map +1 -0
- package/dist/config.js +48 -1
- package/dist/config.js.map +1 -1
- package/dist/ecc.js +60 -0
- package/dist/ecc.js.map +1 -1
- package/dist/index.js +422 -66
- package/dist/index.js.map +1 -1
- package/dist/query.js +87 -2
- package/dist/query.js.map +1 -1
- package/dist/theme.d.ts +3 -0
- package/dist/theme.js +51 -29
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.js.map +1 -1
- package/dist/voice.d.ts +79 -0
- package/dist/voice.js +344 -0
- package/dist/voice.js.map +1 -0
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { buildCommitPrompt, buildPRPrompt, printDiff, printLog } from './git-wor
|
|
|
20
20
|
import { buildReviewPrompt, buildTDDPrompt, buildSecurityReviewPrompt, runAudit, printAuditReport, buildPlanPrompt, buildE2EPrompt, buildBuildFixPrompt, buildEvalPrompt } from './evaluation.js';
|
|
21
21
|
import { printRules } from './rules.js';
|
|
22
22
|
import { buildOrchestrationPrompt } from './orchestration.js';
|
|
23
|
-
import { printBanner as printThemedBanner,
|
|
23
|
+
import { printBanner as printThemedBanner, theme, sym, formatDuration, installScreenReaderDispatch, uninstallScreenReaderDispatch } from './theme.js';
|
|
24
24
|
import { saveExport } from './export.js';
|
|
25
25
|
// New feature modules
|
|
26
26
|
import { buildVerifyPrompt, saveCheckpoint, listCheckpoints } from './verification.js';
|
|
@@ -50,11 +50,15 @@ import { printHookControlStatus } from './hook-controls.js';
|
|
|
50
50
|
// PM2 manager
|
|
51
51
|
import { buildPM2Prompt, isPM2Available, listPM2Services } from './pm2-manager.js';
|
|
52
52
|
// ECC (everything-claude-code) integration
|
|
53
|
-
import { installEcc,
|
|
53
|
+
import { installEcc, getEccCommandPrompt, loadEccState, eccResourcesAvailable, } from './ecc.js';
|
|
54
54
|
// Walkthrough — agent-led tour of Crowcoder (/walkthrough, /tour, /guide)
|
|
55
55
|
import { buildWalkthroughPrompt } from './walkthrough.js';
|
|
56
56
|
// Stitch (Google's AI UI/UX design tool) — /stitch, /stitch-config, /stitch-tools
|
|
57
57
|
import { buildStitchPrompt, buildStitchToolsPrompt, saveStitchConfig, printStitchStatus, stitchConfigured } from './stitch.js';
|
|
58
|
+
// Voice / accessibility — built-in dictation (Whisper) + readout (ElevenLabs)
|
|
59
|
+
import { printVoiceStatus, isVoiceEnabled, getTtsConfig, getSttConfig, getAccessibilityConfig, speak, dictateOnce, } from './voice.js';
|
|
60
|
+
import { isFfmpegAvailable, audioCue, startRecording } from './audio.js';
|
|
61
|
+
import { applyScreenReader } from './accessibility.js';
|
|
58
62
|
/**
|
|
59
63
|
* Unified prompt resolver — prefers the bundled ECC prompt for a given
|
|
60
64
|
* intent and falls back to the built-in builder when ECC isn't installed.
|
|
@@ -139,6 +143,13 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
139
143
|
const h = theme.header;
|
|
140
144
|
const d = theme.dim;
|
|
141
145
|
const c = theme.command;
|
|
146
|
+
// Inline status line: confirms ECC is on (and how many skills it brings)
|
|
147
|
+
// without giving it its own section. ECC has no user-facing commands;
|
|
148
|
+
// it works automatically.
|
|
149
|
+
const eccState = loadEccState();
|
|
150
|
+
if (eccState) {
|
|
151
|
+
console.log(d(`\n ECC: `) + theme.toolStatus('✓ enabled') + d(` — ${eccState.counts.skills} skills, ${eccState.counts.agents} agents, ${eccState.counts.commands + eccState.counts.prompts} workflows auto-loaded`));
|
|
152
|
+
}
|
|
142
153
|
console.log(h('\n ── General ──'));
|
|
143
154
|
console.log(d(' ') + c('/help') + d(' — this help'));
|
|
144
155
|
console.log(d(' ') + c('/config') + d(' — reconfigure provider/model/key'));
|
|
@@ -240,13 +251,23 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
240
251
|
console.log(d(' ') + c('/detect') + d(' — detect package manager, test runner, build tool'));
|
|
241
252
|
console.log(d(' ') + c('/hook-profile') + d(' — show hook profile & controls'));
|
|
242
253
|
console.log(d(' ') + c('/pm2 [action]') + d(' — PM2 service management'));
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
console.log(
|
|
254
|
+
// ECC is bundled, free, auto-installed on first launch, and used
|
|
255
|
+
// automatically. Built-in /tdd /review /security-review /plan /refactor
|
|
256
|
+
// /build-fix use ECC prompts. ECC-only workflows (feature-development,
|
|
257
|
+
// database-migration, add-language-rules) auto-inject when you describe
|
|
258
|
+
// matching work — no slash command needed. Status line below confirms
|
|
259
|
+
// it's enabled.
|
|
260
|
+
console.log(h('\n ── Voice & accessibility ──'));
|
|
261
|
+
console.log(d(' ') + c('/voice') + d(' — show voice config & status (off by default)'));
|
|
262
|
+
console.log(d(' ') + c('/voice on|off') + d(' — master switch for dictation + readout'));
|
|
263
|
+
console.log(d(' ') + c('/voice config') + d(' — quick setup walkthrough'));
|
|
264
|
+
console.log(d(' ') + c('/voice key stt <key>') + d(' — OpenAI key for Whisper dictation'));
|
|
265
|
+
console.log(d(' ') + c('/voice key tts <key>') + d(' — ElevenLabs key for assistant readout'));
|
|
266
|
+
console.log(d(' ') + c('/voice test') + d(' — play a short test utterance'));
|
|
267
|
+
console.log(d(' ') + c('/voice echo|skip-code|speed') + d(' — fine-tune behavior'));
|
|
268
|
+
console.log(d(' ') + c('/dictate [s]') + d(' — one-shot record + transcribe (default 30s)'));
|
|
269
|
+
console.log(d(' ') + c('/accessibility') + d(' — toggle screen-reader mode, audio cues, destructive-confirm'));
|
|
270
|
+
console.log(d(' Hotkeys: F1 dictate · F2 pause · F3 replay · F4 skip · F5 speed+ · F6 speed–'));
|
|
250
271
|
console.log(h('\n ── Stitch (Google AI UI/UX design) ──'));
|
|
251
272
|
console.log(d(' Use ') + c('/mode design') + d(' or ') + c('/design <task>') + d(' for UI work — the agent uses Stitch automatically.'));
|
|
252
273
|
console.log(d(' ') + c('/stitch') + d(' — show config status'));
|
|
@@ -328,6 +349,15 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
328
349
|
mode.current = args;
|
|
329
350
|
const m = MODES[mode.current];
|
|
330
351
|
console.log(chalk.green(` Mode: ${m.label} — ${m.description}`));
|
|
352
|
+
// Accessibility: speak the mode-switch when configured. Doesn't
|
|
353
|
+
// block — fire-and-forget. Errors swallowed (voice should never
|
|
354
|
+
// break the REPL).
|
|
355
|
+
if (isVoiceEnabled(config) && getAccessibilityConfig(config).announceModeSwitches) {
|
|
356
|
+
const tts = getTtsConfig(config);
|
|
357
|
+
if (tts.apiKey) {
|
|
358
|
+
speak(`Mode switched to ${m.label}`, config, { voiceId: tts.assistantVoiceId }).catch(() => { });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
331
361
|
}
|
|
332
362
|
else if (args) {
|
|
333
363
|
console.log(chalk.yellow(` Unknown mode: ${args}`));
|
|
@@ -1068,84 +1098,229 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
1068
1098
|
console.log(chalk.dim(' Restart the REPL for the tool to appear in /tools.'));
|
|
1069
1099
|
return { handled: true };
|
|
1070
1100
|
}
|
|
1071
|
-
// ──
|
|
1072
|
-
//
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1101
|
+
// ── Voice / accessibility ────────────────────────
|
|
1102
|
+
// /voice — show current voice config + status
|
|
1103
|
+
// /voice on | off — master switch
|
|
1104
|
+
// /voice config — interactive setup (asks for keys)
|
|
1105
|
+
// /voice test — synth a short test utterance to verify TTS
|
|
1106
|
+
// /voice key stt <KEY> — set OpenAI key for Whisper STT only
|
|
1107
|
+
// /voice key tts <KEY> — set ElevenLabs key for TTS only
|
|
1108
|
+
// /voice echo on | off — toggle TTS-echo of user input
|
|
1109
|
+
// /voice skip-code on|off — toggle stripping code blocks from TTS
|
|
1110
|
+
// /voice speed <n> — set 0.5..2.0
|
|
1111
|
+
case '/voice': {
|
|
1112
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
1113
|
+
const sub = (parts[0] || '').toLowerCase();
|
|
1114
|
+
if (!sub) {
|
|
1115
|
+
printVoiceStatus(config);
|
|
1116
|
+
return { handled: true };
|
|
1117
|
+
}
|
|
1118
|
+
if (sub === 'on' || sub === 'off') {
|
|
1119
|
+
config.voice = config.voice || {};
|
|
1120
|
+
config.voice.enabled = sub === 'on';
|
|
1121
|
+
saveConfig(config);
|
|
1122
|
+
console.log(chalk.green(` Voice: ${sub === 'on' ? 'ON' : 'OFF'}`));
|
|
1123
|
+
if (sub === 'on') {
|
|
1124
|
+
if (!getTtsConfig(config).apiKey) {
|
|
1125
|
+
console.log(chalk.yellow(' ⚠ No ElevenLabs key set. Run /voice key tts <KEY> to enable readout.'));
|
|
1126
|
+
}
|
|
1127
|
+
if (!getSttConfig(config).apiKey) {
|
|
1128
|
+
console.log(chalk.yellow(' ⚠ No OpenAI key for Whisper. Run /voice key stt <KEY> to enable dictation.'));
|
|
1129
|
+
}
|
|
1130
|
+
isFfmpegAvailable().then((ok) => {
|
|
1131
|
+
if (!ok)
|
|
1132
|
+
console.log(chalk.yellow(' ⚠ ffmpeg not found on PATH. Install ffmpeg: https://ffmpeg.org/'));
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
return { handled: true };
|
|
1136
|
+
}
|
|
1137
|
+
if (sub === 'config') {
|
|
1138
|
+
// Lightweight interactive setup deferred to the prompt — user can
|
|
1139
|
+
// also just use `/voice key stt ...` and `/voice key tts ...`.
|
|
1140
|
+
console.log(chalk.cyan('\n /voice config — quick setup'));
|
|
1141
|
+
console.log(chalk.dim(' 1. Get an OpenAI key for Whisper STT: https://platform.openai.com/api-keys'));
|
|
1142
|
+
console.log(chalk.dim(' 2. Get an ElevenLabs key for TTS: https://elevenlabs.io/app/settings/api-keys'));
|
|
1143
|
+
console.log(chalk.dim(' 3. Run: /voice key stt <openai-key>'));
|
|
1144
|
+
console.log(chalk.dim(' /voice key tts <elevenlabs-key>'));
|
|
1145
|
+
console.log(chalk.dim(' /voice on'));
|
|
1146
|
+
console.log(chalk.dim(' 4. Press F1 to dictate, hear assistant readout automatically.'));
|
|
1147
|
+
console.log();
|
|
1148
|
+
return { handled: true };
|
|
1149
|
+
}
|
|
1150
|
+
if (sub === 'key') {
|
|
1151
|
+
const target = (parts[1] || '').toLowerCase();
|
|
1152
|
+
const key = parts.slice(2).join(' ').trim();
|
|
1153
|
+
if ((target !== 'stt' && target !== 'tts') || !key) {
|
|
1154
|
+
console.log(chalk.yellow(' Usage: /voice key stt <openai-key> | /voice key tts <elevenlabs-key>'));
|
|
1083
1155
|
return { handled: true };
|
|
1084
1156
|
}
|
|
1085
|
-
|
|
1086
|
-
if (
|
|
1087
|
-
|
|
1157
|
+
config.voice = config.voice || {};
|
|
1158
|
+
if (target === 'stt') {
|
|
1159
|
+
config.voice.stt = { ...(config.voice.stt || {}), apiKey: key };
|
|
1160
|
+
console.log(chalk.green(` STT key saved (***${key.slice(-4)}).`));
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
config.voice.tts = { ...(config.voice.tts || {}), apiKey: key };
|
|
1164
|
+
console.log(chalk.green(` TTS key saved (***${key.slice(-4)}).`));
|
|
1165
|
+
}
|
|
1166
|
+
saveConfig(config);
|
|
1167
|
+
return { handled: true };
|
|
1168
|
+
}
|
|
1169
|
+
if (sub === 'test') {
|
|
1170
|
+
const tts = getTtsConfig(config);
|
|
1171
|
+
if (!tts.apiKey) {
|
|
1172
|
+
console.log(chalk.yellow(' No TTS key. Run /voice key tts <elevenlabs-key> first.'));
|
|
1173
|
+
return { handled: true };
|
|
1088
1174
|
}
|
|
1175
|
+
console.log(chalk.dim(' Synthesizing test utterance…'));
|
|
1176
|
+
speak('Voice readout is working. This is the assistant voice.', config, { voiceId: tts.assistantVoiceId })
|
|
1177
|
+
.then((ok) => console.log(ok ? chalk.green(' ✓ Played.') : chalk.yellow(' ✗ Playback failed — check ffmpeg.')));
|
|
1089
1178
|
return { handled: true };
|
|
1090
1179
|
}
|
|
1091
|
-
if (sub === '
|
|
1092
|
-
|
|
1180
|
+
if (sub === 'echo') {
|
|
1181
|
+
const v = (parts[1] || '').toLowerCase();
|
|
1182
|
+
if (v !== 'on' && v !== 'off') {
|
|
1183
|
+
console.log(chalk.yellow(' Usage: /voice echo on|off'));
|
|
1184
|
+
return { handled: true };
|
|
1185
|
+
}
|
|
1186
|
+
config.voice = config.voice || {};
|
|
1187
|
+
config.voice.tts = { ...(config.voice.tts || {}), echoUser: v === 'on' };
|
|
1188
|
+
saveConfig(config);
|
|
1189
|
+
console.log(chalk.green(` User-echo: ${v.toUpperCase()}`));
|
|
1093
1190
|
return { handled: true };
|
|
1094
1191
|
}
|
|
1095
|
-
if (sub === '
|
|
1096
|
-
|
|
1192
|
+
if (sub === 'skip-code') {
|
|
1193
|
+
const v = (parts[1] || '').toLowerCase();
|
|
1194
|
+
if (v !== 'on' && v !== 'off') {
|
|
1195
|
+
console.log(chalk.yellow(' Usage: /voice skip-code on|off'));
|
|
1196
|
+
return { handled: true };
|
|
1197
|
+
}
|
|
1198
|
+
config.voice = config.voice || {};
|
|
1199
|
+
config.voice.tts = { ...(config.voice.tts || {}), skipCode: v === 'on' };
|
|
1200
|
+
saveConfig(config);
|
|
1201
|
+
console.log(chalk.green(` Skip-code: ${v.toUpperCase()}`));
|
|
1097
1202
|
return { handled: true };
|
|
1098
1203
|
}
|
|
1099
|
-
if (sub === '
|
|
1100
|
-
|
|
1204
|
+
if (sub === 'speed') {
|
|
1205
|
+
const n = parseFloat(parts[1] || '');
|
|
1206
|
+
if (isNaN(n) || n < 0.25 || n > 4.0) {
|
|
1207
|
+
console.log(chalk.yellow(' Usage: /voice speed <0.25..4.0>'));
|
|
1208
|
+
return { handled: true };
|
|
1209
|
+
}
|
|
1210
|
+
config.voice = config.voice || {};
|
|
1211
|
+
config.voice.tts = { ...(config.voice.tts || {}), speed: n };
|
|
1212
|
+
saveConfig(config);
|
|
1213
|
+
console.log(chalk.green(` TTS speed: ${n}x`));
|
|
1101
1214
|
return { handled: true };
|
|
1102
1215
|
}
|
|
1103
|
-
|
|
1216
|
+
console.log(chalk.yellow(` Unknown /voice subcommand: ${sub}`));
|
|
1217
|
+
console.log(chalk.dim(' Try: on, off, config, test, key, echo, skip-code, speed'));
|
|
1104
1218
|
return { handled: true };
|
|
1105
1219
|
}
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1220
|
+
// /dictate — one-shot push-to-talk WITHOUT the F1 hotkey, useful when a
|
|
1221
|
+
// user is testing the pipeline or running under a terminal that strips
|
|
1222
|
+
// function keys. Records up to 30s, transcribes, injects as next prompt.
|
|
1223
|
+
case '/dictate': {
|
|
1224
|
+
const maxSec = parseInt(args, 10) || 30;
|
|
1225
|
+
console.log(chalk.dim(` /dictate — recording up to ${maxSec}s…`));
|
|
1226
|
+
// Return as an async-injected prompt; we resolve the recording
|
|
1227
|
+
// synchronously here for simplicity (REPL is blocking anyway).
|
|
1228
|
+
return { handled: true, injectPrompt: '__DICTATE__' + maxSec };
|
|
1229
|
+
}
|
|
1230
|
+
// /accessibility — show or toggle the accessibility sub-block
|
|
1231
|
+
// /accessibility — print status
|
|
1232
|
+
// /accessibility screen-reader on|off
|
|
1233
|
+
// /accessibility cues on|off
|
|
1234
|
+
// /accessibility announce-errors on|off
|
|
1235
|
+
// /accessibility announce-modes on|off
|
|
1236
|
+
// /accessibility confirm-destructive on|off
|
|
1237
|
+
// /accessibility long-resp <words>
|
|
1238
|
+
case '/accessibility':
|
|
1239
|
+
case '/a11y': {
|
|
1240
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
1241
|
+
const sub = (parts[0] || '').toLowerCase();
|
|
1242
|
+
const v = (parts[1] || '').toLowerCase();
|
|
1243
|
+
if (!sub) {
|
|
1244
|
+
printVoiceStatus(config);
|
|
1245
|
+
return { handled: true };
|
|
1246
|
+
}
|
|
1247
|
+
const setBool = (field, label) => {
|
|
1248
|
+
if (v !== 'on' && v !== 'off') {
|
|
1249
|
+
console.log(chalk.yellow(` Usage: /accessibility ${sub} on|off`));
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
config.voice = config.voice || {};
|
|
1253
|
+
config.voice.accessibility = { ...(config.voice.accessibility || {}), [field]: v === 'on' };
|
|
1254
|
+
saveConfig(config);
|
|
1255
|
+
// Screen-reader mode is special: install/uninstall the stdout filter
|
|
1256
|
+
// immediately so the toggle takes effect for the very next log line.
|
|
1257
|
+
if (field === 'screenReader') {
|
|
1258
|
+
if (v === 'on')
|
|
1259
|
+
installScreenReaderDispatch(applyScreenReader);
|
|
1260
|
+
else
|
|
1261
|
+
uninstallScreenReaderDispatch();
|
|
1262
|
+
}
|
|
1263
|
+
console.log(chalk.green(` ${label}: ${v.toUpperCase()}`));
|
|
1264
|
+
};
|
|
1265
|
+
if (sub === 'screen-reader' || sub === 'screenreader' || sub === 'sr') {
|
|
1266
|
+
setBool('screenReader', 'Screen-reader mode');
|
|
1267
|
+
return { handled: true };
|
|
1268
|
+
}
|
|
1269
|
+
if (sub === 'cues' || sub === 'audio-cues') {
|
|
1270
|
+
setBool('audioCues', 'Audio cues');
|
|
1271
|
+
return { handled: true };
|
|
1272
|
+
}
|
|
1273
|
+
if (sub === 'announce-errors' || sub === 'errors') {
|
|
1274
|
+
setBool('announceErrors', 'Announce errors');
|
|
1275
|
+
return { handled: true };
|
|
1276
|
+
}
|
|
1277
|
+
if (sub === 'announce-modes' || sub === 'modes') {
|
|
1278
|
+
setBool('announceModeSwitches', 'Announce mode switches');
|
|
1279
|
+
return { handled: true };
|
|
1280
|
+
}
|
|
1281
|
+
if (sub === 'confirm-destructive' || sub === 'destructive') {
|
|
1282
|
+
setBool('askBeforeDestructive', 'Ask before destructive');
|
|
1110
1283
|
return { handled: true };
|
|
1111
1284
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1285
|
+
if (sub === 'long-resp' || sub === 'threshold') {
|
|
1286
|
+
const n = parseInt(parts[1] || '', 10);
|
|
1287
|
+
if (!n || n < 50) {
|
|
1288
|
+
console.log(chalk.yellow(' Usage: /accessibility long-resp <words≥50>'));
|
|
1289
|
+
return { handled: true };
|
|
1290
|
+
}
|
|
1291
|
+
config.voice = config.voice || {};
|
|
1292
|
+
config.voice.accessibility = { ...(config.voice.accessibility || {}), longResponseThreshold: n };
|
|
1293
|
+
saveConfig(config);
|
|
1294
|
+
console.log(chalk.green(` Long-response threshold: ${n} words`));
|
|
1295
|
+
return { handled: true };
|
|
1115
1296
|
}
|
|
1297
|
+
console.log(chalk.yellow(` Unknown /accessibility subcommand: ${sub}`));
|
|
1298
|
+
console.log(chalk.dim(' Try: screen-reader, cues, announce-errors, announce-modes, confirm-destructive, long-resp'));
|
|
1116
1299
|
return { handled: true };
|
|
1117
1300
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
printEccCommandList();
|
|
1126
|
-
return { handled: true };
|
|
1301
|
+
// ── ECC (everything-claude-code) — no slash commands ───
|
|
1302
|
+
// ECC is bundled, free, auto-installed on first launch, and used
|
|
1303
|
+
// automatically: built-in commands (/tdd /review /security-review /plan
|
|
1304
|
+
// /refactor /build-fix) use ECC prompts; ECC-only workflows
|
|
1305
|
+
// (feature-development, add-language-rules, database-migration) are
|
|
1306
|
+
// registered as auto-matchable skills — describe what you want and the
|
|
1307
|
+
// right workflow prompt injects itself. No /ecc-* slash commands needed.
|
|
1127
1308
|
// ── Config (trigger wizard) ───────────────────────
|
|
1128
1309
|
case '/config':
|
|
1129
1310
|
return { handled: true, shouldExit: false };
|
|
1130
1311
|
case '/exit':
|
|
1131
1312
|
case '/quit':
|
|
1132
1313
|
return { handled: true, shouldExit: true };
|
|
1133
|
-
// ── Default:
|
|
1314
|
+
// ── Default: unknown command ──────────────────────────
|
|
1134
1315
|
default: {
|
|
1135
|
-
if
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
}
|
|
1144
|
-
const available = listEccCommands();
|
|
1145
|
-
console.log(chalk.yellow(` Unknown ECC command: ${cmd}`));
|
|
1146
|
-
if (available.length) {
|
|
1147
|
-
console.log(chalk.dim(` Available: ${available.map(c => `/ecc-${c}`).join(', ')}`));
|
|
1148
|
-
}
|
|
1316
|
+
// Friendly migration aid: if user types any /ecc* command (muscle
|
|
1317
|
+
// memory from earlier versions), redirect them to natural language.
|
|
1318
|
+
if (cmd.startsWith('/ecc')) {
|
|
1319
|
+
console.log(chalk.dim(` ECC works automatically now — just describe what you want.`));
|
|
1320
|
+
console.log(chalk.dim(` Examples:`));
|
|
1321
|
+
console.log(chalk.dim(` "add a database migration for a users table"`));
|
|
1322
|
+
console.log(chalk.dim(` "implement a feature to export CSV"`));
|
|
1323
|
+
console.log(chalk.dim(` "add typescript coding rules to this project"`));
|
|
1149
1324
|
return { handled: true };
|
|
1150
1325
|
}
|
|
1151
1326
|
console.log(chalk.dim(` Unknown command: ${cmd}. Type /help`));
|
|
@@ -1177,6 +1352,12 @@ async function main() {
|
|
|
1177
1352
|
else {
|
|
1178
1353
|
config = loadConfig();
|
|
1179
1354
|
}
|
|
1355
|
+
// Install the screen-reader output filter if the user's config has it on.
|
|
1356
|
+
// Done as early as possible so every subsequent console.log (banner, hooks,
|
|
1357
|
+
// ECC install report, etc.) gets the filter applied uniformly.
|
|
1358
|
+
if (config.voice?.accessibility?.screenReader) {
|
|
1359
|
+
installScreenReaderDispatch(applyScreenReader);
|
|
1360
|
+
}
|
|
1180
1361
|
// Create session
|
|
1181
1362
|
const mode = { current: 'dev' };
|
|
1182
1363
|
const session = createSession(process.cwd(), config.model, config.provider, mode.current);
|
|
@@ -1190,8 +1371,8 @@ async function main() {
|
|
|
1190
1371
|
// Show startup display based on theme setting
|
|
1191
1372
|
const themeMode = config.theme || 'full';
|
|
1192
1373
|
if (themeMode === 'full') {
|
|
1193
|
-
// Full mode: splash
|
|
1194
|
-
|
|
1374
|
+
// Full mode: banner. ASCII splash removed per user request — both `full`
|
|
1375
|
+
// and `compact` themes now render the same banner block.
|
|
1195
1376
|
printThemedBanner(config.provider, config.model, mode.current, config.permissionMode, session.id, ALL_TOOLS.map((t) => t.name));
|
|
1196
1377
|
}
|
|
1197
1378
|
else if (themeMode === 'compact') {
|
|
@@ -1204,12 +1385,172 @@ async function main() {
|
|
|
1204
1385
|
console.log('');
|
|
1205
1386
|
}
|
|
1206
1387
|
let autoRoute = false;
|
|
1388
|
+
// ── F-key hotkey listener ────────────────────────────────
|
|
1389
|
+
// Voice / accessibility hotkeys. All keys are no-ops when voice is off, so
|
|
1390
|
+
// installing the listener unconditionally is safe and lets the user enable
|
|
1391
|
+
// voice mid-session without restarting.
|
|
1392
|
+
//
|
|
1393
|
+
// F1 = push-to-talk dictation (toggle: first press starts, second stops)
|
|
1394
|
+
// F2 = pause / resume current TTS playback
|
|
1395
|
+
// F3 = replay last TTS chunk
|
|
1396
|
+
// F4 = skip current TTS chunk
|
|
1397
|
+
// F5 = speed up TTS (×1.25)
|
|
1398
|
+
// F6 = slow down TTS (×0.8)
|
|
1399
|
+
//
|
|
1400
|
+
// We listen on the raw keypress stream. readline.emitKeypressEvents wires
|
|
1401
|
+
// every keystroke into 'keypress' events; we filter to F-keys only and
|
|
1402
|
+
// pass everything else through to readline's normal line-buffered reader.
|
|
1403
|
+
let dictateController = null;
|
|
1404
|
+
let dictateActive = false;
|
|
1405
|
+
let activePlaybackCtl = null;
|
|
1406
|
+
let lastSpokenChunk = null;
|
|
1407
|
+
// Track aborts so query.ts can register a fresh playback session
|
|
1408
|
+
globalThis.__voicePlaybackCtl = null;
|
|
1409
|
+
globalThis.__voiceLastChunk = null;
|
|
1410
|
+
// emitKeypressEvents sets up the keypress event source; we don't put stdin
|
|
1411
|
+
// in raw mode (readline does that as needed). Some platforms / terminals
|
|
1412
|
+
// don't deliver F-keys at all — failure here is a silent no-op.
|
|
1413
|
+
try {
|
|
1414
|
+
// emitKeypressEvents lives on the callback-flavor 'node:readline' module
|
|
1415
|
+
// (the promises variant doesn't expose it). Dynamically import it so we
|
|
1416
|
+
// don't add another top-level import.
|
|
1417
|
+
const readlineCb = await import('node:readline');
|
|
1418
|
+
readlineCb.emitKeypressEvents(stdin);
|
|
1419
|
+
stdin.on('keypress', (_str, key) => {
|
|
1420
|
+
if (!key)
|
|
1421
|
+
return;
|
|
1422
|
+
const name = String(key.name || '').toLowerCase();
|
|
1423
|
+
// Only intercept F1-F6; everything else falls through to readline
|
|
1424
|
+
if (!['f1', 'f2', 'f3', 'f4', 'f5', 'f6'].includes(name))
|
|
1425
|
+
return;
|
|
1426
|
+
if (!isVoiceEnabled(config)) {
|
|
1427
|
+
// Voice off — only show a one-time hint
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const a = getAccessibilityConfig(config);
|
|
1431
|
+
if (name === 'f1') {
|
|
1432
|
+
// Push-to-talk toggle
|
|
1433
|
+
if (dictateActive) {
|
|
1434
|
+
dictateActive = false;
|
|
1435
|
+
const ctl = dictateController;
|
|
1436
|
+
dictateController = null;
|
|
1437
|
+
if (!ctl)
|
|
1438
|
+
return;
|
|
1439
|
+
(async () => {
|
|
1440
|
+
if (a.audioCues)
|
|
1441
|
+
await audioCue('recording-stop');
|
|
1442
|
+
const buf = await ctl.stop();
|
|
1443
|
+
if (!buf) {
|
|
1444
|
+
console.log(chalk.dim(' [F1] no audio captured.'));
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
if (a.audioCues)
|
|
1448
|
+
await audioCue('processing');
|
|
1449
|
+
const { transcribeAudio } = await import('./voice.js');
|
|
1450
|
+
const transcript = await transcribeAudio(buf, config, 'wav');
|
|
1451
|
+
if (!transcript) {
|
|
1452
|
+
console.log(chalk.dim(' [F1] transcription failed.'));
|
|
1453
|
+
if (a.audioCues)
|
|
1454
|
+
await audioCue('error');
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (a.audioCues)
|
|
1458
|
+
await audioCue('done');
|
|
1459
|
+
// Hand the transcript to readline's input buffer. Most reliable
|
|
1460
|
+
// way is to print the transcript and let the user press Enter,
|
|
1461
|
+
// or — when autoSubmit is on — synthesize a newline so the
|
|
1462
|
+
// current rl.question() resolves with the text.
|
|
1463
|
+
const stt = getSttConfig(config);
|
|
1464
|
+
// We can't directly fill rl's buffer from outside; instead, we
|
|
1465
|
+
// write the transcript and a newline to stdin, which readline
|
|
1466
|
+
// picks up the same as if the user had typed it.
|
|
1467
|
+
stdin.write(transcript);
|
|
1468
|
+
if (stt.autoSubmit)
|
|
1469
|
+
stdin.write('\n');
|
|
1470
|
+
})();
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
// Start recording
|
|
1474
|
+
(async () => {
|
|
1475
|
+
const ok = await isFfmpegAvailable();
|
|
1476
|
+
if (!ok) {
|
|
1477
|
+
console.log(chalk.yellow(' [F1] ffmpeg not on PATH. Install ffmpeg to dictate.'));
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const ctl = await startRecording(60);
|
|
1481
|
+
if (!ctl) {
|
|
1482
|
+
console.log(chalk.yellow(' [F1] could not start mic capture.'));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
dictateController = ctl;
|
|
1486
|
+
dictateActive = true;
|
|
1487
|
+
if (a.audioCues)
|
|
1488
|
+
await audioCue('recording-start');
|
|
1489
|
+
console.log(chalk.dim(' [F1] recording — press F1 again to stop.'));
|
|
1490
|
+
})();
|
|
1491
|
+
}
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (name === 'f2') {
|
|
1495
|
+
// Pause / resume — implement as cancel current chunk; replay restarts.
|
|
1496
|
+
const g = globalThis;
|
|
1497
|
+
if (g.__voicePlaybackCtl && !g.__voicePlaybackCtl.signal.aborted) {
|
|
1498
|
+
g.__voicePlaybackCtl.abort();
|
|
1499
|
+
console.log(chalk.dim(' [F2] TTS paused.'));
|
|
1500
|
+
}
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (name === 'f3') {
|
|
1504
|
+
// Replay last chunk
|
|
1505
|
+
const g = globalThis;
|
|
1506
|
+
const chunk = g.__voiceLastChunk;
|
|
1507
|
+
if (!chunk) {
|
|
1508
|
+
console.log(chalk.dim(' [F3] nothing to replay.'));
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const tts = getTtsConfig(config);
|
|
1512
|
+
if (!tts.apiKey)
|
|
1513
|
+
return;
|
|
1514
|
+
(async () => {
|
|
1515
|
+
await speak(chunk, config, { voiceId: tts.assistantVoiceId });
|
|
1516
|
+
})();
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (name === 'f4') {
|
|
1520
|
+
// Skip = same as pause for now (chunked playback boundaries)
|
|
1521
|
+
const g = globalThis;
|
|
1522
|
+
if (g.__voicePlaybackCtl)
|
|
1523
|
+
g.__voicePlaybackCtl.abort();
|
|
1524
|
+
console.log(chalk.dim(' [F4] TTS skipped.'));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (name === 'f5' || name === 'f6') {
|
|
1528
|
+
config.voice = config.voice || {};
|
|
1529
|
+
const tts = config.voice.tts = { ...(config.voice.tts || {}) };
|
|
1530
|
+
const cur = tts.speed ?? 1.0;
|
|
1531
|
+
const next = name === 'f5' ? Math.min(2.0, cur * 1.25) : Math.max(0.5, cur * 0.8);
|
|
1532
|
+
tts.speed = Math.round(next * 100) / 100;
|
|
1533
|
+
saveConfig(config);
|
|
1534
|
+
console.log(chalk.dim(` [${name.toUpperCase()}] TTS speed: ${tts.speed}x`));
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
// No keypress support — accessibility users can still use /dictate.
|
|
1541
|
+
}
|
|
1542
|
+
// Session-start anchor — used by the [Nm Ns] tag prepended to every prompt
|
|
1543
|
+
// so the user can see at a glance how long the REPL has been open. Combined
|
|
1544
|
+
// with the per-chain timer printed after each model response (see runQuery),
|
|
1545
|
+
// gives both "how long am I here" and "how long was that last response."
|
|
1546
|
+
const sessionStartMs = new Date(session.createdAt).getTime();
|
|
1207
1547
|
// Main REPL loop
|
|
1208
1548
|
while (true) {
|
|
1209
1549
|
let input;
|
|
1210
1550
|
try {
|
|
1551
|
+
const sessionTag = theme.dim(`[${formatDuration(Date.now() - sessionStartMs)}] `);
|
|
1211
1552
|
const modeTag = mode.current !== 'dev' ? theme.dim(`[${mode.current}] `) : '';
|
|
1212
|
-
input = await rl.question(modeTag + theme.prompt(`${sym.prompt} `));
|
|
1553
|
+
input = await rl.question(sessionTag + modeTag + theme.prompt(`${sym.prompt} `));
|
|
1213
1554
|
}
|
|
1214
1555
|
catch {
|
|
1215
1556
|
break;
|
|
@@ -1254,7 +1595,22 @@ async function main() {
|
|
|
1254
1595
|
}
|
|
1255
1596
|
// Some commands inject a prompt into the conversation (e.g. /commit, /review, /tdd)
|
|
1256
1597
|
if (result.injectPrompt) {
|
|
1257
|
-
|
|
1598
|
+
// Special-case the /dictate flow: synthesize the prompt from the mic
|
|
1599
|
+
// before pushing it as a user message. We use the sentinel
|
|
1600
|
+
// "__DICTATE__<seconds>" so the slash handler stays purely sync.
|
|
1601
|
+
if (result.injectPrompt.startsWith('__DICTATE__')) {
|
|
1602
|
+
const maxSec = parseInt(result.injectPrompt.slice('__DICTATE__'.length), 10) || 30;
|
|
1603
|
+
const transcript = await dictateOnce(config, maxSec);
|
|
1604
|
+
if (!transcript) {
|
|
1605
|
+
console.log(chalk.dim(' [dictate] no transcript captured.'));
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
console.log(theme.dim(' [dictate] ') + chalk.white(transcript));
|
|
1609
|
+
messages.push({ role: 'user', content: transcript });
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
messages.push({ role: 'user', content: result.injectPrompt });
|
|
1613
|
+
}
|
|
1258
1614
|
await runQuery({ config, messages, cwd: process.cwd(), rl, sessionId: session.id, mode: mode.current });
|
|
1259
1615
|
await autoSave(session, messages);
|
|
1260
1616
|
continue;
|