bermudis-pi-goodies 0.27.7 → 0.27.8
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 +2 -2
- package/index.ts +1 -1
- package/package.json +1 -1
- package/prefer-tools.ts +204 -68
- package/provider-balance.ts +46 -6
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ extensions. One entry point, thirteen independent features.
|
|
|
8
8
|
| `copy-with-model` | `/copy-with-model` | Copy last assistant message to the clipboard in a code fence tagged with the model name. |
|
|
9
9
|
| `copy-trajectory` | `/copy-trajectory [thinking]` | Copy the whole conversation (user + assistant text, tool calls stripped) to the clipboard; `thinking` also includes assistant thinking blocks. |
|
|
10
10
|
| `name-with-ai` | `/name-with-ai [name]` | Generate a short session name from the first user message (or set one manually). |
|
|
11
|
-
| `prefer-tools` | hook (no command) | Blocks legacy tools: `rm` → `trash <path>` (and `trash-rm`/`trash-empty`, which delete permanently), bare `python`/`pip`/`pytest`/`mypy` → `uv`.
|
|
11
|
+
| `prefer-tools` | hook (no command) | Blocks legacy tools: `rm` → `trash <path>` (and `trash-rm`/`trash-empty`, which delete permanently), bare `python`/`pip`/`pytest`/`mypy` → `uv`. Bash `rg` is allowed, except the replace flag: `-r` (including clusters like `-rn`) is blocked because it is `--replace` and silently replaces matched text — use `--replace` explicitly if you mean it. The built-in grep tool still works. |
|
|
12
12
|
| `keep-model-on-new` | hook (no command) | Keep the active model when `/new` starts a fresh session instead of reverting to pi's saved default model. |
|
|
13
13
|
| `model-thinking` | `/model-thinking` | Per-model default thinking levels: save the current level as this model's default and get it back on every switch to that model, instead of pi's global default. |
|
|
14
14
|
| `clean-tui` | tool overrides (no command) | Collapse built-in tool output for a cleaner TUI: back-to-back same-tool calls share one block (e.g. `read ×2`) until a boundary — visible text (assistant prose or a typed user message) or a thinking block (even an empty one, as OpenAI emits between tool calls) — so reasoning-per-call models render one block per call. Images stay visible without expanding, expand a row with ctrl+o to see the full command and results/diffs. Long bash commands get an AI-generated summary once you pick a model with `/goodies summary-model <provider/model>` (see "Smart summaries" below) — expanding a row swaps the summary back out for the raw command. While enabled, also flips `@bermudi/pi-codex`'s `apply_patch`/`web_search` into the same burst style. Long thinking runs can show a live plain-English line above the editor (`/goodies thinking-summaries on`) instead of a static `Thinking...` — see "Smart summaries". |
|
|
@@ -25,7 +25,7 @@ extensions. One entry point, thirteen independent features.
|
|
|
25
25
|
First install, pinned to a published version:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
pi install npm:bermudis-pi-goodies@0.27.
|
|
28
|
+
pi install npm:bermudis-pi-goodies@0.27.8
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Picking up a new release on an existing install (`pi install` is not the
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - copy-with-model /copy-with-model copy last reply tagged with the model
|
|
7
7
|
* - copy-trajectory /copy-trajectory copy the whole conversation (text only) to the clipboard
|
|
8
8
|
* - name-with-ai /name-with-ai generate a session name via the model
|
|
9
|
-
* - prefer-tools hook block legacy tools (trash/uv; blocks rg
|
|
9
|
+
* - prefer-tools hook block legacy tools (trash/uv; blocks rg's -r replace flag)
|
|
10
10
|
* - keep-model hook preserve the active model across /new
|
|
11
11
|
* - model-thinking /model-thinking per-model default thinking levels, applied on switch
|
|
12
12
|
* - clean-tui tool overrides collapse built-in tool output; keep a one-line call header, hide results/diffs until expanded
|
package/package.json
CHANGED
package/prefer-tools.ts
CHANGED
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
* python/pip/pytest/ -> uv
|
|
17
17
|
* mypy
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* `rg` is allowed, but its replace flag is not: `-r` is ripgrep's
|
|
20
|
+
* `--replace`, so `rg -rn` silently replaces matches with "n" instead of
|
|
21
|
+
* showing line numbers. Any short-option cluster containing `r` is blocked;
|
|
22
|
+
* spell out `--replace` if you actually mean it. The built-in grep tool
|
|
23
|
+
* (structured args, no raw flags) and bash `grep -rn` still work.
|
|
22
24
|
*/
|
|
23
25
|
import {
|
|
24
26
|
isToolCallEventType,
|
|
@@ -49,24 +51,13 @@ const RULES: Rule[] = [
|
|
|
49
51
|
];
|
|
50
52
|
|
|
51
53
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* provider-prefixed "kilo/glm-5" all match. glm-4.x is unaffected.
|
|
54
|
+
* `rg -r` is ripgrep's `--replace`, not "recursive": it replaces matched text
|
|
55
|
+
* in the output, so `rg -rn` replaces with "n" and shows no line numbers
|
|
56
|
+
* (machine lore: it once spawned a phantom `[ln]` log tag). Block the short
|
|
57
|
+
* flag in any cluster, but allow the explicit long form as the escape hatch.
|
|
57
58
|
*/
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
typeof modelId === "string" && /(^|[^a-z0-9])glm-5(?![0-9])/i.test(modelId)
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Extra rule appended when the active model is glm-5.*. */
|
|
65
|
-
export const RG_RULE: Rule = {
|
|
66
|
-
names: ["rg"],
|
|
67
|
-
reason:
|
|
68
|
-
'rg is blocked for this model — it invents ripgrep flags. Use `grep -rn "pattern" path` instead (or pi\'s built-in grep tool).',
|
|
69
|
-
};
|
|
59
|
+
export const RG_REPLACE_REASON =
|
|
60
|
+
'`rg -r` is `--replace` — it silently replaces matched text in the output (`rg -rn` replaces with "n" instead of showing line numbers). If you really mean it, use `--replace` explicitly.';
|
|
70
61
|
|
|
71
62
|
const COMMAND_PREFIX_KEYWORDS = new Set([
|
|
72
63
|
"if",
|
|
@@ -97,19 +88,34 @@ function isAssignmentWord(word: string): boolean {
|
|
|
97
88
|
return /^[A-Za-z_][A-Za-z0-9_]*\+?=/.test(word);
|
|
98
89
|
}
|
|
99
90
|
|
|
100
|
-
function
|
|
101
|
-
name:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (const rule of rules) {
|
|
91
|
+
function commandBase(name: string): string {
|
|
92
|
+
return name.includes("/") ? name.slice(name.lastIndexOf("/") + 1) : name;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function matchCommand(name: string): string | undefined {
|
|
96
|
+
const base = commandBase(name);
|
|
97
|
+
for (const rule of RULES) {
|
|
108
98
|
if (rule.names.includes(base)) return rule.reason;
|
|
109
99
|
}
|
|
110
100
|
return undefined;
|
|
111
101
|
}
|
|
112
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Match a command word, plus the one argument-sensitive rule: an `rg`
|
|
105
|
+
* invocation that uses the replace flag. `argStart` is the offset just past
|
|
106
|
+
* the command word, where its arguments begin.
|
|
107
|
+
*/
|
|
108
|
+
function matchCommandAt(
|
|
109
|
+
command: string,
|
|
110
|
+
word: string,
|
|
111
|
+
argStart: number,
|
|
112
|
+
): string | undefined {
|
|
113
|
+
if (commandBase(word) === "rg" && rgUsesReplaceFlag(command, argStart)) {
|
|
114
|
+
return RG_REPLACE_REASON;
|
|
115
|
+
}
|
|
116
|
+
return matchCommand(word);
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
function readWord(s: string, i: number): { word: string; next: number } {
|
|
114
120
|
// Build the word char by char so backslash escapes strip the backslash:
|
|
115
121
|
// `\rm` → `rm`, `foo\ bar` → `foo bar`. The old slice-based version kept
|
|
@@ -253,7 +259,6 @@ function scanExecutedSubs(
|
|
|
253
259
|
s: string,
|
|
254
260
|
start: number,
|
|
255
261
|
end: number,
|
|
256
|
-
rules: readonly Rule[],
|
|
257
262
|
mode: "plain" | "double",
|
|
258
263
|
): string | undefined {
|
|
259
264
|
let i = start;
|
|
@@ -273,7 +278,7 @@ function scanExecutedSubs(
|
|
|
273
278
|
if (c === '"') {
|
|
274
279
|
const stop = readQuote(s, i, '"', true);
|
|
275
280
|
const innerEnd = s[stop - 1] === '"' ? stop - 1 : stop;
|
|
276
|
-
const reason = scanExecutedSubs(s, i + 1, innerEnd,
|
|
281
|
+
const reason = scanExecutedSubs(s, i + 1, innerEnd, "double");
|
|
277
282
|
if (reason) return reason;
|
|
278
283
|
i = stop;
|
|
279
284
|
continue;
|
|
@@ -293,7 +298,7 @@ function scanExecutedSubs(
|
|
|
293
298
|
if (c === "`") {
|
|
294
299
|
const stop = readQuote(s, i, "`", true);
|
|
295
300
|
const innerEnd = s[stop - 1] === "`" ? stop - 1 : stop;
|
|
296
|
-
const reason = detectLegacyTool(s.slice(i + 1, innerEnd)
|
|
301
|
+
const reason = detectLegacyTool(s.slice(i + 1, innerEnd));
|
|
297
302
|
if (reason) return reason;
|
|
298
303
|
i = stop;
|
|
299
304
|
continue;
|
|
@@ -306,7 +311,7 @@ function scanExecutedSubs(
|
|
|
306
311
|
const stop = skipBalancedParens(s, i, 3);
|
|
307
312
|
const innerEnd =
|
|
308
313
|
s[stop - 1] === ")" && s[stop - 2] === ")" ? stop - 2 : stop;
|
|
309
|
-
const reason = scanExecutedSubs(s, i + 3, innerEnd,
|
|
314
|
+
const reason = scanExecutedSubs(s, i + 3, innerEnd, mode);
|
|
310
315
|
if (reason) return reason;
|
|
311
316
|
i = stop;
|
|
312
317
|
continue;
|
|
@@ -314,7 +319,7 @@ function scanExecutedSubs(
|
|
|
314
319
|
if (c === "$" && s.startsWith("$(", i)) {
|
|
315
320
|
const stop = skipBalancedParens(s, i, 2);
|
|
316
321
|
const innerEnd = s[stop - 1] === ")" ? stop - 1 : stop;
|
|
317
|
-
const reason = detectLegacyTool(s.slice(i + 2, innerEnd)
|
|
322
|
+
const reason = detectLegacyTool(s.slice(i + 2, innerEnd));
|
|
318
323
|
if (reason) return reason;
|
|
319
324
|
i = stop;
|
|
320
325
|
continue;
|
|
@@ -327,7 +332,7 @@ function scanExecutedSubs(
|
|
|
327
332
|
// substitution runs (`"${X:-'$(rm)'}"` executes).
|
|
328
333
|
const stop = skipBalancedBraces(s, i);
|
|
329
334
|
const innerEnd = s[stop - 1] === "}" ? stop - 1 : stop;
|
|
330
|
-
const reason = scanExecutedSubs(s, i + 2, innerEnd,
|
|
335
|
+
const reason = scanExecutedSubs(s, i + 2, innerEnd, mode);
|
|
331
336
|
if (reason) return reason;
|
|
332
337
|
i = stop;
|
|
333
338
|
continue;
|
|
@@ -340,7 +345,6 @@ function scanExecutedSubs(
|
|
|
340
345
|
function readDollar(
|
|
341
346
|
s: string,
|
|
342
347
|
i: number,
|
|
343
|
-
rules: readonly Rule[],
|
|
344
348
|
): { next: number; reason?: string } | null {
|
|
345
349
|
if (i >= s.length || s[i] !== "$") return null;
|
|
346
350
|
|
|
@@ -354,14 +358,14 @@ function readDollar(
|
|
|
354
358
|
const innerEnd = s[end - 1] === ")" && s[end - 2] === ")" ? end - 2 : end;
|
|
355
359
|
return {
|
|
356
360
|
next: end,
|
|
357
|
-
reason: scanExecutedSubs(s, i + 3, innerEnd,
|
|
361
|
+
reason: scanExecutedSubs(s, i + 3, innerEnd, "plain"),
|
|
358
362
|
};
|
|
359
363
|
}
|
|
360
364
|
if (s.startsWith("(", i + 1)) {
|
|
361
365
|
const end = skipBalancedParens(s, i, 2);
|
|
362
366
|
const closeParen = end > 0 && s[end - 1] === ")" ? 1 : 0;
|
|
363
367
|
const inner = s.slice(i + 2, end - closeParen);
|
|
364
|
-
const reason = detectLegacyTool(inner
|
|
368
|
+
const reason = detectLegacyTool(inner);
|
|
365
369
|
return { next: end, reason };
|
|
366
370
|
}
|
|
367
371
|
if (s.startsWith("{", i + 1)) {
|
|
@@ -370,7 +374,7 @@ function readDollar(
|
|
|
370
374
|
const innerEnd = s[end - 1] === "}" ? end - 1 : end;
|
|
371
375
|
return {
|
|
372
376
|
next: end,
|
|
373
|
-
reason: scanExecutedSubs(s, i + 2, innerEnd,
|
|
377
|
+
reason: scanExecutedSubs(s, i + 2, innerEnd, "plain"),
|
|
374
378
|
};
|
|
375
379
|
}
|
|
376
380
|
if (i + 1 < s.length && /[0-9?@*#\-!$]/.test(s[i + 1])) {
|
|
@@ -447,10 +451,7 @@ function readHeredocDelimiter(
|
|
|
447
451
|
* backslash suppresses expansion (`\$(rm)` is literal). Returns the matched
|
|
448
452
|
* rule's reason if a blocked tool is found, undefined otherwise.
|
|
449
453
|
*/
|
|
450
|
-
function scanHeredocLineForCommandSubs(
|
|
451
|
-
line: string,
|
|
452
|
-
rules: readonly Rule[],
|
|
453
|
-
): string | undefined {
|
|
454
|
+
function scanHeredocLineForCommandSubs(line: string): string | undefined {
|
|
454
455
|
let i = 0;
|
|
455
456
|
while (i < line.length) {
|
|
456
457
|
const c = line[i];
|
|
@@ -461,7 +462,7 @@ function scanHeredocLineForCommandSubs(
|
|
|
461
462
|
if (line.startsWith("$(", i)) {
|
|
462
463
|
const end = skipBalancedParens(line, i, 2);
|
|
463
464
|
const inner = line.slice(i + 2, end - 1);
|
|
464
|
-
const reason = detectLegacyTool(inner
|
|
465
|
+
const reason = detectLegacyTool(inner);
|
|
465
466
|
if (reason) return reason;
|
|
466
467
|
i = end;
|
|
467
468
|
continue;
|
|
@@ -469,7 +470,7 @@ function scanHeredocLineForCommandSubs(
|
|
|
469
470
|
if (c === "`") {
|
|
470
471
|
const end = readQuote(line, i, "`", true);
|
|
471
472
|
const inner = line.slice(i + 1, end - 1);
|
|
472
|
-
const reason = detectLegacyTool(inner
|
|
473
|
+
const reason = detectLegacyTool(inner);
|
|
473
474
|
if (reason) return reason;
|
|
474
475
|
i = end;
|
|
475
476
|
continue;
|
|
@@ -485,7 +486,6 @@ function skipHeredocBody(
|
|
|
485
486
|
delimiter: string,
|
|
486
487
|
indented: boolean,
|
|
487
488
|
quoted: boolean,
|
|
488
|
-
rules: readonly Rule[],
|
|
489
489
|
): { next: number; reason?: string } {
|
|
490
490
|
let pos = i;
|
|
491
491
|
while (pos <= s.length) {
|
|
@@ -499,7 +499,7 @@ function skipHeredocBody(
|
|
|
499
499
|
// In unquoted heredocs, command substitutions execute — scan for
|
|
500
500
|
// blocked tools inside $(...) and backticks.
|
|
501
501
|
if (!quoted) {
|
|
502
|
-
const reason = scanHeredocLineForCommandSubs(line
|
|
502
|
+
const reason = scanHeredocLineForCommandSubs(line);
|
|
503
503
|
if (reason) return { next: end, reason };
|
|
504
504
|
}
|
|
505
505
|
if (nl === -1) break;
|
|
@@ -530,11 +530,157 @@ const SUDO_OPTS_WITH_ARG = new Set([
|
|
|
530
530
|
"--user",
|
|
531
531
|
]);
|
|
532
532
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Short rg options that consume a value: the rest of the cluster is their
|
|
535
|
+
* value, or — at cluster end — the next word is. `r` is handled separately
|
|
536
|
+
* (and blocked) before this set is consulted.
|
|
537
|
+
*/
|
|
538
|
+
const RG_SHORT_VALUE_OPTS = new Set([
|
|
539
|
+
"A",
|
|
540
|
+
"B",
|
|
541
|
+
"C",
|
|
542
|
+
"d",
|
|
543
|
+
"E",
|
|
544
|
+
"e",
|
|
545
|
+
"f",
|
|
546
|
+
"g",
|
|
547
|
+
"j",
|
|
548
|
+
"M",
|
|
549
|
+
"m",
|
|
550
|
+
"t",
|
|
551
|
+
"T",
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Long rg options that consume the next word as a value. Values are skipped
|
|
556
|
+
* before flag inspection, so `--field-match-separator -r` (ripgrep accepts
|
|
557
|
+
* `-r` there as literal output text) is not mistaken for the replace flag.
|
|
558
|
+
*/
|
|
559
|
+
const RG_LONG_VALUE_OPTS = new Set([
|
|
560
|
+
"--after-context",
|
|
561
|
+
"--before-context",
|
|
562
|
+
"--context",
|
|
563
|
+
"--encoding",
|
|
564
|
+
"--field-context-separator",
|
|
565
|
+
"--field-match-separator",
|
|
566
|
+
"--file",
|
|
567
|
+
"--glob",
|
|
568
|
+
"--iglob",
|
|
569
|
+
"--max-columns",
|
|
570
|
+
"--max-count",
|
|
571
|
+
"--max-depth",
|
|
572
|
+
"--max-filesize",
|
|
573
|
+
"--path-separator",
|
|
574
|
+
"--pre",
|
|
575
|
+
"--regexp",
|
|
576
|
+
"--replace",
|
|
577
|
+
"--sort",
|
|
578
|
+
"--sortr",
|
|
579
|
+
"--threads",
|
|
580
|
+
"--type",
|
|
581
|
+
"--type-add",
|
|
582
|
+
"--type-clear",
|
|
583
|
+
"--type-not",
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Read one shell word after an `rg` command, keeping quoted content as part
|
|
588
|
+
* of the token so `rg "-r"` and `rg '-rn'` are caught like their unquoted
|
|
589
|
+
* twins. Stops at whitespace or an unquoted operator.
|
|
590
|
+
*/
|
|
591
|
+
function readArgWord(s: string, i: number): { word: string; next: number } {
|
|
592
|
+
let word = "";
|
|
593
|
+
while (i < s.length) {
|
|
594
|
+
const c = s[i];
|
|
595
|
+
if (c === " " || c === "\t" || c === "\r" || c === "\n") break;
|
|
596
|
+
if (readOperator(s, i)) break;
|
|
597
|
+
if (c === "'" || c === '"') {
|
|
598
|
+
const end = readQuote(s, i, c, c === '"');
|
|
599
|
+
const innerEnd = s[end - 1] === c ? end - 1 : end;
|
|
600
|
+
word += s.slice(i + 1, innerEnd);
|
|
601
|
+
i = end;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
if (c === "\\" && i + 1 < s.length) {
|
|
605
|
+
word += s[i + 1];
|
|
606
|
+
i += 2;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (c === "$") {
|
|
610
|
+
const d = readDollar(s, i);
|
|
611
|
+
if (d) {
|
|
612
|
+
word += s.slice(i, d.next);
|
|
613
|
+
i = d.next;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
word += c;
|
|
618
|
+
i++;
|
|
619
|
+
}
|
|
620
|
+
return { word, next: i };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* True when the `rg` invocation starting at `start` uses the replace flag in
|
|
625
|
+
* short form, including clusters (`-rn`, `-rln`, `-nr`). The explicit
|
|
626
|
+
* `--replace` long form stays allowed. Value-taking options are skipped so a
|
|
627
|
+
* pattern/replacement that merely looks like `-r` is not a false hit.
|
|
628
|
+
*/
|
|
629
|
+
function rgUsesReplaceFlag(s: string, start: number): boolean {
|
|
630
|
+
let i = start;
|
|
631
|
+
let skipValue = false;
|
|
632
|
+
while (i < s.length) {
|
|
633
|
+
const c = s[i];
|
|
634
|
+
if (c === " " || c === "\t" || c === "\r") {
|
|
635
|
+
i++;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (c === "\n") break;
|
|
639
|
+
// Unquoted `#` at the start of a word begins a shell comment: bash never
|
|
640
|
+
// passes it (or anything after it) to rg, so `rg -n foo . # avoid -rn`
|
|
641
|
+
// must not be blocked for what its comment merely mentions. Mid-word `#`
|
|
642
|
+
// (`rg foo#bar`) stays inside the word read below, matching bash.
|
|
643
|
+
if (c === "#") break;
|
|
644
|
+
const op = readOperator(s, i);
|
|
645
|
+
if (op) {
|
|
646
|
+
if (op.type !== "redirect") break;
|
|
647
|
+
// Redirections may appear among a command's arguments; consume the
|
|
648
|
+
// target (`>out`) and keep looking at the real rg options after it.
|
|
649
|
+
i = op.next;
|
|
650
|
+
while (i < s.length && (s[i] === " " || s[i] === "\t")) i++;
|
|
651
|
+
if (i < s.length) i = readArgWord(s, i).next;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const { word, next } = readArgWord(s, i);
|
|
655
|
+
if (next === i) {
|
|
656
|
+
i++;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
i = next;
|
|
660
|
+
if (skipValue) {
|
|
661
|
+
skipValue = false;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (word === "--") break;
|
|
665
|
+
if (word.startsWith("--")) {
|
|
666
|
+
if (!word.includes("=") && RG_LONG_VALUE_OPTS.has(word)) skipValue = true;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (!word.startsWith("-") || word === "-") continue;
|
|
670
|
+
for (let k = 1; k < word.length; k++) {
|
|
671
|
+
const flag = word[k];
|
|
672
|
+
if (flag === "r") return true;
|
|
673
|
+
if (RG_SHORT_VALUE_OPTS.has(flag)) {
|
|
674
|
+
// The rest of the cluster (or the next word) is this option's value.
|
|
675
|
+
if (k === word.length - 1) skipValue = true;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export function detectLegacyTool(command: string): string | undefined {
|
|
538
684
|
let i = 0;
|
|
539
685
|
let commandPos = true;
|
|
540
686
|
// Set once an env-var assignment word is seen in the current simple
|
|
@@ -560,7 +706,6 @@ export function detectLegacyTool(
|
|
|
560
706
|
heredoc.delimiter,
|
|
561
707
|
heredoc.indented,
|
|
562
708
|
heredoc.quoted,
|
|
563
|
-
rules,
|
|
564
709
|
);
|
|
565
710
|
if (result.reason) return result.reason;
|
|
566
711
|
i = result.next;
|
|
@@ -671,17 +816,11 @@ export function detectLegacyTool(
|
|
|
671
816
|
// their unquoted twin.
|
|
672
817
|
if (quote === '"') {
|
|
673
818
|
const innerEnd = command[end - 1] === '"' ? end - 1 : end;
|
|
674
|
-
const reason = scanExecutedSubs(
|
|
675
|
-
command,
|
|
676
|
-
i + 1,
|
|
677
|
-
innerEnd,
|
|
678
|
-
rules,
|
|
679
|
-
"double",
|
|
680
|
-
);
|
|
819
|
+
const reason = scanExecutedSubs(command, i + 1, innerEnd, "double");
|
|
681
820
|
if (reason) return reason;
|
|
682
821
|
} else if (quote === "`") {
|
|
683
822
|
const innerEnd = command[end - 1] === "`" ? end - 1 : end;
|
|
684
|
-
const reason = detectLegacyTool(command.slice(i + 1, innerEnd)
|
|
823
|
+
const reason = detectLegacyTool(command.slice(i + 1, innerEnd));
|
|
685
824
|
if (reason) return reason;
|
|
686
825
|
}
|
|
687
826
|
i = end;
|
|
@@ -694,7 +833,7 @@ export function detectLegacyTool(
|
|
|
694
833
|
}
|
|
695
834
|
|
|
696
835
|
if (c === "$") {
|
|
697
|
-
const d = readDollar(command, i
|
|
836
|
+
const d = readDollar(command, i);
|
|
698
837
|
if (d) {
|
|
699
838
|
if (d.reason) return d.reason;
|
|
700
839
|
i = d.next;
|
|
@@ -746,7 +885,7 @@ export function detectLegacyTool(
|
|
|
746
885
|
skipNextWord = false;
|
|
747
886
|
continue;
|
|
748
887
|
}
|
|
749
|
-
const reason =
|
|
888
|
+
const reason = matchCommandAt(command, word, i);
|
|
750
889
|
if (reason) return reason;
|
|
751
890
|
sudoNext = false;
|
|
752
891
|
} else {
|
|
@@ -754,7 +893,7 @@ export function detectLegacyTool(
|
|
|
754
893
|
skipNextWord = false;
|
|
755
894
|
continue;
|
|
756
895
|
}
|
|
757
|
-
const reason =
|
|
896
|
+
const reason = matchCommandAt(command, word, i);
|
|
758
897
|
if (reason) return reason;
|
|
759
898
|
}
|
|
760
899
|
if (!COMMAND_PREFIX_KEYWORDS.has(word)) {
|
|
@@ -769,13 +908,10 @@ export function detectLegacyTool(
|
|
|
769
908
|
}
|
|
770
909
|
|
|
771
910
|
export default function preferTools(pi: ExtensionAPI) {
|
|
772
|
-
pi.on("tool_call", async (event
|
|
911
|
+
pi.on("tool_call", async (event) => {
|
|
773
912
|
if (!isToolCallEventType("bash", event)) return;
|
|
774
913
|
|
|
775
|
-
|
|
776
|
-
// read per call — model switches mid-session take effect immediately).
|
|
777
|
-
const extraRules = isRipgrepBlockedModel(ctx.model?.id) ? [RG_RULE] : [];
|
|
778
|
-
const reason = detectLegacyTool(event.input.command ?? "", extraRules);
|
|
914
|
+
const reason = detectLegacyTool(event.input.command ?? "");
|
|
779
915
|
if (reason) {
|
|
780
916
|
return { block: true, reason };
|
|
781
917
|
}
|
package/provider-balance.ts
CHANGED
|
@@ -1733,10 +1733,19 @@ export default function providerBalance(
|
|
|
1733
1733
|
return;
|
|
1734
1734
|
}
|
|
1735
1735
|
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
|
|
1739
|
-
|
|
1736
|
+
// Hide the balance while credentials are being resolved — but only when
|
|
1737
|
+
// nothing is displayed yet (cold start, provider switch, post-logout).
|
|
1738
|
+
// A same-account refresh keeps its last value on screen: blanking on
|
|
1739
|
+
// every refresh would flicker the footer for the full duration of
|
|
1740
|
+
// keychain reads and OAuth token refreshes (seconds under lock
|
|
1741
|
+
// contention). If resolution reveals a different account, the
|
|
1742
|
+
// displayedCacheKey mismatch below still clears it, so a foreign
|
|
1743
|
+
// reading is never painted. Same principle as the post-fetch re-check
|
|
1744
|
+
// below: never blank a displayed same-account value.
|
|
1745
|
+
if (!balance) {
|
|
1746
|
+
identityPending = true;
|
|
1747
|
+
requestRender?.();
|
|
1748
|
+
}
|
|
1740
1749
|
|
|
1741
1750
|
// Preserve a known same-account value during routine credential refresh,
|
|
1742
1751
|
// but clear it if identity resolution fails or reveals another account.
|
|
@@ -1831,6 +1840,14 @@ export default function providerBalance(
|
|
|
1831
1840
|
);
|
|
1832
1841
|
const observedAtMs = now();
|
|
1833
1842
|
if (generation !== refreshGeneration) return;
|
|
1843
|
+
if (nextBalance.length === 0) {
|
|
1844
|
+
// An empty reading would render as an empty string — visually
|
|
1845
|
+
// identical to the balance vanishing — and parseCachedBalance already
|
|
1846
|
+
// refuses to serve empty entries from the cache. Treat it as a failed
|
|
1847
|
+
// observation: the catch below keeps the previous display and records
|
|
1848
|
+
// the failure.
|
|
1849
|
+
throw new Error("provider returned no balance segments");
|
|
1850
|
+
}
|
|
1834
1851
|
|
|
1835
1852
|
// Login/logout does not emit model_select. Re-resolve identity before
|
|
1836
1853
|
// committing so an account switch during the request cannot paint the
|
|
@@ -1910,7 +1927,14 @@ export default function providerBalance(
|
|
|
1910
1927
|
companionTokenAfter,
|
|
1911
1928
|
);
|
|
1912
1929
|
}
|
|
1913
|
-
// Recovery detected:
|
|
1930
|
+
// Recovery detected: tell the log once, then clear the marker so
|
|
1931
|
+
// sibling idle polls resume.
|
|
1932
|
+
if (readBalanceFailureAt(cacheKey, now(), cacheDir) !== null) {
|
|
1933
|
+
logGoodiesEvent({
|
|
1934
|
+
type: "provider_balance_refresh_recovered",
|
|
1935
|
+
provider: providerId,
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1914
1938
|
clearBalanceFailureMarker(cacheKey, cacheDir);
|
|
1915
1939
|
const persisted = writeCachedBalance(
|
|
1916
1940
|
cacheKey,
|
|
@@ -1929,12 +1953,28 @@ export default function providerBalance(
|
|
|
1929
1953
|
return;
|
|
1930
1954
|
}
|
|
1931
1955
|
identityPending = false;
|
|
1932
|
-
|
|
1956
|
+
// A failed fetch does not make the displayed value wrong — it stays
|
|
1957
|
+
// TTL-gated at render time. Keep a same-account reading on screen
|
|
1958
|
+
// instead of blanking the footer for the whole backoff window; clear
|
|
1959
|
+
// only when the failure landed before any account was identified.
|
|
1960
|
+
if (cacheKey === undefined || cacheKey !== displayedCacheKey) {
|
|
1961
|
+
clearBalance();
|
|
1962
|
+
}
|
|
1933
1963
|
// Record the failure machine-wide so sibling sessions' idle polls back
|
|
1934
1964
|
// off instead of stacking their own timeouts onto the same outage.
|
|
1965
|
+
const firstFailureOfStreak =
|
|
1966
|
+
cacheKey === undefined ||
|
|
1967
|
+
readBalanceFailureAt(cacheKey, now(), cacheDir) === null;
|
|
1935
1968
|
if (cacheKey !== undefined) {
|
|
1936
1969
|
writeBalanceFailureMarker(cacheKey, now(), cacheDir);
|
|
1937
1970
|
}
|
|
1971
|
+
if (firstFailureOfStreak) {
|
|
1972
|
+
logGoodiesEvent({
|
|
1973
|
+
type: "provider_balance_refresh_failed",
|
|
1974
|
+
provider: providerId,
|
|
1975
|
+
message: describeError(error),
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1938
1978
|
// This is a best-effort background refresh. Writing to stdout/stderr while
|
|
1939
1979
|
// Pi owns the terminal corrupts the TUI (the text appears in the editor),
|
|
1940
1980
|
// so expose failures to other extensions without producing terminal output.
|