compact-agent 1.26.1 → 1.27.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/bin/ecc-hooks.cjs +28 -1
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.js +15 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.js +221 -7
- package/dist/index.js.map +1 -1
- package/dist/query.js +61 -0
- package/dist/query.js.map +1 -1
- package/package.json +1 -1
package/bin/ecc-hooks.cjs
CHANGED
|
@@ -162,6 +162,32 @@ const checks = {
|
|
|
162
162
|
* Upstream credit: github.com/zunoworks/gateguard (the underlying idea).
|
|
163
163
|
*/
|
|
164
164
|
'gateguard': () => {
|
|
165
|
+
// ── Disable knob ─────────────────────────────────────
|
|
166
|
+
// Documented in the block message below. Accepts the new
|
|
167
|
+
// COMPACT_AGENT_GATEGUARD env var primarily, with the legacy
|
|
168
|
+
// CROWCODER_GATEGUARD kept as an alias so the previous docs +
|
|
169
|
+
// muscle memory still work.
|
|
170
|
+
const disableEnv = (
|
|
171
|
+
process.env.COMPACT_AGENT_GATEGUARD ||
|
|
172
|
+
process.env.CROWCODER_GATEGUARD ||
|
|
173
|
+
''
|
|
174
|
+
).trim();
|
|
175
|
+
if (/^(off|false|0|no|disabled?)$/i.test(disableEnv)) return ok();
|
|
176
|
+
|
|
177
|
+
// ── yolo bypass ──────────────────────────────────────
|
|
178
|
+
// Permission mode 'yolo' is the user's explicit "trust the agent,
|
|
179
|
+
// skip the speed bumps" contract. GateGuard's investigate-first
|
|
180
|
+
// intervention directly contradicts that — letting it fire in
|
|
181
|
+
// yolo would mean the safest setting in compact-agent is more
|
|
182
|
+
// pedantic than the most-cautious, which is backwards. Silent
|
|
183
|
+
// no-op so the user gets the unblocked flow they asked for.
|
|
184
|
+
const perm = (
|
|
185
|
+
process.env.COMPACT_AGENT_PERMISSION_MODE ||
|
|
186
|
+
process.env.CROWCODER_PERMISSION_MODE ||
|
|
187
|
+
''
|
|
188
|
+
).toLowerCase().trim();
|
|
189
|
+
if (perm === 'yolo') return ok();
|
|
190
|
+
|
|
165
191
|
const fs = require('fs');
|
|
166
192
|
const pathMod = require('path');
|
|
167
193
|
const os = require('os');
|
|
@@ -209,7 +235,8 @@ const checks = {
|
|
|
209
235
|
`(2) Grep for importers / callers / refs so the change doesn't break ` +
|
|
210
236
|
`consumers. (3) If it's a schema/type, check existing data usage. ` +
|
|
211
237
|
`After investigating, retry the edit — GateGuard tracks per-file and ` +
|
|
212
|
-
`will let the retry through. Set
|
|
238
|
+
`will let the retry through. Set COMPACT_AGENT_GATEGUARD=off to disable, ` +
|
|
239
|
+
`or use /perm yolo for a session-wide bypass.`,
|
|
213
240
|
);
|
|
214
241
|
},
|
|
215
242
|
|
package/dist/hooks.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface HookContext {
|
|
|
17
17
|
toolOutput?: string;
|
|
18
18
|
sessionId?: string;
|
|
19
19
|
cwd: string;
|
|
20
|
+
/**
|
|
21
|
+
* Current permission mode at the time the hook fires. Passed to the
|
|
22
|
+
* hook script as $COMPACT_AGENT_PERMISSION_MODE / $CROWCODER_PERMISSION_MODE
|
|
23
|
+
* so checks like GateGuard can no-op in 'yolo' (where the user has
|
|
24
|
+
* explicitly opted in to "approve everything" and pedantic gates
|
|
25
|
+
* contradict that contract).
|
|
26
|
+
*/
|
|
27
|
+
permissionMode?: string;
|
|
20
28
|
}
|
|
21
29
|
export interface HookResult {
|
|
22
30
|
allowed: boolean;
|
package/dist/hooks.js
CHANGED
|
@@ -102,6 +102,13 @@ export async function runHooks(ctx) {
|
|
|
102
102
|
if (!shouldRunHook(hookId, ctx.event)) {
|
|
103
103
|
continue;
|
|
104
104
|
}
|
|
105
|
+
// Hooks receive the active context as env vars. Both the legacy
|
|
106
|
+
// CROWCODER_* names AND the new COMPACT_AGENT_* names are exported
|
|
107
|
+
// so user-written hooks that read either form keep working. The
|
|
108
|
+
// permission mode is new — added so GateGuard (and any future
|
|
109
|
+
// mode-aware hook) can no-op in 'yolo' instead of fighting the
|
|
110
|
+
// user's explicit trust setting.
|
|
111
|
+
const perm = ctx.permissionMode || '';
|
|
105
112
|
const env = {
|
|
106
113
|
...process.env,
|
|
107
114
|
CROWCODER_EVENT: ctx.event,
|
|
@@ -110,6 +117,14 @@ export async function runHooks(ctx) {
|
|
|
110
117
|
CROWCODER_TOOL_OUTPUT: ctx.toolOutput || '',
|
|
111
118
|
CROWCODER_SESSION_ID: ctx.sessionId || '',
|
|
112
119
|
CROWCODER_CWD: ctx.cwd,
|
|
120
|
+
CROWCODER_PERMISSION_MODE: perm,
|
|
121
|
+
COMPACT_AGENT_EVENT: ctx.event,
|
|
122
|
+
COMPACT_AGENT_TOOL: ctx.toolName || '',
|
|
123
|
+
COMPACT_AGENT_TOOL_INPUT: ctx.toolInput ? JSON.stringify(ctx.toolInput) : '',
|
|
124
|
+
COMPACT_AGENT_TOOL_OUTPUT: ctx.toolOutput || '',
|
|
125
|
+
COMPACT_AGENT_SESSION_ID: ctx.sessionId || '',
|
|
126
|
+
COMPACT_AGENT_CWD: ctx.cwd,
|
|
127
|
+
COMPACT_AGENT_PERMISSION_MODE: perm,
|
|
113
128
|
};
|
|
114
129
|
const isBlocking = hook.blocking ?? (ctx.event === 'PreToolUse');
|
|
115
130
|
const timeout = hook.timeout ?? 10_000;
|
package/dist/hooks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,OAAO,CAAC,CAAC;AAChD,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,EAAE,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as readline from 'node:readline/promises';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { readFileSync as fsReadFileSync, writeFileSync as fsWriteFileSync } from 'node:fs';
|
|
4
|
+
import { readFileSync as fsReadFileSync, writeFileSync as fsWriteFileSync, unlinkSync as fsUnlinkSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join as pathJoin } from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
5
8
|
import chalk from 'chalk';
|
|
6
9
|
import { loadConfig, saveConfig, configExists, getConfigDir } from './config.js';
|
|
7
10
|
import { resetClient } from './api.js';
|
|
@@ -9,7 +12,7 @@ import { runQuery } from './query.js';
|
|
|
9
12
|
import { ALL_TOOLS } from './tools/index.js';
|
|
10
13
|
import { PROVIDERS } from './types.js';
|
|
11
14
|
// New systems
|
|
12
|
-
import { createSession, autoSave, listSessions, loadSession, deleteSession } from './sessions.js';
|
|
15
|
+
import { createSession, autoSave, listSessions, loadSession, deleteSession, saveSession, generateSessionId } from './sessions.js';
|
|
13
16
|
import { initHooksDir, runHooks, listHooks, saveHooksConfig, clearQuarantinedHooks } from './hooks.js';
|
|
14
17
|
import { printUsageSummary, setBudget } from './cost-tracker.js';
|
|
15
18
|
import { getCompactionStats } from './compaction.js';
|
|
@@ -221,12 +224,20 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
221
224
|
console.log(d(' ') + c('/palettes') + d(' — list available color palettes with preview'));
|
|
222
225
|
console.log(d(' ') + c('/clear') + d(' — clear conversation'));
|
|
223
226
|
console.log(d(' ') + c('/back [n]') + d(' — rewind to before the nth most-recent user turn (no arg lists turns)'));
|
|
227
|
+
console.log(d(' ') + c('/fork [name]') + d(' — branch current conversation; previous session reachable via /resume (alias: /branch)'));
|
|
228
|
+
console.log(d(' ') + c('/btw <question>') + d(' — side question, model knows not to factor into the main thread'));
|
|
229
|
+
console.log(d(' ') + c('/editor [seed]') + d(' — open $EDITOR / $VISUAL on a tempfile for long prompts (alias: /edit-prompt)'));
|
|
224
230
|
console.log(d(' ') + c('/history') + d(' — message count & token estimate'));
|
|
225
231
|
console.log(d(' ') + c('/export [fmt]') + d(' — export conversation (md/json/txt)'));
|
|
226
232
|
console.log(d(' ') + c('/exit') + d(' — quit (alias: /quit)'));
|
|
227
233
|
console.log(d(' ') + c('/walkthrough') + d(' — agent-led tour of Crowcoder (aliases: /tour, /guide)'));
|
|
228
234
|
console.log(d(' ') + c('!<cmd>') + d(' — run shell command directly'));
|
|
229
|
-
console.log(
|
|
235
|
+
console.log(h('\n ── Productivity hotkeys ──'));
|
|
236
|
+
console.log(d(' ') + c('Shift+Tab') + d(' — cycle permission modes (ask → auto → yolo)'));
|
|
237
|
+
console.log(d(' ') + c('Esc') + d(' — interrupt current turn (alias for Ctrl+G steer; both work)'));
|
|
238
|
+
console.log(d(' ') + c('Esc Esc') + d(' — rewind to the previous user turn (at empty prompt)'));
|
|
239
|
+
console.log(d(' ') + c('Alt+, / Alt+.') + d(' — temperature − / + by 0.1 (more careful / more creative)'));
|
|
240
|
+
console.log(d(' ') + c('Ctrl+G') + d(' — steer (legacy alias for Esc)'));
|
|
230
241
|
console.log(h('\n ── Model & Provider ──'));
|
|
231
242
|
console.log(d(' ') + c('/model [name]') + d(' — switch or show model'));
|
|
232
243
|
console.log(d(' ') + c('/models') + d(' — list available models for provider'));
|
|
@@ -431,6 +442,95 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
431
442
|
console.log(chalk.green(` Rewound to before user turn ${n} (dropped ${dropped} message(s)).`));
|
|
432
443
|
return { handled: true, newMessages };
|
|
433
444
|
}
|
|
445
|
+
// ── Fork — branch the current conversation ────────
|
|
446
|
+
// Borrowed from both Claude Code (/branch) and Codex CLI (/fork).
|
|
447
|
+
// Snapshots the current session under its existing ID (so /resume
|
|
448
|
+
// can return to this point), then re-anchors the live REPL to a
|
|
449
|
+
// FRESH session ID that starts with a copy of all current messages.
|
|
450
|
+
// From here, the two branches diverge — exploring a tangent in the
|
|
451
|
+
// fork doesn't touch the original.
|
|
452
|
+
case '/fork':
|
|
453
|
+
case '/branch': {
|
|
454
|
+
const forkName = args.trim() || `fork of ${session.name}`;
|
|
455
|
+
// Save the current session under its existing ID first so the
|
|
456
|
+
// pre-fork state is recoverable via /resume.
|
|
457
|
+
saveSession({ ...session, messages: [...messages] }).catch(() => { });
|
|
458
|
+
const previousId = session.id;
|
|
459
|
+
// Mutate the active session in place: new ID + name + timestamps,
|
|
460
|
+
// same messages. The REPL keeps running against `session` so
|
|
461
|
+
// mutation is enough — no need to plumb a "swap" through the
|
|
462
|
+
// return value.
|
|
463
|
+
session.id = generateSessionId();
|
|
464
|
+
session.name = forkName;
|
|
465
|
+
session.createdAt = new Date().toISOString();
|
|
466
|
+
session.updatedAt = session.createdAt;
|
|
467
|
+
saveSession({ ...session, messages: [...messages] }).catch(() => { });
|
|
468
|
+
console.log(chalk.green(` Forked session.`));
|
|
469
|
+
console.log(chalk.dim(` Previous: ${previousId} (use /resume to return)`));
|
|
470
|
+
console.log(chalk.dim(` Active: ${session.id} "${forkName}"`));
|
|
471
|
+
return { handled: true };
|
|
472
|
+
}
|
|
473
|
+
// ── BTW — side question without main-thread pollution ──
|
|
474
|
+
// Borrowed from Claude Code's /btw. The model sees the question
|
|
475
|
+
// with a marker so it knows the answer shouldn't influence the
|
|
476
|
+
// ongoing task; the user gets a real response but the message
|
|
477
|
+
// pair is flagged in history so /back N treats it as one
|
|
478
|
+
// "compound turn" to skip.
|
|
479
|
+
//
|
|
480
|
+
// V1 caveat: the messages DO still go into history (otherwise the
|
|
481
|
+
// model can't respond at all). The marker is the contract.
|
|
482
|
+
// Recover from a noisy /btw with /back 1.
|
|
483
|
+
case '/btw': {
|
|
484
|
+
const q = args.trim();
|
|
485
|
+
if (!q) {
|
|
486
|
+
console.log(chalk.yellow(' Usage: /btw <question> — side question, model knows not to factor into the main thread.'));
|
|
487
|
+
return { handled: true };
|
|
488
|
+
}
|
|
489
|
+
const wrapped = '[SIDE QUESTION — do NOT integrate this answer into the ongoing task. ' +
|
|
490
|
+
'Answer briefly, then return to the prior context on the next turn.] ' + q;
|
|
491
|
+
return { handled: true, injectPrompt: wrapped };
|
|
492
|
+
}
|
|
493
|
+
// ── Editor — open $EDITOR on a tempfile for long prompts ──
|
|
494
|
+
// Universal Unix idiom (bash's Ctrl+X Ctrl+E, vim's edit-and-resubmit).
|
|
495
|
+
// Useful when the prompt is long, multi-line, or you want syntax
|
|
496
|
+
// highlighting / paste-from-buffer that the REPL's single-line
|
|
497
|
+
// input doesn't give you. Falls back to nano if $EDITOR is unset.
|
|
498
|
+
case '/editor':
|
|
499
|
+
case '/edit-prompt': {
|
|
500
|
+
const editor = process.env.VISUAL || process.env.EDITOR ||
|
|
501
|
+
(process.platform === 'win32' ? 'notepad' : 'nano');
|
|
502
|
+
let result;
|
|
503
|
+
try {
|
|
504
|
+
const tmpPath = pathJoin(tmpdir(), `compact-agent-prompt-${Date.now()}.md`);
|
|
505
|
+
// Seed with current input buffer if any, plus a help comment.
|
|
506
|
+
const seed = (args.trim() ? args : '') +
|
|
507
|
+
(args.trim() ? '\n\n' : '') +
|
|
508
|
+
'<!-- Write your prompt here. Save + close to send. Empty file = cancel. -->\n';
|
|
509
|
+
fsWriteFileSync(tmpPath, seed, 'utf-8');
|
|
510
|
+
const r = spawnSync(editor, [tmpPath], { stdio: 'inherit' });
|
|
511
|
+
if (r.error) {
|
|
512
|
+
console.log(chalk.yellow(` Could not launch ${editor}: ${r.error.message}`));
|
|
513
|
+
return { handled: true };
|
|
514
|
+
}
|
|
515
|
+
result = fsReadFileSync(tmpPath, 'utf-8');
|
|
516
|
+
// Strip the help comment + any trailing whitespace
|
|
517
|
+
result = result.replace(/<!--[\s\S]*?-->/g, '').trim();
|
|
518
|
+
try {
|
|
519
|
+
fsUnlinkSync(tmpPath);
|
|
520
|
+
}
|
|
521
|
+
catch { /* noop */ }
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
console.log(chalk.yellow(` /editor failed: ${err instanceof Error ? err.message : err}`));
|
|
525
|
+
return { handled: true };
|
|
526
|
+
}
|
|
527
|
+
if (!result) {
|
|
528
|
+
console.log(chalk.dim(' Empty — nothing to send.'));
|
|
529
|
+
return { handled: true };
|
|
530
|
+
}
|
|
531
|
+
console.log(chalk.dim(` Sending ${result.length} chars from editor…`));
|
|
532
|
+
return { handled: true, injectPrompt: result };
|
|
533
|
+
}
|
|
434
534
|
// ── History ───────────────────────────────────────
|
|
435
535
|
case '/history': {
|
|
436
536
|
const stats = getCompactionStats(messages);
|
|
@@ -2092,7 +2192,7 @@ async function main() {
|
|
|
2092
2192
|
const session = createSession(process.cwd(), config.model, config.provider, mode.current);
|
|
2093
2193
|
const messages = [];
|
|
2094
2194
|
// Session start hook + memory persistence
|
|
2095
|
-
await runHooks({ event: 'SessionStart', sessionId: session.id, cwd: process.cwd() });
|
|
2195
|
+
await runHooks({ event: 'SessionStart', sessionId: session.id, cwd: process.cwd(), permissionMode: config.permissionMode });
|
|
2096
2196
|
const memoryContext = onSessionStart(session.id, process.cwd());
|
|
2097
2197
|
if (memoryContext) {
|
|
2098
2198
|
messages.push({ role: 'system', content: memoryContext });
|
|
@@ -2110,7 +2210,27 @@ async function main() {
|
|
|
2110
2210
|
}
|
|
2111
2211
|
else {
|
|
2112
2212
|
// Minimal mode: just a one-liner
|
|
2113
|
-
console.log(theme.brandBold('Compact Agent
|
|
2213
|
+
console.log(theme.brandBold('Compact Agent') + theme.dim(' — terminal AI coding CLI'));
|
|
2214
|
+
console.log('');
|
|
2215
|
+
}
|
|
2216
|
+
// ── Flaky-model warning at REPL launch ───────────────
|
|
2217
|
+
// The setup wizard already warns when a user TYPES one of these
|
|
2218
|
+
// experimental free models, but returning users whose config was
|
|
2219
|
+
// saved before that check landed never see the warning. Print it
|
|
2220
|
+
// every launch so they get a chance to switch via /model before
|
|
2221
|
+
// they hit the "model returns nothing, REPL looks frozen" footgun.
|
|
2222
|
+
const flakyPatterns = [
|
|
2223
|
+
'owl-alpha', 'horizon-alpha', 'horizon-beta',
|
|
2224
|
+
'optimus-alpha', 'quasar-alpha',
|
|
2225
|
+
];
|
|
2226
|
+
const lowerModelAtLaunch = (config.model || '').toLowerCase();
|
|
2227
|
+
if (flakyPatterns.some((p) => lowerModelAtLaunch.includes(p))) {
|
|
2228
|
+
console.log(theme.warning(` ⚠ Active model "${config.model}" is an experimental / free model known to`));
|
|
2229
|
+
console.log(theme.warning(` return empty or "ERROR" responses, or get stuck in token loops.`));
|
|
2230
|
+
console.log(theme.dim(` Switch with /model <id>. Reliable free options on OpenRouter:`));
|
|
2231
|
+
console.log(theme.dim(` /model meta-llama/llama-3.3-70b-instruct:free`));
|
|
2232
|
+
console.log(theme.dim(` /model google/gemini-2.0-flash-exp:free`));
|
|
2233
|
+
console.log(theme.dim(` /model deepseek/deepseek-chat:free`));
|
|
2114
2234
|
console.log('');
|
|
2115
2235
|
}
|
|
2116
2236
|
let autoRoute = false;
|
|
@@ -2170,6 +2290,9 @@ async function main() {
|
|
|
2170
2290
|
'f1', 'f2', 'f3', 'f4', // status announcements (bare)
|
|
2171
2291
|
'f5', 'f6', 'f7', 'f8', 'f9', 'f10', // dictation + playback (bare)
|
|
2172
2292
|
'f11', 'f12', // Tier 1: input + last turn (bare)
|
|
2293
|
+
'tab', // Shift+Tab cycles perm modes
|
|
2294
|
+
'escape', // Esc-Esc rewind at empty prompt
|
|
2295
|
+
',', '.', // Alt+, / Alt+. reasoning effort
|
|
2173
2296
|
// Shifted F-keys carry the Tier-2 and Tier-3 a11y functions. Each
|
|
2174
2297
|
// is checked alongside key.shift below, so a bare F1 still routes
|
|
2175
2298
|
// to "status" while Shift+F1 routes to "queued input."
|
|
@@ -2179,6 +2302,11 @@ async function main() {
|
|
|
2179
2302
|
// 'keypress' listeners. During streaming we detach readline's own
|
|
2180
2303
|
// keypress listener (to prevent echo + line-buffer pollution) while
|
|
2181
2304
|
// keeping this one attached so F1–F12 keep working mid-response.
|
|
2305
|
+
// Esc-Esc detection — two bare Esc presses within 500ms at an empty
|
|
2306
|
+
// prompt buffer triggers /back. Single Esc during streaming triggers
|
|
2307
|
+
// a soft-cancel (alias for Ctrl+G steer). State lives in this closure
|
|
2308
|
+
// so it persists across keypress events.
|
|
2309
|
+
let lastEscapeMs = 0;
|
|
2182
2310
|
const hotkeyListener = function hotkeyListener(_str, key) {
|
|
2183
2311
|
if (!key)
|
|
2184
2312
|
return;
|
|
@@ -2186,6 +2314,19 @@ async function main() {
|
|
|
2186
2314
|
if (!INTERCEPT.has(name))
|
|
2187
2315
|
return;
|
|
2188
2316
|
const shift = !!key.shift;
|
|
2317
|
+
const meta = !!key.meta;
|
|
2318
|
+
const ctrl = !!key.ctrl;
|
|
2319
|
+
// Early-return guards so we don't steal keys that should pass
|
|
2320
|
+
// through to readline (regular typing, tab-completion, etc.):
|
|
2321
|
+
// - bare ',' or '.' is regular typing; only Alt+,/. is ours
|
|
2322
|
+
// - bare Tab is completion; only Shift+Tab is ours
|
|
2323
|
+
// - Shift+Esc / Ctrl+Esc / Alt+Esc aren't ours
|
|
2324
|
+
if ((name === ',' || name === '.') && !meta)
|
|
2325
|
+
return;
|
|
2326
|
+
if (name === 'tab' && !shift)
|
|
2327
|
+
return;
|
|
2328
|
+
if (name === 'escape' && (shift || ctrl || meta))
|
|
2329
|
+
return;
|
|
2189
2330
|
const a = getAccessibilityConfig(config);
|
|
2190
2331
|
const tts = getTtsConfig(config);
|
|
2191
2332
|
// Helper: print to stdout (always — picked up by the OS screen reader)
|
|
@@ -2206,7 +2347,10 @@ async function main() {
|
|
|
2206
2347
|
// - Shift+* : every shifted F-key is information or control,
|
|
2207
2348
|
// never voice-only
|
|
2208
2349
|
const isStatusKey = name === 'f1' || name === 'f2' || name === 'f3' || name === 'f4' ||
|
|
2209
|
-
name === 'f11' || name === 'f12' || shift
|
|
2350
|
+
name === 'f11' || name === 'f12' || shift ||
|
|
2351
|
+
// Productivity bindings (Shift+Tab, Esc, Alt+,/.) work regardless
|
|
2352
|
+
// of voice state — they touch config / readline, not audio.
|
|
2353
|
+
name === 'tab' || name === 'escape' || name === ',' || name === '.';
|
|
2210
2354
|
// F5–F10 (bare) are DICTATION/PLAYBACK hotkeys — they only make
|
|
2211
2355
|
// sense when voice features are enabled. Bail early to avoid
|
|
2212
2356
|
// spurious ffmpeg spawns and "TTS not configured" log lines.
|
|
@@ -2232,6 +2376,19 @@ async function main() {
|
|
|
2232
2376
|
// here keeps them out of the bare-F-key branches below).
|
|
2233
2377
|
// ──────────────────────────────────────────────────────────────
|
|
2234
2378
|
if (shift) {
|
|
2379
|
+
// ── Shift+Tab: cycle permission modes ──────────────
|
|
2380
|
+
// Borrowed from Claude Code, the single most-loved power-user
|
|
2381
|
+
// hotkey: one keystroke flips the safety dial through the
|
|
2382
|
+
// full ask → auto → yolo cycle. Replaces /perm <mode> typing.
|
|
2383
|
+
if (name === 'tab') {
|
|
2384
|
+
const order = ['ask', 'auto', 'yolo'];
|
|
2385
|
+
const cur = config.permissionMode;
|
|
2386
|
+
const next = order[(order.indexOf(cur) + 1) % order.length];
|
|
2387
|
+
config.permissionMode = next;
|
|
2388
|
+
saveConfig(config);
|
|
2389
|
+
announce('Shift+Tab', `Permission mode: ${next}.`);
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2235
2392
|
// ── Shift+F1: queued input ─────────────────────────
|
|
2236
2393
|
if (name === 'f1') {
|
|
2237
2394
|
const g = globalThis;
|
|
@@ -2342,6 +2499,63 @@ async function main() {
|
|
|
2342
2499
|
// Any other shifted F-key: no-op (don't fall through to bare).
|
|
2343
2500
|
return;
|
|
2344
2501
|
}
|
|
2502
|
+
// ── Esc (bare): rewind chord at empty prompt ───────
|
|
2503
|
+
// Two bare Esc presses within 500ms at an empty input buffer
|
|
2504
|
+
// triggers /back (rewind one user turn). Matches the muscle
|
|
2505
|
+
// memory of both Claude Code and Codex CLI ("Esc-Esc to step
|
|
2506
|
+
// back"). When the prompt buffer has content, single Esc clears
|
|
2507
|
+
// the typed buffer (readline default); Esc-Esc still rewinds
|
|
2508
|
+
// only when buffer was empty going in. Mid-stream Esc is handled
|
|
2509
|
+
// at the byte level in query.ts dataHandler instead — by the
|
|
2510
|
+
// time keypress events fire, the input is already suppressed.
|
|
2511
|
+
if (name === 'escape') {
|
|
2512
|
+
const buf = rl.line ?? '';
|
|
2513
|
+
if (buf.trim()) {
|
|
2514
|
+
// Non-empty buffer: don't intercept, let readline do its
|
|
2515
|
+
// default (which is meta-prefix; harmless).
|
|
2516
|
+
lastEscapeMs = 0;
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const now = Date.now();
|
|
2520
|
+
if (now - lastEscapeMs < 500) {
|
|
2521
|
+
// Second Esc within window — fire /back.
|
|
2522
|
+
lastEscapeMs = 0;
|
|
2523
|
+
// Enqueue the slash command as queued input. The REPL loop
|
|
2524
|
+
// picks it up + executes /back the same way as if typed.
|
|
2525
|
+
globalThis.__crowcoderQueuedInput = '/back\n';
|
|
2526
|
+
announce('Esc-Esc', 'Rewinding to previous user turn.');
|
|
2527
|
+
// Nudge readline by writing an empty line so the question
|
|
2528
|
+
// resolves and the main loop's queued-input drain fires.
|
|
2529
|
+
try {
|
|
2530
|
+
stdin.write('\n');
|
|
2531
|
+
}
|
|
2532
|
+
catch { /* noop */ }
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
lastEscapeMs = now;
|
|
2536
|
+
// Single Esc on an empty buffer — show a one-time hint so the
|
|
2537
|
+
// chord is discoverable. Suppressed under screen-reader mode
|
|
2538
|
+
// (would interrupt their reading flow on every Esc).
|
|
2539
|
+
if (config.voice?.accessibility?.screenReader !== true) {
|
|
2540
|
+
console.log(chalk.dim(' [Esc] press Esc again within 500ms to rewind one turn.'));
|
|
2541
|
+
}
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
// ── Alt+, / Alt+. : reasoning effort (temperature) ─
|
|
2545
|
+
// Borrowed from Codex CLI. Lower temperature = more careful /
|
|
2546
|
+
// deterministic; higher = more creative. Step ± 0.1, clamped
|
|
2547
|
+
// to [0.0, 2.0]. Saved immediately so the next API call uses
|
|
2548
|
+
// the new value. Persisted so the setting survives restarts.
|
|
2549
|
+
if ((name === ',' || name === '.') && meta) {
|
|
2550
|
+
const cur = typeof config.temperature === 'number' ? config.temperature : 0.3;
|
|
2551
|
+
const step = name === ',' ? -0.1 : +0.1;
|
|
2552
|
+
const next = Math.max(0, Math.min(2.0, Math.round((cur + step) * 100) / 100));
|
|
2553
|
+
config.temperature = next;
|
|
2554
|
+
saveConfig(config);
|
|
2555
|
+
const label = name === ',' ? 'Alt+,' : 'Alt+.';
|
|
2556
|
+
announce(label, `Temperature ${next.toFixed(2)} (lower = more careful, higher = more creative).`);
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2345
2559
|
// ── F11: read current input buffer (Tier 1, bare) ──
|
|
2346
2560
|
if (name === 'f11') {
|
|
2347
2561
|
// rl.line is readline's internal "what the user has typed so far
|
|
@@ -2746,7 +2960,7 @@ async function main() {
|
|
|
2746
2960
|
}
|
|
2747
2961
|
// Session stop hook + memory persistence
|
|
2748
2962
|
onSessionEnd(session.id, messages, process.cwd());
|
|
2749
|
-
await runHooks({ event: 'SessionStop', sessionId: session.id, cwd: process.cwd() });
|
|
2963
|
+
await runHooks({ event: 'SessionStop', sessionId: session.id, cwd: process.cwd(), permissionMode: config.permissionMode });
|
|
2750
2964
|
// Final save
|
|
2751
2965
|
await autoSave(session, messages);
|
|
2752
2966
|
console.log(chalk.dim(`\nSession saved: ${session.id}`));
|