dsh-hooks 0.2.2 → 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/lib/server.js ADDED
@@ -0,0 +1,128 @@
1
+ import { createRequire } from 'node:module';
2
+ import { describeHook, evaluateHooks, mockContext } from './dry-run.js';
3
+ import { createHookRunner } from './runner.js';
4
+ import { fireNotify } from './notify.js';
5
+ /** Plugin version, read from package.json (this package ships its own). */
6
+ export function pluginVersion() {
7
+ const require = createRequire(import.meta.url);
8
+ try {
9
+ const pkg = require('../package.json');
10
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
11
+ }
12
+ catch {
13
+ return 'unknown';
14
+ }
15
+ }
16
+ const OK = (value) => ({ ok: true, value });
17
+ const FAIL = (code, message) => ({ ok: false, error: { code, message } });
18
+ function json(res, envelope, status = 200) {
19
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
20
+ res.end(JSON.stringify(envelope));
21
+ }
22
+ /** Loopback fence: never let a LAN client reach /dsh-hooks operations. */
23
+ export function isLoopbackRequest(req) {
24
+ const address = req.socket.remoteAddress ?? '';
25
+ return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1';
26
+ }
27
+ async function readJsonBody(req) {
28
+ const chunks = [];
29
+ let total = 0;
30
+ for await (const chunk of req) {
31
+ const buffer = chunk;
32
+ chunks.push(buffer);
33
+ total += buffer.length;
34
+ if (total > 1 << 20)
35
+ return null;
36
+ }
37
+ const text = Buffer.concat(chunks).toString('utf8');
38
+ if (text === '')
39
+ return null;
40
+ try {
41
+ return JSON.parse(text);
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ /** Create the /dsh-hooks route handler (exported for tests). */
48
+ export function createHookHandler(options) {
49
+ const { hooks, history } = options;
50
+ const version = options.version ?? pluginVersion();
51
+ return async (req, res) => {
52
+ if (!isLoopbackRequest(req)) {
53
+ json(res, FAIL('forbidden', 'loopback-only'), 403);
54
+ return;
55
+ }
56
+ const url = new URL(req.url ?? '/', 'http://x');
57
+ const pathname = url.pathname;
58
+ if (req.method === 'GET' && pathname === '/dsh-hooks/status') {
59
+ json(res, OK({ name: 'dsh-hooks', version, hookCount: hooks.length, historyCount: history.recent().length }));
60
+ return;
61
+ }
62
+ if (req.method === 'GET' && pathname === '/dsh-hooks/history') {
63
+ const raw = url.searchParams.get('n');
64
+ const parsed = raw === null ? 50 : Number(raw);
65
+ const n = Number.isFinite(parsed) && parsed > 0 ? Math.min(500, Math.floor(parsed)) : 50;
66
+ const records = history.recent();
67
+ json(res, OK(records.slice(Math.max(0, records.length - n))));
68
+ return;
69
+ }
70
+ if (req.method === 'POST' && pathname === '/dsh-hooks/test') {
71
+ const contentType = req.headers['content-type'] ?? '';
72
+ if (!contentType.toLowerCase().startsWith('application/json')) {
73
+ json(res, FAIL('bad-request', 'POST 需要 application/json'), 415);
74
+ return;
75
+ }
76
+ const payload = await readJsonBody(req);
77
+ if (typeof payload !== 'object' || payload === null) {
78
+ json(res, FAIL('bad-request', 'malformed JSON body'), 400);
79
+ return;
80
+ }
81
+ const body = payload;
82
+ const event = typeof body.event === 'string' && body.event !== '' ? body.event : null;
83
+ if (event === null) {
84
+ json(res, FAIL('bad-request', '缺少 event 字段'), 400);
85
+ return;
86
+ }
87
+ const reason = typeof body.reason === 'string' && body.reason !== '' ? body.reason : undefined;
88
+ const ctx = mockContext(event, {
89
+ reason,
90
+ tool: typeof body.tool === 'string' ? body.tool : undefined,
91
+ sessionName: typeof body.sessionName === 'string' ? body.sessionName : undefined,
92
+ });
93
+ const lines = evaluateHooks(hooks, event, ctx, reason);
94
+ const matchedHooks = lines.filter((line) => line.matched);
95
+ const execute = body.execute === true;
96
+ if (execute) {
97
+ const runner = createHookRunner();
98
+ for (const line of matchedHooks) {
99
+ const hook = hooks[line.index - 1];
100
+ if (hook.run)
101
+ runner.run(hook, ctx);
102
+ else if (hook.notify)
103
+ void fireNotify(hook.notify, ctx);
104
+ }
105
+ }
106
+ json(res, OK({
107
+ event,
108
+ reason,
109
+ executed: execute,
110
+ total: hooks.length,
111
+ matched: matchedHooks.length,
112
+ lines: lines.map((line) => ({
113
+ index: line.index,
114
+ matched: line.matched,
115
+ why: line.why,
116
+ summary: line.summary,
117
+ action: line.matched ? describeHook(hooks[line.index - 1]) : undefined,
118
+ })),
119
+ }));
120
+ return;
121
+ }
122
+ json(res, FAIL('not-found', `unknown route ${pathname}`), 404);
123
+ };
124
+ }
125
+ /** Register the /dsh-hooks prefix route on the shared web server. */
126
+ export function registerHookRoutes(webServer, options) {
127
+ return webServer.register({ kind: 'prefix', path: '/dsh-hooks', handler: createHookHandler(options) });
128
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-hooks",
3
- "version": "0.2.2",
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,20 +57,36 @@
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",
67
- "@types/node": "^22.10.0",
68
- "typescript": "^5.6.0",
69
- "vitest": "^3.0.0"
81
+ "@tsdown/css": "^0.22.14",
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",
88
+ "typescript": "^7.0.2",
89
+ "vitest": "^4.1.10"
70
90
  },
71
91
  "dependencies": {
72
92
  "@larksuiteoapi/node-sdk": "^1.73.0",