@klarkxy/dsh-plugin-kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,8 @@ The shared support package for the feature plugins in this repository: shared re
20
20
  - `./llm-call` — `callLlmText` and `resolveFeatureModel` as separate exports.
21
21
  - `./official-ui` — the shared browser-side style contract: `officialUiCss(roots)`, the `--dsw-*` token allowlist, and the focus/elevation helpers.
22
22
 
23
+ Plugins passing their Cordis context to `resolveFeatureModel` must declare `agents`, `sessionProjections` and `agentDefaultModel` in `inject`. These services supply the live session selection and the host default when the plugin has no saved model.
24
+
23
25
  ## Plugin UI convention
24
26
 
25
27
  A feature plugin's browser half composes the host's own UI rather than
@@ -46,6 +48,13 @@ restyling controls of its own. Concretely:
46
48
  `OFFICIAL_THEME_TOKEN_NAMES` is that allowlist. Adding a token to it is a
47
49
  deliberate act: confirm the name against the host theme first.
48
50
 
51
+ ## Session ancestry
52
+
53
+ `isSubagentSession` uses durable session headers, never the author of a user message.
54
+ Explicit subagent origin or positive delegation depth wins. A seeded human fork with
55
+ zero or omitted depth and ordinary/free classification keeps its parent as history
56
+ lineage; incomplete legacy parent metadata still identifies a child session.
57
+
49
58
  ## Deprecated types
50
59
 
51
60
  `AiPolicy`, `PurposeSpec`, `ModelTarget`, `AiServices`, `AiFeatureScope`, `AuxiliaryRequest`, `AuxiliaryResult` and `UsageReceipt` are retained under `./contracts` marked `@deprecated`. They described the retired shared model-routing service; features must not use them for new calls.
@@ -20,6 +20,8 @@
20
20
  - `./llm-call` — 单独导出 `callLlmText` 与 `resolveFeatureModel`。
21
21
  - `./official-ui` — 浏览器端共享样式契约:`officialUiCss(roots)`、`--dsw-*` token 白名单,以及焦点环与层级辅助。
22
22
 
23
+ 将 Cordis 上下文传给 `resolveFeatureModel` 的插件须在 `inject` 中声明 `agents`、`sessionProjections` 和 `agentDefaultModel`。插件未保存模型时,这三个服务提供当前会话选择和宿主默认模型。
24
+
23
25
  ## 插件 UI 约定
24
26
 
25
27
  功能插件的浏览器端用宿主自己的组件拼装界面,而不是自己重画控件。具体是:
@@ -30,6 +32,10 @@
30
32
 
31
33
  `OFFICIAL_THEME_TOKEN_NAMES` 就是这份白名单。往里加 token 是一个需要确认的动作:先对照宿主主题核实名字。
32
34
 
35
+ ## 会话关系
36
+
37
+ `isSubagentSession` 只读取持久会话头,不根据 user 消息判断真人。明确的子代理来源或正派发深度优先;派发深度为零或由原生宿主省略、明确带 seed 的普通/自由真人分叉保留 parent 作为历史继承关系,仅有 parent 的旧会话及未知元数据仍按子线程处理。
38
+
33
39
  ## 已停用的类型
34
40
 
35
41
  `AiPolicy`、`PurposeSpec`、`ModelTarget`、`AiServices`、`AiFeatureScope`、`AuxiliaryRequest`、`AuxiliaryResult`、`UsageReceipt` 仍保留在 `./contracts` 并标注 `@deprecated`。它们描述已停用的共享模型路由服务,新调用不得使用。
@@ -1,4 +1,10 @@
1
1
  //#region src/client-utils.d.ts
