@klarkxy/dsh-plugin-kit 0.3.4 → 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.
@@ -1,4 +1,5 @@
1
1
  import { AttachmentStore, FileAttachmentRef, ImageAttachmentRef } from "@deepseek-ai/dsh-attachment";
2
+ import { ToolRunContext } from "@deepseek-ai/dsh-tools";
2
3
  //#region src/cli-media.d.ts
3
4
  type MediaKind = 'image' | 'audio' | 'video';
4
5
  type MediaInput = {
@@ -6,18 +7,17 @@ type MediaInput = {
6
7
  attachmentId?: string;
7
8
  kind: MediaKind;
8
9
  };
10
+ type MediaSession = NonNullable<ToolRunContext['agent']>['session'];
9
11
  type MediaExec = {
10
12
  signal: AbortSignal;
11
13
  agent?: {
12
- session: {
13
- id: unknown;
14
- };
14
+ session: MediaSession;
15
15
  };
16
16
  };
17
17
  type MediaHost = {
18
18
  sandboxPolicy: {
19
19
  resolve(input: {
20
- session?: unknown;
20
+ session?: MediaSession;
21
21
  }): {
22
22
  workspaceRoot: string;
23
23
  mode: string;
package/lib/cli-media.js CHANGED
@@ -89,17 +89,24 @@ async function sessionAttachments(host, exec) {
89
89
  if (!exec.agent) throw fail("no-session", "Session attachments need an active session.");
90
90
  const query = host.get?.("sessionQuery");
91
91
  if (!query) throw fail("no-session-query", "The host cannot read current-session attachments.");
92
- const snapshot = await query.readSession(exec.agent.session.id);
93
- exec.signal.throwIfAborted();
94
- const result = /* @__PURE__ */ new Map();
95
- for (const raw of snapshot.events) {
96
- const event = raw;
97
- if (event.type !== "user/message" && event.type !== "tool/result") continue;
98
- const content = event.data?.content ?? event.data?.message?.content;
99
- if (!Array.isArray(content)) continue;
100
- for (const block of content) if ((block?.type === "image" || block?.type === "file") && typeof block.attachment?.attachmentId === "string") result.set(block.attachment.attachmentId, block);
92
+ const snapshot = await query.observeSession(exec.agent.session.id, {
93
+ signal: exec.signal,
94
+ projectionMode: "none"
95
+ });
96
+ try {
97
+ exec.signal.throwIfAborted();
98
+ const result = /* @__PURE__ */ new Map();
99
+ for (const raw of snapshot.events) {
100
+ const event = raw;
101
+ if (event.type !== "user/message" && event.type !== "tool/result") continue;
102
+ const content = event.data?.content ?? event.data?.message?.content;
103
+ if (!Array.isArray(content)) continue;
104
+ for (const block of content) if ((block?.type === "image" || block?.type === "file") && typeof block.attachment?.attachmentId === "string") result.set(block.attachment.attachmentId, block);
105
+ }
106
+ return result;
107
+ } finally {
108
+ snapshot[Symbol.dispose]();
101
109
  }
102
- return result;
103
110
  }
104
111
  async function* bounded(chunks, max, signal) {
105
112
  let bytes = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"cli-media.js","names":[],"sources":["../src/cli-media.ts"],"sourcesContent":["import { createReadStream, createWriteStream } from 'node:fs'\nimport { lstat, realpath, mkdir, open, readFile, writeFile, rename } from 'node:fs/promises'\nimport { pipeline } from 'node:stream/promises'\nimport { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type { AttachmentStore, ImageAttachmentRef, FileAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'\n\nexport type MediaKind = 'image' | 'audio' | 'video'\nexport type MediaInput = { path?: string; attachmentId?: string; kind: MediaKind }\nexport type MediaExec = { signal: AbortSignal; agent?: { session: { id: unknown } } }\nexport type MediaHost = {\n sandboxPolicy: { resolve(input: { session?: unknown }): { workspaceRoot: string; mode: string } };\n attachments: AttachmentStore;\n get?(name: string): unknown;\n}\nexport type PublishedMedia = {\n path: string; name: string; kind: MediaKind; mediaType: string; bytes: number;\n attachment: { type: 'image'; attachment: ImageAttachmentRef } | { type: 'file'; attachment: FileAttachmentRef };\n}\nexport class CliMediaError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliMediaError' }\n}\nconst fail = (code: string, message: string) => new CliMediaError(code, message)\nexport const MEDIA_LIMITS: Record<MediaKind, number> = { image: 30 * 1024 * 1024, audio: 100 * 1024 * 1024, video: 256 * 1024 * 1024 }\nexport function insideDirectory(root: string, path: string): boolean {\n const rel = relative(root, path)\n return !isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)\n}\n/** Resolve each path component; links are not accepted as media or receipt boundaries. */\nexport async function confinedPath(root: string, path: string): Promise<string> {\n const base = await realpath(root), absolute = resolve(base, path)\n if (!insideDirectory(base, absolute) || (await lstat(root)).isSymbolicLink()) throw fail('outside-workspace', 'The file must stay inside the permitted directory.')\n let part = base\n for (const component of relative(base, absolute).split(sep).filter(Boolean)) {\n part = join(part, component)\n if ((await lstat(part)).isSymbolicLink()) throw fail('outside-workspace', 'Symbolic links are not accepted for media files.')\n }\n const canonical = await realpath(absolute)\n if (!insideDirectory(base, canonical)) throw fail('outside-workspace', 'The file must stay inside the permitted directory.')\n return canonical\n}\nexport async function mediaWorkspace(host: MediaHost, exec: MediaExec, write: boolean): Promise<string> {\n exec.signal.throwIfAborted()\n const policy = host.sandboxPolicy.resolve({ session: exec.agent?.session })\n if (write && policy.mode === 'read-only') throw fail('read-only', 'This operation saves media and needs workspace write access.')\n const root = await realpath(policy.workspaceRoot)\n if (!(await lstat(root)).isDirectory()) throw fail('invalid-workspace', 'The workspace is unavailable.')\n return root\n}\nexport async function inspectMedia(path: string, kind: MediaKind): Promise<{ mediaType: string; bytes: number }> {\n const info = await lstat(path)\n if (!info.isFile() || info.isSymbolicLink() || info.size < 4 || info.size > MEDIA_LIMITS[kind]) throw fail('invalid-media', 'The media file is empty, too large, or not a regular file.')\n const file = await open(path, 'r'), head = Buffer.alloc(64)\n try { await file.read(head, 0, head.length, 0) } finally { await file.close() }\n let mediaType: string | undefined\n if (head.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) mediaType = 'image/png'\n else if (head[0] === 255 && head[1] === 216 && head[2] === 255) mediaType = 'image/jpeg'\n else if (/^GIF8[79]a$/.test(head.toString('ascii', 0, 6))) mediaType = 'image/gif'\n else if (head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP') mediaType = 'image/webp'\n else if (head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WAVE') mediaType = 'audio/wav'\n else if (head.toString('ascii', 0, 3) === 'ID3' || (head[0] === 255 && (head[1] & 224) === 224)) mediaType = 'audio/mpeg'\n else if (head.toString('ascii', 0, 4) === 'fLaC') mediaType = 'audio/flac'\n else if (head.toString('ascii', 0, 4) === 'OggS') mediaType = 'audio/ogg'\n else if (head.toString('ascii', 4, 8) === 'ftyp') {\n const brand = head.toString('ascii', 8, 12)\n mediaType = /^(?:heic|heix|hevc|hevx|mif1|msf1)$/.test(brand) ? 'image/heic' : kind === 'audio' ? 'audio/mp4' : brand === 'qt ' ? 'video/quicktime' : 'video/mp4'\n } else if (head.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]))) mediaType = kind === 'audio' ? 'audio/webm' : 'video/webm'\n if (!mediaType?.startsWith(`${kind}/`)) throw fail('invalid-media', `The file is not a supported ${kind} file.`)\n return { mediaType, bytes: info.size }\n}\nasync function sessionAttachments(host: MediaHost, exec: MediaExec): Promise<Map<string, { type: 'image' | 'file'; attachment: ImageAttachmentRef | FileAttachmentRef }>> {\n if (!exec.agent) throw fail('no-session', 'Session attachments need an active session.')\n const query = host.get?.('sessionQuery') as { readSession(id: unknown): Promise<{ events: unknown[] }> } | undefined\n if (!query) throw fail('no-session-query', 'The host cannot read current-session attachments.')\n const snapshot = await query.readSession(exec.agent.session.id)\n exec.signal.throwIfAborted()\n const result = new Map<string, { type: 'image' | 'file'; attachment: ImageAttachmentRef | FileAttachmentRef }>()\n for (const raw of snapshot.events) {\n const event = raw as { type?: string; data?: { content?: unknown; message?: { content?: unknown } } }\n if (event.type !== 'user/message' && event.type !== 'tool/result') continue\n const content = event.data?.content ?? event.data?.message?.content\n if (!Array.isArray(content)) continue\n for (const block of content) {\n if ((block?.type === 'image' || block?.type === 'file') && typeof block.attachment?.attachmentId === 'string') result.set(block.attachment.attachmentId, block)\n }\n }\n return result\n}\nasync function* bounded(chunks: AsyncIterable<Uint8Array>, max: number, signal: AbortSignal): AsyncIterable<Uint8Array> {\n let bytes = 0\n for await (const chunk of chunks) {\n signal.throwIfAborted(); bytes += chunk.byteLength\n if (bytes > max) throw fail('invalid-media', 'The media file exceeds its size limit.')\n yield chunk\n }\n signal.throwIfAborted()\n}\nexport async function stageMedia(host: MediaHost, exec: MediaExec, root: string, directory: string, inputs: MediaInput[]): Promise<{ path: string; mediaType: string; bytes: number }[]> {\n if (inputs.length > 40) throw fail('bad-request', 'At most 40 input files are allowed.')\n const refs = inputs.some(input => input.attachmentId) ? await sessionAttachments(host, exec) : new Map()\n const staged: { path: string; mediaType: string; bytes: number }[] = []\n for (const [index, input] of inputs.entries()) {\n exec.signal.throwIfAborted()\n if (!MEDIA_LIMITS[input.kind] || Boolean(input.path) === Boolean(input.attachmentId)) throw fail('bad-request', 'Provide exactly one workspace path or current-session attachment ID per input.')\n const ref = input.attachmentId ? refs.get(input.attachmentId) : undefined\n if (input.attachmentId && !ref) throw fail('unknown-attachment', 'The attachment does not belong to the current session.')\n const source = input.path ? await confinedPath(root, input.path) : undefined\n if (source) await inspectMedia(source, input.kind)\n const rawName = source ? basename(source) : ref!.attachment.name || 'attachment'\n const extension = /^\\.[a-z0-9]{1,8}$/i.test(extname(rawName)) ? extname(rawName).toLowerCase() : ref?.type === 'image' ? '.' + (ref.attachment as ImageAttachmentRef).mediaType.split('/')[1] : '.bin'\n const target = join(directory, `input-${index}${extension}`)\n if (source) await pipeline(createReadStream(source), createWriteStream(target, { flags: 'wx' }), { signal: exec.signal })\n else if (ref!.type === 'image') {\n if (input.kind !== 'image') throw fail('invalid-media', 'An image attachment cannot be used as audio or video.')\n const stored = await host.attachments.readImage(ref!.attachment as ImageAttachmentRef, exec.signal)\n if (stored.data.byteLength > MEDIA_LIMITS.image) throw fail('invalid-media', 'The image is too large.')\n await writeFile(target, stored.data, { flag: 'wx', signal: exec.signal })\n } else await pipeline(bounded(host.attachments.readFileStream(ref!.attachment as FileAttachmentRef, exec.signal), MEDIA_LIMITS[input.kind], exec.signal), createWriteStream(target, { flags: 'wx' }), { signal: exec.signal })\n const info = await inspectMedia(target, input.kind)\n staged.push({ path: target, ...info })\n }\n return staged\n}\nexport async function jobDirectory(root: string, slug: string, id: string = randomUUID()): Promise<{ id: string; directory: string }> {\n if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail('bad-request', 'Invalid media job identifier.')\n const parent = join(root, `.${slug}`)\n await mkdir(parent, { recursive: true }); await confinedPath(root, parent)\n const directory = join(parent, id)\n await mkdir(directory, { recursive: true }); await confinedPath(root, directory)\n return { id, directory }\n}\nexport async function writeReceipt(directory: string, receipt: unknown): Promise<void> {\n const target = join(directory, 'receipt.json'), temporary = join(directory, `receipt-${randomUUID()}.tmp`)\n await confinedPath(directory, directory)\n await writeFile(temporary, JSON.stringify(receipt, null, 2) + '\\n', { flag: 'wx', mode: 0o600 })\n // Replacement is atomic in this directory; credentials and signed URLs do not belong in receipts.\n await rename(temporary, target)\n}\nexport async function readReceipt<T>(root: string, slug: string, id: string): Promise<T> {\n if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail('bad-request', 'Invalid media job identifier.')\n const path = await confinedPath(root, join(root, `.${slug}`, id, 'receipt.json'))\n if ((await lstat(path)).size > 1024 * 1024) throw fail('invalid-receipt', 'The media receipt is too large.')\n return JSON.parse(await readFile(path, 'utf8')) as T\n}\nexport async function publishMedia(host: MediaHost, source: string, sourceRoot: string, destination: string, workspace: string, kind: MediaKind, signal: AbortSignal): Promise<PublishedMedia> {\n signal.throwIfAborted()\n const path = await confinedPath(sourceRoot, source), info = await inspectMedia(path, kind)\n if (kind === 'image' && !['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(info.mediaType)) throw fail('unsupported-media', 'The host cannot attach this image format. Use PNG, JPEG, GIF or WebP.')\n await confinedPath(workspace, destination)\n const extension = ({ 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'audio/mpeg': '.mp3', 'audio/wav': '.wav', 'audio/flac': '.flac', 'audio/ogg': '.ogg', 'audio/mp4': '.m4a', 'video/mp4': '.mp4', 'video/quicktime': '.mov', 'video/webm': '.webm', 'audio/webm': '.webm' } as Record<string, string>)[info.mediaType]\n const rawName = basename(path).replace(/[^\\w.\\-]/g, '_').slice(-100) || kind\n const name = extension ? rawName.replace(/\\.[^.]*$/, '') + extension : rawName\n const reuse = insideDirectory(destination, path) && await realpath(sourceRoot) === await realpath(workspace)\n const target = reuse ? path : join(destination, `${randomUUID().slice(0, 8)}-${name}`)\n if (kind === 'image') {\n const input = { data: await readFile(path), mediaType: info.mediaType as ImageMediaType, name }\n await host.attachments.validateImage(input)\n signal.throwIfAborted()\n if (!reuse) await writeFile(target, input.data, { flag: 'wx', signal })\n const [attachment] = await host.attachments.saveImages([input])\n signal.throwIfAborted()\n return { path: relative(workspace, target).split(sep).join('/'), name, kind, ...info, attachment: { type: 'image', attachment } }\n }\n if (!reuse) await pipeline(createReadStream(path), createWriteStream(target, { flags: 'wx' }), { signal })\n const attachment = await host.attachments.saveFileStream({ data: bounded(createReadStream(target), MEDIA_LIMITS[kind], signal), name, signal })\n signal.throwIfAborted()\n return { path: relative(workspace, target).split(sep).join('/'), name, kind, ...info, attachment: { type: 'file', attachment } }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-media.js","names":[],"sources":["../src/cli-media.ts"],"sourcesContent":["import { createReadStream, createWriteStream } from 'node:fs'\nimport { lstat, realpath, mkdir, open, readFile, writeFile, rename } from 'node:fs/promises'\nimport { pipeline } from 'node:stream/promises'\nimport { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { randomUUID } from 'node:crypto'\nimport type { AttachmentStore, ImageAttachmentRef, FileAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'\nimport type { ToolRunContext } from '@deepseek-ai/dsh-tools'\n\nexport type MediaKind = 'image' | 'audio' | 'video'\nexport type MediaInput = { path?: string; attachmentId?: string; kind: MediaKind }\ntype MediaSession = NonNullable<ToolRunContext['agent']>['session']\nexport type MediaExec = { signal: AbortSignal; agent?: { session: MediaSession } }\nexport type MediaHost = {\n sandboxPolicy: { resolve(input: { session?: MediaSession }): { workspaceRoot: string; mode: string } };\n attachments: AttachmentStore;\n get?(name: string): unknown;\n}\nexport type PublishedMedia = {\n path: string; name: string; kind: MediaKind; mediaType: string; bytes: number;\n attachment: { type: 'image'; attachment: ImageAttachmentRef } | { type: 'file'; attachment: FileAttachmentRef };\n}\nexport class CliMediaError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliMediaError' }\n}\nconst fail = (code: string, message: string) => new CliMediaError(code, message)\nexport const MEDIA_LIMITS: Record<MediaKind, number> = { image: 30 * 1024 * 1024, audio: 100 * 1024 * 1024, video: 256 * 1024 * 1024 }\nexport function insideDirectory(root: string, path: string): boolean {\n const rel = relative(root, path)\n return !isAbsolute(rel) && rel !== '..' && !rel.startsWith(`..${sep}`)\n}\n/** Resolve each path component; links are not accepted as media or receipt boundaries. */\nexport async function confinedPath(root: string, path: string): Promise<string> {\n const base = await realpath(root), absolute = resolve(base, path)\n if (!insideDirectory(base, absolute) || (await lstat(root)).isSymbolicLink()) throw fail('outside-workspace', 'The file must stay inside the permitted directory.')\n let part = base\n for (const component of relative(base, absolute).split(sep).filter(Boolean)) {\n part = join(part, component)\n if ((await lstat(part)).isSymbolicLink()) throw fail('outside-workspace', 'Symbolic links are not accepted for media files.')\n }\n const canonical = await realpath(absolute)\n if (!insideDirectory(base, canonical)) throw fail('outside-workspace', 'The file must stay inside the permitted directory.')\n return canonical\n}\nexport async function mediaWorkspace(host: MediaHost, exec: MediaExec, write: boolean): Promise<string> {\n exec.signal.throwIfAborted()\n const policy = host.sandboxPolicy.resolve({ session: exec.agent?.session })\n if (write && policy.mode === 'read-only') throw fail('read-only', 'This operation saves media and needs workspace write access.')\n const root = await realpath(policy.workspaceRoot)\n if (!(await lstat(root)).isDirectory()) throw fail('invalid-workspace', 'The workspace is unavailable.')\n return root\n}\nexport async function inspectMedia(path: string, kind: MediaKind): Promise<{ mediaType: string; bytes: number }> {\n const info = await lstat(path)\n if (!info.isFile() || info.isSymbolicLink() || info.size < 4 || info.size > MEDIA_LIMITS[kind]) throw fail('invalid-media', 'The media file is empty, too large, or not a regular file.')\n const file = await open(path, 'r'), head = Buffer.alloc(64)\n try { await file.read(head, 0, head.length, 0) } finally { await file.close() }\n let mediaType: string | undefined\n if (head.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) mediaType = 'image/png'\n else if (head[0] === 255 && head[1] === 216 && head[2] === 255) mediaType = 'image/jpeg'\n else if (/^GIF8[79]a$/.test(head.toString('ascii', 0, 6))) mediaType = 'image/gif'\n else if (head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP') mediaType = 'image/webp'\n else if (head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WAVE') mediaType = 'audio/wav'\n else if (head.toString('ascii', 0, 3) === 'ID3' || (head[0] === 255 && (head[1] & 224) === 224)) mediaType = 'audio/mpeg'\n else if (head.toString('ascii', 0, 4) === 'fLaC') mediaType = 'audio/flac'\n else if (head.toString('ascii', 0, 4) === 'OggS') mediaType = 'audio/ogg'\n else if (head.toString('ascii', 4, 8) === 'ftyp') {\n const brand = head.toString('ascii', 8, 12)\n mediaType = /^(?:heic|heix|hevc|hevx|mif1|msf1)$/.test(brand) ? 'image/heic' : kind === 'audio' ? 'audio/mp4' : brand === 'qt ' ? 'video/quicktime' : 'video/mp4'\n } else if (head.subarray(0, 4).equals(Buffer.from([26, 69, 223, 163]))) mediaType = kind === 'audio' ? 'audio/webm' : 'video/webm'\n if (!mediaType?.startsWith(`${kind}/`)) throw fail('invalid-media', `The file is not a supported ${kind} file.`)\n return { mediaType, bytes: info.size }\n}\nasync function sessionAttachments(host: MediaHost, exec: MediaExec): Promise<Map<string, { type: 'image' | 'file'; attachment: ImageAttachmentRef | FileAttachmentRef }>> {\n if (!exec.agent) throw fail('no-session', 'Session attachments need an active session.')\n const query = host.get?.('sessionQuery') as { observeSession(id: unknown, options: { signal: AbortSignal; projectionMode: 'none' }): Promise<{ events: readonly unknown[]; [Symbol.dispose](): void }> } | undefined\n if (!query) throw fail('no-session-query', 'The host cannot read current-session attachments.')\n const snapshot = await query.observeSession(exec.agent.session.id, { signal: exec.signal, projectionMode: 'none' })\n try {\n exec.signal.throwIfAborted()\n const result = new Map<string, { type: 'image' | 'file'; attachment: ImageAttachmentRef | FileAttachmentRef }>()\n for (const raw of snapshot.events) {\n const event = raw as { type?: string; data?: { content?: unknown; message?: { content?: unknown } } }\n if (event.type !== 'user/message' && event.type !== 'tool/result') continue\n const content = event.data?.content ?? event.data?.message?.content\n if (!Array.isArray(content)) continue\n for (const block of content) {\n if ((block?.type === 'image' || block?.type === 'file') && typeof block.attachment?.attachmentId === 'string') result.set(block.attachment.attachmentId, block)\n }\n }\n return result\n } finally { snapshot[Symbol.dispose]() }\n}\nasync function* bounded(chunks: AsyncIterable<Uint8Array>, max: number, signal: AbortSignal): AsyncIterable<Uint8Array> {\n let bytes = 0\n for await (const chunk of chunks) {\n signal.throwIfAborted(); bytes += chunk.byteLength\n if (bytes > max) throw fail('invalid-media', 'The media file exceeds its size limit.')\n yield chunk\n }\n signal.throwIfAborted()\n}\nexport async function stageMedia(host: MediaHost, exec: MediaExec, root: string, directory: string, inputs: MediaInput[]): Promise<{ path: string; mediaType: string; bytes: number }[]> {\n if (inputs.length > 40) throw fail('bad-request', 'At most 40 input files are allowed.')\n const refs = inputs.some(input => input.attachmentId) ? await sessionAttachments(host, exec) : new Map()\n const staged: { path: string; mediaType: string; bytes: number }[] = []\n for (const [index, input] of inputs.entries()) {\n exec.signal.throwIfAborted()\n if (!MEDIA_LIMITS[input.kind] || Boolean(input.path) === Boolean(input.attachmentId)) throw fail('bad-request', 'Provide exactly one workspace path or current-session attachment ID per input.')\n const ref = input.attachmentId ? refs.get(input.attachmentId) : undefined\n if (input.attachmentId && !ref) throw fail('unknown-attachment', 'The attachment does not belong to the current session.')\n const source = input.path ? await confinedPath(root, input.path) : undefined\n if (source) await inspectMedia(source, input.kind)\n const rawName = source ? basename(source) : ref!.attachment.name || 'attachment'\n const extension = /^\\.[a-z0-9]{1,8}$/i.test(extname(rawName)) ? extname(rawName).toLowerCase() : ref?.type === 'image' ? '.' + (ref.attachment as ImageAttachmentRef).mediaType.split('/')[1] : '.bin'\n const target = join(directory, `input-${index}${extension}`)\n if (source) await pipeline(createReadStream(source), createWriteStream(target, { flags: 'wx' }), { signal: exec.signal })\n else if (ref!.type === 'image') {\n if (input.kind !== 'image') throw fail('invalid-media', 'An image attachment cannot be used as audio or video.')\n const stored = await host.attachments.readImage(ref!.attachment as ImageAttachmentRef, exec.signal)\n if (stored.data.byteLength > MEDIA_LIMITS.image) throw fail('invalid-media', 'The image is too large.')\n await writeFile(target, stored.data, { flag: 'wx', signal: exec.signal })\n } else await pipeline(bounded(host.attachments.readFileStream(ref!.attachment as FileAttachmentRef, exec.signal), MEDIA_LIMITS[input.kind], exec.signal), createWriteStream(target, { flags: 'wx' }), { signal: exec.signal })\n const info = await inspectMedia(target, input.kind)\n staged.push({ path: target, ...info })\n }\n return staged\n}\nexport async function jobDirectory(root: string, slug: string, id: string = randomUUID()): Promise<{ id: string; directory: string }> {\n if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail('bad-request', 'Invalid media job identifier.')\n const parent = join(root, `.${slug}`)\n await mkdir(parent, { recursive: true }); await confinedPath(root, parent)\n const directory = join(parent, id)\n await mkdir(directory, { recursive: true }); await confinedPath(root, directory)\n return { id, directory }\n}\nexport async function writeReceipt(directory: string, receipt: unknown): Promise<void> {\n const target = join(directory, 'receipt.json'), temporary = join(directory, `receipt-${randomUUID()}.tmp`)\n await confinedPath(directory, directory)\n await writeFile(temporary, JSON.stringify(receipt, null, 2) + '\\n', { flag: 'wx', mode: 0o600 })\n // Replacement is atomic in this directory; credentials and signed URLs do not belong in receipts.\n await rename(temporary, target)\n}\nexport async function readReceipt<T>(root: string, slug: string, id: string): Promise<T> {\n if (!/^dsh-[a-z0-9-]+$/.test(slug) || !/^[a-f0-9-]{36}$/i.test(id)) throw fail('bad-request', 'Invalid media job identifier.')\n const path = await confinedPath(root, join(root, `.${slug}`, id, 'receipt.json'))\n if ((await lstat(path)).size > 1024 * 1024) throw fail('invalid-receipt', 'The media receipt is too large.')\n return JSON.parse(await readFile(path, 'utf8')) as T\n}\nexport async function publishMedia(host: MediaHost, source: string, sourceRoot: string, destination: string, workspace: string, kind: MediaKind, signal: AbortSignal): Promise<PublishedMedia> {\n signal.throwIfAborted()\n const path = await confinedPath(sourceRoot, source), info = await inspectMedia(path, kind)\n if (kind === 'image' && !['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(info.mediaType)) throw fail('unsupported-media', 'The host cannot attach this image format. Use PNG, JPEG, GIF or WebP.')\n await confinedPath(workspace, destination)\n const extension = ({ 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'audio/mpeg': '.mp3', 'audio/wav': '.wav', 'audio/flac': '.flac', 'audio/ogg': '.ogg', 'audio/mp4': '.m4a', 'video/mp4': '.mp4', 'video/quicktime': '.mov', 'video/webm': '.webm', 'audio/webm': '.webm' } as Record<string, string>)[info.mediaType]\n const rawName = basename(path).replace(/[^\\w.\\-]/g, '_').slice(-100) || kind\n const name = extension ? rawName.replace(/\\.[^.]*$/, '') + extension : rawName\n const reuse = insideDirectory(destination, path) && await realpath(sourceRoot) === await realpath(workspace)\n const target = reuse ? path : join(destination, `${randomUUID().slice(0, 8)}-${name}`)\n if (kind === 'image') {\n const input = { data: await readFile(path), mediaType: info.mediaType as ImageMediaType, name }\n await host.attachments.validateImage(input)\n signal.throwIfAborted()\n if (!reuse) await writeFile(target, input.data, { flag: 'wx', signal })\n const [attachment] = await host.attachments.saveImages([input])\n signal.throwIfAborted()\n return { path: relative(workspace, target).split(sep).join('/'), name, kind, ...info, attachment: { type: 'image', attachment } }\n }\n if (!reuse) await pipeline(createReadStream(path), createWriteStream(target, { flags: 'wx' }), { signal })\n const attachment = await host.attachments.saveFileStream({ data: bounded(createReadStream(target), MEDIA_LIMITS[kind], signal), name, signal })\n signal.throwIfAborted()\n return { path: relative(workspace, target).split(sep).join('/'), name, kind, ...info, attachment: { type: 'file', attachment } }\n}\n"],"mappings":"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"}
@@ -1,5 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
2
  //#region src/cli-process.d.ts
3
+ /** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */
4
+ declare function cliEnvironment(parent?: NodeJS.ProcessEnv, extra?: readonly string[]): NodeJS.ProcessEnv;
5
+ /** Run a JavaScript CLI with process.execPath even when the host is Electron.
6
+ * Set this explicitly: desktop hosts may already have removed it from their own env.
7
+ * Native executables continue to use cliEnvironment without this Node-mode override.
8
+ */
9
+ declare function cliNodeEnvironment(parent?: NodeJS.ProcessEnv, extra?: readonly string[]): NodeJS.ProcessEnv;
3
10
  /** A resolved executable, never a shell command or an argument string. */
4
11
  type CliExecutable = {
5
12
  command: string;
@@ -29,5 +36,5 @@ type CliOptions = {
29
36
  /** Own one process tree until it exits, including caller cancellation and protocol failure. */
30
37
  declare function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput>;
31
38
  //#endregion
32
- export { CliExecutable, CliOptions, CliOutput, CliProcessError, runCli };
39
+ export { CliExecutable, CliOptions, CliOutput, CliProcessError, cliEnvironment, cliNodeEnvironment, runCli };
33
40
  //# sourceMappingURL=cli-process.d.ts.map
@@ -1,6 +1,52 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { StringDecoder } from "node:string_decoder";
3
3
  //#region src/cli-process.ts
4
+ /** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */
5
+ function cliEnvironment(parent = process.env, extra = []) {
6
+ const allowed = /* @__PURE__ */ new Set([
7
+ "PATH",
8
+ "PATHEXT",
9
+ "SYSTEMROOT",
10
+ "WINDIR",
11
+ "COMSPEC",
12
+ "HOME",
13
+ "USERPROFILE",
14
+ "HOMEDRIVE",
15
+ "HOMEPATH",
16
+ "APPDATA",
17
+ "LOCALAPPDATA",
18
+ "PROGRAMDATA",
19
+ "PROGRAMFILES",
20
+ "PROGRAMFILES(X86)",
21
+ "TEMP",
22
+ "TMP",
23
+ "TMPDIR",
24
+ "LANG",
25
+ "LC_ALL",
26
+ "LC_CTYPE",
27
+ "TZ",
28
+ "HTTP_PROXY",
29
+ "HTTPS_PROXY",
30
+ "ALL_PROXY",
31
+ "NO_PROXY",
32
+ "SSL_CERT_FILE",
33
+ "SSL_CERT_DIR",
34
+ ...extra.map((key) => key.toUpperCase())
35
+ ]);
36
+ return Object.fromEntries(Object.entries(parent).filter(([key, value]) => value !== void 0 && !key.toUpperCase().startsWith("DSH_") && allowed.has(key.toUpperCase())));
37
+ }
38
+ /** Run a JavaScript CLI with process.execPath even when the host is Electron.
39
+ * Set this explicitly: desktop hosts may already have removed it from their own env.
40
+ * Native executables continue to use cliEnvironment without this Node-mode override.
41
+ */
42
+ function cliNodeEnvironment(parent = process.env, extra = []) {
43
+ const env = cliEnvironment(parent, extra);
44
+ for (const key of Object.keys(env)) if (key.toUpperCase() === "ELECTRON_RUN_AS_NODE") delete env[key];
45
+ return {
46
+ ...env,
47
+ ELECTRON_RUN_AS_NODE: "1"
48
+ };
49
+ }
4
50
  var CliProcessError = class extends Error {
5
51
  code;
6
52
  constructor(code, message) {
@@ -20,7 +66,7 @@ async function runCli(executable, args, options) {
20
66
  try {
21
67
  child = (options.spawnChild ?? spawn)(executable.command, [...executable.args ?? [], ...args], {
22
68
  cwd: options.cwd,
23
- env: executable.env ?? process.env,
69
+ env: executable.env ?? cliEnvironment(),
24
70
  shell: false,
25
71
  windowsHide: true,
26
72
  detached: process.platform !== "win32",
@@ -54,6 +100,7 @@ async function runCli(executable, args, options) {
54
100
  "/T",
55
101
  "/F"
56
102
  ], {
103
+ env: cliEnvironment(),
57
104
  windowsHide: true,
58
105
  shell: false,
59
106
  stdio: "ignore"
@@ -161,6 +208,6 @@ async function runCli(executable, args, options) {
161
208
  }
162
209
  }
163
210
  //#endregion
164
- export { CliProcessError, runCli };
211
+ export { CliProcessError, cliEnvironment, cliNodeEnvironment, runCli };
165
212
 
166
213
  //# sourceMappingURL=cli-process.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-process.js","names":[],"sources":["../src/cli-process.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { StringDecoder } from 'node:string_decoder'\n\n/** A resolved executable, never a shell command or an argument string. */\nexport type CliExecutable = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; displayPath: string }\nexport type CliOutput = { stdout: string; stderr: string; exitCode: number }\nexport class CliProcessError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliProcessError' }\n}\nexport type CliOptions = {\n cwd: string; signal: AbortSignal; input?: string; timeoutMs?: number;\n maxOutputBytes?: number; maxLineBytes?: number;\n onLine?: (line: string) => void | Promise<void>;\n spawnChild?: typeof spawn;\n}\n\n/** Own one process tree until it exits, including caller cancellation and protocol failure. */\nexport async function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput> {\n options.signal.throwIfAborted()\n const timed = AbortSignal.timeout(options.timeoutMs ?? 900_000)\n const signal = AbortSignal.any([options.signal, timed])\n const maxBytes = options.maxOutputBytes ?? 16 * 1024 * 1024\n const maxLine = options.maxLineBytes ?? 8 * 1024 * 1024\n let child: ChildProcessWithoutNullStreams\n try {\n child = (options.spawnChild ?? spawn)(executable.command, [...(executable.args ?? []), ...args], {\n cwd: options.cwd, env: executable.env ?? process.env, shell: false,\n windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n } catch { throw new CliProcessError('unavailable', 'Unable to start the official CLI.') }\n let failure: unknown, total = 0, stderr = '', stdout = '', pending = Buffer.alloc(0)\n const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8')\n let exited = false, exitCode = -1, stopping: Promise<void> | undefined\n let lineWork = Promise.resolve()\n const closed = new Promise<void>(resolve => {\n child.once('close', code => { exited = true; exitCode = code ?? -1; resolve() })\n })\n const stop = () => stopping ??= (async () => {\n if (exited) return\n const pid = child.pid\n if (pid && process.platform === 'win32') {\n await new Promise<void>(resolve => {\n const killer = spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, shell: false, stdio: 'ignore' })\n let timer: ReturnType<typeof setTimeout>\n const done = () => { clearTimeout(timer); resolve() }\n killer.once('error', () => { child.kill(); done() })\n killer.once('close', done)\n timer = setTimeout(() => { killer.kill(); child.kill(); resolve() }, 5_000)\n })\n } else if (pid) {\n try { process.kill(-pid, 'SIGTERM') } catch { child.kill() }\n } else child.kill()\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, 2_000) })])\n if (timer) clearTimeout(timer)\n if (!exited) {\n try { pid && process.platform !== 'win32' ? process.kill(-pid, 'SIGKILL') : child.kill('SIGKILL') } catch { /* already exited */ }\n }\n await closed\n })()\n const fail = (error: unknown) => { failure ??= error; void stop() }\n const abort = () => fail(new CliProcessError(timed.aborted && !options.signal.aborted ? 'timeout' : 'cancelled', timed.aborted && !options.signal.aborted ? 'The CLI request timed out.' : 'The CLI request was cancelled.'))\n signal.addEventListener('abort', abort, { once: true })\n child.on('error', () => fail(new CliProcessError('unavailable', 'Unable to start the official CLI.')))\n child.stdin.on('error', () => { if (!exited) fail(new CliProcessError('process-io', 'The CLI input stream closed unexpectedly.')) })\n child.stdout.on('error', () => fail(new CliProcessError('process-io', 'The CLI output stream failed.')))\n child.stderr.on('error', () => fail(new CliProcessError('process-io', 'The CLI error stream failed.')))\n const count = (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) { fail(new CliProcessError('output-limit', 'The CLI output exceeded its limit.')); return false }\n return true\n }\n const acceptLine = (line: string) => {\n if (!options.onLine || !line.trim()) return\n child.stdout.pause()\n lineWork = lineWork.then(async () => { if (!failure) await options.onLine!(line) }).catch(fail).finally(() => child.stdout.resume())\n }\n child.stdout.on('data', (chunk: Buffer) => {\n if (failure || !count(chunk)) return\n if (!options.onLine) { stdout += stdoutDecoder.write(chunk); return }\n pending = Buffer.concat([pending, chunk])\n for (;;) {\n const end = pending.indexOf(10)\n if (end < 0) break\n if (end > maxLine) { fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.')); return }\n acceptLine(pending.subarray(0, end).toString('utf8'))\n pending = pending.subarray(end + 1)\n }\n if (pending.length > maxLine) fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.'))\n })\n child.stderr.on('data', (chunk: Buffer) => { if (count(chunk)) stderr = (stderr + stderrDecoder.write(chunk)).slice(-16_384) })\n try {\n if (signal.aborted) abort()\n if (!failure) child.stdin.end(options.input ?? '')\n await closed\n stdout += stdoutDecoder.end(); stderr += stderrDecoder.end()\n if (pending.length && !failure) acceptLine(pending.toString('utf8'))\n await lineWork\n if (failure) throw failure\n signal.throwIfAborted()\n return { stdout, stderr, exitCode }\n } finally {\n signal.removeEventListener('abort', abort)\n if (!exited) await stop()\n if (stopping) await stopping\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-process.js","names":[],"sources":["../src/cli-process.ts"],"sourcesContent":["import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'\nimport { StringDecoder } from 'node:string_decoder'\n\n/** Only OS/runtime discovery facts cross the default CLI boundary. CLI-specific login configuration must opt in. */\nexport function cliEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const allowed = new Set(['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'SSL_CERT_FILE', 'SSL_CERT_DIR', ...extra.map(key => key.toUpperCase())])\n return Object.fromEntries(Object.entries(parent).filter(([key, value]) => value !== undefined && !key.toUpperCase().startsWith('DSH_') && allowed.has(key.toUpperCase())))\n}\n\n/** Run a JavaScript CLI with process.execPath even when the host is Electron.\n * Set this explicitly: desktop hosts may already have removed it from their own env.\n * Native executables continue to use cliEnvironment without this Node-mode override.\n */\nexport function cliNodeEnvironment(parent: NodeJS.ProcessEnv = process.env, extra: readonly string[] = []): NodeJS.ProcessEnv {\n const env = cliEnvironment(parent, extra)\n // Windows environment names are case-insensitive; avoid conflicting spellings.\n for (const key of Object.keys(env)) if (key.toUpperCase() === 'ELECTRON_RUN_AS_NODE') delete env[key]\n return { ...env, ELECTRON_RUN_AS_NODE: '1' }\n}\n\n/** A resolved executable, never a shell command or an argument string. */\nexport type CliExecutable = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; displayPath: string }\nexport type CliOutput = { stdout: string; stderr: string; exitCode: number }\nexport class CliProcessError extends Error {\n constructor(public readonly code: string, message: string) { super(message); this.name = 'CliProcessError' }\n}\nexport type CliOptions = {\n cwd: string; signal: AbortSignal; input?: string; timeoutMs?: number;\n maxOutputBytes?: number; maxLineBytes?: number;\n onLine?: (line: string) => void | Promise<void>;\n spawnChild?: typeof spawn;\n}\n\n/** Own one process tree until it exits, including caller cancellation and protocol failure. */\nexport async function runCli(executable: CliExecutable, args: string[], options: CliOptions): Promise<CliOutput> {\n options.signal.throwIfAborted()\n const timed = AbortSignal.timeout(options.timeoutMs ?? 900_000)\n const signal = AbortSignal.any([options.signal, timed])\n const maxBytes = options.maxOutputBytes ?? 16 * 1024 * 1024\n const maxLine = options.maxLineBytes ?? 8 * 1024 * 1024\n let child: ChildProcessWithoutNullStreams\n try {\n child = (options.spawnChild ?? spawn)(executable.command, [...(executable.args ?? []), ...args], {\n cwd: options.cwd, env: executable.env ?? cliEnvironment(), shell: false,\n windowsHide: true, detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'],\n }) as ChildProcessWithoutNullStreams\n } catch { throw new CliProcessError('unavailable', 'Unable to start the official CLI.') }\n let failure: unknown, total = 0, stderr = '', stdout = '', pending = Buffer.alloc(0)\n const stdoutDecoder = new StringDecoder('utf8'), stderrDecoder = new StringDecoder('utf8')\n let exited = false, exitCode = -1, stopping: Promise<void> | undefined\n let lineWork = Promise.resolve()\n const closed = new Promise<void>(resolve => {\n child.once('close', code => { exited = true; exitCode = code ?? -1; resolve() })\n })\n const stop = () => stopping ??= (async () => {\n if (exited) return\n const pid = child.pid\n if (pid && process.platform === 'win32') {\n await new Promise<void>(resolve => {\n const killer = spawn('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { env: cliEnvironment(), windowsHide: true, shell: false, stdio: 'ignore' })\n let timer: ReturnType<typeof setTimeout>\n const done = () => { clearTimeout(timer); resolve() }\n killer.once('error', () => { child.kill(); done() })\n killer.once('close', done)\n timer = setTimeout(() => { killer.kill(); child.kill(); resolve() }, 5_000)\n })\n } else if (pid) {\n try { process.kill(-pid, 'SIGTERM') } catch { child.kill() }\n } else child.kill()\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, 2_000) })])\n if (timer) clearTimeout(timer)\n if (!exited) {\n try { pid && process.platform !== 'win32' ? process.kill(-pid, 'SIGKILL') : child.kill('SIGKILL') } catch { /* already exited */ }\n }\n await closed\n })()\n const fail = (error: unknown) => { failure ??= error; void stop() }\n const abort = () => fail(new CliProcessError(timed.aborted && !options.signal.aborted ? 'timeout' : 'cancelled', timed.aborted && !options.signal.aborted ? 'The CLI request timed out.' : 'The CLI request was cancelled.'))\n signal.addEventListener('abort', abort, { once: true })\n child.on('error', () => fail(new CliProcessError('unavailable', 'Unable to start the official CLI.')))\n child.stdin.on('error', () => { if (!exited) fail(new CliProcessError('process-io', 'The CLI input stream closed unexpectedly.')) })\n child.stdout.on('error', () => fail(new CliProcessError('process-io', 'The CLI output stream failed.')))\n child.stderr.on('error', () => fail(new CliProcessError('process-io', 'The CLI error stream failed.')))\n const count = (chunk: Buffer) => {\n total += chunk.length\n if (total > maxBytes) { fail(new CliProcessError('output-limit', 'The CLI output exceeded its limit.')); return false }\n return true\n }\n const acceptLine = (line: string) => {\n if (!options.onLine || !line.trim()) return\n child.stdout.pause()\n lineWork = lineWork.then(async () => { if (!failure) await options.onLine!(line) }).catch(fail).finally(() => child.stdout.resume())\n }\n child.stdout.on('data', (chunk: Buffer) => {\n if (failure || !count(chunk)) return\n if (!options.onLine) { stdout += stdoutDecoder.write(chunk); return }\n pending = Buffer.concat([pending, chunk])\n for (;;) {\n const end = pending.indexOf(10)\n if (end < 0) break\n if (end > maxLine) { fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.')); return }\n acceptLine(pending.subarray(0, end).toString('utf8'))\n pending = pending.subarray(end + 1)\n }\n if (pending.length > maxLine) fail(new CliProcessError('output-limit', 'A CLI protocol frame exceeded its limit.'))\n })\n child.stderr.on('data', (chunk: Buffer) => { if (count(chunk)) stderr = (stderr + stderrDecoder.write(chunk)).slice(-16_384) })\n try {\n if (signal.aborted) abort()\n if (!failure) child.stdin.end(options.input ?? '')\n await closed\n stdout += stdoutDecoder.end(); stderr += stderrDecoder.end()\n if (pending.length && !failure) acceptLine(pending.toString('utf8'))\n await lineWork\n if (failure) throw failure\n signal.throwIfAborted()\n return { stdout, stderr, exitCode }\n } finally {\n signal.removeEventListener('abort', abort)\n if (!exited) await stop()\n if (stopping) await stopping\n }\n}\n"],"mappings":"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"}
@@ -168,28 +168,16 @@ declare function isSubagentSession(session: unknown): boolean;
168
168
  /** Project keys come only from the host-validated session directory, never client input. */
169
169
  declare function projectIdFromCwd(cwd: string | undefined): string | undefined;
170
170
  /**
171
- * Remote contribution for model-settings change notifications.
172
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
173
- * subscribers degrade silently when that package is not installed.
174
- * The stream carries invalidations only — `ready` is never a settings snapshot.
171
+ * One settings owner's Remote stream. Typert accepts either case in an RPC
172
+ * segment, so the Cordis service key and the wire namespace use the same
173
+ * Pascal segment. The scope's first letter stays uppercase.
175
174
  */
176
- declare const MODEL_SETTINGS_CHANGES_REMOTE: {
177
- package: string;
178
- descriptors: {
179
- id: string;
180
- service: string;
181
- namespace: string;
182
- method: string;
183
- mode: "stream";
184
- invocation: {
185
- kind: "direct";
186
- };
187
- parameters: readonly [];
188
- cancellation: {
189
- parameter: "signal";
190
- };
191
- }[];
192
- };
175
+ interface ModelSettingsChangesIdentity {
176
+ readonly service: string;
177
+ readonly namespace: string;
178
+ }
179
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
180
+ declare function modelSettingsChangesIdentity(packageName: string): ModelSettingsChangesIdentity;
193
181
  /**
194
182
  * Repository extension: a native web provider announces that its local
195
183
  * availability changed. Kind is `search` or `fetch`; id is the provider id.
@@ -231,5 +219,5 @@ interface DshWebManagementDescriptor {
231
219
  */
232
220
  declare const CREATOR_PRESET_ID = "cordis";
233
221
  //#endregion
234
- export { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, ContextKnowledge, DshWebManagementDescriptor, EvidenceRef, KnowledgeKind, KnowledgeScope, MODEL_SETTINGS_CHANGES_REMOTE, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, type ModelRoute, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, producerMessageSource, projectIdFromCwd };
222
+ export { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, ContextKnowledge, DshWebManagementDescriptor, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, type ModelRoute, ModelSettingsChangesIdentity, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, modelSettingsChangesIdentity, producerMessageSource, projectIdFromCwd };
235
223
  //# sourceMappingURL=contracts.d.ts.map
package/lib/contracts.js CHANGED
@@ -29,25 +29,18 @@ function projectIdFromCwd(cwd) {
29
29
  if (!normalized) return void 0;
30
30
  return normalized === "/" || /^[A-Za-z]:\/$/.test(normalized) ? normalized : normalized.replace(/\/+$/, "");
31
31
  }
32
- /**
33
- * Remote contribution for model-settings change notifications.
34
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
35
- * subscribers degrade silently when that package is not installed.
36
- * The stream carries invalidations only — `ready` is never a settings snapshot.
37
- */
38
- const MODEL_SETTINGS_CHANGES_REMOTE = {
39
- package: "@klarkxy/dsh-model-hub",
40
- descriptors: [{
41
- id: "modelSettingsChanges/watch",
42
- service: "modelSettingsChangesController",
43
- namespace: "modelSettingsChanges",
44
- method: "watch",
45
- mode: "stream",
46
- invocation: { kind: "direct" },
47
- parameters: [],
48
- cancellation: { parameter: "signal" }
49
- }]
50
- };
32
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
33
+ function modelSettingsChangesIdentity(packageName) {
34
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
35
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
36
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
37
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
38
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
39
+ return {
40
+ service: segment,
41
+ namespace: segment
42
+ };
43
+ }
51
44
  /**
52
45
  * Repository extension: a native web provider announces that its local
53
46
  * availability changed. Kind is `search` or `fetch`; id is the provider id.
@@ -60,6 +53,6 @@ const WEB_PROVIDER_AVAILABILITY_EVENT = "web/provider-availability-updated";
60
53
  */
61
54
  const CREATOR_PRESET_ID = "cordis";
62
55
  //#endregion
63
- export { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, MODEL_SETTINGS_CHANGES_REMOTE, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, producerMessageSource, projectIdFromCwd };
56
+ export { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, modelSettingsChangesIdentity, producerMessageSource, projectIdFromCwd };
64
57
 
65
58
  //# sourceMappingURL=contracts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { ContextFormed } from '@deepseek-ai/dsh-llm'\nimport type { ModelRoute } from '@klarkxy/dsh-model-route'\n\n/**\n * The canonical home is @klarkxy/dsh-model-route; re-exported here so existing\n * plugin-kit consumers keep compiling.\n */\nexport type { ModelRoute }\n\n/** Public, browser-safe contracts. Host implementations stay in their owning packages. */\nexport type ProducerSourceKind = `plugin:${string}`\nexport type ProducerMessageSource = { kind: ProducerSourceKind; plugin: string } & ContextFormed\n\n/**\n * V4 sessions persist the producing extension in `kind`; the retired generic\n * `plugin` wrapper is not admitted. Each feature plugin declares its own kind.\n */\ndeclare module '@deepseek-ai/dsh-llm' {\n interface MessageSourceMap {\n [kind: ProducerSourceKind]: ProducerMessageSource\n }\n}\n\nexport function producerMessageSource(plugin: string): ProducerMessageSource {\n return { kind: `plugin:${plugin}`, plugin }\n}\n\nexport type RpcResult<T = unknown> = { ok: true; value: T } | { ok: false; error: { code: string; message: string } }\nexport const CHAT_EVENTS_SLOT = 'dsh-editor.chat.events'\nexport interface EvidenceRef { sessionId: string; seq: number; kind: 'user' | 'tool' | 'turn' | 'manual'; excerpt?: string }\nexport interface TaskContract {\n id: string\n sessionId: string\n sourceVersion: string\n revision: number\n goal: string\n deliverables: string[]\n inScope: string[]\n outOfScope: string[]\n constraints: string[]\n acceptance: string[]\n assumptions: string[]\n questions: string[]\n evidence: EvidenceRef[]\n readiness: 'pending' | 'clear-request' | 'user-confirmed' | 'disclosed-assumptions' | 'cancelled' | 'stale'\n updatedAt: number\n}\nexport interface TaskCheckpoint {\n id: string\n sessionId: string\n sourceVersion: string\n contractVersion?: number\n fromSeq: number\n toSeq: number\n revision: number\n status: 'running' | 'completed' | 'failed' | 'cancelled'\n items: Array<{ label: string; state: 'pending' | 'verified' | 'failed'; evidence: EvidenceRef[] }>\n constraints: string[]\n nextAction: string\n createdAt: number\n}\nexport type KnowledgeScope = { kind: 'global' } | { kind: 'project'; projectId: string }\nexport type KnowledgeKind = 'preference' | 'project-fact' | 'decision' | 'vocabulary' | 'activity' | 'lesson'\n/** Descriptive context, never an executable instruction or authorization. */\nexport interface ContextKnowledge {\n subject: string\n domain: string\n key: string\n aliases: string[]\n observedAt: number\n /** The user's own time expression, not an inferred completion date. */\n eventTime?: string\n activityStatus?: 'planned' | 'in-progress' | 'blocked' | 'paused' | 'completed' | 'cancelled' | 'unknown'\n}\n/** A scoped method. Observations remain candidates until explicitly accepted. */\nexport interface ProcedureKnowledge {\n origin: 'instruction' | 'observation'\n goal: string\n when: string[]\n steps: string[]\n avoid: string[]\n verify: string[]\n}\nexport interface MemoryRecord {\n id: string\n revision: number\n scope: KnowledgeScope\n kind: KnowledgeKind\n status: 'candidate' | 'active' | 'rejected' | 'superseded' | 'revoked' | 'deleted'\n title: string\n content: string\n tags: string[]\n evidence: EvidenceRef[]\n exceptions: string[]\n source: 'user' | 'memory' | 'dream' | 'self-improvement'\n context?: ContextKnowledge\n procedure?: ProcedureKnowledge\n createdAt: number\n updatedAt: number\n expiresAt?: number\n supersedes?: string[]\n /** Exact record revisions behind a derived candidate; revalidated on adoption. */\n basis?: Array<{ id: string; revision: number }>\n}\nexport type NewMemoryRecord = Omit<MemoryRecord, 'id' | 'revision' | 'createdAt' | 'updatedAt'>\nexport interface MemoryQuery { scope: KnowledgeScope; query?: string; kinds?: KnowledgeKind[]; statuses?: MemoryRecord['status'][]; limit?: number }\nexport interface MemoryMutationOptions { signal?: AbortSignal; isCurrent?: () => boolean }\nexport interface MemoryService {\n list(query: MemoryQuery): Promise<MemoryRecord[]>\n create(record: NewMemoryRecord, options?: MemoryMutationOptions): Promise<MemoryRecord>\n update(id: string, patch: Partial<Pick<MemoryRecord, 'title' | 'content' | 'tags' | 'exceptions' | 'status' | 'expiresAt'>>, expectedRevision: number, options?: MemoryMutationOptions): Promise<MemoryRecord>\n /** Explicit user promotion, persisted atomically on the same record; preserves expiry and evidence. */\n promoteToGlobal(id: string, expectedRevision: number, options?: MemoryMutationOptions): Promise<MemoryRecord>\n remove(id: string, expectedRevision: number, options?: MemoryMutationOptions): Promise<void>\n recall(query: MemoryQuery): Promise<MemoryRecord[]>\n}\n\n/**\n * The session creation metadata the host snapshots when it creates a delegated child\n * thread. Mirrors `CreateAgentOptions.meta` in `@deepseek-ai/dsh-agent`; it is durable\n * session data, so it survives a restart and needs no ancestry walk.\n */\nexport interface SubagentSessionLike {\n readonly header?: {\n readonly parentSession?: unknown\n readonly origin?: unknown\n readonly delegationDepth?: unknown\n readonly isSeeded?: unknown\n readonly classification?: unknown\n }\n}\n\n/**\n * True when `session` is a delegated child thread rather than a human-driven top-level\n * conversation.\n *\n * A message's `source.kind === 'user'` cannot answer this: a lead agent's dispatch prompt\n * is delivered as the child's own user message, so delegated work is indistinguishable\n * from a person typing once you only look at the message. Plugins use this to keep\n * automatic writes and derived model calls on top-level sessions.\n */\nexport function isSubagentSession(session: unknown): boolean {\n const header = (session as SubagentSessionLike | null | undefined)?.header\n if (!header || typeof header !== 'object') return false\n if (header.origin === 'subagent' || Number(header.delegationDepth ?? 0) > 0) return true\n // Native human forks also persist parentSession for history lineage. A fork's\n // seeded header distinguishes it from legacy children whose parent is the\n // only marker. Native rc.2 forks omit depth; patched hosts may persist zero.\n // Explicit delegation and unknown metadata still keep the child guard.\n const humanFork = header.isSeeded === true\n && (header.delegationDepth === undefined || header.delegationDepth === 0)\n && header.origin === undefined\n && (header.classification === undefined || header.classification === 'ordinary' || header.classification === 'free')\n return Boolean(header.parentSession) && !humanFork\n}\n\n/** Project keys come only from the host-validated session directory, never client input. */\nexport function projectIdFromCwd(cwd: string | undefined): string | undefined {\n if (typeof cwd !== 'string') return undefined\n const normalized = cwd.trim().replaceAll('\\\\', '/')\n if (!normalized) return undefined\n return normalized === '/' || /^[A-Za-z]:\\/$/.test(normalized) ? normalized : normalized.replace(/\\/+$/, '')\n}\n\n/**\n * Remote contribution for model-settings change notifications.\n * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);\n * subscribers degrade silently when that package is not installed.\n * The stream carries invalidations only — `ready` is never a settings snapshot.\n */\nexport const MODEL_SETTINGS_CHANGES_REMOTE = {\n package: '@klarkxy/dsh-model-hub',\n descriptors: [{\n id: 'modelSettingsChanges/watch',\n service: 'modelSettingsChangesController',\n namespace: 'modelSettingsChanges',\n method: 'watch',\n mode: 'stream' as const,\n invocation: { kind: 'direct' as const },\n parameters: [] as const,\n cancellation: { parameter: 'signal' as const },\n }],\n}\n\n/**\n * Repository extension: a native web provider announces that its local\n * availability changed. Kind is `search` or `fetch`; id is the provider id.\n * Listeners refresh management state and must not treat this as a network probe.\n */\nexport const WEB_PROVIDER_AVAILABILITY_EVENT = 'web/provider-availability-updated' as const\n\n/**\n * Optional display metadata a provider may attach when it registers directly\n * with `ctx.web`. Execution and credentials stay with the provider; this object\n * is display-only and must never carry secret values.\n */\nexport interface DshWebManagementDescriptor {\n readonly id: string\n readonly label: string\n readonly description: string\n readonly defaultBaseURL?: string\n readonly credentialRef?: string\n /** True when the key is shared with another surface. The page must not delete it. */\n readonly credentialShared?: boolean\n /** Optional notice naming the settings surface that owns a shared credential. */\n readonly credentialHint?: string\n readonly billing: 'request' | 'model-and-tools' | 'none' | 'unknown'\n /** Optional provider-supplied billing summary. */\n readonly pricing?: string\n /** Public HTTPS page explaining provider charges. */\n readonly pricingUrl?: string\n /** Public HTTPS page where the user can create an API key. */\n readonly signupUrl?: string\n /** Plugin that owns configuration. Native providers keep their settings there. */\n readonly configurationOwner?: string\n /** Explicit settings link supplied by the provider, never inferred from its id. */\n readonly configurationUrl?: string\n /** Fixed legacy route migrated by a bundle-owned adapter; not used by native plugins. */\n readonly legacyId?: string\n}\n\n/**\n * Host Creator agent-preset id (`agentPresets.composedPreset`).\n * Plugins that gate Creator-only tools or prompt sections compare against this.\n */\nexport const CREATOR_PRESET_ID = 'cordis'\n"],"mappings":";AAuBA,SAAgB,sBAAsB,QAAuC;CAC3E,OAAO;EAAE,MAAM,UAAU;EAAU;CAAO;AAC5C;AAGA,MAAa,mBAAmB;;;;;;;;;;AAiHhC,SAAgB,kBAAkB,SAA2B;CAC3D,MAAM,SAAU,SAAoD;CACpE,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO;CAClD,IAAI,OAAO,WAAW,cAAc,OAAO,OAAO,mBAAmB,CAAC,IAAI,GAAG,OAAO;CAKpF,MAAM,YAAY,OAAO,aAAa,SAChC,OAAO,oBAAoB,KAAA,KAAa,OAAO,oBAAoB,MACpE,OAAO,WAAW,KAAA,MACjB,OAAO,mBAAmB,KAAA,KAAa,OAAO,mBAAmB,cAAc,OAAO,mBAAmB;CAC/G,OAAO,QAAQ,OAAO,aAAa,KAAK,CAAC;AAC3C;;AAGA,SAAgB,iBAAiB,KAA6C;CAC5E,IAAI,OAAO,QAAQ,UAAU,OAAO,KAAA;CACpC,MAAM,aAAa,IAAI,KAAK,CAAC,CAAC,WAAW,MAAM,GAAG;CAClD,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,OAAO,eAAe,OAAO,gBAAgB,KAAK,UAAU,IAAI,aAAa,WAAW,QAAQ,QAAQ,EAAE;AAC5G;;;;;;;AAQA,MAAa,gCAAgC;CAC3C,SAAS;CACT,aAAa,CAAC;EACZ,IAAI;EACJ,SAAS;EACT,WAAW;EACX,QAAQ;EACR,MAAM;EACN,YAAY,EAAE,MAAM,SAAkB;EACtC,YAAY,CAAC;EACb,cAAc,EAAE,WAAW,SAAkB;CAC/C,CAAC;AACH;;;;;;AAOA,MAAa,kCAAkC;;;;;AAoC/C,MAAa,oBAAoB"}
1
+ {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { ContextFormed } from '@deepseek-ai/dsh-llm'\nimport type { ModelRoute } from '@klarkxy/dsh-model-route'\n\n/**\n * The canonical home is @klarkxy/dsh-model-route; re-exported here so existing\n * plugin-kit consumers keep compiling.\n */\nexport type { ModelRoute }\n\n/** Public, browser-safe contracts. Host implementations stay in their owning packages. */\nexport type ProducerSourceKind = `plugin:${string}`\nexport type ProducerMessageSource = { kind: ProducerSourceKind; plugin: string } & ContextFormed\n\n/**\n * V4 sessions persist the producing extension in `kind`; the retired generic\n * `plugin` wrapper is not admitted. Each feature plugin declares its own kind.\n */\ndeclare module '@deepseek-ai/dsh-llm' {\n interface MessageSourceMap {\n [kind: ProducerSourceKind]: ProducerMessageSource\n }\n}\n\nexport function producerMessageSource(plugin: string): ProducerMessageSource {\n return { kind: `plugin:${plugin}`, plugin }\n}\n\nexport type RpcResult<T = unknown> = { ok: true; value: T } | { ok: false; error: { code: string; message: string } }\nexport const CHAT_EVENTS_SLOT = 'dsh-editor.chat.events'\nexport interface EvidenceRef { sessionId: string; seq: number; kind: 'user' | 'tool' | 'turn' | 'manual'; excerpt?: string }\nexport interface TaskContract {\n id: string\n sessionId: string\n sourceVersion: string\n revision: number\n goal: string\n deliverables: string[]\n inScope: string[]\n outOfScope: string[]\n constraints: string[]\n acceptance: string[]\n assumptions: string[]\n questions: string[]\n evidence: EvidenceRef[]\n readiness: 'pending' | 'clear-request' | 'user-confirmed' | 'disclosed-assumptions' | 'cancelled' | 'stale'\n updatedAt: number\n}\nexport interface TaskCheckpoint {\n id: string\n sessionId: string\n sourceVersion: string\n contractVersion?: number\n fromSeq: number\n toSeq: number\n revision: number\n status: 'running' | 'completed' | 'failed' | 'cancelled'\n items: Array<{ label: string; state: 'pending' | 'verified' | 'failed'; evidence: EvidenceRef[] }>\n constraints: string[]\n nextAction: string\n createdAt: number\n}\nexport type KnowledgeScope = { kind: 'global' } | { kind: 'project'; projectId: string }\nexport type KnowledgeKind = 'preference' | 'project-fact' | 'decision' | 'vocabulary' | 'activity' | 'lesson'\n/** Descriptive context, never an executable instruction or authorization. */\nexport interface ContextKnowledge {\n subject: string\n domain: string\n key: string\n aliases: string[]\n observedAt: number\n /** The user's own time expression, not an inferred completion date. */\n eventTime?: string\n activityStatus?: 'planned' | 'in-progress' | 'blocked' | 'paused' | 'completed' | 'cancelled' | 'unknown'\n}\n/** A scoped method. Observations remain candidates until explicitly accepted. */\nexport interface ProcedureKnowledge {\n origin: 'instruction' | 'observation'\n goal: string\n when: string[]\n steps: string[]\n avoid: string[]\n verify: string[]\n}\nexport interface MemoryRecord {\n id: string\n revision: number\n scope: KnowledgeScope\n kind: KnowledgeKind\n status: 'candidate' | 'active' | 'rejected' | 'superseded' | 'revoked' | 'deleted'\n title: string\n content: string\n tags: string[]\n evidence: EvidenceRef[]\n exceptions: string[]\n source: 'user' | 'memory' | 'dream' | 'self-improvement'\n context?: ContextKnowledge\n procedure?: ProcedureKnowledge\n createdAt: number\n updatedAt: number\n expiresAt?: number\n supersedes?: string[]\n /** Exact record revisions behind a derived candidate; revalidated on adoption. */\n basis?: Array<{ id: string; revision: number }>\n}\nexport type NewMemoryRecord = Omit<MemoryRecord, 'id' | 'revision' | 'createdAt' | 'updatedAt'>\nexport interface MemoryQuery { scope: KnowledgeScope; query?: string; kinds?: KnowledgeKind[]; statuses?: MemoryRecord['status'][]; limit?: number }\nexport interface MemoryMutationOptions { signal?: AbortSignal; isCurrent?: () => boolean }\nexport interface MemoryService {\n list(query: MemoryQuery): Promise<MemoryRecord[]>\n create(record: NewMemoryRecord, options?: MemoryMutationOptions): Promise<MemoryRecord>\n update(id: string, patch: Partial<Pick<MemoryRecord, 'title' | 'content' | 'tags' | 'exceptions' | 'status' | 'expiresAt'>>, expectedRevision: number, options?: MemoryMutationOptions): Promise<MemoryRecord>\n /** Explicit user promotion, persisted atomically on the same record; preserves expiry and evidence. */\n promoteToGlobal(id: string, expectedRevision: number, options?: MemoryMutationOptions): Promise<MemoryRecord>\n remove(id: string, expectedRevision: number, options?: MemoryMutationOptions): Promise<void>\n recall(query: MemoryQuery): Promise<MemoryRecord[]>\n}\n\n/**\n * The session creation metadata the host snapshots when it creates a delegated child\n * thread. Mirrors `CreateAgentOptions.meta` in `@deepseek-ai/dsh-agent`; it is durable\n * session data, so it survives a restart and needs no ancestry walk.\n */\nexport interface SubagentSessionLike {\n readonly header?: {\n readonly parentSession?: unknown\n readonly origin?: unknown\n readonly delegationDepth?: unknown\n readonly isSeeded?: unknown\n readonly classification?: unknown\n }\n}\n\n/**\n * True when `session` is a delegated child thread rather than a human-driven top-level\n * conversation.\n *\n * A message's `source.kind === 'user'` cannot answer this: a lead agent's dispatch prompt\n * is delivered as the child's own user message, so delegated work is indistinguishable\n * from a person typing once you only look at the message. Plugins use this to keep\n * automatic writes and derived model calls on top-level sessions.\n */\nexport function isSubagentSession(session: unknown): boolean {\n const header = (session as SubagentSessionLike | null | undefined)?.header\n if (!header || typeof header !== 'object') return false\n if (header.origin === 'subagent' || Number(header.delegationDepth ?? 0) > 0) return true\n // Native human forks also persist parentSession for history lineage. A fork's\n // seeded header distinguishes it from legacy children whose parent is the\n // only marker. Native rc.2 forks omit depth; patched hosts may persist zero.\n // Explicit delegation and unknown metadata still keep the child guard.\n const humanFork = header.isSeeded === true\n && (header.delegationDepth === undefined || header.delegationDepth === 0)\n && header.origin === undefined\n && (header.classification === undefined || header.classification === 'ordinary' || header.classification === 'free')\n return Boolean(header.parentSession) && !humanFork\n}\n\n/** Project keys come only from the host-validated session directory, never client input. */\nexport function projectIdFromCwd(cwd: string | undefined): string | undefined {\n if (typeof cwd !== 'string') return undefined\n const normalized = cwd.trim().replaceAll('\\\\', '/')\n if (!normalized) return undefined\n return normalized === '/' || /^[A-Za-z]:\\/$/.test(normalized) ? normalized : normalized.replace(/\\/+$/, '')\n}\n\n/**\n * One settings owner's Remote stream. Typert accepts either case in an RPC\n * segment, so the Cordis service key and the wire namespace use the same\n * Pascal segment. The scope's first letter stays uppercase.\n */\nexport interface ModelSettingsChangesIdentity {\n readonly service: string\n readonly namespace: string\n}\n\n/** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */\nexport function modelSettingsChangesIdentity(packageName: string): ModelSettingsChangesIdentity {\n const scoped = /^@([^/]+)\\/(.+)$/.exec(packageName)\n const body = scoped ? `${scoped[1]}/${scoped[2]}` : packageName\n const parts = body.split(/[^A-Za-z0-9]+/).filter(Boolean)\n if (!parts.length) throw new Error('Model settings package name has no RPC segment')\n const segment = `${parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('')}ModelSettingsChanges`\n if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`)\n return { service: segment, namespace: segment }\n}\n\n/**\n * Repository extension: a native web provider announces that its local\n * availability changed. Kind is `search` or `fetch`; id is the provider id.\n * Listeners refresh management state and must not treat this as a network probe.\n */\nexport const WEB_PROVIDER_AVAILABILITY_EVENT = 'web/provider-availability-updated' as const\n\n/**\n * Optional display metadata a provider may attach when it registers directly\n * with `ctx.web`. Execution and credentials stay with the provider; this object\n * is display-only and must never carry secret values.\n */\nexport interface DshWebManagementDescriptor {\n readonly id: string\n readonly label: string\n readonly description: string\n readonly defaultBaseURL?: string\n readonly credentialRef?: string\n /** True when the key is shared with another surface. The page must not delete it. */\n readonly credentialShared?: boolean\n /** Optional notice naming the settings surface that owns a shared credential. */\n readonly credentialHint?: string\n readonly billing: 'request' | 'model-and-tools' | 'none' | 'unknown'\n /** Optional provider-supplied billing summary. */\n readonly pricing?: string\n /** Public HTTPS page explaining provider charges. */\n readonly pricingUrl?: string\n /** Public HTTPS page where the user can create an API key. */\n readonly signupUrl?: string\n /** Plugin that owns configuration. Native providers keep their settings there. */\n readonly configurationOwner?: string\n /** Explicit settings link supplied by the provider, never inferred from its id. */\n readonly configurationUrl?: string\n /** Fixed legacy route migrated by a bundle-owned adapter; not used by native plugins. */\n readonly legacyId?: string\n}\n\n/**\n * Host Creator agent-preset id (`agentPresets.composedPreset`).\n * Plugins that gate Creator-only tools or prompt sections compare against this.\n */\nexport const CREATOR_PRESET_ID = 'cordis'\n"],"mappings":";AAuBA,SAAgB,sBAAsB,QAAuC;CAC3E,OAAO;EAAE,MAAM,UAAU;EAAU;CAAO;AAC5C;AAGA,MAAa,mBAAmB;;;;;;;;;;AAiHhC,SAAgB,kBAAkB,SAA2B;CAC3D,MAAM,SAAU,SAAoD;CACpE,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO;CAClD,IAAI,OAAO,WAAW,cAAc,OAAO,OAAO,mBAAmB,CAAC,IAAI,GAAG,OAAO;CAKpF,MAAM,YAAY,OAAO,aAAa,SAChC,OAAO,oBAAoB,KAAA,KAAa,OAAO,oBAAoB,MACpE,OAAO,WAAW,KAAA,MACjB,OAAO,mBAAmB,KAAA,KAAa,OAAO,mBAAmB,cAAc,OAAO,mBAAmB;CAC/G,OAAO,QAAQ,OAAO,aAAa,KAAK,CAAC;AAC3C;;AAGA,SAAgB,iBAAiB,KAA6C;CAC5E,IAAI,OAAO,QAAQ,UAAU,OAAO,KAAA;CACpC,MAAM,aAAa,IAAI,KAAK,CAAC,CAAC,WAAW,MAAM,GAAG;CAClD,IAAI,CAAC,YAAY,OAAO,KAAA;CACxB,OAAO,eAAe,OAAO,gBAAgB,KAAK,UAAU,IAAI,aAAa,WAAW,QAAQ,QAAQ,EAAE;AAC5G;;AAaA,SAAgB,6BAA6B,aAAmD;CAC9F,MAAM,SAAS,mBAAmB,KAAK,WAAW;CAElD,MAAM,SADO,SAAS,GAAG,OAAO,GAAG,GAAG,OAAO,OAAO,YAAA,CACjC,MAAM,eAAe,CAAC,CAAC,OAAO,OAAO;CACxD,IAAI,CAAC,MAAM,QAAQ,MAAM,IAAI,MAAM,gDAAgD;CACnF,MAAM,UAAU,GAAG,MAAM,KAAI,SAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;CAC5F,IAAI,CAAC,sBAAsB,KAAK,OAAO,GAAG,MAAM,IAAI,MAAM,mDAAmD,SAAS;CACtH,OAAO;EAAE,SAAS;EAAS,WAAW;CAAQ;AAChD;;;;;;AAOA,MAAa,kCAAkC;;;;;AAoC/C,MAAa,oBAAoB"}
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, ContextKnowledge, DshWebManagementDescriptor, EvidenceRef, KnowledgeKind, KnowledgeScope, MODEL_SETTINGS_CHANGES_REMOTE, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRoute, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, producerMessageSource, projectIdFromCwd } from "./contracts.js";
1
+ import { CHAT_EVENTS_SLOT, CREATOR_PRESET_ID, ContextKnowledge, DshWebManagementDescriptor, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRoute, ModelSettingsChangesIdentity, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, WEB_PROVIDER_AVAILABILITY_EVENT, isSubagentSession, modelSettingsChangesIdentity, producerMessageSource, projectIdFromCwd } from "./contracts.js";
2
2
  import { openCompatibleDomain } from "./domain.js";
3
3
  import { HostRpcContext, registerHostRpc } from "./host-rpc.js";
4
4
  import { FeatureModelRoute, LlmTextCaller, callLlmText, resolveFeatureModel } from "./llm-call.js";
5
- export { CHAT_EVENTS_SLOT, type CREATOR_PRESET_ID, type ContextKnowledge, type DshWebManagementDescriptor, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MODEL_SETTINGS_CHANGES_REMOTE, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRoute, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type RpcResult, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type WEB_PROVIDER_AVAILABILITY_EVENT, callLlmText, type isSubagentSession, openCompatibleDomain, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
5
+ export { CHAT_EVENTS_SLOT, type CREATOR_PRESET_ID, type ContextKnowledge, type DshWebManagementDescriptor, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRoute, type ModelSettingsChangesIdentity, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type RpcResult, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type WEB_PROVIDER_AVAILABILITY_EVENT, callLlmText, type isSubagentSession, type modelSettingsChangesIdentity, openCompatibleDomain, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
package/lib/llm-call.d.ts CHANGED
@@ -17,6 +17,8 @@ interface LlmTextRequest {
17
17
  sessionId?: string;
18
18
  /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */
19
19
  purpose?: GenerateOptions['purpose'];
20
+ /** Pure control selection using the exact model captured for this call. */
21
+ configure?: Parameters<LlmRuntime['prepareCall']>[2];
20
22
  /** Rechecked before the call returns text. A false result discards the output. */
21
23
  isCurrent?: () => boolean;
22
24
  }
package/lib/llm-call.js CHANGED
@@ -58,10 +58,7 @@ async function readStream(stream, signal, live) {
58
58
  assembler.push(item.value);
59
59
  }
60
60
  } finally {
61
- try {
62
- const closing = iterator.return?.();
63
- if (closing) closing.catch(() => {});
64
- } catch {}
61
+ await iterator.return?.();
65
62
  }
66
63
  return assembler;
67
64
  }
@@ -76,7 +73,7 @@ async function callLlmText(llm, request) {
76
73
  model: request.route.model,
77
74
  ...effort ? { reasoningEffort: ReasoningEffortId(effort) } : {},
78
75
  maxTokens: request.maxTokens
79
- }, request.signal);
76
+ }, request.signal, request.configure);
80
77
  if (!live()) throw Object.assign(/* @__PURE__ */ new Error("调用已取消。"), { name: "AbortError" });
81
78
  const options = {
82
79
  provider: prepared.config.provider,
@@ -1 +1 @@
1
- {"version":3,"file":"llm-call.js","names":[],"sources":["../src/llm-call.ts"],"sourcesContent":["import {\n BlockAssembler, ReasoningEffortId, createUserMessage,\n type GenerateOptions, type LlmRuntime, type StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { producerMessageSource, type ModelRoute } from './contracts.ts'\n\n/** A feature's own saved model. Empty means the host default chat model. */\nexport interface FeatureModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>\n\nexport interface LlmTextRequest {\n plugin: string\n route: FeatureModelRoute\n system: string\n text: string\n maxTokens: number\n signal: AbortSignal\n sessionId?: string\n /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */\n purpose?: GenerateOptions['purpose']\n /** Rechecked before the call returns text. A false result discards the output. */\n isCurrent?: () => boolean\n}\n\nexport interface LlmTextResult {\n text: string\n provider: string\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\ntype SessionModelsHost = {\n agents?: { get(sessionId: string): { session?: { requestHeader?: () => { config?: ModelRoute; adapterDefaults?: { reasoningEffort?: unknown } } | undefined } } | undefined }\n sessionProjections?: { stateOf(session: unknown, key: string): { pending?: ModelRoute | null } | undefined }\n agentDefaultModel?: { currentSelection(): ModelRoute | undefined }\n}\n\nfunction routeOf(value: { provider?: unknown; model?: unknown; reasoningEffort?: unknown } | undefined): FeatureModelRoute | undefined {\n if (typeof value?.provider !== 'string' || !value.provider.trim()) return undefined\n if (typeof value.model !== 'string' || !value.model.trim()) return undefined\n const effort = typeof value.reasoningEffort === 'string' ? value.reasoningEffort.trim() : ''\n return effort\n ? { provider: value.provider.trim(), model: value.model.trim(), reasoningEffort: effort }\n : { provider: value.provider.trim(), model: value.model.trim() }\n}\n\n/**\n * Plugin selection first; otherwise the live session model, then the host default.\n * Cordis callers must declare agents, sessionProjections and agentDefaultModel\n * in their inject list so the fallback reads use the caller's live services.\n */\nexport function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined {\n const explicit = routeOf(saved)\n if (explicit) return explicit\n const face = host as SessionModelsHost | undefined\n if (sessionId && face?.agents && face.sessionProjections) {\n const session = face.agents.get(sessionId)?.session\n if (session) {\n try {\n const pending = face.sessionProjections.stateOf(session, 'modelSelection')?.pending\n const selected = routeOf(pending ?? undefined)\n if (selected) return selected\n } catch { /* fall through to the recorded route */ }\n const header = session.requestHeader?.()\n const recorded = header?.config\n if (recorded) {\n const selected = header?.adapterDefaults?.reasoningEffort === true\n ? { provider: recorded.provider, model: recorded.model }\n : recorded\n const route = routeOf(selected)\n if (route) return route\n }\n }\n }\n return routeOf(face?.agentDefaultModel?.currentSelection())\n}\n\nfunction joinText(assembler: BlockAssembler): string {\n return assembler.blocks()\n .filter((block): block is { type: 'text'; text: string } => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\nasync function readStream(stream: AsyncIterable<StreamChunk>, signal: AbortSignal, live: () => boolean): Promise<BlockAssembler> {\n const assembler = new BlockAssembler()\n const iterator = stream[Symbol.asyncIterator]()\n try {\n while (live()) {\n if (signal.aborted) break\n const item = await iterator.next()\n if (item.done) break\n assembler.push(item.value)\n }\n } finally {\n try { const closing = iterator.return?.(); if (closing) void closing.catch(() => {}) } catch { /* producer ignored abort */ }\n }\n return assembler\n}\n\n/** One native model call. No retry, timeout policy, or provider queue. */\nexport async function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult> {\n request.signal.throwIfAborted()\n const live = () => request.isCurrent?.() !== false && !request.signal.aborted\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const effort = request.route.reasoningEffort?.trim()\n const prepared = await llm.prepareCall({\n provider: request.route.provider,\n model: request.route.model,\n ...(effort ? { reasoningEffort: ReasoningEffortId(effort) } : {}),\n maxTokens: request.maxTokens,\n }, request.signal)\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const options: GenerateOptions = {\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {}),\n ...(prepared.config.maxTokens !== undefined ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens }),\n ...(prepared.config.temperature !== undefined ? { temperature: prepared.config.temperature } : {}),\n ...(prepared.config.stop ? { stop: [...prepared.config.stop] } : {}),\n system: request.system,\n messages: [createUserMessage({\n source: producerMessageSource(request.plugin),\n content: [{ type: 'text', text: request.text }],\n })],\n signal: request.signal,\n ...(request.purpose ? { purpose: request.purpose } : {}),\n ...(request.sessionId ? { sessionId: request.sessionId as GenerateOptions['sessionId'] } : {}),\n }\n const assembler = await readStream(prepared.stream(options), request.signal, live)\n if (request.signal.aborted || assembler.finish.kind === 'aborted') {\n throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n }\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n if (assembler.finish.kind === 'error') {\n const code = assembler.finish.failure.code\n throw Object.assign(new Error(assembler.finish.failure.message || '模型调用失败。'), { code })\n }\n if (assembler.blocks().some(block => block.type === 'tool-call') || assembler.finish.kind === 'tool-calls') {\n throw new Error('这次调用不使用工具。')\n }\n if (assembler.finish.kind !== 'stop') throw new Error('模型调用未完成。')\n return {\n text: joinText(assembler),\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(assembler.usage ? { inputTokens: assembler.usage.inputTokens, outputTokens: assembler.usage.outputTokens } : {}),\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"llm-call.js","names":[],"sources":["../src/llm-call.ts"],"sourcesContent":["import {\n BlockAssembler, ReasoningEffortId, createUserMessage,\n type GenerateOptions, type LlmRuntime, type StreamChunk,\n} from '@deepseek-ai/dsh-llm'\nimport { producerMessageSource, type ModelRoute } from './contracts.ts'\n\n/** A feature's own saved model. Empty means the host default chat model. */\nexport interface FeatureModelRoute {\n readonly provider: string\n readonly model: string\n readonly reasoningEffort?: string\n}\n\nexport type LlmTextCaller = Pick<LlmRuntime, 'prepareCall' | 'resolveCallConfig'>\n\nexport interface LlmTextRequest {\n plugin: string\n route: FeatureModelRoute\n system: string\n text: string\n maxTokens: number\n signal: AbortSignal\n sessionId?: string\n /** Auxiliary-call marker adapters may special-case (e.g. disable thinking for titles). */\n purpose?: GenerateOptions['purpose']\n /** Pure control selection using the exact model captured for this call. */\n configure?: Parameters<LlmRuntime['prepareCall']>[2]\n /** Rechecked before the call returns text. A false result discards the output. */\n isCurrent?: () => boolean\n}\n\nexport interface LlmTextResult {\n text: string\n provider: string\n model: string\n inputTokens?: number\n outputTokens?: number\n}\n\ntype SessionModelsHost = {\n agents?: { get(sessionId: string): { session?: { requestHeader?: () => { config?: ModelRoute; adapterDefaults?: { reasoningEffort?: unknown } } | undefined } } | undefined }\n sessionProjections?: { stateOf(session: unknown, key: string): { pending?: ModelRoute | null } | undefined }\n agentDefaultModel?: { currentSelection(): ModelRoute | undefined }\n}\n\nfunction routeOf(value: { provider?: unknown; model?: unknown; reasoningEffort?: unknown } | undefined): FeatureModelRoute | undefined {\n if (typeof value?.provider !== 'string' || !value.provider.trim()) return undefined\n if (typeof value.model !== 'string' || !value.model.trim()) return undefined\n const effort = typeof value.reasoningEffort === 'string' ? value.reasoningEffort.trim() : ''\n return effort\n ? { provider: value.provider.trim(), model: value.model.trim(), reasoningEffort: effort }\n : { provider: value.provider.trim(), model: value.model.trim() }\n}\n\n/**\n * Plugin selection first; otherwise the live session model, then the host default.\n * Cordis callers must declare agents, sessionProjections and agentDefaultModel\n * in their inject list so the fallback reads use the caller's live services.\n */\nexport function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined {\n const explicit = routeOf(saved)\n if (explicit) return explicit\n const face = host as SessionModelsHost | undefined\n if (sessionId && face?.agents && face.sessionProjections) {\n const session = face.agents.get(sessionId)?.session\n if (session) {\n try {\n const pending = face.sessionProjections.stateOf(session, 'modelSelection')?.pending\n const selected = routeOf(pending ?? undefined)\n if (selected) return selected\n } catch { /* fall through to the recorded route */ }\n const header = session.requestHeader?.()\n const recorded = header?.config\n if (recorded) {\n const selected = header?.adapterDefaults?.reasoningEffort === true\n ? { provider: recorded.provider, model: recorded.model }\n : recorded\n const route = routeOf(selected)\n if (route) return route\n }\n }\n }\n return routeOf(face?.agentDefaultModel?.currentSelection())\n}\n\nfunction joinText(assembler: BlockAssembler): string {\n return assembler.blocks()\n .filter((block): block is { type: 'text'; text: string } => block.type === 'text')\n .map(block => block.text)\n .join('')\n}\n\nasync function readStream(stream: AsyncIterable<StreamChunk>, signal: AbortSignal, live: () => boolean): Promise<BlockAssembler> {\n const assembler = new BlockAssembler()\n const iterator = stream[Symbol.asyncIterator]()\n try {\n while (live()) {\n if (signal.aborted) break\n const item = await iterator.next()\n if (item.done) break\n assembler.push(item.value)\n }\n } finally {\n // Settlement includes producer cleanup; never leave a detached return()\n // running after the caller has released its lifecycle ownership.\n await iterator.return?.()\n }\n return assembler\n}\n\n/** One native model call. No retry, timeout policy, or provider queue. */\nexport async function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult> {\n request.signal.throwIfAborted()\n const live = () => request.isCurrent?.() !== false && !request.signal.aborted\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const effort = request.route.reasoningEffort?.trim()\n const prepared = await llm.prepareCall({\n provider: request.route.provider,\n model: request.route.model,\n ...(effort ? { reasoningEffort: ReasoningEffortId(effort) } : {}),\n maxTokens: request.maxTokens,\n }, request.signal, request.configure)\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n const options: GenerateOptions = {\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {}),\n ...(prepared.config.maxTokens !== undefined ? { maxTokens: Math.min(request.maxTokens, prepared.config.maxTokens) } : { maxTokens: request.maxTokens }),\n ...(prepared.config.temperature !== undefined ? { temperature: prepared.config.temperature } : {}),\n ...(prepared.config.stop ? { stop: [...prepared.config.stop] } : {}),\n system: request.system,\n messages: [createUserMessage({\n source: producerMessageSource(request.plugin),\n content: [{ type: 'text', text: request.text }],\n })],\n signal: request.signal,\n ...(request.purpose ? { purpose: request.purpose } : {}),\n ...(request.sessionId ? { sessionId: request.sessionId as GenerateOptions['sessionId'] } : {}),\n }\n const assembler = await readStream(prepared.stream(options), request.signal, live)\n if (request.signal.aborted || assembler.finish.kind === 'aborted') {\n throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n }\n if (!live()) throw Object.assign(new Error('调用已取消。'), { name: 'AbortError' })\n if (assembler.finish.kind === 'error') {\n const code = assembler.finish.failure.code\n throw Object.assign(new Error(assembler.finish.failure.message || '模型调用失败。'), { code })\n }\n if (assembler.blocks().some(block => block.type === 'tool-call') || assembler.finish.kind === 'tool-calls') {\n throw new Error('这次调用不使用工具。')\n }\n if (assembler.finish.kind !== 'stop') throw new Error('模型调用未完成。')\n return {\n text: joinText(assembler),\n provider: prepared.config.provider,\n model: prepared.config.model,\n ...(assembler.usage ? { inputTokens: assembler.usage.inputTokens, outputTokens: assembler.usage.outputTokens } : {}),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { ModelSettingsChangesIdentity } from "./contracts.js";
2
+ import { MODEL_SETTINGS_CHANGED_EVENT, ModelSettingsChange } from "./model-settings.js";
3
+ import { ModelSettingsChangeFrame } from "./model-settings-client.js";
4
+ import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
5
+ import { Context } from "@deepseek-ai/cordis";
6
+ //#region src/model-settings-changes.d.ts
7
+ interface ChangeEvents {
8
+ on(name: typeof MODEL_SETTINGS_CHANGED_EVENT, callback: (change: ModelSettingsChange) => void): () => void;
9
+ on(name: 'settings/document-updated', callback: (ns: string, revision: number) => void): () => void;
10
+ }
11
+ /** Attach first, then yield ready: commits during a consumer's baseline read
12
+ * remain queued. Coalescing revisions never invents a settings snapshot.
13
+ * `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */
14
+ declare function watchModelSettingsChanges(events: ChangeEvents, signal: AbortSignal, ownerNs?: string): AsyncGenerator<ModelSettingsChangeFrame>;
15
+ /** One owner's invalidation stream, registered on the calling plugin fiber.
16
+ * Nothing here is stored in a module-level singleton. */
17
+ declare class ModelSettingsChangesController extends TypertRemoteService {
18
+ private readonly lifetime;
19
+ private readonly ownerNs;
20
+ constructor(ctx: Context, identity: ModelSettingsChangesIdentity, ownerNs?: string);
21
+ watch(signal: AbortSignal): AsyncIterable<ModelSettingsChangeFrame>;
22
+ }
23
+ /** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */
24
+ declare function createModelSettingsChanges(ctx: Context, identity: ModelSettingsChangesIdentity, ownerNs?: string): ModelSettingsChangesController;
25
+ //#endregion
26
+ export { ModelSettingsChangesController, createModelSettingsChanges, watchModelSettingsChanges };
27
+ //# sourceMappingURL=model-settings-changes.d.ts.map
@@ -0,0 +1,161 @@
1
+ import { MODEL_SETTINGS_CHANGED_EVENT } from "./model-settings.js";
2
+ import { parseModelSettingsChangeFrame } from "./model-settings-client.js";
3
+ import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
4
+ //#region src/model-settings-changes.ts
5
+ var __runInitializers = function(thisArg, initializers, value) {
6
+ var useValue = arguments.length > 2;
7
+ for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
8
+ return useValue ? value : void 0;
9
+ };
10
+ var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
11
+ function accept(f) {
12
+ if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
13
+ return f;
14
+ }
15
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
16
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
17
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
18
+ var _, done = false;
19
+ for (var i = decorators.length - 1; i >= 0; i--) {
20
+ var context = {};
21
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
22
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
23
+ context.addInitializer = function(f) {
24
+ if (done) throw new TypeError("Cannot add initializers after decoration has completed");
25
+ extraInitializers.push(accept(f || null));
26
+ };
27
+ var result = (0, decorators[i])(kind === "accessor" ? {
28
+ get: descriptor.get,
29
+ set: descriptor.set
30
+ } : descriptor[key], context);
31
+ if (kind === "accessor") {
32
+ if (result === void 0) continue;
33
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
34
+ if (_ = accept(result.get)) descriptor.get = _;
35
+ if (_ = accept(result.set)) descriptor.set = _;
36
+ if (_ = accept(result.init)) initializers.unshift(_);
37
+ } else if (_ = accept(result)) {
38
+ if (kind === "field") initializers.unshift(_);
39
+ else descriptor[key] = _;
40
+ }
41
+ }
42
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
43
+ done = true;
44
+ };
45
+ /** Attach first, then yield ready: commits during a consumer's baseline read
46
+ * remain queued. Coalescing revisions never invents a settings snapshot.
47
+ * `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */
48
+ async function* watchModelSettingsChanges(events, signal, ownerNs) {
49
+ signal.throwIfAborted();
50
+ const pending = /* @__PURE__ */ new Map();
51
+ let full = false;
52
+ let wake;
53
+ const changed = (ns, revision) => {
54
+ if (ownerNs !== void 0 && ns !== ownerNs) return;
55
+ let frame;
56
+ try {
57
+ frame = parseModelSettingsChangeFrame({
58
+ kind: "changed",
59
+ ns,
60
+ revision
61
+ });
62
+ } catch {
63
+ return;
64
+ }
65
+ if (!full) {
66
+ if (!pending.has(ns) && pending.size >= 256) {
67
+ pending.clear();
68
+ full = true;
69
+ } else pending.set(ns, frame);
70
+ }
71
+ wake?.();
72
+ };
73
+ const disposers = [];
74
+ const cleanup = () => {
75
+ for (const off of disposers.splice(0).reverse()) off();
76
+ pending.clear();
77
+ };
78
+ const aborted = () => {
79
+ cleanup();
80
+ wake?.();
81
+ };
82
+ signal.addEventListener("abort", aborted, { once: true });
83
+ try {
84
+ disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)));
85
+ disposers.push(events.on("settings/document-updated", changed));
86
+ signal.throwIfAborted();
87
+ yield { kind: "ready" };
88
+ while (true) {
89
+ signal.throwIfAborted();
90
+ if (full) {
91
+ full = false;
92
+ yield { kind: "ready" };
93
+ continue;
94
+ }
95
+ if (pending.size) {
96
+ const [ns, frame] = pending.entries().next().value;
97
+ pending.delete(ns);
98
+ yield frame;
99
+ continue;
100
+ }
101
+ await new Promise((resolve) => {
102
+ wake = resolve;
103
+ if (signal.aborted || full || pending.size) resolve();
104
+ });
105
+ wake = void 0;
106
+ }
107
+ } finally {
108
+ signal.removeEventListener("abort", aborted);
109
+ cleanup();
110
+ }
111
+ }
112
+ /** One owner's invalidation stream, registered on the calling plugin fiber.
113
+ * Nothing here is stored in a module-level singleton. */
114
+ let ModelSettingsChangesController = (() => {
115
+ let _classSuper = TypertRemoteService;
116
+ let _instanceExtraInitializers = [];
117
+ let _watch_decorators;
118
+ return class ModelSettingsChangesController extends _classSuper {
119
+ static {
120
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
121
+ _watch_decorators = [Remote({ mode: "stream" })];
122
+ __esDecorate(this, null, _watch_decorators, {
123
+ kind: "method",
124
+ name: "watch",
125
+ static: false,
126
+ private: false,
127
+ access: {
128
+ has: (obj) => "watch" in obj,
129
+ get: (obj) => obj.watch
130
+ },
131
+ metadata: _metadata
132
+ }, null, _instanceExtraInitializers);
133
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, {
134
+ enumerable: true,
135
+ configurable: true,
136
+ writable: true,
137
+ value: _metadata
138
+ });
139
+ }
140
+ lifetime = (__runInitializers(this, _instanceExtraInitializers), new AbortController());
141
+ ownerNs;
142
+ constructor(ctx, identity, ownerNs) {
143
+ super(ctx, identity.service, { namespace: identity.namespace });
144
+ this.ownerNs = ownerNs;
145
+ ctx.effect(() => () => {
146
+ this.lifetime.abort(/* @__PURE__ */ new Error("Model settings sync owner unloaded"));
147
+ });
148
+ }
149
+ watch(signal) {
150
+ return watchModelSettingsChanges(this.ctx, AbortSignal.any([this.lifetime.signal, signal]), this.ownerNs);
151
+ }
152
+ };
153
+ })();
154
+ /** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */
155
+ function createModelSettingsChanges(ctx, identity, ownerNs) {
156
+ return new ModelSettingsChangesController(ctx, identity, ownerNs);
157
+ }
158
+ //#endregion
159
+ export { ModelSettingsChangesController, createModelSettingsChanges, watchModelSettingsChanges };
160
+
161
+ //# sourceMappingURL=model-settings-changes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-settings-changes.js","names":[],"sources":["../src/model-settings-changes.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'\nimport type { ModelSettingsChangesIdentity } from './contracts.ts'\nimport { MODEL_SETTINGS_CHANGED_EVENT, type ModelSettingsChange } from './model-settings.ts'\nimport { parseModelSettingsChangeFrame, type ModelSettingsChangeFrame } from './model-settings-client.ts'\n\ninterface ChangeEvents {\n on(name: typeof MODEL_SETTINGS_CHANGED_EVENT, callback: (change: ModelSettingsChange) => void): () => void\n on(name: 'settings/document-updated', callback: (ns: string, revision: number) => void): () => void\n}\n\n/** Attach first, then yield ready: commits during a consumer's baseline read\n * remain queued. Coalescing revisions never invents a settings snapshot.\n * `ownerNs` keeps one plugin's stream to its own settings; omit it to forward every namespace. */\nexport async function* watchModelSettingsChanges(events: ChangeEvents, signal: AbortSignal, ownerNs?: string): AsyncGenerator<ModelSettingsChangeFrame> {\n signal.throwIfAborted()\n const pending = new Map<string, ModelSettingsChangeFrame>()\n let full = false\n let wake: (() => void) | undefined\n const changed = (ns: string, revision: number) => {\n if (ownerNs !== undefined && ns !== ownerNs) return\n let frame: ModelSettingsChangeFrame\n try { frame = parseModelSettingsChangeFrame({ kind: 'changed', ns, revision }) } catch { return }\n if (!full) {\n if (!pending.has(ns) && pending.size >= 256) { pending.clear(); full = true }\n else pending.set(ns, frame)\n }\n wake?.()\n }\n const disposers: (() => void)[] = []\n const cleanup = () => {\n for (const off of disposers.splice(0).reverse()) off()\n pending.clear()\n }\n // Release listeners even if the consumer is paused at a yielded frame.\n const aborted = () => { cleanup(); wake?.() }\n signal.addEventListener('abort', aborted, { once: true })\n try {\n disposers.push(events.on(MODEL_SETTINGS_CHANGED_EVENT, ({ ns, revision }) => changed(ns, revision)))\n disposers.push(events.on('settings/document-updated', changed))\n signal.throwIfAborted()\n yield { kind: 'ready' }\n while (true) {\n signal.throwIfAborted()\n if (full) { full = false; yield { kind: 'ready' }; continue }\n if (pending.size) {\n const [ns, frame] = pending.entries().next().value!\n pending.delete(ns)\n yield frame\n continue\n }\n await new Promise<void>(resolve => {\n wake = resolve\n if (signal.aborted || full || pending.size) resolve()\n })\n wake = undefined\n }\n } finally {\n signal.removeEventListener('abort', aborted)\n cleanup()\n }\n}\n\n/** One owner's invalidation stream, registered on the calling plugin fiber.\n * Nothing here is stored in a module-level singleton. */\nexport class ModelSettingsChangesController extends TypertRemoteService {\n private readonly lifetime = new AbortController()\n private readonly ownerNs: string | undefined\n constructor(ctx: Context, identity: ModelSettingsChangesIdentity, ownerNs?: string) {\n super(ctx, identity.service, { namespace: identity.namespace })\n this.ownerNs = ownerNs\n ctx.effect(() => () => { this.lifetime.abort(new Error('Model settings sync owner unloaded')) })\n }\n\n @Remote({ mode: 'stream' })\n watch(signal: AbortSignal): AsyncIterable<ModelSettingsChangeFrame> {\n return watchModelSettingsChanges(this.ctx as unknown as ChangeEvents, AbortSignal.any([this.lifetime.signal, signal]), this.ownerNs)\n }\n}\n\n/** Register this owner's controller. `ownerNs` limits the stream to that settings namespace. */\nexport function createModelSettingsChanges(ctx: Context, identity: ModelSettingsChangesIdentity, ownerNs?: string): ModelSettingsChangesController {\n return new ModelSettingsChangesController(ctx, identity, ownerNs)\n}\n"],"mappings":";;;;AAAA,IAAI,oBAAwD,SAAU,SAAS,cAAc,OAAO;CAChG,IAAI,WAAW,UAAU,SAAS;CAClC,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KACrC,QAAQ,WAAW,aAAa,EAAE,CAAC,KAAK,SAAS,KAAK,IAAI,aAAa,EAAE,CAAC,KAAK,OAAO;CAE1F,OAAO,WAAW,QAAQ,KAAK;AACnC;AACA,IAAI,eAA8C,SAAU,MAAM,cAAc,YAAY,WAAW,cAAc,mBAAmB;CACpI,SAAS,OAAO,GAAG;EAAE,IAAI,MAAM,KAAK,KAAK,OAAO,MAAM,YAAY,MAAM,IAAI,UAAU,mBAAmB;EAAG,OAAO;CAAG;CACtH,IAAI,OAAO,UAAU,MAAM,MAAM,SAAS,WAAW,QAAQ,SAAS,WAAW,QAAQ;CACzF,IAAI,SAAS,CAAC,gBAAgB,OAAO,UAAU,YAAY,OAAO,KAAK,YAAY;CACnF,IAAI,aAAa,iBAAiB,SAAS,OAAO,yBAAyB,QAAQ,UAAU,IAAI,IAAI,CAAC;CACtG,IAAI,GAAG,OAAO;CACd,KAAK,IAAI,IAAI,WAAW,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,IAAI,UAAU,CAAC;EACf,KAAK,IAAI,KAAK,WAAW,QAAQ,KAAK,MAAM,WAAW,CAAC,IAAI,UAAU;EACtE,KAAK,IAAI,KAAK,UAAU,QAAQ,QAAQ,OAAO,KAAK,UAAU,OAAO;EACrE,QAAQ,iBAAiB,SAAU,GAAG;GAAE,IAAI,MAAM,MAAM,IAAI,UAAU,wDAAwD;GAAG,kBAAkB,KAAK,OAAO,KAAK,IAAI,CAAC;EAAG;EAC5K,IAAI,UAAU,GAAG,WAAW,GAAA,CAAI,SAAS,aAAa;GAAE,KAAK,WAAW;GAAK,KAAK,WAAW;EAAI,IAAI,WAAW,MAAM,OAAO;EAC7H,IAAI,SAAS,YAAY;GACrB,IAAI,WAAW,KAAK,GAAG;GACvB,IAAI,WAAW,QAAQ,OAAO,WAAW,UAAU,MAAM,IAAI,UAAU,iBAAiB;GACxF,IAAI,IAAI,OAAO,OAAO,GAAG,GAAG,WAAW,MAAM;GAC7C,IAAI,IAAI,OAAO,OAAO,GAAG,GAAG,WAAW,MAAM;GAC7C,IAAI,IAAI,OAAO,OAAO,IAAI,GAAG,aAAa,QAAQ,CAAC;EACvD,OACK,IAAI,IAAI,OAAO,MAAM,GAAG;GACzB,IAAI,SAAS,SAAS,aAAa,QAAQ,CAAC;QACvC,WAAW,OAAO;EAC3B;CACJ;CACA,IAAI,QAAQ,OAAO,eAAe,QAAQ,UAAU,MAAM,UAAU;CACpE,OAAO;AACX;;;;AAOA,gBAAuB,0BAA0B,QAAQ,QAAQ,SAAS;CACtE,OAAO,eAAe;CACtB,MAAM,0BAAU,IAAI,IAAI;CACxB,IAAI,OAAO;CACX,IAAI;CACJ,MAAM,WAAW,IAAI,aAAa;EAC9B,IAAI,YAAY,KAAA,KAAa,OAAO,SAChC;EACJ,IAAI;EACJ,IAAI;GACA,QAAQ,8BAA8B;IAAE,MAAM;IAAW;IAAI;GAAS,CAAC;EAC3E,QACM;GACF;EACJ;EACA,IAAI,CAAC,MAAM;GACP,IAAI,CAAC,QAAQ,IAAI,EAAE,KAAK,QAAQ,QAAQ,KAAK;IACzC,QAAQ,MAAM;IACd,OAAO;GACX,OAEI,QAAQ,IAAI,IAAI,KAAK;EAC7B;EACA,OAAO;CACX;CACA,MAAM,YAAY,CAAC;CACnB,MAAM,gBAAgB;EAClB,KAAK,MAAM,OAAO,UAAU,OAAO,CAAC,CAAC,CAAC,QAAQ,GAC1C,IAAI;EACR,QAAQ,MAAM;CAClB;CAEA,MAAM,gBAAgB;EAAE,QAAQ;EAAG,OAAO;CAAG;CAC7C,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CACxD,IAAI;EACA,UAAU,KAAK,OAAO,GAAG,+BAA+B,EAAE,IAAI,eAAe,QAAQ,IAAI,QAAQ,CAAC,CAAC;EACnG,UAAU,KAAK,OAAO,GAAG,6BAA6B,OAAO,CAAC;EAC9D,OAAO,eAAe;EACtB,MAAM,EAAE,MAAM,QAAQ;EACtB,OAAO,MAAM;GACT,OAAO,eAAe;GACtB,IAAI,MAAM;IACN,OAAO;IACP,MAAM,EAAE,MAAM,QAAQ;IACtB;GACJ;GACA,IAAI,QAAQ,MAAM;IACd,MAAM,CAAC,IAAI,SAAS,QAAQ,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC;IAC7C,QAAQ,OAAO,EAAE;IACjB,MAAM;IACN;GACJ;GACA,MAAM,IAAI,SAAQ,YAAW;IACzB,OAAO;IACP,IAAI,OAAO,WAAW,QAAQ,QAAQ,MAClC,QAAQ;GAChB,CAAC;GACD,OAAO,KAAA;EACX;CACJ,UACQ;EACJ,OAAO,oBAAoB,SAAS,OAAO;EAC3C,QAAQ;CACZ;AACJ;;;AAGA,IAAI,wCAAwC;CACxC,IAAI,cAAc;CAClB,IAAI,6BAA6B,CAAC;CAClC,IAAI;CACJ,OAAO,MAAM,uCAAuC,YAAY;EAC5D;GACI,MAAM,YAAY,OAAO,WAAW,cAAc,OAAO,WAAW,OAAO,OAAO,YAAY,OAAO,aAAa,IAAI,IAAI,KAAK;GAC/H,oBAAoB,CAAC,OAAO,EAAE,MAAM,SAAS,CAAC,CAAC;GAC/C,aAAa,MAAM,MAAM,mBAAmB;IAAE,MAAM;IAAU,MAAM;IAAS,QAAQ;IAAO,SAAS;IAAO,QAAQ;KAAE,MAAK,QAAO,WAAW;KAAK,MAAK,QAAO,IAAI;IAAM;IAAG,UAAU;GAAU,GAAG,MAAM,0BAA0B;GAClO,IAAI,WAAW,OAAO,eAAe,MAAM,OAAO,UAAU;IAAE,YAAY;IAAM,cAAc;IAAM,UAAU;IAAM,OAAO;GAAU,CAAC;EAC1I;EACA,YAAY,kBAAkB,MAAM,0BAA0B,GAAG,IAAI,gBAAgB;EACrF;EACA,YAAY,KAAK,UAAU,SAAS;GAChC,MAAM,KAAK,SAAS,SAAS,EAAE,WAAW,SAAS,UAAU,CAAC;GAC9D,KAAK,UAAU;GACf,IAAI,mBAAmB;IAAE,KAAK,SAAS,sBAAM,IAAI,MAAM,oCAAoC,CAAC;GAAG,CAAC;EACpG;EACA,MAAM,QAAQ;GACV,OAAO,0BAA0B,KAAK,KAAK,YAAY,IAAI,CAAC,KAAK,SAAS,QAAQ,MAAM,CAAC,GAAG,KAAK,OAAO;EAC5G;CACJ;AACJ,EAAA,CAAG;;AAGH,SAAgB,2BAA2B,KAAK,UAAU,SAAS;CAC/D,OAAO,IAAI,+BAA+B,KAAK,UAAU,OAAO;AACpE"}
@@ -1,3 +1,4 @@
1
+ import { ModelSettingsChangesIdentity } from "./contracts.js";
1
2
  import { TypertRemoteContribution } from "@deepseek-ai/dsh-typert-protocol";
2
3
  //#region src/model-settings-client.d.ts
3
4
  type ModelSettingsChangeFrame = {
@@ -9,8 +10,14 @@ type ModelSettingsChangeFrame = {
9
10
  };
10
11
  /** This stream carries invalidations only; ready is never a settings snapshot. */
11
12
  declare function parseModelSettingsChangeFrame(value: unknown): ModelSettingsChangeFrame;
12
- /** One contribution mounted by Model Hub; consumers only inject its service. */
13
- declare const modelSettingsChangesRemote: TypertRemoteContribution;
13
+ /** One owner's contribution. Each package mounts only its own endpoint. */
14
+ declare function modelSettingsChangesRemote(packageName: string, identity?: ModelSettingsChangesIdentity): TypertRemoteContribution;
15
+ interface RemoteMountContext {
16
+ inject(dependencies: string[], callback: (scope: unknown) => void): unknown;
17
+ }
18
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
19
+ * then withdraws it, so the contribution cannot outlive the plugin. */
20
+ declare function mountModelSettingsChanges(ctx: RemoteMountContext, packageName: string): void;
14
21
  interface StreamItem {
15
22
  generation: number;
16
23
  value: unknown;
@@ -20,17 +27,17 @@ interface StreamItem {
20
27
  interface ChangeStream extends AsyncIterable<StreamItem> {
21
28
  dispose(): Promise<void>;
22
29
  }
30
+ type OpenChangeStream = (options: {
31
+ name: string;
32
+ open(signal: AbortSignal): AsyncIterable<unknown>;
33
+ ended(accepted: boolean): Error;
34
+ }) => ChangeStream;
23
35
  interface ModelSettingsSubscriberScope {
24
36
  remote: {
25
- modelSettingsChanges: {
26
- watch(signal: AbortSignal): AsyncIterable<unknown>;
27
- };
28
- $stream(options: {
29
- name: string;
30
- open(signal: AbortSignal): AsyncIterable<unknown>;
31
- ended(accepted: boolean): Error;
32
- }): ChangeStream;
33
- };
37
+ $stream: OpenChangeStream;
38
+ } & Record<string, {
39
+ watch?(signal: AbortSignal): AsyncIterable<unknown>;
40
+ } | OpenChangeStream | undefined>;
34
41
  effect(callback: () => () => void | Promise<void>): unknown;
35
42
  }
36
43
  interface ModelSettingsSubscriberContext {
@@ -41,7 +48,7 @@ interface ModelSettingsSubscriber {
41
48
  }
42
49
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
43
50
  * A factory shares one logical stream while at least one subscriber remains. */
44
- declare function createModelSettingsSubscriber(ctx: ModelSettingsSubscriberContext, namespace: string): ModelSettingsSubscriber;
51
+ declare function createModelSettingsSubscriber(ctx: ModelSettingsSubscriberContext, packageName: string, identity?: ModelSettingsChangesIdentity, accept?: (ns: string) => boolean): ModelSettingsSubscriber;
45
52
  //#endregion
46
- export { ModelSettingsChangeFrame, ModelSettingsSubscriber, ModelSettingsSubscriberContext, ModelSettingsSubscriberScope, createModelSettingsSubscriber, modelSettingsChangesRemote, parseModelSettingsChangeFrame };
53
+ export { ModelSettingsChangeFrame, ModelSettingsSubscriber, ModelSettingsSubscriberContext, ModelSettingsSubscriberScope, createModelSettingsSubscriber, modelSettingsChangesRemote, mountModelSettingsChanges, parseModelSettingsChangeFrame };
47
54
  //# sourceMappingURL=model-settings-client.d.ts.map
@@ -1,4 +1,4 @@
1
- import { MODEL_SETTINGS_CHANGES_REMOTE } from "./contracts.js";
1
+ import { modelSettingsChangesIdentity } from "./contracts.js";
2
2
  //#region src/model-settings-client.ts
3
3
  /** This stream carries invalidations only; ready is never a settings snapshot. */
4
4
  function parseModelSettingsChangeFrame(value) {
@@ -12,21 +12,44 @@ function parseModelSettingsChangeFrame(value) {
12
12
  };
13
13
  throw new Error("Invalid model settings change frame");
14
14
  }
15
- /** One contribution mounted by Model Hub; consumers only inject its service. */
16
- const modelSettingsChangesRemote = {
17
- package: MODEL_SETTINGS_CHANGES_REMOTE.package,
18
- descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
19
- ...descriptor,
20
- result: {
21
- mode: "strict",
22
- typeSymbol: "ModelSettings.ChangeFrame",
23
- create: () => ({ parse: parseModelSettingsChangeFrame })
24
- }
25
- }))
26
- };
15
+ /** One owner's contribution. Each package mounts only its own endpoint. */
16
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
17
+ return {
18
+ package: packageName,
19
+ descriptors: [{
20
+ id: `${identity.namespace}/watch`,
21
+ service: identity.service,
22
+ namespace: identity.namespace,
23
+ method: "watch",
24
+ mode: "stream",
25
+ invocation: { kind: "direct" },
26
+ parameters: [],
27
+ cancellation: { parameter: "signal" },
28
+ result: {
29
+ mode: "strict",
30
+ typeSymbol: "ModelSettings.ChangeFrame",
31
+ create: () => ({ parse: parseModelSettingsChangeFrame })
32
+ }
33
+ }]
34
+ };
35
+ }
36
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
37
+ * then withdraws it, so the contribution cannot outlive the plugin. */
38
+ function mountModelSettingsChanges(ctx, packageName) {
39
+ const contribution = modelSettingsChangesRemote(packageName);
40
+ ctx.inject(["remote"], (injected) => {
41
+ const scope = injected;
42
+ const mounting = scope.remote.$mount(contribution);
43
+ mounting.catch(() => {});
44
+ scope.effect(() => async () => {
45
+ await (await mounting.catch(() => void 0))?.();
46
+ });
47
+ });
48
+ }
27
49
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
28
50
  * A factory shares one logical stream while at least one subscriber remains. */
29
- function createModelSettingsSubscriber(ctx, namespace) {
51
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
52
+ const endpoint = identity;
30
53
  const listeners = /* @__PURE__ */ new Set();
31
54
  let injection;
32
55
  let current;
@@ -52,12 +75,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
52
75
  };
53
76
  const start = () => {
54
77
  const life = ++lifecycle;
55
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
78
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
56
79
  if (life !== lifecycle || !listeners.size) return;
57
80
  const scope = injected;
81
+ const service = scope.remote[endpoint.namespace];
82
+ if (!service || !("watch" in service) || !service.watch) {
83
+ report("Model settings sync service unavailable");
84
+ return;
85
+ }
86
+ const watch = service.watch;
58
87
  const stream = scope.remote.$stream({
59
88
  name: "Model settings sync",
60
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
89
+ open: (signal) => watch(signal),
61
90
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
62
91
  });
63
92
  const session = {
@@ -92,7 +121,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
92
121
  invalidate();
93
122
  } else {
94
123
  if (!ready) throw new Error("Model settings sync change arrived before ready");
95
- if (namespace === "*" || frame.ns === namespace) invalidate();
124
+ if (accept(frame.ns)) invalidate();
96
125
  }
97
126
  }
98
127
  } catch (error) {
@@ -138,6 +167,6 @@ function createModelSettingsSubscriber(ctx, namespace) {
138
167
  } });
139
168
  }
140
169
  //#endregion
141
- export { createModelSettingsSubscriber, modelSettingsChangesRemote, parseModelSettingsChangeFrame };
170
+ export { createModelSettingsSubscriber, modelSettingsChangesRemote, mountModelSettingsChanges, parseModelSettingsChangeFrame };
142
171
 
143
172
  //# sourceMappingURL=model-settings-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-settings-client.js","names":[],"sources":["../src/model-settings-client.ts"],"sourcesContent":["import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'\nimport { MODEL_SETTINGS_CHANGES_REMOTE } from './contracts.ts'\n\nexport type ModelSettingsChangeFrame = { kind: 'ready' } | { kind: 'changed'; ns: string; revision: number }\n\n/** This stream carries invalidations only; ready is never a settings snapshot. */\nexport function parseModelSettingsChangeFrame(value: unknown): ModelSettingsChangeFrame {\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid model settings change frame')\n const frame = value as Record<string, unknown>\n if (frame.kind === 'ready' && Object.keys(frame).length === 1) return { kind: 'ready' }\n if (frame.kind === 'changed' && Object.keys(frame).length === 3 && typeof frame.ns === 'string' && frame.ns.length > 0\n && typeof frame.revision === 'number' && Number.isSafeInteger(frame.revision) && frame.revision >= 0) {\n return { kind: 'changed', ns: frame.ns, revision: frame.revision }\n }\n throw new Error('Invalid model settings change frame')\n}\n\n/** One contribution mounted by Model Hub; consumers only inject its service. */\nexport const modelSettingsChangesRemote: TypertRemoteContribution = {\n package: MODEL_SETTINGS_CHANGES_REMOTE.package,\n descriptors: MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map(descriptor => ({\n ...descriptor,\n result: { mode: 'strict' as const, typeSymbol: 'ModelSettings.ChangeFrame', create: () => ({ parse: parseModelSettingsChangeFrame }) },\n })),\n}\n\ninterface StreamItem {\n generation: number\n value: unknown\n signal: AbortSignal\n accept(): void\n}\ninterface ChangeStream extends AsyncIterable<StreamItem> { dispose(): Promise<void> }\nexport interface ModelSettingsSubscriberScope {\n remote: {\n modelSettingsChanges: { watch(signal: AbortSignal): AsyncIterable<unknown> }\n $stream(options: {\n name: string\n open(signal: AbortSignal): AsyncIterable<unknown>\n ended(accepted: boolean): Error\n }): ChangeStream\n }\n effect(callback: () => () => void | Promise<void>): unknown\n}\nexport interface ModelSettingsSubscriberContext {\n inject(dependencies: string[], callback: (scope: unknown) => void): unknown\n}\nexport interface ModelSettingsSubscriber {\n subscribe(onChange: () => void, onError?: (message: string) => void): () => void\n}\n\n/** Native RemoteStream owns carrier reconnects; no timers or browser transport.\n * A factory shares one logical stream while at least one subscriber remains. */\nexport function createModelSettingsSubscriber(ctx: ModelSettingsSubscriberContext, namespace: string): ModelSettingsSubscriber {\n const listeners = new Set<{ change: () => void; error?: (message: string) => void }>()\n let injection: { dispose(): void | Promise<void> } | undefined\n let current: { stream: ChangeStream; active: boolean } | undefined\n let lifecycle = 0\n let failureMessage: string | undefined\n const report = (message: string) => {\n failureMessage = message\n for (const listener of [...listeners]) {\n try { listener.error?.(message) } catch { /* Isolate page callbacks. */ }\n }\n }\n const invalidate = () => {\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue\n try { listener.change() } catch (error) {\n try { listener.error?.(error instanceof Error ? error.message : String(error)) } catch { /* Isolate callbacks. */ }\n }\n }\n }\n const start = () => {\n const life = ++lifecycle\n injection = ctx.inject(['remote', 'remote.modelSettingsChanges'], injected => {\n if (life !== lifecycle || !listeners.size) return\n const scope = injected as ModelSettingsSubscriberScope\n const stream = scope.remote.$stream({\n name: 'Model settings sync',\n open: signal => scope.remote.modelSettingsChanges.watch(signal),\n ended: accepted => new Error(`Model settings sync stream ended ${accepted ? 'after' : 'before'} ready; refresh or reopen the page`),\n })\n const session = { stream, active: true }\n current = session\n scope.effect(() => () => {\n session.active = false\n if (current === session) {\n current = undefined\n if (life === lifecycle && listeners.size) report('Model settings sync service unavailable')\n }\n return stream.dispose()\n })\n void (async () => {\n let generation: number | undefined\n let ready = false\n try {\n for await (const item of stream) {\n if (!session.active || life !== lifecycle || !listeners.size) break\n if (item.signal.aborted) continue\n if (generation !== item.generation) { generation = item.generation; ready = false }\n const frame = parseModelSettingsChangeFrame(item.value)\n if (frame.kind === 'ready') {\n ready = true\n failureMessage = undefined\n // Accept the validated invalidation baseline, not fetched data.\n // Every current listener queues its own latest settings read.\n item.accept()\n invalidate()\n } else {\n if (!ready) throw new Error('Model settings sync change arrived before ready')\n if (namespace === '*' || frame.ns === namespace) invalidate()\n }\n }\n } catch (error) {\n if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error))\n } finally {\n if (current === session) current = undefined\n session.active = false\n await stream.dispose().catch(() => {})\n }\n })()\n }) as { dispose(): void | Promise<void> }\n queueMicrotask(() => {\n if (life === lifecycle && listeners.size && !current) report('Model settings sync service unavailable')\n })\n }\n return Object.freeze({ subscribe(onChange: () => void, onError?: (message: string) => void) {\n const listener = { change: onChange, error: onError }\n listeners.add(listener)\n if (!injection) start()\n else if (failureMessage) queueMicrotask(() => {\n if (listeners.has(listener) && failureMessage) {\n try { listener.error?.(failureMessage) } catch { /* Isolate callbacks. */ }\n }\n })\n return () => {\n if (!listeners.delete(listener) || listeners.size) return\n lifecycle++\n if (current) { current.active = false; void current.stream.dispose().catch(() => {}) }\n current = undefined\n failureMessage = undefined\n const previous = injection\n injection = undefined\n try { void Promise.resolve(previous?.dispose()).catch(() => {}) } catch { /* Already unloaded. */ }\n }\n } })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model-settings-client.js","names":[],"sources":["../src/model-settings-client.ts"],"sourcesContent":["import type { TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol'\nimport { modelSettingsChangesIdentity, type ModelSettingsChangesIdentity } from './contracts.ts'\n\nexport type ModelSettingsChangeFrame = { kind: 'ready' } | { kind: 'changed'; ns: string; revision: number }\n\n/** This stream carries invalidations only; ready is never a settings snapshot. */\nexport function parseModelSettingsChangeFrame(value: unknown): ModelSettingsChangeFrame {\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid model settings change frame')\n const frame = value as Record<string, unknown>\n if (frame.kind === 'ready' && Object.keys(frame).length === 1) return { kind: 'ready' }\n if (frame.kind === 'changed' && Object.keys(frame).length === 3 && typeof frame.ns === 'string' && frame.ns.length > 0\n && typeof frame.revision === 'number' && Number.isSafeInteger(frame.revision) && frame.revision >= 0) {\n return { kind: 'changed', ns: frame.ns, revision: frame.revision }\n }\n throw new Error('Invalid model settings change frame')\n}\n\n/** One owner's contribution. Each package mounts only its own endpoint. */\nexport function modelSettingsChangesRemote(packageName: string, identity: ModelSettingsChangesIdentity = modelSettingsChangesIdentity(packageName)): TypertRemoteContribution {\n return {\n package: packageName,\n descriptors: [{\n id: `${identity.namespace}/watch`,\n service: identity.service,\n namespace: identity.namespace,\n method: 'watch',\n mode: 'stream' as const,\n invocation: { kind: 'direct' as const },\n parameters: [],\n cancellation: { parameter: 'signal' as const },\n result: { mode: 'strict' as const, typeSymbol: 'ModelSettings.ChangeFrame', create: () => ({ parse: parseModelSettingsChangeFrame }) },\n }],\n }\n}\n\ninterface RemoteMountScope {\n remote: { $mount(contribution: TypertRemoteContribution): Promise<() => Promise<void>> }\n effect(callback: () => () => void | Promise<void>): unknown\n}\ninterface RemoteMountContext {\n inject(dependencies: string[], callback: (scope: unknown) => void): unknown\n}\n\n/** Mount from a synchronous apply. Disposal waits for an in-flight mount and\n * then withdraws it, so the contribution cannot outlive the plugin. */\nexport function mountModelSettingsChanges(ctx: RemoteMountContext, packageName: string): void {\n const contribution = modelSettingsChangesRemote(packageName)\n ctx.inject(['remote'], injected => {\n const scope = injected as RemoteMountScope\n const mounting = scope.remote.$mount(contribution)\n void mounting.catch(() => {})\n scope.effect(() => async () => {\n const unmount = await mounting.catch(() => undefined)\n await unmount?.()\n })\n })\n}\n\ninterface StreamItem {\n generation: number\n value: unknown\n signal: AbortSignal\n accept(): void\n}\ninterface ChangeStream extends AsyncIterable<StreamItem> { dispose(): Promise<void> }\ntype OpenChangeStream = (options: {\n name: string\n open(signal: AbortSignal): AsyncIterable<unknown>\n ended(accepted: boolean): Error\n}) => ChangeStream\nexport interface ModelSettingsSubscriberScope {\n remote: { $stream: OpenChangeStream } & Record<string, { watch?(signal: AbortSignal): AsyncIterable<unknown> } | OpenChangeStream | undefined>\n effect(callback: () => () => void | Promise<void>): unknown\n}\nexport interface ModelSettingsSubscriberContext {\n inject(dependencies: string[], callback: (scope: unknown) => void): unknown\n}\nexport interface ModelSettingsSubscriber {\n subscribe(onChange: () => void, onError?: (message: string) => void): () => void\n}\n\n/** Native RemoteStream owns carrier reconnects; no timers or browser transport.\n * A factory shares one logical stream while at least one subscriber remains. */\nexport function createModelSettingsSubscriber(\n ctx: ModelSettingsSubscriberContext,\n packageName: string,\n identity: ModelSettingsChangesIdentity = modelSettingsChangesIdentity(packageName),\n accept: (ns: string) => boolean = ns => ns === packageName,\n): ModelSettingsSubscriber {\n const endpoint = identity\n const listeners = new Set<{ change: () => void; error?: (message: string) => void }>()\n let injection: { dispose(): void | Promise<void> } | undefined\n let current: { stream: ChangeStream; active: boolean } | undefined\n let lifecycle = 0\n let failureMessage: string | undefined\n const report = (message: string) => {\n failureMessage = message\n for (const listener of [...listeners]) {\n try { listener.error?.(message) } catch { /* Isolate page callbacks. */ }\n }\n }\n const invalidate = () => {\n for (const listener of [...listeners]) {\n if (!listeners.has(listener)) continue\n try { listener.change() } catch (error) {\n try { listener.error?.(error instanceof Error ? error.message : String(error)) } catch { /* Isolate callbacks. */ }\n }\n }\n }\n const start = () => {\n const life = ++lifecycle\n injection = ctx.inject(['remote', `remote.${endpoint.namespace}`], injected => {\n if (life !== lifecycle || !listeners.size) return\n const scope = injected as ModelSettingsSubscriberScope\n const service = scope.remote[endpoint.namespace]\n if (!service || !('watch' in service) || !service.watch) { report('Model settings sync service unavailable'); return }\n const watch = service.watch\n const stream = scope.remote.$stream({\n name: 'Model settings sync',\n open: signal => watch(signal),\n ended: accepted => new Error(`Model settings sync stream ended ${accepted ? 'after' : 'before'} ready; refresh or reopen the page`),\n })\n const session = { stream, active: true }\n current = session\n scope.effect(() => () => {\n session.active = false\n if (current === session) {\n current = undefined\n if (life === lifecycle && listeners.size) report('Model settings sync service unavailable')\n }\n return stream.dispose()\n })\n void (async () => {\n let generation: number | undefined\n let ready = false\n try {\n for await (const item of stream) {\n if (!session.active || life !== lifecycle || !listeners.size) break\n if (item.signal.aborted) continue\n if (generation !== item.generation) { generation = item.generation; ready = false }\n const frame = parseModelSettingsChangeFrame(item.value)\n if (frame.kind === 'ready') {\n ready = true\n failureMessage = undefined\n // Accept the validated invalidation baseline, not fetched data.\n // Every current listener queues its own latest settings read.\n item.accept()\n invalidate()\n } else {\n if (!ready) throw new Error('Model settings sync change arrived before ready')\n if (accept(frame.ns)) invalidate()\n }\n }\n } catch (error) {\n if (session.active && life === lifecycle) report(error instanceof Error ? error.message : String(error))\n } finally {\n if (current === session) current = undefined\n session.active = false\n await stream.dispose().catch(() => {})\n }\n })()\n }) as { dispose(): void | Promise<void> }\n queueMicrotask(() => {\n if (life === lifecycle && listeners.size && !current) report('Model settings sync service unavailable')\n })\n }\n return Object.freeze({ subscribe(onChange: () => void, onError?: (message: string) => void) {\n const listener = { change: onChange, error: onError }\n listeners.add(listener)\n if (!injection) start()\n else if (failureMessage) queueMicrotask(() => {\n if (listeners.has(listener) && failureMessage) {\n try { listener.error?.(failureMessage) } catch { /* Isolate callbacks. */ }\n }\n })\n return () => {\n if (!listeners.delete(listener) || listeners.size) return\n lifecycle++\n if (current) { current.active = false; void current.stream.dispose().catch(() => {}) }\n current = undefined\n failureMessage = undefined\n const previous = injection\n injection = undefined\n try { void Promise.resolve(previous?.dispose()).catch(() => {}) } catch { /* Already unloaded. */ }\n }\n } })\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klarkxy/dsh-plugin-kit",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Shared support library for klarkxy DSH plugins.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -35,6 +35,11 @@
35
35
  "import": "./lib/model-settings.js",
36
36
  "default": "./lib/model-settings.js"
37
37
  },
38
+ "./model-settings-changes": {
39
+ "types": "./lib/model-settings-changes.d.ts",
40
+ "import": "./lib/model-settings-changes.js",
41
+ "default": "./lib/model-settings-changes.js"
42
+ },
38
43
  "./client-utils": {
39
44
  "types": "./lib/client-utils.d.ts",
40
45
  "import": "./lib/client-utils.js",
@@ -62,14 +67,14 @@
62
67
  "default": "./lib/model-settings-client.js"
63
68
  },
64
69
  "./cli-process": {
70
+ "types": "./lib/cli-process.d.ts",
65
71
  "import": "./lib/cli-process.js",
66
- "default": "./lib/cli-process.js",
67
- "types": "./lib/cli-process.d.ts"
72
+ "default": "./lib/cli-process.js"
68
73
  },
69
74
  "./cli-media": {
75
+ "types": "./lib/cli-media.d.ts",
70
76
  "import": "./lib/cli-media.js",
71
- "default": "./lib/cli-media.js",
72
- "types": "./lib/cli-media.d.ts"
77
+ "default": "./lib/cli-media.js"
73
78
  }
74
79
  },
75
80
  "files": [
@@ -84,14 +89,15 @@
84
89
  "test": "vitest run --root ../.. --config vitest.editor-plugins.config.ts --maxWorkers=2 plugins/dsh-plugin-kit/src"
85
90
  },
86
91
  "dependencies": {
87
- "@klarkxy/dsh-model-route": "0.2.4"
92
+ "@klarkxy/dsh-model-route": "0.2.5"
88
93
  },
89
94
  "peerDependencies": {
90
95
  "@deepseek-ai/dsh-llm": ">=0.2.0-rc.2",
91
96
  "react": "^18.2.0",
92
97
  "react-dom": "^18.2.0",
93
98
  "@deepseek-ai/dsh-typert-protocol": ">=0.2.0-rc.2",
94
- "@deepseek-ai/dsh-attachment": ">=0.2.0-rc.2"
99
+ "@deepseek-ai/dsh-attachment": ">=0.2.0-rc.2",
100
+ "@deepseek-ai/dsh-tools": ">=0.2.1-rc.1"
95
101
  },
96
102
  "peerDependenciesMeta": {
97
103
  "@deepseek-ai/dsh-llm": {
@@ -108,21 +114,25 @@
108
114
  },
109
115
  "@deepseek-ai/dsh-attachment": {
110
116
  "optional": true
117
+ },
118
+ "@deepseek-ai/dsh-tools": {
119
+ "optional": true
111
120
  }
112
121
  },
113
122
  "devDependencies": {
114
- "zod": "4.5.4",
115
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
116
- "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
117
- "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2",
118
- "@deepseek-ai/dsh-storage-json": "0.2.0-rc.2",
123
+ "zod": "4.4.3",
124
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
125
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.1",
126
+ "@deepseek-ai/dsh-storage-domain": "0.2.1-rc.1",
127
+ "@deepseek-ai/dsh-storage-json": "0.2.1-rc.1",
119
128
  "@types/react": "18.3.31",
120
129
  "@types/react-dom": "^18.3.0",
121
130
  "react": "18.3.1",
122
131
  "react-dom": "18.3.1",
123
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
124
- "@deepseek-ai/dsh-api-gateway": "0.2.0-rc.2",
125
- "@deepseek-ai/dsh-attachment": "0.2.0-rc.2"
132
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.1",
133
+ "@deepseek-ai/dsh-api-gateway": "0.2.1-rc.1",
134
+ "@deepseek-ai/dsh-attachment": "0.2.1-rc.1",
135
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.1"
126
136
  },
127
137
  "keywords": [
128
138
  "dsh",
@@ -139,6 +149,6 @@
139
149
  "registry": "https://registry.npmjs.org/"
140
150
  },
141
151
  "dshRelease": {
142
- "contentHash": "9705cf86aa07082e9bb893d2a59e59e56c16d23b157ab3b8bcbb4da2f82759ea"
152
+ "contentHash": "6620f8edbb623c6b1095bd4410cfa73567c76e9ba6bdd5294e962c5e580d6ea9"
143
153
  }
144
154
  }