dsh-opencode 0.1.2 → 0.1.4

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/lib/client.js ADDED
@@ -0,0 +1,537 @@
1
+ window.__ModuleLoader__.load({id:'dsh-opencode',factory:(require)=>{var module={exports:{}};var exports=module.exports;
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ let react = require("react");
4
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
+ let react_jsx_runtime = require("react/jsx-runtime");
6
+ //#region src/shared/opencode.ts
7
+ /** Browser-safe identifiers shared by the Host metadata and Client UI. */
8
+ const CLIENT_MODULE_ID = "dsh-opencode";
9
+ const SETTINGS_NAMESPACE = "opencode-live";
10
+ const ROUTES = {
11
+ zen: "opencode-zen-live",
12
+ go: "opencode-go-live"
13
+ };
14
+ const HELP_URLS = {
15
+ auth: "https://opencode.ai/auth",
16
+ zen: "https://opencode.ai/docs/zen/",
17
+ go: "https://opencode.ai/docs/go/"
18
+ };
19
+ //#endregion
20
+ //#region src/client/OpenCodeCredentialForm.tsx
21
+ function OpenCodeCredentialForm(props) {
22
+ const [draft, setDraft] = (0, react.useState)("");
23
+ const [saving, setSaving] = (0, react.useState)(false);
24
+ const [message, setMessage] = (0, react.useState)("");
25
+ const [error, setError] = (0, react.useState)(false);
26
+ const id = `opencode-api-key-${props.route}`;
27
+ const unavailable = props.state.kind !== "known";
28
+ const readOnly = props.state.kind === "known" && !props.state.writable;
29
+ const disabled = saving || unavailable || readOnly;
30
+ const submit = (event) => {
31
+ event.preventDefault();
32
+ if (props.state.kind !== "known") return;
33
+ setSaving(true);
34
+ setError(false);
35
+ props.controller.save(props.route, draft, props.state.ref).then((result) => {
36
+ setSaving(false);
37
+ setMessage(result.message);
38
+ setError(result.kind === "error");
39
+ if (result.kind !== "error") setDraft("");
40
+ });
41
+ };
42
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
43
+ onSubmit: submit,
44
+ noValidate: true,
45
+ children: [
46
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: props.route === "zen" ? "OpenCode Zen (Live)" : "OpenCode Go (Live)" }),
47
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
48
+ htmlFor: id,
49
+ children: "OpenCode API キー"
50
+ }),
51
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
52
+ id,
53
+ type: "password",
54
+ value: draft,
55
+ onChange: (event) => setDraft(event.currentTarget.value),
56
+ autoComplete: "new-password",
57
+ spellCheck: false,
58
+ disabled,
59
+ "aria-invalid": error,
60
+ "aria-describedby": `${id}-status`
61
+ }),
62
+ unavailable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: props.state.kind === "unavailable" ? props.state.reason : "状態を確認中です。" }),
63
+ readOnly && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "この認証参照は読み取り専用です。" }),
64
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
65
+ id: `${id}-status`,
66
+ role: error ? "alert" : "status",
67
+ "aria-live": "polite",
68
+ children: message
69
+ }),
70
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
71
+ type: "submit",
72
+ variant: "primary",
73
+ disabled,
74
+ children: "保存"
75
+ }),
76
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
77
+ type: "button",
78
+ variant: "ghost",
79
+ onClick: () => {
80
+ setDraft("");
81
+ props.onCancel();
82
+ },
83
+ children: "キャンセル"
84
+ })
85
+ ]
86
+ });
87
+ }
88
+ //#endregion
89
+ //#region src/client/credential-controller.ts
90
+ function routeFromProvider(value) {
91
+ if (value === ROUTES.zen) return "zen";
92
+ if (value === ROUTES.go) return "go";
93
+ }
94
+ function isRecord(value) {
95
+ return typeof value === "object" && value !== null;
96
+ }
97
+ function unavailable(route, reason) {
98
+ return {
99
+ kind: "unavailable",
100
+ route,
101
+ reason
102
+ };
103
+ }
104
+ function decodeDescribe(result, ref) {
105
+ if (!isRecord(result) || result.ok !== true || !isRecord(result.value)) return void 0;
106
+ const info = result.value[ref];
107
+ if (!isRecord(info) || typeof info.configured !== "boolean" || typeof info.writable !== "boolean") return void 0;
108
+ return {
109
+ configured: info.configured,
110
+ writable: info.writable,
111
+ ...typeof info.source === "string" ? { source: info.source } : {}
112
+ };
113
+ }
114
+ function isSuccessfulWrite(result) {
115
+ return isRecord(result) && result.ok === true;
116
+ }
117
+ var CredentialController = class {
118
+ ctx;
119
+ inFlight = /* @__PURE__ */ new Set();
120
+ states = /* @__PURE__ */ new Map();
121
+ generations = /* @__PURE__ */ new Map();
122
+ subscriptions = [];
123
+ disposed = false;
124
+ disposalGeneration = 0;
125
+ constructor(ctx) {
126
+ this.ctx = ctx;
127
+ const settings = ctx.settingsScope.describe();
128
+ this.subscriptions.push(settings.subscribe(() => this.invalidate()));
129
+ if (typeof ctx.remote.$on === "function") for (const event of ["credentials/reference-updated", "settings/document-updated"]) this.subscriptions.push(ctx.remote.$on(event, () => this.invalidate()));
130
+ const events = ctx;
131
+ if (typeof events.on === "function") this.subscriptions.push(events.on("connection/reset", () => this.invalidate()));
132
+ }
133
+ dispose() {
134
+ this.disposed = true;
135
+ this.disposalGeneration += 1;
136
+ for (const unsubscribe of this.subscriptions.splice(0)) unsubscribe();
137
+ this.generations.clear();
138
+ this.states.clear();
139
+ this.inFlight.clear();
140
+ }
141
+ subscribe(listener) {
142
+ const listeners = this.listeners;
143
+ listeners.add(listener);
144
+ return () => listeners.delete(listener);
145
+ }
146
+ listeners = /* @__PURE__ */ new Set();
147
+ invalidate() {
148
+ for (const route of ["zen", "go"]) this.generations.set(route, (this.generations.get(route) ?? 0) + 1);
149
+ for (const listener of this.listeners) listener();
150
+ }
151
+ currentSettings() {
152
+ const face = this.ctx.settingsScope.describe();
153
+ const snapshot = face.getSnapshot();
154
+ if (snapshot.status !== "ready" || !isRecord(snapshot.view)) return void 0;
155
+ const namespace = snapshot.view.namespaces.find((item) => item.ns === SETTINGS_NAMESPACE);
156
+ if (namespace === void 0 || !isRecord(namespace.value)) return void 0;
157
+ return {
158
+ value: namespace.value,
159
+ face
160
+ };
161
+ }
162
+ refFor(value, route) {
163
+ const ref = value.providers?.[ROUTES[route]]?.apiKeyEnv;
164
+ return typeof ref === "string" && ref.length > 0 ? ref : void 0;
165
+ }
166
+ async loadRoute(route, signal) {
167
+ const generation = (this.generations.get(route) ?? 0) + 1;
168
+ this.generations.set(route, generation);
169
+ this.states.set(route, {
170
+ kind: "loading",
171
+ route
172
+ });
173
+ if (signal?.aborted || this.disposed) return unavailable(route, "確認がキャンセルされました。");
174
+ const settings = this.ctx.settingsScope.describe();
175
+ try {
176
+ await settings.ensure();
177
+ const current = this.currentSettings();
178
+ if (current === void 0) return this.commit(route, generation, unavailable(route, "設定の credential reference を確認できません。"));
179
+ const ref = this.refFor(current.value, route);
180
+ if (ref === void 0) return this.commit(route, generation, unavailable(route, "設定の credential reference を確認できません。"));
181
+ const info = decodeDescribe(await this.ctx.remote.credentials.describe([ref]), ref);
182
+ if (info === void 0) return this.commit(route, generation, unavailable(route, "認証状態を確認できません。"));
183
+ const sharedWith = [];
184
+ for (const other of ["zen", "go"]) if (other !== route && this.refFor(current.value, other) === ref) sharedWith.push(other);
185
+ return this.commit(route, generation, {
186
+ kind: "known",
187
+ route,
188
+ ref,
189
+ ...info,
190
+ sharedWith
191
+ });
192
+ } catch {
193
+ return this.commit(route, generation, unavailable(route, "設定または認証状態を確認できません。"));
194
+ }
195
+ }
196
+ commit(route, generation, state) {
197
+ if (!this.disposed && this.generations.get(route) === generation) {
198
+ this.states.set(route, state);
199
+ for (const listener of this.listeners) listener();
200
+ }
201
+ return state;
202
+ }
203
+ async loadRoutes(signal) {
204
+ const [zen, go] = await Promise.all([this.loadRoute("zen", signal), this.loadRoute("go", signal)]);
205
+ return {
206
+ zen,
207
+ go
208
+ };
209
+ }
210
+ state(route) {
211
+ return this.states.get(route);
212
+ }
213
+ async save(route, value, displayedRef) {
214
+ const normalized = value.trim();
215
+ if (normalized.length === 0 || /[\r\n]/.test(normalized) || /^['"].*['"]$/.test(normalized) || normalized.includes("=")) return {
216
+ kind: "error",
217
+ message: "API キーの形式を確認してください。"
218
+ };
219
+ const displayed = this.states.get(route);
220
+ const fence = this.disposalGeneration;
221
+ const current = await this.loadRoute(route);
222
+ if (this.disposed || this.disposalGeneration !== fence) return {
223
+ kind: "error",
224
+ message: "設定画面が閉じられました。再試行してください。"
225
+ };
226
+ if (displayed?.kind !== "known" || current.kind !== "known" || displayed.ref !== displayedRef || current.ref !== displayedRef) return {
227
+ kind: "error",
228
+ message: "設定が更新されました。状態を再読み込みしてから再試行してください。"
229
+ };
230
+ if (!current.writable) return {
231
+ kind: "error",
232
+ message: "この認証参照は読み取り専用です。"
233
+ };
234
+ if (this.inFlight.has(current.ref)) return {
235
+ kind: "error",
236
+ message: "同じ認証参照の保存が進行中です。"
237
+ };
238
+ this.inFlight.add(current.ref);
239
+ try {
240
+ if (this.disposed || this.disposalGeneration !== fence) return {
241
+ kind: "error",
242
+ message: "設定画面が閉じられました。再試行してください。"
243
+ };
244
+ if (!isSuccessfulWrite(await this.ctx.remote.credentials.set(current.ref, normalized))) return {
245
+ kind: "error",
246
+ message: "API キーを保存できませんでした。"
247
+ };
248
+ const confirmed = await this.loadRoute(route);
249
+ return confirmed.kind === "known" && confirmed.configured ? {
250
+ kind: "saved",
251
+ message: "保存しました。キーの値は表示しません。"
252
+ } : {
253
+ kind: "saved-unconfirmed",
254
+ message: "保存要求は成功しましたが、状態を再確認できませんでした。"
255
+ };
256
+ } catch {
257
+ return {
258
+ kind: "error",
259
+ message: "API キーを保存できませんでした。"
260
+ };
261
+ } finally {
262
+ this.inFlight.delete(current.ref);
263
+ }
264
+ }
265
+ };
266
+ //#endregion
267
+ //#region src/client/OpenCodeProviderCard.tsx
268
+ function OpenCodeProviderCard(props) {
269
+ const route = routeFromProvider(props.provider.provider);
270
+ const [state, setState] = (0, react.useState)(() => ({
271
+ kind: "loading",
272
+ route: route ?? "zen"
273
+ }));
274
+ (0, react.useEffect)(() => {
275
+ if (route === void 0) return;
276
+ let active = true;
277
+ const dispose = props.controller.subscribe(() => {
278
+ const next = props.controller.state(route);
279
+ if (active && next !== void 0) setState(next);
280
+ });
281
+ props.controller.loadRoute(route).then((next) => {
282
+ if (active) setState(next);
283
+ });
284
+ return () => {
285
+ active = false;
286
+ dispose();
287
+ };
288
+ }, [props.controller, route]);
289
+ if (route === void 0) return null;
290
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
291
+ "data-dsh-opencode-provider": route,
292
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenCodeCredentialForm, {
293
+ route,
294
+ state,
295
+ controller: props.controller,
296
+ onCancel: () => void 0
297
+ })
298
+ });
299
+ }
300
+ //#endregion
301
+ //#region src/client/OpenCodeSetupDialog.tsx
302
+ function OpenCodeSetupDialog(props) {
303
+ const snapshot = (0, react.useSyncExternalStore)(props.controller.subscribe, props.controller.getSnapshot, props.controller.getSnapshot);
304
+ if (!snapshot.open) return null;
305
+ const close = () => props.controller.close();
306
+ const body = snapshot.route === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
307
+ role: "status",
308
+ children: [snapshot.message ?? "設定状況を確認しました。", /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusRows, { controller: props.controller })]
309
+ }) : snapshot.route === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CredentialFormForRoute, {
310
+ route: snapshot.route,
311
+ controller: props.controller,
312
+ onCancel: close
313
+ });
314
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
315
+ open: true,
316
+ onClose: close,
317
+ title: "OpenCode API キー設定",
318
+ closeLabel: "閉じる",
319
+ description: "キーの値は表示しません。",
320
+ children: [
321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: "OpenCode にサインインし、Zen または Go の API キーを作成して保存してください。" }),
322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
324
+ href: HELP_URLS.auth,
325
+ target: "_blank",
326
+ rel: "noopener noreferrer",
327
+ children: "サインイン"
328
+ }),
329
+ " | ",
330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
331
+ href: HELP_URLS.zen,
332
+ target: "_blank",
333
+ rel: "noopener noreferrer",
334
+ children: "Zen の手順"
335
+ }),
336
+ " | ",
337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
338
+ href: HELP_URLS.go,
339
+ target: "_blank",
340
+ rel: "noopener noreferrer",
341
+ children: "Go の手順"
342
+ })
343
+ ] }),
344
+ body,
345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
346
+ type: "button",
347
+ variant: "ghost",
348
+ onClick: close,
349
+ children: "キャンセル"
350
+ })
351
+ ]
352
+ });
353
+ }
354
+ function CredentialFormForRoute(props) {
355
+ const state = props.controller.state(props.route) ?? {
356
+ kind: "loading",
357
+ route: props.route
358
+ };
359
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenCodeCredentialForm, {
360
+ route: props.route,
361
+ state,
362
+ controller: props.controller.credentials,
363
+ onCancel: props.onCancel
364
+ });
365
+ }
366
+ function StatusRows(props) {
367
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: ["zen", "go"].map((route) => {
368
+ const state = props.controller.state(route);
369
+ const text = state?.kind === "known" ? state.configured ? "API キー保存済み" : "API キー未設定" : state?.kind === "unavailable" ? state.reason : "状態を確認中です";
370
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
371
+ route === "zen" ? "Zen" : "Go",
372
+ ": ",
373
+ text
374
+ ] }, route);
375
+ }) });
376
+ }
377
+ //#endregion
378
+ //#region src/client/setup-controller.ts
379
+ var SetupController = class {
380
+ ctx;
381
+ credentials;
382
+ current = { open: false };
383
+ listeners = /* @__PURE__ */ new Set();
384
+ constructor(ctx) {
385
+ this.ctx = ctx;
386
+ this.credentials = new CredentialController(ctx);
387
+ }
388
+ getSnapshot = () => this.current;
389
+ subscribe = (listener) => {
390
+ this.listeners.add(listener);
391
+ const credentialDispose = this.credentials.subscribe(listener);
392
+ return () => {
393
+ this.listeners.delete(listener);
394
+ credentialDispose();
395
+ };
396
+ };
397
+ dispose() {
398
+ this.credentials.dispose();
399
+ this.listeners.clear();
400
+ this.close();
401
+ }
402
+ update(next) {
403
+ this.current = next;
404
+ for (const listener of this.listeners) listener();
405
+ }
406
+ open(route, message) {
407
+ this.update({
408
+ open: true,
409
+ route,
410
+ ...message === void 0 ? {} : { message }
411
+ });
412
+ }
413
+ close() {
414
+ this.update({ open: false });
415
+ }
416
+ sessionId;
417
+ async select(option, sessionId) {
418
+ this.sessionId = sessionId;
419
+ if (option.id === "zen" || option.id === "go") {
420
+ this.open(option.id);
421
+ await this.credentials.loadRoute(option.id);
422
+ return;
423
+ }
424
+ if (option.id === "status") {
425
+ this.open("status", "設定状況を確認中…");
426
+ await this.credentials.loadRoutes();
427
+ return;
428
+ }
429
+ if (option.id === "refresh") {
430
+ this.open("status", "モデル一覧を更新中…");
431
+ const execute = commandsExecute(this.ctx);
432
+ if (execute === void 0 || this.sessionId === void 0) {
433
+ this.open("status", "更新コマンドを利用できません。/opencode-refresh all を実行してください。");
434
+ return;
435
+ }
436
+ try {
437
+ const result = await execute(this.sessionId, "/opencode-refresh all", [], void 0);
438
+ this.open("status", result.ok ? "モデル一覧の更新コマンドを送信しました。" : "モデル一覧を更新できませんでした。");
439
+ } catch {
440
+ this.open("status", "モデル一覧を更新できませんでした。");
441
+ }
442
+ }
443
+ }
444
+ state(route) {
445
+ return this.credentials.state(route);
446
+ }
447
+ };
448
+ function commandsExecute(ctx) {
449
+ const commands = ctx.remote.commands;
450
+ if (typeof commands !== "object" || commands === null) return void 0;
451
+ const execute = commands.execute;
452
+ return typeof execute === "function" ? execute.bind(commands) : void 0;
453
+ }
454
+ //#endregion
455
+ //#region src/client/index.ts
456
+ /** Client packages required by this entry's injected services and UI modules. */
457
+ const inject = [
458
+ "@deepseek-ai/dsh-api-remotes",
459
+ "@deepseek-ai/dsh-commands",
460
+ "@deepseek-ai/dsh-client-locale",
461
+ "@deepseek-ai/dsh-client-store",
462
+ "@deepseek-ai/dsh-client-ui-commands",
463
+ "@deepseek-ai/dsh-client-ui-layout",
464
+ "@deepseek-ai/dsh-client-ui-primitives",
465
+ "@deepseek-ai/dsh-client-ui-renderer",
466
+ "@deepseek-ai/dsh-client-ui-settings",
467
+ "@deepseek-ai/dsh-client-ui-settings-models",
468
+ "@deepseek-ai/dsh-client-ui-slots"
469
+ ];
470
+ const services = [
471
+ "commandUi",
472
+ "remote.credentials",
473
+ "remote.commands",
474
+ "settingsScope",
475
+ "slots"
476
+ ];
477
+ function apply(ctx) {
478
+ ctx.inject(services, (injected) => {
479
+ const client = injected;
480
+ client.effect(() => {
481
+ const setup = new SetupController(client);
482
+ const disposers = [];
483
+ disposers.push(client.commandUi.decorate({
484
+ name: "dsh-opencode",
485
+ available: () => true,
486
+ ui: {
487
+ kind: "popupSelect",
488
+ options: async (_session, signal) => {
489
+ const states = await setup.credentials.loadRoutes(signal);
490
+ const options = ["zen", "go"].map((route) => {
491
+ const state = states[route];
492
+ return {
493
+ id: route,
494
+ label: route === "zen" ? "OpenCode Zen を設定" : "OpenCode Go を設定",
495
+ detail: state.kind === "known" ? state.configured ? "API キー保存済み" : "API キー未設定" : state.kind === "unavailable" ? state.reason : "状態を確認中です",
496
+ active: state.kind === "known" && state.configured
497
+ };
498
+ });
499
+ options.push({
500
+ id: "status",
501
+ label: "設定状況を確認",
502
+ detail: "キーの値は表示しません"
503
+ }, {
504
+ id: "refresh",
505
+ label: "モデル一覧を更新",
506
+ detail: "Host の更新コマンドを実行してください"
507
+ });
508
+ return options;
509
+ },
510
+ onSelect: (option, session) => {
511
+ setup.select(option, session.sessionId);
512
+ }
513
+ }
514
+ }));
515
+ disposers.push(client.slots.inject("settings.models.provider-card", () => client.slots.register({
516
+ name: "settings.models.provider-card",
517
+ key: "opencode-live",
518
+ inject: () => ({ controller: setup.credentials })
519
+ }, OpenCodeProviderCard)));
520
+ disposers.push(client.slots.inject("shell.overlay", () => client.slots.register({
521
+ name: "shell.overlay",
522
+ id: "opencode-live-setup",
523
+ inject: () => ({ controller: setup })
524
+ }, OpenCodeSetupDialog)));
525
+ return () => {
526
+ setup.dispose();
527
+ for (const dispose of disposers.splice(0)) dispose();
528
+ };
529
+ }, `${CLIENT_MODULE_ID}: client registrations`);
530
+ });
531
+ }
532
+ //#endregion
533
+ exports.SetupController = SetupController;
534
+ exports.apply = apply;
535
+ exports.inject = inject;
536
+
537
+ return module.exports;}});
package/lib/commands.js CHANGED
@@ -2,7 +2,7 @@ import { PRODUCT_BY_ROUTE, ROUTE_BY_PRODUCT, describeNonReadyState } from "./nor
2
2
  //#region src/commands.ts
