deepseek-harness-background 0.2.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/index.js ADDED
@@ -0,0 +1,540 @@
1
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { randomBytes } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
+ import { extname, join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ //#region src/settings.ts
8
+ /** Durable background settings — pure constants/types shared by both halves. */
9
+ /** Settings namespace owned by the background plugin. */
10
+ const BACKGROUND_SETTINGS_NAMESPACE = "ui-background";
11
+ /**
12
+ * The schema's field names, in schema order. Used as a whitelist when a
13
+ * section write commits: the settings layer keeps unknown keys in the user
14
+ * document (a legacy schema field would linger forever under a merge-only
15
+ * write), so the write path strips everything outside this list.
16
+ */
17
+ const BACKGROUND_SETTINGS_FIELDS = [
18
+ "enabled",
19
+ "uploadId",
20
+ "url",
21
+ "opacity",
22
+ "scrim",
23
+ "panelOpacity",
24
+ "blur",
25
+ "wallpaperBlur",
26
+ "fit"
27
+ ];
28
+ /** Browser-facing base path of the background API (shared with the host routes). */
29
+ const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
30
+ /** Minimum scrim opacity (0 = no veil at all). */
31
+ const SCRIM_MIN = 0;
32
+ /** Maximum scrim opacity (a nearly opaque veil keeps text legible over any art). */
33
+ const SCRIM_MAX = .95;
34
+ /** Default scrim strength applied over user art (a light readability veil). */
35
+ const DEFAULT_SCRIM = .25;
36
+ /** Default background image opacity (1 = fully opaque). */
37
+ const DEFAULT_OPACITY = 1;
38
+ /** Minimum background image opacity (0 = fully invisible). */
39
+ const OPACITY_MIN = 0;
40
+ /** Maximum background image opacity (1 = fully opaque). */
41
+ const OPACITY_MAX = 1;
42
+ /** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
43
+ const DEFAULT_PANEL_OPACITY = .15;
44
+ /** Default background rendering mode. */
45
+ const DEFAULT_FIT = "cover";
46
+ /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
47
+ const FIT_MODES = ["cover", "contain"];
48
+ //#endregion
49
+ //#region src/schema.ts
50
+ /** Host-side schema for the `ui-background` settings namespace. */
51
+ /**
52
+ * Durable background section; also the wire envelope the browser scope
53
+ * validates against. `uploadId` and `url` name the two exclusive sources; the
54
+ * schema stays structural (no trim/transform — a function callback would break
55
+ * the schema's toJSON wire serialization).
56
+ */
57
+ const BackgroundSettingsSchema = z.object({
58
+ enabled: z.boolean().default(false),
59
+ /** Content-addressed local upload id or empty. */
60
+ uploadId: z.string().default(""),
61
+ /** Image URL or empty (mutually exclusive with uploadId at runtime). */
62
+ url: z.string().default(""),
63
+ opacity: z.number().min(0).max(1).default(1),
64
+ scrim: z.number().min(0).max(SCRIM_MAX).default(DEFAULT_SCRIM),
65
+ panelOpacity: z.number().min(0).max(1).default(DEFAULT_PANEL_OPACITY),
66
+ blur: z.number().min(0).max(40).default(16),
67
+ wallpaperBlur: z.number().min(0).max(60).default(0),
68
+ fit: z.union([...FIT_MODES]).default(DEFAULT_FIT)
69
+ });
70
+ //#endregion
71
+ //#region src/harness-home.ts
72
+ /**
73
+ * Harness-home resolution — where the uploaded wallpaper images live.
74
+ *
75
+ * Mirrors the dsh launcher's precedence (see apps/cli reference and the
76
+ * skin-center harness-home.ts in dsh-web-ui): an explicit `--home`-formatted
77
+ * override, else `$DSH_HOME`, else `<homedir>/.dsh`. Kept as a tiny pure
78
+ * function so the host half needs no out-of-tree dependency on the internal
79
+ * home-paths package.
80
+ */
81
+ /** First non-blank string in a candidate list, else undefined. */
82
+ function firstNonBlank(...values) {
83
+ for (const value of values) if (typeof value === "string" && value.trim() !== "") return value.trim();
84
+ }
85
+ /**
86
+ * Resolve the harness home (the directory containing `profiles/`): explicit
87
+ * override wins, then `$DSH_HOME`, then `~/.dsh`.
88
+ * @param env - environment (defaults to process.env).
89
+ * @returns the absolute harness home directory.
90
+ */
91
+ function resolveHarnessHome(env = process.env) {
92
+ return firstNonBlank(env.DSH_HOME) ?? join(homedir(), ".dsh");
93
+ }
94
+ /** Subdirectory under the harness home that this plugin owns. */
95
+ const PLUGIN_HOME_REL = join("deepseek-harness-background");
96
+ //#endregion
97
+ //#region src/routes.ts
98
+ /**
99
+ * Host HTTP routes for the background plugin — the browser half talks to the
100
+ * host through plain same-origin endpoints (no settings RPC involved, so the
101
+ * api-proxy's settings allowlist cannot shadow the namespace). The section
102
+ * still lives in the official settings document: reads/writes go through
103
+ * `ctx.settings` in-process, which the allowlist only gates at the RPC
104
+ * boundary.
105
+ *
106
+ * Route family (`/api/bg-wallpaper`):
107
+ * GET /settings read the section
108
+ * POST /settings write the section (cross-site fenced)
109
+ * POST /upload accept an uploaded image body, store it under the
110
+ * harness home, return its content-addressed url
111
+ * GET /image/<id> serve a stored upload (path-escape fenced)
112
+ *
113
+ * Same pattern as dsh-web-ui's `/api/skin-center` and `/api/pet` families.
114
+ */
115
+ /** Resolved section when the stored document has no user layer. */
116
+ const DEFAULTS = {
117
+ enabled: false,
118
+ uploadId: "",
119
+ url: "",
120
+ opacity: 1,
121
+ scrim: DEFAULT_SCRIM,
122
+ panelOpacity: DEFAULT_PANEL_OPACITY,
123
+ blur: 16,
124
+ wallpaperBlur: 0,
125
+ fit: DEFAULT_FIT
126
+ };
127
+ /** Keep only the schema's known fields (see BACKGROUND_SETTINGS_FIELDS). */
128
+ function pickKnown(section) {
129
+ const out = {};
130
+ const src = section;
131
+ for (const key of BACKGROUND_SETTINGS_FIELDS) if (key in src) out[key] = src[key];
132
+ return out;
133
+ }
134
+ /** Accepted upload MIME type → file extension. */
135
+ const ACCEPTED_TYPES = {
136
+ "image/jpeg": "jpg",
137
+ "image/png": "png",
138
+ "image/webp": "webp",
139
+ "image/gif": "gif"
140
+ };
141
+ /** Numeric knob bounds — same ranges as the schema, duplicated here so the
142
+ * POST route can reject out-of-range numbers before they reach the schema's
143
+ * JSON layer (that layer runs `Number(...)` on string inputs, which silently
144
+ * coerces e.g. `"2"` into `2` instead of rejecting the body). */
145
+ const NUM_BOUNDS = {
146
+ opacity: {
147
+ min: 0,
148
+ max: 1
149
+ },
150
+ scrim: {
151
+ min: 0,
152
+ max: SCRIM_MAX
153
+ },
154
+ panelOpacity: {
155
+ min: 0,
156
+ max: 1
157
+ },
158
+ blur: {
159
+ min: 0,
160
+ max: 40
161
+ },
162
+ wallpaperBlur: {
163
+ min: 0,
164
+ max: 60
165
+ }
166
+ };
167
+ /** Magic-byte signatures for the accepted formats (first bytes). */
168
+ function sniffImage(bytes) {
169
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "jpg";
170
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) return "png";
171
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "webp";
172
+ if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56) return "gif";
173
+ return null;
174
+ }
175
+ /** Per-upload size cap (20 MiB). */
176
+ const MAX_UPLOAD_BYTES = 20971520;
177
+ /** A stored upload id is `<up-` + hex + `>`; the on-disk file appends its extension. */
178
+ function isUploadId(id) {
179
+ return /^up-[a-f0-9]+$/.test(id);
180
+ }
181
+ /** One JSON response. */
182
+ function json(res, status, body) {
183
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
184
+ res.end(JSON.stringify(body));
185
+ }
186
+ /** Require the method or answer 405. */
187
+ function requireMethod(req, res, method) {
188
+ if (req.method === method) return true;
189
+ json(res, 405, {
190
+ ok: false,
191
+ error: "method-not-allowed"
192
+ });
193
+ return false;
194
+ }
195
+ /**
196
+ * Same-origin fence. Browsers send `Sec-Fetch-Site` on every fetch; a
197
+ * cross-site fetch is always rejected, and an `Origin` that does not match
198
+ * the request `Host` is rejected. Requests without either header (curl, node
199
+ * http) pass — this is a local single-user tool, and the fence only targets
200
+ * the cross-site browser vector.
201
+ */
202
+ function isSameOriginRequest(req) {
203
+ const site = req.headers["sec-fetch-site"];
204
+ if (typeof site === "string" && site === "cross-site") return false;
205
+ const origin = req.headers.origin;
206
+ if (typeof origin === "string" && origin !== "" && origin !== "null") {
207
+ const host = req.headers.host;
208
+ if (typeof host !== "string" || host === "") return false;
209
+ try {
210
+ return new URL(origin).host === host;
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+ return true;
216
+ }
217
+ /** Read the raw request body (bounded; rejects without destroying the socket
218
+ * so the caller can still write a clean error response). */
219
+ function readRawBody(req, limit) {
220
+ return new Promise((resolve, reject) => {
221
+ const chunks = [];
222
+ let size = 0;
223
+ let tooLarge = false;
224
+ req.on("data", (chunk) => {
225
+ size += chunk.length;
226
+ if (size > limit) {
227
+ tooLarge = true;
228
+ req.pause();
229
+ reject(/* @__PURE__ */ new Error("body-too-large"));
230
+ return;
231
+ }
232
+ chunks.push(chunk);
233
+ });
234
+ req.on("end", () => {
235
+ if (tooLarge) return;
236
+ resolve(Buffer.concat(chunks));
237
+ });
238
+ req.on("error", reject);
239
+ });
240
+ }
241
+ /** Read the request body as JSON (bounded; rejects without destroying the socket). */
242
+ function readJsonBody(req) {
243
+ return new Promise((resolve, reject) => {
244
+ const parts = [];
245
+ let size = 0;
246
+ let tooLarge = false;
247
+ req.on("data", (chunk) => {
248
+ size += chunk.length;
249
+ if (size > 65536) {
250
+ tooLarge = true;
251
+ req.pause();
252
+ reject(/* @__PURE__ */ new Error("body-too-large"));
253
+ return;
254
+ }
255
+ parts.push(chunk);
256
+ });
257
+ req.on("end", () => {
258
+ if (tooLarge) return;
259
+ try {
260
+ const text = Buffer.concat(parts).toString("utf8");
261
+ resolve(text === "" ? {} : JSON.parse(text));
262
+ } catch {
263
+ reject(/* @__PURE__ */ new Error("invalid-json"));
264
+ }
265
+ });
266
+ req.on("error", reject);
267
+ });
268
+ }
269
+ /**
270
+ * Validate a client-posted section body. Returns an error code when the body
271
+ * must be rejected, or null when it may be written. Beyond shape checks this
272
+ * enforces the section's exclusivity contract (`uploadId` and `url` are
273
+ * alternative sources — at most one may be set) and that a referenced upload
274
+ * actually exists on disk, so a stale id cannot render a blank backdrop.
275
+ * @param body - the parsed JSON body.
276
+ * @param home - the plugin home used to resolve upload ids (tests inject a surrogate).
277
+ * @returns an error code, or null when the body is acceptable.
278
+ */
279
+ function validateSectionBody(body, home) {
280
+ const url = body.url;
281
+ const uploadId = body.uploadId;
282
+ if (url !== void 0) {
283
+ if (typeof url !== "string" || url.length > 4096 || url !== "" && !/^https?:\/\//i.test(url)) return "invalid-url";
284
+ }
285
+ if (uploadId !== void 0) {
286
+ if (typeof uploadId !== "string" || uploadId !== "" && !isUploadId(uploadId)) return "invalid-upload-id";
287
+ if (uploadId !== "" && typeof url === "string" && url !== "") return "mutually-exclusive-source";
288
+ if (uploadId !== "" && uploadPath(uploadId, home) === "") return "upload-not-found";
289
+ }
290
+ for (const [key, { min, max }] of Object.entries(NUM_BOUNDS)) {
291
+ const raw = body[key];
292
+ if (raw === void 0) continue;
293
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < min || raw > max) return "invalid-range";
294
+ }
295
+ return null;
296
+ }
297
+ /**
298
+ * The directory this plugin owns under the harness home. `homeOverride`
299
+ * injects a surrogate for tests; default resolves via process.env.
300
+ */
301
+ function pluginHome(homeOverride) {
302
+ return join(homeOverride ?? resolveHarnessHome(), PLUGIN_HOME_REL);
303
+ }
304
+ /** Ensure and return the uploads directory. */
305
+ function ensurePluginHome(homeOverride) {
306
+ const dir = pluginHome(homeOverride);
307
+ mkdirSync(dir, { recursive: true });
308
+ return dir;
309
+ }
310
+ /**
311
+ * Stores a validated image body under the plugin home, content-addressed by a
312
+ * fresh random id. Returns the id and the browser-facing url.
313
+ */
314
+ function storeUpload(bytes, mime, homeOverride) {
315
+ const declaredExt = ACCEPTED_TYPES[mime];
316
+ const sniffed = sniffImage(bytes);
317
+ if (declaredExt === void 0 || sniffed === null || declaredExt !== sniffed) throw new Error("unsupported-or-mismatched-image");
318
+ if (bytes.length > MAX_UPLOAD_BYTES) throw new Error("upload-too-large");
319
+ const id = "up-" + randomBytes(12).toString("hex");
320
+ const dir = ensurePluginHome(homeOverride);
321
+ writeFileSync(join(dir, `${id}.${sniffed}`), bytes);
322
+ return {
323
+ id,
324
+ url: `${BACKGROUND_API_PREFIX}/image/${id}`
325
+ };
326
+ }
327
+ /** Resolve an upload id to its on-disk path (or "" if invalid / missing). */
328
+ function uploadPath(id, homeOverride) {
329
+ if (!isUploadId(id)) return "";
330
+ const dir = ensurePluginHome(homeOverride);
331
+ for (const ext of [
332
+ "jpg",
333
+ "jpeg",
334
+ "png",
335
+ "webp",
336
+ "gif"
337
+ ]) {
338
+ const candidate = join(dir, `${id}.${ext}`);
339
+ if (existsSync(candidate)) return candidate;
340
+ }
341
+ return "";
342
+ }
343
+ /**
344
+ * Delete one stored upload file. Missing files are ignored; only well-formed
345
+ * upload ids reach the disk (the same path-escape fence as uploadPath).
346
+ */
347
+ function deleteUploadFile(id, homeOverride) {
348
+ if (!isUploadId(id)) return;
349
+ const abs = uploadPath(id, homeOverride);
350
+ if (abs === "") return;
351
+ try {
352
+ unlinkSync(abs);
353
+ } catch {}
354
+ }
355
+ /** MIME for a stored file path. */
356
+ function mimeForPath(path) {
357
+ const ext = extname(path).slice(1);
358
+ if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
359
+ if (ext === "png") return "image/png";
360
+ if (ext === "webp") return "image/webp";
361
+ if (ext === "gif") return "image/gif";
362
+ }
363
+ /** One background API route family over the settings provider + home. */
364
+ function makeBackgroundRoutes(settings, opts = {}) {
365
+ const readSection = () => {
366
+ const section = settings.get(BACKGROUND_SETTINGS_NAMESPACE);
367
+ return {
368
+ ...DEFAULTS,
369
+ ...section
370
+ };
371
+ };
372
+ return [
373
+ {
374
+ kind: "exact",
375
+ path: `${BACKGROUND_API_PREFIX}/settings`,
376
+ handler: async (req, res) => {
377
+ if (!isSameOriginRequest(req)) {
378
+ json(res, 403, {
379
+ ok: false,
380
+ error: "cross-site-request-rejected"
381
+ });
382
+ return;
383
+ }
384
+ if (req.method === "GET") {
385
+ json(res, 200, {
386
+ ok: true,
387
+ value: readSection()
388
+ });
389
+ return;
390
+ }
391
+ if (req.method === "POST") {
392
+ try {
393
+ const before = readSection();
394
+ const body = await readJsonBody(req);
395
+ const validationError = validateSectionBody(body, opts.home);
396
+ if (validationError !== null) {
397
+ json(res, 400, {
398
+ ok: false,
399
+ error: validationError
400
+ });
401
+ return;
402
+ }
403
+ const merged = {
404
+ ...pickKnown(readSection()),
405
+ ...pickKnown(body)
406
+ };
407
+ await settings.replace(BACKGROUND_SETTINGS_NAMESPACE, merged);
408
+ const after = readSection();
409
+ if (before.uploadId !== "" && before.uploadId !== after.uploadId) deleteUploadFile(before.uploadId, opts.home);
410
+ json(res, 200, {
411
+ ok: true,
412
+ value: after
413
+ });
414
+ } catch (error) {
415
+ json(res, 400, {
416
+ ok: false,
417
+ error: error instanceof Error ? error.message : String(error)
418
+ });
419
+ }
420
+ return;
421
+ }
422
+ json(res, 405, {
423
+ ok: false,
424
+ error: "method-not-allowed"
425
+ });
426
+ }
427
+ },
428
+ {
429
+ kind: "exact",
430
+ path: `${BACKGROUND_API_PREFIX}/upload`,
431
+ handler: async (req, res) => {
432
+ if (!isSameOriginRequest(req)) {
433
+ json(res, 403, {
434
+ ok: false,
435
+ error: "cross-site-request-rejected"
436
+ });
437
+ return;
438
+ }
439
+ if (!requireMethod(req, res, "POST")) return;
440
+ const mime = req.headers["content-type"]?.split(";")[0]?.trim() ?? "";
441
+ if (!ACCEPTED_TYPES[mime]) {
442
+ json(res, 400, {
443
+ ok: false,
444
+ error: "unsupported-media-type"
445
+ });
446
+ return;
447
+ }
448
+ if (req.readableAborted || req.destroyed) {
449
+ json(res, 400, {
450
+ ok: false,
451
+ error: "request-aborted"
452
+ });
453
+ return;
454
+ }
455
+ try {
456
+ const body = await readRawBody(req, 20971521);
457
+ if (body.length === 0) {
458
+ json(res, 400, {
459
+ ok: false,
460
+ error: "empty-body"
461
+ });
462
+ return;
463
+ }
464
+ const { id, url } = storeUpload(body, mime, opts.home);
465
+ json(res, 200, {
466
+ ok: true,
467
+ id,
468
+ url
469
+ });
470
+ } catch (error) {
471
+ json(res, 400, {
472
+ ok: false,
473
+ error: error instanceof Error ? error.message : String(error)
474
+ });
475
+ }
476
+ }
477
+ },
478
+ {
479
+ kind: "prefix",
480
+ path: `${BACKGROUND_API_PREFIX}/image`,
481
+ handler: (req, res) => {
482
+ if (!isSameOriginRequest(req)) {
483
+ json(res, 403, {
484
+ ok: false,
485
+ error: "cross-site-request-rejected"
486
+ });
487
+ return;
488
+ }
489
+ if (!requireMethod(req, res, "GET")) return;
490
+ const rawPath = new URL(req.url ?? "/", "http://x").pathname;
491
+ const abs = uploadPath(rawPath.startsWith(`/api/bg-wallpaper/image/`) ? decodeURIComponent(rawPath.slice(`${BACKGROUND_API_PREFIX}/image/`.length)) : "", opts.home);
492
+ if (abs === "") {
493
+ json(res, 404, {
494
+ ok: false,
495
+ error: "not-found"
496
+ });
497
+ return;
498
+ }
499
+ try {
500
+ const data = readFileSync(abs);
501
+ const mime = mimeForPath(abs) ?? "application/octet-stream";
502
+ const stat = statSync(abs);
503
+ res.writeHead(200, {
504
+ "content-type": mime,
505
+ "content-length": String(data.length),
506
+ "last-modified": stat.mtime.toUTCString(),
507
+ "cache-control": "public, max-age=31536000, immutable"
508
+ });
509
+ res.end(data);
510
+ } catch {
511
+ json(res, 404, {
512
+ ok: false,
513
+ error: "not-found"
514
+ });
515
+ }
516
+ }
517
+ }
518
+ ];
519
+ }
520
+ //#endregion
521
+ //#region src/index.ts
522
+ const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
523
+ /**
524
+ * Register the durable background section and its API routes when the Host
525
+ * settings/webServer services are composed. Effect timing is `live`: the
526
+ * browser half repaints through the route without a restart.
527
+ * @param ctx - Host context that may acquire the settings and webServer services.
528
+ */
529
+ function apply(ctx) {
530
+ ctx.inject(["settings", "webServer"], (hostCtx) => {
531
+ hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
532
+ try {
533
+ for (const route of makeBackgroundRoutes(hostCtx.settings)) hostCtx.effect(() => hostCtx.webServer.register(route), "deepseek-harness-background: settings route");
534
+ } catch (error) {
535
+ console.error("[deepseek-harness-background] route registration failed:", error);
536
+ }
537
+ });
538
+ }
539
+ //#endregion
540
+ export { BACKGROUND_API_PREFIX, BACKGROUND_SETTINGS_NAMESPACE, BackgroundSettingsSchema, DEFAULT_FIT, DEFAULT_OPACITY, DEFAULT_PANEL_OPACITY, DEFAULT_SCRIM, FIT_MODES, OPACITY_MAX, OPACITY_MIN, PLUGIN_HOME_REL, SCRIM_MAX, SCRIM_MIN, apply, makeBackgroundRoutes, resolveHarnessHome };
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "deepseek-harness-background",
3
+ "description": "Custom background image plugin for the DeepSeek Harness web GUI: upload a local picture or paste an image URL, and render it behind the whole app surface with adjustable opacity, scrim, panel transparency and frosted-glass blur.",
4
+ "version": "0.2.0",
5
+ "keywords": [
6
+ "deepseek-harness",
7
+ "dsh",
8
+ "plugin",
9
+ "dsh-plugin",
10
+ "theme",
11
+ "wallpaper",
12
+ "background",
13
+ "web-ui"
14
+ ],
15
+ "author": "HaoyueQin",
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/HaoyueQin/deepseek-harness-background.git"
20
+ },
21
+ "homepage": "https://github.com/HaoyueQin/deepseek-harness-background",
22
+ "packageManager": "pnpm@11.9.0",
23
+ "type": "module",
24
+ "main": "lib/index.js",
25
+ "exports": {
26
+ ".": "./lib/index.js",
27
+ "./client": "./lib/client.js",
28
+ "./src/*": "./src/*",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "dsh": {
32
+ "bundle": {
33
+ "patch": "./cordis.patch.yml"
34
+ },
35
+ "client": {
36
+ "inject": [
37
+ "@deepseek-ai/dsh-client-runtime",
38
+ "@deepseek-ai/dsh-client-locale"
39
+ ],
40
+ "platform": "web"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "tsdown",
45
+ "prepare": "tsdown",
46
+ "watch": "tsdown --watch",
47
+ "test": "vitest run",
48
+ "typecheck": "tsc --noEmit -p tsconfig.json"
49
+ },
50
+ "license": "MIT",
51
+ "files": [
52
+ "lib",
53
+ "cordis.patch.yml",
54
+ "README.md",
55
+ "README.zh.md"
56
+ ],
57
+ "peerDependencies": {
58
+ "@deepseek-ai/cordis": "^4.0.1",
59
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
60
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
61
+ "react": "^18.2.0"
62
+ },
63
+ "dependencies": {
64
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
65
+ "@deepseek-ai/schemastery": "^3.18.1"
66
+ },
67
+ "devDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.1",
69
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
70
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.8",
72
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
73
+ "@deepseek-ai/schemastery": "^3.18.1",
74
+ "@testing-library/dom": "^10.4.1",
75
+ "@testing-library/react": "^16.3.0",
76
+ "@types/node": "^22.0.0",
77
+ "@types/react": "~18.3.1",
78
+ "@types/react-dom": "~18.3.0",
79
+ "clsx": "^2.1.1",
80
+ "jsdom": "^29.1.1",
81
+ "lightningcss": "^1.33.0",
82
+ "react": "^18.2.0",
83
+ "react-dom": "^18.2.0",
84
+ "tsdown": "^0.22.14",
85
+ "typescript": "^5.8.0",
86
+ "vitest": "^4.1.8"
87
+ }
88
+ }