@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
|
@@ -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
|
-
|
|
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(
|
|
78
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
116
|
+
return node;
|
|
89
117
|
});
|
|
90
118
|
}
|
|
91
119
|
|