@klarkxy/dsh-plugin-kit 0.3.3 → 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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"}
@@ -2,7 +2,7 @@ import { ModelCatalogClient, ModelCatalogClient as ModelCatalogClient$1, ModelMe
2
2
  //#region src/model-menu.d.ts
3
3
  /** The stored selection. Empty provider/model means "follow the default". */
4
4
  type ModelMenuRoute = ModelRoute;
5
- /** Load the host catalog once per mount and republish whenever it answers. */
5
+ /** @deprecated Use useModelCatalog and ModelCatalogStatus from model-route/ui for errors and retries. */
6
6
  declare function useModelMenuChoices(client: ModelCatalogClient$1, bound?: ModelMenuRoute): ModelMenuChoice$1[];
7
7
  //#endregion
8
8
  export { type ModelCatalogClient, type ModelMenuChoice, type ModelMenuGroup, ModelMenuRoute, groupModelMenuChoices, knownModelFromChoices, modelMenuChoiceKey, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuOverride, modelMenuShortName, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
package/lib/model-menu.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
- import { groupModelMenuChoices, knownModelFromChoices, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuShortName, modelRouteKey as modelMenuChoiceKey, modelRouteOverride as modelMenuOverride, normalizeModelRoute as normalizeModelMenuRoute, parseModelMenuChoices, parseModelMenuChoices as parseModelMenuChoices$1, parseModelRouteKey as parseModelMenuChoiceKey } from "@klarkxy/dsh-model-route";
2
+ import { groupModelMenuChoices, knownModelFromChoices, modelMenuEffortLabel, modelMenuEffortOptions, modelMenuShortName, modelRouteKey as modelMenuChoiceKey, modelRouteOverride as modelMenuOverride, normalizeModelRoute as normalizeModelMenuRoute, parseModelMenuChoices, parseModelMenuChoices as parseModelMenuChoices$1, parseModelRouteKey as parseModelMenuChoiceKey, readModelCatalog } from "@klarkxy/dsh-model-route";
3
3
  //#region src/model-menu.ts
4
4
  /**
5
5
  * Shared plugin-page model menu.
@@ -13,14 +13,14 @@ import { groupModelMenuChoices, knownModelFromChoices, modelMenuEffortLabel, mod
13
13
  * bundle — the popover component and its styles live in
14
14
  * `@klarkxy/dsh-model-route/ui`.
15
15
  */
16
- /** Load the host catalog once per mount and republish whenever it answers. */
16
+ /** @deprecated Use useModelCatalog and ModelCatalogStatus from model-route/ui for errors and retries. */
17
17
  function useModelMenuChoices(client, bound) {
18
18
  const [choices, setChoices] = useState([]);
19
19
  useEffect(() => {
20
20
  if (typeof document === "undefined") return;
21
21
  let live = true;
22
22
  const load = () => {
23
- client.remote?.session?.modelCatalog?.()?.then((value) => {
23
+ readModelCatalog(client).then((value) => {
24
24
  if (live) setChoices(parseModelMenuChoices$1(value, bound));
25
25
  }).catch(() => {
26
26
  if (live) setChoices([]);