compact-agent 1.24.2 → 1.26.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 (46) hide show
  1. package/README.md +140 -277
  2. package/bin/ecc-hooks.cjs +394 -394
  3. package/dist/codemaps.js +3 -2
  4. package/dist/codemaps.js.map +1 -1
  5. package/dist/config.d.ts +17 -0
  6. package/dist/config.js +89 -5
  7. package/dist/config.js.map +1 -1
  8. package/dist/cost-tracker.js +1 -1
  9. package/dist/ecc.js +10 -10
  10. package/dist/ecc.js.map +1 -1
  11. package/dist/hooks.js +3 -3
  12. package/dist/hooks.js.map +1 -1
  13. package/dist/index.js +203 -19
  14. package/dist/index.js.map +1 -1
  15. package/dist/learning.js +1 -1
  16. package/dist/login.js +1 -1
  17. package/dist/memory.js +1 -1
  18. package/dist/mempalace/index.d.ts +6 -2
  19. package/dist/mempalace/index.js +10 -3
  20. package/dist/mempalace/index.js.map +1 -1
  21. package/dist/mempalace/store.d.ts +2 -2
  22. package/dist/mempalace/store.js +5 -5
  23. package/dist/mempalace/store.js.map +1 -1
  24. package/dist/mempalace/types.d.ts +4 -4
  25. package/dist/mempalace/types.js +2 -2
  26. package/dist/package-detect.d.ts +2 -2
  27. package/dist/package-detect.js +8 -6
  28. package/dist/package-detect.js.map +1 -1
  29. package/dist/query.js +17 -0
  30. package/dist/query.js.map +1 -1
  31. package/dist/rules.js +795 -795
  32. package/dist/rules.js.map +1 -1
  33. package/dist/sessions.js +1 -1
  34. package/dist/skills.js +1 -1
  35. package/dist/stitch.js +132 -132
  36. package/dist/system-prompt.js +85 -85
  37. package/dist/theme.js +1 -1
  38. package/dist/theme.js.map +1 -1
  39. package/dist/tools/stitch.d.ts +1 -1
  40. package/dist/users.js +1 -1
  41. package/dist/voice.d.ts +6 -0
  42. package/dist/voice.js +16 -0
  43. package/dist/voice.js.map +1 -1
  44. package/dist/walkthrough.js +111 -111
  45. package/package.json +73 -68
  46. package/resources/ecc/skills/repo-scan/SKILL.md +15 -15