2
+ /** Fixed pause for failing background reads; manual work is never replayed. */
3
+ declare function createPluginReadGate(now?: () => number): {
4
+ canRead: () => boolean;
5
+ reset: () => void;
6
+ run<T>(read: () => Promise<T>): Promise<T>;
7
+ };
2
8
  interface NativeSurfaceClient {
3
9
  sessions: {
4
10
  binding?(sessionId: string): {
@@ -48,5 +54,5 @@ declare function useNativeSeat(client: NativeSurfaceClient, props?: unknown): {
48
54
  /** Refresh on native log boundaries/reconnect/focus. Read-only settling is bounded to an observed job. */
49
55
  declare function useFeatureRefresh(client: NativeSurfaceClient, sessionId: string, refresh: () => void, working?: boolean, enabled?: boolean): void;
50
56
  //#endregion
51
- export { NativeSurfaceClient, selectedSessionId, useFeatureRefresh, useNativeSeat };
57
+ export { NativeSurfaceClient, createPluginReadGate, selectedSessionId, useFeatureRefresh, useNativeSeat };
52
58
  //# sourceMappingURL=client-utils.d.ts.map
@@ -1,5 +1,29 @@
1
1
  import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
2
  //#region src/client-utils.ts
3
+ /** Fixed pause for failing background reads; manual work is never replayed. */
4
+ function createPluginReadGate(now = Date.now) {
5
+ let pausedUntil = 0;
6
+ let epoch = 0;
7
+ return {
8
+ canRead: () => now() >= pausedUntil,
9
+ reset: () => {
10
+ epoch++;
11
+ pausedUntil = 0;
12
+ },
13
+ async run(read) {
14
+ const current = epoch;
15
+ try {
16
+ const value = await read();
17
+ if (current === epoch) pausedUntil = 0;
18
+ return value;
19
+ } catch (error) {
20
+ const message = error instanceof Error ? error.message : "";
21
+ if (current === epoch && (/(?:^|\s)transport failure for .+: HTTP (?:404|405|503)\b/.test(message) || error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\.?)$/i.test(message))) pausedUntil = now() + 3e4;
22
+ throw error;
23
+ }
24
+ }
25
+ };
26
+ }
3
27
  /** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */
4
28
  function selectedSessionId(client) {
5
29
  const workspace = client.uiWorkspace?.current;
@@ -89,6 +113,6 @@ function useFeatureRefresh(client, sessionId, refresh, working = false, enabled
89
113
  ]);
90
114
  }
91
115
  //#endregion
92
- export { selectedSessionId, useFeatureRefresh, useNativeSeat };
116
+ export { createPluginReadGate, selectedSessionId, useFeatureRefresh, useNativeSeat };
93
117
 
94
118
  //# sourceMappingURL=client-utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-utils.js","names":[],"sources":["../src/client-utils.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'\n\nexport interface NativeSurfaceClient {\n sessions: {\n binding?(sessionId: string): { eventSource: { subscribe(listener: () => void): () => void } } | undefined\n }\n uiWorkspace: { current?: { getSnapshot(): { sessionId: string } | undefined; subscribe(listener: () => void): () => void } }\n uiSession: { adapter: { current: { getSnapshot(): { key?: string }; subscribe(listener: () => void): () => void } } }\n locale: { getSnapshot(): { active: string }; subscribe(listener: () => void): () => void }\n connection?: { generation?: { subscribe(listener: () => void): () => void } }\n}\n\n/** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */\nexport function selectedSessionId(client: Pick<NativeSurfaceClient, 'uiWorkspace' | 'uiSession'>): string {\n const workspace = client.uiWorkspace?.current\n return workspace\n ? workspace.getSnapshot()?.sessionId ?? ''\n : client.uiSession.adapter.current.getSnapshot().key ?? ''\n}\n\n/**\n * Owner props are data the host composed, never a Cordis context: reading an\n * undeclared property off one throws instead of returning undefined, which\n * would crash the slot entry and abdicate its cell for good. Tolerate a\n * throwing read so a mistaken prop degrades to \"not supplied\".\n */\nfunction read(source: unknown, key: string): unknown {\n if (source === null || typeof source !== 'object') return undefined\n try {\n return (source as Record<string, unknown>)[key]\n } catch {\n return undefined\n }\n}\n\nfunction readString(source: unknown, key: string): string | undefined {\n const value = read(source, key)\n return typeof value === 'string' ? value : undefined\n}\n\n/** Owner props win; native settings only supplies close, so resolve its selected session. */\nexport function useNativeSeat(client: NativeSurfaceClient, props?: unknown) {\n const current = useSyncExternalStore(\n useCallback((fn: () => void) => (client.uiWorkspace?.current ?? client.uiSession.adapter.current).subscribe(fn), [client]),\n useCallback(() => selectedSessionId(client), [client]),\n () => '',\n )\n const language = useSyncExternalStore(\n useCallback((fn: () => void) => client.locale.subscribe(fn), [client]),\n useCallback(() => client.locale.getSnapshot().active, [client]),\n () => 'en',\n )\n const sessionId = readString(props, 'sessionId') ?? readString(read(props, 'owner'), 'sessionId') ?? current\n const requestedLocale = read(props, 'locale') ?? read(read(props, 'owner'), 'locale') ?? language\n return { sessionId, locale: (String(requestedLocale).startsWith('zh') ? 'zh' : 'en') as 'zh' | 'en',\n hidden: read(props, 'hidden') === true || read(read(props, 'owner'), 'hidden') === true }\n}\n\n/** Refresh on native log boundaries/reconnect/focus. Read-only settling is bounded to an observed job. */\nexport function useFeatureRefresh(\n client: NativeSurfaceClient, sessionId: string, refresh: () => void,\n working = false, enabled = true,\n): void {\n const refreshRef = useRef(refresh)\n refreshRef.current = refresh\n useEffect(() => {\n if (!enabled) return\n let timer: ReturnType<typeof setTimeout> | undefined\n const queue = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return\n clearTimeout(timer)\n timer = setTimeout(() => refreshRef.current(), 150)\n }\n const source = sessionId ? client.sessions.binding?.(sessionId)?.eventSource : undefined\n const stopEvents = source?.subscribe(queue)\n const stopConnection = client.connection?.generation?.subscribe(queue)\n const visibility = () => { if (document.visibilityState === 'visible') queue() }\n window.addEventListener('focus', queue)\n document.addEventListener('visibilitychange', visibility)\n // Details are collapsed by default; opening an existing panel must get current state.\n document.addEventListener('toggle', queue, true)\n return () => {\n clearTimeout(timer)\n stopEvents?.(); stopConnection?.()\n window.removeEventListener('focus', queue)\n document.removeEventListener('visibilitychange', visibility)\n document.removeEventListener('toggle', queue, true)\n }\n }, [client, sessionId, enabled])\n useEffect(() => {\n if (!enabled || !working) return\n let delay = 250\n const until = Date.now() + 300_000\n let timer: ReturnType<typeof setTimeout>\n const tick = () => {\n if (Date.now() >= until) return\n if (document.visibilityState !== 'hidden') refreshRef.current()\n delay = Math.min(delay * 2, 2000)\n timer = setTimeout(tick, delay)\n }\n timer = setTimeout(tick, delay)\n return () => clearTimeout(timer)\n }, [client, sessionId, working, enabled])\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client-utils.js","names":[],"sources":["../src/client-utils.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useSyncExternalStore } from 'react'\n\n/** Fixed pause for failing background reads; manual work is never replayed. */\nexport function createPluginReadGate(now: () => number = Date.now) {\n let pausedUntil = 0\n let epoch = 0\n return {\n canRead: () => now() >= pausedUntil,\n reset: () => { epoch++; pausedUntil = 0 },\n async run<T>(read: () => Promise<T>): Promise<T> {\n const current = epoch\n try {\n const value = await read()\n if (current === epoch) pausedUntil = 0\n return value\n } catch (error) {\n // The native Connection error is an Error with this exact HTTP text.\n // Authentication, application failures, and cancellation stay distinct.\n const message = error instanceof Error ? error.message : ''\n if (current === epoch && (/(?:^|\\s)transport failure for .+: HTTP (?:404|405|503)\\b/.test(message)\n || (error instanceof TypeError && /^(?:Failed to fetch|fetch failed|NetworkError when attempting to fetch resource\\.?)$/i.test(message)))) {\n pausedUntil = now() + 30_000\n }\n throw error\n }\n },\n }\n}\n\nexport interface NativeSurfaceClient {\n sessions: {\n binding?(sessionId: string): { eventSource: { subscribe(listener: () => void): () => void } } | undefined\n }\n uiWorkspace: { current?: { getSnapshot(): { sessionId: string } | undefined; subscribe(listener: () => void): () => void } }\n uiSession: { adapter: { current: { getSnapshot(): { key?: string }; subscribe(listener: () => void): () => void } } }\n locale: { getSnapshot(): { active: string }; subscribe(listener: () => void): () => void }\n connection?: { generation?: { subscribe(listener: () => void): () => void } }\n}\n\n/** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */\nexport function selectedSessionId(client: Pick<NativeSurfaceClient, 'uiWorkspace' | 'uiSession'>): string {\n const workspace = client.uiWorkspace?.current\n return workspace\n ? workspace.getSnapshot()?.sessionId ?? ''\n : client.uiSession.adapter.current.getSnapshot().key ?? ''\n}\n\n/**\n * Owner props are data the host composed, never a Cordis context: reading an\n * undeclared property off one throws instead of returning undefined, which\n * would crash the slot entry and abdicate its cell for good. Tolerate a\n * throwing read so a mistaken prop degrades to \"not supplied\".\n */\nfunction read(source: unknown, key: string): unknown {\n if (source === null || typeof source !== 'object') return undefined\n try {\n return (source as Record<string, unknown>)[key]\n } catch {\n return undefined\n }\n}\n\nfunction readString(source: unknown, key: string): string | undefined {\n const value = read(source, key)\n return typeof value === 'string' ? value : undefined\n}\n\n/** Owner props win; native settings only supplies close, so resolve its selected session. */\nexport function useNativeSeat(client: NativeSurfaceClient, props?: unknown) {\n const current = useSyncExternalStore(\n useCallback((fn: () => void) => (client.uiWorkspace?.current ?? client.uiSession.adapter.current).subscribe(fn), [client]),\n useCallback(() => selectedSessionId(client), [client]),\n () => '',\n )\n const language = useSyncExternalStore(\n useCallback((fn: () => void) => client.locale.subscribe(fn), [client]),\n useCallback(() => client.locale.getSnapshot().active, [client]),\n () => 'en',\n )\n const sessionId = readString(props, 'sessionId') ?? readString(read(props, 'owner'), 'sessionId') ?? current\n const requestedLocale = read(props, 'locale') ?? read(read(props, 'owner'), 'locale') ?? language\n return { sessionId, locale: (String(requestedLocale).startsWith('zh') ? 'zh' : 'en') as 'zh' | 'en',\n hidden: read(props, 'hidden') === true || read(read(props, 'owner'), 'hidden') === true }\n}\n\n/** Refresh on native log boundaries/reconnect/focus. Read-only settling is bounded to an observed job. */\nexport function useFeatureRefresh(\n client: NativeSurfaceClient, sessionId: string, refresh: () => void,\n working = false, enabled = true,\n): void {\n const refreshRef = useRef(refresh)\n refreshRef.current = refresh\n useEffect(() => {\n if (!enabled) return\n let timer: ReturnType<typeof setTimeout> | undefined\n const queue = () => {\n if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return\n clearTimeout(timer)\n timer = setTimeout(() => refreshRef.current(), 150)\n }\n const source = sessionId ? client.sessions.binding?.(sessionId)?.eventSource : undefined\n const stopEvents = source?.subscribe(queue)\n const stopConnection = client.connection?.generation?.subscribe(queue)\n const visibility = () => { if (document.visibilityState === 'visible') queue() }\n window.addEventListener('focus', queue)\n document.addEventListener('visibilitychange', visibility)\n // Details are collapsed by default; opening an existing panel must get current state.\n document.addEventListener('toggle', queue, true)\n return () => {\n clearTimeout(timer)\n stopEvents?.(); stopConnection?.()\n window.removeEventListener('focus', queue)\n document.removeEventListener('visibilitychange', visibility)\n document.removeEventListener('toggle', queue, true)\n }\n }, [client, sessionId, enabled])\n useEffect(() => {\n if (!enabled || !working) return\n let delay = 250\n const until = Date.now() + 300_000\n let timer: ReturnType<typeof setTimeout>\n const tick = () => {\n if (Date.now() >= until) return\n if (document.visibilityState !== 'hidden') refreshRef.current()\n delay = Math.min(delay * 2, 2000)\n timer = setTimeout(tick, delay)\n }\n timer = setTimeout(tick, delay)\n return () => clearTimeout(timer)\n }, [client, sessionId, working, enabled])\n}\n"],"mappings":";;;AAGA,SAAgB,qBAAqB,MAAoB,KAAK,KAAK;CACjE,IAAI,cAAc;CAClB,IAAI,QAAQ;CACZ,OAAO;EACL,eAAe,IAAI,KAAK;EACxB,aAAa;GAAE;GAAS,cAAc;EAAE;EACxC,MAAM,IAAO,MAAoC;GAC/C,MAAM,UAAU;GAChB,IAAI;IACF,MAAM,QAAQ,MAAM,KAAK;IACzB,IAAI,YAAY,OAAO,cAAc;IACrC,OAAO;GACT,SAAS,OAAO;IAGd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;IACzD,IAAI,YAAY,UAAU,2DAA2D,KAAK,OAAO,KAC3F,iBAAiB,aAAa,wFAAwF,KAAK,OAAO,IACtI,cAAc,IAAI,IAAI;IAExB,MAAM;GACR;EACF;CACF;AACF;;AAaA,SAAgB,kBAAkB,QAAwE;CACxG,MAAM,YAAY,OAAO,aAAa;CACtC,OAAO,YACH,UAAU,YAAY,CAAC,EAAE,aAAa,KACtC,OAAO,UAAU,QAAQ,QAAQ,YAAY,CAAC,CAAC,OAAO;AAC5D;;;;;;;AAQA,SAAS,KAAK,QAAiB,KAAsB;CACnD,IAAI,WAAW,QAAQ,OAAO,WAAW,UAAU,OAAO,KAAA;CAC1D,IAAI;EACF,OAAQ,OAAmC;CAC7C,QAAQ;EACN;CACF;AACF;AAEA,SAAS,WAAW,QAAiB,KAAiC;CACpE,MAAM,QAAQ,KAAK,QAAQ,GAAG;CAC9B,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C;;AAGA,SAAgB,cAAc,QAA6B,OAAiB;CAC1E,MAAM,UAAU,qBACd,aAAa,QAAoB,OAAO,aAAa,WAAW,OAAO,UAAU,QAAQ,QAAA,CAAS,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GACzH,kBAAkB,kBAAkB,MAAM,GAAG,CAAC,MAAM,CAAC,SAC/C,EACR;CACA,MAAM,WAAW,qBACf,aAAa,OAAmB,OAAO,OAAO,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,GACrE,kBAAkB,OAAO,OAAO,YAAY,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SACxD,IACR;CACA,MAAM,YAAY,WAAW,OAAO,WAAW,KAAK,WAAW,KAAK,OAAO,OAAO,GAAG,WAAW,KAAK;CACrG,MAAM,kBAAkB,KAAK,OAAO,QAAQ,KAAK,KAAK,KAAK,OAAO,OAAO,GAAG,QAAQ,KAAK;CACzF,OAAO;EAAE;EAAW,QAAS,OAAO,eAAe,CAAC,CAAC,WAAW,IAAI,IAAI,OAAO;EAC7E,QAAQ,KAAK,OAAO,QAAQ,MAAM,QAAQ,KAAK,KAAK,OAAO,OAAO,GAAG,QAAQ,MAAM;CAAK;AAC5F;;AAGA,SAAgB,kBACd,QAA6B,WAAmB,SAChD,UAAU,OAAO,UAAU,MACrB;CACN,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,gBAAgB;EACd,IAAI,CAAC,SAAS;EACd,IAAI;EACJ,MAAM,cAAc;GAClB,IAAI,OAAO,aAAa,eAAe,SAAS,oBAAoB,UAAU;GAC9E,aAAa,KAAK;GAClB,QAAQ,iBAAiB,WAAW,QAAQ,GAAG,GAAG;EACpD;EAEA,MAAM,cADS,YAAY,OAAO,SAAS,UAAU,SAAS,CAAC,EAAE,cAAc,KAAA,EAAA,EACpD,UAAU,KAAK;EAC1C,MAAM,iBAAiB,OAAO,YAAY,YAAY,UAAU,KAAK;EACrE,MAAM,mBAAmB;GAAE,IAAI,SAAS,oBAAoB,WAAW,MAAM;EAAE;EAC/E,OAAO,iBAAiB,SAAS,KAAK;EACtC,SAAS,iBAAiB,oBAAoB,UAAU;EAExD,SAAS,iBAAiB,UAAU,OAAO,IAAI;EAC/C,aAAa;GACX,aAAa,KAAK;GAClB,aAAa;GAAG,iBAAiB;GACjC,OAAO,oBAAoB,SAAS,KAAK;GACzC,SAAS,oBAAoB,oBAAoB,UAAU;GAC3D,SAAS,oBAAoB,UAAU,OAAO,IAAI;EACpD;CACF,GAAG;EAAC;EAAQ;EAAW;CAAO,CAAC;CAC/B,gBAAgB;EACd,IAAI,CAAC,WAAW,CAAC,SAAS;EAC1B,IAAI,QAAQ;EACZ,MAAM,QAAQ,KAAK,IAAI,IAAI;EAC3B,IAAI;EACJ,MAAM,aAAa;GACjB,IAAI,KAAK,IAAI,KAAK,OAAO;GACzB,IAAI,SAAS,oBAAoB,UAAU,WAAW,QAAQ;GAC9D,QAAQ,KAAK,IAAI,QAAQ,GAAG,GAAI;GAChC,QAAQ,WAAW,MAAM,KAAK;EAChC;EACA,QAAQ,WAAW,MAAM,KAAK;EAC9B,aAAa,aAAa,KAAK;CACjC,GAAG;EAAC;EAAQ;EAAW;EAAS;CAAO,CAAC;AAC1C"}
@@ -1,4 +1,5 @@
1
1
  import { ContextFormed } from "@deepseek-ai/dsh-llm";
2
+ import { ModelRoute } from "@klarkxy/dsh-model-route";
2
3
  //#region src/contracts.d.ts
3
4
  /** Public, browser-safe contracts. Host implementations stay in their owning packages. */
4
5
  type ProducerSourceKind = `plugin:${string}`;
@@ -16,12 +17,6 @@ declare module '@deepseek-ai/dsh-llm' {
16
17
  }
17
18
  }
18
19
  declare function producerMessageSource(plugin: string): ProducerMessageSource;
19
- /** A concrete provider model. Features store this on their own settings page. */
20
- interface ModelRoute {
21
- provider: string;
22
- model: string;
23
- reasoningEffort?: string;
24
- }
25
20
  /** @deprecated Legacy call shape. Features should call host `llm` directly. */
26
21
  type ModelRole = 'normal' | 'weak' | 'strong' | 'fantasy';
27
22
  /** @deprecated Legacy call shape. Features should call host `llm` directly. */
@@ -260,6 +255,8 @@ interface SubagentSessionLike {
260
255
  readonly parentSession?: unknown;
261
256
  readonly origin?: unknown;
262
257
  readonly delegationDepth?: unknown;
258
+ readonly isSeeded?: unknown;
259
+ readonly classification?: unknown;
263
260
  };
264
261
  }
