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 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 CROWCODER_GATEGUARD=off to disable.`,
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":"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"}
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(d(' ') + c('Ctrl+G') + d(' — steer: cancel current turn + use what you\'ve queued as the next message'));
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 v1.1.0') + theme.dim(' — A dense, feature-rich AI coding 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}`));