package/dist/index.js CHANGED
@@ -118,7 +118,7 @@ async function setupWizard(rl) {
118
118
  // user can make an informed choice — most users want this on.
119
119
  console.log(chalk.white('\n MemPalace persistent memory'));
120
120
  console.log(chalk.dim(' Lets the agent remember your preferences, codebase landmarks, and lessons across sessions.'));
121
- console.log(chalk.dim(' Two stores: global (~/.crowcoder/memory) for cross-project facts, project (.crowcoder/memory'));
121
+ console.log(chalk.dim(' Two stores: global (~/.compact-agent/memory) for cross-project facts, project (.compact-agent/memory'));
122
122
  console.log(chalk.dim(' in each repo) for codebase-specific knowledge. Searchable via /memory or by the agent itself.'));
123
123
  console.log(chalk.dim(' Zero external dependencies; storage is local JSON files. Can be toggled anytime via /memory disable.'));
124
124
  const memoryChoice = await rl.question(chalk.yellow(' Enable MemPalace memory? [Y/n]: '));
@@ -319,6 +319,8 @@ export function handleSlashCommand(input, config, messages, session, mode) {
319
319
  console.log(d(' ') + c('/accessibility') + d(' — toggle screen-reader mode, audio cues, destructive-confirm'));
320
320
  console.log(d(' Status hotkeys: F1 what now · F2 where am I · F3 read full · F4 read summary'));
321
321
  console.log(d(' Playback hotkeys: F5 dictate · F6 pause · F7 replay · F8 skip · F9 speed+ · F10 speed–'));
322
+ console.log(d(' Read hotkeys: F11 input buffer · F12 your last turn'));
323
+ console.log(d(' Shift+Fn: Shift+F1 queued · F2 key pool · F3 last tool · F4 toggle SR · F5 cancel · F6 panic · F12 hotkey list'));
322
324
  console.log(h('\n ── Stitch (Google AI UI/UX design) ──'));
323
325
  console.log(d(' Use ') + c('/mode design') + d(' or ') + c('/design <task>') + d(' for UI work — the agent uses Stitch automatically.'));
324
326
  console.log(d(' ') + c('/stitch') + d(' — show config status'));
@@ -707,7 +709,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
707
709
  case '/hooks': {
708
710
  const hooks = listHooks();
709
711
  if (hooks.length === 0) {
710
- console.log(chalk.dim(' No hooks configured. Edit ~/.crowcoder/hooks.json'));
712
+ console.log(chalk.dim(' No hooks configured. Edit ~/.compact-agent/hooks.json'));
711
713
  }
712
714
  else {
713
715
  console.log(chalk.cyan(`\n Hooks (${hooks.length}):`));
@@ -932,7 +934,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
932
934
  if (items.length > 20)
933
935
  console.log(chalk.dim(` … and ${items.length - 20} more`));
934
936
  }
935
- console.log(chalk.dim('\n Act on findings with /prune (instincts) or by editing ~/.crowcoder/skills/.\n'));
937
+ console.log(chalk.dim('\n Act on findings with /prune (instincts) or by editing ~/.compact-agent/skills/.\n'));
936
938
  return { handled: true };
937
939
  }
938
940
  // ── ECC agent shortcuts (5 commands, ECC audit item 4) ─────
@@ -1580,7 +1582,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1580
1582
  return { handled: true };
1581
1583
  }
1582
1584
  saveStitchConfig(key);
1583
- console.log(chalk.green(` Stitch API key saved to ~/.crowcoder/stitch.json`));
1585
+ console.log(chalk.green(` Stitch API key saved to ~/.compact-agent/stitch.json`));
1584
1586
  console.log(chalk.dim(' The `stitch` tool is now available to the agent.'));
1585
1587
  console.log(chalk.dim(' Restart the REPL for the tool to appear in /tools.'));
1586
1588
  return { handled: true };
@@ -1707,7 +1709,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
1707
1709
  return { handled: true };
1708
1710
  }
1709
1711
  // ── Reset hooks (clear stale entries from old installs) ──
1710
- // Wipes ~/.crowcoder/hooks.json, clears the in-memory quarantine, and
1712
+ // Wipes ~/.compact-agent/hooks.json, clears the in-memory quarantine, and
1711
1713
  // re-seeds the ECC default hooks against this install's bin path. Use
1712
1714
  // this when stale dev-machine paths from a prior install are crashing
1713
1715
  // every tool call.
