@pugi/cli 0.1.0-alpha.9 → 0.1.0-beta.10

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.
Files changed (74) hide show
  1. package/README.md +33 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-mascot.ansi +16 -0
  4. package/dist/commands/deploy.js +439 -0
  5. package/dist/core/agents/loader.js +104 -0
  6. package/dist/core/agents/registry.js +1 -1
  7. package/dist/core/consensus/anvil-fanout.js +276 -0
  8. package/dist/core/consensus/diff-capture.js +382 -0
  9. package/dist/core/consensus/rubric.js +233 -0
  10. package/dist/core/context/index.js +21 -0
  11. package/dist/core/context/pugiignore.js +316 -0
  12. package/dist/core/context/repo-skeleton.js +533 -0
  13. package/dist/core/context/watcher.js +342 -0
  14. package/dist/core/context/working-set.js +165 -0
  15. package/dist/core/edits/dispatch.js +185 -0
  16. package/dist/core/edits/index.js +15 -0
  17. package/dist/core/edits/layer-a-apply.js +217 -0
  18. package/dist/core/edits/layer-b-apply.js +211 -0
  19. package/dist/core/edits/layer-c-apply.js +160 -0
  20. package/dist/core/edits/layer-d-ast.js +29 -0
  21. package/dist/core/edits/marker-parser.js +401 -0
  22. package/dist/core/edits/security-gate.js +223 -0
  23. package/dist/core/edits/worktree.js +322 -0
  24. package/dist/core/engine/native-pugi.js +6 -1
  25. package/dist/core/engine/prompts.js +8 -0
  26. package/dist/core/engine/tool-bridge.js +33 -1
  27. package/dist/core/lsp/client.js +719 -0
  28. package/dist/core/repl/ask.js +512 -0
  29. package/dist/core/repl/cancellation.js +98 -0
  30. package/dist/core/repl/dispatch-fsm.js +220 -0
  31. package/dist/core/repl/privacy-banner.js +71 -0
  32. package/dist/core/repl/session.js +1908 -13
  33. package/dist/core/repl/slash-commands.js +92 -32
  34. package/dist/core/repl/store/index.js +12 -0
  35. package/dist/core/repl/store/jsonl-log.js +321 -0
  36. package/dist/core/repl/store/lockfile.js +155 -0
  37. package/dist/core/repl/store/session-store.js +792 -0
  38. package/dist/core/repl/store/types.js +44 -0
  39. package/dist/core/repl/store/uuid-v7.js +68 -0
  40. package/dist/core/repl/workspace-context.js +72 -1
  41. package/dist/core/skills/defaults.js +457 -0
  42. package/dist/core/skills/loader.js +454 -0
  43. package/dist/core/skills/sources.js +480 -0
  44. package/dist/core/skills/trust.js +172 -0
  45. package/dist/runtime/cli.js +998 -12
  46. package/dist/runtime/commands/agents.js +385 -0
  47. package/dist/runtime/commands/config.js +338 -8
  48. package/dist/runtime/commands/delegate.js +289 -0
  49. package/dist/runtime/commands/lsp.js +206 -0
  50. package/dist/runtime/commands/patch.js +128 -0
  51. package/dist/runtime/commands/review-consensus.js +399 -0
  52. package/dist/runtime/commands/roster.js +117 -0
  53. package/dist/runtime/commands/skills.js +401 -0
  54. package/dist/runtime/commands/worktree.js +177 -0
  55. package/dist/runtime/plan-decompose.js +531 -0
  56. package/dist/tools/apply-patch.js +495 -0
  57. package/dist/tools/file-tools.js +90 -0
  58. package/dist/tools/lsp-tools.js +189 -0
  59. package/dist/tools/registry.js +26 -0
  60. package/dist/tools/web-fetch.js +1 -1
  61. package/dist/tui/agent-tree-pane.js +9 -0
  62. package/dist/tui/ask-cli.js +52 -0
  63. package/dist/tui/ask-modal.js +211 -0
  64. package/dist/tui/conversation-pane.js +48 -3
  65. package/dist/tui/input-box.js +48 -5
  66. package/dist/tui/markdown-render.js +266 -0
  67. package/dist/tui/repl-render.js +319 -3
  68. package/dist/tui/repl-splash-mascot.js +130 -0
  69. package/dist/tui/repl-splash.js +7 -1
  70. package/dist/tui/repl.js +96 -12
  71. package/dist/tui/status-bar.js +63 -3
  72. package/dist/tui/tool-stream-pane.js +91 -0
  73. package/docs/examples/codegraph.mcp.json +10 -0
  74. package/package.json +14 -6
