@pugi/cli 0.1.0-alpha.18 → 0.1.0-alpha.20
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/README.md +33 -0
- package/dist/commands/deploy.js +439 -0
- package/dist/core/edits/dispatch.js +185 -0
- package/dist/core/edits/index.js +15 -0
- package/dist/core/edits/layer-a-apply.js +217 -0
- package/dist/core/edits/layer-b-apply.js +211 -0
- package/dist/core/edits/layer-c-apply.js +160 -0
- package/dist/core/edits/layer-d-ast.js +29 -0
- package/dist/core/edits/marker-parser.js +401 -0
- package/dist/core/edits/security-gate.js +223 -0
- package/dist/core/engine/native-pugi.js +6 -1
- package/dist/core/engine/tool-bridge.js +33 -1
- package/dist/core/repl/cancellation.js +98 -0
- package/dist/core/repl/dispatch-fsm.js +220 -0
- package/dist/core/repl/privacy-banner.js +4 -4
- package/dist/core/repl/session.js +556 -0
- package/dist/core/repl/slash-commands.js +12 -3
- package/dist/runtime/cli.js +193 -1
- package/dist/runtime/commands/config.js +136 -0
- package/dist/tools/file-tools.js +90 -0
- package/dist/tui/input-box.js +48 -5
- package/dist/tui/repl.js +24 -2
- package/dist/tui/status-bar.js +63 -3
- package/package.json +2 -2
package/dist/tui/input-box.js
CHANGED
|
@@ -131,14 +131,57 @@ export function InputBox(props) {
|
|
|
131
131
|
useInput((input, key) => {
|
|
132
132
|
if (key.ctrl && input === 'c') {
|
|
133
133
|
const t = now();
|
|
134
|
-
|
|
134
|
+
// α6.9: Claude Code-style double-press semantics. First Ctrl+C
|
|
135
|
+
// ALWAYS attempts to cancel an in-flight dispatch (when the
|
|
136
|
+
// session reports non-idle); second Ctrl+C within 1s exits the
|
|
137
|
+
// process. If onCancel is omitted (legacy callers, tests), the
|
|
138
|
+
// old behaviour is preserved: first Ctrl+C clears the buffer +
|
|
139
|
+
// arms the exit timer, second Ctrl+C exits.
|
|
140
|
+
const withinDoubleTapWindow = typeof lastCtrlCAt === 'number' && t - lastCtrlCAt <= CTRL_C_DOUBLE_TAP_MS;
|
|
141
|
+
if (withinDoubleTapWindow) {
|
|
142
|
+
// Second press inside the window — always exit. This matches
|
|
143
|
+
// Claude Code: even mid-dispatch, the second Ctrl+C wins so
|
|
144
|
+
// the operator can always escape a stuck REPL.
|
|
135
145
|
props.onExit();
|
|
136
146
|
return;
|
|
137
147
|
}
|
|
148
|
+
// First press in a fresh window. If the host wired a cancel
|
|
149
|
+
// surface and there is something to cancel, abort the dispatch.
|
|
150
|
+
// The buffer is left untouched on a cancel (the operator's
|
|
151
|
+
// current input is NOT trashed by an accidental Ctrl+C while a
|
|
152
|
+
// tool is running).
|
|
153
|
+
//
|
|
154
|
+
// Three-valued onCancel return (see prop docstring):
|
|
155
|
+
// - true → dispatch cancelled, keep buffer, arm exit timer
|
|
156
|
+
// - false → idle, clear buffer (legacy), arm exit timer
|
|
157
|
+
// - undefined → handler bypassed (modal owns input); NO state
|
|
158
|
+
// change at all. Buffer stays, exit timer NOT
|
|
159
|
+
// armed (otherwise the modal would silently
|
|
160
|
+
// promote a Ctrl+C to "press again to exit",
|
|
161
|
+
// which is wrong context for a modal cancel).
|
|
162
|
+
let cancelResult;
|
|
163
|
+
if (props.onCancel) {
|
|
164
|
+
cancelResult = props.onCancel();
|
|
165
|
+
}
|
|
166
|
+
if (cancelResult === undefined && props.onCancel) {
|
|
167
|
+
// Bypass path - modal owns the input. Drop the press silently
|
|
168
|
+
// so the modal's own cancel surface (Esc / its own Ctrl+C
|
|
169
|
+
// binding inside the modal component) takes effect on its own
|
|
170
|
+
// terms. P2 fix: previously this fell through to the
|
|
171
|
+
// legacy buffer-clear + setLastCtrlCAt path and wiped modal
|
|
172
|
+
// draft text on first Ctrl+C.
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
138
175
|
setLastCtrlCAt(t);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
176
|
+
// Legacy behaviour: on idle (or no onCancel wired), clear the
|
|
177
|
+
// buffer + reset search so the operator's screen is calm before
|
|
178
|
+
// they confirm exit. When we DID cancel a live dispatch, keep
|
|
179
|
+
// the buffer so a half-typed brief is not lost.
|
|
180
|
+
if (cancelResult !== true) {
|
|
181
|
+
setLine('');
|
|
182
|
+
setCursor(0);
|
|
183
|
+
setSearch(undefined);
|
|
184
|
+
}
|
|
142
185
|
return;
|
|
143
186
|
}
|
|
144
187
|
// Search-mode key handling. Ctrl+R / Ctrl+S cycle, Enter accepts,
|
|
@@ -456,7 +499,7 @@ export function InputBox(props) {
|
|
|
456
499
|
: Math.min(paletteIndex, paletteView.rows.length - 1);
|
|
457
500
|
const divider = '─'.repeat(innerWidth);
|
|
458
501
|
const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
|
|
459
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", 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: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "cyan", 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 ×2 exit' }) })] }));
|
|
502
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", 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: "cyan", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "cyan", 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' }) })] }));
|
|
460
503
|
}
|
|
461
504
|
/**
|
|
462
505
|
* Render the line with the cursor glyph inserted at `cursor`. The cursor
|
package/dist/tui/repl.js
CHANGED
|
@@ -156,11 +156,33 @@ export function Repl(props) {
|
|
|
156
156
|
const handlePlanReviewResolve = useCallback((result) => {
|
|
157
157
|
void props.session.resolvePlanReview(result);
|
|
158
158
|
}, [props.session]);
|
|
159
|
-
|
|
159
|
+
// α6.9: Ctrl+C abort handler. Forwards to ReplSession.cancel() which
|
|
160
|
+
// aborts the in-flight dispatch, closes the SSE stream, and surfaces
|
|
161
|
+
// "Aborted." in the transcript.
|
|
162
|
+
//
|
|
163
|
+
// Return contract (consumed by InputBox):
|
|
164
|
+
// - true - dispatch was cancelled (keep the buffer + DO arm
|
|
165
|
+
// the press-again-to-exit timer; second Ctrl+C in
|
|
166
|
+
// the window exits).
|
|
167
|
+
// - false - idle / nothing to cancel (legacy: clear buffer +
|
|
168
|
+
// arm the exit timer so the operator sees the hint
|
|
169
|
+
// and can confirm exit on the next press).
|
|
170
|
+
// - undefined - bypassed entirely (e.g. a modal owns the input).
|
|
171
|
+
// InputBox MUST NOT arm the exit timer and MUST
|
|
172
|
+
// NOT clear the buffer. P2 fix: previously this
|
|
173
|
+
// returned `false` and the buffer-clear path wiped
|
|
174
|
+
// the operator's mid-typed modal text on the first
|
|
175
|
+
// Ctrl+C, costing a press of work.
|
|
176
|
+
const handleCancel = useCallback(() => {
|
|
177
|
+
if (modalActive)
|
|
178
|
+
return undefined;
|
|
179
|
+
return props.session.cancel();
|
|
180
|
+
}, [props.session, modalActive]);
|
|
181
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, 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, 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, now: props.now,
|
|
160
182
|
// Slug from process.cwd() (full path) so two workspaces with
|
|
161
183
|
// the same basename do not share history. state.workspaceLabel
|
|
162
184
|
// is the basename only. Codex review P2.
|
|
163
|
-
workspaceSlug: slugForCwd(process.cwd()) })), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase, pugiMdCount: workspaceContext.pugiMdCount, mcpServerCount: workspaceContext.mcpServerCount, skillCount: workspaceContext.skillCount, quotaPct: props.quotaPct })] })] }));
|
|
185
|
+
workspaceSlug: slugForCwd(process.cwd()) })), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase, pugiMdCount: workspaceContext.pugiMdCount, mcpServerCount: workspaceContext.mcpServerCount, skillCount: workspaceContext.skillCount, quotaPct: props.quotaPct, dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel })] })] }));
|
|
164
186
|
}
|
|
165
187
|
function Header({ state }) {
|
|
166
188
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "cyan", children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: "cyan", children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
|
package/dist/tui/status-bar.js
CHANGED
|
@@ -12,7 +12,12 @@ export function StatusBar(props) {
|
|
|
12
12
|
const tokenLabel = formatTokens(props.tokensDownstreamTotal);
|
|
13
13
|
const phase = clampPhase(props.pulsePhase);
|
|
14
14
|
const glyph = PULSE_DOTS[Math.min(phase, PULSE_DOTS.length - 1)] ?? PULSE_DOTS[0];
|
|
15
|
-
|
|
15
|
+
// α6.9: composite status label — connection problems trump dispatch
|
|
16
|
+
// state because the operator needs to know about a dropped admin-api
|
|
17
|
+
// first. When the connection is healthy (`on_watch` / `connecting`),
|
|
18
|
+
// the FSM dispatch state takes over to show the dispatch lifecycle
|
|
19
|
+
// (`dispatching` / `tool: read` / `aborting` / etc.).
|
|
20
|
+
const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel);
|
|
16
21
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: status.color, children: `${glyph ?? '●'} ${status.label}` }), _jsx(Text, { dimColor: true, children: ` · ${props.activeAgentCount} agents · ` }), _jsx(Text, { children: `↓ ${tokenLabel} tokens` }), _jsx(Text, { dimColor: true, children: ` · ${elapsedLabel}` })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${formatCount(props.pugiMdCount)} PUGI.md · ${formatCount(props.mcpServerCount)} MCP · ${formatCount(props.skillCount)} skills · ${formatQuota(props.quotaPct)} quota` }) })] }));
|
|
17
22
|
}
|
|
18
23
|
/**
|
|
@@ -28,10 +33,17 @@ function formatQuota(pct) {
|
|
|
28
33
|
return '—';
|
|
29
34
|
return `${Math.round(pct)}%`;
|
|
30
35
|
}
|
|
31
|
-
|
|
36
|
+
// Exported for test introspection (status-bar-fsm.spec.tsx asserts
|
|
37
|
+
// connecting vs on_watch render in distinct colors; ink-testing-library
|
|
38
|
+
// strips ANSI from lastFrame() so we read the resolved color directly).
|
|
39
|
+
export function connectionLabel(connection) {
|
|
32
40
|
switch (connection) {
|
|
33
41
|
case 'connecting':
|
|
34
|
-
|
|
42
|
+
// P2 fix: was 'cyan', same as 'on_watch' - operator could not
|
|
43
|
+
// tell boot from stable. Magenta is distinct from every other
|
|
44
|
+
// state in this palette (cyan steady, yellow reconnect, gray
|
|
45
|
+
// offline) so a brief flicker through this state stands out.
|
|
46
|
+
return { label: 'connecting', color: 'magenta' };
|
|
35
47
|
case 'on_watch':
|
|
36
48
|
return { label: 'on watch', color: 'cyan' };
|
|
37
49
|
case 'reconnecting':
|
|
@@ -40,6 +52,54 @@ function connectionLabel(connection) {
|
|
|
40
52
|
return { label: 'offline', color: 'gray' };
|
|
41
53
|
}
|
|
42
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* α6.9: compose the visible status label from connection + FSM state.
|
|
57
|
+
*
|
|
58
|
+
* Priority order:
|
|
59
|
+
*
|
|
60
|
+
* 1. `offline` / `reconnecting` — transport health wins; the
|
|
61
|
+
* operator needs to know about a dropped stream before anything
|
|
62
|
+
* about the dispatch.
|
|
63
|
+
* 2. `aborting` / `aborted` / `failed` — operator-visible terminal
|
|
64
|
+
* states the FSM reached; the colour shifts to amber/red so the
|
|
65
|
+
* anomaly stands out vs the calm cyan baseline.
|
|
66
|
+
* 3. `tool_running` — surfaces the tool label when available
|
|
67
|
+
* (`tool: read`), falls back to `tool` when not.
|
|
68
|
+
* 4. `awaiting_response` — `dispatching` (matches Codex CLI's verb
|
|
69
|
+
* for the same state).
|
|
70
|
+
* 5. `completed` — `shipped` (matches the agent tree status glyph
|
|
71
|
+
* so the operator's eye links the two surfaces).
|
|
72
|
+
* 6. `idle` / unknown — connection label (`on watch` / `connecting`).
|
|
73
|
+
*
|
|
74
|
+
* The dispatch label `dispatchToolLabel` is already shaped as
|
|
75
|
+
* `tool: <kind>` upstream so we just concatenate; null falls through
|
|
76
|
+
* to the bare `tool` placeholder.
|
|
77
|
+
*/
|
|
78
|
+
function composeStatusLabel(connection, dispatchState, toolLabel) {
|
|
79
|
+
// Transport health wins.
|
|
80
|
+
if (connection === 'offline' || connection === 'reconnecting') {
|
|
81
|
+
return connectionLabel(connection);
|
|
82
|
+
}
|
|
83
|
+
// FSM dispatch state overlay (only when the FSM was wired).
|
|
84
|
+
switch (dispatchState) {
|
|
85
|
+
case 'aborting':
|
|
86
|
+
return { label: 'aborting', color: 'yellow' };
|
|
87
|
+
case 'aborted':
|
|
88
|
+
return { label: 'aborted', color: 'gray' };
|
|
89
|
+
case 'failed':
|
|
90
|
+
return { label: 'failed', color: 'red' };
|
|
91
|
+
case 'tool_running':
|
|
92
|
+
return { label: toolLabel ?? 'tool', color: 'cyan' };
|
|
93
|
+
case 'awaiting_response':
|
|
94
|
+
return { label: 'dispatching', color: 'cyan' };
|
|
95
|
+
case 'completed':
|
|
96
|
+
return { label: 'shipped', color: 'green' };
|
|
97
|
+
case 'idle':
|
|
98
|
+
case undefined:
|
|
99
|
+
default:
|
|
100
|
+
return connectionLabel(connection);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
43
103
|
function formatElapsed(startedAt, now) {
|
|
44
104
|
if (typeof startedAt !== 'number')
|
|
45
105
|
return 'idle';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pugi/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"undici": "^8.3.0",
|
|
53
53
|
"zod": "^3.23.0",
|
|
54
54
|
"@pugi/personas": "0.1.1",
|
|
55
|
-
"@pugi/sdk": "0.1.0-alpha.
|
|
55
|
+
"@pugi/sdk": "0.1.0-alpha.20"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^22.0.0",
|