compact-agent 1.28.0 → 1.28.2
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 +51 -7
- package/dist/export.js +1 -1
- package/dist/export.js.map +1 -1
- package/dist/index.js +74 -20
- package/dist/index.js.map +1 -1
- package/dist/modes.js +1 -1
- package/dist/query.js +33 -10
- package/dist/query.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/theme.js +1 -1
- package/dist/theme.js.map +1 -1
- package/dist/tools/web-fetch.js +1 -1
- package/dist/tools/web-fetch.js.map +1 -1
- package/dist/walkthrough.d.ts +1 -1
- package/dist/walkthrough.js +5 -5
- package/package.json +1 -1
package/bin/ecc-hooks.cjs
CHANGED
|
@@ -19,13 +19,31 @@
|
|
|
19
19
|
|
|
20
20
|
const checkName = process.argv[2] || '';
|
|
21
21
|
|
|
22
|
+
// Parse the tool-input env var. Previously a parse failure silently
|
|
23
|
+
// fell back to `{}`, which makes every per-file check return ok() at
|
|
24
|
+
// its empty-path guard — i.e. a malformed payload was a free bypass.
|
|
25
|
+
// Now: empty/unset is treated as `{}` (legitimate for events that
|
|
26
|
+
// don't carry input, like SessionStart); a NON-empty payload that
|
|
27
|
+
// fails to parse is treated as a security event and the hook
|
|
28
|
+
// blocks. Fail closed > fail open.
|
|
29
|
+
const rawToolInput = process.env.COMPACT_AGENT_TOOL_INPUT || process.env.CROWCODER_TOOL_INPUT || '';
|
|
22
30
|
let toolInput = {};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
if (rawToolInput.length > 0) {
|
|
32
|
+
try {
|
|
33
|
+
toolInput = JSON.parse(rawToolInput);
|
|
34
|
+
} catch {
|
|
35
|
+
process.stderr.write('[ECC] BLOCKED: tool input env var was not valid JSON.\n');
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
26
39
|
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
// Both env-var names accepted. COMPACT_AGENT_* is the post-rebrand
|
|
41
|
+
// primary; CROWCODER_* is the legacy alias kept for back-compat with
|
|
42
|
+
// user-written wrappers that haven't migrated. The parent process
|
|
43
|
+
// exports both via src/hooks.ts so either form works regardless of
|
|
44
|
+
// which the user's shell or wrapper set.
|
|
45
|
+
const tool = process.env.COMPACT_AGENT_TOOL || process.env.CROWCODER_TOOL || '';
|
|
46
|
+
const cwd = process.env.COMPACT_AGENT_CWD || process.env.CROWCODER_CWD || process.cwd();
|
|
29
47
|
|
|
30
48
|
// ── Helpers ─────────────────────────────────────────────
|
|
31
49
|
function bashCommand() {
|
|
@@ -73,6 +91,19 @@ const checks = {
|
|
|
73
91
|
if (/(^|\s)--no-gpg-sign(\s|$)/.test(cmd)) {
|
|
74
92
|
return block('`--no-gpg-sign` bypasses signing — ask the user first.');
|
|
75
93
|
}
|
|
94
|
+
// `git commit -n` is the short form of --no-verify. The previous
|
|
95
|
+
// regex missed it entirely. Scope the check to `git commit`
|
|
96
|
+
// specifically — `-n` in other git subcommands means different
|
|
97
|
+
// things (e.g. `git log -n 5` = limit count, NOT skip hooks).
|
|
98
|
+
if (/\bgit\s+(?:commit|merge|rebase)\b[^|;&]*\s-n(\s|$)/.test(cmd)) {
|
|
99
|
+
return block('`-n` is the short form of `--no-verify` — fix the failing hook instead.');
|
|
100
|
+
}
|
|
101
|
+
// `-c core.hooksPath=/dev/null` (or similar) disables git hooks
|
|
102
|
+
// entirely without using --no-verify. Catches the documented
|
|
103
|
+
// bypass vector even when --no-verify isn't on the command line.
|
|
104
|
+
if (/\bgit\s+-c\s+core\.hooksPath\s*=/i.test(cmd)) {
|
|
105
|
+
return block('`-c core.hooksPath=…` disables git hooks — fix the failure instead.');
|
|
106
|
+
}
|
|
76
107
|
return ok();
|
|
77
108
|
},
|
|
78
109
|
|
|
@@ -207,7 +238,16 @@ const checks = {
|
|
|
207
238
|
if (!fs.existsSync(targetPath)) return ok();
|
|
208
239
|
} catch { /* if statSync fails, fall through to the normal path */ }
|
|
209
240
|
|
|
210
|
-
|
|
241
|
+
// Sanitize sessionId before joining into a path. Without this,
|
|
242
|
+
// a sessionId of e.g. "../../../.ssh/authorized_keys" would let
|
|
243
|
+
// `path.join` traverse out of the state dir, and the subsequent
|
|
244
|
+
// writeFileSync would overwrite arbitrary files with attacker-
|
|
245
|
+
// controllable JSON (the touched-set array, which can include
|
|
246
|
+
// model-controlled file paths). Allow only [A-Za-z0-9_-], length
|
|
247
|
+
// <=64. Anything else falls back to "unknown" — degraded UX
|
|
248
|
+
// (gateguard tracking won't persist), not a security breach.
|
|
249
|
+
const rawSessionId = process.env.COMPACT_AGENT_SESSION_ID || process.env.CROWCODER_SESSION_ID || '';
|
|
250
|
+
const sessionId = /^[A-Za-z0-9_-]{1,64}$/.test(rawSessionId) ? rawSessionId : 'unknown';
|
|
211
251
|
const stateDir = pathMod.join(os.homedir(), '.compact-agent', 'state', 'gateguard');
|
|
212
252
|
const stateFile = pathMod.join(stateDir, `${sessionId}.json`);
|
|
213
253
|
|
|
@@ -349,7 +389,11 @@ const checks = {
|
|
|
349
389
|
const fs = require('fs');
|
|
350
390
|
const pathMod = require('path');
|
|
351
391
|
const os = require('os');
|
|
352
|
-
|
|
392
|
+
// Read both env-var names + sanitize, same pattern as gateguard.
|
|
393
|
+
// Previously only read the legacy CROWCODER_SESSION_ID, which broke
|
|
394
|
+
// for any wrapper that exported only the new COMPACT_AGENT_* names.
|
|
395
|
+
const rawSessionId = process.env.COMPACT_AGENT_SESSION_ID || process.env.CROWCODER_SESSION_ID || '';
|
|
396
|
+
const sessionId = /^[A-Za-z0-9_-]{1,64}$/.test(rawSessionId) ? rawSessionId : 'unknown';
|
|
353
397
|
const stateDir = pathMod.join(os.homedir(), '.compact-agent', 'state', 'quality-hint');
|
|
354
398
|
const stateFile = pathMod.join(stateDir, `${sessionId}.json`);
|
|
355
399
|
|
package/dist/export.js
CHANGED
|
@@ -101,7 +101,7 @@ function getExtension(format) {
|
|
|
101
101
|
export function saveExport(messages, format) {
|
|
102
102
|
const formatted = formatMessages(messages, format);
|
|
103
103
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
104
|
-
const filename = `
|
|
104
|
+
const filename = `compact-agent-export-${timestamp}.${getExtension(format)}`;
|
|
105
105
|
const filepath = path.join(process.cwd(), filename);
|
|
106
106
|
fs.writeFileSync(filepath, formatted, 'utf-8');
|
|
107
107
|
return filepath;
|
package/dist/export.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -244,7 +244,7 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
244
244
|
console.log(d(' ') + c('/history') + d(' — message count & token estimate'));
|
|
245
245
|
console.log(d(' ') + c('/export [fmt]') + d(' — export conversation (md/json/txt)'));
|
|
246
246
|
console.log(d(' ') + c('/exit') + d(' — quit (alias: /quit)'));
|
|
247
|
-
console.log(d(' ') + c('/walkthrough') + d(' — agent-led tour of
|
|
247
|
+
console.log(d(' ') + c('/walkthrough') + d(' — agent-led tour of compact-agent (aliases: /tour, /guide)'));
|
|
248
248
|
console.log(d(' ') + c('!<cmd>') + d(' — run shell command directly'));
|
|
249
249
|
console.log(h('\n ── Productivity hotkeys ──'));
|
|
250
250
|
console.log(d(' ') + c('Shift+Tab') + d(' — cycle permission modes (ask → auto → yolo)'));
|
|
@@ -405,9 +405,19 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
405
405
|
}
|
|
406
406
|
return { handled: true };
|
|
407
407
|
// ── Clear ─────────────────────────────────────────
|
|
408
|
-
case '/clear':
|
|
408
|
+
case '/clear': {
|
|
409
|
+
// Also reset the global state that's keyed to the conversation
|
|
410
|
+
// so Shift+F3 / Shift+F1 / F12 don't surface stale data from
|
|
411
|
+
// before the clear. The main REPL loop replaces `messages` via
|
|
412
|
+
// `newMessages`; we just nuke the side-channel globals.
|
|
413
|
+
const g = globalThis;
|
|
414
|
+
g.__crowcoderQueuedInput = '';
|
|
415
|
+
g.__voiceLastFullResponse = null;
|
|
416
|
+
g.__voiceLastChunk = null;
|
|
417
|
+
g.__lastToolCall = null;
|
|
409
418
|
console.log(chalk.dim(' Conversation cleared.'));
|
|
410
419
|
return { handled: true, newMessages: [] };
|
|
420
|
+
}
|
|
411
421
|
// ── Backtrack — rewind to a prior user turn (Codex audit item 4) ──
|
|
412
422
|
// /back list recent user messages with numbers
|
|
413
423
|
// /back <n> truncate conversation to BEFORE the nth most-recent
|
|
@@ -466,14 +476,23 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
466
476
|
case '/fork':
|
|
467
477
|
case '/branch': {
|
|
468
478
|
const forkName = args.trim() || `fork of ${session.name}`;
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
|
|
479
|
+
// Snapshot the pre-fork state in a SEPARATE object before
|
|
480
|
+
// mutating the live `session` reference — otherwise both save
|
|
481
|
+
// calls write to whichever ID the mutation has currently set,
|
|
482
|
+
// overwriting the original branch and silently breaking the
|
|
483
|
+
// "previous session reachable via /resume" promise. (Bug found
|
|
484
|
+
// by audit; previously the spread happened AFTER `session.id`
|
|
485
|
+
// was reassigned, so both saves landed under the new ID.)
|
|
472
486
|
const previousId = session.id;
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
487
|
+
const previousSnapshot = {
|
|
488
|
+
...session,
|
|
489
|
+
id: previousId,
|
|
490
|
+
messages: [...messages],
|
|
491
|
+
};
|
|
492
|
+
saveSession(previousSnapshot).catch(() => { });
|
|
493
|
+
// Now mutate the active session in place. The REPL keeps
|
|
494
|
+
// running against `session` so mutation is enough — no need
|
|
495
|
+
// to plumb a swap through the return value.
|
|
477
496
|
session.id = generateSessionId();
|
|
478
497
|
session.name = forkName;
|
|
479
498
|
session.createdAt = new Date().toISOString();
|
|
@@ -2100,11 +2119,22 @@ export function handleSlashCommand(input, config, messages, session, mode) {
|
|
|
2100
2119
|
// user is testing the pipeline or running under a terminal that strips
|
|
2101
2120
|
// function keys. Records up to 30s, transcribes, injects as next prompt.
|
|
2102
2121
|
case '/dictate': {
|
|
2103
|
-
|
|
2104
|
-
|
|
2122
|
+
// Parse + clamp the duration argument.
|
|
2123
|
+
// /dictate → 30s default
|
|
2124
|
+
// /dictate 60 → 60s, clamped to [1, 300]
|
|
2125
|
+
// /dictate 0 → 30s (user clearly wanted default or
|
|
2126
|
+
// cancel; previously parseInt(0) || 30
|
|
2127
|
+
// gave 30 silently)
|
|
2128
|
+
// /dictate -5 → 30s (negative is nonsense)
|
|
2129
|
+
// /dictate abc → 30s default
|
|
2130
|
+
const parsed = parseInt(args, 10);
|
|
2131
|
+
const sec = Number.isFinite(parsed) && parsed > 0
|
|
2132
|
+
? Math.min(300, parsed)
|
|
2133
|
+
: 30;
|
|
2134
|
+
console.log(chalk.dim(` /dictate — recording up to ${sec}s…`));
|
|
2105
2135
|
// Return as an async-injected prompt; we resolve the recording
|
|
2106
2136
|
// synchronously here for simplicity (REPL is blocking anyway).
|
|
2107
|
-
return { handled: true, injectPrompt: '__DICTATE__' +
|
|
2137
|
+
return { handled: true, injectPrompt: '__DICTATE__' + sec };
|
|
2108
2138
|
}
|
|
2109
2139
|
// /accessibility — show or toggle the accessibility sub-block
|
|
2110
2140
|
// /accessibility — print status
|
|
@@ -2598,10 +2628,20 @@ async function main() {
|
|
|
2598
2628
|
// memory of both Claude Code and Codex CLI ("Esc-Esc to step
|
|
2599
2629
|
// back"). When the prompt buffer has content, single Esc clears
|
|
2600
2630
|
// the typed buffer (readline default); Esc-Esc still rewinds
|
|
2601
|
-
// only when buffer was empty going in.
|
|
2602
|
-
// at the byte level in query.ts dataHandler instead — by the
|
|
2603
|
-
// time keypress events fire, the input is already suppressed.
|
|
2631
|
+
// only when buffer was empty going in.
|
|
2604
2632
|
if (name === 'escape') {
|
|
2633
|
+
// Mid-stream Esc is handled at the byte level in query.ts
|
|
2634
|
+
// dataHandler (where it triggers the steer cancel). The
|
|
2635
|
+
// hotkey-listener Esc branch must explicitly bail when a
|
|
2636
|
+
// turn is in progress, otherwise the user sees BOTH the
|
|
2637
|
+
// steer effect AND the "press Esc again to rewind" hint
|
|
2638
|
+
// print, and a second Esc enqueues /back on top of the
|
|
2639
|
+
// already-cancelled turn. Audit P0.
|
|
2640
|
+
const turnCtl = globalThis.__turnAbortCtl;
|
|
2641
|
+
if (turnCtl && !turnCtl.signal.aborted) {
|
|
2642
|
+
lastEscapeMs = 0;
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2605
2645
|
const buf = rl.line ?? '';
|
|
2606
2646
|
if (buf.trim()) {
|
|
2607
2647
|
// Non-empty buffer: don't intercept, let readline do its
|
|
@@ -2614,15 +2654,29 @@ async function main() {
|
|
|
2614
2654
|
// Second Esc within window — fire /back.
|
|
2615
2655
|
lastEscapeMs = 0;
|
|
2616
2656
|
// Enqueue the slash command as queued input. The REPL loop
|
|
2617
|
-
// picks it up +
|
|
2657
|
+
// picks it up + dispatches /back on the next iteration.
|
|
2618
2658
|
globalThis.__crowcoderQueuedInput = '/back\n';
|
|
2619
2659
|
announce('Esc-Esc', 'Rewinding to previous user turn.');
|
|
2620
|
-
//
|
|
2621
|
-
//
|
|
2660
|
+
// Resolve the pending rl.question() so the main loop
|
|
2661
|
+
// actually moves on. Previously this used stdin.write('\n')
|
|
2662
|
+
// which DOESN'T interrupt readline — it merely adds a
|
|
2663
|
+
// newline to the input stream that readline reads as if
|
|
2664
|
+
// the user typed it, leaving the REPL stuck until the user
|
|
2665
|
+
// pressed Enter manually. emit('line', '') triggers the
|
|
2666
|
+
// 'line' event that rl.question internally listens for,
|
|
2667
|
+
// resolving the promise immediately. (Bug found by audit.)
|
|
2622
2668
|
try {
|
|
2623
|
-
|
|
2669
|
+
rl.emit('line', '');
|
|
2670
|
+
}
|
|
2671
|
+
catch {
|
|
2672
|
+
// Fallback path if rl.emit isn't accepted for some reason
|
|
2673
|
+
// (e.g. on a future readline version): still nudge via
|
|
2674
|
+
// stdin so the user can recover by pressing any key.
|
|
2675
|
+
try {
|
|
2676
|
+
stdin.write('\n');
|
|
2677
|
+
}
|
|
2678
|
+
catch { /* noop */ }
|
|
2624
2679
|
}
|
|
2625
|
-
catch { /* noop */ }
|
|
2626
2680
|
return;
|
|
2627
2681
|
}
|
|
2628
2682
|
lastEscapeMs = now;
|