package/dist/tui/repl.js CHANGED
@@ -20,26 +20,41 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
20
20
  import { useCallback, useEffect, useMemo, useState } from 'react';
21
21
  import { Box, Text, useApp, useInput } from 'ink';
22
22
  import { PUGI_TAGLINE, THE_TEN } from '@pugi/personas';
23
- import { AgentTree } from './agent-tree.js';
23
+ import { AgentTreePane } from './agent-tree-pane.js';
24
+ import { AskModal, PlanReviewModal } from './ask-modal.js';
24
25
  import { ConversationPane } from './conversation-pane.js';
25
26
  import { InputBox } from './input-box.js';
26
27
  import { ReplSplash } from './repl-splash.js';
27
28
  import { StatusBar } from './status-bar.js';
29
+ import { ToolStreamPane } from './tool-stream-pane.js';
28
30
  import { UpdateBanner } from './update-banner.js';
29
31
  import { collectWorkspaceContext } from './workspace-context.js';
30
32
  import { slugForCwd } from '../core/repl/history.js';
31
33
  import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
32
34
  const TICK_INTERVAL_MS = 200;
33
35
  const PULSE_INTERVAL_MS = 700;
36
+ // α6.12: maximum transcript rows the conversation pane renders at once.
37
+ // Older rows scroll off the top; full history stays in session state.
38
+ const CONVERSATION_WINDOW = 12;
34
39
  export function Repl(props) {
35
40
  const [state, setState] = useState(props.session.getState());
36
41
  const [overlay, setOverlay] = useState('none');
37
42
  const [pulsePhase, setPulsePhase] = useState(0);
38
43
  const [tickNow, setTickNow] = useState((props.now ?? Date.now)());
44
+ // α6.12: operator-driven collapse for the tool stream pane. The CLI
45
+ // host can hide the pane entirely via `--no-tool-stream`; this state
46
+ // is the runtime toggle (Ctrl+T) for operators who want the pane on
47
+ // screen but folded to a single row while they read a long reply.
48
+ const [toolStreamCollapsed, setToolStreamCollapsed] = useState(false);
39
49
  // α6.14 wave 3: boot splash visible until first input, first
40
50
  // `agent.spawned` event, or 10s idle. The host gates the initial
41
51
  // visibility on `--no-splash` / PUGI_SKIP_SPLASH via `skipSplash`.
42
- const [splashVisible, setSplashVisible] = useState(props.skipSplash !== true);
52
+ // α6.14.6 CEO dogfood 2026-05-25: default splash to HIDDEN at boot
53
+ // (parity with Claude Code's minimal one-line banner). Operator can
54
+ // opt back in via `/splash` slash. The chafa pug pre-print + header
55
+ // line already give the brand cue without the multi-row Plan/Model/
56
+ // Tenant block crowding the top.
57
+ const [splashVisible, setSplashVisible] = useState(false);
43
58
  const dismissSplash = useCallback(() => setSplashVisible(false), []);
44
59
  // α6.14 wave 3: workspace context snapshot for the status bar. We
45
60
  // read once at mount and freeze; a brand-new PUGI.md or skill is
@@ -122,22 +137,86 @@ export function Repl(props) {
122
137
  setOverlay('none');
123
138
  }
124
139
  }, { isActive: overlay === 'help' || overlay === 'roster' });
