@pugi/cli 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +20 -0
- package/dist/commands/jobs.js +245 -0
- package/dist/core/agents/registry.js +69 -0
- package/dist/core/bash-classifier.js +1001 -0
- package/dist/core/context/builder.js +114 -0
- package/dist/core/context/compaction-events.js +99 -0
- package/dist/core/context/compaction.js +602 -0
- package/dist/core/context/invariants.js +250 -0
- package/dist/core/context/markdown-loader.js +270 -0
- package/dist/core/engine/compaction-hook.js +154 -0
- package/dist/core/engine/index.js +5 -0
- package/dist/core/engine/prompts.js +42 -0
- package/dist/core/engine/tool-bridge.js +159 -61
- package/dist/core/hooks.js +415 -0
- package/dist/core/jobs/registry.js +462 -0
- package/dist/core/mcp/client.js +316 -0
- package/dist/core/mcp/registry.js +171 -0
- package/dist/core/mcp/trust.js +91 -0
- package/dist/core/permission.js +221 -116
- package/dist/core/repl/cap-warning.js +91 -0
- package/dist/core/repl/session.js +399 -0
- package/dist/core/repl/slash-commands.js +116 -0
- package/dist/core/session.js +168 -0
- package/dist/core/subagents/dispatcher.js +258 -0
- package/dist/core/subagents/index.js +26 -0
- package/dist/core/subagents/spawn.js +86 -0
- package/dist/core/trust.js +109 -0
- package/dist/runtime/cli.js +157 -45
- package/dist/runtime/commands/budget.js +192 -0
- package/dist/runtime/commands/config.js +231 -0
- package/dist/runtime/commands/privacy.js +107 -0
- package/dist/runtime/commands/undo.js +329 -0
- package/dist/tools/bash.js +660 -0
- package/dist/tui/agent-tree.js +66 -0
- package/dist/tui/conversation-pane.js +45 -0
- package/dist/tui/input-box.js +91 -0
- package/dist/tui/login-picker.js +69 -0
- package/dist/tui/render.js +68 -0
- package/dist/tui/repl-render.js +218 -0
- package/dist/tui/repl.js +152 -0
- package/dist/tui/splash-data.js +61 -0
- package/dist/tui/splash.js +31 -0
- package/dist/tui/status-bar.js +58 -0
- package/package.json +11 -5
package/dist/tui/repl.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* REPL root component - Sprint α5.7 (ADR-0056 PR-PUGI-CLI-REPL-DEFAULT).
|
|
4
|
+
*
|
|
5
|
+
* Three-zone layout:
|
|
6
|
+
*
|
|
7
|
+
* header - `Pugi.io · workspace: <name> · v<X> on watch`
|
|
8
|
+
* main - conversation pane (top half) + agent tree (bottom half)
|
|
9
|
+
* footer - input box + status bar + key hints
|
|
10
|
+
*
|
|
11
|
+
* The component subscribes to a ReplSession instance for state. It does
|
|
12
|
+
* NOT own the SSE client or the transport - the session module does.
|
|
13
|
+
* This keeps the component a pure render over a typed state plus three
|
|
14
|
+
* callbacks (submit, exit, overlay request).
|
|
15
|
+
*
|
|
16
|
+
* Overlays (`/help`, `/agents`) are rendered as full-pane modals that
|
|
17
|
+
* eclipse the main area so they read top-to-bottom even on small
|
|
18
|
+
* terminals. Pressing any key dismisses an overlay.
|
|
19
|
+
*/
|
|
20
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
21
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
22
|
+
import { PUGI_TAGLINE, THE_TEN } from '@pugi/personas';
|
|
23
|
+
import { AgentTree } from './agent-tree.js';
|
|
24
|
+
import { ConversationPane } from './conversation-pane.js';
|
|
25
|
+
import { InputBox } from './input-box.js';
|
|
26
|
+
import { StatusBar } from './status-bar.js';
|
|
27
|
+
import { SLASH_COMMAND_HELP } from '../core/repl/slash-commands.js';
|
|
28
|
+
const TICK_INTERVAL_MS = 200;
|
|
29
|
+
const PULSE_INTERVAL_MS = 700;
|
|
30
|
+
export function Repl(props) {
|
|
31
|
+
const [state, setState] = useState(props.session.getState());
|
|
32
|
+
const [overlay, setOverlay] = useState('none');
|
|
33
|
+
const [pulsePhase, setPulsePhase] = useState(0);
|
|
34
|
+
const [tickNow, setTickNow] = useState((props.now ?? Date.now)());
|
|
35
|
+
// Subscribe to session state updates. The session module fires the
|
|
36
|
+
// callback synchronously inside `patch` so we mirror without a
|
|
37
|
+
// batching layer.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const unsubscribe = props.session.subscribe(setState);
|
|
40
|
+
return unsubscribe;
|
|
41
|
+
}, [props.session]);
|
|
42
|
+
// 200ms tick for the status-bar wall-clock + token counter rerender.
|
|
43
|
+
// Cheaper than re-mounting the session subscription and lets the
|
|
44
|
+
// status bar lag the dispatcher events by at most one frame.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const interval = setInterval(() => {
|
|
47
|
+
setTickNow((props.now ?? Date.now)());
|
|
48
|
+
}, TICK_INTERVAL_MS);
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, [props.now]);
|
|
51
|
+
// Pulse the on-watch dot every 700ms. Three glyphs cycle, so the
|
|
52
|
+
// operator sees a gentle three-step animation rather than a binary
|
|
53
|
+
// blink. Subtle by design - brand voice forbids flashing.
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const interval = setInterval(() => {
|
|
56
|
+
setPulsePhase((prev) => (prev + 1) % 3);
|
|
57
|
+
}, PULSE_INTERVAL_MS);
|
|
58
|
+
return () => clearInterval(interval);
|
|
59
|
+
}, []);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
props.onOverlayChange?.(overlay);
|
|
62
|
+
}, [overlay, props]);
|
|
63
|
+
const personaNames = useMemo(() => buildPersonaNameMap(), []);
|
|
64
|
+
const { exit } = useApp();
|
|
65
|
+
const handleSubmit = useCallback((line) => {
|
|
66
|
+
// Run async without awaiting - the session module owns the
|
|
67
|
+
// network call, errors land in the transcript automatically.
|
|
68
|
+
void props.session.handleInput(line).then((verdict) => {
|
|
69
|
+
applyVerdictSideEffects(verdict, {
|
|
70
|
+
showHelp: () => setOverlay('help'),
|
|
71
|
+
showRoster: () => setOverlay('roster'),
|
|
72
|
+
farewell: () => {
|
|
73
|
+
setOverlay('farewell');
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
props.session.close();
|
|
76
|
+
exit();
|
|
77
|
+
}, 800);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}, [props.session, exit]);
|
|
82
|
+
const handleExit = useCallback(() => {
|
|
83
|
+
setOverlay('farewell');
|
|
84
|
+
setTimeout(() => {
|
|
85
|
+
props.session.close();
|
|
86
|
+
exit();
|
|
87
|
+
}, 400);
|
|
88
|
+
}, [props.session, exit]);
|
|
89
|
+
// Any keystroke dismisses an overlay; mounting useInput at the root
|
|
90
|
+
// gives us the dismiss without colliding with the input box (which
|
|
91
|
+
// unmounts while an overlay is active).
|
|
92
|
+
useInput((_input, _key) => {
|
|
93
|
+
if (overlay !== 'none' && overlay !== 'farewell') {
|
|
94
|
+
setOverlay('none');
|
|
95
|
+
}
|
|
96
|
+
}, { isActive: overlay === 'help' || overlay === 'roster' });
|
|
97
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_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 })), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase })] })] }));
|
|
98
|
+
}
|
|
99
|
+
function Header({ state }) {
|
|
100
|
+
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('_', ' ') })] }));
|
|
101
|
+
}
|
|
102
|
+
function MainArea({ state, personaNames, nowEpochMs, }) {
|
|
103
|
+
// Show the last 12 transcript rows in the conversation slot so the
|
|
104
|
+
// bottom of the frame stays anchored to the input box. The agent
|
|
105
|
+
// tree drops below the transcript; new agents push the operator
|
|
106
|
+
// line up the screen, mirroring Claude Code / Codex CLI.
|
|
107
|
+
const conversationSlice = state.transcript.slice(-12);
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(ConversationPane, { rows: conversationSlice, personaNames: personaNames }), _jsx(Box, { marginTop: 1, children: _jsx(AgentTree, { agents: state.agents, nowEpochMs: nowEpochMs }) })] }));
|
|
109
|
+
}
|
|
110
|
+
function HelpOverlay() {
|
|
111
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pugi REPL help" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: SLASH_COMMAND_HELP.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name))) }), _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.' }) })] }));
|
|
112
|
+
}
|
|
113
|
+
function RosterOverlay() {
|
|
114
|
+
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.' }) })] }));
|
|
115
|
+
}
|
|
116
|
+
function FarewellOverlay() {
|
|
117
|
+
return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "cyan", children: PUGI_TAGLINE }) }));
|
|
118
|
+
}
|
|
119
|
+
function applyVerdictSideEffects(verdict, handlers) {
|
|
120
|
+
switch (verdict.kind) {
|
|
121
|
+
case 'help':
|
|
122
|
+
handlers.showHelp();
|
|
123
|
+
return;
|
|
124
|
+
case 'roster':
|
|
125
|
+
handlers.showRoster();
|
|
126
|
+
return;
|
|
127
|
+
case 'quit':
|
|
128
|
+
handlers.farewell();
|
|
129
|
+
return;
|
|
130
|
+
case 'dispatch':
|
|
131
|
+
case 'stop':
|
|
132
|
+
case 'error':
|
|
133
|
+
case 'noop':
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function countActive(state) {
|
|
138
|
+
let count = 0;
|
|
139
|
+
for (const agent of state.agents) {
|
|
140
|
+
if (agent.status === 'queued' || agent.status === 'thinking')
|
|
141
|
+
count += 1;
|
|
142
|
+
}
|
|
143
|
+
return count;
|
|
144
|
+
}
|
|
145
|
+
function buildPersonaNameMap() {
|
|
146
|
+
const map = new Map();
|
|
147
|
+
for (const persona of THE_TEN) {
|
|
148
|
+
map.set(persona.slug, persona.name);
|
|
149
|
+
}
|
|
150
|
+
return map;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=repl.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { DEFAULT_API_URL, maskApiKey, normalizeApiUrl, readCredentialsFile, resolveActiveCredential, } from '../core/credentials.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decode the JWT payload for display only — no signature check. The
|
|
5
|
+
* splash never sends the token over the wire so a stale or malformed
|
|
6
|
+
* JWT is still safe to show. Mirrors `decodeJwtPrincipal` in
|
|
7
|
+
* runtime/cli.ts, intentionally duplicated rather than imported to
|
|
8
|
+
* keep the TUI layer independent of the giant CLI module.
|
|
9
|
+
*/
|
|
10
|
+
function decodeJwtPayload(token) {
|
|
11
|
+
try {
|
|
12
|
+
const parts = token.split('.');
|
|
13
|
+
if (parts.length < 2)
|
|
14
|
+
return null;
|
|
15
|
+
const payload = parts[1];
|
|
16
|
+
if (!payload)
|
|
17
|
+
return null;
|
|
18
|
+
const padded = payload
|
|
19
|
+
.replace(/-/g, '+')
|
|
20
|
+
.replace(/_/g, '/')
|
|
21
|
+
.padEnd(payload.length + ((4 - (payload.length % 4)) % 4), '=');
|
|
22
|
+
const json = Buffer.from(padded, 'base64').toString('utf8');
|
|
23
|
+
const obj = JSON.parse(json);
|
|
24
|
+
if (!obj || typeof obj !== 'object')
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
sub: typeof obj.sub === 'string' ? obj.sub : undefined,
|
|
28
|
+
email: typeof obj.email === 'string' ? obj.email : undefined,
|
|
29
|
+
customerId: typeof obj.customerId === 'string' ? obj.customerId : undefined,
|
|
30
|
+
plan: typeof obj.plan === 'string' ? obj.plan : undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function collectSplashData(input) {
|
|
38
|
+
const env = input.env ?? process.env;
|
|
39
|
+
const home = input.home ?? homedir();
|
|
40
|
+
const credential = resolveActiveCredential(env, home);
|
|
41
|
+
if (!credential) {
|
|
42
|
+
const file = readCredentialsFile(home);
|
|
43
|
+
const apiUrl = normalizeApiUrl(env.PUGI_API_URL ?? file.activeApiUrl ?? DEFAULT_API_URL);
|
|
44
|
+
return {
|
|
45
|
+
cliVersion: input.cliVersion,
|
|
46
|
+
apiUrl,
|
|
47
|
+
isAuthenticated: false,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const principal = decodeJwtPayload(credential.apiKey);
|
|
51
|
+
return {
|
|
52
|
+
cliVersion: input.cliVersion,
|
|
53
|
+
apiUrl: credential.apiUrl,
|
|
54
|
+
isAuthenticated: true,
|
|
55
|
+
apiKeyMasked: maskApiKey(credential.apiKey),
|
|
56
|
+
...(principal?.email ? { email: principal.email } : {}),
|
|
57
|
+
...(principal?.customerId ? { tenant: principal.customerId } : {}),
|
|
58
|
+
...(principal?.plan ? { plan: principal.plan } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=splash-data.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Bare `pugi` (no args) splash. Rendered only on a real TTY by the
|
|
5
|
+
* CLI dispatcher; non-TTY callers see the existing usage dump.
|
|
6
|
+
*
|
|
7
|
+
* Layout intentionally restrained: bold "pugi.io" wordmark, dim
|
|
8
|
+
* version/endpoint metadata, neutral body. No background colors, no
|
|
9
|
+
* emoji decoration, ASCII separators only.
|
|
10
|
+
*/
|
|
11
|
+
export function Splash({ data }) {
|
|
12
|
+
const accountLine = data.isAuthenticated
|
|
13
|
+
? `${data.email ?? data.apiKeyMasked ?? 'authenticated'}${data.tenant ? ` (tenant: ${data.tenant})` : ''}`
|
|
14
|
+
: 'not signed in';
|
|
15
|
+
const primaryHint = data.isAuthenticated
|
|
16
|
+
? {
|
|
17
|
+
cmd: 'pugi code "fix the bug"',
|
|
18
|
+
gloss: 'Run a one-shot coding task',
|
|
19
|
+
}
|
|
20
|
+
: {
|
|
21
|
+
cmd: 'pugi login',
|
|
22
|
+
gloss: 'Connect this terminal to your Pugi account',
|
|
23
|
+
};
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "cyan", 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
|
+
}
|
|
26
|
+
function HintRow({ command, gloss }) {
|
|
27
|
+
// Pad command names so the gloss column lines up across rows.
|
|
28
|
+
const padded = command.padEnd(28, ' ');
|
|
29
|
+
return (_jsxs(Text, { children: [_jsx(Text, { children: ` ${padded}` }), _jsx(Text, { dimColor: true, children: gloss })] }));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=splash.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
/**
|
|
4
|
+
* Cyan dot glyphs across the pulse cycle. Three steps keep the motion
|
|
5
|
+
* subtle - a true gradient would force an Ink rerender on every
|
|
6
|
+
* 16ms frame and waste cycles on a status bar.
|
|
7
|
+
*/
|
|
8
|
+
const PULSE_DOTS = ['●', '◉', '○'];
|
|
9
|
+
export function StatusBar(props) {
|
|
10
|
+
const now = props.nowEpochMs ?? Date.now();
|
|
11
|
+
const elapsedLabel = formatElapsed(props.briefStartedAtEpochMs, now);
|
|
12
|
+
const tokenLabel = formatTokens(props.tokensDownstreamTotal);
|
|
13
|
+
const phase = clampPhase(props.pulsePhase);
|
|
14
|
+
const glyph = PULSE_DOTS[Math.min(phase, PULSE_DOTS.length - 1)] ?? PULSE_DOTS[0];
|
|
15
|
+
const status = connectionLabel(props.connection);
|
|
16
|
+
return (_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}` })] }));
|
|
17
|
+
}
|
|
18
|
+
function connectionLabel(connection) {
|
|
19
|
+
switch (connection) {
|
|
20
|
+
case 'connecting':
|
|
21
|
+
return { label: 'connecting', color: 'cyan' };
|
|
22
|
+
case 'on_watch':
|
|
23
|
+
return { label: 'on watch', color: 'cyan' };
|
|
24
|
+
case 'reconnecting':
|
|
25
|
+
return { label: 'reconnecting', color: 'yellow' };
|
|
26
|
+
case 'offline':
|
|
27
|
+
return { label: 'offline', color: 'gray' };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function formatElapsed(startedAt, now) {
|
|
31
|
+
if (typeof startedAt !== 'number')
|
|
32
|
+
return 'idle';
|
|
33
|
+
const ms = Math.max(0, now - startedAt);
|
|
34
|
+
if (ms < 60_000) {
|
|
35
|
+
return `${Math.floor(ms / 1000)}s`;
|
|
36
|
+
}
|
|
37
|
+
const minutes = Math.floor(ms / 60_000);
|
|
38
|
+
const seconds = Math.floor((ms % 60_000) / 1000);
|
|
39
|
+
return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format the downstream token counter as 1.2k / 12.4k / 1.0m. Anvil
|
|
43
|
+
* F1 emits totals in the tens-of-thousands range during a single
|
|
44
|
+
* brief, so anything more than three significant figures is noise.
|
|
45
|
+
*/
|
|
46
|
+
function formatTokens(total) {
|
|
47
|
+
if (total < 1_000)
|
|
48
|
+
return total.toString();
|
|
49
|
+
if (total < 1_000_000)
|
|
50
|
+
return `${(total / 1_000).toFixed(1)}k`;
|
|
51
|
+
return `${(total / 1_000_000).toFixed(1)}m`;
|
|
52
|
+
}
|
|
53
|
+
function clampPhase(phase) {
|
|
54
|
+
if (typeof phase !== 'number' || Number.isNaN(phase))
|
|
55
|
+
return 0;
|
|
56
|
+
return Math.max(0, Math.min(PULSE_DOTS.length - 1, Math.floor(phase)));
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=status-bar.js.map
|
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.5",
|
|
4
4
|
"description": "Pugi CLI — terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -38,19 +38,25 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
+
"ink": "^5.0.1",
|
|
42
|
+
"react": "^18.3.1",
|
|
43
|
+
"tinyglobby": "^0.2.16",
|
|
41
44
|
"zod": "^3.23.0",
|
|
42
|
-
"@pugi/
|
|
45
|
+
"@pugi/personas": "0.1.0",
|
|
46
|
+
"@pugi/sdk": "0.1.0-alpha.5"
|
|
43
47
|
},
|
|
44
48
|
"devDependencies": {
|
|
45
49
|
"@types/node": "^22.0.0",
|
|
50
|
+
"@types/react": "^18.3.3",
|
|
51
|
+
"ink-testing-library": "^4.0.0",
|
|
46
52
|
"tsx": "^4.19.0",
|
|
47
53
|
"typescript": "~5.6.0"
|
|
48
54
|
},
|
|
49
55
|
"scripts": {
|
|
50
|
-
"build": "pnpm --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
56
|
+
"build": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json && node scripts/make-bin-executable.mjs",
|
|
51
57
|
"dev": "tsx src/index.ts",
|
|
52
|
-
"typecheck": "pnpm --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
|
|
53
|
-
"test": "pnpm run build && node --test --import tsx test
|
|
58
|
+
"typecheck": "pnpm --filter @pugi/personas --filter @pugi/sdk build && tsc -p tsconfig.json --noEmit",
|
|
59
|
+
"test": "pnpm run build && node --test --import tsx 'test/**/*.spec.ts' 'test/**/*.spec.tsx'",
|
|
54
60
|
"version:cli": "tsx src/index.ts version",
|
|
55
61
|
"doctor": "tsx src/index.ts doctor --json",
|
|
56
62
|
"pack:smoke": "node scripts/pack-smoke.mjs"
|