@pugi/cli 0.1.0-beta.36 → 0.1.0-beta.38
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/core/engine/budgets.js +11 -2
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/mcp/orchestrator-tools.js +118 -51
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +144 -53
- package/dist/core/permissions/index.js +3 -1
- package/dist/core/permissions/mode.js +132 -60
- package/dist/core/permissions/state.js +33 -7
- package/dist/core/repl/slash-commands.js +16 -12
- package/dist/core/session.js +48 -0
- package/dist/runtime/cli.js +6 -6
- package/dist/runtime/commands/permissions.js +11 -9
- package/dist/runtime/commands/plan.js +4 -4
- package/dist/runtime/version.js +1 -1
- package/dist/tui/input-box.js +24 -1
- package/dist/tui/permissions-picker.js +14 -6
- package/dist/tui/repl.js +29 -1
- package/package.json +2 -2
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* `/quit` confirmation and `/help` footer - never inline.
|
|
30
30
|
*/
|
|
31
31
|
import { listRoles } from '../agents/registry.js';
|
|
32
|
+
import { PERMISSION_MODES, parsePermissionMode, } from '../permissions/index.js';
|
|
32
33
|
/**
|
|
33
34
|
* Deterministic stub copy returned by the Tier 3 commands. Spec'd
|
|
34
35
|
* inline so the unit test can pin the exact text without poking at
|
|
@@ -86,7 +87,7 @@ export const SLASH_COMMAND_HELP = Object.freeze([
|
|
|
86
87
|
// Settings
|
|
87
88
|
{ name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
|
|
88
89
|
{ name: 'privacy', args: '', gloss: 'Show privacy mode + contract', group: 'Settings' },
|
|
89
|
-
{ name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (plan /
|
|
90
|
+
{ name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (default / acceptEdits / plan / auto / dontAsk / bypassPermissions) (also: /plan, Shift+Tab cycle)', group: 'Settings' },
|
|
90
91
|
{ name: 'plan', args: '[--back | --persist] [<prompt>]', gloss: 'Switch to plan mode (read-only). Same as /permissions plan, slicker UX.', group: 'Settings' },
|
|
91
92
|
{ name: 'model', args: '[<slug>]', gloss: 'Show or select the active model. Bare /model lists tier-gated options', group: 'Settings' },
|
|
92
93
|
{ name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
|
|
@@ -332,26 +333,29 @@ export function parseSlashCommand(input) {
|
|
|
332
333
|
}
|
|
333
334
|
case 'permissions':
|
|
334
335
|
case 'perms': {
|
|
335
|
-
//
|
|
336
|
+
// Wave 7: `/permissions [mode] [--persist] [--confirm]`.
|
|
336
337
|
//
|
|
337
338
|
// Argument grammar (single line, no quoting):
|
|
338
|
-
// /permissions
|
|
339
|
-
// /permissions plan|
|
|
340
|
-
// /permissions
|
|
341
|
-
//
|
|
339
|
+
// /permissions -> show + table
|
|
340
|
+
// /permissions default|acceptEdits|plan|auto|dontAsk -> flip mode
|
|
341
|
+
// /permissions bypassPermissions --confirm -> flip to
|
|
342
|
+
// bypassPermissions (refused
|
|
343
|
+
// без --confirm — safety)
|
|
342
344
|
// /permissions <mode> --persist -> also write to ~/.pugi/config.json
|
|
343
345
|
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
+
// α6 aliases (`ask`, `allow`, `bypass`) are accepted и mapped to
|
|
347
|
+
// their Wave 7 canonical names via `parsePermissionMode`.
|
|
346
348
|
const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
|
|
347
349
|
if (tokens.length === 0) {
|
|
348
350
|
return { kind: 'permissions', persist: false, confirmBypass: false };
|
|
349
351
|
}
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
+
const headRaw = tokens[0] ?? '';
|
|
353
|
+
const mode = parsePermissionMode(headRaw);
|
|
354
|
+
if (!mode) {
|
|
355
|
+
const modeList = [...PERMISSION_MODES].join('|');
|
|
352
356
|
return {
|
|
353
357
|
kind: 'error',
|
|
354
|
-
message: `Usage: /permissions [
|
|
358
|
+
message: `Usage: /permissions [${modeList}] [--persist] [--confirm]; unknown mode '${headRaw}'`,
|
|
355
359
|
};
|
|
356
360
|
}
|
|
357
361
|
const flags = tokens.slice(1);
|
|
@@ -371,7 +375,7 @@ export function parseSlashCommand(input) {
|
|
|
371
375
|
};
|
|
372
376
|
}
|
|
373
377
|
}
|
|
374
|
-
return { kind: 'permissions', mode
|
|
378
|
+
return { kind: 'permissions', mode, persist, confirmBypass };
|
|
375
379
|
}
|
|
376
380
|
case 'init': {
|
|
377
381
|
// β1 Sl11: surface the init flow inside the REPL. Tail args
|
package/dist/core/session.js
CHANGED
|
@@ -62,6 +62,54 @@ export async function fireSessionStartMvp(session) {
|
|
|
62
62
|
return 0;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Wave 7 P1 — fire the v2 `SessionStart` event from `~/.pugi/hooks.json`
|
|
67
|
+
* (global) + `<workspaceRoot>/.pugi/hooks.json` (project). Companion to
|
|
68
|
+
* `fireSessionStartMvp`; both surfaces run because they read different
|
|
69
|
+
* config files.
|
|
70
|
+
*
|
|
71
|
+
* Headless by default (no trust prompt) — the v2 trust ledger gates
|
|
72
|
+
* first-run executions. Operators with no prior trust decision will see
|
|
73
|
+
* the SessionStart hook skipped with a `denied by trust ledger` stderr
|
|
74
|
+
* note; running `pugi hooks trust allow <command>` enrolls it.
|
|
75
|
+
*
|
|
76
|
+
* Returns the number of hooks that ran (excluding trust-denied skips).
|
|
77
|
+
* Never throws.
|
|
78
|
+
*/
|
|
79
|
+
export async function fireSessionStartV2(session) {
|
|
80
|
+
try {
|
|
81
|
+
const { fireSessionStart } = await import('./hooks/v2/index.js');
|
|
82
|
+
const outcome = await fireSessionStart({
|
|
83
|
+
sessionId: session.id,
|
|
84
|
+
workspaceRoot: session.root,
|
|
85
|
+
transcriptPath: session.eventsPath,
|
|
86
|
+
permissionMode: 'ask',
|
|
87
|
+
});
|
|
88
|
+
return outcome.results.filter((r) => r.exitCode !== -1).length;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Wave 7 P1 — fire the v2 `SessionEnd` event. Called by the REPL
|
|
96
|
+
* teardown path. Companion to `fireSessionStartV2`.
|
|
97
|
+
*/
|
|
98
|
+
export async function fireSessionEndV2(session) {
|
|
99
|
+
try {
|
|
100
|
+
const { fireSessionEnd } = await import('./hooks/v2/index.js');
|
|
101
|
+
const outcome = await fireSessionEnd({
|
|
102
|
+
sessionId: session.id,
|
|
103
|
+
workspaceRoot: session.root,
|
|
104
|
+
transcriptPath: session.eventsPath,
|
|
105
|
+
permissionMode: 'ask',
|
|
106
|
+
});
|
|
107
|
+
return outcome.results.filter((r) => r.exitCode !== -1).length;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
65
113
|
export function recordCommandStarted(session, command) {
|
|
66
114
|
if (!session.enabled)
|
|
67
115
|
return;
|
package/dist/runtime/cli.js
CHANGED
|
@@ -755,7 +755,7 @@ async function dispatchRewind(args, flags, _session) {
|
|
|
755
755
|
async function dispatchPermissions(args, flags, _session) {
|
|
756
756
|
const head = args[0];
|
|
757
757
|
if (head && parsePermissionMode(head) === null) {
|
|
758
|
-
writeOutput(flags, { error: 'unknown_mode', mode: head }, `Unknown mode '${head}'. Allowed: plan,
|
|
758
|
+
writeOutput(flags, { error: 'unknown_mode', mode: head }, `Unknown mode '${head}'. Allowed: default, acceptEdits, plan, auto, dontAsk, bypassPermissions (α6 aliases ask/allow/bypass accepted).`);
|
|
759
759
|
process.exitCode = 1;
|
|
760
760
|
return;
|
|
761
761
|
}
|
|
@@ -780,8 +780,8 @@ async function dispatchPermissions(args, flags, _session) {
|
|
|
780
780
|
await runPermissionsCommand({
|
|
781
781
|
mode: chosen,
|
|
782
782
|
persist: Boolean(flags.persist),
|
|
783
|
-
// The picker selection IS the confirm gesture for `
|
|
784
|
-
confirmBypass: chosen === '
|
|
783
|
+
// The picker selection IS the confirm gesture for `bypassPermissions`.
|
|
784
|
+
confirmBypass: chosen === 'bypassPermissions' ? true : Boolean(flags.confirm),
|
|
785
785
|
}, {
|
|
786
786
|
workspaceRoot: process.cwd(),
|
|
787
787
|
writeOutput: (text) => writeOutput(flags, { text }, text),
|
|
@@ -1524,7 +1524,7 @@ function parseArgs(argv) {
|
|
|
1524
1524
|
else if (arg === '--mode') {
|
|
1525
1525
|
const next = argv[index + 1];
|
|
1526
1526
|
if (!next || next.startsWith('--')) {
|
|
1527
|
-
throw new Error('--mode requires plan|ask|allow|bypass');
|
|
1527
|
+
throw new Error('--mode requires default|acceptEdits|plan|auto|dontAsk|bypassPermissions (α6 aliases ask|allow|bypass accepted)');
|
|
1528
1528
|
}
|
|
1529
1529
|
flags.mode = next;
|
|
1530
1530
|
index += 1;
|
|
@@ -1669,12 +1669,12 @@ const COMMAND_HELP_BODIES = {
|
|
|
1669
1669
|
' pugi explain "trace the auth flow in src/auth/"',
|
|
1670
1670
|
],
|
|
1671
1671
|
code: [
|
|
1672
|
-
'pugi code "<brief>" — engineering-mode write loop (
|
|
1672
|
+
'pugi code "<brief>" — engineering-mode write loop (80k token budget).',
|
|
1673
1673
|
'',
|
|
1674
1674
|
'Writes files in the current workspace. Use --no-tty in CI / pipes.',
|
|
1675
1675
|
],
|
|
1676
1676
|
fix: [
|
|
1677
|
-
'pugi fix "<brief>" — minimal-diff bugfix loop (
|
|
1677
|
+
'pugi fix "<brief>" — minimal-diff bugfix loop (50k token budget).',
|
|
1678
1678
|
'',
|
|
1679
1679
|
'Same as `pugi code` but the prompt biases toward the smallest patch',
|
|
1680
1680
|
'that closes the brief — refuses scope creep / refactor invitations.',
|
|
@@ -29,9 +29,10 @@ export async function runPermissionsCommand(command, ctx) {
|
|
|
29
29
|
renderModeTable(ctx);
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
if (command.mode === '
|
|
33
|
-
ctx.writeOutput('
|
|
34
|
-
ctx.writeOutput('
|
|
32
|
+
if (command.mode === 'bypassPermissions' && !command.confirmBypass) {
|
|
33
|
+
ctx.writeOutput('bypassPermissions disables policy hooks (skill steering, denial tracking) AND skips the deny-list.');
|
|
34
|
+
ctx.writeOutput('Catastrophic patterns (rm -rf /, fork bomb, dd if=/) still trip the circuit-breaker, но that is the only guardrail left.');
|
|
35
|
+
ctx.writeOutput('Run `/permissions bypassPermissions --confirm` to acknowledge before flipping.');
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
setCurrentMode(ctx.workspaceRoot, command.mode);
|
|
@@ -42,8 +43,8 @@ export async function runPermissionsCommand(command, ctx) {
|
|
|
42
43
|
? ' Persisted to ~/.pugi/config.json for future sessions.'
|
|
43
44
|
: '';
|
|
44
45
|
ctx.writeOutput(`Permission mode set to '${command.mode}'.${persistedHint} ${PERMISSION_MODE_GLOSS[command.mode]}`);
|
|
45
|
-
if (command.mode === '
|
|
46
|
-
ctx.writeOutput('
|
|
46
|
+
if (command.mode === 'bypassPermissions') {
|
|
47
|
+
ctx.writeOutput('bypassPermissions — all tools execute without prompts AND policy hooks disabled (circuit-breaker still trips on rm -rf /). Switch back with /permissions dontAsk.');
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
/**
|
|
@@ -68,12 +69,13 @@ function renderCurrentMode(ctx) {
|
|
|
68
69
|
*/
|
|
69
70
|
function renderModeTable(ctx) {
|
|
70
71
|
ctx.writeOutput('');
|
|
71
|
-
ctx.writeOutput('Permission modes:');
|
|
72
|
+
ctx.writeOutput('Permission modes (Shift+Tab cycles in REPL):');
|
|
72
73
|
for (const mode of PERMISSION_MODES) {
|
|
73
|
-
|
|
74
|
+
// Wave 7: longest canonical name is `bypassPermissions` (17 chars).
|
|
75
|
+
ctx.writeOutput(` ${mode.padEnd(18)} ${PERMISSION_MODE_GLOSS[mode]}`);
|
|
74
76
|
}
|
|
75
77
|
ctx.writeOutput('');
|
|
76
|
-
ctx.writeOutput('Switch with `/permissions <mode> [--persist]`.
|
|
78
|
+
ctx.writeOutput('Switch with `/permissions <mode> [--persist]`. bypassPermissions requires `--confirm`.');
|
|
77
79
|
}
|
|
78
80
|
/**
|
|
79
81
|
* Render the one-shot banner shown on session boot when the effective
|
|
@@ -82,7 +84,7 @@ function renderModeTable(ctx) {
|
|
|
82
84
|
* but the caller is responsible for the once-only semantics.
|
|
83
85
|
*/
|
|
84
86
|
export function renderBypassBanner(writeOutput) {
|
|
85
|
-
writeOutput('
|
|
87
|
+
writeOutput('bypassPermissions — all tools execute without prompts AND policy hooks disabled (circuit-breaker still trips on rm -rf /). Switch back with /permissions dontAsk.');
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
90
|
* Resolve the effective mode + the layered source label used by the
|
|
@@ -135,9 +135,9 @@ function effectiveMode(ctx) {
|
|
|
135
135
|
const global = getGlobalDefaultMode(ctx.homeDir);
|
|
136
136
|
if (global)
|
|
137
137
|
return global;
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
return PERMISSION_MODES[
|
|
138
|
+
// Wave 7 canonical default — `PERMISSION_MODES[0]` is `default` (the
|
|
139
|
+
// CC-parity ask-every-call ground state). Fall back literally if the
|
|
140
|
+
// array is empty (defensive — should never happen).
|
|
141
|
+
return PERMISSION_MODES[0] ?? 'default';
|
|
142
142
|
}
|
|
143
143
|
//# sourceMappingURL=plan.js.map
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.38');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/dist/tui/input-box.js
CHANGED
|
@@ -107,6 +107,11 @@ export function InputBox(props) {
|
|
|
107
107
|
// panes when Ctrl+L wipes the terminal (the parent React tree is
|
|
108
108
|
// otherwise stable and would not redraw on a stdout.write alone).
|
|
109
109
|
const [, setRedrawTick] = useState(0);
|
|
110
|
+
// Wave 7 Shift+Tab toast — flashed for 2s after a mode cycle so the
|
|
111
|
+
// operator sees `Mode → acceptEdits` под the input divider. Cleared
|
|
112
|
+
// by a setTimeout so a quick second Shift+Tab refreshes the toast.
|
|
113
|
+
const [modeCycleToast, setModeCycleToast] = useState(null);
|
|
114
|
+
const modeCycleTimerRef = useRef(null);
|
|
110
115
|
const now = props.now ?? Date.now;
|
|
111
116
|
const { stdout } = useStdout();
|
|
112
117
|
const columns = stdout?.columns ?? FALLBACK_COLUMNS;
|
|
@@ -197,6 +202,24 @@ export function InputBox(props) {
|
|
|
197
202
|
}
|
|
198
203
|
return;
|
|
199
204
|
}
|
|
205
|
+
// Wave 7 — Claude Code parity: Shift+Tab cycles permission mode.
|
|
206
|
+
// The host owns the cycle logic + persistence; we just intercept
|
|
207
|
+
// the chord and surface a one-line toast on success. Place this
|
|
208
|
+
// BEFORE the search-mode and palette branches so a Shift+Tab fires
|
|
209
|
+
// even while reverse-search is active (operator habit-driven).
|
|
210
|
+
if (key.shift && key.tab && props.onCyclePermissionMode) {
|
|
211
|
+
const nextMode = props.onCyclePermissionMode();
|
|
212
|
+
if (nextMode) {
|
|
213
|
+
setModeCycleToast(`Mode → ${nextMode}`);
|
|
214
|
+
if (modeCycleTimerRef.current)
|
|
215
|
+
clearTimeout(modeCycleTimerRef.current);
|
|
216
|
+
modeCycleTimerRef.current = setTimeout(() => {
|
|
217
|
+
setModeCycleToast(null);
|
|
218
|
+
modeCycleTimerRef.current = null;
|
|
219
|
+
}, 2_000);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
200
223
|
// Search-mode key handling. Ctrl+R / Ctrl+S cycle, Enter accepts,
|
|
201
224
|
// Esc cancels (restoring the pre-search draft), backspace shortens
|
|
202
225
|
// the query, typed characters extend it.
|
|
@@ -543,7 +566,7 @@ export function InputBox(props) {
|
|
|
543
566
|
: Math.min(paletteIndex, paletteView.rows.length - 1);
|
|
544
567
|
const divider = '─'.repeat(innerWidth);
|
|
545
568
|
const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
|
|
546
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
|
|
569
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), modeCycleToast ? (_jsx(Box, { children: _jsx(Text, { color: "#3da9fc", bold: true, children: ` ${modeCycleToast}` }) })) : null, line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Shift+Tab mode · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
|
|
547
570
|
}
|
|
548
571
|
/**
|
|
549
572
|
* Render the line with the cursor glyph inserted at `cursor`. The cursor
|
|
@@ -49,15 +49,21 @@ export function PermissionsPicker(props) {
|
|
|
49
49
|
props.onCancel();
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
//
|
|
52
|
+
// Wave 7: number shortcuts mirror the canonical 6-mode order.
|
|
53
|
+
// Operators coming from Claude Code reach the same mode with the
|
|
54
|
+
// same digit (1=default, 2=acceptEdits, …, 6=bypassPermissions).
|
|
53
55
|
if (input === '1')
|
|
54
|
-
props.onSelect('
|
|
56
|
+
props.onSelect('default');
|
|
55
57
|
if (input === '2')
|
|
56
|
-
props.onSelect('
|
|
58
|
+
props.onSelect('acceptEdits');
|
|
57
59
|
if (input === '3')
|
|
58
|
-
props.onSelect('
|
|
60
|
+
props.onSelect('plan');
|
|
59
61
|
if (input === '4')
|
|
60
|
-
props.onSelect('
|
|
62
|
+
props.onSelect('auto');
|
|
63
|
+
if (input === '5')
|
|
64
|
+
props.onSelect('dontAsk');
|
|
65
|
+
if (input === '6')
|
|
66
|
+
props.onSelect('bypassPermissions');
|
|
61
67
|
});
|
|
62
68
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Permission mode" }), _jsx(Text, { dimColor: true, children: ` (current: ${props.currentMode} — ${props.sourceLabel})` })] }), props.firstRun ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "First time? Mode = Ask \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E. Use /permissions \u043A change later." }) })) : null, _jsx(Box, { marginTop: 1, flexDirection: "column", children: ITEMS.map((item, itemIndex) => {
|
|
63
69
|
const isSelected = itemIndex === index;
|
|
@@ -71,7 +77,9 @@ function PickerRow({ isSelected, isCurrent, title, hint, }) {
|
|
|
71
77
|
// mode (separate from cursor focus) so an operator instantly sees
|
|
72
78
|
// which row is "what I have now" vs "what I'm hovering".
|
|
73
79
|
const indicator = isSelected ? '▸ ' : ' ';
|
|
74
|
-
|
|
80
|
+
// Wave 7: longest title is `BypassPermissions` (17 chars). Bump the
|
|
81
|
+
// pad column так the gloss column stays aligned across all 6 rows.
|
|
82
|
+
const padded = title.padEnd(18, ' ');
|
|
75
83
|
const currentMarker = isCurrent ? ' ●' : ' ';
|
|
76
84
|
return (_jsxs(Text, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [indicator, padded] }), _jsx(Text, { color: isCurrent ? 'green' : undefined, children: currentMarker }), _jsx(Text, { dimColor: true, children: ` ${hint}` })] }));
|
|
77
85
|
}
|
package/dist/tui/repl.js
CHANGED
|
@@ -196,6 +196,34 @@ export function Repl(props) {
|
|
|
196
196
|
const verdict = props.session.walkbackLastTurn();
|
|
197
197
|
return verdict === 'walked-back' ? 'walked-back' : 'nothing';
|
|
198
198
|
}, [props.session, modalActive]);
|
|
199
|
+
// Wave 7 — Shift+Tab cycles the 6 canonical permission modes (CC
|
|
200
|
+
// parity). Refuses while a modal is active so the operator does not
|
|
201
|
+
// accidentally flip mode mid-prompt; otherwise resolves the current
|
|
202
|
+
// mode through the workspace > global > default merge, advances via
|
|
203
|
+
// `nextPermissionMode`, и persists к .pugi/session.json. Returns the
|
|
204
|
+
// new mode string so the InputBox can flash a one-line toast.
|
|
205
|
+
const handleCyclePermissionMode = useCallback(() => {
|
|
206
|
+
if (modalActive)
|
|
207
|
+
return null;
|
|
208
|
+
try {
|
|
209
|
+
// Lazy-require так this code path doesn't drag the permissions
|
|
210
|
+
// module into the splash + boot stages where it isn't needed.
|
|
211
|
+
// The require is sync but the inner work is pure JSON IO.
|
|
212
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
213
|
+
const perm = require('../core/permissions/index.js');
|
|
214
|
+
const workspaceRoot = process.cwd();
|
|
215
|
+
const current = perm.resolveMode({ workspaceRoot });
|
|
216
|
+
const next = perm.nextPermissionMode(current);
|
|
217
|
+
perm.setCurrentMode(workspaceRoot, next);
|
|
218
|
+
return next;
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Persistence is best-effort — if .pugi/session.json is read-only
|
|
222
|
+
// или ENOENT-on-parent the toast is suppressed so we don't lie
|
|
223
|
+
// about the flip к the operator.
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}, [modalActive]);
|
|
199
227
|
// α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
|
|
200
228
|
// box pinned to alt-screen BOTTOM, conversation grows above it.
|
|
201
229
|
// Beta.3's height={rows} fix broke keystroke focus - raw echo at
|
|
@@ -204,7 +232,7 @@ export function Repl(props) {
|
|
|
204
232
|
// input, and the input stays the sole focusable surface adjacent
|
|
205
233
|
// to the cursor row, so all keystrokes route through it.
|
|
206
234
|
const altScreenRows = process.stdout.rows ?? 24;
|
|
207
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, now: props.now,
|
|
235
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, onCyclePermissionMode: handleCyclePermissionMode, now: props.now,
|
|
208
236
|
// Slug from process.cwd() (full path) so two workspaces with
|
|
209
237
|
// the same basename do not share history. state.workspaceLabel
|
|
210
238
|
// is the basename only. Codex review P2.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.38",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"undici": "^8.3.0",
|
|
56
56
|
"zod": "^3.23.0",
|
|
57
57
|
"@pugi/personas": "0.1.2",
|
|
58
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
58
|
+
"@pugi/sdk": "0.1.0-beta.38"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^22.0.0",
|