125
- 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 })) : 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 })) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, now: props.now,
140
+ // α6.12: Ctrl+T toggles the tool stream pane between expanded and
141
+ // collapsed states. Active only while no overlay is open, so the
142
+ // toggle never fights the help/roster dismiss handler. The input box
143
+ // owns its own raw-input mode, so this listener only fires on the
144
+ // global Ctrl+T binding rather than every printable keystroke.
145
+ useInput((input, key) => {
146
+ if (key.ctrl && input === 't') {
147
+ setToolStreamCollapsed((prev) => !prev);
148
+ }
149
+ }, { isActive: overlay === 'none' && props.hideToolStream !== true });
150
+ // α6.3 office-hours: a pending ask or plan-review modal pauses input
151
+ // until the operator resolves it. The modal owns its own useInput
152
+ // hook, so the InputBox unmounts while a modal is open to avoid two
153
+ // raw-input listeners competing for the same keystroke. Resolution
154
+ // forwards through ReplSession.resolveAsk / resolvePlanReview.
155
+ const askPending = state.pendingAsk !== null;
156
+ const planPending = state.pendingPlanReview !== null;
157
+ const modalActive = askPending || planPending;
158
+ const handleAskResolve = useCallback((verdict) => {
159
+ void props.session.resolveAsk(verdict);
160
+ }, [props.session]);
161
+ const handlePlanReviewResolve = useCallback((result) => {
162
+ void props.session.resolvePlanReview(result);
163
+ }, [props.session]);
164
+ // α6.9: Ctrl+C abort handler. Forwards to ReplSession.cancel() which
165
+ // aborts the in-flight dispatch, closes the SSE stream, and surfaces
166
+ // "Aborted." in the transcript.
167
+ //
168
+ // Return contract (consumed by InputBox):
169
+ // - true - dispatch was cancelled (keep the buffer + DO arm
170
+ // the press-again-to-exit timer; second Ctrl+C in
171
+ // the window exits).
172
+ // - false - idle / nothing to cancel (legacy: clear buffer +
173
+ // arm the exit timer so the operator sees the hint
174
+ // and can confirm exit on the next press).
175
+ // - undefined - bypassed entirely (e.g. a modal owns the input).
176
+ // InputBox MUST NOT arm the exit timer and MUST
177
+ // NOT clear the buffer. P2 fix: previously this
178
+ // returned `false` and the buffer-clear path wiped
179
+ // the operator's mid-typed modal text on the first
180
+ // Ctrl+C, costing a press of work.
181
+ const handleCancel = useCallback(() => {
182
+ if (modalActive)
183
+ return undefined;
184
+ return props.session.cancel();
185
+ }, [props.session, modalActive]);
186
+ // α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
187
+ // box pinned to alt-screen BOTTOM, conversation grows above it.
188
+ // Beta.3's height={rows} fix broke keystroke focus - raw echo at
189
+ // viewport bottom. The right pattern is minHeight on the root +
190
+ // flexGrow=1 on the MainArea Box: empty alt-screen lives ABOVE the
191
+ // input, and the input stays the sole focusable surface adjacent
192
+ // to the cursor row, so all keystrokes route through it.
193
+ const altScreenRows = process.stdout.rows ?? 24;
194
+ 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, now: props.now,
126
195
  // Slug from process.cwd() (full path) so two workspaces with
127
196
  // the same basename do not share history. state.workspaceLabel
128
197
  // is the basename only. Codex review P2.
129
- 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 })] })] }));
198
+ 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 })] })] }));
130
199
  }
131
200
  function Header({ state }) {
132
201
  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('_', ' ') })] }));
133
202
  }
134
- function MainArea({ state, personaNames, nowEpochMs, }) {
135
- // Show the last 12 transcript rows in the conversation slot so the
136
- // bottom of the frame stays anchored to the input box. The agent
137
- // tree drops below the transcript; new agents push the operator
138
- // line up the screen, mirroring Claude Code / Codex CLI.
139
- const conversationSlice = state.transcript.slice(-12);
140
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ConversationPane, { rows: conversationSlice, personaNames: personaNames }), _jsx(Box, { marginTop: 1, children: _jsx(AgentTree, { agents: state.agents, nowEpochMs: nowEpochMs }) })] }));
203
+ function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamCollapsed, }) {
204
+ // α6.12: three vertical panes stacked above the input box.
205
+ //
206
+ // 1. Conversation pane (top) - transcript with Markdown render.
207
+ // 2. Tool stream pane (mid) - live Read/Edit/Bash/Grep lines.
208
+ // Hidden when `--no-tool-stream` is
209
+ // set; collapsed via Ctrl+T while
210
+ // the pane is visible.
211
+ // 3. Agent tree pane (bottom) - Cyber-Zoo roster with persona /
212
+ // status / duration / token counts.
213
+ //
214
+ // The window over the transcript is small (last 12 rows) so the
215
+ // bottom of the frame stays anchored to the input box. New agents
216
+ // push the operator line up the screen, mirroring Claude Code /
217
+ // Codex CLI / Gemini CLI rendering.
218
+ const conversationSlice = state.transcript.slice(-CONVERSATION_WINDOW);
219
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ConversationPane, { rows: conversationSlice, personaNames: personaNames }), hideToolStream ? null : (_jsx(Box, { marginTop: 1, children: _jsx(ToolStreamPane, { calls: state.toolCalls, collapsed: toolStreamCollapsed }) })), _jsx(Box, { marginTop: 1, children: _jsx(AgentTreePane, { agents: state.agents, nowEpochMs: nowEpochMs }) })] }));
141
220
  }
