@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 +9 -0
- package/docs/README.zh-CN.md +6 -0
- package/lib/client-utils.d.ts +7 -1
- package/lib/client-utils.js +25 -1
- package/lib/client-utils.js.map +1 -1
- package/lib/contracts.d.ts +4 -7
- package/lib/contracts.js +3 -1
- package/lib/contracts.js.map +1 -1
- package/lib/domain.d.ts +26 -0
- package/lib/domain.js +41 -0
- package/lib/domain.js.map +1 -0
- package/lib/host-rpc.d.ts +22 -3
- package/lib/host-rpc.js +87 -68
- package/lib/host-rpc.js.map +1 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +2 -1
- package/lib/llm-call.d.ts +5 -1
- package/lib/llm-call.js +5 -1
- package/lib/llm-call.js.map +1 -1
- package/lib/model-menu-ui.d.ts +1 -0
- package/lib/model-menu-ui.js +1 -0
- package/lib/model-menu.d.ts +3 -36
- package/lib/model-menu.js +11 -120
- package/lib/model-menu.js.map +1 -1
- package/package.json +29 -6
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.
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -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`。它们描述已停用的共享模型路由服务,新调用不得使用。
|
package/lib/client-utils.d.ts
CHANGED
|
@@ -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
|
package/lib/client-utils.js
CHANGED
|
@@ -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
|
package/lib/client-utils.js.map
CHANGED
|
@@ -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":"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"}
|
package/lib/contracts.d.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/contracts.js.map
CHANGED
|
@@ -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"}
|
package/lib/domain.d.ts
ADDED
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
-
/**
|
|
3
|
-
function
|
|
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
|
-
|
|
9
|
-
res.
|
|
10
|
-
res.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 >
|
|
42
|
-
|
|
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
|
|
59
|
+
let body;
|
|
50
60
|
try {
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
package/lib/host-rpc.js.map
CHANGED
|
@@ -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
|
|
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":";;AAwBA,SAAgB,yBACd,KACA,SACA,UACA,UAAmC,CAAC,GACxB;CACZ,MAAM,YAAY,QAAQ,gBAAgB;CAC1C,IAAI,CAAC,OAAO,cAAc,SAAS,KAAK,aAAa,GAAG,MAAM,IAAI,MAAM,wBAAwB;CAChG,MAAM,aAAa,QAAQ,gBAAgB,aAAqB,YAAY,KAAK,QAAQ;CACzF,OAAO,IAAI,UAAU,SAAS;EAAE,MAAM;EAAU,MAAM;EAAS,SAAS,OAAO,KAAK,QAAQ;GAC1F,MAAM,UAAU,QAAgB,OAAiB,aAAsB;IACrE,IAAI,IAAI,WAAW;IACnB,IAAI,WAAW,KAAK,IAAI,UAAU,QAAQ,EAAE,YAAY,QAAQ,CAAC;SAC5D,IAAI,UAAU,MAAM;IACzB,IAAI,IAAI,YAAY,QAAQ,YAAY,QAAQ,KAAK,MAAM,WAAW,MAAM,uBAAuB,GAAG;GACxG;GACA,IAAI;IAEF,IAAI,OAAO,IAAI,WAAW,qBAAqB,YAAY;KAAE,OAAO,GAAG;KAAG;IAAO;IACjF,MAAM,YAAY,IAAI,WAAW,iBAAiB,GAAG;IACrD,IAAI,cAAc,KAAA,GAAW;KAAE,OAAO,SAAS;KAAG;IAAO;IACzD,MAAM,WAAW,IAAI,IAAI,IAAI,OAAO,KAAK,qBAAqB,CAAC,CAAC;IAChE,MAAM,WAAW,SAAS,WAAW,GAAG,QAAQ,EAAE,IAAI,SAAS,MAAM,QAAQ,SAAS,CAAC,IAAI;IAC3F,IAAI,IAAI,WAAW,UAAU,CAAC,WAAW,QAAQ,GAAG;KAAE,OAAO,GAAG;KAAG;IAAO;IAC1E,MAAM,cAAc,OAAO,IAAI,QAAQ,mBAAmB,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK;IACrF,KAAK,QAAQ,6BAA6B,YAAY,YAAY,IAAI,iBAAiB,oBAAoB;KACzG,OAAO,GAAG;KAAG;IACf;IACA,MAAM,UAAU,QAAQ,aAAa,KAAK,QAAQ;IAClD,IAAI,SAAS;KAAE,OAAO,QAAQ,QAAQ,KAAA,GAAW,QAAQ,IAAI;KAAG;IAAO;IAEvE,MAAM,aAAa,IAAI,gBAAgB;IACvC,IAAI,GAAG,eAAe;KAAE,IAAI,CAAC,IAAI,eAAe,WAAW,MAAM;IAAE,CAAC;IACpE,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,WAAW;MAAE,OAAO,GAAG;MAAG,IAAI,QAAQ;MAAG;KAAO;KAC3D,OAAO,KAAK,MAAM;IACpB;IACA,IAAI;IACJ,IAAI;KAAE,OAAO,KAAK,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM,MAAM,QAAQ,kBAAkB,SAAS,GAAG;IAAE,QACrG;KAAE,OAAO,GAAG;KAAG;IAAO;IAC5B,MAAM,SAAS,UAAU,MAAM,WAAW,QAAQ,GAAG;GACvD,SAAS,OAAO;IACd,OAAO,KAAK,KAAK;GACnB;EACF;CAAE,CAAC;AACL;;AAGA,SAAgB,oBAAoB,UAA0B,OAAe,QAAiB,UAAU,MAAY;CAClH,IAAI,SAAS,WAAW;CACxB,SAAS,UAAU,KAAK;EAAE,gBAAgB;EAAoB,GAAI,UAAU,EAAE,iBAAiB,WAAW,IAAI,CAAC;CAAG,CAAC;CACnH,SAAS,IAAI,KAAK,UAAU;EAAE,MAAM;EAAmB;EAAO;CAAO,CAAC,CAAC;AACzE;;AAGA,SAAgB,gBACd,KACA,SACA,MACA,UAA0B,CAAC,GACf;CACZ,OAAO,yBAAyB,KAAK,SAAS,OAAO,UAAU,MAAM,QAAQ,QAAQ;EACnF,IAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,MAAM,QAAQ,IAAI,GAAG;GAAE,IAAI,UAAU,GAAG;GAAG,IAAI,IAAI;GAAG;EAAO;EACtG,MAAM,UAAU;EAChB,IAAI,QAAQ,SAAS,oBAAoB,QAAQ,WAAW,YAAY,OAAO,QAAQ,UAAU,YAC3F,QAAQ,kBAAkB,CAAC,OAAO,OAAO,SAAS,SAAS,GAAI;GACnE,IAAI,UAAU,GAAG;GAAG,IAAI,IAAI;GAAG;EACjC;EACA,MAAM,MAAM,MAAM,KAAK,UAAU,QAAQ,SAAS,MAAM;EACxD,MAAM,SAAS,IAAI,KAAK,MAAM;GAAE,GAAG;GAAK,OAAO;IAAE,SAAS,CAAC;IAAG,GAAG,IAAI;GAAM;EAAE;EAC7E,oBAAoB,KAAK,QAAQ,OAAO,MAAM;CAChD,GAAG,OAAO;AACZ"}
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/lib/llm-call.js.map
CHANGED
|
@@ -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";
|
package/lib/model-menu.d.ts
CHANGED
|
@@ -1,42 +1,9 @@
|
|
|
1
|
-
import { ModelRoute } from "
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
package/lib/model-menu.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-menu.js","names":[],"sources":["../src/model-menu.ts"],"sourcesContent":["/**\n * Shared plugin-page model menu.\n *\n *
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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": "
|
|
116
|
+
"contentHash": "3538c0ac9d7243d8847099ad3a7fa0c1f3459a7432034ea8917690889cd6e550"
|
|
94
117
|
}
|
|
95
118
|
}
|