@minidoll/agentkit-dsh-plugin 0.0.0-stage → 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,667 @@
1
+ // src/index.ts
2
+ import { appendFileSync, mkdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
5
+
6
+ // ../core/dist/config.js
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ var CONFIG_CANDIDATES = [".agent/hooks/hooks.json", ".claude/settings.json", ".claude/hooks.json"];
10
+ var DEFAULT_TIMEOUT_MS = 6e5;
11
+ function loadHooks(projectRoot) {
12
+ const errors = [];
13
+ for (const rel of CONFIG_CANDIDATES) {
14
+ const abs = join(projectRoot, rel);
15
+ if (!existsSync(abs))
16
+ continue;
17
+ let parsed;
18
+ try {
19
+ parsed = JSON.parse(readFileSync(abs, "utf8"));
20
+ } catch (err) {
21
+ errors.push(`${abs}: \u4E0D\u662F\u5408\u6CD5 JSON \u2014 ${err.message}`);
22
+ return { hooks: [], source: abs, errors };
23
+ }
24
+ return { ...parseEntries(parsed, rel), source: abs };
25
+ }
26
+ return { hooks: [], source: null, errors };
27
+ }
28
+ function parseEntries(doc, where) {
29
+ const errors = [];
30
+ const hooks = [];
31
+ const raw = doc?.hooks;
32
+ if (!Array.isArray(raw)) {
33
+ errors.push(`${where}: \u9876\u5C42\u7F3A\u5C11 hooks \u6570\u7EC4`);
34
+ return { hooks, errors };
35
+ }
36
+ for (const [i, entry] of raw.entries()) {
37
+ const e = entry;
38
+ const command = typeof e?.command === "string" ? e.command : "";
39
+ if (command === "") {
40
+ errors.push(`${where}: \u7B2C ${i + 1} \u6761\u7F3A command \u2014\u2014 \u8DF3\u8FC7`);
41
+ continue;
42
+ }
43
+ const event = e?.event === "PostToolUse" ? "PostToolUse" : "PreToolUse";
44
+ const matcher = e?.matcher?.tool;
45
+ hooks.push({
46
+ id: typeof e?.id === "string" ? e.id : `hook-${i + 1}`,
47
+ event,
48
+ ...typeof matcher === "string" ? { tool: matcher } : {},
49
+ command,
50
+ timeoutMs: typeof e?.timeoutMs === "number" ? e.timeoutMs : DEFAULT_TIMEOUT_MS
51
+ });
52
+ }
53
+ return { hooks, errors };
54
+ }
55
+ function matches(hook, toolName) {
56
+ if (hook.tool === void 0)
57
+ return true;
58
+ try {
59
+ return new RegExp(hook.tool, "i").test(toolName);
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+ function exitCodeBlocks(hook, exitCode) {
65
+ return hook.event === "PreToolUse" && exitCode !== 0;
66
+ }
67
+
68
+ // ../core/dist/match-project.js
69
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "node:fs";
70
+ import { join as join2 } from "node:path";
71
+ function isProject(dir) {
72
+ return existsSync2(join2(dir, ".agent", "protocol.yml")) || existsSync2(join2(dir, ".agent", "board.json"));
73
+ }
74
+ function declaredName(dir) {
75
+ try {
76
+ const text = readFileSync2(join2(dir, ".agent", "protocol.yml"), "utf8");
77
+ const m = /^meta:\s*\n(?:[\s\S]*?)\s+name:\s*([A-Za-z0-9._-]+)/m.exec(text);
78
+ return m?.[1];
79
+ } catch {
80
+ return void 0;
81
+ }
82
+ }
83
+ function listProjects(containerDir, maxChildren = 64) {
84
+ if (!existsSync2(containerDir))
85
+ return [];
86
+ let names = [];
87
+ try {
88
+ names = readdirSync(containerDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).slice(0, maxChildren);
89
+ } catch {
90
+ return [];
91
+ }
92
+ return names.map((name) => {
93
+ const dir = join2(containerDir, name);
94
+ if (!isProject(dir))
95
+ return null;
96
+ const declared = declaredName(dir);
97
+ return { name, dir, ...declared !== void 0 ? { declaredName: declared } : {} };
98
+ }).filter((x) => x !== null);
99
+ }
100
+ function matchProjectByName(text, containerDir) {
101
+ const all = listProjects(containerDir);
102
+ if (all.length === 0) {
103
+ return { root: null, matched: [], scanned: all, reason: `${containerDir} \u4E0B\u6CA1\u6709\u5E26 .agent/ \u7684\u9879\u76EE` };
104
+ }
105
+ if (text.trim() === "") {
106
+ return { root: null, matched: [], scanned: all, reason: "\u6CA1\u6709\u53EF\u5339\u914D\u7684\u6587\u672C" };
107
+ }
108
+ const matched = all.filter((p) => {
109
+ const names = [p.name, ...p.declaredName !== void 0 ? [p.declaredName] : []];
110
+ return names.some((n) => new RegExp(`(^|[^A-Za-z0-9_-])${escapeRe(n)}([^A-Za-z0-9_-]|$)`).test(text));
111
+ });
112
+ if (matched.length === 1) {
113
+ return { root: matched[0].dir, matched, scanned: all, reason: `\u6587\u672C\u63D0\u5230\u9879\u76EE ${matched[0].name}` };
114
+ }
115
+ if (matched.length === 0) {
116
+ return { root: null, matched: [], scanned: all, reason: "\u6587\u672C\u6CA1\u6709\u63D0\u5230\u4EFB\u4F55\u5DF2\u77E5\u9879\u76EE" };
117
+ }
118
+ return {
119
+ root: null,
120
+ matched,
121
+ scanned: all,
122
+ reason: `\u6587\u672C\u540C\u65F6\u63D0\u5230\u591A\u4E2A\u9879\u76EE\uFF08${matched.map((m) => m.name).join(", ")}\uFF09\u2014\u2014 \u4E0D\u9009`
123
+ };
124
+ }
125
+ function escapeRe(s) {
126
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
+ }
128
+
129
+ // ../core/dist/resolve.js
130
+ import { existsSync as existsSync3, statSync } from "node:fs";
131
+ import { dirname, isAbsolute, join as join3 } from "node:path";
132
+ var WORKSPACE_MARKERS = [".agent"];
133
+ function resolveProjectRoot(opts) {
134
+ const sessionCwd = opts.sessionCwd;
135
+ const raw = opts.targetPath;
136
+ if (raw === void 0 || raw.trim() === "")
137
+ return sessionCwd;
138
+ const abs = isAbsolute(raw) ? raw : join3(sessionCwd, raw);
139
+ let dir = abs;
140
+ try {
141
+ if (!statSync(abs).isDirectory())
142
+ dir = dirname(abs);
143
+ } catch {
144
+ dir = dirname(abs);
145
+ }
146
+ const limit = opts.maxDepth ?? 24;
147
+ const home = process.env.HOME ?? "";
148
+ for (let i = 0; i < limit; i++) {
149
+ if (home !== "" && dir === home)
150
+ break;
151
+ if (dir === dirname(dir))
152
+ break;
153
+ if (WORKSPACE_MARKERS.some((m) => existsSync3(join3(dir, m)))) {
154
+ return dir;
155
+ }
156
+ dir = dirname(dir);
157
+ }
158
+ return sessionCwd;
159
+ }
160
+ function findProjectRoot(candidates, sessionCwd) {
161
+ const tried = [];
162
+ for (const cand of candidates) {
163
+ const resolved = resolveProjectRoot({ targetPath: cand, sessionCwd });
164
+ const hit = existsSync3(join3(resolved, ".agent"));
165
+ tried.push({ path: cand, resolved, hit });
166
+ if (hit)
167
+ return { root: resolved, tried };
168
+ }
169
+ return { root: null, tried };
170
+ }
171
+
172
+ // src/inject.ts
173
+ import { randomUUID } from "node:crypto";
174
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "node:fs";
175
+ import { join as join4 } from "node:path";
176
+ var MAX_CONTEXT_BYTES = 900;
177
+ var factoryLoader = null;
178
+ async function loadFactory() {
179
+ factoryLoader ??= (async () => {
180
+ try {
181
+ const pkg = "@deepseek-ai/dsh-llm";
182
+ const mod = await import(pkg);
183
+ return typeof mod.createUserMessage === "function" ? mod.createUserMessage : null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ })();
188
+ return factoryLoader;
189
+ }
190
+ function handcraft(text) {
191
+ return {
192
+ role: "user",
193
+ id: randomUUID(),
194
+ content: [{ type: "text", text }],
195
+ source: { kind: "user" }
196
+ };
197
+ }
198
+ function assertUserMessage(value) {
199
+ if (typeof value !== "object" || value === null) return false;
200
+ const m = value;
201
+ if (m.role !== "user") return false;
202
+ if (typeof m.id !== "string" || m.id === "") return false;
203
+ const src = m.source;
204
+ if (src === void 0 || src.kind !== "user") return false;
205
+ const content = m.content;
206
+ if (!Array.isArray(content) || content.length === 0) return false;
207
+ return content.every((b) => {
208
+ const blk = b;
209
+ return blk.type === "text" && typeof blk.text === "string" && blk.text.length > 0;
210
+ });
211
+ }
212
+ var INJECT_CONFIG_REL = ".agent/inject.json";
213
+ function injectEnabled(root) {
214
+ if (process.env.AGENTKIT_CONTEXT_INJECT === "1") return true;
215
+ if (root === void 0 || root === "") return false;
216
+ try {
217
+ const f = join4(root, INJECT_CONFIG_REL);
218
+ if (!existsSync4(f)) return false;
219
+ const doc = JSON.parse(readFileSync3(f, "utf8"));
220
+ return doc.enabled === true;
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ var injected = /* @__PURE__ */ new Set();
226
+ var PATH_RULES = [
227
+ { pattern: /^contracts\//, hint: "\u5951\u7EA6\u53D8\u66F4\uFF1A\u5B9E\u73B0\u5FC5\u987B\u540C\u6B65\uFF08AGENTS \u94C1\u5F8B\uFF09\uFF0C\u5E76\u5728\u4EA4\u63A5\u5355\u58F0\u660E\u5951\u7EA6\u5F71\u54CD\u4E0E\u8BC1\u636E\u7EA7\u522B L1-L4" },
228
+ { pattern: /^docs\/rules\//, hint: "\u89C4\u5219\u6587\u6863\uFF1A\u6539\u5B83\u7B49\u4E8E\u6539\u6240\u6709\u4EBA\u7684\u884C\u4E3A\u7EA6\u675F \u2014\u2014 \u5148\u786E\u8BA4\u4E0E protocol.yml / AGENTS.md \u4E0D\u51B2\u7A81" },
229
+ { pattern: /^docs\/design\//, hint: '\u8BBE\u8BA1\u6587\u6863\uFF1A\u9700\u4E0E codemap \u91CC\u7684\u6A21\u5757\u5BF9\u5E94\uFF08M4 \u4F1A\u6821\u9A8C"\u6587\u6863\u63D0\u5230\u8BE5\u6A21\u5757"\uFF09' },
230
+ { pattern: /^\.agent\/decisions\/\d{4}-/, hint: "ADR\uFF1Aaccepted \u7684\u6709\u7EA6\u675F\u529B \u2014\u2014 \u8981\u63A8\u7FFB\u5148\u5199\u65B0 ADR \u6807 superseded\uFF0C\u4E0D\u8981\u76F4\u63A5\u6539\u65E7\u7ED3\u8BBA" },
231
+ { pattern: /^AGENTS\.md$/, hint: "AGENTS.md \u662F\u6D3E\u751F\u6587\u4EF6\uFF08\u7531 protocol.yml \u751F\u6210\uFF09\u2014\u2014 \u6539\u5B83\u8BF7\u6539 protocol.yml \u518D akit sync" }
232
+ ];
233
+ function buildInjectText(input) {
234
+ const lines = [];
235
+ const rels = [];
236
+ if (input.filePath !== void 0 && input.filePath !== "") rels.push(input.filePath);
237
+ for (const p of input.extraPaths ?? []) if (p !== "") rels.push(p);
238
+ for (const raw of rels) {
239
+ const rel = String(raw).replace(/\\/g, "/").replace(/^\.\//, "");
240
+ for (const rule of PATH_RULES) {
241
+ if (rule.pattern.test(rel)) {
242
+ lines.push(`- \u7EAA\u5F8B\uFF1A${rule.hint}`);
243
+ break;
244
+ }
245
+ }
246
+ if (lines.length > 0) break;
247
+ }
248
+ for (const raw of rels) {
249
+ const rel = String(raw).replace(/\\/g, "/").replace(/^\.\//, "");
250
+ try {
251
+ const cm = join4(input.root, ".agent", "codemap.json");
252
+ if (!existsSync4(cm)) break;
253
+ const map = JSON.parse(readFileSync3(cm, "utf8"));
254
+ const mod = (map.modules ?? []).find((m) => String(m.path ?? "").replace(/^\.\//, "") === rel);
255
+ if (mod !== void 0) {
256
+ if (mod.purpose !== void 0 && mod.purpose !== "") lines.push(`- \u6A21\u5757\u804C\u8D23\uFF08codemap\uFF09\uFF1A${mod.purpose}`);
257
+ if (mod.doc !== void 0 && mod.doc !== "") lines.push(`- \u8BBE\u8BA1\u6587\u6863\uFF1A${mod.doc}`);
258
+ break;
259
+ }
260
+ } catch {
261
+ }
262
+ }
263
+ for (const raw of rels) {
264
+ if (lines.length >= 4) break;
265
+ const rel = String(raw).replace(/\\/g, "/");
266
+ const stem = rel.split("/").pop()?.replace(/\.(ts|mjs|java|kt|py|go|md)$/, "") ?? "";
267
+ if (stem.length < 3) continue;
268
+ try {
269
+ const dir = join4(input.root, ".agent", "decisions");
270
+ if (!existsSync4(dir)) continue;
271
+ for (const name of readdirSync2(dir).sort()) {
272
+ if (!/^\d{4}-.*\.md$/.test(name)) continue;
273
+ const body = readFileSync3(join4(dir, name), "utf8");
274
+ if (!body.includes(stem) && !body.includes(rel)) continue;
275
+ const title = /^#\s+(.+)$/m.exec(body)?.[1] ?? name;
276
+ const status = /-?\s*\*\*状态\*\*[::]\s*(.+)/.exec(body)?.[1] ?? "";
277
+ lines.push(`- \u76F8\u5173\u51B3\u7B56\uFF1A${name.replace(/\.md$/, "")} ${title}${status ? `\uFF08${status}\uFF09` : ""}`);
278
+ if (lines.length >= 4) break;
279
+ }
280
+ } catch {
281
+ }
282
+ }
283
+ if (lines.length === 0) return null;
284
+ let text = ["<!-- agentkit: \u6309\u9700\u4E0A\u4E0B\u6587\uFF08\u53EA\u8BFB\uFF0C\u6765\u81EA codemap/ADR\uFF09 -->", ...lines].join("\n");
285
+ if (text.length > MAX_CONTEXT_BYTES) text = `${text.slice(0, MAX_CONTEXT_BYTES - 20)}
286
+ \u2026\uFF08\u5DF2\u622A\u65AD\uFF09`;
287
+ return text;
288
+ }
289
+ async function buildAdditionalContexts(input) {
290
+ try {
291
+ if (!injectEnabled(input.root)) {
292
+ input.trace?.("inject skipped: disabled (no .agent/inject.json / env)");
293
+ return { ok: false, reason: "inject disabled (no .agent/inject.json / env)" };
294
+ }
295
+ const key = `${input.sessionId ?? ""}|${input.filePath ?? ""}`;
296
+ if (injected.has(key)) {
297
+ input.trace?.("inject skipped: already injected for this session+file");
298
+ return { ok: false, reason: "already injected for this session+file" };
299
+ }
300
+ const text = buildInjectText({
301
+ root: input.root,
302
+ ...input.filePath !== void 0 ? { filePath: input.filePath } : {},
303
+ ...input.extraPaths !== void 0 ? { extraPaths: input.extraPaths } : {}
304
+ });
305
+ if (text === null) {
306
+ input.trace?.("inject skipped: nothing to inject for this path");
307
+ return { ok: false, reason: "nothing to inject for this path" };
308
+ }
309
+ const factory = await loadFactory();
310
+ const msg = factory !== null ? factory({ content: [{ type: "text", text }], source: { kind: "user" } }) : handcraft(text);
311
+ if (!assertUserMessage(msg)) {
312
+ input.trace?.(`inject rejected: shape check failed (${factory !== null ? "factory" : "handcraft"})`);
313
+ return { ok: false, reason: "shape check failed" };
314
+ }
315
+ injected.add(key);
316
+ input.trace?.(
317
+ `inject ok: ${String(text.length)}B via ${factory !== null ? "factory" : "handcraft"}`
318
+ );
319
+ return { ok: true, messages: [msg] };
320
+ } catch (err) {
321
+ input.trace?.(`inject skipped: ${err.message}`);
322
+ return { ok: false, reason: err.message };
323
+ }
324
+ }
325
+
326
+ // src/payload.ts
327
+ import { existsSync as existsSync5 } from "node:fs";
328
+ import { dirname as dirname2, join as join5 } from "node:path";
329
+ function buildPayload(input) {
330
+ return {
331
+ event: input.event,
332
+ hookId: input.hookId,
333
+ toolName: input.toolName,
334
+ args: input.args,
335
+ sessionId: input.sessionId,
336
+ cwd: input.cwd ?? null
337
+ };
338
+ }
339
+ function payloadStdin(payload) {
340
+ return `${JSON.stringify(payload)}
341
+ `;
342
+ }
343
+ function hookEnv(cwd, hookId, sessionId) {
344
+ const nodeDir = nodeBinDir() ?? dirname2(process.execPath);
345
+ const hostPath = process.env.PATH;
346
+ return {
347
+ CLAUDE_PROJECT_DIR: cwd,
348
+ DSH_HOOK_ID: hookId,
349
+ DSH_SESSION_ID: sessionId ?? "",
350
+ // PATH = node 所在目录 + 宿主 PATH(node 目录放最前,保证 `node` 可解析)
351
+ PATH: [nodeDir, hostPath ?? "/usr/local/bin:/usr/bin:/bin"].filter((p) => p !== "").join(":")
352
+ };
353
+ }
354
+ function nodeBinDir() {
355
+ const candidates = [dirname2(process.execPath)];
356
+ for (const dir of (process.env.PATH ?? "").split(":")) if (dir !== "") candidates.push(dir);
357
+ candidates.push("/usr/local/bin", "/usr/bin", "/bin");
358
+ for (const dir of candidates) {
359
+ try {
360
+ if (existsSync5(join5(dir, "node"))) return dir;
361
+ } catch {
362
+ }
363
+ }
364
+ return null;
365
+ }
366
+
367
+ // src/execute.ts
368
+ import { spawnSync } from "node:child_process";
369
+ async function runHookCommand(req, shell) {
370
+ if (shell !== void 0 && typeof shell.run === "function" && typeof shell.resolve === "function") {
371
+ try {
372
+ const resolved = shell.resolve({
373
+ command: req.command,
374
+ workdir: req.cwd,
375
+ timeoutMs: req.timeoutMs,
376
+ stdin: req.stdin,
377
+ env: req.env
378
+ });
379
+ const r2 = await shell.run(resolved);
380
+ return { exitCode: r2.exitCode };
381
+ } catch (err) {
382
+ return { exitCode: 1, error: `ctx.shell failure: ${err.message}` };
383
+ }
384
+ }
385
+ const r = spawnSync("sh", ["-c", req.command], {
386
+ cwd: req.cwd,
387
+ timeout: req.timeoutMs,
388
+ input: req.stdin,
389
+ env: { ...process.env, ...req.env },
390
+ encoding: "utf8"
391
+ });
392
+ return { exitCode: r.status ?? 1, ...r.status === null ? { error: `signal: ${String(r.signal)}` } : {} };
393
+ }
394
+ function safeShell(ctx) {
395
+ try {
396
+ const shell = ctx?.shell;
397
+ return shell !== void 0 && typeof shell.run === "function" && typeof shell.resolve === "function" ? shell : void 0;
398
+ } catch {
399
+ return void 0;
400
+ }
401
+ }
402
+
403
+ // src/index.ts
404
+ function extractCandidatePaths(exec) {
405
+ const args = exec.arguments;
406
+ if (args === void 0 || args === null || typeof args !== "object") return [];
407
+ const out = [];
408
+ for (const key of ["file_path", "path", "notebook_path", "target_file"]) {
409
+ const v = args[key];
410
+ if (typeof v === "string" && v !== "") out.push(v);
411
+ }
412
+ const cmd = typeof args.command === "string" ? args.command : "";
413
+ if (cmd === "") return out;
414
+ const cdTargets = [];
415
+ const cdRe = /\bcd\s+([^\s;&|'"]+)/g;
416
+ let m;
417
+ while ((m = cdRe.exec(cmd)) !== null) {
418
+ const raw = m[1];
419
+ cdTargets.push(isAbsolute2(raw) ? raw : join6(process.cwd(), raw));
420
+ }
421
+ const absTargets = [];
422
+ const absRe = /(?<=^|[\s'"=(])(\/[^\s'";&|)]+)/g;
423
+ while ((m = absRe.exec(cmd)) !== null) {
424
+ const raw = m[1];
425
+ if (!absTargets.includes(raw)) absTargets.push(raw);
426
+ }
427
+ for (const t of [...out, ...cdTargets, ...absTargets]) {
428
+ if (!out.includes(t)) out.push(t);
429
+ }
430
+ return [...new Set(out)];
431
+ }
432
+ function trace(dshHome, line) {
433
+ try {
434
+ mkdirSync(dshHome, { recursive: true });
435
+ appendFileSync(join6(dshHome, ".agentkit-plugin.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
436
+ `, "utf8");
437
+ } catch {
438
+ }
439
+ }
440
+ var ALLOW = { kind: "allow" };
441
+ var PLUGIN_MARK = "v2";
442
+ function observeOnly() {
443
+ return process.env.AGENTKIT_HOOKS_ENFORCE !== "1";
444
+ }
445
+ var RECENT_CAP = 20;
446
+ function createProjectHooksPlugin(ctx) {
447
+ const state = { runs: [] };
448
+ const shell = safeShell(ctx);
449
+ const note = (text) => {
450
+ try {
451
+ ctx.write?.(text);
452
+ } catch {
453
+ }
454
+ };
455
+ const record = (run) => {
456
+ state.runs = [run, ...state.runs].slice(0, RECENT_CAP);
457
+ };
458
+ const recordEvent = (root, event) => {
459
+ try {
460
+ const dir = join6(root, ".agent", "audit");
461
+ mkdirSync(dir, { recursive: true });
462
+ const shard = process.env.AGENTKIT_EVENT_SHARD ?? `pid-${process.pid}`;
463
+ const file = join6(dir, `events.${String(shard).replace(/[^A-Za-z0-9._-]/g, "_")}.jsonl`);
464
+ appendFileSync(file, `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...event })}
465
+ `, "utf8");
466
+ } catch (err) {
467
+ trace(
468
+ process.env.DSH_HOME ?? join6(homedir(), ".dsh"),
469
+ `event write skipped: ${err.message}`
470
+ );
471
+ }
472
+ };
473
+ const recordPersistent = (root, run) => {
474
+ try {
475
+ const dir = join6(root, ".agent", "audit");
476
+ mkdirSync(dir, { recursive: true });
477
+ const row = {
478
+ ts: new Date(run.at).toISOString(),
479
+ hookId: run.hookId,
480
+ event: run.event,
481
+ tool: run.toolName,
482
+ exitCode: run.exitCode,
483
+ client: "dsh",
484
+ ...run.sessionId !== void 0 && run.sessionId !== null ? { sessionId: run.sessionId } : {},
485
+ ...run.skill !== void 0 ? { skill: run.skill } : {},
486
+ ...run.error !== void 0 ? { error: run.error } : {}
487
+ };
488
+ appendFileSync(join6(dir, "hooks.jsonl"), `${JSON.stringify(row)}
489
+ `, "utf8");
490
+ } catch (err) {
491
+ trace(
492
+ process.env.DSH_HOME ?? join6(homedir(), ".dsh"),
493
+ `audit write skipped: ${err.message}`
494
+ );
495
+ }
496
+ };
497
+ const tryInjectContext = async (exec) => {
498
+ try {
499
+ const sessionCwd = exec.agent?.session?.header?.cwd ?? process.cwd();
500
+ const candidates = extractCandidatePaths(exec);
501
+ const found = findProjectRoot(candidates, sessionCwd);
502
+ if (found.root === null) return null;
503
+ const root = found.root;
504
+ const inside = candidates.filter((c) => {
505
+ const abs = isAbsolute2(c) ? c : join6(sessionCwd, c);
506
+ return abs === root || abs.startsWith(root + "/");
507
+ });
508
+ const filePath = inside[0];
509
+ const sessionId = exec.agent?.session?.id ?? null;
510
+ const r = await buildAdditionalContexts({
511
+ root,
512
+ ...filePath !== void 0 ? { filePath } : {},
513
+ ...sessionId !== null ? { sessionId } : {},
514
+ trace: (line) => trace(process.env.DSH_HOME ?? join6(homedir(), ".dsh"), line)
515
+ });
516
+ return r.ok && r.messages !== void 0 ? r.messages : null;
517
+ } catch (err) {
518
+ trace(
519
+ process.env.DSH_HOME ?? join6(homedir(), ".dsh"),
520
+ `inject outer failed (ignored): ${err.message}`
521
+ );
522
+ return null;
523
+ }
524
+ };
525
+ const handle = async (event, exec) => {
526
+ try {
527
+ const decision = await runHooks(event, exec);
528
+ if (event === "PostToolUse" && decision.kind === "allow") {
529
+ const injected2 = await tryInjectContext(exec);
530
+ if (injected2 !== null) return { kind: "accept", additionalContexts: injected2 };
531
+ }
532
+ return decision;
533
+ } catch (err) {
534
+ trace(process.env.DSH_HOME ?? join6(homedir(), ".dsh"), `internal error (allowing call): ${err.message}`);
535
+ note(`project-hooks: internal error (allowing call): ${err.message}
536
+ `);
537
+ return ALLOW;
538
+ }
539
+ };
540
+ const runHooks = async (event, exec) => {
541
+ const sessionCwd = exec.agent?.session?.header?.cwd ?? process.cwd();
542
+ const candidates = extractCandidatePaths(exec);
543
+ const targetPath = candidates[0];
544
+ const found = findProjectRoot(candidates, sessionCwd);
545
+ const cwd = found.root ?? sessionCwd;
546
+ trace(
547
+ process.env.DSH_HOME ?? join6(homedir(), ".dsh"),
548
+ found.root !== null ? `root resolve: hit ${found.root}\uFF08\u5019\u9009 ${String(candidates.length)} \u4E2A\uFF09` : `root resolve: miss\uFF08\u5019\u9009 ${candidates.length ? candidates.join(", ") : "(none)"}\uFF09\u2192 sessionCwd`
549
+ );
550
+ const sessionId = exec.agent?.session?.id ?? null;
551
+ let pendingSkillLoad = null;
552
+ const toolName = String(exec.name ?? "");
553
+ const args = exec.arguments ?? {};
554
+ trace(process.env.DSH_HOME ?? join6(homedir(), ".dsh"), `run ${event} ${toolName} root=${cwd}`);
555
+ if (event === "PostToolUse" && toolName === "skill") {
556
+ const asked = args.name;
557
+ pendingSkillLoad = {
558
+ hookId: "(skill-load)",
559
+ event: "PostToolUse",
560
+ toolName: "skill",
561
+ exitCode: 0,
562
+ at: Date.now(),
563
+ sessionId,
564
+ ...typeof asked === "string" ? { skill: asked } : {}
565
+ };
566
+ trace(process.env.DSH_HOME ?? join6(homedir(), ".dsh"), `skill-load ${typeof asked === "string" ? asked : "(unnamed)"}`);
567
+ }
568
+ const dshHome = process.env.DSH_HOME ?? join6(homedir(), ".dsh");
569
+ let effectiveCwd = cwd;
570
+ let hookLoad = loadHooks(effectiveCwd);
571
+ if (hookLoad.source === null) {
572
+ const cmdText = typeof args.command === "string" ? args.command : "";
573
+ const byName = matchProjectByName(cmdText, cwd);
574
+ if (byName.root !== null) {
575
+ effectiveCwd = byName.root;
576
+ hookLoad = loadHooks(effectiveCwd);
577
+ trace(dshHome, `project root by name: ${effectiveCwd}\uFF08${byName.reason}\uFF09`);
578
+ } else if (cmdText !== "") {
579
+ trace(dshHome, `no project root: ${byName.reason}`);
580
+ }
581
+ }
582
+ const { hooks, source, errors } = hookLoad;
583
+ for (const e of errors) note(`project-hooks: ${e}
584
+ `);
585
+ if (pendingSkillLoad !== null && source !== null) {
586
+ recordPersistent(effectiveCwd, pendingSkillLoad);
587
+ trace(dshHome, `skill-load ${pendingSkillLoad.skill ?? "(unnamed)"}`);
588
+ pendingSkillLoad = null;
589
+ }
590
+ if (event === "PostToolUse" && source !== null) {
591
+ recordEvent(effectiveCwd, {
592
+ kind: "tool",
593
+ tool: toolName,
594
+ status: "ok",
595
+ files: targetPath !== void 0 && targetPath !== "" ? [targetPath] : [],
596
+ client: "dsh"
597
+ });
598
+ }
599
+ if (source === null) {
600
+ trace(dshHome, `no hooks config found under ${cwd}`);
601
+ return ALLOW;
602
+ }
603
+ const applicable = hooks.filter((h) => h.event === event && matches(h, toolName));
604
+ if (applicable.length === 0) {
605
+ trace(
606
+ process.env.DSH_HOME ?? join6(homedir(), ".dsh"),
607
+ `no applicable hooks (loaded=${String(hooks.length)} tool=${toolName})`
608
+ );
609
+ return ALLOW;
610
+ }
611
+ let decision = ALLOW;
612
+ for (const hook of applicable) {
613
+ trace(dshHome, `start ${hook.id} cmd=${hook.command.slice(0, 60)}`);
614
+ const payload = buildPayload({ event, hookId: hook.id, toolName, args, sessionId, cwd });
615
+ const { exitCode, error } = await runHookCommand(
616
+ {
617
+ command: hook.command,
618
+ cwd,
619
+ timeoutMs: hook.timeoutMs,
620
+ stdin: payloadStdin(payload),
621
+ env: hookEnv(cwd, hook.id, sessionId)
622
+ },
623
+ shell
624
+ );
625
+ const run = {
626
+ hookId: hook.id,
627
+ event,
628
+ toolName,
629
+ exitCode,
630
+ at: Date.now(),
631
+ ...error !== void 0 ? { error } : {}
632
+ };
633
+ record(run);
634
+ recordPersistent(effectiveCwd, { ...run, sessionId });
635
+ trace(
636
+ process.env.DSH_HOME ?? join6(homedir(), ".dsh"),
637
+ `hook ${hook.id} exit=${exitCode}${error === void 0 ? "" : ` error=${error}`}`
638
+ );
639
+ if (error !== void 0) note(`project-hooks: ${hook.id}: ${error}
640
+ `);
641
+ if (exitCodeBlocks(hook, exitCode)) {
642
+ decision = {
643
+ kind: "deny",
644
+ reason: `project-hooks: ${hook.id} exited ${exitCode}${error === void 0 ? "" : ` (${error})`}`
645
+ };
646
+ }
647
+ }
648
+ if (decision.kind === "deny" && observeOnly()) {
649
+ note(`project-hooks: observe mode \u2014 would deny via ${decision.reason ?? "(no reason)"}
650
+ `);
651
+ return ALLOW;
652
+ }
653
+ return decision;
654
+ };
655
+ ctx.on("tools/pre-execute", (exec) => handle("PreToolUse", exec));
656
+ ctx.on("tools/post-execute", (exec) => handle("PostToolUse", exec));
657
+ return state;
658
+ }
659
+ function apply(ctx) {
660
+ const home = process.env.DSH_HOME ?? join6(homedir(), ".dsh");
661
+ trace(home, `apply registered seams v${PLUGIN_MARK} (observe=${String(observeOnly())})`);
662
+ return createProjectHooksPlugin(ctx);
663
+ }
664
+ export {
665
+ apply,
666
+ createProjectHooksPlugin
667
+ };