142
221
  function HelpOverlay() {
143
222
  // Group commands by their `group` field so the operator scans the
@@ -187,12 +266,17 @@ function applyVerdictSideEffects(verdict, handlers) {
187
266
  case 'clear':
188
267
  case 'version':
189
268
  case 'jobs':
269
+ case 'ask':
270
+ case 'consensus':
190
271
  case 'diff':
191
272
  case 'cost':
192
273
  case 'status':
274
+ case 'resume':
193
275
  case 'stub':
194
276
  // All non-overlay verdicts: the session module already appended
195
- // any operator-visible system lines. No further UI side effect.
277
+ // any operator-visible system lines (and, for `ask`, set
278
+ // pendingAsk so the modal renders on the next frame). No further
279
+ // UI side effect needed here.
196
280
  return;
197
281
  }
198
282
  }
@@ -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
- const status = connectionLabel(props.connection);
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
- function connectionLabel(connection) {
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
- return { label: 'connecting', color: 'cyan' };
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';
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const DEFAULT_COLLAPSE_THRESHOLD = 5;
4
+ const DEFAULT_MAX_ROWS = 8;
5
+ export function ToolStreamPane(props) {
6
+ const calls = props.calls;
7
+ const collapseThreshold = props.collapseThreshold ?? DEFAULT_COLLAPSE_THRESHOLD;
8
+ const maxRows = props.maxRows ?? DEFAULT_MAX_ROWS;
9
+ if (calls.length === 0) {
10
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PaneHeader, { count: 0, collapsed: props.collapsed }), _jsx(Text, { dimColor: true, children: "No tool calls yet. Dispatch a brief and watch tools land here." })] }));
11
+ }
12
+ if (props.collapsed === true) {
13
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(PaneHeader, { count: calls.length, collapsed: true }) }));
14
+ }
15
+ // Tail-window the rows so a long-running session does not overflow
16
+ // the bottom half of the frame. Older rows stay in the session state
17
+ // for /jobs and /diff inspection.
18
+ const visible = calls.slice(-maxRows);
19
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PaneHeader, { count: calls.length, collapsed: false }), visible.map((call) => (_jsx(ToolCallRow, { call: call, collapseThreshold: collapseThreshold }, call.id)))] }));
20
+ }
21
+ function PaneHeader({ count, collapsed }) {
22
+ const verb = collapsed ? 'Ctrl+T to expand' : 'Ctrl+T to collapse';
23
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, dimColor: true, children: '─ tools ' }), _jsx(Text, { dimColor: true, children: `(${count}) ` }), _jsx(Text, { dimColor: true, children: verb })] }));
24
+ }
25
+ function ToolCallRow({ call, collapseThreshold, }) {
26
+ const glyph = statusGlyph(call.status);
27
+ const color = statusColor(call.status);
28
+ const label = formatToolLabel(call.tool, call.args);
29
+ const summary = formatSummary(call);
30
+ const showHint = (call.resultLines ?? 0) > collapseThreshold;
31
+ return (_jsxs(Box, { children: [_jsx(Text, { color: color, children: glyph }), _jsx(Text, { children: ' ' }), _jsx(Text, { bold: true, children: label }), _jsx(Text, { dimColor: true, children: ` ${summary}` }), showHint ? (_jsx(Text, { dimColor: true, children: ` · ${call.resultLines} lines, Ctrl+O to expand` })) : null] }));
32
+ }
33
+ function statusGlyph(status) {
34
+ switch (status) {
35
+ case 'running':
36
+ return '→';
37
+ case 'ok':
38
+ return '✓';
39
+ case 'error':
40
+ return '✗';
41
+ }
42
+ }
43
+ function statusColor(status) {
44
+ switch (status) {
45
+ case 'running':
46
+ return 'cyan';
47
+ case 'ok':
48
+ return 'green';
49
+ case 'error':
50
+ return 'red';
51
+ }
52
+ }
53
+ /**
54
+ * Render the canonical `Tool(args)` form. Tool names are capitalised
55
+ * the way Claude Code shows them; args are truncated to 60 chars so
56
+ * the row stays single-line even on 80-col terminals.
57
+ */
58
+ function formatToolLabel(tool, args) {
59
+ const name = toolDisplayName(tool);
60
+ const trimmedArgs = args.length > 60 ? `${args.slice(0, 57)}…` : args;
61
+ return `${name}(${trimmedArgs})`;
62
+ }
63
+ function toolDisplayName(tool) {
64
+ switch (tool) {
65
+ case 'read':
66
+ return 'Read';
67
+ case 'edit':
68
+ return 'Edit';
69
+ case 'bash':
70
+ return 'Bash';
71
+ case 'grep':
72
+ return 'Grep';
73
+ case 'glob':
74
+ return 'Glob';
75
+ case 'web_fetch':
76
+ return 'WebFetch';
77
+ }
78
+ }
79
+ function formatSummary(call) {
80
+ if (call.status === 'running') {
81
+ return call.detail ?? 'running...';
82
+ }
83
+ if (call.status === 'error') {
84
+ return call.detail ?? 'error';
85
+ }
86
+ // ok
87
+ const duration = typeof call.durationMs === 'number' ? `${call.durationMs}ms` : '';
88
+ const detail = call.detail ?? 'OK';
89
+ return duration.length > 0 ? `${detail} ${duration}` : detail;
90
+ }
91
+ //# sourceMappingURL=tool-stream-pane.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "servers": {
3
+ "codegraph": {
4
+ "command": "codegraph",
5
+ "args": ["serve", "--mcp"],
6
+ "env": {},
7
+ "trust": "pending"
8
+ }
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-alpha.9",
4
- "description": "Pugi CLI terminal-native software execution system",
3
+ "version": "0.1.0-beta.10",
4
+ "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,30 +28,38 @@
28
28
  "files": [
29
29
  "bin/run.js",
30
30
  "dist/**/*.js",
31
+ "assets/**/*.ansi",
32
+ "docs/examples/**/*.json",
31
33
  "README.md",
32
- "LICENSE"
34
+ "LICENSE",
35
+ "THIRD_PARTY_NOTICES.md"
33
36
  ],
