dsh-opencode 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,38 @@
1
+ import { Product } from "./normalize.js";
2
+ import { RetryPolicyConfig } from "@deepseek-ai/dsh-llm";
3
+ import z from "@deepseek-ai/schemastery";
4
+ //#region src/config.d.ts
5
+ /** Configuration for one fixed live route; the `providers` dict key IS the route. */
6
+ interface OpenCodeProviderConfig {
7
+ /** Which OpenCode product this route serves; must match the route key. */
8
+ product?: Product;
9
+ /** Credential reference (environment-variable name) resolved per request. */
10
+ apiKeyEnv?: string;
11
+ /** Display name shown by configuration surfaces. */
12
+ displayName?: string;
13
+ /** Additional deployment-owned request headers. */
14
+ headers?: Record<string, string>;
15
+ /** Provider-owned model-request retry policy. */
16
+ retryPolicy?: RetryPolicyConfig;
17
+ /** Maximum provider idle time while one stream read is outstanding. */
18
+ streamIdleTimeoutMs?: number;
19
+ }
20
+ /** Catalog refresh configuration. */
21
+ interface CatalogConfigInput {
22
+ refreshIntervalMs?: number;
23
+ listRevalidateAfterMs?: number;
24
+ timeoutMs?: number;
25
+ maxStaleMs?: number;
26
+ requireFresh?: boolean;
27
+ /** Cache file location; defaults under the DSH home. */
28
+ cachePath?: string;
29
+ }
30
+ /** Plugin configuration. */
31
+ interface Config {
32
+ providers?: Record<string, OpenCodeProviderConfig>;
33
+ catalog?: CatalogConfigInput;
34
+ }
35
+ /** Runtime schema for {@link Config}. */
36
+ declare const Config: z<Config>;
37
+ //#endregion
38
+ export { CatalogConfigInput, Config, OpenCodeProviderConfig };
package/lib/config.js ADDED
@@ -0,0 +1,142 @@
1
+ import { PRODUCT_BY_ROUTE, ROUTE_BY_PRODUCT } from "./normalize.js";
2
+ import { RetryPolicySchema, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
5
+ //#region src/config.ts
6
+ /**
7
+ * Non-secret configuration schema for the `opencode-live` plugin.
8
+ *
9
+ * The composition base (cordis.patch.yml) and the optional user-settings
10
+ * section share this schema. Configuration carries references, never secret
11
+ * values: the API key is named through `apiKeyEnv` and resolved per request
12
+ * through the DSH credential seam.
13
+ *
14
+ * The plugin owns exactly two fixed routes. A provider entry keyed anything
15
+ * else, or keyed with the wrong product, is refused where it is written.
16
+ *
17
+ * @module opencode-live/config
18
+ */
19
+ /** Upper bound shared with DSH's timer facilities. */
20
+ const MAX_TIMER_DELAY_MS = 2147483647;
21
+ /** The credential reference both OpenCode products document. */
22
+ const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
23
+ const DEFAULT_REFRESH_INTERVAL_MS = 9e5;
24
+ const DEFAULT_LIST_REVALIDATE_AFTER_MS = 6e4;
25
+ const DEFAULT_TIMEOUT_MS = 15e3;
26
+ const DEFAULT_MAX_STALE_MS = 6048e5;
27
+ const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 3e5;
28
+ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 20 * 1024 * 1024;
29
+ const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
30
+ const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
31
+ const productSchema = z.union([z.const("zen").required(), z.const("go").required()]);
32
+ const providerSchema = z.object({
33
+ product: productSchema,
34
+ apiKeyEnv: z.string().role("credential-ref"),
35
+ displayName: z.string(),
36
+ headers: z.dict(z.string()),
37
+ retryPolicy: RetryPolicySchema,
38
+ streamIdleTimeoutMs: z.number().min(Number.MIN_VALUE).max(MAX_TIMER_DELAY_MS)
39
+ });
40
+ const catalogSchema = z.object({
41
+ refreshIntervalMs: z.number().step(1).min(1),
42
+ listRevalidateAfterMs: z.number().step(1).min(1),
43
+ timeoutMs: z.number().step(1).min(1),
44
+ maxStaleMs: z.number().step(1).min(1),
45
+ requireFresh: z.boolean(),
46
+ cachePath: z.string()
47
+ });
48
+ /** Runtime schema for {@link Config}. */
49
+ const Config = z.object({
50
+ providers: z.dict(providerSchema),
51
+ catalog: catalogSchema.default({})
52
+ });
53
+ /** Whether one number is a positive finite integer within timer bounds. */
54
+ function isBoundedPositiveInteger(value, max) {
55
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= max;
56
+ }
57
+ /** Validate one bounded catalog interval field. */
58
+ function requireInterval(value, fallback, label, max = MAX_TIMER_DELAY_MS) {
59
+ const resolved = value ?? fallback;
60
+ if (!isBoundedPositiveInteger(resolved, max)) throw new Error(`opencode-live: catalog.${label} must be a positive integer no greater than ${max}`);
61
+ return resolved;
62
+ }
63
+ /**
64
+ * Validate configuration and resolve every default. This is the one explicit
65
+ * resolve step; an invalid route key, a product/route mismatch, or an out-of
66
+ * -bound interval fails loudly here instead of disabling a route silently.
67
+ * @param config - the raw configuration value.
68
+ * @returns detached validated configuration.
69
+ */
70
+ function resolveConfig(config) {
71
+ const providers = /* @__PURE__ */ new Map();
72
+ const entries = Object.entries(config.providers ?? {});
73
+ if (entries.length === 0) for (const route of [ROUTE_BY_PRODUCT.zen, ROUTE_BY_PRODUCT.go]) providers.set(route, resolveProvider(route, {
74
+ product: PRODUCT_BY_ROUTE[route],
75
+ apiKeyEnv: DEFAULT_API_KEY_ENV
76
+ }));
77
+ for (const [route, source] of entries) {
78
+ if (route !== ROUTE_BY_PRODUCT.zen && route !== ROUTE_BY_PRODUCT.go) throw new Error(`opencode-live: provider "${route}" is not a route this plugin owns; the fixed routes are ${ROUTE_BY_PRODUCT.zen} and ${ROUTE_BY_PRODUCT.go}`);
79
+ const resolved = resolveProvider(route, source);
80
+ if (providers.get(resolved.route) !== void 0) throw new Error(`opencode-live: provider "${route}" is declared twice`);
81
+ providers.set(resolved.route, resolved);
82
+ }
83
+ return {
84
+ providers,
85
+ catalog: {
86
+ refreshIntervalMs: requireInterval(config.catalog?.refreshIntervalMs, DEFAULT_REFRESH_INTERVAL_MS, "refreshIntervalMs"),
87
+ listRevalidateAfterMs: requireInterval(config.catalog?.listRevalidateAfterMs, DEFAULT_LIST_REVALIDATE_AFTER_MS, "listRevalidateAfterMs"),
88
+ timeoutMs: requireInterval(config.catalog?.timeoutMs, DEFAULT_TIMEOUT_MS, "timeoutMs"),
89
+ maxStaleMs: requireInterval(config.catalog?.maxStaleMs, DEFAULT_MAX_STALE_MS, "maxStaleMs"),
90
+ requireFresh: config.catalog?.requireFresh ?? false,
91
+ ...config.catalog?.cachePath !== void 0 ? { cachePath: requireCachePath(config.catalog.cachePath) } : {}
92
+ }
93
+ };
94
+ }
95
+ /** Validate one provider entry against its fixed route. */
96
+ function resolveProvider(route, source) {
97
+ const product = source.product ?? PRODUCT_BY_ROUTE[route];
98
+ if (PRODUCT_BY_ROUTE[route] !== product) throw new Error(`opencode-live: provider "${route}" declares product "${source.product}", but this route serves "${PRODUCT_BY_ROUTE[route]}"`);
99
+ const apiKeyEnv = credentialRef(source.apiKeyEnv ?? "OPENCODE_API_KEY");
100
+ const streamIdleTimeoutMs = source.streamIdleTimeoutMs ?? 3e5;
101
+ if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0 || streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) throw new Error(`opencode-live: provider "${route}" streamIdleTimeoutMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
102
+ if (source.displayName !== void 0 && source.displayName.length === 0) throw new Error(`opencode-live: provider "${route}" has an empty displayName`);
103
+ assertValidHeaders(route, source.headers);
104
+ return {
105
+ route,
106
+ product,
107
+ apiKeyEnv,
108
+ displayName: source.displayName ?? defaultDisplayName(route),
109
+ ...source.headers !== void 0 ? { headers: { ...source.headers } } : {},
110
+ retryPolicy: resolveRetryPolicy(source.retryPolicy, `opencode-live: provider "${route}" retryPolicy`),
111
+ streamIdleTimeoutMs
112
+ };
113
+ }
114
+ /** The default display name for one fixed route. */
115
+ function defaultDisplayName(route) {
116
+ return route === ROUTE_BY_PRODUCT.zen ? "OpenCode Zen (Live)" : "OpenCode Go (Live)";
117
+ }
118
+ /** Reject a header Fetch cannot carry, naming the route and field. */
119
+ function assertValidHeaders(route, headers) {
120
+ for (const [name, value] of Object.entries(headers ?? {})) try {
121
+ new Headers([[name, value]]);
122
+ } catch {
123
+ throw new Error(`opencode-live: provider "${route}" header "${name}" is not valid for Fetch; use a valid HTTP field name and a single-line value representable as bytes`);
124
+ }
125
+ }
126
+ /** Validate the optional cache path override. */
127
+ function requireCachePath(path) {
128
+ if (path.length === 0) throw new Error("opencode-live: catalog.cachePath must be a non-empty path when set");
129
+ return path;
130
+ }
131
+ /**
132
+ * Refuse a section this plugin could not serve. Registered as the settings
133
+ * namespace's validator so an invalid route or interval is rejected where it
134
+ * is written instead of silently disabling a route.
135
+ * @param config - the resolved section to check.
136
+ * @throws Error naming the offending configuration entry.
137
+ */
138
+ function assertServiceable(config) {
139
+ resolveConfig(config);
140
+ }
141
+ //#endregion
142
+ export { Config, DEFAULT_API_KEY_ENV, DEFAULT_LIST_REVALIDATE_AFTER_MS, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_STALE_MS, DEFAULT_REFRESH_INTERVAL_MS, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, assertServiceable, resolveConfig };
@@ -0,0 +1,88 @@
1
+ import { LlmError, assertUsableApiKey } from "@deepseek-ai/dsh-llm";
2
+ import "@deepseek-ai/dsh-credentials";
3
+ import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
4
+ //#region src/credentials.ts
5
+ /**
6
+ * Resolve the API key for one inference call.
7
+ *
8
+ * Mirrors the fail-loud reference semantics of the DSH pi-ai adapter: a named
9
+ * reference that misses throws `MISSING_CREDENTIAL` naming the route and the
10
+ * reference, never a key fragment, and never falls back to an ambient key
11
+ * another provider might have left in the environment.
12
+ * @param ctx - the plugin context carrying the optional credential service.
13
+ * @param route - the live route the credential is resolved for.
14
+ * @param profile - the resolved profile naming the credential reference.
15
+ * @returns the validated key for this call.
16
+ * @throws {LlmError} code `MISSING_CREDENTIAL` when the reference is unset.
17
+ */
18
+ async function resolveApiKeyFor(ctx, route, profile) {
19
+ const ref = profile.apiKeyEnv;
20
+ if (ref === void 0) return void 0;
21
+ const credentials = ctx.get("credentials");
22
+ const hit = credentials !== void 0 ? (await credentials.resolve(ref))?.value : launchEnvironmentOf(ctx).get(ref)?.value;
23
+ if (hit !== void 0 && hit.length > 0) return assertUsableApiKey(hit, "opencode-live", ref);
24
+ throw new LlmError(`opencode-live: no credential for provider route "${route}"; its profile resolves ${ref}, which is not set — store it through the credentials service (the web Models page writes it) or export it in the launching environment`, "MISSING_CREDENTIAL");
25
+ }
26
+ /**
27
+ * Api-key auth for a route the plugin authenticates itself.
28
+ *
29
+ * `Models` calls this when the request carries no `apiKey` override (for
30
+ * example a status check): it reports the route as unconfigured rather than
31
+ * inventing ambient credentials. The per-request key override remains the
32
+ * only real authentication path.
33
+ * @param name - display name used as the resolution's status label.
34
+ * @returns the api-key auth method.
35
+ */
36
+ function apiKeyOnlyAuth(name) {
37
+ return {
38
+ name,
39
+ resolve: ({ credential }) => Promise.resolve({
40
+ auth: credential?.key === void 0 ? {} : { apiKey: credential.key },
41
+ source: name
42
+ })
43
+ };
44
+ }
45
+ /**
46
+ * Whether the configured reference is currently set, for status display.
47
+ * Presence is reported without ever exposing the value.
48
+ * @param ctx - the plugin context.
49
+ * @param ref - the credential reference to describe.
50
+ * @returns presence facts, or `undefined` without a credential service.
51
+ */
52
+ async function describeCredential(ctx, ref) {
53
+ const credentials = ctx.get("credentials");
54
+ if (credentials === void 0) return void 0;
55
+ return credentials.describe(ref);
56
+ }
57
+ /**
58
+ * The stable auth bridge handed to `PiAiAdapter`.
59
+ *
60
+ * pi-ai requires both members; this plugin's routes authenticate exclusively
61
+ * through the per-request key, so the store is inert (nothing to read, no
62
+ * sign-in to persist) and the context never answers ambient questions. A
63
+ * write attempt is refused loudly rather than silently dropped: a login that
64
+ * believed it persisted would fail every later request.
65
+ * @returns the injection for `createModels()`.
66
+ */
67
+ function staticAuthBridge() {
68
+ return {
69
+ credentials: {
70
+ async read() {},
71
+ async list() {
72
+ return [];
73
+ },
74
+ async modify() {
75
+ throw new LlmError("opencode-live: stored credential sign-ins are not supported; its routes authenticate through the apiKeyEnv reference resolved by the DSH credential service", "NO_CREDENTIAL_STORE");
76
+ },
77
+ async delete() {}
78
+ },
79
+ authContext: {
80
+ async env() {},
81
+ async fileExists() {
82
+ return false;
83
+ }
84
+ }
85
+ };
86
+ }
87
+ //#endregion
88
+ export { apiKeyOnlyAuth, describeCredential, resolveApiKeyFor, staticAuthBridge };
package/lib/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Product, ROUTE_BY_PRODUCT, RouteId } from "./normalize.js";
2
+ import { Config } from "./config.js";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+
5
+ //#region src/index.d.ts
6
+ /** Cordis plugin name. */
7
+ declare const name = "opencode-live";
8
+ /** Services required before the plugin can activate. */
9
+ declare const inject: string[];
10
+ /** The settings namespace this plugin owns. */
11
+ declare const SETTINGS_NAMESPACE = "opencode-live";
12
+ /**
13
+ * Register the plugin against one composition context.
14
+ * @param ctx - the Cordis context.
15
+ * @param config - the composition base configuration for this plugin.
16
+ */
17
+ declare function apply(ctx: Context, config?: Config): void;
18
+ //#endregion
19
+ export { Config, type Product, ROUTE_BY_PRODUCT, type RouteId, SETTINGS_NAMESPACE, apply, inject, name };
package/lib/index.js ADDED
@@ -0,0 +1,208 @@
1
+ import { ROUTE_BY_PRODUCT } from "./normalize.js";
2
+ import { LiveOpenCodeAdapter } from "./adapter.js";
3
+ import { readySetHash } from "./snapshot.js";
4
+ import { CatalogManager } from "./catalog.js";
5
+ import { Config, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, assertServiceable, resolveConfig } from "./config.js";
6
+ import { apiKeyOnlyAuth, describeCredential, resolveApiKeyFor, staticAuthBridge } from "./credentials.js";
7
+ import { registerCommands } from "./commands.js";
8
+ import { buildRouteProvider } from "./transport.js";
9
+ import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
10
+ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
11
+ //#region src/index.ts
12
+ /** Cordis plugin name. */
13
+ const name = "opencode-live";
14
+ /** Services required before the plugin can activate. */
15
+ const inject = ["llm"];
16
+ /** The settings namespace this plugin owns. */
17
+ const SETTINGS_NAMESPACE = "opencode-live";
18
+ /**
19
+ * Register the plugin against one composition context.
20
+ * @param ctx - the Cordis context.
21
+ * @param config - the composition base configuration for this plugin.
22
+ */
23
+ function apply(ctx, config = {}) {
24
+ const resolved = resolveConfig(config);
25
+ let currentConfig = resolved;
26
+ let configRevision = 0;
27
+ const catalog = new CatalogManager({
28
+ config: {
29
+ ...resolved.catalog,
30
+ ...resolved.catalog.cachePath === void 0 ? { cachePath: defaultCachePath() } : {}
31
+ },
32
+ onChange: (snapshot) => onCatalogChange(snapshot),
33
+ warn: (message) => ctx.logger.warn(message)
34
+ });
35
+ const adapter = new LiveOpenCodeAdapter({
36
+ profiles,
37
+ resolveApiKey: (route, profile) => resolveApiKeyFor(ctx, route, profile),
38
+ auth: staticAuthBridge(),
39
+ resolveAttachments: () => ctx.get("attachments"),
40
+ resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, (hostPath) => ctx.get("fs")?.processPathFromHostPath(hostPath), ref),
41
+ catalog,
42
+ initialWaitMs: currentConfig.catalog.timeoutMs,
43
+ requireFresh: currentConfig.catalog.requireFresh,
44
+ onReplayDegrade: ({ provider, model, reason }) => {
45
+ ctx.logger.warn(`opencode-live: unusable replay state on assistant history for route "${provider}/${model}"; sending that message as provider-neutral content (${reason})`);
46
+ }
47
+ });
48
+ /** Memoized profiles keyed by configuration revision and catalog content. */
49
+ let memoKey;
50
+ let memoProfiles;
51
+ function profiles() {
52
+ const key = `${configRevision}:${catalog.current?.contentHash ?? "empty"}`;
53
+ if (memoKey === key && memoProfiles !== void 0) return memoProfiles;
54
+ const map = /* @__PURE__ */ new Map();
55
+ for (const [route, providerConfig] of currentConfig.providers) {
56
+ const view = catalog.current?.products[providerConfig.product];
57
+ const ready = view === void 0 ? [] : view.readyIds.map((id) => view.candidates.get(id)).filter((candidate) => candidate !== void 0);
58
+ map.set(route, {
59
+ provider: route,
60
+ displayName: providerConfig.displayName,
61
+ apiKeyEnv: providerConfig.apiKeyEnv,
62
+ streamIdleTimeoutMs: providerConfig.streamIdleTimeoutMs,
63
+ maxRequestImageBytes: DEFAULT_MAX_REQUEST_IMAGE_BYTES,
64
+ requestImagePixelBudget: DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET,
65
+ requestImageMaxBytes: DEFAULT_REQUEST_IMAGE_MAX_BYTES,
66
+ retryPolicy: providerConfig.retryPolicy ?? resolveRetryPolicy(void 0, `opencode-live: provider "${route}" retryPolicy`),
67
+ ...providerConfig.headers !== void 0 ? { headers: { ...providerConfig.headers } } : {},
68
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
69
+ piProvider: buildRouteProvider({
70
+ route,
71
+ product: providerConfig.product,
72
+ displayName: providerConfig.displayName,
73
+ ready,
74
+ auth: { apiKey: apiKeyOnlyAuth(providerConfig.displayName) }
75
+ })
76
+ });
77
+ }
78
+ memoKey = key;
79
+ memoProfiles = map;
80
+ return map;
81
+ }
82
+ let registration;
83
+ let registeredFacts;
84
+ let lastReadyHash;
85
+ /** The current route list in fixed order. */
86
+ function routeList() {
87
+ return [...currentConfig.providers.keys()];
88
+ }
89
+ /** Registration facts: routes with their display names and retry policies. */
90
+ function registrationFacts() {
91
+ return JSON.stringify(routeList().map((route) => {
92
+ const provider = currentConfig.providers.get(route);
93
+ return {
94
+ route,
95
+ displayName: provider?.displayName,
96
+ retryPolicy: provider?.retryPolicy
97
+ };
98
+ }));
99
+ }
100
+ /** Register or atomically replace this adapter's routes. */
101
+ function ensureRegistration() {
102
+ const routes = routeList();
103
+ const facts = registrationFacts();
104
+ if (registration === void 0) {
105
+ if (routes.length === 0) {
106
+ registeredFacts = facts;
107
+ return;
108
+ }
109
+ registration = ctx.llm.registerAdapter(routes, adapter);
110
+ registeredFacts = facts;
111
+ return;
112
+ }
113
+ registration.replace(routes);
114
+ registeredFacts = facts;
115
+ }
116
+ /** The directory entries for the two fixed routes. */
117
+ function directoryEntries() {
118
+ return routeList().map((route) => ({
119
+ provider: route,
120
+ displayName: currentConfig.providers.get(route)?.displayName ?? route,
121
+ settingsNs: SETTINGS_NAMESPACE,
122
+ settingsPath: ["providers", route],
123
+ declared: true
124
+ }));
125
+ }
126
+ let directory;
127
+ /** Register or atomically replace the configurable-provider directory. */
128
+ function ensureDirectory() {
129
+ const entries = directoryEntries();
130
+ JSON.stringify(entries);
131
+ if (directory === void 0) {
132
+ if (entries.length === 0) return;
133
+ directory = ctx.llm.registerConfigurableProviders(entries);
134
+ return;
135
+ }
136
+ directory.replace(entries);
137
+ }
138
+ /** Re-register only when the executable set actually changed. */
139
+ function onCatalogChange(snapshot) {
140
+ if (snapshot === void 0) return;
141
+ const hash = readySetHash(snapshot);
142
+ if (hash === lastReadyHash) return;
143
+ lastReadyHash = hash;
144
+ if (registration !== void 0) ensureRegistration();
145
+ }
146
+ ensureRegistration();
147
+ ensureDirectory();
148
+ let commandDisposers = [];
149
+ ctx.inject(["commands"], (commandsCtx) => {
150
+ commandDisposers = registerCommands(commandsCtx, {
151
+ catalog,
152
+ config: () => currentConfig,
153
+ describeCredential: async (route) => {
154
+ const provider = currentConfig.providers.get(route);
155
+ if (provider === void 0) return void 0;
156
+ return describeCredential(ctx, provider.apiKeyEnv);
157
+ }
158
+ });
159
+ });
160
+ let source = () => config;
161
+ ctx.inject(["settings"], (settingsCtx) => {
162
+ settingsCtx.settings.installSection(ctx, SETTINGS_NAMESPACE, Config, config, {
163
+ validate: assertServiceable,
164
+ setSource: (next) => {
165
+ source = next;
166
+ },
167
+ onChange: () => {
168
+ try {
169
+ const next = resolveConfig(source());
170
+ const catalogChanged = JSON.stringify(next.catalog) !== JSON.stringify(currentConfig.catalog);
171
+ const routesChanged = registrationFacts() !== registeredFacts;
172
+ currentConfig = next;
173
+ configRevision += 1;
174
+ adapter.updateOptions({
175
+ initialWaitMs: next.catalog.timeoutMs,
176
+ requireFresh: next.catalog.requireFresh
177
+ });
178
+ if (catalogChanged) catalog.reconfigure({
179
+ ...next.catalog,
180
+ ...next.catalog.cachePath === void 0 ? {} : { cachePath: next.catalog.cachePath }
181
+ });
182
+ if (routesChanged) ensureRegistration();
183
+ ensureDirectory();
184
+ } catch (error) {
185
+ ctx.logger.error("opencode-live: keeping the previously registered routes after a refused settings update");
186
+ ctx.logger.error(error);
187
+ }
188
+ }
189
+ });
190
+ });
191
+ ctx.effect(function* () {
192
+ yield () => {
193
+ catalog.stop();
194
+ for (const dispose of commandDisposers) dispose();
195
+ commandDisposers = [];
196
+ };
197
+ }, "opencode-live effects");
198
+ catalog.start().catch((error) => {
199
+ ctx.logger.warn("opencode-live: the initial catalog refresh failed; the catalog will retry periodically");
200
+ ctx.logger.warn(error);
201
+ });
202
+ }
203
+ /** The default cache location under the resolved DSH home. */
204
+ function defaultCachePath() {
205
+ return dshHomePath("cache", "opencode-live", "catalog.json");
206
+ }
207
+ //#endregion
208
+ export { Config, ROUTE_BY_PRODUCT, SETTINGS_NAMESPACE, apply, inject, name };
@@ -0,0 +1,10 @@
1
+ //#region src/normalize.d.ts
2
+ /** The two OpenCode products this plugin serves. */
3
+ type Product = 'zen' | 'go';
4
+ /** DSH route keys this plugin registers; fixed for the plugin's lifetime. */
5
+ declare const ROUTE_ZEN = "opencode-zen-live";
6
+ declare const ROUTE_GO = "opencode-go-live";
7
+ type RouteId = typeof ROUTE_ZEN | typeof ROUTE_GO;
8
+ declare const ROUTE_BY_PRODUCT: Readonly<Record<Product, RouteId>>;
9
+ //#endregion
10
+ export { Product, ROUTE_BY_PRODUCT, ROUTE_GO, ROUTE_ZEN, RouteId };