pum-agent 0.2.5-beta.1 → 0.2.7-beta.1
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 +3 -3
- package/package.json +1 -1
- package/src/app.tsx +5 -4
- package/src/check-policy.ts +237 -0
- package/src/status-bar.tsx +6 -0
- package/src/status-metadata.ts +19 -4
- package/src/theme.ts +11 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ The following screens are real OpenTUI renders captured through `tmux`. A local
|
|
|
40
40
|
|
|
41
41
|
## Why PUM
|
|
42
42
|
|
|
43
|
-
- **Compact terminal UI:** Streaming Markdown, syntax highlighting, thinking traces, tool rows, usage, cost, and Git status.
|
|
43
|
+
- **Compact terminal UI:** Streaming Markdown, syntax highlighting, thinking traces, tool rows, usage, cost, launch directory, and Git status.
|
|
44
44
|
- **Full coding loop:** Built-in `read`, `write`, `edit`, `bash`, and atomic `apply_patch` tools.
|
|
45
45
|
- **Parallel subagents:** Persistent agents work in isolated Git worktrees, communicate durably, and report lifecycle transitions to their direct spawners.
|
|
46
46
|
- **Prompt control:** Steer active work, answer model questionnaires, use an ownership-aware message cache, attach clipboard images, and resume sessions with metadata-rich history.
|
|
@@ -223,10 +223,10 @@ Trigger events target one exact main or retained child session. A missing sessio
|
|
|
223
223
|
Select a Check mode profile in `Ctrl+P`. It applies to `bash`, `edit`, `apply_patch`, and external-trigger process execution:
|
|
224
224
|
|
|
225
225
|
- **Strict:** Run deterministic hard rules, then require a clear verifier approval.
|
|
226
|
-
- **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls
|
|
226
|
+
- **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls, explicit non-sensitive external reads, and narrow lifecycle-disabled npm release verification operations whose writes stay in approved roots. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
|
|
227
227
|
- **Ask:** Show the approval popup for every checked call that passes hard rules, unless an exact session or project approval already matches. A verifier `SAFE`, unclear, error, or unavailable result still requires approval.
|
|
228
228
|
|
|
229
|
-
Every active profile hard-blocks external writes, location changes, execution operands, ambiguous path access, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. Balanced permits
|
|
229
|
+
Every active profile hard-blocks external writes, location changes, execution operands, ambiguous path access, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. Balanced permits explicit, deterministically classified, non-sensitive external reads. It accepts one direct `npm pack` only when lifecycle scripts are disabled, an explicit cache stays in an approved root, output stays in an approved root, and any package operand is one exact registry version. Balanced also accepts one direct `npm install` of one exact registry version only when `--ignore-scripts`, an approved `--prefix`, and an approved `--cache` are explicit. File, Git, URL, tag, range, composed, general install, and global-install forms remain blocked. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only publication exception is a deterministic match for direct main-agent `npm publish` or `npm dist-tag add`. The verifier category does not control this exception. The exception still requires explicit popup approval. Managed subagents cannot use it.
|
|
230
230
|
|
|
231
231
|
Use `/check-path list`, `/check-path add <directory>`, `/check-path remove <directory>`, or `/check-path clear` to manage up to 16 additional directory roots for the current launch project. Bash, edit, and external-trigger checks can use these roots; `apply_patch` remains project-local. Added roots are canonicalized and remain subject to credential, traversal, symlink or junction, broad-deletion, and other hard blocks.
|
|
232
232
|
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodePasteBytes, type ScrollBoxRenderable, type TextareaRenderable } from "@opentui/core";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { useKeyboard, usePaste, useTerminalDimensions } from "@opentui/react";
|
|
4
|
-
import type
|
|
4
|
+
import { getSupportedThinkingLevels, type Model } from "@earendil-works/pi-ai";
|
|
5
5
|
import type { AgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { AnimationProvider, supportsTrueColor, useWorkingRule, type WorkingRuleRole } from "./animation";
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
moveSettingSelection,
|
|
14
14
|
SettingsPopup,
|
|
15
15
|
SETTINGS_ROWS,
|
|
16
|
-
THINKING_LEVELS,
|
|
17
16
|
type SettingRowId,
|
|
18
17
|
type ThinkingLevel,
|
|
19
18
|
} from "./settings-popup";
|
|
@@ -1095,8 +1094,9 @@ export function App({
|
|
|
1095
1094
|
};
|
|
1096
1095
|
|
|
1097
1096
|
const stepThinking = (step: number) => {
|
|
1098
|
-
const
|
|
1099
|
-
const
|
|
1097
|
+
const levels = getSupportedThinkingLevels(session.agent.state.model);
|
|
1098
|
+
const i = levels.indexOf(thinkingLevel);
|
|
1099
|
+
const target = levels[Math.max(0, Math.min(levels.length - 1, i + step))]!;
|
|
1100
1100
|
session.setThinkingLevel(target);
|
|
1101
1101
|
// setThinkingLevel clamps to what the model supports — show the real value.
|
|
1102
1102
|
setThinkingLevel(session.agent.state.thinkingLevel as ThinkingLevel);
|
|
@@ -2309,6 +2309,7 @@ export function App({
|
|
|
2309
2309
|
theme={theme}
|
|
2310
2310
|
modelId={visibleModelId}
|
|
2311
2311
|
thinkingLevel={visibleThinkingLevel}
|
|
2312
|
+
cwd={cwd}
|
|
2312
2313
|
branch={visibleBranch}
|
|
2313
2314
|
outgoingTokens={visibleUsage.outgoing}
|
|
2314
2315
|
incomingTokens={visibleUsage.incoming}
|
package/src/check-policy.ts
CHANGED
|
@@ -17,6 +17,8 @@ export type CheckPolicyFindingCode =
|
|
|
17
17
|
| "external-read-exfiltration"
|
|
18
18
|
| "destructive-git"
|
|
19
19
|
| "broad-deletion"
|
|
20
|
+
| "unsafe-npm-install"
|
|
21
|
+
| "unsafe-npm-pack"
|
|
20
22
|
| "suspicious-execution"
|
|
21
23
|
| "shell-complexity"
|
|
22
24
|
| "mutation"
|
|
@@ -209,12 +211,172 @@ const EXPLICIT_READ_COMMANDS = new Set([
|
|
|
209
211
|
"cat", "head", "tail", "less", "more", "stat", "file", "ls", "tree", "du", "wc", "realpath", "readlink",
|
|
210
212
|
]);
|
|
211
213
|
const DATA_OPERAND_COMMANDS = new Set(["printf", "echo"]);
|
|
214
|
+
|
|
215
|
+
type NpmPackCommand = {
|
|
216
|
+
valid: boolean;
|
|
217
|
+
reason?: string;
|
|
218
|
+
packageSpec?: string;
|
|
219
|
+
cache?: string;
|
|
220
|
+
packDestination: string;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
type NpmInstallCommand = {
|
|
224
|
+
valid: boolean;
|
|
225
|
+
reason?: string;
|
|
226
|
+
packageSpec?: string;
|
|
227
|
+
prefix?: string;
|
|
228
|
+
cache?: string;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
function isExactRegistryPackageVersion(value: string): boolean {
|
|
232
|
+
const packageName = String.raw`(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)`;
|
|
233
|
+
const numericIdentifier = String.raw`(?:0|[1-9]\d*)`;
|
|
234
|
+
const prereleaseIdentifier = String.raw`(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)`;
|
|
235
|
+
const buildIdentifier = String.raw`[0-9a-zA-Z-]+`;
|
|
236
|
+
const version = String.raw`${numericIdentifier}\.${numericIdentifier}\.${numericIdentifier}(?:-${prereleaseIdentifier}(?:\.${prereleaseIdentifier})*)?(?:\+${buildIdentifier}(?:\.${buildIdentifier})*)?`;
|
|
237
|
+
return new RegExp(`^${packageName}@${version}$`).test(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function npmInstallCommand(argv: string[]): NpmInstallCommand | undefined {
|
|
241
|
+
if (commandName(argv[0]) !== "npm" || argv[1] !== "install") return undefined;
|
|
242
|
+
const positionals: string[] = [];
|
|
243
|
+
let prefix: string | undefined;
|
|
244
|
+
let cache: string | undefined;
|
|
245
|
+
let ignoreScriptsCount = 0;
|
|
246
|
+
const pathOptions = new Map([["--prefix", "prefix"], ["--cache", "cache"]] as const);
|
|
247
|
+
|
|
248
|
+
for (let index = 2; index < argv.length; index++) {
|
|
249
|
+
const value = argv[index]!;
|
|
250
|
+
if (value === "--ignore-scripts") {
|
|
251
|
+
ignoreScriptsCount++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const separate = pathOptions.get(value as "--prefix" | "--cache");
|
|
255
|
+
if (separate) {
|
|
256
|
+
const path = argv[++index];
|
|
257
|
+
if (!path || path.startsWith("-")) return { valid: false, reason: `${value} requires one explicit path` };
|
|
258
|
+
if (separate === "prefix") {
|
|
259
|
+
if (prefix !== undefined) return { valid: false, reason: "npm install --prefix must occur exactly once" };
|
|
260
|
+
prefix = path;
|
|
261
|
+
} else {
|
|
262
|
+
if (cache !== undefined) return { valid: false, reason: "npm install --cache must occur exactly once" };
|
|
263
|
+
cache = path;
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const attached = /^(--prefix|--cache)=(.*)$/.exec(value);
|
|
268
|
+
if (attached) {
|
|
269
|
+
if (!attached[2]) return { valid: false, reason: `${attached[1]} requires one explicit path` };
|
|
270
|
+
if (attached[1] === "--prefix") {
|
|
271
|
+
if (prefix !== undefined) return { valid: false, reason: "npm install --prefix must occur exactly once" };
|
|
272
|
+
prefix = attached[2];
|
|
273
|
+
} else {
|
|
274
|
+
if (cache !== undefined) return { valid: false, reason: "npm install --cache must occur exactly once" };
|
|
275
|
+
cache = attached[2];
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (value.startsWith("-")) {
|
|
280
|
+
return { valid: false, reason: `npm install option ${value} is not in the deterministic allowlist` };
|
|
281
|
+
}
|
|
282
|
+
positionals.push(value);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (ignoreScriptsCount !== 1) {
|
|
286
|
+
return { valid: false, reason: "npm install must disable lifecycle scripts exactly once with --ignore-scripts" };
|
|
287
|
+
}
|
|
288
|
+
if (!prefix) return { valid: false, reason: "npm install must set an explicit approved --prefix path" };
|
|
289
|
+
if (!cache) return { valid: false, reason: "npm install must set an explicit approved --cache path" };
|
|
290
|
+
if ([prefix, cache].some((path) => /[*?\[\]{}]/.test(path))) {
|
|
291
|
+
return { valid: false, reason: "npm install write paths must not contain shell expansion patterns" };
|
|
292
|
+
}
|
|
293
|
+
if (positionals.length !== 1 || !isExactRegistryPackageVersion(positionals[0]!)) {
|
|
294
|
+
return { valid: false, reason: "npm install must use one exact registry package version" };
|
|
295
|
+
}
|
|
296
|
+
return { valid: true, packageSpec: positionals[0], prefix, cache };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function npmPackCommand(argv: string[]): NpmPackCommand | undefined {
|
|
300
|
+
if (commandName(argv[0]) !== "npm" || argv[1] !== "pack") return undefined;
|
|
301
|
+
const positionals: string[] = [];
|
|
302
|
+
let cache: string | undefined;
|
|
303
|
+
let packDestination = ".";
|
|
304
|
+
let packDestinationSet = false;
|
|
305
|
+
let ignoreScripts = false;
|
|
306
|
+
const booleanOptions = new Set(["--dry-run", "--json", "--ignore-scripts"]);
|
|
307
|
+
const pathOptions = new Map([["--cache", "cache"], ["--pack-destination", "packDestination"]] as const);
|
|
308
|
+
|
|
309
|
+
for (let index = 2; index < argv.length; index++) {
|
|
310
|
+
const value = argv[index]!;
|
|
311
|
+
if (booleanOptions.has(value)) {
|
|
312
|
+
if (value === "--ignore-scripts") ignoreScripts = true;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const separate = pathOptions.get(value as "--cache" | "--pack-destination");
|
|
316
|
+
if (separate) {
|
|
317
|
+
const path = argv[++index];
|
|
318
|
+
if (!path || path.startsWith("-")) return { valid: false, reason: `${value} requires one explicit path`, packDestination };
|
|
319
|
+
if (separate === "cache") {
|
|
320
|
+
if (cache !== undefined) return { valid: false, reason: "npm pack --cache must occur exactly once", packDestination };
|
|
321
|
+
cache = path;
|
|
322
|
+
} else {
|
|
323
|
+
if (packDestinationSet) return { valid: false, reason: "npm pack --pack-destination must occur at most once", packDestination };
|
|
324
|
+
packDestination = path;
|
|
325
|
+
packDestinationSet = true;
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const attached = /^(--cache|--pack-destination)=(.*)$/.exec(value);
|
|
330
|
+
if (attached) {
|
|
331
|
+
if (!attached[2]) return { valid: false, reason: `${attached[1]} requires one explicit path`, packDestination };
|
|
332
|
+
if (attached[1] === "--cache") {
|
|
333
|
+
if (cache !== undefined) return { valid: false, reason: "npm pack --cache must occur exactly once", packDestination };
|
|
334
|
+
cache = attached[2];
|
|
335
|
+
} else {
|
|
336
|
+
if (packDestinationSet) return { valid: false, reason: "npm pack --pack-destination must occur at most once", packDestination };
|
|
337
|
+
packDestination = attached[2];
|
|
338
|
+
packDestinationSet = true;
|
|
339
|
+
}
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (value === "--") {
|
|
343
|
+
positionals.push(...argv.slice(index + 1));
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
if (value.startsWith("-")) return { valid: false, reason: `npm pack option ${value} is not in the deterministic allowlist`, packDestination };
|
|
347
|
+
positionals.push(value);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!ignoreScripts) return { valid: false, reason: "npm pack must disable lifecycle scripts with --ignore-scripts", packDestination };
|
|
351
|
+
if (!cache) return { valid: false, reason: "npm pack must set an explicit project-local --cache path", packDestination };
|
|
352
|
+
if (positionals.length > 1) return { valid: false, reason: "npm pack accepts at most one deterministic package spec", cache, packDestination };
|
|
353
|
+
if (positionals[0] && !isExactRegistryPackageVersion(positionals[0])) {
|
|
354
|
+
return { valid: false, reason: "npm pack package spec must be an exact registry package version", cache, packDestination };
|
|
355
|
+
}
|
|
356
|
+
return { valid: true, packageSpec: positionals[0], cache, packDestination };
|
|
357
|
+
}
|
|
358
|
+
|
|
212
359
|
function commandName(argv0: string | undefined): string {
|
|
213
360
|
if (!argv0) return "";
|
|
214
361
|
const basename = argv0.replaceAll("\\", "/").split("/").at(-1) ?? argv0;
|
|
215
362
|
return basename.replace(/\.(?:exe|cmd|bat)$/i, "").toLowerCase();
|
|
216
363
|
}
|
|
217
364
|
|
|
365
|
+
function npmSubcommand(argv: string[]): string | undefined {
|
|
366
|
+
if (commandName(argv[0]) !== "npm") return undefined;
|
|
367
|
+
const valueOptions = new Set(["--cache", "--prefix", "--registry", "--userconfig"]);
|
|
368
|
+
for (let index = 1; index < argv.length; index++) {
|
|
369
|
+
const value = argv[index]!;
|
|
370
|
+
if (valueOptions.has(value)) {
|
|
371
|
+
index++;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (value.startsWith("-")) continue;
|
|
375
|
+
return value.toLowerCase();
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
218
380
|
function effectiveArgv(argv: string[]): string[] {
|
|
219
381
|
let current = argv;
|
|
220
382
|
for (let depth = 0; depth < 4; depth++) {
|
|
@@ -352,6 +514,8 @@ function mutationIndicators(argv: string[], redirections: BashRedirection[]): st
|
|
|
352
514
|
if (name === "git" && argv[1] && !new Set(["status", "diff", "log", "show", "rev-parse", "ls-files", "branch"]).has(argv[1])) {
|
|
353
515
|
indicators.push("Git state change");
|
|
354
516
|
}
|
|
517
|
+
if (npmInstallCommand(argv)?.valid) indicators.push("package installation or cache output");
|
|
518
|
+
if (npmPackCommand(argv)?.valid) indicators.push("package archive or cache output");
|
|
355
519
|
return [...new Set(indicators)];
|
|
356
520
|
}
|
|
357
521
|
|
|
@@ -742,6 +906,18 @@ function classifyStageAccesses(stage: BashStage): ClassifiedAccess[] {
|
|
|
742
906
|
}
|
|
743
907
|
|
|
744
908
|
if (DATA_OPERAND_COMMANDS.has(name)) return accesses;
|
|
909
|
+
const npmInstall = npmInstallCommand(argv);
|
|
910
|
+
if (npmInstall?.valid) {
|
|
911
|
+
accesses.push({ path: npmInstall.prefix!, mode: "write", source: "operand" });
|
|
912
|
+
accesses.push({ path: npmInstall.cache!, mode: "write", source: "operand" });
|
|
913
|
+
return accesses;
|
|
914
|
+
}
|
|
915
|
+
const npmPack = npmPackCommand(argv);
|
|
916
|
+
if (npmPack?.valid) {
|
|
917
|
+
accesses.push({ path: npmPack.cache!, mode: "write", source: "operand" });
|
|
918
|
+
accesses.push({ path: npmPack.packDestination, mode: "write", source: "operand" });
|
|
919
|
+
return accesses;
|
|
920
|
+
}
|
|
745
921
|
if (["cd", "chdir", "set-location"].includes(name)) {
|
|
746
922
|
return [...accesses, ...positionalOperands(argv).map((path) => ({ path, mode: "location" as const, source: "operand" as const }))];
|
|
747
923
|
}
|
|
@@ -930,6 +1106,61 @@ function inspectHardBlocks(
|
|
|
930
1106
|
const argv = effectiveArgv(stage.argv);
|
|
931
1107
|
const name = commandName(argv[0]);
|
|
932
1108
|
const lowerArgs = argv.map((arg) => arg.toLowerCase());
|
|
1109
|
+
const npmInstall = npmInstallCommand(argv);
|
|
1110
|
+
if (npmInstall) {
|
|
1111
|
+
const direct = commandName(stage.argv[0]) === "npm"
|
|
1112
|
+
&& analysis.stages.length === 1
|
|
1113
|
+
&& analysis.operators.length === 0
|
|
1114
|
+
&& stage.substitutions.length === 0
|
|
1115
|
+
&& stage.redirections.length === 0
|
|
1116
|
+
&& Object.keys(stage.envAssignments).length === 0;
|
|
1117
|
+
if (!direct || !npmInstall.valid) {
|
|
1118
|
+
addFinding(findings, {
|
|
1119
|
+
code: "unsafe-npm-install",
|
|
1120
|
+
severity: "hard-block",
|
|
1121
|
+
message: !direct ? "npm install must be one direct command without shell composition" : npmInstall.reason!,
|
|
1122
|
+
stage: stage.index,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
} else if (name === "npm" && new Set([
|
|
1126
|
+
"install", "i", "add", "ci", "install-ci-test", "install-test", "rebuild",
|
|
1127
|
+
"update", "upgrade", "remove", "uninstall", "link",
|
|
1128
|
+
]).has(npmSubcommand(argv) ?? "")) {
|
|
1129
|
+
addFinding(findings, {
|
|
1130
|
+
code: "unsafe-npm-install",
|
|
1131
|
+
severity: "hard-block",
|
|
1132
|
+
message: "only the deterministic direct npm install verification form is supported",
|
|
1133
|
+
stage: stage.index,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const npmPack = npmPackCommand(argv);
|
|
1137
|
+
if (npmPack) {
|
|
1138
|
+
const direct = commandName(stage.argv[0]) === "npm"
|
|
1139
|
+
&& analysis.stages.length === 1
|
|
1140
|
+
&& analysis.operators.length === 0
|
|
1141
|
+
&& stage.substitutions.length === 0
|
|
1142
|
+
&& stage.redirections.length === 0
|
|
1143
|
+
&& Object.keys(stage.envAssignments).length === 0;
|
|
1144
|
+
if (!direct || !npmPack.valid) {
|
|
1145
|
+
addFinding(findings, {
|
|
1146
|
+
code: "unsafe-npm-pack",
|
|
1147
|
+
severity: "hard-block",
|
|
1148
|
+
message: !direct ? "npm pack must be one direct command without shell composition" : npmPack.reason!,
|
|
1149
|
+
stage: stage.index,
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
const globalPackageWrite = (name === "npm" || name === "bun")
|
|
1154
|
+
&& lowerArgs.some((arg) => new Set(["install", "add", "i", "update", "upgrade", "remove", "uninstall", "link"]).has(arg))
|
|
1155
|
+
&& lowerArgs.some((arg) => arg === "-g" || arg === "--global");
|
|
1156
|
+
if (globalPackageWrite) {
|
|
1157
|
+
addFinding(findings, {
|
|
1158
|
+
code: "outside-project",
|
|
1159
|
+
severity: "hard-block",
|
|
1160
|
+
message: "global package installation writes outside the project and approved roots",
|
|
1161
|
+
stage: stage.index,
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
933
1164
|
if (PRIVILEGE_COMMANDS.has(name)
|
|
934
1165
|
|| ((name === "powershell" || name === "start-process") && lowerArgs.includes("runas"))) {
|
|
935
1166
|
addFinding(findings, { code: "privilege-escalation", severity: "hard-block", message: `${name} can escalate privileges`, stage: stage.index });
|
|
@@ -1191,6 +1422,12 @@ function stageNetworkCommand(stage: BashStage): string | undefined {
|
|
|
1191
1422
|
if (NETWORK_COMMANDS.has(name)) return name;
|
|
1192
1423
|
const subcommand = (argv[1] ?? "").toLowerCase();
|
|
1193
1424
|
if (name === "git" && new Set(["clone", "fetch", "pull", "push", "ls-remote"]).has(subcommand)) return `git ${subcommand}`;
|
|
1425
|
+
if (name === "npm" && subcommand === "pack") {
|
|
1426
|
+
return npmPackCommand(argv)?.packageSpec ? "npm pack" : undefined;
|
|
1427
|
+
}
|
|
1428
|
+
if (name === "npm" && subcommand === "install") {
|
|
1429
|
+
return npmInstallCommand(argv)?.valid ? "npm install" : undefined;
|
|
1430
|
+
}
|
|
1194
1431
|
if (["npm", "pnpm", "yarn"].includes(name)
|
|
1195
1432
|
&& new Set(["add", "audit", "install", "publish", "search", "update", "upgrade", "view", "info"]).has(subcommand)) {
|
|
1196
1433
|
return `${name} ${subcommand}`;
|
package/src/status-bar.tsx
CHANGED
|
@@ -15,6 +15,7 @@ export type StatusProps = {
|
|
|
15
15
|
theme: Theme;
|
|
16
16
|
modelId: string;
|
|
17
17
|
thinkingLevel: string;
|
|
18
|
+
cwd: string;
|
|
18
19
|
branch: string | null;
|
|
19
20
|
outgoingTokens: number;
|
|
20
21
|
incomingTokens: number;
|
|
@@ -65,6 +66,7 @@ type StatusBarLayoutInput = Pick<
|
|
|
65
66
|
StatusProps,
|
|
66
67
|
| "modelId"
|
|
67
68
|
| "thinkingLevel"
|
|
69
|
+
| "cwd"
|
|
68
70
|
| "branch"
|
|
69
71
|
| "outgoingTokens"
|
|
70
72
|
| "incomingTokens"
|
|
@@ -183,6 +185,10 @@ export function statusBarLayout(input: StatusBarLayoutInput): StatusBarLayout {
|
|
|
183
185
|
layout.trailingSpace = false;
|
|
184
186
|
measureLayout(input, layout);
|
|
185
187
|
}
|
|
188
|
+
if (layout.totalWidth > input.width) {
|
|
189
|
+
layout.metadata = layout.metadata.filter((item) => item.key !== "cwd");
|
|
190
|
+
measureLayout(input, layout);
|
|
191
|
+
}
|
|
186
192
|
if (layout.totalWidth > input.width) {
|
|
187
193
|
layout.showIdleAgents = false;
|
|
188
194
|
measureLayout(input, layout);
|
package/src/status-metadata.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { fg, type TextChunk } from "@opentui/core";
|
|
|
2
2
|
import type { Theme } from "./theme";
|
|
3
3
|
|
|
4
4
|
export type StatusMetadataValues = {
|
|
5
|
+
cwd?: string;
|
|
5
6
|
branch: string | null;
|
|
6
7
|
outgoingTokens: number;
|
|
7
8
|
incomingTokens: number;
|
|
@@ -11,9 +12,9 @@ export type StatusMetadataValues = {
|
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
export type StatusMetadataItem = {
|
|
14
|
-
key: "branch" | "outgoing" | "incoming" | "cacheRead" | "cost" | "context";
|
|
15
|
+
key: "cwd" | "branch" | "outgoing" | "incoming" | "cacheRead" | "cost" | "context";
|
|
15
16
|
text: string;
|
|
16
|
-
tone: "branch" | "dim" | "warn";
|
|
17
|
+
tone: "cwd" | "branch" | "dim" | "warn";
|
|
17
18
|
priority: number;
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -28,8 +29,20 @@ export const formatCost = (value: number): string =>
|
|
|
28
29
|
|
|
29
30
|
export const statusTextWidth = (text: string): number => Bun.stringWidth(text);
|
|
30
31
|
|
|
32
|
+
/** Show the launch directory without the long parent path. */
|
|
33
|
+
export function formatWorkingDirectory(cwd: string): string {
|
|
34
|
+
const withoutTrailingSeparators = cwd.replace(/[\\/]+$/, "");
|
|
35
|
+
if (!withoutTrailingSeparators) return "/";
|
|
36
|
+
if (/^[A-Za-z]:$/.test(withoutTrailingSeparators)) return `${withoutTrailingSeparators}\\`;
|
|
37
|
+
const name = withoutTrailingSeparators.split(/[\\/]/).at(-1) || withoutTrailingSeparators;
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export function statusMetadataItems(values: StatusMetadataValues): StatusMetadataItem[] {
|
|
32
42
|
const items: StatusMetadataItem[] = [];
|
|
43
|
+
if (values.cwd) {
|
|
44
|
+
items.push({ key: "cwd", text: formatWorkingDirectory(values.cwd), tone: "cwd", priority: 85 });
|
|
45
|
+
}
|
|
33
46
|
if (values.branch) {
|
|
34
47
|
items.push({ key: "branch", text: values.branch, tone: "branch", priority: 90 });
|
|
35
48
|
}
|
|
@@ -101,8 +114,10 @@ export function statusMetadataChunks(
|
|
|
101
114
|
const chunks: TextChunk[] = [];
|
|
102
115
|
for (const item of items) {
|
|
103
116
|
if (chunks.length) chunks.push(fg(theme.dim)(" · "));
|
|
104
|
-
const color = item.tone === "
|
|
105
|
-
? theme.
|
|
117
|
+
const color = item.tone === "cwd"
|
|
118
|
+
? theme.statusCwd
|
|
119
|
+
: item.tone === "branch"
|
|
120
|
+
? theme.toolArg
|
|
106
121
|
: item.tone === "warn"
|
|
107
122
|
? theme.warn
|
|
108
123
|
: theme.dim;
|
package/src/theme.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type Theme = {
|
|
|
9
9
|
fg: string;
|
|
10
10
|
dim: string;
|
|
11
11
|
accent: string;
|
|
12
|
+
/** Foreground for the current working directory in the status bar. */
|
|
13
|
+
statusCwd: string;
|
|
12
14
|
border: string;
|
|
13
15
|
/** Foreground and background of a user turn's full-width bar. */
|
|
14
16
|
user: string;
|
|
@@ -47,6 +49,7 @@ const tokyonight: Theme = {
|
|
|
47
49
|
fg: "#c0caf5",
|
|
48
50
|
dim: "#565f89",
|
|
49
51
|
accent: "#7aa2f7",
|
|
52
|
+
statusCwd: "#2ac3de",
|
|
50
53
|
border: "#292e42",
|
|
51
54
|
user: "#c0caf5",
|
|
52
55
|
userBg: "#283457",
|
|
@@ -79,6 +82,7 @@ const gruvbox: Theme = {
|
|
|
79
82
|
fg: "#ebdbb2",
|
|
80
83
|
dim: "#928374",
|
|
81
84
|
accent: "#83a598",
|
|
85
|
+
statusCwd: "#8ec07c",
|
|
82
86
|
border: "#3c3836",
|
|
83
87
|
user: "#ebdbb2",
|
|
84
88
|
userBg: "#3c3836",
|
|
@@ -111,6 +115,7 @@ const catppuccin: Theme = {
|
|
|
111
115
|
fg: "#cdd6f4",
|
|
112
116
|
dim: "#6c7086",
|
|
113
117
|
accent: "#89b4fa",
|
|
118
|
+
statusCwd: "#94e2d5",
|
|
114
119
|
border: "#313244",
|
|
115
120
|
user: "#cdd6f4",
|
|
116
121
|
userBg: "#313244",
|
|
@@ -143,6 +148,7 @@ const nord: Theme = {
|
|
|
143
148
|
fg: "#d8dee9",
|
|
144
149
|
dim: "#7b88a1",
|
|
145
150
|
accent: "#88c0d0",
|
|
151
|
+
statusCwd: "#8fbcbb",
|
|
146
152
|
border: "#3b4252",
|
|
147
153
|
user: "#eceff4",
|
|
148
154
|
userBg: "#434c5e",
|
|
@@ -175,6 +181,7 @@ const dracula: Theme = {
|
|
|
175
181
|
fg: "#f8f8f2",
|
|
176
182
|
dim: "#6272a4",
|
|
177
183
|
accent: "#bd93f9",
|
|
184
|
+
statusCwd: "#8be9fd",
|
|
178
185
|
border: "#44475a",
|
|
179
186
|
user: "#f8f8f2",
|
|
180
187
|
userBg: "#44475a",
|
|
@@ -207,6 +214,7 @@ const rosepine: Theme = {
|
|
|
207
214
|
fg: "#e0def4",
|
|
208
215
|
dim: "#6e6a86",
|
|
209
216
|
accent: "#c4a7e7",
|
|
217
|
+
statusCwd: "#9ccfd8",
|
|
210
218
|
border: "#26233a",
|
|
211
219
|
user: "#e0def4",
|
|
212
220
|
userBg: "#26233a",
|
|
@@ -239,6 +247,7 @@ const solarized: Theme = {
|
|
|
239
247
|
fg: "#839496",
|
|
240
248
|
dim: "#586e75",
|
|
241
249
|
accent: "#268bd2",
|
|
250
|
+
statusCwd: "#2aa198",
|
|
242
251
|
border: "#073642",
|
|
243
252
|
user: "#93a1a1",
|
|
244
253
|
userBg: "#073642",
|
|
@@ -271,6 +280,7 @@ const kanagawa: Theme = {
|
|
|
271
280
|
fg: "#dcd7ba",
|
|
272
281
|
dim: "#727169",
|
|
273
282
|
accent: "#7e9cd8",
|
|
283
|
+
statusCwd: "#7fb4ca",
|
|
274
284
|
border: "#2a2a37",
|
|
275
285
|
user: "#dcd7ba",
|
|
276
286
|
userBg: "#2d4f67",
|
|
@@ -303,6 +313,7 @@ const githubLight: Theme = {
|
|
|
303
313
|
fg: "#24292f",
|
|
304
314
|
dim: "#6e7781",
|
|
305
315
|
accent: "#0969da",
|
|
316
|
+
statusCwd: "#0a3069",
|
|
306
317
|
border: "#d0d7de",
|
|
307
318
|
user: "#24292f",
|
|
308
319
|
userBg: "#ddf4ff",
|