34
37
  "engines": {
35
- "node": ">=20"
38
+ "node": ">=22.5.0"
36
39
  },
40
+ "//publishConfig": "CEO sign-off 2026-05-23 alpha launch: @pugi/cli is the customer-facing CLI and ships PUBLIC so `npm i -g @pugi/cli` resolves anonymously. The HARD default-restricted memory rule (2026-05-25) applies to INTERNAL @pugi/* packages (db-client, telegram bot helpers, etc.) - those must ship access:restricted unless an additional CEO sign-off granted. @pugi/sdk + @pugi/personas were published public alongside the CLI under the same alpha-launch sign-off because the CLI's customer install pulls them as transitive deps; review before α7 GA.",
37
41
  "publishConfig": {
38
42
  "access": "public"
39
43
  },
40
44
  "dependencies": {
41
45
  "@mozilla/readability": "^0.6.0",
46
+ "chokidar": "^3.6.0",
47
+ "ignore": "^5.3.2",
42
48
  "ink": "^5.0.1",
43
49
  "linkedom": "^0.18.12",
44
50
  "react": "^18.3.1",
51
+ "tar": "^6.2.1",
45
52
  "tinyglobby": "^0.2.16",
46
53
  "turndown": "^7.2.4",
47
54
  "undici": "^8.3.0",
48
55
  "zod": "^3.23.0",
49
- "@pugi/personas": "0.1.0",
50
- "@pugi/sdk": "0.1.0-alpha.9"
56
+ "@pugi/personas": "0.1.2",
57
+ "@pugi/sdk": "0.1.0-beta.9"
51
58
  },
52
59
  "devDependencies": {
53
60
  "@types/node": "^22.0.0",
54
61
  "@types/react": "^18.3.3",
62
+ "@types/tar": "^6.1.13",
55
63
  "@types/turndown": "^5.0.6",
56
64
  "ink-testing-library": "^4.0.0",
57
65
  "tsx": "^4.19.0",