3
3
  const USAGE_REFRESH = "Usage: /opencode-refresh [all|zen|go]";
4
4
  const USAGE_MODELS = "Usage: /opencode-models <zen|go> [--all]";
5
- const USAGE_ENABLE = "Usage: /dsh-opencode — report status; store the API key through the web Models page";
5
+ const USAGE_ENABLE = "Usage: /dsh-opencode [status|help]";
6
6
  /** Whether one date stamp renders as a short local time. */
7
7
  function renderTime(timestamp) {
8
8
  if (timestamp === void 0) return "never";
@@ -130,35 +130,20 @@ function commandDefinitions(ctx, services) {
130
130
  },
131
131
  {
132
132
  name: "dsh-opencode",
133
- description: "Show OpenCode status and where to store the API key",
133
+ description: "Show OpenCode setup and credential status",
134
134
  recordInput: false,
135
135
  handler: async (invocation) => {
136
- if (invocation.rawInput.trim().length > 0) return {
136
+ const input = invocation.rawInput.trim();
137
+ if (input !== "" && input !== "status" && input !== "help") return {
137
138
  kind: "error",
138
- text: [USAGE_ENABLE, "This command never accepts the key as text; paste it into the masked API key input on the web Models page instead."].join("\n")
139
+ text: USAGE_ENABLE
139
140
  };
140
141
  const routes = [ROUTE_BY_PRODUCT.zen, ROUTE_BY_PRODUCT.go];
141
- if (!(await Promise.all(routes.map(async (route) => {
142
- return (await services.describeCredential(route))?.configured === true;
143
- }))).some((configured) => configured)) return {
144
- kind: "success",
145
- text: [
146
- "No OpenCode API key is configured yet.",
147
- "Open Settings > Models, edit the OpenCode Zen (Live) provider, and paste the key into its masked API key input.",
148
- "This command never displays key values."
149
- ].join("\n")
150
- };
151
142
  if (invocation.signal.aborted) return {
152
143
  kind: "success",
153
144
  text: "Refresh cancelled."
154
145
  };
155
- try {
156
- await services.catalog.refresh({
157
- products: ["zen", "go"],
158
- signal: invocation.signal
159
- });
160
- } catch {}
161
- const lines = ["OpenCode API key configured. Zen and Go are enabled:"];
146
+ const lines = ["OpenCode setup status (use the Client setup form to save a key):"];
162
147
  for (const route of routes) lines.push(await productStatus(ctx, services, PRODUCT_BY_ROUTE[route]));
163
148
  return {
164
149
  kind: "success",
package/lib/config.js CHANGED
@@ -20,6 +20,17 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
20
20
  const MAX_TIMER_DELAY_MS = 2147483647;
21
21
  /** The credential reference both OpenCode products document. */
22
22
  const DEFAULT_API_KEY_ENV = "OPENCODE_API_KEY";
23
+ /** Settings-schema defaults published to Client Settings descriptors. */
24
+ const DEFAULT_PROVIDERS = {
25
+ [ROUTE_BY_PRODUCT.zen]: {
26
+ product: "zen",
27
+ apiKeyEnv: DEFAULT_API_KEY_ENV
28
+ },
29
+ [ROUTE_BY_PRODUCT.go]: {
30
+ product: "go",
31
+ apiKeyEnv: DEFAULT_API_KEY_ENV
32
+ }
33
+ };
23
34
  const DEFAULT_REFRESH_INTERVAL_MS = 9e5;
24
35
  const DEFAULT_LIST_REVALIDATE_AFTER_MS = 6e4;
25
36
  const DEFAULT_TIMEOUT_MS = 15e3;
@@ -47,7 +58,7 @@ const catalogSchema = z.object({
47
58
  });
48
59
  /** Runtime schema for {@link Config}. */
49
60
  const Config = z.object({
50
- providers: z.dict(providerSchema),
61
+ providers: z.dict(providerSchema).default(DEFAULT_PROVIDERS),
51
62
  catalog: catalogSchema.default({})
52
63
  });
53
64
  /** Whether one number is a positive finite integer within timer bounds. */
@@ -139,4 +150,4 @@ function assertServiceable(config) {
139
150
  resolveConfig(config);
140
151
  }
141
152
  //#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 };
153
+ export { Config, DEFAULT_API_KEY_ENV, DEFAULT_LIST_REVALIDATE_AFTER_MS, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_MAX_STALE_MS, DEFAULT_PROVIDERS, DEFAULT_REFRESH_INTERVAL_MS, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, assertServiceable, resolveConfig };
package/lib/index.js CHANGED
@@ -87,9 +87,9 @@ function apply(ctx, config = {}) {
87
87
  return [...currentConfig.providers.keys()];
88
88
  }
89
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);
90
+ function registrationFacts(config = currentConfig) {
91
+ return JSON.stringify([...config.providers.keys()].map((route) => {
92
+ const provider = config.providers.get(route);
93
93
  return {
94
94
  route,
95
95
  displayName: provider?.displayName,
@@ -149,7 +149,6 @@ function apply(ctx, config = {}) {
149
149
  ctx.inject(["commands"], (commandsCtx) => {
150
150
  commandDisposers = registerCommands(commandsCtx, {
151
151
  catalog,
152
- config: () => currentConfig,
153
152
  describeCredential: async (route) => {
154
153
  const provider = currentConfig.providers.get(route);
155
154
  if (provider === void 0) return void 0;
@@ -168,7 +167,7 @@ function apply(ctx, config = {}) {
168
167
  try {
169
168
  const next = resolveConfig(source());
170
169
  const catalogChanged = JSON.stringify(next.catalog) !== JSON.stringify(currentConfig.catalog);
171
- const routesChanged = registrationFacts() !== registeredFacts;
170
+ const routesChanged = registrationFacts(next) !== registeredFacts;
172
171
  currentConfig = next;
173
172
  configRevision += 1;
174
173
  adapter.updateOptions({
package/lib/transport.js CHANGED
@@ -27,7 +27,7 @@ import { openAIResponsesApi } from "@earendil-works/pi-ai/api/openai-responses.l
27
27
  */
28
28
  /** The plugin's honest client identification value. */
29
29
  const PLUGIN_ID = "opencode-live";
30
- const PLUGIN_VERSION = "0.1.1";
30
+ const PLUGIN_VERSION = "0.1.3";
31
31
  /** Header OpenCode Go documents for coding-agent session identification. */
32
32
  const SESSION_HEADER = "x-opencode-session";
33
33
  /** Header carrying this plugin's honest client identity alongside DSH attribution. */