@andares/pi-toolkits 0.11.0 → 0.11.2

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.
@@ -57,6 +57,23 @@ export interface CommandUnit {
57
57
 
58
58
  const SENTINEL = "\u0000";
59
59
 
60
+ /**
61
+ * Device sinks: writing to them is a no-op, not a boundary decision. They are
62
+ * excluded whenever they appear as a plain operand (`curl -o /dev/null`),
63
+ * not just as a redirect target (`cmd > /dev/null`). Commands that mutate the
64
+ * node itself (`rm /dev/null`, `mv /dev/null x`) still collect the target —
65
+ * the write-path branch below keeps them visible to the policy.
66
+ */
67
+ const DEVICE_SINKS = new Set(["/dev/null", "/dev/stdout", "/dev/stderr"]);
68
+
69
+ /**
70
+ * Commands whose ordinary arguments are never filesystem paths. `echo` /
71
+ * `printf` can only write through a redirect, which is collected separately,
72
+ * so skipping their operands cannot hide a write and avoids treating a
73
+ * message containing `/` plus an unresolved variable as an opaque path.
74
+ */
75
+ const NO_PATH_OPERANDS = new Set(["echo", "printf"]);
76
+
60
77
  /* -------------------------------------------------------------------------- */
61
78
  /* Read/write classification */
62
79
  /* -------------------------------------------------------------------------- */
@@ -768,9 +785,7 @@ function analyzeSegment(
768
785
  for (const redirect of redirects) {
769
786
  if (redirect.target === undefined) continue;
770
787
  if (redirect.op === "<<<") continue; // herestring: data, not a path
771
- if (redirect.target === "/dev/null" || redirect.target === "/dev/stdout" || redirect.target === "/dev/stderr") {
772
- continue;
773
- }
788
+ if (DEVICE_SINKS.has(redirect.target)) continue;
774
789
  const descriptor =
775
790
  (redirect.op === ">&" || redirect.op === "<&" || redirect.op === "&>") &&
776
791
  /^\d+$/.test(redirect.target);
@@ -839,9 +854,12 @@ function analyzeSegment(
839
854
  }
840
855
  targets.push({ path: pathValue, effect: "write", source: "operand" });
841
856
  }
842
- } else if (command === "dd") { for (const arg of args) {
857
+ } else if (command === "dd") {
858
+ for (const arg of args) {
843
859
  if (arg.startsWith("of=")) {
844
- const pathValue = resolveTargetPath(base, arg.slice(3));
860
+ const value = arg.slice(3);
861
+ if (DEVICE_SINKS.has(value)) continue;
862
+ const pathValue = resolveTargetPath(base, value);
845
863
  if (pathValue === undefined) {
846
864
  return {
847
865
  units: [opaqueUnit(raw, headWord, command, base, "unresolved dd target")],
@@ -858,28 +876,35 @@ function analyzeSegment(
858
876
  }
859
877
  }
860
878
 
861
- // Path-like arguments are targets for every command.
862
- for (const arg of args) {
863
- if (!looksLikePath(arg)) continue;
864
- // `sed -i` operands were already classified above; its script is not a path.
865
- if (command === "sed" && effect === "write") continue;
866
- const pathValue = resolveTargetPath(commandBase, arg);
867
- if (pathValue === undefined) {
868
- return {
869
- units: [opaqueUnit(raw, headWord, command, base, "unresolved path argument")],
870
- nextBase: base,
871
- };
879
+ // Path-like arguments are targets for every command, except for commands
880
+ // whose arguments are data (`echo`, `printf`) and device sinks.
881
+ if (!NO_PATH_OPERANDS.has(command)) {
882
+ for (const arg of args) {
883
+ if (DEVICE_SINKS.has(arg)) continue;
884
+ if (!looksLikePath(arg)) continue;
885
+ // `sed -i` operands were already classified above; its script is not a path.
886
+ if (command === "sed" && effect === "write") continue;
887
+ const pathValue = resolveTargetPath(commandBase, arg);
888
+ if (pathValue === undefined) {
889
+ return {
890
+ units: [opaqueUnit(raw, headWord, command, base, "unresolved path argument")],
891
+ nextBase: base,
892
+ };
893
+ }
894
+ targets.push({ path: pathValue, effect, source: "operand" });
872
895
  }
873
- targets.push({ path: pathValue, effect, source: "operand" });
874
896
  }
875
897
 
876
- // `-o FILE` / `--output FILE` writes whatever the command is.
877
- for (let i = 0; i < args.length; i++) {
878
- if (args[i] !== "-o" && args[i] !== "--output") continue;
879
- const value = args[i + 1];
880
- if (value === undefined) continue;
881
- const pathValue = resolveTargetPath(commandBase, value);
882
- if (pathValue) targets.push({ path: pathValue, effect: "write", source: "operand" });
898
+ // `-o FILE` / `--output FILE` writes whatever the command is. `echo` and
899
+ // `printf` do not have an output option, so their `-o` is data too.
900
+ if (!NO_PATH_OPERANDS.has(command)) {
901
+ for (let i = 0; i < args.length; i++) {
902
+ if (args[i] !== "-o" && args[i] !== "--output") continue;
903
+ const value = args[i + 1];
904
+ if (value === undefined || DEVICE_SINKS.has(value)) continue;
905
+ const pathValue = resolveTargetPath(commandBase, value);
906
+ if (pathValue) targets.push({ path: pathValue, effect: "write", source: "operand" });
907
+ }
883
908
  }
884
909
 
885
910
  units.push({
@@ -21,7 +21,7 @@ import type { KeyId } from "@earendil-works/pi-tui";
21
21
  /**
22
22
  * Stash hotkey.
23
23
  *
24
- * Compatibility rationale (verified against pi 0.99.0–1.4.0 keybindings, live
24
+ * Compatibility rationale (verified against pi 0.99.0–1.5.0 keybindings, live
25
25
  * matchesKey probes and terminal/OS behavior):
26
26
  * - pi: no default binding for ctrl+alt+<letter> (only ctrl+alt+] is taken);
27
27
  * extension shortcuts dispatch first while the editor is focused
package/src/lib/gates.ts CHANGED
@@ -18,9 +18,9 @@
18
18
  * promise. The queue serializes our own presentations; the host slot is
19
19
  * still shared with other extensions (pi does not arbitrate globally).
20
20
  * pi 1.4.0 stacks extension dialogs so a second one no longer strands the
21
- * first (#11166), but the 1.1–1.3 hosts in the supported window still
21
+ * first (#11166), but the 1.2–1.3 hosts in the supported window still
22
22
  * replace it — the queue stays, and it also keeps one confirmation visible
23
- * at a time instead of a pile of stacked dialogs on 1.4.0.
23
+ * at a time instead of a pile of stacked dialogs on later hosts.
24
24
  * - A gate that throws blocks the call (fail closed), matching pi's own
25
25
  * `tool_call` handler contract.
26
26
  *
@@ -4,8 +4,8 @@
4
4
  * (review, compact-model — favorites keeps its own store-local key logic).
5
5
  *
6
6
  * Types are derived from pi-coding-agent's own exports (ModelRegistry)
7
- * rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (1.1.0
8
- * through 1.4.0 alike) re-exports `Model`/`Api` via extensioned
7
+ * rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (1.2.0
8
+ * through 1.5.0 alike) re-exports `Model`/`Api` via extensioned
9
9
  * `./types.ts` specifiers, which tsc 5.9 bundler resolution substitutes to
10
10
  * `.d.ts` but some tsserver builds do not. Deriving from ModelRegistry keeps
11
11
  * every resolver in agreement with the exact type
@@ -9,11 +9,12 @@
9
9
  *
10
10
  * The component is driven through a small ModelRuntime facade backed by
11
11
  * ctx.modelRegistry (pi's extension-facing wrapper). SAFETY: verified
12
- * against pi 1.1.0–1.4.0 dist (modes/interactive/components/model-selector.js +
13
- * model-catalog-refresh.js — byte-identical across that span) — the
14
- * component only calls getAvailableSnapshot, getModel, getError and refresh
15
- * on modelRuntime, and ModelRegistry provides exactly those
16
- * (getAvailable/find/getError/refresh).
12
+ * against pi 1.2.0–1.5.0 dist (modes/interactive/components/model-selector.js +
13
+ * model-catalog-refresh.js) — the component only calls getAvailableSnapshot,
14
+ * getModel and getError on modelRuntime (plus refresh, which ModelRegistry
15
+ * provides) across that whole span. 1.5.0 changed the selector's rendering
16
+ * and key handling (page keys, disambiguating model names, viewport-input
17
+ * capture) but not the runtime surface this facade implements.
17
18
  *
18
19
  * `current` is passed as the component's currentModel: the built-in selector
19
20
  * preselects, ✓-marks and sorts-first its currentModel, so one Enter accepts