agentkit-dsh-plugin 0.0.0-stage → 0.0.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.
- package/README.md +39 -2
- package/cordis.patch.yml +5 -0
- package/dist/cli.js +248 -0
- package/dist/index.js +170 -0
- package/package.json +50 -4
package/README.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agentkit-dsh-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> DeepSeek Harness 插件:**项目自包含**的 lifecycle hooks —— 配置随仓库走,
|
|
4
|
+
> 按会话 cwd 发现,不需要为每个项目改宿主目录。
|
|
5
|
+
|
|
6
|
+
## 它做什么
|
|
7
|
+
|
|
8
|
+
在 DSH 的两个执行缝(`tools/pre-execute` / `tools/post-execute`)上跑命令钩子:
|
|
9
|
+
|
|
10
|
+
- **配置按会话 cwd 发现**:`<项目>/.agent/hooks/hooks.json`(CC 方言),
|
|
11
|
+
其次 `.claude/settings.json`、`.claude/hooks.json`;无配置 = 静默跳过
|
|
12
|
+
- **退出码语义**:`PreToolUse` 任何非零 → **阻断**(deny);`PostToolUse` → 只记录
|
|
13
|
+
- 配置坏掉**报错**,不会伪装成"没有配置"(否则等于静默放行)
|
|
14
|
+
- payload 走 stdin(`{event, hookId, toolName, args, sessionId, cwd}`),
|
|
15
|
+
env 含 `CLAUDE_PROJECT_DIR` / `DSH_HOOK_ID` / `DSH_SESSION_ID`
|
|
16
|
+
|
|
17
|
+
## 安装(宿主侧,显式部署动作)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 装进某个 profile(会写 ~/.dsh/profiles/<p>/package.json + node_modules)
|
|
21
|
+
pnpm add agentkit-dsh-plugin --dir ~/.dsh/profiles/web
|
|
22
|
+
# 或用 agentkit 的落地命令(幂等、带备份与审计)
|
|
23
|
+
dsh-landing install --home ~/.dsh --profile web
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
条目本身由 bundle 自带的 `cordis.patch.yml` 插入;`dsh-landing rollback` 可撤销。
|
|
27
|
+
|
|
28
|
+
## 项目侧
|
|
29
|
+
|
|
30
|
+
项目里**零改动**:agentkit 的 `dsh` 适配器会生成 `.agent/hooks/hooks.json`。
|
|
31
|
+
想手写也可以,CC 方言即可。
|
|
32
|
+
|
|
33
|
+
## 版本跟随
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
0.0.x ↔ DeepSeek Harness 0.2.x
|
|
37
|
+
seam:tools/pre-execute · tools/post-execute · ctx.shell
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
设计与论证见 agentkit 仓的 `docs/design/0001`(为什么自己做)与 `0002`(Target 模型)。
|
package/cordis.patch.yml
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { homedir as homedir2 } from "node:os";
|
|
5
|
+
import { join as join2 } from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/install.ts
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
var ROW_ID = "project-hooks";
|
|
13
|
+
var ROW_NAME = "agentkit-dsh-plugin";
|
|
14
|
+
var STATE_DIR = ".agentkit-landing";
|
|
15
|
+
function landingPaths(home2, profile2) {
|
|
16
|
+
const patch = join(home2, "profiles", profile2, "cordis.patch.yml");
|
|
17
|
+
return { home: home2, profile: profile2, patch, stateDir: join(home2, "profiles", profile2, STATE_DIR) };
|
|
18
|
+
}
|
|
19
|
+
function rowPresent(patchText) {
|
|
20
|
+
return patchText.includes(`id: ${ROW_ID}`) || patchText.includes(ROW_NAME);
|
|
21
|
+
}
|
|
22
|
+
function renderRow(configFile2) {
|
|
23
|
+
const lines = [`- id: ${ROW_ID}`, ` name: ${ROW_NAME}`];
|
|
24
|
+
if (configFile2 !== void 0) {
|
|
25
|
+
lines.push(" config:", ` configFile: ${configFile2}`);
|
|
26
|
+
}
|
|
27
|
+
return `${lines.join("\n")}
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
function audit(paths, action, detail) {
|
|
31
|
+
mkdirSync(paths.stateDir, { recursive: true });
|
|
32
|
+
const rec = { at: (/* @__PURE__ */ new Date()).toISOString(), action, profile: paths.profile, ...detail };
|
|
33
|
+
appendFileSync(join(paths.stateDir, "ledger.jsonl"), `${JSON.stringify(rec)}
|
|
34
|
+
`, "utf8");
|
|
35
|
+
}
|
|
36
|
+
function install(opts) {
|
|
37
|
+
const paths = landingPaths(opts.home, opts.profile);
|
|
38
|
+
if (!existsSync(join(opts.home, "profiles", opts.profile))) {
|
|
39
|
+
return { ok: false, changed: false, path: paths.patch, reason: `profile not found: ${opts.home}/profiles/${opts.profile}` };
|
|
40
|
+
}
|
|
41
|
+
if (!existsSync(paths.patch)) {
|
|
42
|
+
mkdirSync(paths.stateDir, { recursive: true });
|
|
43
|
+
writeFileSync(paths.patch, `${renderRow(opts.configFile)}`, "utf8");
|
|
44
|
+
audit(paths, "install", { backup: null, configFile: opts.configFile ?? null, created: true });
|
|
45
|
+
return { ok: true, changed: true, path: paths.patch, reason: "created" };
|
|
46
|
+
}
|
|
47
|
+
const before = readFileSync(paths.patch, "utf8");
|
|
48
|
+
if (rowPresent(before)) {
|
|
49
|
+
audit(paths, "install-noop", { reason: "already present" });
|
|
50
|
+
return { ok: true, changed: false, path: paths.patch, reason: "already present" };
|
|
51
|
+
}
|
|
52
|
+
mkdirSync(paths.stateDir, { recursive: true });
|
|
53
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
54
|
+
const backup = join(paths.stateDir, `backup-${stamp}.yml`);
|
|
55
|
+
copyFileSync(paths.patch, backup);
|
|
56
|
+
const next = before.endsWith("\n") || before === "" ? `${before}${renderRow(opts.configFile)}` : `${before}
|
|
57
|
+
${renderRow(opts.configFile)}`;
|
|
58
|
+
writeFileSync(paths.patch, next, "utf8");
|
|
59
|
+
audit(paths, "install", { backup, configFile: opts.configFile ?? null });
|
|
60
|
+
return { ok: true, changed: true, path: paths.patch, backup };
|
|
61
|
+
}
|
|
62
|
+
function rollback(opts) {
|
|
63
|
+
const paths = landingPaths(opts.home, opts.profile);
|
|
64
|
+
if (!existsSync(paths.stateDir)) {
|
|
65
|
+
return { ok: false, changed: false, path: paths.patch, reason: "no state directory \u2014 nothing to roll back" };
|
|
66
|
+
}
|
|
67
|
+
const backups = readdirSync(paths.stateDir).filter((f) => f.startsWith("backup-") && f.endsWith(".yml")).sort();
|
|
68
|
+
const last = backups.at(-1);
|
|
69
|
+
if (last === void 0) {
|
|
70
|
+
return { ok: false, changed: false, path: paths.patch, reason: "no backup recorded" };
|
|
71
|
+
}
|
|
72
|
+
copyFileSync(join(paths.stateDir, last), paths.patch);
|
|
73
|
+
audit(paths, "rollback", { restoredFrom: last });
|
|
74
|
+
return { ok: true, changed: true, path: paths.patch, backup: join(paths.stateDir, last) };
|
|
75
|
+
}
|
|
76
|
+
function status(opts) {
|
|
77
|
+
const paths = landingPaths(opts.home, opts.profile);
|
|
78
|
+
if (!existsSync(paths.patch)) {
|
|
79
|
+
return { ok: true, installed: false, path: paths.patch, backups: [] };
|
|
80
|
+
}
|
|
81
|
+
const text = readFileSync(paths.patch, "utf8");
|
|
82
|
+
const m = new RegExp(`configFile:\\s*(\\S+)`).exec(text);
|
|
83
|
+
const backups = existsSync(paths.stateDir) ? readdirSync(paths.stateDir).filter((f) => f.startsWith("backup-")).sort() : [];
|
|
84
|
+
return { ok: true, installed: rowPresent(text), path: paths.patch, backups, ...m?.[1] !== void 0 ? { configFile: m[1] } : {} };
|
|
85
|
+
}
|
|
86
|
+
function readProfileJson(home2, profile2) {
|
|
87
|
+
const p = join(home2, "profiles", profile2, "package.json");
|
|
88
|
+
if (!existsSync(p)) return null;
|
|
89
|
+
try {
|
|
90
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function installBundle(opts) {
|
|
96
|
+
const profileDir = join(opts.home, "profiles", opts.profile);
|
|
97
|
+
if (!existsSync(join(opts.home, "profiles", opts.profile))) {
|
|
98
|
+
return { ok: false, added: false, alreadyInstalled: false, error: `profile not found: ${profileDir}` };
|
|
99
|
+
}
|
|
100
|
+
const pkg = readProfileJson(opts.home, opts.profile);
|
|
101
|
+
if (pkg === null) return { ok: false, added: false, alreadyInstalled: false, error: `cannot read ${join(profileDir, "package.json")}` };
|
|
102
|
+
const deps = pkg.dependencies ?? {};
|
|
103
|
+
const dsh = pkg.dsh ?? {};
|
|
104
|
+
const bundles = dsh.profile?.bundles ?? [];
|
|
105
|
+
const installed = Object.keys(deps).some((k) => k === ROW_NAME) || bundles.includes(ROW_NAME);
|
|
106
|
+
if (installed) return { ok: true, added: false, alreadyInstalled: true };
|
|
107
|
+
if (opts.skipPackageManager !== true) {
|
|
108
|
+
const pm = resolvePnpm();
|
|
109
|
+
if (pm === null) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
added: false,
|
|
113
|
+
alreadyInstalled: false,
|
|
114
|
+
error: "pnpm not found (tried PATH, ~/.local/share/pnpm/pnpm, npx). Install pnpm, or rerun with --skip-pnpm and install the package into the profile by hand."
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const r = spawnSync(pm, ["add", opts.from], { cwd: profileDir, encoding: "utf8", timeout: 18e4 });
|
|
118
|
+
if (r.status !== 0) {
|
|
119
|
+
const detail = [r.error?.message, r.stdout, r.stderr].filter((x) => typeof x === "string").join("\n").trim();
|
|
120
|
+
return { ok: false, added: false, alreadyInstalled: false, error: `${pm} add failed (exit ${String(r.status)})
|
|
121
|
+
${detail.slice(-600)}` };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const next = {
|
|
125
|
+
...pkg,
|
|
126
|
+
dependencies: { ...deps, [ROW_NAME]: opts.skipPackageManager === true ? "file:." : opts.from },
|
|
127
|
+
dsh: { ...dsh, profile: { ...dsh.profile ?? {}, bundles: [...bundles, ROW_NAME] } }
|
|
128
|
+
};
|
|
129
|
+
writeFileSync(join(profileDir, "package.json"), `${JSON.stringify(next, null, 2)}
|
|
130
|
+
`, "utf8");
|
|
131
|
+
audit(landingPaths(opts.home, opts.profile), "install-bundle", { from: opts.from });
|
|
132
|
+
return { ok: true, added: true, alreadyInstalled: false };
|
|
133
|
+
}
|
|
134
|
+
function resolvePnpm() {
|
|
135
|
+
const candidates = ["pnpm", join(homedirPath(), ".local/share/pnpm/pnpm")];
|
|
136
|
+
for (const c of candidates) {
|
|
137
|
+
const probe = spawnSync(c, ["--version"], { encoding: "utf8", timeout: 15e3 });
|
|
138
|
+
if (probe.status === 0) return c;
|
|
139
|
+
}
|
|
140
|
+
const viaNpx = spawnSync("npx", ["--no-install", "pnpm", "--version"], { encoding: "utf8", timeout: 2e4 });
|
|
141
|
+
if (viaNpx.status === 0) return "npx pnpm";
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
function homedirPath() {
|
|
145
|
+
try {
|
|
146
|
+
return homedir();
|
|
147
|
+
} catch {
|
|
148
|
+
return process.env.HOME ?? "";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/cli.ts
|
|
153
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
154
|
+
stream.on("error", (err) => {
|
|
155
|
+
if (err.code === "EPIPE") process.exit(0);
|
|
156
|
+
throw err;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
var USAGE = `dsh-landing <install|rollback|status> [--profile <name>] [--home <path>] [--configFile <abs>]
|
|
160
|
+
|
|
161
|
+
install \u628A\u63D2\u4EF6\u6761\u76EE\u8FFD\u52A0\u8FDB <home>/profiles/<profile>/cordis.patch.yml\uFF08\u5E42\u7B49\uFF1B\u5199\u524D\u5907\u4EFD\uFF09
|
|
162
|
+
rollback \u6062\u590D\u6700\u8FD1\u4E00\u6B21\u5907\u4EFD\uFF08\u64A4\u9500\u6211\u4EEC\u8FFD\u52A0\u7684\u6761\u76EE\uFF0C\u4E0D\u52A8\u522B\u4EBA\u7684\u5185\u5BB9\uFF09
|
|
163
|
+
status \u62A5\u544A\u662F\u5426\u5DF2\u5B89\u88C5\u3001\u5907\u4EFD\u6570\u3001configFile
|
|
164
|
+
|
|
165
|
+
\u5BBF\u4E3B\u6539\u52A8\u662F**\u663E\u5F0F\u90E8\u7F72\u52A8\u4F5C**\uFF1A\u6BCF\u6B21\u52A8\u4F5C\u5199\u5BA1\u8BA1\u5230 <profile>/.agentkit-landing/ledger.jsonl\u3002`;
|
|
166
|
+
function flag(args2, name) {
|
|
167
|
+
const i = args2.indexOf(`--${name}`);
|
|
168
|
+
return i >= 0 ? args2[i + 1] : void 0;
|
|
169
|
+
}
|
|
170
|
+
var args = process.argv.slice(2);
|
|
171
|
+
var cmd = args[0];
|
|
172
|
+
if (cmd === void 0 || cmd === "--help" || cmd === "-h") {
|
|
173
|
+
process.stdout.write(`${USAGE}
|
|
174
|
+
`);
|
|
175
|
+
process.exit(cmd === void 0 ? 1 : 0);
|
|
176
|
+
}
|
|
177
|
+
var home = flag(args, "home") ?? process.env.DSH_HOME ?? join2(homedir2(), ".dsh");
|
|
178
|
+
var profile = flag(args, "profile") ?? "web";
|
|
179
|
+
var configFile = flag(args, "configFile");
|
|
180
|
+
if (cmd === "install") {
|
|
181
|
+
const from = flag(args, "from");
|
|
182
|
+
if (from !== void 0) {
|
|
183
|
+
const b = installBundle({
|
|
184
|
+
home,
|
|
185
|
+
profile,
|
|
186
|
+
from,
|
|
187
|
+
...args.includes("--skip-pnpm") ? { skipPackageManager: true } : {}
|
|
188
|
+
});
|
|
189
|
+
if (!b.ok) {
|
|
190
|
+
process.stderr.write(`dsh-landing: bundle install failed \u2014 ${b.error}
|
|
191
|
+
`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write(
|
|
195
|
+
b.alreadyInstalled ? `dsh-landing: plugin already installed in profile ${profile}
|
|
196
|
+
` : `dsh-landing: plugin installed into ${home}/profiles/${profile}/node_modules and registered in dsh.profile.bundles
|
|
197
|
+
`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (from !== void 0 && configFile === void 0) {
|
|
201
|
+
process.stdout.write(" row: provided by the bundle's own cordis.patch.yml (no override needed)\n");
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
const r = install({ home, profile, ...configFile !== void 0 ? { configFile } : {} });
|
|
205
|
+
if (!r.ok) {
|
|
206
|
+
process.stderr.write(`dsh-landing: install failed \u2014 ${r.reason}
|
|
207
|
+
`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
process.stdout.write(`dsh-landing: ${r.changed ? "installed" : "already present"} \u2192 ${r.path}
|
|
211
|
+
`);
|
|
212
|
+
if (r.backup !== void 0) process.stdout.write(` backup: ${r.backup}
|
|
213
|
+
`);
|
|
214
|
+
if (r.changed) {
|
|
215
|
+
process.stdout.write(" next: restart DSH (or /hooks-reload) \u2014 \u63D2\u4EF6\u6761\u76EE\u9700\u8981 profile \u91CD\u65B0\u5408\u6210\u624D\u751F\u6548\n");
|
|
216
|
+
process.stdout.write(" undo: dsh-landing rollback\n");
|
|
217
|
+
}
|
|
218
|
+
} else if (cmd === "rollback") {
|
|
219
|
+
const r = rollback({ home, profile });
|
|
220
|
+
if (!r.ok) {
|
|
221
|
+
process.stderr.write(`dsh-landing: rollback failed \u2014 ${r.reason}
|
|
222
|
+
`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
process.stdout.write(`dsh-landing: rolled back ${r.path} from ${r.backup}
|
|
226
|
+
`);
|
|
227
|
+
} else if (cmd === "status") {
|
|
228
|
+
const s = status({ home, profile });
|
|
229
|
+
const p = landingPaths(home, profile);
|
|
230
|
+
process.stdout.write(`dsh-landing: profile ${profile} (${home}/profiles/${profile})
|
|
231
|
+
`);
|
|
232
|
+
process.stdout.write(` installed: ${s.installed ? "yes" : "no"}
|
|
233
|
+
`);
|
|
234
|
+
process.stdout.write(` patch: ${s.path}
|
|
235
|
+
`);
|
|
236
|
+
process.stdout.write(` configFile:${s.configFile ?? " (project-discovered default)"}
|
|
237
|
+
`);
|
|
238
|
+
process.stdout.write(` backups: ${s.backups.length}
|
|
239
|
+
`);
|
|
240
|
+
process.stdout.write(` audit: ${join2(p.stateDir, "ledger.jsonl")}
|
|
241
|
+
`);
|
|
242
|
+
} else {
|
|
243
|
+
process.stderr.write(`dsh-landing: unknown command ${cmd}
|
|
244
|
+
|
|
245
|
+
${USAGE}
|
|
246
|
+
`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// ../core/dist/config.js
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
var CONFIG_CANDIDATES = [".agent/hooks/hooks.json", ".claude/settings.json", ".claude/hooks.json"];
|
|
5
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
6
|
+
function loadHooks(projectRoot) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
for (const rel of CONFIG_CANDIDATES) {
|
|
9
|
+
const abs = join(projectRoot, rel);
|
|
10
|
+
if (!existsSync(abs))
|
|
11
|
+
continue;
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(readFileSync(abs, "utf8"));
|
|
15
|
+
} catch (err) {
|
|
16
|
+
errors.push(`${abs}: \u4E0D\u662F\u5408\u6CD5 JSON \u2014 ${err.message}`);
|
|
17
|
+
return { hooks: [], source: abs, errors };
|
|
18
|
+
}
|
|
19
|
+
return { ...parseEntries(parsed, rel), source: abs };
|
|
20
|
+
}
|
|
21
|
+
return { hooks: [], source: null, errors };
|
|
22
|
+
}
|
|
23
|
+
function parseEntries(doc, where) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
const hooks = [];
|
|
26
|
+
const raw = doc?.hooks;
|
|
27
|
+
if (!Array.isArray(raw)) {
|
|
28
|
+
errors.push(`${where}: \u9876\u5C42\u7F3A\u5C11 hooks \u6570\u7EC4`);
|
|
29
|
+
return { hooks, errors };
|
|
30
|
+
}
|
|
31
|
+
for (const [i, entry] of raw.entries()) {
|
|
32
|
+
const e = entry;
|
|
33
|
+
const command = typeof e?.command === "string" ? e.command : "";
|
|
34
|
+
if (command === "") {
|
|
35
|
+
errors.push(`${where}: \u7B2C ${i + 1} \u6761\u7F3A command \u2014\u2014 \u8DF3\u8FC7`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const event = e?.event === "PostToolUse" ? "PostToolUse" : "PreToolUse";
|
|
39
|
+
const matcher = e?.matcher?.tool;
|
|
40
|
+
hooks.push({
|
|
41
|
+
id: typeof e?.id === "string" ? e.id : `hook-${i + 1}`,
|
|
42
|
+
event,
|
|
43
|
+
...typeof matcher === "string" ? { tool: matcher } : {},
|
|
44
|
+
command,
|
|
45
|
+
timeoutMs: typeof e?.timeoutMs === "number" ? e.timeoutMs : DEFAULT_TIMEOUT_MS
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return { hooks, errors };
|
|
49
|
+
}
|
|
50
|
+
function matches(hook, toolName) {
|
|
51
|
+
if (hook.tool === void 0)
|
|
52
|
+
return true;
|
|
53
|
+
try {
|
|
54
|
+
return new RegExp(hook.tool).test(toolName);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function exitCodeBlocks(hook, exitCode) {
|
|
60
|
+
return hook.event === "PreToolUse" && exitCode !== 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/payload.ts
|
|
64
|
+
function buildPayload(input) {
|
|
65
|
+
return {
|
|
66
|
+
event: input.event,
|
|
67
|
+
hookId: input.hookId,
|
|
68
|
+
toolName: input.toolName,
|
|
69
|
+
args: input.args,
|
|
70
|
+
sessionId: input.sessionId,
|
|
71
|
+
cwd: input.cwd ?? null
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function payloadStdin(payload) {
|
|
75
|
+
return `${JSON.stringify(payload)}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
function hookEnv(cwd, hookId, sessionId) {
|
|
79
|
+
return {
|
|
80
|
+
CLAUDE_PROJECT_DIR: cwd,
|
|
81
|
+
DSH_HOOK_ID: hookId,
|
|
82
|
+
DSH_SESSION_ID: sessionId ?? ""
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/execute.ts
|
|
87
|
+
import { spawnSync } from "node:child_process";
|
|
88
|
+
async function runHookCommand(req, shell) {
|
|
89
|
+
if (shell !== void 0 && typeof shell.run === "function" && typeof shell.resolve === "function") {
|
|
90
|
+
try {
|
|
91
|
+
const resolved = shell.resolve({
|
|
92
|
+
command: req.command,
|
|
93
|
+
workdir: req.cwd,
|
|
94
|
+
timeoutMs: req.timeoutMs,
|
|
95
|
+
stdin: req.stdin,
|
|
96
|
+
env: req.env
|
|
97
|
+
});
|
|
98
|
+
const r2 = await shell.run(resolved);
|
|
99
|
+
return { exitCode: r2.exitCode };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return { exitCode: 1, error: `ctx.shell failure: ${err.message}` };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const r = spawnSync("sh", ["-c", req.command], {
|
|
105
|
+
cwd: req.cwd,
|
|
106
|
+
timeout: req.timeoutMs,
|
|
107
|
+
input: req.stdin,
|
|
108
|
+
env: { ...process.env, ...req.env },
|
|
109
|
+
encoding: "utf8"
|
|
110
|
+
});
|
|
111
|
+
return { exitCode: r.status ?? 1, ...r.status === null ? { error: `signal: ${String(r.signal)}` } : {} };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/index.ts
|
|
115
|
+
var RECENT_CAP = 20;
|
|
116
|
+
function createProjectHooksPlugin(ctx) {
|
|
117
|
+
const state = { runs: [] };
|
|
118
|
+
const note = (text) => {
|
|
119
|
+
ctx.write?.(text);
|
|
120
|
+
};
|
|
121
|
+
const record = (run) => {
|
|
122
|
+
state.runs = [run, ...state.runs].slice(0, RECENT_CAP);
|
|
123
|
+
};
|
|
124
|
+
const handle = async (event, exec) => {
|
|
125
|
+
const cwd = exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
126
|
+
const sessionId = exec.agent?.session?.id ?? null;
|
|
127
|
+
const toolName = String(exec.toolName ?? "");
|
|
128
|
+
const args = exec.args ?? {};
|
|
129
|
+
const { hooks, source, errors } = loadHooks(cwd);
|
|
130
|
+
for (const e of errors) note(`project-hooks: ${e}
|
|
131
|
+
`);
|
|
132
|
+
if (source === null) return void 0;
|
|
133
|
+
const applicable = hooks.filter((h) => h.event === event && matches(h, toolName));
|
|
134
|
+
if (applicable.length === 0) return void 0;
|
|
135
|
+
let decision;
|
|
136
|
+
for (const hook of applicable) {
|
|
137
|
+
const payload = buildPayload({ event, hookId: hook.id, toolName, args, sessionId, cwd });
|
|
138
|
+
const { exitCode, error } = await runHookCommand(
|
|
139
|
+
{
|
|
140
|
+
command: hook.command,
|
|
141
|
+
cwd,
|
|
142
|
+
timeoutMs: hook.timeoutMs,
|
|
143
|
+
stdin: payloadStdin(payload),
|
|
144
|
+
env: hookEnv(cwd, hook.id, sessionId)
|
|
145
|
+
},
|
|
146
|
+
ctx.shell
|
|
147
|
+
);
|
|
148
|
+
record({ hookId: hook.id, event, toolName, exitCode, at: Date.now(), ...error !== void 0 ? { error } : {} });
|
|
149
|
+
if (error !== void 0) note(`project-hooks: ${hook.id}: ${error}
|
|
150
|
+
`);
|
|
151
|
+
if (exitCodeBlocks(hook, exitCode)) {
|
|
152
|
+
decision = {
|
|
153
|
+
kind: "deny",
|
|
154
|
+
reason: `project-hooks: ${hook.id} exited ${exitCode}${error === void 0 ? "" : ` (${error})`}`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return decision;
|
|
159
|
+
};
|
|
160
|
+
ctx.on("tools/pre-execute", (exec) => handle("PreToolUse", exec));
|
|
161
|
+
ctx.on("tools/post-execute", (exec) => handle("PostToolUse", exec));
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
function apply(ctx) {
|
|
165
|
+
return createProjectHooksPlugin(ctx);
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
apply,
|
|
169
|
+
createProjectHooksPlugin
|
|
170
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentkit-dsh-plugin",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "DeepSeek Harness 插件:项目自包含的 lifecycle hooks(配置随仓库走,per-session 发现)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"cordis.patch.yml",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "esbuild src/index.ts src/cli.ts --bundle --platform=node --format=esm --outdir=dist --target=node20",
|
|
20
|
+
"prepack": "npm run build",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^22",
|
|
26
|
+
"agentkit-plugin-core": "workspace:*"
|
|
27
|
+
},
|
|
28
|
+
"dsh": {
|
|
29
|
+
"bundle": {
|
|
30
|
+
"patch": "./cordis.patch.yml"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"dsh-landing": "./dist/cli.js"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public",
|
|
38
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/agentkit/agentkit.git",
|
|
43
|
+
"directory": "packages/plugins/dsh"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"deepseek-harness",
|
|
47
|
+
"dsh",
|
|
48
|
+
"agent",
|
|
49
|
+
"hooks",
|
|
50
|
+
"agentkit"
|
|
51
|
+
]
|
|
52
|
+
}
|