dsh-opencode 0.1.3 → 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/README.md +9 -7
- package/lib/client.d.ts +1738 -0
- package/lib/client.js +537 -0
- package/lib/commands.js +8 -37
- package/lib/config.js +13 -2
- package/lib/index.js +5 -10
- package/lib/transport.js +1 -1
- package/package.json +34 -2
- package/lib/keyring.js +0 -109
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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PRODUCT_BY_ROUTE, ROUTE_BY_PRODUCT, describeNonReadyState } from "./normalize.js";
|
|
2
|
-
import "./keyring.js";
|
|
3
2
|
//#region src/commands.ts
|
|
4
3
|
const USAGE_REFRESH = "Usage: /opencode-refresh [all|zen|go]";
|
|
5
4
|
const USAGE_MODELS = "Usage: /opencode-models <zen|go> [--all]";
|
|
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,49 +130,20 @@ function commandDefinitions(ctx, services) {
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
name: "dsh-opencode",
|
|
133
|
-
description: "
|
|
134
|
-
input: { hint: "Paste the OpenCode API key here (required only when unset)" },
|
|
133
|
+
description: "Show OpenCode setup and credential status",
|
|
135
134
|
recordInput: false,
|
|
136
135
|
handler: async (invocation) => {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
kind: "success",
|
|
142
|
-
text: "Storing cancelled."
|
|
143
|
-
};
|
|
144
|
-
const failure = await services.storeApiKey(key);
|
|
145
|
-
if (failure !== void 0) return {
|
|
146
|
-
kind: "error",
|
|
147
|
-
text: failure
|
|
148
|
-
};
|
|
149
|
-
const lines = ["OpenCode API key stored. Zen and Go are enabled:"];
|
|
150
|
-
for (const route of routes) lines.push(await productStatus(ctx, services, PRODUCT_BY_ROUTE[route]));
|
|
151
|
-
return {
|
|
152
|
-
kind: "success",
|
|
153
|
-
text: lines.join("\n")
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
if (!await services.keyConfigured()) return {
|
|
157
|
-
kind: "success",
|
|
158
|
-
text: [
|
|
159
|
-
"No OpenCode API key is configured yet.",
|
|
160
|
-
"Type the key into this command's input field and run again, or export it in the launch environment.",
|
|
161
|
-
...await services.keyReadonly() ? ["The credential reference is read-only in this deployment."] : [],
|
|
162
|
-
"This command never displays key values."
|
|
163
|
-
].join("\n")
|
|
136
|
+
const input = invocation.rawInput.trim();
|
|
137
|
+
if (input !== "" && input !== "status" && input !== "help") return {
|
|
138
|
+
kind: "error",
|
|
139
|
+
text: USAGE_ENABLE
|
|
164
140
|
};
|
|
141
|
+
const routes = [ROUTE_BY_PRODUCT.zen, ROUTE_BY_PRODUCT.go];
|
|
165
142
|
if (invocation.signal.aborted) return {
|
|
166
143
|
kind: "success",
|
|
167
144
|
text: "Refresh cancelled."
|
|
168
145
|
};
|
|
169
|
-
|
|
170
|
-
await services.catalog.refresh({
|
|
171
|
-
products: ["zen", "go"],
|
|
172
|
-
signal: invocation.signal
|
|
173
|
-
});
|
|
174
|
-
} catch {}
|
|
175
|
-
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):"];
|
|
176
147
|
for (const route of routes) lines.push(await productStatus(ctx, services, PRODUCT_BY_ROUTE[route]));
|
|
177
148
|
return {
|
|
178
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
|
@@ -4,7 +4,6 @@ import { readySetHash } from "./snapshot.js";
|
|
|
4
4
|
import { CatalogManager } from "./catalog.js";
|
|
5
5
|
import { Config, DEFAULT_MAX_REQUEST_IMAGE_BYTES, DEFAULT_REQUEST_IMAGE_MAX_BYTES, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET, assertServiceable, resolveConfig } from "./config.js";
|
|
6
6
|
import { apiKeyOnlyAuth, describeCredential, resolveApiKeyFor, staticAuthBridge } from "./credentials.js";
|
|
7
|
-
import { keyConfigured, keyReadonly, storeApiKey } from "./keyring.js";
|
|
8
7
|
import { registerCommands } from "./commands.js";
|
|
9
8
|
import { buildRouteProvider } from "./transport.js";
|
|
10
9
|
import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
@@ -88,9 +87,9 @@ function apply(ctx, config = {}) {
|
|
|
88
87
|
return [...currentConfig.providers.keys()];
|
|
89
88
|
}
|
|
90
89
|
/** Registration facts: routes with their display names and retry policies. */
|
|
91
|
-
function registrationFacts() {
|
|
92
|
-
return JSON.stringify(
|
|
93
|
-
const provider =
|
|
90
|
+
function registrationFacts(config = currentConfig) {
|
|
91
|
+
return JSON.stringify([...config.providers.keys()].map((route) => {
|
|
92
|
+
const provider = config.providers.get(route);
|
|
94
93
|
return {
|
|
95
94
|
route,
|
|
96
95
|
displayName: provider?.displayName,
|
|
@@ -150,15 +149,11 @@ function apply(ctx, config = {}) {
|
|
|
150
149
|
ctx.inject(["commands"], (commandsCtx) => {
|
|
151
150
|
commandDisposers = registerCommands(commandsCtx, {
|
|
152
151
|
catalog,
|
|
153
|
-
config: () => currentConfig,
|
|
154
152
|
describeCredential: async (route) => {
|
|
155
153
|
const provider = currentConfig.providers.get(route);
|
|
156
154
|
if (provider === void 0) return void 0;
|
|
157
155
|
return describeCredential(ctx, provider.apiKeyEnv);
|
|
158
|
-
}
|
|
159
|
-
storeApiKey: (value) => storeApiKey(ctx, currentConfig.providers.values(), value),
|
|
160
|
-
keyConfigured: () => keyConfigured(ctx, currentConfig.providers.values()),
|
|
161
|
-
keyReadonly: () => keyReadonly(ctx, currentConfig.providers.values())
|
|
156
|
+
}
|
|
162
157
|
});
|
|
163
158
|
});
|
|
164
159
|
let source = () => config;
|
|
@@ -172,7 +167,7 @@ function apply(ctx, config = {}) {
|
|
|
172
167
|
try {
|
|
173
168
|
const next = resolveConfig(source());
|
|
174
169
|
const catalogChanged = JSON.stringify(next.catalog) !== JSON.stringify(currentConfig.catalog);
|
|
175
|
-
const routesChanged = registrationFacts() !== registeredFacts;
|
|
170
|
+
const routesChanged = registrationFacts(next) !== registeredFacts;
|
|
176
171
|
currentConfig = next;
|
|
177
172
|
configRevision += 1;
|
|
178
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.
|
|
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. */
|