265
262
  /**
@@ -275,5 +272,5 @@ declare function isSubagentSession(session: unknown): boolean;
275
272
  /** Project keys come only from the host-validated session directory, never client input. */
276
273
  declare function projectIdFromCwd(cwd: string | undefined): string | undefined;
277
274
  //#endregion
278
- export { AiFeatureScope, AiPolicy, AiServices, AuxiliaryRequest, AuxiliaryResult, CHAT_EVENTS_SLOT, ContextKnowledge, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRole, ModelRoute, ModelTarget, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, PurposeSpec, ResolvedRoute, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, UsageReceipt, isSubagentSession, producerMessageSource, projectIdFromCwd };
275
+ export { AiFeatureScope, AiPolicy, AiServices, AuxiliaryRequest, AuxiliaryResult, CHAT_EVENTS_SLOT, ContextKnowledge, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRole, type ModelRoute, ModelTarget, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, PurposeSpec, ResolvedRoute, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, UsageReceipt, isSubagentSession, producerMessageSource, projectIdFromCwd };
279
276
  //# sourceMappingURL=contracts.d.ts.map
package/lib/contracts.js CHANGED
@@ -18,7 +18,9 @@ const CHAT_EVENTS_SLOT = "dsh-editor.chat.events";
18
18
  function isSubagentSession(session) {
19
19
  const header = session?.header;
20
20
  if (!header || typeof header !== "object") return false;
21
- return Boolean(header.parentSession) || header.origin === "subagent" || Number(header.delegationDepth ?? 0) > 0;
21
+ if (header.origin === "subagent" || Number(header.delegationDepth ?? 0) > 0) return true;
22
+ const humanFork = header.isSeeded === true && (header.delegationDepth === void 0 || header.delegationDepth === 0) && header.origin === void 0 && (header.classification === void 0 || header.classification === "ordinary" || header.classification === "free");
23
+ return Boolean(header.parentSession) && !humanFork;
22
24
  }
23
25
  /** Project keys come only from the host-validated session directory, never client input. */
