@groeponline/pi-wishcraft 0.23.2 → 0.23.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.23.4] - 2026-08-20
6
+
7
+ ### Fixed
8
+ - Command hooks, session context inject, and `/repairs` run: `setupHooks` is registered on activation.
9
+ - Hook runner ignores EPIPE when a command exits before reading stdin.
10
+
11
+ ## [0.23.3] - 2026-08-20
12
+
13
+ ### Fixed
14
+ - Nested settings writes reject `__proto__` / `constructor` / `prototype` keys in the assignment loop.
15
+ - Test workflow pins `GITHUB_TOKEN` to `contents: read`.
16
+
5
17
  ## [0.23.2] - 2026-08-20
6
18
 
7
19
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "0.23.2",
3
+ "version": "0.23.4",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
@@ -110,12 +110,20 @@ export function runHookCommand(
110
110
  finish(null);
111
111
  });
112
112
  child.on("close", (code) => finish(code));
113
+ // Hooks that exit before reading stdin (typical `exit 2` deny scripts)
114
+ // close the pipe; ignore EPIPE so the harness still records the exit code.
115
+ child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
116
+ if (error.code === "EPIPE") return;
117
+ stderr += String(error);
118
+ });
113
119
 
114
120
  try {
115
121
  child.stdin?.write(JSON.stringify(payload));
116
122
  child.stdin?.end();
117
- } catch {
118
- // stdin weg hook zonder input; laat 'm zelf falen
123
+ } catch (error) {
124
+ if ((error as NodeJS.ErrnoException).code !== "EPIPE") {
125
+ stderr += String(error);
126
+ }
119
127
  }
120
128
  });
121
129
  }
@@ -74,11 +74,15 @@ function basePayload(
74
74
  }
75
75
 
76
76
  /** Registreer de hooks + repairs op de pi extension API. */
77
- export function setupHooks(pi: ExtensionAPI, rt: RuntimeState, cwd: string): void {
78
- refreshSettings(cwd);
77
+ export function setupHooks(
78
+ pi: ExtensionAPI,
79
+ rt: RuntimeState,
80
+ cwd: string = process.cwd(),
81
+ ): void {
82
+ refreshSettings(rt.currentCtx?.cwd ?? cwd);
79
83
 
80
84
  pi.on("session_start", async () => {
81
- refreshSettings(cwd);
85
+ refreshSettings(rt.currentCtx?.cwd ?? process.cwd());
82
86
  if (!hooksEnabled) return;
83
87
  const cmds = commandsFor(hooksSettings, "sessionStart");
84
88
  if (cmds.length === 0) return;
@@ -6,6 +6,7 @@ import { registerCustomSegments } from "../../segments/index.ts";
6
6
  import { readSettings } from "../settings/settings-io.ts";
7
7
  import { registerSessionLifecycle } from "./session-lifecycle.ts";
8
8
  import { registerCommands } from "../commands/commands.ts";
9
+ import { setupHooks } from "../hooks/index.ts";
9
10
  import { setupInlineInvocation } from "../skills/inline-invocation.ts";
10
11
  import {
11
12
  config,
@@ -26,4 +27,5 @@ export default function powerlineFooter(pi: ExtensionAPI) {
26
27
  registerSessionLifecycle(pi, rt);
27
28
  registerCommands(pi, rt);
28
29
  setupInlineInvocation(pi, rt);
30
+ setupHooks(pi, rt, process.cwd());
29
31
  }
@@ -55,7 +55,44 @@ export function readConfigPath(settings: Record<string, unknown>, path: string):
55
55
  return null;
56
56
  }
57
57
 
58
- /** Genest schrijven (immutabel op elk niveau) + persist naar settings.json. */
58
+ /** True for path segments that would mutate Object.prototype. */
59
+ export function isUnsafeConfigKey(key: string): boolean {
60
+ return key === "__proto__" || key === "constructor" || key === "prototype";
61
+ }
62
+
63
+ /**
64
+ * Set `parts` (after the root key) on `root`. Returns false and leaves
65
+ * `root` unchanged when a segment is `__proto__`, `constructor`, or `prototype`.
66
+ */
67
+ export function assignNestedConfigValue(
68
+ root: Record<string, unknown>,
69
+ parts: string[],
70
+ value: ConfigValue,
71
+ ): boolean {
72
+ for (const part of parts) {
73
+ if (part === "__proto__" || part === "constructor" || part === "prototype") {
74
+ return false;
75
+ }
76
+ }
77
+ let node = root;
78
+ for (let i = 0; i < parts.length; i++) {
79
+ const key = parts[i]!;
80
+ // Guard in this loop so CodeQL sees the key check next to the assignment.
81
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
82
+ return false;
83
+ }
84
+ if (i === parts.length - 1) {
85
+ if (value === null) delete node[key];
86
+ else node[key] = value;
87
+ } else {
88
+ if (!isRecord(node[key])) node[key] = {};
89
+ node = node[key] as Record<string, unknown>;
90
+ }
91
+ }
92
+ return true;
93
+ }
94
+
95
+ /** Nested write (new object per level) + persist to settings.json. */
59
96
  export function writeConfigPath(
60
97
  cwd: string,
61
98
  path: string,
@@ -63,29 +100,20 @@ export function writeConfigPath(
63
100
  ): boolean {
64
101
  const parts = path.split(".");
65
102
  const rootKey = parts[0]!;
66
- // Weiger prototype-pollution keys (CodeQL).
67
- if (parts.some((p) => p === "__proto__" || p === "constructor" || p === "prototype")) {
103
+ if (!rootKey || parts.some(isUnsafeConfigKey)) {
68
104
  return false;
69
105
  }
70
106
  return writeSettingKey(cwd, rootKey, (existing) => {
71
- // Shorthand string onder powerline (bv. "chef") is een preset-naam: bewaar 'm.
72
- let node: Record<string, unknown> = isRecord(existing)
107
+ // Shorthand string under powerline (e.g. "chef") is a preset name: keep it.
108
+ const node: Record<string, unknown> = isRecord(existing)
73
109
  ? existing
74
110
  : rootKey === "powerline" && typeof existing === "string"
75
111
  ? { preset: existing }
76
112
  : {};
77
- const root = node;
78
- for (let i = 1; i < parts.length; i++) {
79
- const key = parts[i]!;
80
- if (i === parts.length - 1) {
81
- if (value === null) delete node[key];
82
- else node[key] = value;
83
- } else {
84
- if (!isRecord(node[key])) node[key] = {};
85
- node = node[key] as Record<string, unknown>;
86
- }
113
+ if (!assignNestedConfigValue(node, parts.slice(1), value)) {
114
+ return existing;
87
115
  }
88
- return root;
116
+ return node;
89
117
  });
90
118
  }
91
119