@pugi/cli 0.1.0-beta.47 → 0.1.0-beta.49
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/bash-classifier.js +160 -0
- package/dist/runtime/version.js +1 -1
- package/dist/tui/repl.js +11 -8
- package/dist/tui/update-banner.js +8 -1
- package/package.json +2 -2
|
@@ -346,6 +346,93 @@ const BUILD_TEST_PREFIXES = [
|
|
|
346
346
|
'tsc -p',
|
|
347
347
|
'eslint',
|
|
348
348
|
'prettier --check',
|
|
349
|
+
// P0 fix 2026-05-29 (#37 CRITICAL): customer-blocking gap surfaced
|
|
350
|
+
// during dogfood. Engine emitted `chmod +x build.sh`, `node script.js`,
|
|
351
|
+
// `python3 -m pytest`, `git status`, `pnpm build`, `docker ps`, etc.
|
|
352
|
+
// and the classifier returned `unknown` → permission matrix denied
|
|
353
|
+
// в bypassPermissions mode (which the customer expected to auto-allow
|
|
354
|
+
// basic dev tools). Customers could not run ANY real build/test/git
|
|
355
|
+
// workflow through Pugi.
|
|
356
|
+
//
|
|
357
|
+
// The prefixes below cover three classes of developer tooling that
|
|
358
|
+
// are always allowed in `auto`/`dontAsk`/`bypassPermissions` modes,
|
|
359
|
+
// `ask` in interactive modes, and `deny` in `plan` (read-only) mode:
|
|
360
|
+
// - Language runtimes: `node`, `python`, `python3`, `ruby`, etc.
|
|
361
|
+
// - Native build chains: `gcc`, `clang`, `cmake`, `rustc`, etc.
|
|
362
|
+
// - Container/k8s read-class: `docker ps/inspect/logs`, `kubectl get`.
|
|
363
|
+
//
|
|
364
|
+
// Destructive variants are already gated upstream by DESTRUCTIVE_PATTERNS
|
|
365
|
+
// (e.g. `docker system prune`, `kubectl delete --all`). The first-token
|
|
366
|
+
// gate in classifyComponent runs THIS list before the unknown fallback.
|
|
367
|
+
//
|
|
368
|
+
// Language runtime invocations (first-token match, with or without args).
|
|
369
|
+
'node',
|
|
370
|
+
'python',
|
|
371
|
+
'python3',
|
|
372
|
+
'ruby',
|
|
373
|
+
'perl',
|
|
374
|
+
'php',
|
|
375
|
+
'deno',
|
|
376
|
+
'bun',
|
|
377
|
+
'tsx',
|
|
378
|
+
'ts-node',
|
|
379
|
+
// Native build chains.
|
|
380
|
+
'gcc',
|
|
381
|
+
'g++',
|
|
382
|
+
'clang',
|
|
383
|
+
'clang++',
|
|
384
|
+
'cmake',
|
|
385
|
+
'rustc',
|
|
386
|
+
'javac',
|
|
387
|
+
'java',
|
|
388
|
+
// Container/k8s read-class (the destructive subcommands are pre-empted
|
|
389
|
+
// by DESTRUCTIVE_PATTERNS: `docker system prune`, `kubectl delete --all`,
|
|
390
|
+
// `kubectl delete namespace`).
|
|
391
|
+
'docker ps',
|
|
392
|
+
'docker images',
|
|
393
|
+
'docker inspect',
|
|
394
|
+
'docker logs',
|
|
395
|
+
'docker version',
|
|
396
|
+
'docker info',
|
|
397
|
+
'docker exec',
|
|
398
|
+
'docker run',
|
|
399
|
+
'docker stop',
|
|
400
|
+
'docker start',
|
|
401
|
+
'docker restart',
|
|
402
|
+
'docker rm',
|
|
403
|
+
'docker rmi',
|
|
404
|
+
'docker build',
|
|
405
|
+
'docker tag',
|
|
406
|
+
'docker compose',
|
|
407
|
+
'docker-compose',
|
|
408
|
+
'kubectl get',
|
|
409
|
+
'kubectl describe',
|
|
410
|
+
'kubectl logs',
|
|
411
|
+
'kubectl exec',
|
|
412
|
+
'kubectl apply',
|
|
413
|
+
'kubectl create',
|
|
414
|
+
'kubectl rollout',
|
|
415
|
+
'kubectl port-forward',
|
|
416
|
+
'kubectl config',
|
|
417
|
+
// Git read+write surface (network ops already handled by NETWORK_PREFIXES;
|
|
418
|
+
// destructive ops `reset --hard`/`clean -fdx`/`push --force` blocked above).
|
|
419
|
+
// Note: WRITE_WORKSPACE_PREFIXES already covers `git commit/add/checkout/...`.
|
|
420
|
+
// These entries handle plain `git rev-list`, `git cherry-pick`, `git worktree`,
|
|
421
|
+
// `git submodule`, etc that customer scripts commonly invoke.
|
|
422
|
+
'git rev-list',
|
|
423
|
+
'git cherry-pick',
|
|
424
|
+
'git worktree',
|
|
425
|
+
'git submodule',
|
|
426
|
+
'git blame',
|
|
427
|
+
'git describe',
|
|
428
|
+
'git tag --list',
|
|
429
|
+
'git tag -l',
|
|
430
|
+
'git for-each-ref',
|
|
431
|
+
'git ls-remote',
|
|
432
|
+
// gh CLI (GitHub). `gh repo delete` / `gh release delete` reach into
|
|
433
|
+
// network operations but are non-destructive for the local workspace.
|
|
434
|
+
// Permission matrix asks before allowing in auto.
|
|
435
|
+
'gh',
|
|
349
436
|
];
|
|
350
437
|
/** Single-token read-only commands. Argument-free entries match exact. */
|
|
351
438
|
const READ_TOKENS = new Set([
|
|
@@ -384,6 +471,16 @@ const READ_TOKENS = new Set([
|
|
|
384
471
|
'cut',
|
|
385
472
|
'sort',
|
|
386
473
|
'uniq',
|
|
474
|
+
// P0 fix 2026-05-29 (#37 CRITICAL): structured-data inspection tools
|
|
475
|
+
// are pure stdin/stdout transformers (no FS write, no network) when
|
|
476
|
+
// не paired с `>` redirection (the redirection branch above promotes
|
|
477
|
+
// к write_workspace independently). Common в dev scripts for parsing
|
|
478
|
+
// package.json, tsconfig.json, Helm values.yaml, etc.
|
|
479
|
+
// `tee` is INTENTIONALLY excluded — it writes by definition, even
|
|
480
|
+
// в protected paths (`tee /etc/...` is already in DESTRUCTIVE_PATTERNS).
|
|
481
|
+
'jq',
|
|
482
|
+
'yq',
|
|
483
|
+
'column',
|
|
387
484
|
]);
|
|
388
485
|
const READ_PREFIXES = [
|
|
389
486
|
'git status',
|
|
@@ -418,6 +515,16 @@ const WRITE_WORKSPACE_PREFIXES = [
|
|
|
418
515
|
'git tag',
|
|
419
516
|
'git rebase',
|
|
420
517
|
'git merge',
|
|
518
|
+
// P0 fix 2026-05-29 (#37 CRITICAL): file-permission ops are common
|
|
519
|
+
// в build scripts (`chmod +x build.sh`, `chown $USER file`). The
|
|
520
|
+
// destructive variants (`chmod 777 /`, `chmod -R 777 /`, `chmod -R
|
|
521
|
+
// 777 ~`, `chown -R root /`, `chown -R / ...`) are pre-empted by
|
|
522
|
+
// DESTRUCTIVE_PATTERNS which runs BEFORE this list — safe to add
|
|
523
|
+
// here for the non-destructive path. detectProtectedWrite's `\bchmod\b`
|
|
524
|
+
// / `\bchown\b` regex also catches writes into protected paths
|
|
525
|
+
// regardless of this list.
|
|
526
|
+
'chmod ',
|
|
527
|
+
'chown ',
|
|
421
528
|
];
|
|
422
529
|
/**
|
|
423
530
|
* Protected-write triggers. If a command writes to any of these paths
|
|
@@ -636,6 +743,25 @@ function classifyComponent(cmd, ctx) {
|
|
|
636
743
|
if (trimmed === 'make' || trimmed.startsWith('make ')) {
|
|
637
744
|
return { class: 'build_test', reason: 'make runner', matched: 'make' };
|
|
638
745
|
}
|
|
746
|
+
// 7c. Operator-override safe tokens (P0 fix 2026-05-29 #37).
|
|
747
|
+
// `PUGI_CLASSIFIER_EXTRA_SAFE=tool1,tool2,...` extends the BUILD_TEST
|
|
748
|
+
// first-token list at runtime. This is a security-sensitive escape
|
|
749
|
+
// hatch — operators can add their custom build tools without a
|
|
750
|
+
// recompile. Destructive patterns ALREADY ran above (step 1) so this
|
|
751
|
+
// cannot whitelist `rm`, `mkfs`, `git push --force`, etc. The match
|
|
752
|
+
// is strict first-token equality — not substring — and the env var
|
|
753
|
+
// is read fresh on every classify call so tests can mutate it.
|
|
754
|
+
const extraSafe = readExtraSafeTokens();
|
|
755
|
+
if (extraSafe.size > 0) {
|
|
756
|
+
const firstTokenForExtraSafe = trimmed.split(/\s+/)[0] ?? '';
|
|
757
|
+
if (extraSafe.has(firstTokenForExtraSafe)) {
|
|
758
|
+
return {
|
|
759
|
+
class: 'build_test',
|
|
760
|
+
reason: `PUGI_CLASSIFIER_EXTRA_SAFE override: ${firstTokenForExtraSafe}`,
|
|
761
|
+
matched: firstTokenForExtraSafe,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
639
765
|
// 7c. Bare `cd <path>` (inside workspace — the cwd-escape detector
|
|
640
766
|
// upgrades the class to write_protected when the target is
|
|
641
767
|
// outside). Standalone `cd` (HOME) is escape, also handled by the
|
|
@@ -756,6 +882,40 @@ function nestingDepth(cmd, open, close) {
|
|
|
756
882
|
function escapeRegex(s) {
|
|
757
883
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
758
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Operator-override safe tokens. Read from `PUGI_CLASSIFIER_EXTRA_SAFE`
|
|
887
|
+
* (comma-separated). Allows operators to extend the BUILD_TEST first-
|
|
888
|
+
* token list at runtime for site-specific tooling без recompile.
|
|
889
|
+
*
|
|
890
|
+
* Security note: destructive substring patterns run BEFORE this gate
|
|
891
|
+
* (step 1 in classifyComponent), so this cannot whitelist `rm`, `mkfs`,
|
|
892
|
+
* `git push --force`, etc. The env var only adds tools to the benign
|
|
893
|
+
* build_test class. Invalid entries (empty strings, тokens containing
|
|
894
|
+
* shell metas) are silently dropped to avoid surprising classifications.
|
|
895
|
+
*
|
|
896
|
+
* Read fresh on every call so per-test mutations work и so operators
|
|
897
|
+
* can update without restarting the agent loop. The cost (one env var
|
|
898
|
+
* read + Set construction per call) is negligible for the classifier's
|
|
899
|
+
* call frequency.
|
|
900
|
+
*/
|
|
901
|
+
function readExtraSafeTokens() {
|
|
902
|
+
const raw = process.env.PUGI_CLASSIFIER_EXTRA_SAFE;
|
|
903
|
+
if (!raw || raw.trim() === '')
|
|
904
|
+
return new Set();
|
|
905
|
+
const tokens = new Set();
|
|
906
|
+
for (const candidate of raw.split(',')) {
|
|
907
|
+
const trimmed = candidate.trim();
|
|
908
|
+
if (trimmed === '')
|
|
909
|
+
continue;
|
|
910
|
+
// Reject anything containing shell metas or whitespace — only bare
|
|
911
|
+
// tool names allowed. Defends against accidental
|
|
912
|
+
// `PUGI_CLASSIFIER_EXTRA_SAFE='rm -rf /'` smuggling.
|
|
913
|
+
if (/[\s;|&<>$`(){}\[\]'"\\]/.test(trimmed))
|
|
914
|
+
continue;
|
|
915
|
+
tokens.add(trimmed);
|
|
916
|
+
}
|
|
917
|
+
return tokens;
|
|
918
|
+
}
|
|
759
919
|
function detectProtectedWrite(cmd, ctx) {
|
|
760
920
|
// Surface every write target this command produces so we can both
|
|
761
921
|
// protected-path-check and outside-workspace-check them uniformly.
|
package/dist/runtime/version.js
CHANGED
|
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
|
|
|
44
44
|
* during import). When bumping the CLI version BOTH literals must be
|
|
45
45
|
* updated; the release smoke-test (`pack:smoke`) verifies they agree.
|
|
46
46
|
*/
|
|
47
|
-
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.
|
|
47
|
+
export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.49');
|
|
48
48
|
/**
|
|
49
49
|
* Outbound: the CLI's installed semver. Read at request time by
|
|
50
50
|
* `version-interceptor.ts` and injected on every `fetch` call.
|
package/dist/tui/repl.js
CHANGED
|
@@ -108,17 +108,20 @@ export function Repl(props) {
|
|
|
108
108
|
setSplashVisible(false);
|
|
109
109
|
}
|
|
110
110
|
}, [splashVisible, state.agents.length, state.transcript.length]);
|
|
111
|
-
// CEO P0 #2 (2026-05-29):
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
111
|
+
// CEO P0 #2 (2026-05-29) v2: welcome banner stays until the operator
|
|
112
|
+
// actively engages the loop — first agent spawn. Boot-time auto-init
|
|
113
|
+
// emits system rows into `state.transcript` (skip-trust hints, dirty
|
|
114
|
+
// tree warnings) which used к dismiss the banner within ~2s, hiding
|
|
115
|
+
// the brand mascot before the operator could read it. Drop the
|
|
116
|
+
// `transcript.length` trigger; agent spawn (= real dispatch) remains
|
|
117
|
+
// the sole signal that the operator stopped reading the banner.
|
|
115
118
|
useEffect(() => {
|
|
116
119
|
if (!welcomeVisible)
|
|
117
120
|
return;
|
|
118
|
-
if (state.agents.length > 0
|
|
121
|
+
if (state.agents.length > 0) {
|
|
119
122
|
setWelcomeVisible(false);
|
|
120
123
|
}
|
|
121
|
-
}, [welcomeVisible, state.agents.length
|
|
124
|
+
}, [welcomeVisible, state.agents.length]);
|
|
122
125
|
const personaNames = useMemo(() => buildPersonaNameMap(), []);
|
|
123
126
|
const { exit } = useApp();
|
|
124
127
|
const handleSubmit = useCallback((line) => {
|
|
@@ -255,7 +258,7 @@ export function Repl(props) {
|
|
|
255
258
|
// input, and the input stays the sole focusable surface adjacent
|
|
256
259
|
// to the cursor row, so all keystrokes route through it.
|
|
257
260
|
const altScreenRows = process.stdout.rows ?? 24;
|
|
258
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [
|
|
261
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [welcomeVisible && props.welcomeData ? (_jsx(WelcomeBanner, { data: props.welcomeData, mascotPrePrinted: props.mascotPrePrinted === true, autoInitStatus: props.autoInitStatus ?? null })) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, onCyclePermissionMode: handleCyclePermissionMode, now: props.now,
|
|
259
262
|
// Slug from process.cwd() (full path) so two workspaces with
|
|
260
263
|
// the same basename do not share history. state.workspaceLabel
|
|
261
264
|
// is the basename only. Codex review P2.
|
|
@@ -263,7 +266,7 @@ export function Repl(props) {
|
|
|
263
266
|
// α7 cost-meter sprint — surface accumulated session totals
|
|
264
267
|
// + per-turn delta flash on the status bar's top row. The
|
|
265
268
|
// session module owns accumulation; the bar is a pure render.
|
|
266
|
-
sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta })] })] }));
|
|
269
|
+
sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta }), props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null] })] }));
|
|
267
270
|
}
|
|
268
271
|
function Header({ state }) {
|
|
269
272
|
// Leak L30 (2026-05-27): the header `.io` brand accent + connection
|
|
@@ -13,6 +13,13 @@ export function resolveDisplayedLatest(npmLatest, serverRecommended) {
|
|
|
13
13
|
? serverRecommended
|
|
14
14
|
: npmLatest;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Claude-Code-style corner banner: single line, dim orange, right
|
|
18
|
+
* aligned. Renders to the bottom of the REPL frame so it never
|
|
19
|
+
* displaces conversation content. Only mounts when an update is
|
|
20
|
+
* actually available; identical to no-op when the registry poll
|
|
21
|
+
* reports current. Suppress with `PUGI_SKIP_UPDATE_BANNER=1`.
|
|
22
|
+
*/
|
|
16
23
|
export function UpdateBanner({ result }) {
|
|
17
24
|
const command = upgradeCommand(result.method);
|
|
18
25
|
// Read the cache lazily inside the render so a server response that
|
|
@@ -21,6 +28,6 @@ export function UpdateBanner({ result }) {
|
|
|
21
28
|
// do per render.
|
|
22
29
|
const serverRecommended = getCachedServerRecommendation();
|
|
23
30
|
const displayedLatest = resolveDisplayedLatest(result.latest, serverRecommended);
|
|
24
|
-
return (
|
|
31
|
+
return (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { color: "#d97706", children: [_jsx(Text, { dimColor: true, children: 'Update available! ' }), _jsx(Text, { dimColor: true, children: `v${displayedLatest} — Run: ` }), _jsx(Text, { bold: true, children: command })] }) }));
|
|
25
32
|
}
|
|
26
33
|
//# 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.49",
|
|
4
4
|
"description": "Pugi CLI - terminal-native software execution system",
|
|
5
5
|
"homepage": "https://pugi.io",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"undici": "^8.3.0",
|
|
56
56
|
"zod": "^3.23.0",
|
|
57
57
|
"@pugi/personas": "0.1.2",
|
|
58
|
-
"@pugi/sdk": "0.1.0-beta.
|
|
58
|
+
"@pugi/sdk": "0.1.0-beta.49"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/node": "^22.0.0",
|