@huanlin/dsh-plugin-interpreters 0.1.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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * dsh-interpreters — browser half.
3
+ *
4
+ * Registers the `interpreters` card into the shell-declared
5
+ * `settings.plugin.item` slot (the plugin-config settings page — id
6
+ * `dsh-interpreters`, order 50, after the upstream bash / agent-loop /
7
+ * web-search cards). The card's store reads/writes the `interpreters` config
8
+ * through the host gateway `/api/interpreters/get|set` RPC channel, and keeps
9
+ * fresh on pushed invalidations.
10
+ *
11
+ * Export discipline: the client half value-imports ONLY the frozen platform
12
+ * module table (CLIENT_EXTERNALS); every other `@deepseek-ai/*` import is
13
+ * type-only (erased at build) — values arrive via cordis injection
14
+ * (`ctx.get('connection')`, slot inject faces).
15
+ *
16
+ * @module @huanlin/dsh-plugin-interpreters/client
17
+ */
18
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
19
+ import { type InterpretersKey } from './locales.ts';
20
+ export type { InterpretersCardInjected, InterpretersCardProps } from './InterpretersCard.tsx';
21
+ export type { InterpretersKey } from './locales.ts';
22
+ export type { InterpretersCardState, InterpretersCardController } from './store.ts';
23
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
24
+ interface LocaleNamespaceMap {
25
+ /** The interpreters card copy. */
26
+ 'interpreters': InterpretersKey;
27
+ }
28
+ }
29
+ /** Required services (cordis fiber inject). The target slot is declared by
30
+ * ui-plugin-config's apply, whose activation order relative to this one is
31
+ * NOT constrained; registration depends on the slot through `slots.inject()`. */
32
+ export declare const inject: string[];
33
+ /**
34
+ * Register the interpreters card once the `settings.plugin.item` declaration
35
+ * is on the ledger, wire its store to the connection, and keep it fresh on
36
+ * every pushed invalidation.
37
+ * @param ctx - client root context.
38
+ */
39
+ export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * dsh-interpreters — browser half.
3
+ *
4
+ * Registers the `interpreters` card into the shell-declared
5
+ * `settings.plugin.item` slot (the plugin-config settings page — id
6
+ * `dsh-interpreters`, order 50, after the upstream bash / agent-loop /
7
+ * web-search cards). The card's store reads/writes the `interpreters` config
8
+ * through the host gateway `/api/interpreters/get|set` RPC channel, and keeps
9
+ * fresh on pushed invalidations.
10
+ *
11
+ * Export discipline: the client half value-imports ONLY the frozen platform
12
+ * module table (CLIENT_EXTERNALS); every other `@deepseek-ai/*` import is
13
+ * type-only (erased at build) — values arrive via cordis injection
14
+ * (`ctx.get('connection')`, slot inject faces).
15
+ *
16
+ * @module @huanlin/dsh-plugin-interpreters/client
17
+ */
18
+ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react';
19
+ import { InterpretersCard } from "./InterpretersCard.js";
20
+ import { InterpretersCardController, refreshIfLoaded } from "./store.js";
21
+ import { en, NS, zh } from "./locales.js";
22
+ /** Required services (cordis fiber inject). The target slot is declared by
23
+ * ui-plugin-config's apply, whose activation order relative to this one is
24
+ * NOT constrained; registration depends on the slot through `slots.inject()`. */
25
+ export const inject = ['slots', 'locale', 'connection'];
26
+ /**
27
+ * Register the interpreters card once the `settings.plugin.item` declaration
28
+ * is on the ledger, wire its store to the connection, and keep it fresh on
29
+ * every pushed invalidation.
30
+ * @param ctx - client root context.
31
+ */
32
+ export function apply(ctx) {
33
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-interpreters: dictionaries');
34
+ // The store reads/writes the interpreters config over the plugin's
35
+ // self-hosted HTTP route (`/interpreters/api/get` + `/interpreters/api/set`).
36
+ const controller = new InterpretersCardController();
37
+ const useSnapshot = bindSnapshotSelector(controller.store);
38
+ // Pushed invalidations converge the open surface without polling. The dsh
39
+ // snapshot removed the `settings/changed` host passthrough from the client
40
+ // runtime Events vocabulary, so convergence rides `connection/reset` — a
41
+ // connection reset invalidates the whole client state. A burst of resets
42
+ // coalesces into a single refetch via the microtask debounce, and
43
+ // `refreshIfLoaded` keeps an unopened card idle.
44
+ ctx.effect(() => {
45
+ let pending = false;
46
+ const refresh = () => {
47
+ if (pending)
48
+ return;
49
+ pending = true;
50
+ queueMicrotask(() => {
51
+ pending = false;
52
+ refreshIfLoaded(controller);
53
+ });
54
+ };
55
+ const disposers = [ctx.on('connection/reset', refresh)];
56
+ return () => { for (const dispose of disposers)
57
+ dispose(); };
58
+ }, 'dsh-interpreters: pushed invalidations');
59
+ // The card registers into the plugin-config page's card slot with the
60
+ // upstream card shape — generator + `yield`, `locale: NS`, and an inject
61
+ // face carrying ONLY the business surface (controller + useSnapshot). The
62
+ // typed `t` seat is synthesized by the renderer from `locale: NS`.
63
+ ctx.slots.inject('settings.plugin.item', function* () {
64
+ yield ctx.slots.register({
65
+ name: 'settings.plugin.item',
66
+ id: 'dsh-interpreters',
67
+ order: 50, // bash 0 / agent-loop 10 / web-search 20 / advisor 30 / interpreters 50
68
+ locale: NS,
69
+ inject: () => ({ controller, useSnapshot }),
70
+ }, InterpretersCard);
71
+ });
72
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * locales.ts — i18n dictionaries for the interpreters configuration card.
3
+ *
4
+ * Keys cover both the card chrome (replicated from upstream PluginCard:
5
+ * expand/collapse/unsaved/saveFailed/readOnly/save/saving/discard) and the
6
+ * plugin's own copy (title/intro + the three field labels and hints).
7
+ *
8
+ * @module dsh-interpreters/client/locales
9
+ */
10
+ export declare const NS: "interpreters";
11
+ export type InterpretersKey = 'title' | 'intro' | 'pythonPath' | 'pythonHelp' | 'nodePath' | 'nodeHelp' | 'timeoutMs' | 'timeoutHelp' | 'save' | 'saving' | 'discard' | 'unsaved' | 'saveFailed' | 'readOnly' | 'expand' | 'collapse' | 'namespaceUnavailable' | 'retry';
12
+ export declare const zh: Record<InterpretersKey, string>;
13
+ export declare const en: Record<InterpretersKey, string>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * locales.ts — i18n dictionaries for the interpreters configuration card.
3
+ *
4
+ * Keys cover both the card chrome (replicated from upstream PluginCard:
5
+ * expand/collapse/unsaved/saveFailed/readOnly/save/saving/discard) and the
6
+ * plugin's own copy (title/intro + the three field labels and hints).
7
+ *
8
+ * @module dsh-interpreters/client/locales
9
+ */
10
+ export const NS = 'interpreters';
11
+ export const zh = {
12
+ title: '解释器路径',
13
+ intro: '配置 run_python / run_node 工具使用的解释器路径。',
14
+ pythonPath: 'Python 可执行文件路径',
15
+ pythonHelp: '模型通过 run_python 工具调用该路径执行 Python 代码。留空使用系统默认 python。',
16
+ nodePath: 'Node.js 可执行文件路径',
17
+ nodeHelp: '模型通过 run_node 工具调用该路径执行 Node.js 代码。留空使用系统默认 node。',
18
+ timeoutMs: '执行超时(毫秒)',
19
+ timeoutHelp: '超过该时长进程将被强制终止。',
20
+ save: '保存',
21
+ saving: '保存中…',
22
+ discard: '放弃修改',
23
+ unsaved: '未保存',
24
+ saveFailed: '本部署没有接受这些值,已保留供你修改。',
25
+ readOnly: '本部署的设置为只读。',
26
+ expand: '展开设置',
27
+ collapse: '收起设置',
28
+ namespaceUnavailable: '解释器配置通道当前不可用。请稍后重试。',
29
+ retry: '重试',
30
+ };
31
+ export const en = {
32
+ title: 'Interpreter paths',
33
+ intro: 'Configure the interpreter executables used by the run_python / run_node tools.',
34
+ pythonPath: 'Python executable path',
35
+ pythonHelp: 'The model uses this path to execute Python code via the run_python tool. Leave empty to use the system default python.',
36
+ nodePath: 'Node.js executable path',
37
+ nodeHelp: 'The model uses this path to execute Node.js code via the run_node tool. Leave empty to use the system default node.',
38
+ timeoutMs: 'Execution timeout (ms)',
39
+ timeoutHelp: 'The process is killed after this duration.',
40
+ save: 'Save',
41
+ saving: 'Saving…',
42
+ discard: 'Discard',
43
+ unsaved: 'Unsaved',
44
+ saveFailed: 'The deployment did not accept these values; they were left for you to correct.',
45
+ readOnly: 'This deployment stores settings read-only.',
46
+ expand: 'Show settings',
47
+ collapse: 'Hide settings',
48
+ namespaceUnavailable: 'The interpreter configuration channel is unavailable. Please retry later.',
49
+ retry: 'Retry',
50
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * store.ts — the interpreters card's staged form over the
3
+ * `/interpreters/api/get|set` HTTP route.
4
+ *
5
+ * The DSH settings RPC domain only serves allowlisted namespaces to
6
+ * configuration clients, so this store reads and writes the `interpreters`
7
+ * namespace through the plugin's self-hosted HTTP route
8
+ * (`fetch('/interpreters/api/get'|'set')`) instead of the host's typertRemote
9
+ * dispatch. State publishes through a `SnapshotStore` so the card binds a
10
+ * selector hook via `bindSnapshotSelector`; the store tracks load status,
11
+ * the staged draft, and the apply lifecycle (idle/saving/saved/error).
12
+ *
13
+ * @module dsh-interpreters/client/store
14
+ */
15
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
16
+ /** The persisted shape of the `interpreters` namespace. */
17
+ export interface InterpretersSettings {
18
+ pythonPath?: string;
19
+ nodePath?: string;
20
+ timeoutMs?: number;
21
+ }
22
+ /** Apply lifecycle states (mirrors advisor-store's ApplyState shape). */
23
+ export type ApplyState = {
24
+ kind: 'idle';
25
+ } | {
26
+ kind: 'saving';
27
+ } | {
28
+ kind: 'saved';
29
+ } | {
30
+ kind: 'error';
31
+ message: string;
32
+ };
33
+ /** Card state published through the snapshot store. */
34
+ export interface InterpretersCardState {
35
+ /** 'idle' before the first load fires; 'loading' while in flight; 'ready' once seeded. */
36
+ status: 'idle' | 'loading' | 'ready';
37
+ /** False until the first successful load gates `connection/reset` refreshes. */
38
+ loaded: boolean;
39
+ /** False while the namespace is not served to this client; the card renders the unavailable notice. */
40
+ available: boolean;
41
+ /** Whether the Host document accepts writes. */
42
+ writable: boolean;
43
+ /** Staged draft (last-known host config + local edits). */
44
+ draft: InterpretersSettings;
45
+ /** Whether the form holds edits that a save would write. */
46
+ dirty: boolean;
47
+ /** Apply lifecycle. */
48
+ applyState: ApplyState;
49
+ }
50
+ /** A number field renders empty when the section carries none. */
51
+ declare function formatNumber(value: unknown): string;
52
+ /** A text field renders the empty string when absent. */
53
+ declare function formatText(value: unknown): string;
54
+ /**
55
+ * The card's staged form over the interpreters settings.
56
+ *
57
+ * The store publishes through a `SnapshotStore` because slot components read
58
+ * through a snapshot selector; both the HTTP read and the local drafts
59
+ * change underneath, and every projection is rebuilt from the two together.
60
+ */
61
+ export declare class InterpretersCardController {
62
+ readonly store: SnapshotStore<InterpretersCardState>;
63
+ /** True after the first successful load; gates `connection/reset` refreshes. */
64
+ loaded: boolean;
65
+ private generation;
66
+ private staged;
67
+ constructor();
68
+ /**
69
+ * Read the resolved config from the Host HTTP route and publish it.
70
+ * @returns settlement after the read.
71
+ */
72
+ load(): Promise<void>;
73
+ /** Stage draft text for one field. */
74
+ edit(field: keyof InterpretersSettings, text: string): void;
75
+ /** Drop every staged edit. */
76
+ discard(): void;
77
+ /** Write every staged edit, then re-seed from what the Host accepted. */
78
+ save(): void;
79
+ private doSave;
80
+ /** The staged edits as one patch (only changed fields). */
81
+ private patchOf;
82
+ }
83
+ /** Refresh the store only after its first load (background invalidation gate). */
84
+ export declare function refreshIfLoaded(controller: InterpretersCardController): void;
85
+ /** Format helpers exposed for the card component. */
86
+ export declare const formatFieldText: typeof formatText;
87
+ export declare const formatFieldNumber: typeof formatNumber;
88
+ export {};
@@ -0,0 +1,196 @@
1
+ /**
2
+ * store.ts — the interpreters card's staged form over the
3
+ * `/interpreters/api/get|set` HTTP route.
4
+ *
5
+ * The DSH settings RPC domain only serves allowlisted namespaces to
6
+ * configuration clients, so this store reads and writes the `interpreters`
7
+ * namespace through the plugin's self-hosted HTTP route
8
+ * (`fetch('/interpreters/api/get'|'set')`) instead of the host's typertRemote
9
+ * dispatch. State publishes through a `SnapshotStore` so the card binds a
10
+ * selector hook via `bindSnapshotSelector`; the store tracks load status,
11
+ * the staged draft, and the apply lifecycle (idle/saving/saved/error).
12
+ *
13
+ * @module dsh-interpreters/client/store
14
+ */
15
+ import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
16
+ /** Initial empty state. */
17
+ function initialState() {
18
+ return {
19
+ status: 'idle',
20
+ loaded: false,
21
+ available: false,
22
+ writable: false,
23
+ draft: {},
24
+ dirty: false,
25
+ applyState: { kind: 'idle' },
26
+ };
27
+ }
28
+ /** A number field renders empty when the section carries none. */
29
+ function formatNumber(value) {
30
+ return typeof value === 'number' ? String(value) : '';
31
+ }
32
+ /** A text field renders the empty string when absent. */
33
+ function formatText(value) {
34
+ return typeof value === 'string' ? value : '';
35
+ }
36
+ /**
37
+ * The card's staged form over the interpreters settings.
38
+ *
39
+ * The store publishes through a `SnapshotStore` because slot components read
40
+ * through a snapshot selector; both the HTTP read and the local drafts
41
+ * change underneath, and every projection is rebuilt from the two together.
42
+ */
43
+ export class InterpretersCardController {
44
+ store;
45
+ /** True after the first successful load; gates `connection/reset` refreshes. */
46
+ loaded = false;
47
+ generation = 0;
48
+ staged = new Map();
49
+ constructor() {
50
+ this.store = createSnapshotStore(initialState());
51
+ void this.load();
52
+ }
53
+ /**
54
+ * Read the resolved config from the Host HTTP route and publish it.
55
+ * @returns settlement after the read.
56
+ */
57
+ async load() {
58
+ const gen = ++this.generation;
59
+ this.store.update((s) => { s.status = 'loading'; });
60
+ let config;
61
+ try {
62
+ const response = await fetch('/interpreters/api/get', {
63
+ method: 'POST',
64
+ headers: { 'content-type': 'application/json' },
65
+ body: '{}',
66
+ });
67
+ if (response.ok) {
68
+ const parsed = await response.json().catch(() => null);
69
+ if (parsed?.ok === true && parsed.value !== undefined) {
70
+ config = parsed.value.config;
71
+ }
72
+ }
73
+ }
74
+ catch {
75
+ // Channel unreachable: leave the card unavailable; not a hard error.
76
+ }
77
+ if (gen !== this.generation)
78
+ return;
79
+ if (config === undefined) {
80
+ this.store.update((s) => {
81
+ s.status = 'ready';
82
+ s.available = false;
83
+ s.writable = false;
84
+ });
85
+ return;
86
+ }
87
+ this.loaded = true;
88
+ this.staged.clear();
89
+ this.store.update((s) => {
90
+ s.status = 'ready';
91
+ s.available = true;
92
+ s.writable = true;
93
+ s.draft = { ...config };
94
+ s.dirty = false;
95
+ s.applyState = { kind: 'idle' };
96
+ });
97
+ }
98
+ /** Stage draft text for one field. */
99
+ edit(field, text) {
100
+ this.staged.set(field, text);
101
+ this.store.update((s) => {
102
+ s.draft = { ...s.draft, [field]: text };
103
+ s.dirty = true;
104
+ s.applyState = { kind: 'idle' };
105
+ });
106
+ }
107
+ /** Drop every staged edit. */
108
+ discard() {
109
+ if (this.staged.size === 0) {
110
+ this.store.update((s) => { s.applyState = { kind: 'idle' }; });
111
+ return;
112
+ }
113
+ this.staged.clear();
114
+ // Re-seed draft from the last-known host config (drop local edits).
115
+ void this.load();
116
+ }
117
+ /** Write every staged edit, then re-seed from what the Host accepted. */
118
+ save() {
119
+ void this.doSave();
120
+ }
121
+ async doSave() {
122
+ const gen = ++this.generation;
123
+ const patch = this.patchOf();
124
+ if (Object.keys(patch).length === 0) {
125
+ this.staged.clear();
126
+ this.store.update((s) => { s.dirty = false; s.applyState = { kind: 'idle' }; });
127
+ return;
128
+ }
129
+ this.store.update((s) => { s.applyState = { kind: 'saving' }; });
130
+ try {
131
+ const response = await fetch('/interpreters/api/set', {
132
+ method: 'POST',
133
+ headers: { 'content-type': 'application/json' },
134
+ body: JSON.stringify({ patch }),
135
+ });
136
+ if (gen !== this.generation)
137
+ return;
138
+ if (!response.ok) {
139
+ const parsed = await response.json().catch(() => null);
140
+ const message = parsed?.error?.message ?? `HTTP ${response.status}`;
141
+ this.store.update((s) => { s.applyState = { kind: 'error', message }; });
142
+ return;
143
+ }
144
+ const parsed = await response.json().catch(() => null);
145
+ if (parsed?.ok !== true || parsed.value === undefined) {
146
+ const message = parsed?.error?.message ?? 'unknown error';
147
+ this.store.update((s) => { s.applyState = { kind: 'error', message }; });
148
+ return;
149
+ }
150
+ const next = parsed.value.config;
151
+ this.staged.clear();
152
+ this.store.update((s) => {
153
+ s.draft = { ...next };
154
+ s.dirty = false;
155
+ s.applyState = { kind: 'saved' };
156
+ });
157
+ }
158
+ catch (error) {
159
+ if (gen !== this.generation)
160
+ return;
161
+ this.store.update((s) => {
162
+ s.applyState = { kind: 'error', message: error instanceof Error ? error.message : String(error) };
163
+ });
164
+ }
165
+ }
166
+ /** The staged edits as one patch (only changed fields). */
167
+ patchOf() {
168
+ const patch = {};
169
+ for (const [field, text] of this.staged) {
170
+ const value = parseField(field, text);
171
+ if (value === undefined)
172
+ continue;
173
+ patch[field] = value;
174
+ }
175
+ return patch;
176
+ }
177
+ }
178
+ /** Parse one field's draft text into a stored value; the empty string clears. */
179
+ function parseField(field, text) {
180
+ const trimmed = text.trim();
181
+ if (trimmed === '')
182
+ return '';
183
+ if (field === 'timeoutMs') {
184
+ const parsed = Number(trimmed);
185
+ return Number.isFinite(parsed) ? parsed : undefined;
186
+ }
187
+ return trimmed;
188
+ }
189
+ /** Refresh the store only after its first load (background invalidation gate). */
190
+ export function refreshIfLoaded(controller) {
191
+ if (controller.loaded)
192
+ void controller.load();
193
+ }
194
+ /** Format helpers exposed for the card component. */
195
+ export const formatFieldText = formatText;
196
+ export const formatFieldNumber = formatNumber;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * config.ts — composition-layer schema, resolved config shape, and resolver.
3
+ *
4
+ * The composition `Config` (cordis.patch.yml) is the first-boot seed; the
5
+ * settings user layer composes on top of it at runtime. `resolveConfig`
6
+ * normalises any combination of partial source values (composition, user
7
+ * layer, or both) into a fully-populated {@link ResolvedConfig} the tool
8
+ * registration and gateway can consume.
9
+ *
10
+ * @module dsh-interpreters/config
11
+ */
12
+ import z from 'schemastery';
13
+ /** Composition + user-layer config shape (all fields optional at the boundary). */
14
+ export interface Config {
15
+ pythonPath?: string;
16
+ nodePath?: string;
17
+ timeoutMs?: number;
18
+ }
19
+ /** Fully-resolved config with fallbacks applied; what the tools and gateway serve. */
20
+ export interface ResolvedConfig {
21
+ pythonPath: string;
22
+ nodePath: string;
23
+ timeoutMs: number;
24
+ }
25
+ /** Schemastery schema for the composition entry and the `interpreters` settings namespace. */
26
+ export declare const Config: z<Config>;
27
+ /**
28
+ * Resolve config with fallbacks for missing / invalid values.
29
+ * @param config - raw config from cordis.yml or settings scope.
30
+ * @returns a fully-populated {@link ResolvedConfig}.
31
+ */
32
+ export declare function resolveConfig(config: Config): ResolvedConfig;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * config.ts — composition-layer schema, resolved config shape, and resolver.
3
+ *
4
+ * The composition `Config` (cordis.patch.yml) is the first-boot seed; the
5
+ * settings user layer composes on top of it at runtime. `resolveConfig`
6
+ * normalises any combination of partial source values (composition, user
7
+ * layer, or both) into a fully-populated {@link ResolvedConfig} the tool
8
+ * registration and gateway can consume.
9
+ *
10
+ * @module dsh-interpreters/config
11
+ */
12
+ import z from 'schemastery';
13
+ /** Schemastery schema for the composition entry and the `interpreters` settings namespace. */
14
+ export const Config = z.object({
15
+ pythonPath: z.string().default('python').description('Path to the Python interpreter executable.'),
16
+ nodePath: z.string().default('node').description('Path to the Node.js interpreter executable.'),
17
+ timeoutMs: z.number().default(30000).description('Maximum execution time in milliseconds before the process is killed.'),
18
+ });
19
+ /**
20
+ * Resolve config with fallbacks for missing / invalid values.
21
+ * @param config - raw config from cordis.yml or settings scope.
22
+ * @returns a fully-populated {@link ResolvedConfig}.
23
+ */
24
+ export function resolveConfig(config) {
25
+ const pythonPath = typeof config.pythonPath === 'string' && config.pythonPath !== '' ? config.pythonPath : 'python';
26
+ const nodePath = typeof config.nodePath === 'string' && config.nodePath !== '' ? config.nodePath : 'node';
27
+ const timeoutMs = typeof config.timeoutMs === 'number' && config.timeoutMs > 0 ? config.timeoutMs : 30000;
28
+ return { pythonPath, nodePath, timeoutMs };
29
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * gateway.ts — host-side HTTP gateway exposing the `interpreters` config to
3
+ * the browser through a self-hosted `/interpreters/api` route.
4
+ *
5
+ * The DSH typertGateway `/api` RPC dispatch was the original channel
6
+ * (TypertRemoteService + @Remote), but the host's SRC discovery
7
+ * (ctx.reflect.props enumeration) is not claiming plugin-owned service
8
+ * endpoints on the current dsh snapshot. The self-hosted HTTP route
9
+ * mirrors the better-sidebar pattern: `ctx.webServer.register` claims a
10
+ * prefix route, the handler reads/writes the settings seam in-process
11
+ * (no wire-layer allowlist gate), and the browser reaches it through
12
+ * `fetch('/interpreters/api/<method>')`.
13
+ *
14
+ * Route shape:
15
+ * POST /interpreters/api/get → { ok: true, value: { config: ResolvedConfig } }
16
+ * POST /interpreters/api/set body: { patch: Partial<Config> }
17
+ * → { ok: true, value: { config: ResolvedConfig } }
18
+ * Errors carry { ok: false, error: { code, message } }.
19
+ *
20
+ * @module dsh-interpreters/gateway
21
+ */
22
+ import type { Context } from '@deepseek-ai/cordis';
23
+ import type { Settings } from '@deepseek-ai/dsh-settings';
24
+ import { type Config as ConfigType, type ResolvedConfig } from './config.js';
25
+ import { type InterpretersSettingsBridge } from './settings.js';
26
+ /** Wire view returned by both `get` and `set`: the fully-resolved config. */
27
+ export interface InterpretersConfigView {
28
+ config: ResolvedConfig;
29
+ }
30
+ /** Patch shape the `set` endpoint accepts (every field optional, null = clear). */
31
+ export type InterpretersConfigPatch = Partial<ConfigType>;
32
+ /**
33
+ * Register the `/interpreters/api` HTTP route on the host's web server.
34
+ *
35
+ * The route reads/writes the `interpreters` settings namespace in-process
36
+ * through the bridge + `ctx.settings`. The settings service is optional:
37
+ * when absent, `get` degrades to the entry source and `set` returns a
38
+ * clear error.
39
+ * @param ctx - host context carrying `webServer`.
40
+ * @param bridge - the settings bridge the route reads through.
41
+ */
42
+ export declare function registerHttpGateway(ctx: Context, bridge: InterpretersSettingsBridge): void;
43
+ /**
44
+ * Handle the `set` method: validate the patch, write the user layer, return
45
+ * the new resolved config.
46
+ * @param body - the parsed JSON body from the request.
47
+ * @param settings - the live settings service (undefined when unavailable).
48
+ * @param bridge - the settings bridge for reading the source.
49
+ * @returns the new resolved config view.
50
+ * @throws when the settings service is unavailable.
51
+ */
52
+ export declare function handleSet(body: unknown, settings: Settings | undefined, bridge: InterpretersSettingsBridge): Promise<InterpretersConfigView>;
53
+ /**
54
+ * Extract and validate the patch from the request body.
55
+ *
56
+ * JSON wire boundary: null = "delete" (filtered), undefined never crosses
57
+ * JSON. Unknown keys are dropped (the settings service is non-strict and
58
+ * would otherwise store them). Light type guards constrain paths to
59
+ * strings and timeout to a finite number.
60
+ * @param body - the parsed JSON body.
61
+ * @returns the normalized patch (only known, well-typed keys).
62
+ */
63
+ export declare function extractPatch(body: unknown): Record<string, unknown>;