my-pi 0.1.10 → 0.1.12
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 +22 -2
- package/dist/{api-C7cSSbTg.js → api-CB0OXSW_.js} +179 -18
- package/dist/api-CB0OXSW_.js.map +1 -0
- package/dist/api.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +9 -9
- package/src/extensions/hooks-resolution/env.test.ts +41 -0
- package/src/extensions/hooks-resolution/env.ts +55 -0
- package/src/extensions/hooks-resolution/index.test.ts +99 -1
- package/src/extensions/hooks-resolution/index.ts +162 -47
- package/src/extensions/hooks-resolution/trust.test.ts +46 -0
- package/src/extensions/hooks-resolution/trust.ts +63 -0
- package/dist/api-C7cSSbTg.js.map +0 -1
package/README.md
CHANGED
|
@@ -313,8 +313,28 @@ HTTP MCP servers are supported too:
|
|
|
313
313
|
Use `"type": "http"` or `"type": "streamable-http"` for remote MCP
|
|
314
314
|
servers. If `url` is present, my-pi treats the entry as HTTP.
|
|
315
315
|
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
Global MCP config is loaded automatically. Project-local `mcp.json` is
|
|
317
|
+
untrusted by default; interactive sessions prompt before loading it
|
|
318
|
+
and headless sessions skip it unless `MY_PI_MCP_PROJECT_CONFIG=allow`
|
|
319
|
+
or `MY_PI_MCP_PROJECT_CONFIG=trust` is set. If both configs define the
|
|
320
|
+
same server name, the trusted project config wins.
|
|
321
|
+
|
|
322
|
+
### Hooks
|
|
323
|
+
|
|
324
|
+
Claude-style hooks are discovered from `.claude/settings.json`,
|
|
325
|
+
`.rulesync/hooks.json`, and `.pi/hooks.json`. Because hook commands
|
|
326
|
+
run through `bash -lc`, project hook config is untrusted by default.
|
|
327
|
+
Interactive sessions show the hook source files and commands before
|
|
328
|
+
allowing execution; headless sessions skip hooks unless
|
|
329
|
+
`MY_PI_HOOKS_CONFIG=allow` or `MY_PI_HOOKS_CONFIG=trust` is set.
|
|
330
|
+
Trusted hook approvals are remembered per project directory and
|
|
331
|
+
hook-config hash.
|
|
332
|
+
|
|
333
|
+
Hook commands receive a restricted child-process environment by
|
|
334
|
+
default: baseline shell variables plus `CLAUDE_PROJECT_DIR`. Use
|
|
335
|
+
`MY_PI_HOOKS_ENV_ALLOWLIST=NAME,OTHER_NAME` or the shared
|
|
336
|
+
`MY_PI_CHILD_ENV_ALLOWLIST` to pass selected ambient variables
|
|
337
|
+
through.
|
|
318
338
|
|
|
319
339
|
### Commands
|
|
320
340
|
|
|
@@ -13,11 +13,81 @@ import skills_extension, { create_skills_manager } from "@spences10/pi-skills";
|
|
|
13
13
|
import sqlite_tools_extension from "@spences10/pi-sqlite-tools";
|
|
14
14
|
import { create_telemetry_extension } from "@spences10/pi-telemetry";
|
|
15
15
|
import { spawn } from "node:child_process";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
18
|
import { Container, SettingsList, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
18
19
|
import { complete } from "@mariozechner/pi-ai";
|
|
20
|
+
//#region src/extensions/hooks-resolution/env.ts
|
|
21
|
+
const BASE_CHILD_ENV_KEYS = new Set([
|
|
22
|
+
"CI",
|
|
23
|
+
"COLORTERM",
|
|
24
|
+
"FORCE_COLOR",
|
|
25
|
+
"HOME",
|
|
26
|
+
"LANG",
|
|
27
|
+
"LOGNAME",
|
|
28
|
+
"NO_COLOR",
|
|
29
|
+
"PATH",
|
|
30
|
+
"SHELL",
|
|
31
|
+
"TEMP",
|
|
32
|
+
"TERM",
|
|
33
|
+
"TMP",
|
|
34
|
+
"TMPDIR",
|
|
35
|
+
"USER"
|
|
36
|
+
]);
|
|
37
|
+
const EXTRA_ENV_ALLOWLIST_KEYS = ["MY_PI_CHILD_ENV_ALLOWLIST", "MY_PI_HOOKS_ENV_ALLOWLIST"];
|
|
38
|
+
function create_child_process_env(explicit_env = {}, source_env = process.env) {
|
|
39
|
+
const env = {};
|
|
40
|
+
const allowed_keys = new Set(BASE_CHILD_ENV_KEYS);
|
|
41
|
+
for (const key of Object.keys(source_env)) if (key.startsWith("LC_")) allowed_keys.add(key);
|
|
42
|
+
for (const allowlist_key of EXTRA_ENV_ALLOWLIST_KEYS) for (const key of parse_env_allowlist(source_env[allowlist_key])) allowed_keys.add(key);
|
|
43
|
+
for (const key of allowed_keys) {
|
|
44
|
+
const value = source_env[key];
|
|
45
|
+
if (typeof value === "string") env[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
...env,
|
|
49
|
+
...explicit_env
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function parse_env_allowlist(value) {
|
|
53
|
+
if (!value) return [];
|
|
54
|
+
return value.split(",").map((key) => key.trim()).filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/extensions/hooks-resolution/trust.ts
|
|
58
|
+
function default_hooks_trust_store_path() {
|
|
59
|
+
return join(homedir(), ".pi", "agent", "trusted-hooks.json");
|
|
60
|
+
}
|
|
61
|
+
function is_hooks_config_trusted(project_dir, hash, trust_store_path = default_hooks_trust_store_path()) {
|
|
62
|
+
return read_trusted_hooks(trust_store_path)[project_dir]?.hash === hash;
|
|
63
|
+
}
|
|
64
|
+
function trust_hooks_config(project_dir, hash, trust_store_path = default_hooks_trust_store_path()) {
|
|
65
|
+
const trusted_hooks = read_trusted_hooks(trust_store_path);
|
|
66
|
+
trusted_hooks[project_dir] = {
|
|
67
|
+
project_dir,
|
|
68
|
+
hash,
|
|
69
|
+
trusted_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
70
|
+
};
|
|
71
|
+
mkdirSync(dirname(trust_store_path), { recursive: true });
|
|
72
|
+
writeFileSync(trust_store_path, JSON.stringify(trusted_hooks, null, " ") + "\n", {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
mode: 384
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function read_trusted_hooks(trust_store_path) {
|
|
78
|
+
if (!existsSync(trust_store_path)) return {};
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(trust_store_path, "utf-8");
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
19
88
|
//#region src/extensions/hooks-resolution/index.ts
|
|
20
89
|
const HOOK_TIMEOUT_MS = 600 * 1e3;
|
|
90
|
+
const HOOKS_CONFIG_ENV = "MY_PI_HOOKS_CONFIG";
|
|
21
91
|
function is_file(path) {
|
|
22
92
|
try {
|
|
23
93
|
return statSync(path).isFile();
|
|
@@ -134,20 +204,47 @@ function parse_simple_hooks_file(config, source, project_dir) {
|
|
|
134
204
|
}
|
|
135
205
|
return hooks;
|
|
136
206
|
}
|
|
207
|
+
function hook_config_paths(project_dir) {
|
|
208
|
+
return [
|
|
209
|
+
join(project_dir, ".claude", "settings.json"),
|
|
210
|
+
join(project_dir, ".rulesync", "hooks.json"),
|
|
211
|
+
join(project_dir, ".pi", "hooks.json")
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
function parse_hooks_config_file(path, project_dir) {
|
|
215
|
+
const config = read_json_file(path);
|
|
216
|
+
if (config === void 0) return [];
|
|
217
|
+
if (path.endsWith(join(".claude", "settings.json"))) return parse_claude_settings_hooks(config, path, project_dir);
|
|
218
|
+
return parse_simple_hooks_file(config, path, project_dir);
|
|
219
|
+
}
|
|
137
220
|
function load_hooks(cwd) {
|
|
138
221
|
const project_dir = find_project_dir(cwd);
|
|
139
|
-
const hooks = [];
|
|
140
|
-
const claude_settings_path = join(project_dir, ".claude", "settings.json");
|
|
141
|
-
const rulesync_hooks_path = join(project_dir, ".rulesync", "hooks.json");
|
|
142
|
-
const pi_hooks_path = join(project_dir, ".pi", "hooks.json");
|
|
143
|
-
const claude_settings = read_json_file(claude_settings_path);
|
|
144
|
-
if (claude_settings !== void 0) hooks.push(...parse_claude_settings_hooks(claude_settings, claude_settings_path, project_dir));
|
|
145
|
-
const rulesync_hooks = read_json_file(rulesync_hooks_path);
|
|
146
|
-
if (rulesync_hooks !== void 0) hooks.push(...parse_simple_hooks_file(rulesync_hooks, rulesync_hooks_path, project_dir));
|
|
147
|
-
const pi_hooks = read_json_file(pi_hooks_path);
|
|
148
|
-
if (pi_hooks !== void 0) hooks.push(...parse_simple_hooks_file(pi_hooks, pi_hooks_path, project_dir));
|
|
149
222
|
return {
|
|
150
223
|
project_dir,
|
|
224
|
+
hooks: hook_config_paths(project_dir).flatMap((path) => parse_hooks_config_file(path, project_dir))
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function get_hooks_config_info(cwd) {
|
|
228
|
+
const project_dir = find_project_dir(cwd);
|
|
229
|
+
const sources = hook_config_paths(project_dir).filter(is_file);
|
|
230
|
+
if (sources.length === 0) return void 0;
|
|
231
|
+
const hash = createHash("sha256");
|
|
232
|
+
for (const source of sources) {
|
|
233
|
+
hash.update(source);
|
|
234
|
+
hash.update("\0");
|
|
235
|
+
hash.update(readFileSync(source, "utf8"));
|
|
236
|
+
hash.update("\0");
|
|
237
|
+
}
|
|
238
|
+
const hooks = sources.flatMap((source) => parse_hooks_config_file(source, project_dir)).map((hook) => ({
|
|
239
|
+
event_name: hook.event_name,
|
|
240
|
+
matcher_text: hook.matcher_text,
|
|
241
|
+
command: hook.command,
|
|
242
|
+
source: hook.source
|
|
243
|
+
}));
|
|
244
|
+
return {
|
|
245
|
+
project_dir,
|
|
246
|
+
hash: hash.digest("hex"),
|
|
247
|
+
sources,
|
|
151
248
|
hooks
|
|
152
249
|
};
|
|
153
250
|
}
|
|
@@ -216,10 +313,7 @@ async function run_command_hook(command, cwd, payload) {
|
|
|
216
313
|
const started_at = Date.now();
|
|
217
314
|
const child = spawn("bash", ["-lc", command], {
|
|
218
315
|
cwd,
|
|
219
|
-
env: {
|
|
220
|
-
...process.env,
|
|
221
|
-
CLAUDE_PROJECT_DIR: cwd
|
|
222
|
-
},
|
|
316
|
+
env: create_child_process_env({ CLAUDE_PROJECT_DIR: cwd }),
|
|
223
317
|
stdio: [
|
|
224
318
|
"pipe",
|
|
225
319
|
"pipe",
|
|
@@ -284,6 +378,66 @@ function hook_name(command) {
|
|
|
284
378
|
if (sh_path_match) return basename(sh_path_match[0]);
|
|
285
379
|
return basename(command.trim().split(/\s+/)[0] ?? "hook");
|
|
286
380
|
}
|
|
381
|
+
function normalize_hooks_config_decision(value) {
|
|
382
|
+
const normalized = value?.trim().toLowerCase();
|
|
383
|
+
if (!normalized) return void 0;
|
|
384
|
+
if ([
|
|
385
|
+
"1",
|
|
386
|
+
"true",
|
|
387
|
+
"yes",
|
|
388
|
+
"allow"
|
|
389
|
+
].includes(normalized)) return "allow";
|
|
390
|
+
if (normalized === "trust") return "trust";
|
|
391
|
+
if ([
|
|
392
|
+
"0",
|
|
393
|
+
"false",
|
|
394
|
+
"no",
|
|
395
|
+
"skip",
|
|
396
|
+
"disable"
|
|
397
|
+
].includes(normalized)) return "skip";
|
|
398
|
+
}
|
|
399
|
+
function format_hooks_config_prompt(info) {
|
|
400
|
+
const source_lines = info.sources.map((source) => `- ${source}`);
|
|
401
|
+
const hook_lines = info.hooks.length === 0 ? ["- no valid command hooks detected"] : info.hooks.map((hook) => {
|
|
402
|
+
const matcher = hook.matcher_text ? ` matcher=${hook.matcher_text}` : "";
|
|
403
|
+
return `- ${hook.event_name}${matcher}: ${hook.command}`;
|
|
404
|
+
});
|
|
405
|
+
return [
|
|
406
|
+
"Project hook config can execute shell commands after tool use. Trust these hooks?",
|
|
407
|
+
`Project: ${info.project_dir}`,
|
|
408
|
+
`SHA-256: ${info.hash}`,
|
|
409
|
+
"Sources:",
|
|
410
|
+
...source_lines,
|
|
411
|
+
"Commands:",
|
|
412
|
+
...hook_lines
|
|
413
|
+
].join("\n");
|
|
414
|
+
}
|
|
415
|
+
async function should_load_hooks_config(cwd, ctx) {
|
|
416
|
+
const info = get_hooks_config_info(cwd);
|
|
417
|
+
if (!info) return true;
|
|
418
|
+
if (is_hooks_config_trusted(info.project_dir, info.hash)) return true;
|
|
419
|
+
const env_decision = normalize_hooks_config_decision(process.env[HOOKS_CONFIG_ENV]);
|
|
420
|
+
if (env_decision === "trust") {
|
|
421
|
+
trust_hooks_config(info.project_dir, info.hash);
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
if (env_decision === "allow") return true;
|
|
425
|
+
if (env_decision === "skip") return false;
|
|
426
|
+
if (!ctx?.hasUI) {
|
|
427
|
+
console.warn(`Skipping untrusted hook config in ${info.project_dir}. Set ${HOOKS_CONFIG_ENV}=allow to enable hooks for this run.`);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
const choice = await ctx.ui.select(format_hooks_config_prompt(info), [
|
|
431
|
+
"Allow once for this session",
|
|
432
|
+
"Trust this repo until hook config changes",
|
|
433
|
+
"Skip project hooks"
|
|
434
|
+
]);
|
|
435
|
+
if (choice === "Trust this repo until hook config changes") {
|
|
436
|
+
trust_hooks_config(info.project_dir, info.hash);
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
return choice === "Allow once for this session";
|
|
440
|
+
}
|
|
287
441
|
function create_hooks_resolution_extension(options = {}) {
|
|
288
442
|
const load_hooks_impl = options.load_hooks ?? load_hooks;
|
|
289
443
|
const run_command_hook_impl = options.run_command_hook ?? run_command_hook;
|
|
@@ -292,11 +446,18 @@ function create_hooks_resolution_extension(options = {}) {
|
|
|
292
446
|
project_dir: process.cwd(),
|
|
293
447
|
hooks: []
|
|
294
448
|
};
|
|
295
|
-
const refresh_hooks = (cwd) => {
|
|
449
|
+
const refresh_hooks = async (cwd, ctx) => {
|
|
450
|
+
if (!await should_load_hooks_config(cwd, ctx)) {
|
|
451
|
+
state = {
|
|
452
|
+
project_dir: cwd,
|
|
453
|
+
hooks: []
|
|
454
|
+
};
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
296
457
|
state = load_hooks_impl(cwd);
|
|
297
458
|
};
|
|
298
|
-
pi.on("session_start", (_event, ctx) => {
|
|
299
|
-
refresh_hooks(ctx.cwd);
|
|
459
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
460
|
+
await refresh_hooks(ctx.cwd, ctx);
|
|
300
461
|
});
|
|
301
462
|
pi.on("tool_result", async (event, ctx) => {
|
|
302
463
|
if (state.hooks.length === 0) return;
|
|
@@ -1958,4 +2119,4 @@ async function create_my_pi(options = {}) {
|
|
|
1958
2119
|
//#endregion
|
|
1959
2120
|
export { runPrintMode$1 as i, create_my_pi as n, get_force_disabled_builtins as r, InteractiveMode$1 as t };
|
|
1960
2121
|
|
|
1961
|
-
//# sourceMappingURL=api-
|
|
2122
|
+
//# sourceMappingURL=api-CB0OXSW_.js.map
|