dsh-hooks 0.3.0 → 0.5.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 +38 -7
- package/README.zh.md +37 -6
- package/bin/dsh-hooks.mjs +37 -197
- package/examples/notify-feishu.d.mts +31 -0
- package/lib/client.js +683 -0
- package/lib/feishu-session.d.ts +47 -0
- package/lib/feishu-session.js +94 -0
- package/lib/feishu.d.ts +119 -0
- package/lib/feishu.js +282 -0
- package/lib/history.d.ts +5 -0
- package/lib/history.js +102 -3
- package/lib/index.js +13 -4
- package/lib/server.d.ts +15 -3
- package/lib/server.js +93 -0
- package/package.json +26 -6
package/lib/history.js
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* Hook execution history: an in-memory ring buffer plus a best-effort
|
|
3
3
|
* JSONL append log under ~/.dsh/dsh-hooks/ (0600, owner-only). History is
|
|
4
4
|
* strictly best-effort — a failed write never breaks a hook.
|
|
5
|
+
*
|
|
6
|
+
* The buffer is not process-private memory only: it seeds from the JSONL at
|
|
7
|
+
* startup and `sync()` incrementally ingests bytes appended since the last
|
|
8
|
+
* read, so records written before a restart (or by another dsh process
|
|
9
|
+
* sharing the file, e.g. a task-board Host) surface in the web GUI instead
|
|
10
|
+
* of vanishing with the process.
|
|
5
11
|
*/
|
|
6
|
-
import { appendFileSync, chmodSync, mkdirSync } from 'node:fs';
|
|
12
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, } from 'node:fs';
|
|
7
13
|
import { homedir } from 'node:os';
|
|
8
14
|
import { dirname, join } from 'node:path';
|
|
9
15
|
export const DEFAULT_HISTORY_PATH = join(homedir(), '.dsh', 'dsh-hooks', 'history.jsonl');
|
|
@@ -15,11 +21,101 @@ export function createHistorySink(options = {}) {
|
|
|
15
21
|
const buffer = [];
|
|
16
22
|
let dirReady = false;
|
|
17
23
|
let chmodded = false;
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
/** Bytes of `file` already ingested into the buffer. */
|
|
25
|
+
let syncedBytes = 0;
|
|
26
|
+
/** Trailing fragment of the last read that did not end with a newline. */
|
|
27
|
+
let pending = '';
|
|
28
|
+
function push(entry) {
|
|
20
29
|
buffer.push(entry);
|
|
21
30
|
if (buffer.length > max)
|
|
22
31
|
buffer.splice(0, buffer.length - max);
|
|
32
|
+
}
|
|
33
|
+
/** Parse complete JSONL lines into the ring buffer; incomplete tails stay pending. */
|
|
34
|
+
function ingest(text) {
|
|
35
|
+
pending += text;
|
|
36
|
+
const lines = pending.split('\n');
|
|
37
|
+
pending = lines.pop() ?? '';
|
|
38
|
+
for (const line of lines) {
|
|
39
|
+
if (line === '')
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
const entry = JSON.parse(line);
|
|
43
|
+
if (typeof entry !== 'object' || entry === null || typeof entry.ts !== 'number')
|
|
44
|
+
continue;
|
|
45
|
+
push(entry);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Broken line (mid-write or foreign content): skip, never fail.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Rebuild the buffer from the whole file (startup seed / truncated file). */
|
|
53
|
+
function rebuild() {
|
|
54
|
+
buffer.length = 0;
|
|
55
|
+
pending = '';
|
|
56
|
+
syncedBytes = 0;
|
|
57
|
+
const text = readFileSync(file, 'utf8');
|
|
58
|
+
syncedBytes = Buffer.byteLength(text, 'utf8');
|
|
59
|
+
ingest(text);
|
|
60
|
+
}
|
|
61
|
+
/** Seed the ring buffer from an existing JSONL log (best-effort). */
|
|
62
|
+
function seed() {
|
|
63
|
+
if (!enabled)
|
|
64
|
+
return;
|
|
65
|
+
try {
|
|
66
|
+
if (!existsSync(file))
|
|
67
|
+
return;
|
|
68
|
+
rebuild();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Seeding is best-effort; recording starts from an empty buffer.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Ingest every byte appended since the last read (own writes included). */
|
|
75
|
+
function sync() {
|
|
76
|
+
if (!enabled)
|
|
77
|
+
return;
|
|
78
|
+
try {
|
|
79
|
+
if (!existsSync(file))
|
|
80
|
+
return;
|
|
81
|
+
const size = statSync(file).size;
|
|
82
|
+
if (size === syncedBytes)
|
|
83
|
+
return;
|
|
84
|
+
if (size < syncedBytes) {
|
|
85
|
+
// The file shrank (rotation/truncation): rebuild from its tail.
|
|
86
|
+
rebuild();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const deltaBytes = size - syncedBytes;
|
|
90
|
+
const fd = openSync(file, 'r');
|
|
91
|
+
try {
|
|
92
|
+
const chunk = Buffer.allocUnsafe(deltaBytes);
|
|
93
|
+
let total = 0;
|
|
94
|
+
while (total < deltaBytes) {
|
|
95
|
+
const n = readSync(fd, chunk, total, deltaBytes - total, syncedBytes + total);
|
|
96
|
+
if (n <= 0)
|
|
97
|
+
break;
|
|
98
|
+
total += n;
|
|
99
|
+
}
|
|
100
|
+
ingest(chunk.subarray(0, total).toString('utf8'));
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
closeSync(fd);
|
|
104
|
+
}
|
|
105
|
+
syncedBytes = size;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Sync is best-effort; the next call retries.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function record(partial) {
|
|
112
|
+
const entry = { ...partial, ts: Date.now() };
|
|
113
|
+
// Ingest other processes' appends BEFORE our own entry so the buffer
|
|
114
|
+
// stays in file order, and `syncedBytes` stays a true prefix of the
|
|
115
|
+
// file (otherwise our own advance would skip the foreign appends).
|
|
116
|
+
if (enabled)
|
|
117
|
+
sync();
|
|
118
|
+
push(entry);
|
|
23
119
|
if (!enabled)
|
|
24
120
|
return;
|
|
25
121
|
try {
|
|
@@ -28,6 +124,7 @@ export function createHistorySink(options = {}) {
|
|
|
28
124
|
dirReady = true;
|
|
29
125
|
}
|
|
30
126
|
appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8');
|
|
127
|
+
syncedBytes = statSync(file).size;
|
|
31
128
|
if (!chmodded) {
|
|
32
129
|
try {
|
|
33
130
|
chmodSync(file, 0o600);
|
|
@@ -42,9 +139,11 @@ export function createHistorySink(options = {}) {
|
|
|
42
139
|
// History is best-effort: a failed write never breaks a hook.
|
|
43
140
|
}
|
|
44
141
|
}
|
|
142
|
+
seed();
|
|
45
143
|
return {
|
|
46
144
|
record,
|
|
47
145
|
recent: () => buffer,
|
|
146
|
+
sync,
|
|
48
147
|
dispose: () => { },
|
|
49
148
|
};
|
|
50
149
|
}
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { eventLabel } from './context.js';
|
|
|
5
5
|
import { createHookRunner } from './runner.js';
|
|
6
6
|
import { fireNotify } from './notify.js';
|
|
7
7
|
import { createHistorySink } from './history.js';
|
|
8
|
+
import { createFeishuSetupManager } from './feishu-session.js';
|
|
8
9
|
import { registerHookRoutes } from './server.js';
|
|
9
10
|
export const name = 'dsh-hooks';
|
|
10
11
|
// Dependency on the session service: `session/event` only exists once a
|
|
@@ -22,12 +23,20 @@ export function apply(ctx, config = {}) {
|
|
|
22
23
|
const hooks = config.hooks ?? [];
|
|
23
24
|
const history = createHistorySink(config.history ?? undefined);
|
|
24
25
|
const runner = createHookRunner((line) => ctx.logger?.info(line), (record) => history.record(record));
|
|
25
|
-
// Web-profile extras: /dsh-hooks routes
|
|
26
|
-
// services are optional — CLI/headless
|
|
27
|
-
// plugin keeps working there untouched.
|
|
26
|
+
// Web-profile extras: /dsh-hooks routes (incl. the Feishu connect flow)
|
|
27
|
+
// and the agent announcement. Both services are optional — CLI/headless
|
|
28
|
+
// profiles provide neither, and the plugin keeps working there untouched.
|
|
28
29
|
const webServer = ctx.get('webServer', false);
|
|
29
30
|
if (webServer !== undefined) {
|
|
30
|
-
|
|
31
|
+
const feishu = createFeishuSetupManager();
|
|
32
|
+
ctx.effect(() => {
|
|
33
|
+
const unregister = registerHookRoutes(webServer, { hooks, history, feishu: { manager: feishu } });
|
|
34
|
+
return () => {
|
|
35
|
+
unregister();
|
|
36
|
+
// Abort an in-flight QR scan so it never outlives the plugin.
|
|
37
|
+
feishu.dispose();
|
|
38
|
+
};
|
|
39
|
+
}, 'dsh-hooks: /dsh-hooks routes');
|
|
31
40
|
}
|
|
32
41
|
const systemPrompt = ctx.get('systemPrompt', false);
|
|
33
42
|
if (systemPrompt !== undefined) {
|
package/lib/server.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* /dsh-hooks/* HTTP routes for the web profile: status, execution history,
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* a dry-run-style test trigger, and the Feishu connect flow (QR setup /
|
|
4
|
+
* cancel / test card). Registered only when the shared webserver service
|
|
5
|
+
* exists (web profile) — CLI/headless environments never see them.
|
|
6
|
+
* Loopback-only with JSON envelopes; POSTs require an explicit
|
|
6
7
|
* application/json content-type (CSRF hardening, same posture as
|
|
7
8
|
* dsh-aionui-panel).
|
|
8
9
|
*/
|
|
9
10
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
11
|
import type { HookSpec } from './config.js';
|
|
11
12
|
import type { HistorySink } from './history.js';
|
|
13
|
+
import { type FeishuSetupManager } from './feishu-session.js';
|
|
14
|
+
import { runFeishuTest } from './feishu.js';
|
|
12
15
|
/** Minimal structural shape of the shared web server (dsh-host-webserver). */
|
|
13
16
|
export interface WebServerLike {
|
|
14
17
|
register(spec: {
|
|
@@ -21,10 +24,19 @@ export interface WebServerLike {
|
|
|
21
24
|
export declare function pluginVersion(): string;
|
|
22
25
|
/** Loopback fence: never let a LAN client reach /dsh-hooks operations. */
|
|
23
26
|
export declare function isLoopbackRequest(req: IncomingMessage): boolean;
|
|
27
|
+
export interface FeishuRouteDeps {
|
|
28
|
+
/** QR-scan session manager (one in-flight flow at a time). */
|
|
29
|
+
manager: FeishuSetupManager;
|
|
30
|
+
/** Test-card sender, injectable for tests. */
|
|
31
|
+
runTest?: typeof runFeishuTest;
|
|
32
|
+
/** Credential file the status route summarizes. */
|
|
33
|
+
configPath?: string;
|
|
34
|
+
}
|
|
24
35
|
export interface HookRoutesOptions {
|
|
25
36
|
hooks: readonly HookSpec[];
|
|
26
37
|
history: HistorySink;
|
|
27
38
|
version?: string;
|
|
39
|
+
feishu?: FeishuRouteDeps;
|
|
28
40
|
}
|
|
29
41
|
/** Create the /dsh-hooks route handler (exported for tests). */
|
|
30
42
|
export declare function createHookHandler(options: HookRoutesOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
package/lib/server.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { describeHook, evaluateHooks, mockContext } from './dry-run.js';
|
|
3
3
|
import { createHookRunner } from './runner.js';
|
|
4
4
|
import { fireNotify } from './notify.js';
|
|
5
|
+
import { FEISHU_SETUP_BUSY } from './feishu-session.js';
|
|
6
|
+
import { readFeishuSummary, runFeishuTest, updateFeishuResultMaxChars } from './feishu.js';
|
|
5
7
|
/** Plugin version, read from package.json (this package ships its own). */
|
|
6
8
|
export function pluginVersion() {
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
@@ -48,6 +50,9 @@ async function readJsonBody(req) {
|
|
|
48
50
|
export function createHookHandler(options) {
|
|
49
51
|
const { hooks, history } = options;
|
|
50
52
|
const version = options.version ?? pluginVersion();
|
|
53
|
+
const feishu = options.feishu;
|
|
54
|
+
const runFeishuTestCard = feishu?.runTest ?? runFeishuTest;
|
|
55
|
+
const feishuConfigPath = feishu?.configPath;
|
|
51
56
|
return async (req, res) => {
|
|
52
57
|
if (!isLoopbackRequest(req)) {
|
|
53
58
|
json(res, FAIL('forbidden', 'loopback-only'), 403);
|
|
@@ -56,6 +61,9 @@ export function createHookHandler(options) {
|
|
|
56
61
|
const url = new URL(req.url ?? '/', 'http://x');
|
|
57
62
|
const pathname = url.pathname;
|
|
58
63
|
if (req.method === 'GET' && pathname === '/dsh-hooks/status') {
|
|
64
|
+
// Pull in disk records (pre-restart and other-process appends) so the
|
|
65
|
+
// badge reflects the durable log, not just this process's memory.
|
|
66
|
+
history.sync();
|
|
59
67
|
json(res, OK({ name: 'dsh-hooks', version, hookCount: hooks.length, historyCount: history.recent().length }));
|
|
60
68
|
return;
|
|
61
69
|
}
|
|
@@ -63,6 +71,7 @@ export function createHookHandler(options) {
|
|
|
63
71
|
const raw = url.searchParams.get('n');
|
|
64
72
|
const parsed = raw === null ? 50 : Number(raw);
|
|
65
73
|
const n = Number.isFinite(parsed) && parsed > 0 ? Math.min(500, Math.floor(parsed)) : 50;
|
|
74
|
+
history.sync();
|
|
66
75
|
const records = history.recent();
|
|
67
76
|
json(res, OK(records.slice(Math.max(0, records.length - n))));
|
|
68
77
|
return;
|
|
@@ -119,6 +128,90 @@ export function createHookHandler(options) {
|
|
|
119
128
|
}));
|
|
120
129
|
return;
|
|
121
130
|
}
|
|
131
|
+
if (feishu !== undefined && req.method === 'GET' && pathname === '/dsh-hooks/feishu/status') {
|
|
132
|
+
const summary = readFeishuSummary(feishuConfigPath);
|
|
133
|
+
json(res, OK({ ...summary, setup: feishu.manager.status() }));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (feishu !== undefined && req.method === 'POST' && pathname === '/dsh-hooks/feishu/setup') {
|
|
137
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
138
|
+
if (!contentType.toLowerCase().startsWith('application/json')) {
|
|
139
|
+
json(res, FAIL('bad-request', 'POST 需要 application/json'), 415);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const payload = await readJsonBody(req);
|
|
143
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
144
|
+
json(res, FAIL('bad-request', 'malformed JSON body'), 400);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const body = payload;
|
|
148
|
+
const profile = typeof body.profile === 'string' && body.profile.trim() !== '' ? body.profile.trim() : 'web';
|
|
149
|
+
const resultMaxChars = typeof body.resultMaxChars === 'number' && Number.isFinite(body.resultMaxChars)
|
|
150
|
+
? body.resultMaxChars
|
|
151
|
+
: undefined;
|
|
152
|
+
try {
|
|
153
|
+
const setup = resultMaxChars === undefined
|
|
154
|
+
? await feishu.manager.start(profile)
|
|
155
|
+
: await feishu.manager.start(profile, { resultMaxChars });
|
|
156
|
+
json(res, OK({ setup }));
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
json(res, FAIL('pending', message), message === FEISHU_SETUP_BUSY ? 409 : 500);
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (feishu !== undefined && req.method === 'POST' && pathname === '/dsh-hooks/feishu/config') {
|
|
165
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
166
|
+
if (!contentType.toLowerCase().startsWith('application/json')) {
|
|
167
|
+
json(res, FAIL('bad-request', 'POST 需要 application/json'), 415);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const payload = await readJsonBody(req);
|
|
171
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
172
|
+
json(res, FAIL('bad-request', 'malformed JSON body'), 400);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const value = payload.resultMaxChars;
|
|
176
|
+
if (typeof value !== 'number') {
|
|
177
|
+
json(res, FAIL('bad-request', '缺少数字字段 resultMaxChars'), 400);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const resultMaxChars = updateFeishuResultMaxChars(feishuConfigPath, value);
|
|
182
|
+
json(res, OK({ resultMaxChars }));
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
186
|
+
json(res, FAIL('bad-request', message), 400);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (feishu !== undefined && req.method === 'POST' && pathname === '/dsh-hooks/feishu/cancel') {
|
|
191
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
192
|
+
if (!contentType.toLowerCase().startsWith('application/json')) {
|
|
193
|
+
json(res, FAIL('bad-request', 'POST 需要 application/json'), 415);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
json(res, OK({ cancelled: feishu.manager.cancel() }));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (feishu !== undefined && req.method === 'POST' && pathname === '/dsh-hooks/feishu/test') {
|
|
200
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
201
|
+
if (!contentType.toLowerCase().startsWith('application/json')) {
|
|
202
|
+
json(res, FAIL('bad-request', 'POST 需要 application/json'), 415);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
const message = await runFeishuTestCard();
|
|
207
|
+
json(res, OK({ message }));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
json(res, FAIL('send-failed', message), 500);
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
122
215
|
json(res, FAIL('not-found', `unknown route ${pathname}`), 404);
|
|
123
216
|
};
|
|
124
217
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-hooks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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 + Feishu connect).",
|
|
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
|
},
|