@pugi/cli 0.1.0-beta.12 → 0.1.0-beta.13
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/consensus/diff-capture.js +73 -0
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/engine/anvil-client.js +80 -5
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +663 -249
- package/dist/core/engine/prompts.js +52 -2
- package/dist/core/engine/tool-bridge.js +311 -9
- package/dist/core/lsp/client.js +57 -0
- package/dist/core/mcp/client.js +9 -0
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +328 -12
- package/dist/core/repl/slash-commands.js +18 -4
- package/dist/core/settings.js +43 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +534 -268
- package/dist/runtime/commands/lsp.js +165 -5
- package/dist/runtime/commands/mcp.js +537 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +192 -0
- package/dist/tools/apply-patch.js +62 -1
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/registry.js +5 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/conversation-pane.js +1 -1
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/repl-render.js +105 -15
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +10 -4
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/update-banner.js +20 -2
- package/package.json +5 -4
package/dist/tui/repl-render.js
CHANGED
|
@@ -24,6 +24,7 @@ import { ReplSession, } from '../core/repl/session.js';
|
|
|
24
24
|
import { resolveWorkspaceContext } from '../core/repl/workspace-context.js';
|
|
25
25
|
import { SqliteSessionStore } from '../core/repl/store/index.js';
|
|
26
26
|
import { slugForCwd } from '../core/repl/history.js';
|
|
27
|
+
import { loadSettings } from '../core/settings.js';
|
|
27
28
|
import { WorkingSet, buildRepoSkeleton, loadPugiIgnore, PugiWatcher, } from '../core/context/index.js';
|
|
28
29
|
/**
|
|
29
30
|
* Mount the REPL and resolve when the user exits via Ctrl+C × 2 or
|
|
@@ -50,22 +51,44 @@ export async function renderRepl(options) {
|
|
|
50
51
|
// top, and our finally{} restore drops the floor only after Ink
|
|
51
52
|
// has cleanly torn down (or never mounted on a bootstrap crash).
|
|
52
53
|
const bootstrap = claimTerminalForRepl();
|
|
53
|
-
// beta.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
54
|
+
// beta.13 auto-init wire (CEO dogfood 2026-05-26): scaffold the
|
|
55
|
+
// `.pugi/` workspace silently on REPL boot so launching `pugi` in a
|
|
56
|
+
// fresh cwd no longer demands an explicit `pugi init` round-trip.
|
|
57
|
+
// Idempotent — every helper inside scaffoldPugiWorkspace is a
|
|
58
|
+
// `*_IfMissing` write, so re-running over an existing workspace is
|
|
59
|
+
// a no-op. Fail-safe: any FS / perms error never blocks REPL launch.
|
|
60
|
+
// Operator escape hatch: PUGI_NO_AUTO_INIT=1.
|
|
61
|
+
//
|
|
62
|
+
// Beta.13 P2 fix 2026-05-26: gate the scaffold on project-root markers
|
|
63
|
+
// so launching `pugi` from `$HOME` / `/tmp` / arbitrary dirs does NOT
|
|
64
|
+
// sprinkle `.pugi/` directories all over the filesystem. The gate
|
|
65
|
+
// mirrors `isBoundWorkspace` from workspace-context.ts but also
|
|
66
|
+
// accepts non-JS roots (Cargo / pyproject / go.mod) because the CLI
|
|
67
|
+
// is language-agnostic and an operator working in a Rust repo deserves
|
|
68
|
+
// the same auto-init UX as a Node operator. Already-bound `.pugi/`
|
|
69
|
+
// dirs also opt back in so the scaffold can fill any missing
|
|
70
|
+
// sub-artifacts the operator deleted.
|
|
71
|
+
if (process.env.PUGI_NO_AUTO_INIT !== '1' && isProjectRoot(process.cwd())) {
|
|
61
72
|
try {
|
|
62
|
-
const {
|
|
63
|
-
|
|
73
|
+
const { scaffoldPugiWorkspace } = await import('../runtime/cli.js');
|
|
74
|
+
await scaffoldPugiWorkspace({
|
|
75
|
+
cwd: process.cwd(),
|
|
76
|
+
noDefaults: true,
|
|
77
|
+
log: () => {
|
|
78
|
+
/* silent — never leak scaffold progress into the REPL alt-screen */
|
|
79
|
+
},
|
|
80
|
+
});
|
|
64
81
|
}
|
|
65
|
-
catch {
|
|
66
|
-
// Fail-safe:
|
|
67
|
-
//
|
|
68
|
-
//
|
|
82
|
+
catch (err) {
|
|
83
|
+
// Fail-safe: read-only FS or perms error never blocks REPL launch.
|
|
84
|
+
// Beta.13 P2 fix 2026-05-26: bare-catch swallowed the diagnostic;
|
|
85
|
+
// surface it on stderr under PUGI_DEBUG=1 so operator-triage on
|
|
86
|
+
// "why isn't .pugi/ being created?" has a starting point without
|
|
87
|
+
// having to re-instrument the bootstrap.
|
|
88
|
+
if (process.env.PUGI_DEBUG === '1') {
|
|
89
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
+
process.stderr.write(`[pugi-debug] auto-init failed: ${msg}\n`);
|
|
91
|
+
}
|
|
69
92
|
}
|
|
70
93
|
}
|
|
71
94
|
const transport = createProductionTransport();
|
|
@@ -74,6 +97,12 @@ export async function renderRepl(options) {
|
|
|
74
97
|
// best-effort — any FS error falls back to a basename-only summary,
|
|
75
98
|
// never blocks REPL launch. Wave 4 fix 2026-05-25.
|
|
76
99
|
const workspace = options.workspace ?? resolveWorkspaceContext(process.cwd());
|
|
100
|
+
// Beta.13 P1 fix 2026-05-26: read `ui.cyberZoo` from
|
|
101
|
+
// `.pugi/settings.json` so the operator's splash posture flows to
|
|
102
|
+
// admin-api on session open. Without this, the renderer's `cyberZoo`
|
|
103
|
+
// parameter (added beta.13) was always defaulted to 'on' regardless
|
|
104
|
+
// of the operator's actual setting.
|
|
105
|
+
const cyberZoo = readCyberZooSetting(process.cwd());
|
|
77
106
|
// α6.4: open the local SessionStore for `/resume` persistence. The
|
|
78
107
|
// store lives under `~/.pugi/projects/<slug>/`; failure is fail-safe
|
|
79
108
|
// — we log a one-line warning to stderr and continue with the REPL
|
|
@@ -101,6 +130,7 @@ export async function renderRepl(options) {
|
|
|
101
130
|
cliVersion: options.cliVersion,
|
|
102
131
|
transport,
|
|
103
132
|
workspace,
|
|
133
|
+
cyberZoo,
|
|
104
134
|
store,
|
|
105
135
|
localSessionId: openedSessionId,
|
|
106
136
|
repoSkeleton: skeleton,
|
|
@@ -260,6 +290,57 @@ export function drainBufferedStdin(stdin = process.stdin) {
|
|
|
260
290
|
return 0;
|
|
261
291
|
}
|
|
262
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Project-root probe — beta.13 P2 fix 2026-05-26.
|
|
295
|
+
*
|
|
296
|
+
* Beta.13 auto-init was unconditional and silently created `.pugi/` in
|
|
297
|
+
* every cwd the REPL was launched from, including `$HOME` and `/tmp`.
|
|
298
|
+
* Operators who ran `pugi` to ask a quick question outside of any
|
|
299
|
+
* project ended up with stray `.pugi/` directories polluting their
|
|
300
|
+
* filesystem. The gate looks for any of six project-root markers
|
|
301
|
+
* before scaffolding:
|
|
302
|
+
*
|
|
303
|
+
* - `package.json` — JS / TS workspaces
|
|
304
|
+
* - `.git` — any cloned repo regardless of language
|
|
305
|
+
* - `.pugi` — already-bound Pugi workspace (re-scaffold
|
|
306
|
+
* fills any missing artifacts the operator
|
|
307
|
+
* deleted, idempotent over existing files)
|
|
308
|
+
* - `Cargo.toml` — Rust crates
|
|
309
|
+
* - `pyproject.toml` — Python projects (PEP 518)
|
|
310
|
+
* - `go.mod` — Go modules
|
|
311
|
+
*
|
|
312
|
+
* The probe is six cheap `existsSync` calls; the cost is negligible
|
|
313
|
+
* compared with the alt-screen + Ink mount that follows. Exported so a
|
|
314
|
+
* future unit spec can lock the contract.
|
|
315
|
+
*/
|
|
316
|
+
export function isProjectRoot(cwd) {
|
|
317
|
+
// Local import keeps the bootstrap free of top-of-file `fs` calls
|
|
318
|
+
// that would run at module-load time — Ink + the SSE transport are
|
|
319
|
+
// happier when this file's side-effect surface stays small.
|
|
320
|
+
const { existsSync } = require('node:fs');
|
|
321
|
+
const { resolve } = require('node:path');
|
|
322
|
+
return (existsSync(resolve(cwd, 'package.json')) ||
|
|
323
|
+
existsSync(resolve(cwd, '.git')) ||
|
|
324
|
+
existsSync(resolve(cwd, '.pugi')) ||
|
|
325
|
+
existsSync(resolve(cwd, 'Cargo.toml')) ||
|
|
326
|
+
existsSync(resolve(cwd, 'pyproject.toml')) ||
|
|
327
|
+
existsSync(resolve(cwd, 'go.mod')));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Read the operator's cyber-zoo posture from `.pugi/settings.json`.
|
|
331
|
+
* Best-effort: when the file is missing / malformed, fall through to
|
|
332
|
+
* the historical 'on' default so the REPL never refuses to launch on
|
|
333
|
+
* a settings error. Beta.13 P1 fix 2026-05-26.
|
|
334
|
+
*/
|
|
335
|
+
function readCyberZooSetting(cwd) {
|
|
336
|
+
try {
|
|
337
|
+
const settings = loadSettings(cwd);
|
|
338
|
+
return settings.ui?.cyberZoo ?? 'on';
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return 'on';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
263
344
|
/**
|
|
264
345
|
* Open the local SessionStore for the REPL bootstrap. Returns
|
|
265
346
|
* `{ store: null, openedSessionId: undefined }` on any error so the
|
|
@@ -353,11 +434,18 @@ async function bootstrapContext(input) {
|
|
|
353
434
|
/* ------------------------------------------------------------------ */
|
|
354
435
|
export function createProductionTransport() {
|
|
355
436
|
return {
|
|
356
|
-
async createSession({ apiUrl, apiKey, workspace }) {
|
|
437
|
+
async createSession({ apiUrl, apiKey, workspace, cyberZoo }) {
|
|
357
438
|
// Forward the workspace bundle in the POST body so admin-api can
|
|
358
439
|
// surface `<workspace-context>` in Mira's prompt. Older admin-api
|
|
359
440
|
// builds ignore unknown fields, so this stays forward-compatible.
|
|
360
441
|
// Wave 4 fix 2026-05-25.
|
|
442
|
+
//
|
|
443
|
+
// Beta.13 P1 fix 2026-05-26: also forward `cyberZoo` so admin-api
|
|
444
|
+
// can render Mira's `<cyber-zoo>` marker matching the operator's
|
|
445
|
+
// `.pugi/settings.json::ui.cyberZoo` toggle instead of the
|
|
446
|
+
// historical 'on' default. Only included on the wire when set
|
|
447
|
+
// explicitly so a missing setting still survives older admin-api
|
|
448
|
+
// builds that do not declare the DTO field.
|
|
361
449
|
const body = {};
|
|
362
450
|
if (workspace?.workspaceCwd)
|
|
363
451
|
body.workspaceCwd = workspace.workspaceCwd;
|
|
@@ -365,6 +453,8 @@ export function createProductionTransport() {
|
|
|
365
453
|
body.workspaceSlug = workspace.workspaceSlug;
|
|
366
454
|
if (workspace?.workspaceSummary)
|
|
367
455
|
body.workspaceSummary = workspace.workspaceSummary;
|
|
456
|
+
if (cyberZoo === 'on' || cyberZoo === 'off')
|
|
457
|
+
body.cyberZoo = cyberZoo;
|
|
368
458
|
const response = await fetch(joinUrl(apiUrl, '/api/pugi/sessions'), {
|
|
369
459
|
method: 'POST',
|
|
370
460
|
headers: jsonHeaders(apiKey),
|
package/dist/tui/repl-splash.js
CHANGED
|
@@ -67,7 +67,7 @@ export function ReplSplash(props) {
|
|
|
67
67
|
// pugs. The header card still renders inline so wordmark + status
|
|
68
68
|
// rows stay attached to the splash flow.
|
|
69
69
|
const showHandCraftedMascot = props.mascotPrePrinted !== true;
|
|
70
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", children: [showHandCraftedMascot ? _jsx(MascotColumn, {}) : null, _jsxs(Box, { flexDirection: "column", marginLeft: showHandCraftedMascot ? 2 : 0, marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "
|
|
70
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", children: [showHandCraftedMascot ? _jsx(MascotColumn, {}) : null, _jsxs(Box, { flexDirection: "column", marginLeft: showHandCraftedMascot ? 2 : 0, marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "#3da9fc", children: ".io" }), _jsx(Text, { dimColor: true, children: ` v${props.cliVersion}` })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HeaderRow, { label: "Plan", value: props.plan ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Model", value: props.model ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Tenant", value: props.tenant ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Workspace", value: props.workspaceLabel })] })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(40) }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Tips for getting started:" }), _jsx(TipRow, { index: 1, text: "Type a brief, the workforce dispatches" }), _jsx(TipRow, { index: 2, text: "/help for slash commands, /web <url> to pull a page" }), _jsx(TipRow, { index: 3, text: "/skills install <name> for Anthropic / OpenClaw skills" })] })] }));
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
73
|
* Renders the multi-line ASCII pug. Each row is split into colored
|
|
@@ -105,7 +105,7 @@ function MascotRow({ row, mask, }) {
|
|
|
105
105
|
if (buffer.length > 0) {
|
|
106
106
|
runs.push({ text: buffer, cyan: bufferCyan });
|
|
107
107
|
}
|
|
108
|
-
return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: "
|
|
108
|
+
return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: "#3da9fc", children: run.text }, runIndex)) : (_jsx(Text, { color: "gray", children: run.text }, runIndex))) }));
|
|
109
109
|
}
|
|
110
110
|
function HeaderRow({ label, value }) {
|
|
111
111
|
const padded = `${label}:`.padEnd(11, ' ');
|
package/dist/tui/repl.js
CHANGED
|
@@ -195,10 +195,14 @@ export function Repl(props) {
|
|
|
195
195
|
// Slug from process.cwd() (full path) so two workspaces with
|
|
196
196
|
// the same basename do not share history. state.workspaceLabel
|
|
197
197
|
// is the basename only. Codex review P2.
|
|
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
|
|
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, lastCompletedOutcome: state.lastCompletedOutcome,
|
|
199
|
+
// α7 cost-meter sprint — surface accumulated session totals
|
|
200
|
+
// + per-turn delta flash on the status bar's top row. The
|
|
201
|
+
// session module owns accumulation; the bar is a pure render.
|
|
202
|
+
sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta })] })] }));
|
|
199
203
|
}
|
|
200
204
|
function Header({ state }) {
|
|
201
|
-
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "
|
|
205
|
+
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "#3da9fc", children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: "#3da9fc", children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
|
|
202
206
|
}
|
|
203
207
|
function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamCollapsed, }) {
|
|
204
208
|
// α6.12: three vertical panes stacked above the input box.
|
|
@@ -238,14 +242,14 @@ function HelpOverlay() {
|
|
|
238
242
|
const rows = grouped.get(group);
|
|
239
243
|
if (!rows || rows.length === 0)
|
|
240
244
|
return null;
|
|
241
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ` -- ${group} --` }), rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "
|
|
245
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ` -- ${group} --` }), rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#3da9fc", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name)))] }, group));
|
|
242
246
|
}), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `${PUGI_TAGLINE}` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
|
|
243
247
|
}
|
|
244
248
|
function RosterOverlay() {
|
|
245
249
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "On-watch roster" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: THE_TEN.map((persona) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: ` ${persona.name.padEnd(10, ' ')}` }), _jsx(Text, { dimColor: true, children: `${persona.role.padEnd(20, ' ')}` }), _jsx(Text, { children: persona.oneLiner })] }, persona.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
|
|
246
250
|
}
|
|
247
251
|
function FarewellOverlay() {
|
|
248
|
-
return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "
|
|
252
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "#3da9fc", children: PUGI_TAGLINE }) }));
|
|
249
253
|
}
|
|
250
254
|
function applyVerdictSideEffects(verdict, handlers) {
|
|
251
255
|
switch (verdict.kind) {
|
|
@@ -270,8 +274,10 @@ function applyVerdictSideEffects(verdict, handlers) {
|
|
|
270
274
|
case 'consensus':
|
|
271
275
|
case 'diff':
|
|
272
276
|
case 'cost':
|
|
277
|
+
case 'quota':
|
|
273
278
|
case 'status':
|
|
274
279
|
case 'resume':
|
|
280
|
+
case 'mcp':
|
|
275
281
|
case 'stub':
|
|
276
282
|
// All non-overlay verdicts: the session module already appended
|
|
277
283
|
// any operator-visible system lines (and, for `ask`, set
|
package/dist/tui/splash.js
CHANGED
|
@@ -21,7 +21,7 @@ export function Splash({ data }) {
|
|
|
21
21
|
cmd: 'pugi login',
|
|
22
22
|
gloss: 'Connect this terminal to your Pugi account',
|
|
23
23
|
};
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "#3da9fc", children: "pugi.io" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `v${data.cliVersion} · ${data.apiUrl}` }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Account: " }), _jsx(Text, { children: accountLine })] }), data.isAuthenticated && data.plan ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Plan: " }), _jsx(Text, { children: data.plan })] })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Quick start:" }), _jsx(HintRow, { command: primaryHint.cmd, gloss: primaryHint.gloss }), data.isAuthenticated ? (_jsx(HintRow, { command: 'pugi login', gloss: 'Re-authenticate or switch accounts' })) : (_jsx(HintRow, { command: 'pugi code "fix the bug"', gloss: 'Run a one-shot coding task' })), _jsx(HintRow, { command: 'pugi review --triple', gloss: 'Run the Anvil triple-review gate' }), _jsx(HintRow, { command: 'pugi help', gloss: 'Full command reference' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Docs: https://pugi.dev \u00B7 Status: https://pugi.io/status" }) })] }));
|
|
25
25
|
}
|
|
26
26
|
function HintRow({ command, gloss }) {
|
|
27
27
|
// Pad command names so the gloss column lines up across rows.
|
package/dist/tui/status-bar.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { formatCostUsd, formatTokens } from '../core/repl/model-pricing.js';
|
|
4
|
+
/**
|
|
5
|
+
* Window during which the per-turn delta flash stays visible on the
|
|
6
|
+
* cost-meter row. CEO spec: ~2 seconds after completion. Past that, the
|
|
7
|
+
* flash dimms out and the row shows session totals only.
|
|
8
|
+
*/
|
|
9
|
+
const TURN_DELTA_FLASH_MS = 2_000;
|
|
3
10
|
/**
|
|
4
11
|
* Cyan dot glyphs across the pulse cycle. Three steps keep the motion
|
|
5
12
|
* subtle - a true gradient would force an Ink rerender on every
|
|
@@ -17,8 +24,73 @@ export function StatusBar(props) {
|
|
|
17
24
|
// first. When the connection is healthy (`on_watch` / `connecting`),
|
|
18
25
|
// the FSM dispatch state takes over to show the dispatch lifecycle
|
|
19
26
|
// (`dispatching` / `tool: read` / `aborting` / etc.).
|
|
20
|
-
const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel);
|
|
21
|
-
|
|
27
|
+
const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel, props.lastCompletedOutcome);
|
|
28
|
+
// α7 cost-meter sprint — the cost row anchors above the legacy
|
|
29
|
+
// dispatch-state line so the operator's eye lands on the meter first
|
|
30
|
+
// (matches Claude Code TUI footer rhythm). The session-elapsed slot
|
|
31
|
+
// uses sessionStartedAtEpochMs (REPL boot), distinct from the
|
|
32
|
+
// per-brief `elapsedLabel` on the row below.
|
|
33
|
+
const costRow = renderCostMeterRow(props.sessionTokensIn ?? 0, props.sessionTokensOut ?? 0, props.sessionCostUsd ?? 0, props.sessionStartedAtEpochMs, now);
|
|
34
|
+
const deltaFlash = renderTurnDeltaFlash(props.lastTurnDelta, now);
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: `↑ ${costRow.tokensInLabel}` }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: `↓ ${costRow.tokensOutLabel}` }), _jsx(Text, { dimColor: true, children: ` · ` }), _jsx(Text, { bold: true, children: costRow.costLabel }), _jsx(Text, { dimColor: true, children: ` · ${costRow.elapsedLabel}` }), deltaFlash ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "green", children: deltaFlash })] })) : null] }), _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` }) })] }));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* α7 cost-meter sprint — assemble the cost-meter row labels. Pure helper
|
|
39
|
+
* so the snapshot tests assert the formatted shape without standing up
|
|
40
|
+
* an Ink renderer.
|
|
41
|
+
*/
|
|
42
|
+
export function renderCostMeterRow(tokensIn, tokensOut, costUsd, sessionStartedAtEpochMs, nowEpochMs) {
|
|
43
|
+
const elapsedMs = typeof sessionStartedAtEpochMs === 'number'
|
|
44
|
+
? Math.max(0, nowEpochMs - sessionStartedAtEpochMs)
|
|
45
|
+
: 0;
|
|
46
|
+
return {
|
|
47
|
+
tokensInLabel: formatTokens(tokensIn),
|
|
48
|
+
tokensOutLabel: formatTokens(tokensOut),
|
|
49
|
+
costLabel: formatCostUsd(costUsd),
|
|
50
|
+
elapsedLabel: formatElapsedShort(elapsedMs),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* α7 cost-meter sprint — render the per-turn delta flash when the most
|
|
55
|
+
* recent turn completed within the flash window. Returns null when no
|
|
56
|
+
* turn has completed yet OR the flash has expired (the elapsed slot
|
|
57
|
+
* gets the slot back). The flash format mirrors the spec:
|
|
58
|
+
*
|
|
59
|
+
* `+200/+1.1k +$0.01`
|
|
60
|
+
*
|
|
61
|
+
* Exported for snapshot tests.
|
|
62
|
+
*/
|
|
63
|
+
export function renderTurnDeltaFlash(delta, nowEpochMs) {
|
|
64
|
+
if (!delta)
|
|
65
|
+
return null;
|
|
66
|
+
const elapsedSinceMs = nowEpochMs - delta.completedAtEpochMs;
|
|
67
|
+
if (elapsedSinceMs < 0 || elapsedSinceMs > TURN_DELTA_FLASH_MS)
|
|
68
|
+
return null;
|
|
69
|
+
const inLabel = formatTokens(delta.tokensIn);
|
|
70
|
+
const outLabel = formatTokens(delta.tokensOut);
|
|
71
|
+
const costLabel = delta.costUsd > 0 ? ` +${formatCostUsd(delta.costUsd)}` : '';
|
|
72
|
+
return `+${inLabel}/+${outLabel}${costLabel}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* α7 cost-meter sprint — local copy of the session elapsed formatter.
|
|
76
|
+
* Mirrors the helper in `core/repl/session.ts` so the status bar stays
|
|
77
|
+
* a pure leaf component without a circular import on session.ts (the
|
|
78
|
+
* model-pricing module's `formatDuration` is similar but ships an
|
|
79
|
+
* `XhYm` ceiling that does not match the CEO spec's `2m44s` shape).
|
|
80
|
+
*/
|
|
81
|
+
function formatElapsedShort(elapsedMs) {
|
|
82
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs <= 0)
|
|
83
|
+
return '0s';
|
|
84
|
+
const totalSec = Math.floor(elapsedMs / 1000);
|
|
85
|
+
if (totalSec < 60)
|
|
86
|
+
return `${totalSec}s`;
|
|
87
|
+
const min = Math.floor(totalSec / 60);
|
|
88
|
+
const sec = totalSec % 60;
|
|
89
|
+
if (min < 60)
|
|
90
|
+
return `${min}m${sec.toString().padStart(2, '0')}s`;
|
|
91
|
+
const hr = Math.floor(min / 60);
|
|
92
|
+
const restMin = min % 60;
|
|
93
|
+
return `${hr}h${restMin.toString().padStart(2, '0')}m`;
|
|
22
94
|
}
|
|
23
95
|
/**
|
|
24
96
|
* Render a count badge — number if defined, `—` placeholder otherwise.
|
|
@@ -75,7 +147,7 @@ export function connectionLabel(connection) {
|
|
|
75
147
|
* `tool: <kind>` upstream so we just concatenate; null falls through
|
|
76
148
|
* to the bare `tool` placeholder.
|
|
77
149
|
*/
|
|
78
|
-
function composeStatusLabel(connection, dispatchState, toolLabel) {
|
|
150
|
+
function composeStatusLabel(connection, dispatchState, toolLabel, lastCompletedOutcome) {
|
|
79
151
|
// Transport health wins.
|
|
80
152
|
if (connection === 'offline' || connection === 'reconnecting') {
|
|
81
153
|
return connectionLabel(connection);
|
|
@@ -93,6 +165,16 @@ function composeStatusLabel(connection, dispatchState, toolLabel) {
|
|
|
93
165
|
case 'awaiting_response':
|
|
94
166
|
return { label: 'dispatching', color: 'cyan' };
|
|
95
167
|
case 'completed':
|
|
168
|
+
// Branch on the work-done outcome so the bottom-bar tells the
|
|
169
|
+
// same truth as the agent-tree (2026-05-26 — memory
|
|
170
|
+
// feedback_no_fake_dispatch_promises). `'replied'` = text-only
|
|
171
|
+
// turn, render with the same neutral gray + arrow used in the
|
|
172
|
+
// agent-tree. `'shipped'` = real side-effect (or older server
|
|
173
|
+
// that omits the outcome field). Defaults to `'shipped'` so
|
|
174
|
+
// older callers without the prop wired stay back-compat.
|
|
175
|
+
if (lastCompletedOutcome === 'replied') {
|
|
176
|
+
return { label: 'replied', color: 'gray' };
|
|
177
|
+
}
|
|
96
178
|
return { label: 'shipped', color: 'green' };
|
|
97
179
|
case 'idle':
|
|
98
180
|
case undefined:
|
|
@@ -111,18 +193,14 @@ function formatElapsed(startedAt, now) {
|
|
|
111
193
|
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
112
194
|
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
113
195
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (total < 1_000_000)
|
|
123
|
-
return `${(total / 1_000).toFixed(1)}k`;
|
|
124
|
-
return `${(total / 1_000_000).toFixed(1)}m`;
|
|
125
|
-
}
|
|
196
|
+
// `formatTokens` for the downstream-throughput slot is imported from
|
|
197
|
+
// `core/repl/model-pricing.ts` — single source of truth for token
|
|
198
|
+
// formatting across the cost-meter row, `/cost` slash, and the legacy
|
|
199
|
+
// downstream-tokens slot. The shape is identical to the prior local
|
|
200
|
+
// helper (`<1000` raw, `<1m` one-decimal k, `≥1m` one-decimal m); the
|
|
201
|
+
// only semantic difference is non-finite / negative inputs render as
|
|
202
|
+
// `0` instead of throwing, matching the cost-meter row's defensive
|
|
203
|
+
// posture.
|
|
126
204
|
function clampPhase(phase) {
|
|
127
205
|
if (typeof phase !== 'number' || Number.isNaN(phase))
|
|
128
206
|
return 0;
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
import { upgradeCommand } from '../runtime/update-check.js';
|
|
3
|
+
import { compareVersions, upgradeCommand, } from '../runtime/update-check.js';
|
|
4
|
+
import { getCachedServerRecommendation } from '../core/transport/version-interceptor.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the `latest` value the banner should show. Exported so the
|
|
7
|
+
* spec can lock the merge logic without rendering Ink.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveDisplayedLatest(npmLatest, serverRecommended) {
|
|
10
|
+
if (!serverRecommended)
|
|
11
|
+
return npmLatest;
|
|
12
|
+
return compareVersions(serverRecommended, npmLatest) > 0
|
|
13
|
+
? serverRecommended
|
|
14
|
+
: npmLatest;
|
|
15
|
+
}
|
|
4
16
|
export function UpdateBanner({ result }) {
|
|
5
17
|
const command = upgradeCommand(result.method);
|
|
6
|
-
|
|
18
|
+
// Read the cache lazily inside the render so a server response that
|
|
19
|
+
// landed AFTER the banner was constructed still shows up on the next
|
|
20
|
+
// re-render. The cache lookup is a single map read — cheap enough to
|
|
21
|
+
// do per render.
|
|
22
|
+
const serverRecommended = getCachedServerRecommendation();
|
|
23
|
+
const displayedLatest = resolveDisplayedLatest(result.latest, serverRecommended);
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: '─ ' }), _jsx(Text, { bold: true, color: "#3da9fc", children: 'Pugi ' }), _jsx(Text, { children: result.installed }), _jsx(Text, { dimColor: true, children: ' (installed) → ' }), _jsx(Text, { bold: true, children: displayedLatest }), _jsx(Text, { dimColor: true, children: ' (latest)' })] }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: ' Update: ' }), _jsx(Text, { color: "#3da9fc", children: command })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ' Skip with PUGI_SKIP_UPDATE_BANNER=1' }) })] }));
|
|
7
25
|
}
|
|
8
26
|
//# sourceMappingURL=update-banner.js.map
|
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.13",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"undici": "^8.3.0",
|
|
55
55
|
"zod": "^3.23.0",
|
|
56
56
|
"@pugi/personas": "0.1.2",
|
|
57
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
57
|
+
"@pugi/sdk": "0.1.0-beta.13"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^22.0.0",
|
|
@@ -69,9 +69,10 @@
|
|
|
69
69
|
"build": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
70
70
|
"dev": "tsx src/index.ts",
|
|
71
71
|
"typecheck": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
|
|
72
|
-
"test": "pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx'",
|
|
72
|
+
"test": "pnpm run check:version-lockstep && pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx'",
|
|
73
73
|
"version:cli": "tsx src/index.ts version",
|
|
74
74
|
"doctor": "tsx src/index.ts doctor --json",
|
|
75
|
-
"
|
|
75
|
+
"check:version-lockstep": "bash ../../scripts/check-version-lockstep.sh",
|
|
76
|
+
"pack:smoke": "pnpm run check:version-lockstep && node scripts/pack-smoke.mjs"
|
|
76
77
|
}
|
|
77
78
|
}
|