@pugi/cli 0.1.0-beta.35 → 0.1.0-beta.37
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/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 +662 -0
- 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 +4 -4
- package/dist/runtime/commands/mcp.js +66 -11
- 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
|
@@ -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.37');
|
|
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.37",
|
|
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.37"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^22.0.0",
|