24
26
  function projectIdFromCwd(cwd) {
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.js","names":[],"sources":["../src/contracts.ts"],"sourcesContent":["import type { ContextFormed } from '@deepseek-ai/dsh-llm'\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\n/** A concrete provider model. Features store this on their own settings page. */\nexport interface ModelRoute { provider: string; model: string; reasoningEffort?: string }\n\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport type ModelRole = 'normal' | 'weak' | 'strong' | 'fantasy'\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport type ModelTarget = { kind: 'role'; role: ModelRole } | { kind: 'session' } | ({ kind: 'model' } & ModelRoute)\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AiPolicy {\n revision: number\n roles: Partial<Record<ModelRole, ModelRoute>>\n purposes: Record<string, ModelTarget>\n limits: { concurrency: number; timeoutMs: number; maxInputChars: number; maxOutputTokens: number; maxAttempts: number }\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface PurposeSpec {\n id: string\n label: string\n defaultTarget: ModelTarget\n maxOutputTokens?: number\n maxInputChars?: number\n timeoutMs?: number\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface ResolvedRoute extends ModelRoute {\n source: 'override' | 'purpose' | 'default'\n target: ModelTarget\n policyRevision: number\n inheritedRole?: ModelRole\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AuxiliaryRequest {\n purpose: string\n sessionId?: string\n input: string\n system: string\n sourceVersion: string\n contractVersion?: number\n promptVersion?: string\n schemaVersion?: string\n override?: ModelTarget\n signal?: AbortSignal\n isCurrent?: () => boolean\n priority?: 'interactive' | 'background'\n insert?: { maxChars: number }\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface UsageReceipt {\n id: string\n plugin: string\n purpose: string\n sessionId?: string\n sourceVersion: string\n contractVersion?: number\n promptVersion?: string\n schemaVersion?: string\n route?: ResolvedRoute\n status: 'success' | 'failed' | 'cancelled' | 'superseded' | 'skipped'\n attempts: number\n inputTokens?: number\n outputTokens?: number\n cost: number | null\n startedAt: number\n finishedAt: number\n error?: string\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AuxiliaryResult { text: string; receipt: UsageReceipt }\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AiFeatureScope {\n readonly plugin: string\n readonly signal: AbortSignal\n readonly active: boolean\n registerPurpose(spec: PurposeSpec): () => void\n run(request: AuxiliaryRequest): Promise<AuxiliaryResult>\n dispose(): void\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AiServices {\n activate(plugin: string): AiFeatureScope\n importPurposes(migrationId: string, defaults: Record<string, ModelTarget>, roles?: AiPolicy['roles']): Promise<AiPolicy>\n getPolicy(): AiPolicy\n updatePolicy(policy: Omit<AiPolicy, 'revision'>, expectedRevision: number): Promise<AiPolicy>\n resolve(purpose: string, sessionId?: string, override?: ModelTarget): Promise<ResolvedRoute>\n purposes(): Array<PurposeSpec & { plugin: string }>\n usage(): UsageReceipt[]\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 }\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 return Boolean(header.parentSession)\n || header.origin === 'subagent'\n || Number(header.delegationDepth ?? 0) > 0\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"],"mappings":";AAgBA,SAAgB,sBAAsB,QAAuC;CAC3E,OAAO;EAAE,MAAM,UAAU;EAAU;CAAO;AAC5C;AA2FA,MAAa,mBAAmB;;;;;;;;;;AA+GhC,SAAgB,kBAAkB,SAA2B;CAC3D,MAAM,SAAU,SAAoD;CACpE,IAAI,CAAC,UAAU,OAAO,WAAW,UAAU,OAAO;CAClD,OAAO,QAAQ,OAAO,aAAa,KAC9B,OAAO,WAAW,cAClB,OAAO,OAAO,mBAAmB,CAAC,IAAI;AAC7C;;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"}
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\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport type ModelRole = 'normal' | 'weak' | 'strong' | 'fantasy'\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport type ModelTarget = { kind: 'role'; role: ModelRole } | { kind: 'session' } | ({ kind: 'model' } & ModelRoute)\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AiPolicy {\n revision: number\n roles: Partial<Record<ModelRole, ModelRoute>>\n purposes: Record<string, ModelTarget>\n limits: { concurrency: number; timeoutMs: number; maxInputChars: number; maxOutputTokens: number; maxAttempts: number }\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface PurposeSpec {\n id: string\n label: string\n defaultTarget: ModelTarget\n maxOutputTokens?: number\n maxInputChars?: number\n timeoutMs?: number\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface ResolvedRoute extends ModelRoute {\n source: 'override' | 'purpose' | 'default'\n target: ModelTarget\n policyRevision: number\n inheritedRole?: ModelRole\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AuxiliaryRequest {\n purpose: string\n sessionId?: string\n input: string\n system: string\n sourceVersion: string\n contractVersion?: number\n promptVersion?: string\n schemaVersion?: string\n override?: ModelTarget\n signal?: AbortSignal\n isCurrent?: () => boolean\n priority?: 'interactive' | 'background'\n insert?: { maxChars: number }\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface UsageReceipt {\n id: string\n plugin: string\n purpose: string\n sessionId?: string\n sourceVersion: string\n contractVersion?: number\n promptVersion?: string\n schemaVersion?: string\n route?: ResolvedRoute\n status: 'success' | 'failed' | 'cancelled' | 'superseded' | 'skipped'\n attempts: number\n inputTokens?: number\n outputTokens?: number\n cost: number | null\n startedAt: number\n finishedAt: number\n error?: string\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AuxiliaryResult { text: string; receipt: UsageReceipt }\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AiFeatureScope {\n readonly plugin: string\n readonly signal: AbortSignal\n readonly active: boolean\n registerPurpose(spec: PurposeSpec): () => void\n run(request: AuxiliaryRequest): Promise<AuxiliaryResult>\n dispose(): void\n}\n/** @deprecated Legacy call shape. Features should call host `llm` directly. */\nexport interface AiServices {\n activate(plugin: string): AiFeatureScope\n importPurposes(migrationId: string, defaults: Record<string, ModelTarget>, roles?: AiPolicy['roles']): Promise<AiPolicy>\n getPolicy(): AiPolicy\n updatePolicy(policy: Omit<AiPolicy, 'revision'>, expectedRevision: number): Promise<AiPolicy>\n resolve(purpose: string, sessionId?: string, override?: ModelTarget): Promise<ResolvedRoute>\n purposes(): Array<PurposeSpec & { plugin: string }>\n usage(): UsageReceipt[]\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"],"mappings":";AAuBA,SAAgB,sBAAsB,QAAuC;CAC3E,OAAO;EAAE,MAAM,UAAU;EAAU;CAAO;AAC5C;AAwFA,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"}
@@ -0,0 +1,26 @@
1
+ //#region src/domain.d.ts
2
+ type PluginDomainSpec = {
3
+ version: number;
4
+ compatibleVersions?: readonly number[];
5
+ layout?: 'single' | 'per-record';
6
+ tables: Record<string, unknown>;
7
+ global?: never;
8
+ };
9
+ type PluginDomainHandle = {
10
+ table(name: string): {
11
+ entries(): IterableIterator<[unknown, unknown]>;
12
+ };
13
+ close(): Promise<void>;
14
+ };
15
+ /**
16
+ * Open table-only plugin state using the host's lossless legacy bootstrap.
17
+ * The JSON backend ignores compatibleVersions in single layout, and treats
18
+ * unsupported/malformed legacy files as absent in per-record layout. Probe
19
+ * an empty result so persistent settings cannot silently become defaults.
20
+ * A populated per-record tree is authoritative; the retained legacy source
21
+ * is an initial backup, not a current-data rollback after subsequent writes.
22
+ */
23
+ declare function openCompatibleDomain<S extends PluginDomainSpec, D extends PluginDomainHandle>(open: (spec: S) => Promise<D>, spec: S): Promise<D>;
24
+ //#endregion
25
+ export { openCompatibleDomain };
26
+ //# sourceMappingURL=domain.d.ts.map
package/lib/domain.js ADDED
@@ -0,0 +1,41 @@
1
+ //#region src/domain.ts
2
+ /**
3
+ * Open table-only plugin state using the host's lossless legacy bootstrap.
4
+ * The JSON backend ignores compatibleVersions in single layout, and treats
5
+ * unsupported/malformed legacy files as absent in per-record layout. Probe
6
+ * an empty result so persistent settings cannot silently become defaults.
7
+ * A populated per-record tree is authoritative; the retained legacy source
8
+ * is an initial backup, not a current-data rollback after subsequent writes.
9
+ */
10
+ async function openCompatibleDomain(open, spec) {
11
+ const perRecord = {
12
+ ...spec,
13
+ layout: "per-record"
14
+ };
15
+ const domain = await open(perRecord);
16
+ let populated = false;
17
+ try {
18
+ populated = Object.keys(spec.tables).some((table) => !domain.table(table).entries().next().done);
19
+ } finally {
20
+ if (!populated) await domain.close();
21
+ }
22
+ if (populated) return domain;
23
+ let mismatch;
24
+ for (const version of /* @__PURE__ */ new Set([spec.version, ...spec.compatibleVersions ?? []])) try {
25
+ await (await open({
26
+ ...spec,
27
+ layout: "single",
28
+ version,
29
+ compatibleVersions: []
30
+ })).close();
31
+ return open(perRecord);
32
+ } catch (error) {
33
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "version-mismatch") throw error;
34
+ mismatch = error;
35
+ }
36
+ throw mismatch;
37
+ }
38
+ //#endregion
39
+ export { openCompatibleDomain };
40
+
41
+ //# sourceMappingURL=domain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain.js","names":[],"sources":["../src/domain.ts"],"sourcesContent":["type PluginDomainSpec = {\n version: number\n compatibleVersions?: readonly number[]\n layout?: 'single' | 'per-record'\n tables: Record<string, unknown>\n global?: never\n}\n\ntype PluginDomainHandle = {\n table(name: string): { entries(): IterableIterator<[unknown, unknown]> }\n close(): Promise<void>\n}\n\n/**\n * Open table-only plugin state using the host's lossless legacy bootstrap.\n * The JSON backend ignores compatibleVersions in single layout, and treats\n * unsupported/malformed legacy files as absent in per-record layout. Probe\n * an empty result so persistent settings cannot silently become defaults.\n * A populated per-record tree is authoritative; the retained legacy source\n * is an initial backup, not a current-data rollback after subsequent writes.\n */\nexport async function openCompatibleDomain<S extends PluginDomainSpec, D extends PluginDomainHandle>(\n open: (spec: S) => Promise<D>, spec: S,\n): Promise<D> {\n const perRecord = { ...spec, layout: 'per-record' } as S\n const domain = await open(perRecord)\n let populated = false\n try {\n populated = Object.keys(spec.tables).some(table => !domain.table(table).entries().next().done)\n } finally {\n if (!populated) await domain.close()\n }\n if (populated) return domain\n\n let mismatch: unknown\n for (const version of new Set([spec.version, ...spec.compatibleVersions ?? []])) {\n try {\n const legacy = await open({ ...spec, layout: 'single', version, compatibleVersions: [] } as S)\n await legacy.close()\n return open(perRecord)\n } catch (error) {\n if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'version-mismatch') throw error\n mismatch = error\n }\n }\n throw mismatch\n}\n"],"mappings":";;;;;;;;;AAqBA,eAAsB,qBACpB,MAA+B,MACnB;CACZ,MAAM,YAAY;EAAE,GAAG;EAAM,QAAQ;CAAa;CAClD,MAAM,SAAS,MAAM,KAAK,SAAS;CACnC,IAAI,YAAY;CAChB,IAAI;EACF,YAAY,OAAO,KAAK,KAAK,MAAM,CAAC,CAAC,MAAK,UAAS,CAAC,OAAO,MAAM,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI;CAC/F,UAAU;EACR,IAAI,CAAC,WAAW,MAAM,OAAO,MAAM;CACrC;CACA,IAAI,WAAW,OAAO;CAEtB,IAAI;CACJ,KAAK,MAAM,2BAAW,IAAI,IAAI,CAAC,KAAK,SAAS,GAAG,KAAK,sBAAsB,CAAC,CAAC,CAAC,GAC5E,IAAI;EAEF,OAAM,MADe,KAAK;GAAE,GAAG;GAAM,QAAQ;GAAU;GAAS,oBAAoB,CAAC;EAAE,CAAM,EAAA,CAChF,MAAM;EACnB,OAAO,KAAK,SAAS;CACvB,SAAS,OAAO;EACd,IAAI,CAAC,SAAS,OAAO,UAAU,YAAY,EAAE,UAAU,UAAU,MAAM,SAAS,oBAAoB,MAAM;EAC1G,WAAW;CACb;CAEF,MAAM;AACR"}
package/lib/host-rpc.d.ts CHANGED
@@ -13,8 +13,27 @@ interface HostRpcContext {
13
13
  requestRejection?: (request: IncomingMessage) => number | undefined;
14
14
  };
15
15
  }
16
- /** Safe host RPC registration. Missing requestRejection fails closed. Result type accepts any RpcResult shape. */
17
- declare function registerHostRpc<R extends RpcResult = RpcResult>(ctx: HostRpcContext, channel: string, call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>): () => void;
16
+ type Rejection = {
17
+ status: number;
18
+ body?: string;
19
+ };
20
+ interface HostRpcTransportOptions {
21
+ maxBodyBytes?: number;
22
+ isEndpoint?: (endpoint: string) => boolean;
23
+ caseInsensitiveContentType?: boolean;
24
+ emptyBodyAsNull?: boolean;
25
+ beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined;
26
+ errorBody?: (status: number, error?: unknown) => string | undefined;
27
+ }
28
+ interface HostRpcOptions extends HostRpcTransportOptions {
29
+ requirePayload?: boolean;
30
+ }
31
+ /** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
32
+ declare function registerHostRpcTransport(ctx: HostRpcContext, channel: string, dispatch: (endpoint: string, body: unknown, signal: AbortSignal, response: ServerResponse) => void | Promise<void>, options?: HostRpcTransportOptions): () => void;
33
+ /** Write the native Connection response envelope after a plugin has handled a request. */
34
+ declare function sendHostRpcResponse(response: ServerResponse, rpcId: string, result: unknown, noStore?: boolean): void;
35
+ /** Safe host RPC registration. Missing requestRejection fails closed. */
36
+ declare function registerHostRpc<R extends RpcResult = RpcResult>(ctx: HostRpcContext, channel: string, call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>, options?: HostRpcOptions): () => void;
18
37
  //#endregion
19
- export { HostRpcContext, registerHostRpc };
38
+ export { HostRpcContext, HostRpcOptions, HostRpcTransportOptions, registerHostRpc, registerHostRpcTransport, sendHostRpcResponse };
20
39
  //# sourceMappingURL=host-rpc.d.ts.map
package/lib/host-rpc.js CHANGED
@@ -1,95 +1,114 @@
1
1
  //#region src/host-rpc.ts
2
- /** Safe host RPC registration. Missing requestRejection fails closed. Result type accepts any RpcResult shape. */
3
- function registerHostRpc(ctx, channel, call) {
2
+ /** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
3
+ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
4
+ const bodyLimit = options.maxBodyBytes ?? 65536;
5
+ if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
6
+ const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
4
7
  return ctx.webServer.register({
5
8
  kind: "prefix",
6
9
  path: channel,
7
10
  handler: async (req, res) => {
8
- if (!ctx.connection.requestRejection) {
9
- res.writeHead(503);
10
- res.end();
11
- return;
12
- }
13
- const rejection = ctx.connection.requestRejection(req);
14
- if (rejection !== void 0) {
15
- res.writeHead(rejection);
16
- res.end();
17
- return;
18
- }
19
- const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
20
- const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
21
- if (req.method !== "POST" || !/^[a-z.]+$/.test(endpoint)) {
22
- res.writeHead(404);
23
- res.end();
24
- return;
25
- }
26
- if (String(req.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") {
27
- res.writeHead(415);
28
- res.end();
29
- return;
30
- }
31
- const controller = new AbortController();
32
- res.on("close", () => {
33
- if (!res.writableEnded) controller.abort();
34
- });
11
+ const reject = (status, error, override) => {
12
+ if (res.destroyed) return;
13
+ if (status === 413) res.writeHead(status, { connection: "close" });
14
+ else res.writeHead(status);
15
+ res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
16
+ };
35
17
  try {
18
+ if (typeof ctx.connection.requestRejection !== "function") {
19
+ reject(503);
20
+ return;
21
+ }
22
+ const rejection = ctx.connection.requestRejection(req);
23
+ if (rejection !== void 0) {
24
+ reject(rejection);
25
+ return;
26
+ }
27
+ const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
28
+ const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
29
+ if (req.method !== "POST" || !isEndpoint(endpoint)) {
30
+ reject(404);
31
+ return;
32
+ }
33
+ const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
34
+ if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
35
+ reject(415);
36
+ return;
37
+ }
38
+ const guarded = options.beforeBody?.(req, endpoint);
39
+ if (guarded) {
40
+ reject(guarded.status, void 0, guarded.body);
41
+ return;
42
+ }
43
+ const controller = new AbortController();
44
+ res.on("close", () => {
45
+ if (!res.writableEnded) controller.abort();
46
+ });
36
47
  const chunks = [];
37
48
  let size = 0;
38
49
  for await (const chunk of req) {
39
50
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40
51
  size += buffer.length;
41
- if (size > 65536) {
42
- res.writeHead(413, { connection: "close" });
43
- res.end();
52
+ if (size > bodyLimit) {
53
+ reject(413);
44
54
  req.destroy();
45
55
  return;
46
56
  }
47
57
  chunks.push(buffer);
48
58
  }
49
- let request;
59
+ let body;
50
60
  try {
51
- const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
52
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error();
53
- request = parsed;
61
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
54
62
  } catch {
55
- res.writeHead(400);
56
- res.end();
57
- return;
58
- }
59
- if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string") {
60
- res.writeHead(400);
61
- res.end();
63
+ reject(400);
62
64
  return;
63
65
  }
64
- const raw = await call(endpoint, request.payload, controller.signal);
65
- const result = raw.ok ? raw : {
66
- ...raw,
67
- error: {
68
- details: {},
69
- ...raw.error
70
- }
71
- };
72
- if (!res.destroyed) {
73
- res.writeHead(200, {
74
- "content-type": "application/json",
75
- "cache-control": "no-store"
76
- });
77
- res.end(JSON.stringify({
78
- type: "server-response",
79
- rpcId: request.rpcId,
80
- result
81
- }));
82
- }
83
- } catch {
84
- if (!res.destroyed) {
85
- res.writeHead(500);
86
- res.end("rpc request failed");
87
- }
66
+ await dispatch(endpoint, body, controller.signal, res);
67
+ } catch (error) {
68
+ reject(500, error);
88
69
  }
89
70
  }
90
71
  });
91
72
  }
73
+ /** Write the native Connection response envelope after a plugin has handled a request. */
74
+ function sendHostRpcResponse(response, rpcId, result, noStore = true) {
75
+ if (response.destroyed) return;
76
+ response.writeHead(200, {
77
+ "content-type": "application/json",
78
+ ...noStore ? { "cache-control": "no-store" } : {}
79
+ });
80
+ response.end(JSON.stringify({
81
+ type: "server-response",
82
+ rpcId,
83
+ result
84
+ }));
85
+ }
86
+ /** Safe host RPC registration. Missing requestRejection fails closed. */
87
+ function registerHostRpc(ctx, channel, call, options = {}) {
88
+ return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
89
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
90
+ res.writeHead(400);
91
+ res.end();
92
+ return;
93
+ }
94
+ const request = body;
95
+ if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
96
+ res.writeHead(400);
97
+ res.end();
98
+ return;
99
+ }
100
+ const raw = await call(endpoint, request.payload, signal);
101
+ const result = raw.ok ? raw : {
102
+ ...raw,
103
+ error: {
104
+ details: {},
105
+ ...raw.error
106
+ }
107
+ };
108
+ sendHostRpcResponse(res, request.rpcId, result);
109
+ }, options);
110
+ }
92
111
  //#endregion
93
- export { registerHostRpc };
112
+ export { registerHostRpc, registerHostRpcTransport, sendHostRpcResponse };
94
113
 
95
114
  //# sourceMappingURL=host-rpc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"host-rpc.js","names":[],"sources":["../src/host-rpc.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RpcResult } from './contracts.ts'\n\nexport interface HostRpcContext {\n webServer: { register(route: { kind: 'prefix'; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise<void> }): () => void }\n connection: { requestRejection?: (request: IncomingMessage) => number | undefined }\n}\n\n/** Safe host RPC registration. Missing requestRejection fails closed. Result type accepts any RpcResult shape. */\nexport function registerHostRpc<R extends RpcResult = RpcResult>(\n ctx: HostRpcContext,\n channel: string,\n call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>,\n): () => void {\n return ctx.webServer.register({ kind: 'prefix', path: channel, handler: async (req, res) => {\n // Require the host's origin/auth policy. Missing policy must not expose settings.\n if (!ctx.connection.requestRejection) { res.writeHead(503); res.end(); return }\n const rejection = ctx.connection.requestRejection(req)\n if (rejection !== undefined) { res.writeHead(rejection); res.end(); return }\n const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname\n const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : ''\n if (req.method !== 'POST' || !/^[a-z.]+$/.test(endpoint)) { res.writeHead(404); res.end(); return }\n if (String(req.headers['content-type'] ?? '').split(';')[0]?.trim() !== 'application/json') {\n res.writeHead(415); res.end(); return\n }\n const controller = new AbortController()\n res.on('close', () => { if (!res.writableEnded) controller.abort() })\n try {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > 64 * 1024) { res.writeHead(413, { connection: 'close' }); res.end(); req.destroy(); return }\n chunks.push(buffer)\n }\n let request: Record<string, unknown>\n try {\n const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8'))\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error()\n request = parsed as Record<string, unknown>\n } catch { res.writeHead(400); res.end(); return }\n if (request.type !== 'client-request' || request.method !== endpoint || typeof request.rpcId !== 'string') {\n res.writeHead(400); res.end(); return\n }\n const raw = await call(endpoint, request.payload, controller.signal)\n const result = raw.ok ? raw : { ...raw, error: { details: {}, ...raw.error } }\n if (!res.destroyed) {\n res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })\n res.end(JSON.stringify({ type: 'server-response', rpcId: request.rpcId, result }))\n }\n } catch {\n if (!res.destroyed) { res.writeHead(500); res.end('rpc request failed') }\n }\n } })\n}\n"],"mappings":";;AASA,SAAgB,gBACd,KACA,SACA,MACY;CACZ,OAAO,IAAI,UAAU,SAAS;EAAE,MAAM;EAAU,MAAM;EAAS,SAAS,OAAO,KAAK,QAAQ;GAE1F,IAAI,CAAC,IAAI,WAAW,kBAAkB;IAAE,IAAI,UAAU,GAAG;IAAG,IAAI,IAAI;IAAG;GAAO;GAC9E,MAAM,YAAY,IAAI,WAAW,iBAAiB,GAAG;GACrD,IAAI,cAAc,KAAA,GAAW;IAAE,IAAI,UAAU,SAAS;IAAG,IAAI,IAAI;IAAG;GAAO;GAC3E,MAAM,WAAW,IAAI,IAAI,IAAI,OAAO,KAAK,qBAAqB,CAAC,CAAC;GAChE,MAAM,WAAW,SAAS,WAAW,GAAG,QAAQ,EAAE,IAAI,SAAS,MAAM,QAAQ,SAAS,CAAC,IAAI;GAC3F,IAAI,IAAI,WAAW,UAAU,CAAC,YAAY,KAAK,QAAQ,GAAG;IAAE,IAAI,UAAU,GAAG;IAAG,IAAI,IAAI;IAAG;GAAO;GAClG,IAAI,OAAO,IAAI,QAAQ,mBAAmB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,MAAM,oBAAoB;IAC1F,IAAI,UAAU,GAAG;IAAG,IAAI,IAAI;IAAG;GACjC;GACA,MAAM,aAAa,IAAI,gBAAgB;GACvC,IAAI,GAAG,eAAe;IAAE,IAAI,CAAC,IAAI,eAAe,WAAW,MAAM;GAAE,CAAC;GACpE,IAAI;IACF,MAAM,SAAmB,CAAC;IAC1B,IAAI,OAAO;IACX,WAAW,MAAM,SAAS,KAAK;KAC7B,MAAM,SAAS,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK;KACjE,QAAQ,OAAO;KACf,IAAI,OAAO,OAAW;MAAE,IAAI,UAAU,KAAK,EAAE,YAAY,QAAQ,CAAC;MAAG,IAAI,IAAI;MAAG,IAAI,QAAQ;MAAG;KAAO;KACtG,OAAO,KAAK,MAAM;IACpB;IACA,IAAI;IACJ,IAAI;KACF,MAAM,SAAkB,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC;KACzE,IAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG,MAAM,IAAI,MAAM;KACpF,UAAU;IACZ,QAAQ;KAAE,IAAI,UAAU,GAAG;KAAG,IAAI,IAAI;KAAG;IAAO;IAChD,IAAI,QAAQ,SAAS,oBAAoB,QAAQ,WAAW,YAAY,OAAO,QAAQ,UAAU,UAAU;KACzG,IAAI,UAAU,GAAG;KAAG,IAAI,IAAI;KAAG;IACjC;IACA,MAAM,MAAM,MAAM,KAAK,UAAU,QAAQ,SAAS,WAAW,MAAM;IACnE,MAAM,SAAS,IAAI,KAAK,MAAM;KAAE,GAAG;KAAK,OAAO;MAAE,SAAS,CAAC;MAAG,GAAG,IAAI;KAAM;IAAE;IAC7E,IAAI,CAAC,IAAI,WAAW;KAClB,IAAI,UAAU,KAAK;MAAE,gBAAgB;MAAoB,iBAAiB;KAAW,CAAC;KACtF,IAAI,IAAI,KAAK,UAAU;MAAE,MAAM;MAAmB,OAAO,QAAQ;MAAO;KAAO,CAAC,CAAC;IACnF;GACF,QAAQ;IACN,IAAI,CAAC,IAAI,WAAW;KAAE,IAAI,UAAU,GAAG;KAAG,IAAI,IAAI,oBAAoB;IAAE;GAC1E;EACF;CAAE,CAAC;AACL"}
1
+ {"version":3,"file":"host-rpc.js","names":[],"sources":["../src/host-rpc.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { RpcResult } from './contracts.ts'\n\nexport interface HostRpcContext {\n webServer: { register(route: { kind: 'prefix'; path: string; handler(req: IncomingMessage, res: ServerResponse): void | Promise<void> }): () => void }\n connection: { requestRejection?: (request: IncomingMessage) => number | undefined }\n}\n\ntype Rejection = { status: number; body?: string }\n\nexport interface HostRpcTransportOptions {\n maxBodyBytes?: number\n isEndpoint?: (endpoint: string) => boolean\n caseInsensitiveContentType?: boolean\n emptyBodyAsNull?: boolean\n beforeBody?: (request: IncomingMessage, endpoint: string) => Rejection | undefined\n errorBody?: (status: number, error?: unknown) => string | undefined\n}\n\nexport interface HostRpcOptions extends HostRpcTransportOptions {\n requirePayload?: boolean\n}\n\n/** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */\nexport function registerHostRpcTransport(\n ctx: HostRpcContext,\n channel: string,\n dispatch: (endpoint: string, body: unknown, signal: AbortSignal, response: ServerResponse) => void | Promise<void>,\n options: HostRpcTransportOptions = {},\n): () => void {\n const bodyLimit = options.maxBodyBytes ?? 64 * 1024\n if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error('Invalid RPC body limit')\n const isEndpoint = options.isEndpoint ?? ((endpoint: string) => /^[a-z.]+$/.test(endpoint))\n return ctx.webServer.register({ kind: 'prefix', path: channel, handler: async (req, res) => {\n const reject = (status: number, error?: unknown, override?: string) => {\n if (res.destroyed) return\n if (status === 413) res.writeHead(status, { connection: 'close' })\n else res.writeHead(status)\n res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? 'rpc request failed' : ''))\n }\n try {\n // Host admission always runs before plugin-specific checks or body reads.\n if (typeof ctx.connection.requestRejection !== 'function') { reject(503); return }\n const rejection = ctx.connection.requestRejection(req)\n if (rejection !== undefined) { reject(rejection); return }\n const pathname = new URL(req.url ?? '/', 'http://dsh.internal').pathname\n const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : ''\n if (req.method !== 'POST' || !isEndpoint(endpoint)) { reject(404); return }\n const contentType = String(req.headers['content-type'] ?? '').split(';', 1)[0]?.trim()\n if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== 'application/json') {\n reject(415); return\n }\n const guarded = options.beforeBody?.(req, endpoint)\n if (guarded) { reject(guarded.status, undefined, guarded.body); return }\n\n const controller = new AbortController()\n res.on('close', () => { if (!res.writableEnded) controller.abort() })\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += buffer.length\n if (size > bodyLimit) { reject(413); req.destroy(); return }\n chunks.push(buffer)\n }\n let body: unknown\n try { body = JSON.parse(Buffer.concat(chunks).toString('utf8') || (options.emptyBodyAsNull ? 'null' : '')) }\n catch { reject(400); return }\n await dispatch(endpoint, body, controller.signal, res)\n } catch (error) {\n reject(500, error)\n }\n } })\n}\n\n/** Write the native Connection response envelope after a plugin has handled a request. */\nexport function sendHostRpcResponse(response: ServerResponse, rpcId: string, result: unknown, noStore = true): void {\n if (response.destroyed) return\n response.writeHead(200, { 'content-type': 'application/json', ...(noStore ? { 'cache-control': 'no-store' } : {}) })\n response.end(JSON.stringify({ type: 'server-response', rpcId, result }))\n}\n\n/** Safe host RPC registration. Missing requestRejection fails closed. */\nexport function registerHostRpc<R extends RpcResult = RpcResult>(\n ctx: HostRpcContext,\n channel: string,\n call: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<R>,\n options: HostRpcOptions = {},\n): () => void {\n return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {\n if (!body || typeof body !== 'object' || Array.isArray(body)) { res.writeHead(400); res.end(); return }\n const request = body as Record<string, unknown>\n if (request.type !== 'client-request' || request.method !== endpoint || typeof request.rpcId !== 'string'\n || (options.requirePayload && !Object.hasOwn(request, 'payload'))) {\n res.writeHead(400); res.end(); return\n }\n const raw = await call(endpoint, request.payload, signal)\n const result = raw.ok ? raw : { ...raw, error: { details: {}, ...raw.error } }\n sendHostRpcResponse(res, request.rpcId, result)\n }, options)\n}\n"],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { AiFeatureScope, AiPolicy, AiServices, AuxiliaryRequest, AuxiliaryResult, CHAT_EVENTS_SLOT, ContextKnowledge, EvidenceRef, KnowledgeKind, KnowledgeScope, MemoryMutationOptions, MemoryQuery, MemoryRecord, MemoryService, ModelRole, ModelRoute, ModelTarget, NewMemoryRecord, ProcedureKnowledge, ProducerMessageSource, ProducerSourceKind, PurposeSpec, ResolvedRoute, RpcResult, SubagentSessionLike, TaskCheckpoint, TaskContract, UsageReceipt, isSubagentSession, producerMessageSource, projectIdFromCwd } from "./contracts.js";
2
+ import { openCompatibleDomain } from "./domain.js";
2
3
  import { HostRpcContext, registerHostRpc } from "./host-rpc.js";
3
4
  import { FeatureModelRoute, LlmTextCaller, callLlmText, resolveFeatureModel } from "./llm-call.js";
4
- export { type AiFeatureScope, type AiPolicy, type AiServices, type AuxiliaryRequest, type AuxiliaryResult, CHAT_EVENTS_SLOT, type ContextKnowledge, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRole, type ModelRoute, type ModelTarget, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type PurposeSpec, type ResolvedRoute, type RpcResult, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type UsageReceipt, callLlmText, type isSubagentSession, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
5
+ export { type AiFeatureScope, type AiPolicy, type AiServices, type AuxiliaryRequest, type AuxiliaryResult, CHAT_EVENTS_SLOT, type ContextKnowledge, type EvidenceRef, type FeatureModelRoute, type HostRpcContext, type KnowledgeKind, type KnowledgeScope, type LlmTextCaller, type MemoryMutationOptions, type MemoryQuery, type MemoryRecord, type MemoryService, type ModelRole, type ModelRoute, type ModelTarget, type NewMemoryRecord, type ProcedureKnowledge, type ProducerMessageSource, type ProducerSourceKind, type PurposeSpec, type ResolvedRoute, type RpcResult, type SubagentSessionLike, type TaskCheckpoint, type TaskContract, type UsageReceipt, callLlmText, type isSubagentSession, openCompatibleDomain, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CHAT_EVENTS_SLOT, producerMessageSource, projectIdFromCwd } from "./contracts.js";
2
2
  import { registerHostRpc } from "./host-rpc.js";
3
+ import { openCompatibleDomain } from "./domain.js";
3
4
  import { callLlmText, resolveFeatureModel } from "./llm-call.js";
4
- export { CHAT_EVENTS_SLOT, callLlmText, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
5
+ export { CHAT_EVENTS_SLOT, callLlmText, openCompatibleDomain, producerMessageSource, projectIdFromCwd, registerHostRpc, resolveFeatureModel };
package/lib/llm-call.d.ts CHANGED
@@ -27,7 +27,11 @@ interface LlmTextResult {
27
27
  inputTokens?: number;
28
28
  outputTokens?: number;
29
29
  }
30
- /** Plugin selection first; otherwise the live session model, then the host default. */
30
+ /**
31
+ * Plugin selection first; otherwise the live session model, then the host default.
32
+ * Cordis callers must declare agents, sessionProjections and agentDefaultModel
33
+ * in their inject list so the fallback reads use the caller's live services.
34
+ */
31
35
  declare function resolveFeatureModel(host: unknown, saved: FeatureModelRoute | undefined, sessionId?: string): FeatureModelRoute | undefined;
32
36
  /** One native model call. No retry, timeout policy, or provider queue. */
33
37
  declare function callLlmText(llm: LlmTextCaller, request: LlmTextRequest): Promise<LlmTextResult>;
package/lib/llm-call.js CHANGED
@@ -14,7 +14,11 @@ function routeOf(value) {
14
14
  model: value.model.trim()
15
15
  };
16
16
  }
17
- /** Plugin selection first; otherwise the live session model, then the host default. */
17
+ /**
18
+ * Plugin selection first; otherwise the live session model, then the host default.
19
+ * Cordis callers must declare agents, sessionProjections and agentDefaultModel
20
+ * in their inject list so the fallback reads use the caller's live services.
21
+ */
18
22
  function resolveFeatureModel(host, saved, sessionId) {
19
23
  const explicit = routeOf(saved);
20
24
  if (explicit) return explicit;
@@ -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/** Plugin selection first; otherwise the live session model, then the host default. */\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 /** 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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"}
@@ -0,0 +1 @@
1
+ export * from "@klarkxy/dsh-model-route/ui";
@@ -0,0 +1 @@
1
+ export * from "@klarkxy/dsh-model-route/ui";
@@ -1,42 +1,9 @@
1
- import { ModelRoute } from "./contracts.js";
1
+ import { ModelCatalogClient, ModelCatalogClient as ModelCatalogClient$1, ModelMenuChoice, ModelMenuChoice as ModelMenuChoice$1, ModelMenuGroup, ModelRoute, groupModelMenuChoices, knownModelFromChoices, modelMenuEffortOptions, modelMenuShortName, modelRouteKey as modelMenuChoiceKey, modelRouteOverride as modelMenuOverride, normalizeModelRoute as normalizeModelMenuRoute, parseModelMenuChoices, parseModelRouteKey as parseModelMenuChoiceKey } from "@klarkxy/dsh-model-route";
2
2
  //#region src/model-menu.d.ts
3
- /** One selectable model. `efforts` are the advertised reasoning efforts. */
4
- interface ModelMenuChoice {
5
- readonly provider: string;
6
- readonly model: string;
7
- readonly label: string;
8
- readonly efforts: ReadonlyArray<{
9
- id: string;
10
- name: string;
11
- }>;
12
- }
13
3
  /** The stored selection. Empty provider/model means "follow the default". */
14
4
  type ModelMenuRoute = ModelRoute;
15
- /** The catalog face this helper needs; stays structural so hosts may add more. */
16
- interface ModelCatalogClient {
17
- readonly remote?: {
18
- readonly session?: {
19
- readonly modelCatalog?: () => Promise<unknown>;
20
- };
21
- };
22
- }
23
- declare function modelMenuChoiceKey(provider: string, model: string): string;
24
- declare function parseModelMenuChoiceKey(value: string): {
25
- provider: string;
26
- model: string;
27
- } | undefined;
28
- /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
29
- declare function parseModelMenuChoices(value: unknown, bound?: ModelMenuRoute): ModelMenuChoice[];
30
- /** An empty saved route means no explicit model. */
31
- declare function modelMenuOverride(route: ModelMenuRoute | undefined): ModelRoute | undefined;
32
- declare function normalizeModelMenuRoute(value: unknown): ModelMenuRoute;
33
5
  /** Load the host catalog once per mount and republish whenever it answers. */
34
- declare function useModelMenuChoices(client: ModelCatalogClient, bound?: ModelMenuRoute): ModelMenuChoice[];
35
- /** Reasoning-effort options for one choice; the saved value stays selectable. */
36
- declare function modelMenuEffortOptions(choice: ModelMenuChoice | undefined, current?: string): Array<{
37
- id: string;
38
- name: string;
39
- }>;
6
+ declare function useModelMenuChoices(client: ModelCatalogClient$1, bound?: ModelMenuRoute): ModelMenuChoice$1[];
40
7
  //#endregion
41
- export { ModelCatalogClient, ModelMenuChoice, ModelMenuRoute, modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
8
+ export { type ModelCatalogClient, type ModelMenuChoice, type ModelMenuGroup, ModelMenuRoute, groupModelMenuChoices, knownModelFromChoices, modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, modelMenuShortName, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
42
9
  //# sourceMappingURL=model-menu.d.ts.map
package/lib/model-menu.js CHANGED
@@ -1,118 +1,18 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { groupModelMenuChoices, knownModelFromChoices, 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
3
  //#region src/model-menu.ts
3
4
  /**
4
5
  * Shared plugin-page model menu.
5
6
  *
6
- * A feature plugin's own settings row exposes a model select. An empty
7
- * selection means the host default chat model for that feature's own call.
8
- *
9
- * Browser-safe and React-optional: pure helpers live here so hosts and clients
10
- * share one catalog contract without bundling React into the host build.
7
+ * Everything lives in @klarkxy/dsh-model-route: the pure contract (route
8
+ * values, schema marker, catalog parsing, menu grouping) in its core entry,
9
+ * and the ModelMenu popover component in its `./ui` browser entry. This entry
10
+ * re-exports the core so existing plugin-kit consumers keep working, and adds
11
+ * the React catalog hook on top. It must stay free of any ui-primitives
12
+ * import so host-side and node-test consumers never load the component
13
+ * bundle — the popover component and its styles are re-exported from
14
+ * `./model-menu-ui` instead.
11
15
  */
12
- const KEY_SEPARATOR = "";
13
- function modelMenuChoiceKey(provider, model) {
14
- return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : "";
15
- }
16
- function parseModelMenuChoiceKey(value) {
17
- const index = value.indexOf(KEY_SEPARATOR);
18
- if (index <= 0) return void 0;
19
- const provider = value.slice(0, index);
20
- const model = value.slice(index + 1);
21
- if (!provider || !model) return void 0;
22
- return {
23
- provider,
24
- model
25
- };
26
- }
27
- function asRecord(value) {
28
- return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
29
- }
30
- function asText(value) {
31
- return typeof value === "string" && value.trim() ? value.trim() : void 0;
32
- }
33
- function parseEffort(value) {
34
- const row = asRecord(value);
35
- const id = row ? asText(row.id) : void 0;
36
- if (!id) return void 0;
37
- return {
38
- id,
39
- name: row ? asText(row.name) ?? id : id
40
- };
41
- }
42
- /** Parse the host session catalog into flat choices, keeping a saved route selectable. */
43
- function parseModelMenuChoices(value, bound) {
44
- const envelope = asRecord(value);
45
- const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : void 0) ?? envelope ?? {};
46
- const groups = Array.isArray(root.groups) ? root.groups : [];
47
- const choices = [];
48
- for (const group of groups) {
49
- const groupRow = asRecord(group);
50
- const provider = groupRow ? asText(groupRow.id) : void 0;
51
- if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue;
52
- const providerName = asText(groupRow.name) ?? provider;
53
- for (const entry of groupRow.models) {
54
- const modelRow = asRecord(entry);
55
- const model = modelRow ? asText(modelRow.id) : void 0;
56
- if (!model || !modelRow) continue;
57
- const name = asText(modelRow.name) ?? model;
58
- const reasoning = asRecord(modelRow.reasoning);
59
- const efforts = reasoning && Array.isArray(reasoning.efforts) ? reasoning.efforts.flatMap((effort) => {
60
- const parsed = parseEffort(effort);
61
- return parsed ? [parsed] : [];
62
- }) : [];
63
- choices.push({
64
- provider,
65
- model,
66
- label: `${providerName} / ${name}`,
67
- efforts
68
- });
69
- }
70
- }
71
- if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
72
- provider: bound.provider,
73
- model: bound.model,
74
- label: `${bound.provider} / ${bound.model}`,
75
- efforts: []
76
- });
77
- return choices;
78
- }
79
- /** An empty saved route means no explicit model. */
80
- function modelMenuOverride(route) {
81
- const provider = route?.provider?.trim();
82
- const model = route?.model?.trim();
83
- if (!provider || !model) return void 0;
84
- const effort = route?.reasoningEffort?.trim();
85
- return effort ? {
86
- provider,
87
- model,
88
- reasoningEffort: effort
89
- } : {
90
- provider,
91
- model
92
- };
93
- }
94
- function normalizeModelMenuRoute(value) {
95
- const row = asRecord(value);
96
- if (!row) return {
97
- provider: "",
98
- model: ""
99
- };
100
- const provider = asText(row.provider) ?? "";
101
- const model = asText(row.model) ?? "";
102
- const effort = asText(row.reasoningEffort);
103
- if (!provider || !model || provider.length > 250 || model.length > 250) return {
104
- provider: "",
105
- model: ""
106
- };
107
- return effort && effort.length <= 80 ? {
108
- provider,
109
- model,
110
- reasoningEffort: effort
111
- } : {
112
- provider,
113
- model
114
- };
115
- }
116
16
  /** Load the host catalog once per mount and republish whenever it answers. */
117
17
  function useModelMenuChoices(client, bound) {
118
18
  const [choices, setChoices] = useState([]);
@@ -121,7 +21,7 @@ function useModelMenuChoices(client, bound) {
121
21
  let live = true;
122
22
  const load = () => {
123
23
  client.remote?.session?.modelCatalog?.()?.then((value) => {
124
- if (live) setChoices(parseModelMenuChoices(value, bound));
24
+ if (live) setChoices(parseModelMenuChoices$1(value, bound));
125
25
  }).catch(() => {
126
26
  if (live) setChoices([]);
127
27
  });
@@ -137,16 +37,7 @@ function useModelMenuChoices(client, bound) {
137
37
  ]);
138
38
  return choices;
139
39
  }
140
- /** Reasoning-effort options for one choice; the saved value stays selectable. */
141
- function modelMenuEffortOptions(choice, current) {
142
- const listed = (choice?.efforts ?? []).map((item) => ({ ...item }));
143
- if (current && !listed.some((item) => item.id === current)) listed.push({
144
- id: current,
145
- name: current
146
- });
147
- return listed;
148
- }
149
40
  //#endregion
150
- export { modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
41
+ export { groupModelMenuChoices, knownModelFromChoices, modelMenuChoiceKey, modelMenuEffortOptions, modelMenuOverride, modelMenuShortName, normalizeModelMenuRoute, parseModelMenuChoiceKey, parseModelMenuChoices, useModelMenuChoices };
151
42
 
152
43
  //# sourceMappingURL=model-menu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-menu.js","names":[],"sources":["../src/model-menu.ts"],"sourcesContent":["/**\n * Shared plugin-page model menu.\n *\n * A feature plugin's own settings row exposes a model select. An empty\n * selection means the host default chat model for that feature's own call.\n *\n * Browser-safe and React-optional: pure helpers live here so hosts and clients\n * share one catalog contract without bundling React into the host build.\n */\nimport { useEffect, useState } from 'react'\nimport type { ModelRoute } from './contracts.ts'\n\n/** One selectable model. `efforts` are the advertised reasoning efforts. */\nexport interface ModelMenuChoice {\n readonly provider: string\n readonly model: string\n readonly label: string\n readonly efforts: ReadonlyArray<{ id: string; name: string }>\n}\n\n/** The stored selection. Empty provider/model means \"follow the default\". */\nexport type ModelMenuRoute = ModelRoute\n\n/** The catalog face this helper needs; stays structural so hosts may add more. */\nexport interface ModelCatalogClient {\n readonly remote?: {\n readonly session?: { readonly modelCatalog?: () => Promise<unknown> }\n }\n}\n\nconst KEY_SEPARATOR = '\\u001f'\n\nexport function modelMenuChoiceKey(provider: string, model: string): string {\n return provider && model ? `${provider}${KEY_SEPARATOR}${model}` : ''\n}\n\nexport function parseModelMenuChoiceKey(value: string): { provider: string; model: string } | undefined {\n const index = value.indexOf(KEY_SEPARATOR)\n if (index <= 0) return undefined\n const provider = value.slice(0, index)\n const model = value.slice(index + KEY_SEPARATOR.length)\n if (!provider || !model) return undefined\n return { provider, model }\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined\n}\n\nfunction asText(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value.trim() : undefined\n}\n\nfunction parseEffort(value: unknown): { id: string; name: string } | undefined {\n const row = asRecord(value)\n const id = row ? asText(row.id) : undefined\n if (!id) return undefined\n return { id, name: row ? asText(row.name) ?? id : id }\n}\n\n/** Parse the host session catalog into flat choices, keeping a saved route selectable. */\nexport function parseModelMenuChoices(value: unknown, bound?: ModelMenuRoute): ModelMenuChoice[] {\n const envelope = asRecord(value)\n // Host remote calls resolve to a result envelope ({ ok, value }); older hosts\n // and unit tests hand over the bare catalog. Accept both.\n const root = (envelope && envelope.ok === true ? asRecord(envelope.value) : undefined) ?? envelope ?? {}\n const groups = Array.isArray(root.groups) ? root.groups : []\n const choices: ModelMenuChoice[] = []\n for (const group of groups) {\n const groupRow = asRecord(group)\n const provider = groupRow ? asText(groupRow.id) : undefined\n if (!provider || !groupRow || !Array.isArray(groupRow.models)) continue\n const providerName = asText(groupRow.name) ?? provider\n for (const entry of groupRow.models) {\n const modelRow = asRecord(entry)\n const model = modelRow ? asText(modelRow.id) : undefined\n if (!model || !modelRow) continue\n const name = asText(modelRow.name) ?? model\n const reasoning = asRecord(modelRow.reasoning)\n const efforts = reasoning && Array.isArray(reasoning.efforts)\n ? reasoning.efforts.flatMap(effort => { const parsed = parseEffort(effort); return parsed ? [parsed] : [] })\n : []\n choices.push({ provider, model, label: `${providerName} / ${name}`, efforts })\n }\n }\n if (bound?.provider && bound.model && !choices.some(item => item.provider === bound.provider && item.model === bound.model)) {\n choices.push({ provider: bound.provider, model: bound.model, label: `${bound.provider} / ${bound.model}`, efforts: [] })\n }\n return choices\n}\n\n/** An empty saved route means no explicit model. */\nexport function modelMenuOverride(route: ModelMenuRoute | undefined): ModelRoute | undefined {\n const provider = route?.provider?.trim()\n const model = route?.model?.trim()\n if (!provider || !model) return undefined\n const effort = route?.reasoningEffort?.trim()\n return effort ? { provider, model, reasoningEffort: effort } : { provider, model }\n}\n\nexport function normalizeModelMenuRoute(value: unknown): ModelMenuRoute {\n const row = asRecord(value)\n if (!row) return { provider: '', model: '' }\n const provider = asText(row.provider) ?? ''\n const model = asText(row.model) ?? ''\n const effort = asText(row.reasoningEffort)\n if (!provider || !model || provider.length > 250 || model.length > 250) return { provider: '', model: '' }\n return effort && effort.length <= 80 ? { provider, model, reasoningEffort: effort } : { provider, model }\n}\n\n/** Load the host catalog once per mount and republish whenever it answers. */\nexport function useModelMenuChoices(client: ModelCatalogClient, bound?: ModelMenuRoute): ModelMenuChoice[] {\n const [choices, setChoices] = useState<ModelMenuChoice[]>([])\n useEffect(() => {\n if (typeof document === 'undefined') return\n let live = true\n const load = () => {\n void client.remote?.session?.modelCatalog?.()\n ?.then(value => { if (live) setChoices(parseModelMenuChoices(value, bound)) })\n .catch(() => { if (live) setChoices([]) })\n }\n load()\n return () => { live = false }\n }, [client, bound?.provider, bound?.model])\n return choices\n}\n\n/** Reasoning-effort options for one choice; the saved value stays selectable. */\nexport function modelMenuEffortOptions(\n choice: ModelMenuChoice | undefined,\n current?: string,\n): Array<{ id: string; name: string }> {\n const listed = (choice?.efforts ?? []).map(item => ({ ...item }))\n if (current && !listed.some(item => item.id === current)) listed.push({ id: current, name: current })\n return listed\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model-menu.js","names":["parseModelMenuChoices"],"sources":["../src/model-menu.ts"],"sourcesContent":["/**\n * Shared plugin-page model menu.\n *\n * Everything lives in @klarkxy/dsh-model-route: the pure contract (route\n * values, schema marker, catalog parsing, menu grouping) in its core entry,\n * and the ModelMenu popover component in its `./ui` browser entry. This entry\n * re-exports the core so existing plugin-kit consumers keep working, and adds\n * the React catalog hook on top. It must stay free of any ui-primitives\n * import so host-side and node-test consumers never load the component\n * bundle — the popover component and its styles are re-exported from\n * `./model-menu-ui` instead.\n */\nimport { useEffect, useState } from 'react'\nimport { parseModelMenuChoices, type ModelCatalogClient, type ModelMenuChoice, type ModelRoute } from '@klarkxy/dsh-model-route'\n\nexport type { ModelCatalogClient, ModelMenuChoice, ModelMenuGroup } from '@klarkxy/dsh-model-route'\nexport {\n groupModelMenuChoices,\n knownModelFromChoices,\n modelMenuEffortOptions,\n modelMenuShortName,\n modelRouteKey as modelMenuChoiceKey,\n modelRouteOverride as modelMenuOverride,\n normalizeModelRoute as normalizeModelMenuRoute,\n parseModelMenuChoices,\n parseModelRouteKey as parseModelMenuChoiceKey,\n} from '@klarkxy/dsh-model-route'\n\n/** The stored selection. Empty provider/model means \"follow the default\". */\nexport type ModelMenuRoute = ModelRoute\n\n/** Load the host catalog once per mount and republish whenever it answers. */\nexport function useModelMenuChoices(client: ModelCatalogClient, bound?: ModelMenuRoute): ModelMenuChoice[] {\n const [choices, setChoices] = useState<ModelMenuChoice[]>([])\n useEffect(() => {\n if (typeof document === 'undefined') return\n let live = true\n const load = () => {\n void client.remote?.session?.modelCatalog?.()\n ?.then(value => { if (live) setChoices(parseModelMenuChoices(value, bound)) })\n .catch(() => { if (live) setChoices([]) })\n }\n load()\n return () => { live = false }\n }, [client, bound?.provider, bound?.model])\n return choices\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgCA,SAAgB,oBAAoB,QAA4B,OAA2C;CACzG,MAAM,CAAC,SAAS,cAAc,SAA4B,CAAC,CAAC;CAC5D,gBAAgB;EACd,IAAI,OAAO,aAAa,aAAa;EACrC,IAAI,OAAO;EACX,MAAM,aAAa;GACjB,OAAY,QAAQ,SAAS,eAAe,CAAC,EACzC,MAAK,UAAS;IAAE,IAAI,MAAM,WAAWA,wBAAsB,OAAO,KAAK,CAAC;GAAE,CAAC,CAAC,CAC7E,YAAY;IAAE,IAAI,MAAM,WAAW,CAAC,CAAC;GAAE,CAAC;EAC7C;EACA,KAAK;EACL,aAAa;GAAE,OAAO;EAAM;CAC9B,GAAG;EAAC;EAAQ,OAAO;EAAU,OAAO;CAAK,CAAC;CAC1C,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klarkxy/dsh-plugin-kit",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared types, model-menu helpers, and host RPC registration. Does not call models.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -25,6 +25,11 @@
25
25
  "import": "./lib/host-rpc.js",
26
26
  "default": "./lib/host-rpc.js"
27
27
  },
28
+ "./domain": {
29
+ "types": "./lib/domain.d.ts",
30
+ "import": "./lib/domain.js",
31
+ "default": "./lib/domain.js"
32
+ },
28
33
  "./client-utils": {
29
34
  "types": "./lib/client-utils.d.ts",
30
35
  "import": "./lib/client-utils.js",
@@ -35,6 +40,11 @@
35
40
  "import": "./lib/model-menu.js",
36
41
  "default": "./lib/model-menu.js"
37
42
  },
43
+ "./model-menu-ui": {
44
+ "types": "./lib/model-menu-ui.d.ts",
45
+ "import": "./lib/model-menu-ui.js",
46
+ "default": "./lib/model-menu-ui.js"
47
+ },
38
48
  "./llm-call": {
39
49
  "types": "./lib/llm-call.d.ts",
40
50
  "import": "./lib/llm-call.js",
@@ -58,9 +68,13 @@
58
68
  "typecheck": "tsc --noEmit -p tsconfig.json",
59
69
  "test": "vitest run --root ../.. --config vitest.editor-plugins.config.ts --maxWorkers=2 plugins/dsh-plugin-kit/src"
60
70
  },
71
+ "dependencies": {
72
+ "@klarkxy/dsh-model-route": "0.1.0"
73
+ },
61
74
  "peerDependencies": {
62
- "@deepseek-ai/dsh-llm": ">=0.1.7-rc.2",
63
- "react": "^18.2.0"
75
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.2",
76
+ "react": "^18.2.0",
77
+ "react-dom": "^18.2.0"
64
78
  },
65
79
  "peerDependenciesMeta": {
66
80
  "@deepseek-ai/dsh-llm": {
@@ -68,12 +82,21 @@
68
82
  },
69
83
  "react": {
70
84
  "optional": true
85
+ },
86
+ "react-dom": {
87
+ "optional": true
71
88
  }
72
89
  },
73
90
  "devDependencies": {
74
- "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
91
+ "zod": "4.5.4",
92
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
93
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
94
+ "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.2",
95
+ "@deepseek-ai/dsh-storage-json": "0.2.0-rc.2",
96
+ "@types/react": "18.3.31",
97
+ "@types/react-dom": "^18.3.0",
75
98
  "react": "^18.2.0",
76
- "@types/react": "18.3.31"
99
+ "react-dom": "^18.2.0"
77
100
  },
78
101
  "keywords": [
79
102
  "dsh",
@@ -90,6 +113,6 @@
90
113
  "registry": "https://registry.npmjs.org/"
91
114
  },
92
115
  "dshRelease": {
93
- "contentHash": "ab243984edb562899e4f9e0bb6eed29c13e78592d53b360b538cbf40897b658a"
116
+ "contentHash": "3538c0ac9d7243d8847099ad3a7fa0c1f3459a7432034ea8917690889cd6e550"
94
117
  }
95
118
  }