dsh-hooks 0.3.0 → 0.4.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/README.md +14 -2
- package/README.zh.md +13 -1
- package/lib/client.js +365 -0
- package/package.json +26 -6
package/README.md
CHANGED
|
@@ -4,17 +4,19 @@ Config-driven lifecycle hooks plugin for [DeepSeek Harness](https://github.com/d
|
|
|
4
4
|
|
|
5
5
|
Declare `event -> command` hooks directly in your profile's `cordis.patch.yml` — like Codex CLI / OpenCode hooks, but for dsh. No plugin code required.
|
|
6
6
|
|
|
7
|
-
[中文文档](README.zh.md) | [Design](#design) | [Feishu example](examples/notify-feishu.mjs) | [Web GUI
|
|
7
|
+
[中文文档](README.zh.md) | [Design](#design) | [Feishu example](examples/notify-feishu.mjs) | [Web GUI](#web-gui)
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
One package ships everything (hook engine + Web GUI settings page):
|
|
12
|
+
|
|
11
13
|
```sh
|
|
12
14
|
dsh plugin --profile web add dsh-hooks # from npm
|
|
13
15
|
# or straight from git:
|
|
14
16
|
dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
Restart `dsh web`.
|
|
19
|
+
Restart `dsh web`. The settings panel gains a "Hooks" section (see [Web GUI](#web-gui)).
|
|
18
20
|
|
|
19
21
|
## Configure
|
|
20
22
|
|
|
@@ -172,6 +174,16 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # end-to-end: actually r
|
|
|
172
174
|
|
|
173
175
|
`dry-run` reads the profile's `cordis.patch.yml` (the `id: dsh-hooks` block) and validates the config (bad regexes fail here).
|
|
174
176
|
|
|
177
|
+
## Web GUI
|
|
178
|
+
|
|
179
|
+
After install, the dsh web settings panel gains a "Hooks" section (beside General and Plugins):
|
|
180
|
+
|
|
181
|
+
- **Status badges**: plugin version, hook count, history count
|
|
182
|
+
- **Execution-history timeline**: the latest 30 triggers (time / event / command / outcome / stderr tail), refreshed every 5s
|
|
183
|
+
- **Manual tester**: pick an event (14 kinds) + reason/tool; "Simulate" shows the per-hook match report, "Execute" really triggers the matching hooks
|
|
184
|
+
|
|
185
|
+
CLI/headless environments are unaffected: the browser half loads only in the web GUI and the core has no UI runtime dependencies.
|
|
186
|
+
|
|
175
187
|
## Web profile HTTP routes
|
|
176
188
|
|
|
177
189
|
In the web profile (when the shared webServer service exists) dsh-hooks registers loopback-only `/dsh-hooks/*` routes — CLI/headless environments never see them:
|
package/README.zh.md
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
11
|
+
一个包搞定全部(hook 引擎 + Web GUI 设置页):
|
|
12
|
+
|
|
11
13
|
```sh
|
|
12
14
|
dsh plugin --profile web add dsh-hooks # 从 npm 安装
|
|
13
15
|
# 或直接从 git 安装:
|
|
14
16
|
dsh plugin --profile web add github:PeterBon/dsh-hooks
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
重启 `dsh web`
|
|
19
|
+
重启 `dsh web` 生效。安装后设置面板里会出现「Hooks」分区(见 [Web GUI](#web-gui))。
|
|
18
20
|
|
|
19
21
|
## 配置
|
|
20
22
|
|
|
@@ -154,6 +156,16 @@ dsh-hooks dry-run tool/call --tool ssh_exec --execute # 端到端真跑匹配
|
|
|
154
156
|
|
|
155
157
|
`dry-run` 直接读 profile 的 `cordis.patch.yml`(`id: dsh-hooks` 配置块),配置校验(非法正则等)会在这一步报错。
|
|
156
158
|
|
|
159
|
+
## Web GUI
|
|
160
|
+
|
|
161
|
+
安装后,dsh web 的设置面板里会出现「Hooks」分区(与「通用」「插件」平级):
|
|
162
|
+
|
|
163
|
+
- **状态徽章**:插件版本、hook 数、历史条数
|
|
164
|
+
- **执行历史时间线**:最近 30 条触发(时间 / 事件 / 命令 / 结果 / stderr 尾部),5 秒自动刷新
|
|
165
|
+
- **手动测试**:选事件(14 类)+ reason/tool,「模拟」看逐 hook 匹配报告,「执行」真实触发
|
|
166
|
+
|
|
167
|
+
CLI/headless 环境完全不受影响:浏览器半只在 web 加载,核心零 UI 运行时依赖。
|
|
168
|
+
|
|
157
169
|
## Web profile HTTP 路由
|
|
158
170
|
|
|
159
171
|
web profile 里(存在共享 webServer 服务时)dsh-hooks 自动注册 loopback-only 的 `/dsh-hooks/*` 路由——CLI/headless 环境完全无感:
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-hooks",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
//#region src/client/api.ts
|
|
10
|
+
async function getJson(path, fetchFn) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetchFn(path, { headers: { accept: "application/json" } });
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
console.warn(`[dsh-hooks-ui] GET ${path} → HTTP ${response.status}`);
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const envelope = await response.json();
|
|
18
|
+
if (!envelope.ok) {
|
|
19
|
+
console.warn(`[dsh-hooks-ui] GET ${path} → ${envelope.error?.message ?? "unknown error"}`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return envelope.value ?? null;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`[dsh-hooks-ui] GET ${path} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function fetchStatus(fetchFn = fetch) {
|
|
29
|
+
return getJson("/dsh-hooks/status", fetchFn);
|
|
30
|
+
}
|
|
31
|
+
async function fetchHistory(n = 50, fetchFn = fetch) {
|
|
32
|
+
return getJson(`/dsh-hooks/history?n=${Math.max(1, Math.min(500, Math.floor(n)))}`, fetchFn);
|
|
33
|
+
}
|
|
34
|
+
async function postTest(body, fetchFn = fetch) {
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetchFn("/dsh-hooks/test", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: {
|
|
39
|
+
"content-type": "application/json",
|
|
40
|
+
accept: "application/json"
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(body)
|
|
43
|
+
});
|
|
44
|
+
const envelope = await response.json();
|
|
45
|
+
if (!response.ok || !envelope.ok) {
|
|
46
|
+
console.warn(`[dsh-hooks-ui] POST /dsh-hooks/test → ${envelope.error?.message ?? `HTTP ${response.status}`}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return envelope.value ?? null;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn(`[dsh-hooks-ui] POST /dsh-hooks/test failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** `HH:MM:SS` local time for a timestamp. */
|
|
56
|
+
function formatTime(ts) {
|
|
57
|
+
const date = new Date(ts);
|
|
58
|
+
const p = (value) => String(value).padStart(2, "0");
|
|
59
|
+
return `${p(date.getHours())}:${p(date.getMinutes())}:${p(date.getSeconds())}`;
|
|
60
|
+
}
|
|
61
|
+
/** Chinese outcome labels. */
|
|
62
|
+
const OUTCOME_LABELS = {
|
|
63
|
+
spawned: "已启动",
|
|
64
|
+
"spawn-failed": "启动失败",
|
|
65
|
+
timeout: "超时",
|
|
66
|
+
"exit-0": "成功",
|
|
67
|
+
"exit-nonzero": "失败",
|
|
68
|
+
sent: "已发送",
|
|
69
|
+
"send-failed": "发送失败"
|
|
70
|
+
};
|
|
71
|
+
function outcomeLabel(outcome) {
|
|
72
|
+
return OUTCOME_LABELS[outcome] ?? outcome;
|
|
73
|
+
}
|
|
74
|
+
const OUTCOME_TONES = {
|
|
75
|
+
"exit-0": "ok",
|
|
76
|
+
sent: "ok",
|
|
77
|
+
"exit-nonzero": "bad",
|
|
78
|
+
"spawn-failed": "bad",
|
|
79
|
+
"send-failed": "bad",
|
|
80
|
+
timeout: "warn",
|
|
81
|
+
spawned: "neutral"
|
|
82
|
+
};
|
|
83
|
+
function outcomeTone(outcome) {
|
|
84
|
+
return OUTCOME_TONES[outcome] ?? "neutral";
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/client/settings-card.tsx
|
|
88
|
+
/**
|
|
89
|
+
* The dsh-hooks settings card: status badges, execution-history timeline,
|
|
90
|
+
* and a manual event tester — all served by the core plugin's /dsh-hooks/*
|
|
91
|
+
* routes. Degrades gracefully: fetch failures show an inline notice, never
|
|
92
|
+
* a crash. Registered into the shell's `web-ui.plugin.item` slot.
|
|
93
|
+
*/
|
|
94
|
+
const EVENTS = [
|
|
95
|
+
"turn/start",
|
|
96
|
+
"turn/end",
|
|
97
|
+
"step/end",
|
|
98
|
+
"tool/call",
|
|
99
|
+
"tool/result",
|
|
100
|
+
"user/message",
|
|
101
|
+
"approval/asked",
|
|
102
|
+
"session/title",
|
|
103
|
+
"session/created",
|
|
104
|
+
"session/disposed",
|
|
105
|
+
"agent/created",
|
|
106
|
+
"agent/disposed",
|
|
107
|
+
"agent/error",
|
|
108
|
+
"agent/status"
|
|
109
|
+
];
|
|
110
|
+
/** Settings-slot component; the shell's slot machinery supplies the props. */
|
|
111
|
+
function HooksSettingsCard(_props) {
|
|
112
|
+
const [status, setStatus] = (0, react.useState)(null);
|
|
113
|
+
const [history, setHistory] = (0, react.useState)(null);
|
|
114
|
+
const [loadError, setLoadError] = (0, react.useState)(false);
|
|
115
|
+
const [event, setEvent] = (0, react.useState)("turn/end");
|
|
116
|
+
const [reason, setReason] = (0, react.useState)("completed");
|
|
117
|
+
const [tool, setTool] = (0, react.useState)("");
|
|
118
|
+
const [testResult, setTestResult] = (0, react.useState)(null);
|
|
119
|
+
const refresh = (0, react.useCallback)(async () => {
|
|
120
|
+
const [statusInfo, records] = await Promise.all([fetchStatus(), fetchHistory(30)]);
|
|
121
|
+
setStatus(statusInfo);
|
|
122
|
+
setHistory(records);
|
|
123
|
+
setLoadError(statusInfo === null && records === null);
|
|
124
|
+
}, []);
|
|
125
|
+
(0, react.useEffect)(() => {
|
|
126
|
+
refresh();
|
|
127
|
+
const timer = setInterval(() => void refresh(), 5e3);
|
|
128
|
+
return () => clearInterval(timer);
|
|
129
|
+
}, [refresh]);
|
|
130
|
+
const runTest = async (execute) => {
|
|
131
|
+
const result = await postTest({
|
|
132
|
+
event,
|
|
133
|
+
reason: event === "turn/end" && reason !== "" ? reason : void 0,
|
|
134
|
+
tool: tool !== "" ? tool : void 0,
|
|
135
|
+
execute
|
|
136
|
+
});
|
|
137
|
+
setTestResult(result);
|
|
138
|
+
if (execute) refresh();
|
|
139
|
+
};
|
|
140
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
141
|
+
className: "dh-card",
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
144
|
+
className: "dh-card-head",
|
|
145
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
146
|
+
className: "dh-card-title",
|
|
147
|
+
children: "dsh-hooks"
|
|
148
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
149
|
+
className: "dh-badges",
|
|
150
|
+
children: status !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
151
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
152
|
+
className: "dh-badge",
|
|
153
|
+
children: ["v", status.version]
|
|
154
|
+
}),
|
|
155
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
156
|
+
className: "dh-badge",
|
|
157
|
+
children: [status.hookCount, " hooks"]
|
|
158
|
+
}),
|
|
159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
160
|
+
className: "dh-badge",
|
|
161
|
+
children: [status.historyCount, " 记录"]
|
|
162
|
+
})
|
|
163
|
+
] })
|
|
164
|
+
})]
|
|
165
|
+
}),
|
|
166
|
+
loadError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
167
|
+
className: "dh-error-banner",
|
|
168
|
+
children: "无法访问 /dsh-hooks/* 路由:请确认 dsh-hooks 核心插件已安装且 dsh web 已重启。"
|
|
169
|
+
}),
|
|
170
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
171
|
+
className: "dh-section-title",
|
|
172
|
+
children: "执行历史(最近 30 条)"
|
|
173
|
+
}), history === null || history.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
174
|
+
className: "dh-empty",
|
|
175
|
+
children: history === null ? "加载中…" : "暂无记录"
|
|
176
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
177
|
+
className: "dh-timeline",
|
|
178
|
+
children: [...history].reverse().map((record, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
179
|
+
className: "dh-record",
|
|
180
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
181
|
+
className: "dh-record-main",
|
|
182
|
+
children: [
|
|
183
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
184
|
+
className: "dh-record-top",
|
|
185
|
+
children: [
|
|
186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
187
|
+
className: "dh-record-time",
|
|
188
|
+
children: formatTime(record.ts)
|
|
189
|
+
}),
|
|
190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
191
|
+
className: "dh-record-event",
|
|
192
|
+
children: record.event
|
|
193
|
+
}),
|
|
194
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
195
|
+
className: `dh-outcome ${outcomeClass(record.outcome)}`,
|
|
196
|
+
children: outcomeLabel(record.outcome)
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
}),
|
|
200
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
201
|
+
className: "dh-record-command",
|
|
202
|
+
title: record.command,
|
|
203
|
+
children: record.command
|
|
204
|
+
}),
|
|
205
|
+
record.error !== void 0 && record.error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
206
|
+
className: "dh-record-error",
|
|
207
|
+
children: record.error.slice(0, 200)
|
|
208
|
+
})
|
|
209
|
+
]
|
|
210
|
+
})
|
|
211
|
+
}, `${record.ts}-${index}`))
|
|
212
|
+
})] }),
|
|
213
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
214
|
+
className: "dh-section-title",
|
|
215
|
+
children: "手动测试"
|
|
216
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
217
|
+
className: "dh-test-form",
|
|
218
|
+
children: [
|
|
219
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
220
|
+
className: "dh-test-row",
|
|
221
|
+
children: [
|
|
222
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
223
|
+
className: "dh-field",
|
|
224
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
225
|
+
className: "dh-field-label",
|
|
226
|
+
children: "事件"
|
|
227
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
228
|
+
className: "dh-select",
|
|
229
|
+
value: event,
|
|
230
|
+
onChange: (e) => setEvent(e.target.value),
|
|
231
|
+
children: EVENTS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
232
|
+
value: name,
|
|
233
|
+
children: name
|
|
234
|
+
}, name))
|
|
235
|
+
})]
|
|
236
|
+
}),
|
|
237
|
+
event === "turn/end" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
238
|
+
className: "dh-field",
|
|
239
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
240
|
+
className: "dh-field-label",
|
|
241
|
+
children: "reason"
|
|
242
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
243
|
+
className: "dh-input",
|
|
244
|
+
value: reason,
|
|
245
|
+
onChange: (e) => setReason(e.target.value),
|
|
246
|
+
placeholder: "completed"
|
|
247
|
+
})]
|
|
248
|
+
}),
|
|
249
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
250
|
+
className: "dh-field",
|
|
251
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
252
|
+
className: "dh-field-label",
|
|
253
|
+
children: "tool(可选)"
|
|
254
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
255
|
+
className: "dh-input",
|
|
256
|
+
value: tool,
|
|
257
|
+
onChange: (e) => setTool(e.target.value),
|
|
258
|
+
placeholder: "pwsh"
|
|
259
|
+
})]
|
|
260
|
+
})
|
|
261
|
+
]
|
|
262
|
+
}),
|
|
263
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
264
|
+
className: "dh-buttons",
|
|
265
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
266
|
+
type: "button",
|
|
267
|
+
className: "dh-button",
|
|
268
|
+
onClick: () => void runTest(false),
|
|
269
|
+
children: "模拟(看匹配)"
|
|
270
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
271
|
+
type: "button",
|
|
272
|
+
className: "dh-button dh-button-primary",
|
|
273
|
+
onClick: () => void runTest(true),
|
|
274
|
+
children: "执行(真实触发)"
|
|
275
|
+
})]
|
|
276
|
+
}),
|
|
277
|
+
testResult !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
278
|
+
className: "dh-test-results",
|
|
279
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
280
|
+
className: "dh-test-line",
|
|
281
|
+
children: [
|
|
282
|
+
testResult.event,
|
|
283
|
+
":",
|
|
284
|
+
testResult.matched,
|
|
285
|
+
"/",
|
|
286
|
+
testResult.total,
|
|
287
|
+
" 个 hook 触发",
|
|
288
|
+
testResult.executed ? "(已执行)" : ""
|
|
289
|
+
]
|
|
290
|
+
}, "head"), testResult.lines.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
291
|
+
className: `dh-test-line ${line.matched ? "dh-test-line-match" : "dh-test-line-skip"}`,
|
|
292
|
+
children: [
|
|
293
|
+
line.matched ? "✅" : "⏭",
|
|
294
|
+
" [",
|
|
295
|
+
line.index,
|
|
296
|
+
"] ",
|
|
297
|
+
line.summary,
|
|
298
|
+
!line.matched && line.why !== "" ? ` —— ${line.why}` : ""
|
|
299
|
+
]
|
|
300
|
+
}, line.index))]
|
|
301
|
+
})
|
|
302
|
+
]
|
|
303
|
+
})] })
|
|
304
|
+
]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function outcomeClass(outcome) {
|
|
308
|
+
switch (outcomeTone(outcome)) {
|
|
309
|
+
case "ok": return "dh-outcome-ok";
|
|
310
|
+
case "bad": return "dh-outcome-bad";
|
|
311
|
+
case "warn": return "dh-outcome-warn";
|
|
312
|
+
default: return "dh-outcome-neutral";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/client/settings-card.module.css?inline
|
|
317
|
+
var settings_card_module_default = ":root {\n --dsh-hooks-border: #80849038;\n --dsh-hooks-muted: #767c85;\n --dsh-hooks-accent: #4d8df7;\n --dsh-hooks-ok: #3fb56b;\n --dsh-hooks-bad: #e5534b;\n --dsh-hooks-warn: #d9a13c;\n}\n\n.dh-card {\n flex-direction: column;\n gap: 14px;\n padding: 12px 4px;\n font-size: 13px;\n line-height: 1.5;\n display: flex;\n}\n\n.dh-card-head {\n align-items: center;\n gap: 10px;\n display: flex;\n}\n\n.dh-card-title {\n font-size: 14px;\n font-weight: 600;\n}\n\n.dh-badges {\n gap: 6px;\n display: flex;\n}\n\n.dh-badge {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n background: #80849029;\n border-radius: 9px;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-section-title {\n color: var(--dsh-hooks-muted);\n text-transform: uppercase;\n letter-spacing: .04em;\n margin: 0 0 8px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dh-timeline {\n flex-direction: column;\n gap: 6px;\n display: flex;\n}\n\n.dh-record {\n border: 1px solid var(--dsh-hooks-border);\n border-radius: 6px;\n gap: 8px;\n padding: 7px 9px;\n display: flex;\n}\n\n.dh-record-main {\n flex: 1;\n min-width: 0;\n}\n\n.dh-record-top {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dh-record-time {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n font-size: 11px;\n}\n\n.dh-record-event {\n white-space: nowrap;\n text-overflow: ellipsis;\n font-weight: 600;\n overflow: hidden;\n}\n\n.dh-record-command {\n color: var(--dsh-hooks-muted);\n white-space: nowrap;\n text-overflow: ellipsis;\n text-align: left;\n direction: rtl;\n font-size: 12px;\n overflow: hidden;\n}\n\n.dh-outcome {\n white-space: nowrap;\n border-radius: 9px;\n align-self: flex-start;\n padding: 1px 7px;\n font-size: 11px;\n}\n\n.dh-outcome-ok {\n color: var(--dsh-hooks-ok);\n background: #3fb56b29;\n}\n\n.dh-outcome-bad {\n color: var(--dsh-hooks-bad);\n background: #e5534b29;\n}\n\n.dh-outcome-warn {\n color: var(--dsh-hooks-warn);\n background: #d9a13c29;\n}\n\n.dh-outcome-neutral {\n color: var(--dsh-hooks-muted);\n background: #80849029;\n}\n\n.dh-record-error {\n color: var(--dsh-hooks-bad);\n white-space: pre-wrap;\n word-break: break-all;\n margin-top: 4px;\n font-size: 11px;\n}\n\n.dh-empty {\n color: var(--dsh-hooks-muted);\n padding: 6px 2px;\n font-size: 12px;\n}\n\n.dh-test-form {\n flex-direction: column;\n gap: 8px;\n display: flex;\n}\n\n.dh-test-row {\n gap: 8px;\n display: flex;\n}\n\n.dh-field {\n flex-direction: column;\n flex: 1;\n gap: 3px;\n min-width: 0;\n display: flex;\n}\n\n.dh-field-label {\n color: var(--dsh-hooks-muted);\n font-size: 11px;\n}\n\n.dh-input, .dh-select {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n box-sizing: border-box;\n background: #8084901f;\n border-radius: 5px;\n outline: none;\n width: 100%;\n padding: 5px 8px;\n font-size: 12px;\n}\n\n.dh-input:focus, .dh-select:focus {\n border-color: var(--dsh-hooks-accent);\n}\n\n.dh-buttons {\n gap: 8px;\n display: flex;\n}\n\n.dh-button {\n border: 1px solid var(--dsh-hooks-border);\n color: inherit;\n cursor: pointer;\n background: #8084901f;\n border-radius: 5px;\n padding: 5px 12px;\n font-size: 12px;\n}\n\n.dh-button:hover {\n background: #80849038;\n}\n\n.dh-button-primary {\n background: var(--dsh-hooks-accent);\n border-color: var(--dsh-hooks-accent);\n color: #fff;\n}\n\n.dh-button-primary:hover {\n background: #3c7de8;\n}\n\n.dh-test-results {\n flex-direction: column;\n gap: 4px;\n display: flex;\n}\n\n.dh-test-line {\n word-break: break-all;\n border-radius: 5px;\n padding: 4px 8px;\n font-size: 12px;\n}\n\n.dh-test-line-match {\n color: var(--dsh-hooks-ok);\n background: #3fb56b24;\n}\n\n.dh-test-line-skip {\n color: var(--dsh-hooks-muted);\n background: #8084901a;\n}\n\n.dh-error-banner {\n color: var(--dsh-hooks-bad);\n background: #e5534b1f;\n border: 1px solid #e5534b66;\n border-radius: 6px;\n padding: 8px 10px;\n font-size: 12px;\n}\n";
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/client/index.ts
|
|
320
|
+
const name = "dsh-hooks";
|
|
321
|
+
/** Required services: the slot registry must be up before this plugin applies. */
|
|
322
|
+
const inject = ["slots"];
|
|
323
|
+
const STYLE_ID = "dsh-hooks-ui-style";
|
|
324
|
+
/** Single-application guard: first apply wins; later calls become no-ops. */
|
|
325
|
+
let applied = false;
|
|
326
|
+
function apply(ctx) {
|
|
327
|
+
if (typeof document === "undefined") return;
|
|
328
|
+
if (applied) return;
|
|
329
|
+
applied = true;
|
|
330
|
+
injectCardStyle();
|
|
331
|
+
try {
|
|
332
|
+
ctx.slots.inject("settings.section", () => {
|
|
333
|
+
const unregister = ctx.slots.register({
|
|
334
|
+
name: "settings.section",
|
|
335
|
+
id: "dsh-hooks",
|
|
336
|
+
order: 100,
|
|
337
|
+
label: "Hooks"
|
|
338
|
+
}, HooksSettingsCard);
|
|
339
|
+
return () => {
|
|
340
|
+
unregister();
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error("[dsh-hooks-ui] slot registration failed:", error);
|
|
345
|
+
}
|
|
346
|
+
ctx.effect(() => () => {
|
|
347
|
+
applied = false;
|
|
348
|
+
document.getElementById(STYLE_ID)?.remove();
|
|
349
|
+
}, "dsh-hooks-ui: card");
|
|
350
|
+
}
|
|
351
|
+
/** Inject the card stylesheet once (bundled as a string via .css?inline). */
|
|
352
|
+
function injectCardStyle() {
|
|
353
|
+
if (document.getElementById(STYLE_ID) !== null) return;
|
|
354
|
+
const style = document.createElement("style");
|
|
355
|
+
style.id = STYLE_ID;
|
|
356
|
+
style.textContent = settings_card_module_default;
|
|
357
|
+
document.head.appendChild(style);
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
exports.apply = apply;
|
|
361
|
+
exports.inject = inject;
|
|
362
|
+
exports.name = name;
|
|
363
|
+
return module.exports;
|
|
364
|
+
}
|
|
365
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"packageManager": "pnpm@11.21.0",
|
|
5
|
-
"description": "Config-driven lifecycle hooks plugin for DeepSeek Harness: declare event -> command hooks in cordis.patch.yml, no plugin code required.",
|
|
5
|
+
"description": "Config-driven lifecycle hooks plugin for DeepSeek Harness: declare event -> command hooks in cordis.patch.yml, no plugin code required. Includes a Hooks section in the Web GUI settings (history timeline + manual tester).",
|
|
6
6
|
"author": "PeterBon",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
"dsh-hooks": "./bin/dsh-hooks.mjs"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
|
-
".":
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./lib/index.d.ts",
|
|
33
|
+
"default": "./lib/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./client": "./lib/client.js",
|
|
32
36
|
"./package.json": "./package.json"
|
|
33
37
|
},
|
|
34
38
|
"files": [
|
|
@@ -41,8 +45,8 @@
|
|
|
41
45
|
"LICENSE"
|
|
42
46
|
],
|
|
43
47
|
"scripts": {
|
|
44
|
-
"build": "tsc -p tsconfig.json",
|
|
45
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"build": "tsc -p tsconfig.json && tsdown",
|
|
49
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
46
50
|
"typecheck:test": "tsc -p tsconfig.test.json --noEmit",
|
|
47
51
|
"test": "vitest run",
|
|
48
52
|
"check": "pnpm run typecheck && pnpm run typecheck:test && pnpm run test && pnpm run build"
|
|
@@ -53,18 +57,34 @@
|
|
|
53
57
|
"dsh": {
|
|
54
58
|
"bundle": {
|
|
55
59
|
"patch": "./cordis.patch.yml"
|
|
60
|
+
},
|
|
61
|
+
"client": {
|
|
62
|
+
"inject": [
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
64
|
+
],
|
|
65
|
+
"platform": "web"
|
|
56
66
|
}
|
|
57
67
|
},
|
|
58
68
|
"peerDependencies": {
|
|
59
69
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
70
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
61
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
71
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
72
|
+
"react": "^18.2.0"
|
|
62
73
|
},
|
|
63
74
|
"devDependencies": {
|
|
64
75
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
76
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
65
79
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
66
80
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
81
|
+
"@tsdown/css": "^0.22.14",
|
|
67
82
|
"@types/node": "^26.2.0",
|
|
83
|
+
"@types/react": "~18.3.1",
|
|
84
|
+
"@types/react-dom": "^18.3.5",
|
|
85
|
+
"react": "^18.3.1",
|
|
86
|
+
"react-dom": "^18.3.1",
|
|
87
|
+
"tsdown": "^0.22.2",
|
|
68
88
|
"typescript": "^7.0.2",
|
|
69
89
|
"vitest": "^4.1.10"
|
|
70
90
|
},
|