@@ -2128,36 +2130,218 @@ async function main() {
2128
2130
  const { describeStatus, describeLocation } = await import('./status.js');
2129
2131
  readlineCb.emitKeypressEvents(stdin);
2130
2132
  // Set of keys we intercept. Anything not in this set falls through to
2131
- // readline so normal typing isn't affected. All bare F-keys; no
2132
- // modifiers needed, no screen-reader conflicts.
2133
+ // readline so normal typing isn't affected. All bare or shifted F-keys
2134
+ // no Insert/CapsLock/Ctrl-Option modifiers, so we never collide with
2135
+ // NVDA, JAWS, Narrator, Orca, or VoiceOver. F11 + F12 are also browser-
2136
+ // reserved keys (fullscreen / devtools) and therefore reliably free in
2137
+ // every terminal that isn't masquerading as a browser.
2133
2138
  const INTERCEPT = new Set([
2134
- 'f1', 'f2', 'f3', 'f4', // status announcements
2135
- 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', // dictation + playback
2139
+ 'f1', 'f2', 'f3', 'f4', // status announcements (bare)
2140
+ 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', // dictation + playback (bare)
2141
+ 'f11', 'f12', // Tier 1: input + last turn (bare)
2142
+ // Shifted F-keys carry the Tier-2 and Tier-3 a11y functions. Each
2143
+ // is checked alongside key.shift below, so a bare F1 still routes
2144
+ // to "status" while Shift+F1 routes to "queued input."
2136
2145
  ]);
2137
2146
  // Define the hotkey listener as a NAMED, TAGGED function so
2138
2147
  // suppressInputDuringStream() in query.ts can isolate it among stdin's
2139
2148
  // 'keypress' listeners. During streaming we detach readline's own
2140
2149
  // keypress listener (to prevent echo + line-buffer pollution) while
2141
- // keeping this one attached so F1–F10 keep working mid-response.
2150
+ // keeping this one attached so F1–F12 keep working mid-response.
2142
2151
  const hotkeyListener = function hotkeyListener(_str, key) {
2143
2152
  if (!key)
2144
2153
  return;
2145
2154
  const name = String(key.name || '').toLowerCase();
2146
2155
  if (!INTERCEPT.has(name))
2147
2156
  return;
2157
+ const shift = !!key.shift;
2148
2158
  const a = getAccessibilityConfig(config);
2149
2159
  const tts = getTtsConfig(config);
2150
- // F1–F4 are STATUS hotkeys. They always work, even when voice is off
2151
- // and even when there's no TTS key they print the status line to
2152
- // stdout regardless. TTS is only added on top when a key is present.
2153
- // The whole point of these keys is "tell me what's happening", which
2154
- // is just as useful via text + screen reader as via voice.
2155
- const isStatusKey = name === 'f1' || name === 'f2' || name === 'f3' || name === 'f4';
2156
- // F5–F10 are DICTATION/PLAYBACK hotkeys they only make sense when
2157
- // voice features are enabled. Bail early to avoid spurious ffmpeg
2158
- // spawns and "TTS not configured" log lines.
2160
+ // Helper: print to stdout (always picked up by the OS screen reader)
2161
+ // and optionally layer TTS on top if a key is configured. Used by
2162
+ // every "announce something" branch in the new tier of bindings.
2163
+ const announce = (label, text) => {
2164
+ console.log(chalk.dim(` [${label}] `) + text);
2165
+ if (tts.apiKey) {
2166
+ speak(text, config, { voiceId: tts.assistantVoiceId }).catch(() => { });
2167
+ }
2168
+ };
2169
+ // STATUS hotkeys always work, even when voice is off and even when
2170
+ // there's no TTS key — they print to stdout so an OS-level screen
2171
+ // reader still has something to announce. TTS layers on top when a
2172
+ // key is present. Applies to:
2173
+ // - F1–F4 : original status / location / replay set
2174
+ // - F11/F12 : input buffer / last user turn (Tier 1)
2175
+ // - Shift+* : every shifted F-key is information or control,
2176
+ // never voice-only
2177
+ const isStatusKey = name === 'f1' || name === 'f2' || name === 'f3' || name === 'f4' ||
2178
+ name === 'f11' || name === 'f12' || shift;
2179
+ // F5–F10 (bare) are DICTATION/PLAYBACK hotkeys — they only make
2180
+ // sense when voice features are enabled. Bail early to avoid
2181
+ // spurious ffmpeg spawns and "TTS not configured" log lines.
2159
2182
  if (!isStatusKey && !isVoiceEnabled(config))
2160
2183
  return;
2184
+ // ──────────────────────────────────────────────────────────────
2185
+ // Tier 2 + 3: shifted F-keys.
2186
+ //
2187
+ // Dispatched BEFORE the bare F-key branches because Shift+F5
2188
+ // shares its `name` ('f5') with the bare F5 dictation toggle —
2189
+ // we want the shifted variant to win without each bare branch
2190
+ // having to add a `!shift` guard.
2191
+ //
2192
+ // Shift+F1 queued input ("3 messages queued: …")
2193
+ // Shift+F2 key-pool health ("3 keys healthy, 1 cooling")
2194
+ // Shift+F3 last tool-call ("bash: ok, 'ls -la' → …")
2195
+ // Shift+F4 toggle screen-reader (persists to config.json)
2196
+ // Shift+F5 soft-cancel turn (graceful abort, partial kept)
2197
+ // Shift+F6 panic-stop TTS (silences for 5s, drops queue)
2198
+ // Shift+F12 read hotkey list (discoverability)
2199
+ //
2200
+ // Unbound shifted F-keys are no-ops and fall through (returning
2201
+ // here keeps them out of the bare-F-key branches below).
2202
+ // ──────────────────────────────────────────────────────────────
2203
+ if (shift) {
2204
+ // ── Shift+F1: queued input ─────────────────────────
2205
+ if (name === 'f1') {
2206
+ const g = globalThis;
2207
+ const q = (g.__crowcoderQueuedInput || '').trim();
2208
+ announce('Shift+F1', q
2209
+ ? `Queued during last chain: ${q.slice(0, 200)}`
2210
+ : 'Nothing queued.');
2211
+ return;
2212
+ }
2213
+ // ── Shift+F2: key-pool health ──────────────────────
2214
+ if (name === 'f2') {
2215
+ const ks = keyPoolStatus();
2216
+ if (ks.length === 0) {
2217
+ announce('Shift+F2', 'Key pool: 1 key (no pool configured). Use /keys add to add more.');
2218
+ return;
2219
+ }
2220
+ const healthy = ks.filter((s) => s.healthy).length;
2221
+ const cooling = ks.length - healthy;
2222
+ const cooldownNotes = ks
2223
+ .filter((s) => !s.healthy && s.coolDownRemainingSec)
2224
+ .map((s) => `${s.tail} cooling ${s.coolDownRemainingSec}s`)
2225
+ .join(', ');
2226
+ const text = cooling > 0
2227
+ ? `Key pool: ${healthy} healthy, ${cooling} cooling. ${cooldownNotes}.`
2228
+ : `Key pool: ${healthy} healthy, all keys ready.`;
2229
+ announce('Shift+F2', text);
2230
+ return;
2231
+ }
2232
+ // ── Shift+F3: last tool call ───────────────────────
2233
+ if (name === 'f3') {
2234
+ const g = globalThis;
2235
+ const tc = g.__lastToolCall;
2236
+ if (!tc) {
2237
+ announce('Shift+F3', 'No tool calls yet this session.');
2238
+ return;
2239
+ }
2240
+ const status = tc.isError ? 'error' : 'ok';
2241
+ // Output preview kept short for TTS; full output is already on
2242
+ // stdout from the original tool-call print.
2243
+ announce('Shift+F3', `Last tool: ${tc.name}, ${status}. ${tc.argsPreview}${tc.outputPreview ? ' → ' + tc.outputPreview.slice(0, 100) : ''}`);
2244
+ return;
2245
+ }
2246
+ // ── Shift+F4: toggle screen-reader mode ────────────
2247
+ if (name === 'f4') {
2248
+ config.voice = config.voice || {};
2249
+ config.voice.accessibility = config.voice.accessibility || {};
2250
+ const cur = config.voice.accessibility.screenReader === true;
2251
+ config.voice.accessibility.screenReader = !cur;
2252
+ saveConfig(config);
2253
+ const text = !cur
2254
+ ? 'Screen-reader mode ON. ANSI colors stripped. Restart recommended for full effect.'
2255
+ : 'Screen-reader mode OFF. Colors restored on next prompt.';
2256
+ announce('Shift+F4', text);
2257
+ return;
2258
+ }
2259
+ // ── Shift+F5: soft-cancel current turn ─────────────
2260
+ if (name === 'f5') {
2261
+ const g = globalThis;
2262
+ if (g.__turnAbortCtl && !g.__turnAbortCtl.signal.aborted) {
2263
+ try {
2264
+ g.__turnAbortCtl.abort();
2265
+ }
2266
+ catch { /* noop */ }
2267
+ announce('Shift+F5', 'Turn cancelled. Partial response kept.');
2268
+ }
2269
+ else {
2270
+ announce('Shift+F5', 'No turn in progress.');
2271
+ }
2272
+ return;
2273
+ }
2274
+ // ── Shift+F6: panic-stop TTS ───────────────────────
2275
+ if (name === 'f6') {
2276
+ // Abort the current playback (same as F6/F8) AND open a 5-second
2277
+ // suppression window so incidental utterances (error
2278
+ // announcements, mode switches, audio cues fired by other code
2279
+ // paths) can't immediately fill the silence.
2280
+ const g = globalThis;
2281
+ if (g.__voicePlaybackCtl && !g.__voicePlaybackCtl.signal.aborted) {
2282
+ try {
2283
+ g.__voicePlaybackCtl.abort();
2284
+ }
2285
+ catch { /* noop */ }
2286
+ }
2287
+ g.__voiceSuppressUntilMs = Date.now() + 5000;
2288
+ // Print only — don't speak this acknowledgement (would defeat
2289
+ // the purpose of "shut up now").
2290
+ console.log(chalk.dim(' [Shift+F6] TTS panic-stop — silenced for 5s.'));
2291
+ return;
2292
+ }
2293
+ // ── Shift+F12: read hotkey list ────────────────────
2294
+ if (name === 'f12') {
2295
+ const lines = [
2296
+ 'Hotkey reference.',
2297
+ 'F1 status. F2 location. F3 read full last response. F4 read summary.',
2298
+ 'F5 dictate. F6 pause. F7 replay. F8 skip. F9 speed up. F10 slow down.',
2299
+ 'F11 read input buffer. F12 read your previous turn.',
2300
+ 'Shift+F1 queued input. Shift+F2 key pool. Shift+F3 last tool. Shift+F4 toggle screen-reader.',
2301
+ 'Shift+F5 soft-cancel turn. Shift+F6 panic-stop TTS. Shift+F12 this list.',
2302
+ ];
2303
+ for (const ln of lines)
2304
+ console.log(chalk.dim(' [Shift+F12] ') + ln);
2305
+ if (tts.apiKey) {
2306
+ // Speak as one continuous string so the chunker can pace it.
2307
+ speak(lines.join(' '), config, { voiceId: tts.assistantVoiceId }).catch(() => { });
2308
+ }
2309
+ return;
2310
+ }
2311
+ // Any other shifted F-key: no-op (don't fall through to bare).
2312
+ return;
2313
+ }
2314
+ // ── F11: read current input buffer (Tier 1, bare) ──
2315
+ if (name === 'f11') {
2316
+ // rl.line is readline's internal "what the user has typed so far
2317
+ // on the current prompt." Empty string when the prompt is fresh
2318
+ // or the buffer was just submitted.
2319
+ const buf = rl.line ?? '';
2320
+ announce('F11', buf
2321
+ ? `Input buffer: ${buf}`
2322
+ : 'Input buffer is empty.');
2323
+ return;
2324
+ }
2325
+ // ── F12: read previous submitted user turn (Tier 1) ──
2326
+ if (name === 'f12') {
2327
+ // Walk messages newest-first looking for the most-recent user
2328
+ // message. `messages` is the live REPL conversation array; the
2329
+ // last user entry is the prompt the model just answered (or is
2330
+ // answering). Skips system-injected "auto-resume" markers and
2331
+ // tool-result envelopes (those have role 'tool', not 'user').
2332
+ let last = null;
2333
+ for (let i = messages.length - 1; i >= 0; i--) {
2334
+ const m = messages[i];
2335
+ if (m.role === 'user' && typeof m.content === 'string' && m.content.trim()) {
2336
+ last = m.content;
2337
+ break;
2338
+ }
2339
+ }
2340
+ announce('F12', last
2341
+ ? `Your last message: ${last.slice(0, 400)}`
2342
+ : 'No prior user message this session.');
2343
+ return;
2344
+ }
2161
2345
  // ── F5: push-to-talk dictation toggle ──────────────
2162
2346
  if (name === 'f5') {
2163
2347
  if (dictateActive) {