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 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 面板](packages/dsh-hooks-ui/README.md)
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.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
- ".": "./lib/index.js",
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
  },