dsh-opencode 0.1.2 → 0.1.3

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 CHANGED
@@ -20,16 +20,22 @@ pnpm dsh plugin --profile web remove dsh-opencode
20
20
  ## 使い方
21
21
 
22
22
  1. DSH を再起動すると両ルートが登録される(以降のカタログ更新に再起動は不要)
23
- 2. Web 設定 > モデル > OpenCode Zen (Live) の「編集」を開き、マスク済み API キー入力欄にキーを貼り付けて保存
23
+ 2. `/dsh-opencode` を実行して API キーを設定する
24
+ - キー未設定ならコマンドの入力フィールドにキーを貼って再度実行
25
+ - DSH の opencode キー保存先(`OPENCODE_API_KEY`)に書き込み、設定や他の参照を変えていないか検証
24
26
  3. モデルセレクターから OpenCode のモデルを選んで使う
25
27
 
26
28
  | コマンド | 効果 |
27
29
  |---|---|
28
- | `/dsh-opencode` | ステータス表示とキー設定場所の案内 |
30
+ | `/dsh-opencode [<api-key>]` | API キーを保存して Zen/Go を有効化(未入力なら状態表示) |
29
31
  | `/opencode-refresh [all\|zen\|go]` | カタログを強制更新 |
30
32
  | `/opencode-status` | 更新時刻・エラー・モデル数・キー設定状況 |
31
33
  | `/opencode-models <zen\|go> [--all]` | モデル一覧(`--all` で非対応含む) |
32
34
 
35
+ - API キーはコマンドの入力フィールドで受け取り、`recordInput: false` でログに残さない
36
+ - 保存後、設定セクションと他の認証参照が変わっていないことを検証してから成功を報告する
37
+ - 環境変数 `OPENCODE_API_KEY`(両ルート既定参照)でも可
38
+
33
39
  ## 設定
34
40
 
35
41
  名前空間 `opencode-live`(デフォルトは `cordis.patch.yml` 参照):
@@ -48,7 +54,6 @@ catalog:
48
54
  requireFresh: false
49
55
  ```
50
56
 
51
- - API キーは設定 UI から保存(コマンド引数では受け付けない)。環境変数 `OPENCODE_API_KEY` でも可
52
57
  - `cachePath` でキャッシュ位置を変更可能(既定は DSH ホーム下 `cache/opencode-live/catalog.json`)
53
58
 
54
59
  ## 開発
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";
2
3
  //#region src/commands.ts
3
4
  const USAGE_REFRESH = "Usage: /opencode-refresh [all|zen|go]";
4
5
  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";
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,21 +130,35 @@ 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: "Store the OpenCode API key, or report status",
134
+ input: { hint: "Paste the OpenCode API key here (required only when unset)" },
134
135
  recordInput: false,
135
136
  handler: async (invocation) => {
136
- if (invocation.rawInput.trim().length > 0) return {
137
- 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
- };
140
137
  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 {
138
+ const key = invocation.rawInput.trim();
139
+ if (key.length > 0) {
140
+ if (invocation.signal.aborted) return {
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 {
144
157
  kind: "success",
145
158
  text: [
146
159
  "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.",
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."] : [],
148
162
  "This command never displays key values."
149
163
  ].join("\n")
150
164
  };
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ 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";
7
8
  import { registerCommands } from "./commands.js";
8
9
  import { buildRouteProvider } from "./transport.js";
9
10
  import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
@@ -154,7 +155,10 @@ function apply(ctx, config = {}) {
154
155
  const provider = currentConfig.providers.get(route);
155
156
  if (provider === void 0) return void 0;
156
157
  return describeCredential(ctx, provider.apiKeyEnv);
157
- }
158
+ },
159
+ storeApiKey: (value) => storeApiKey(ctx, currentConfig.providers.values(), value),
160
+ keyConfigured: () => keyConfigured(ctx, currentConfig.providers.values()),
161
+ keyReadonly: () => keyReadonly(ctx, currentConfig.providers.values())
158
162
  });
159
163
  });
160
164
  let source = () => config;
package/lib/keyring.js ADDED
@@ -0,0 +1,109 @@
1
+ import { SETTINGS_NAMESPACE } from "./index.js";
2
+ import { assertUsableApiKey } from "@deepseek-ai/dsh-llm";
3
+ //#region src/keyring.ts
4
+ /**
5
+ * Read one reference's presence, treating an absent credential service as a
6
+ * stable `false` rather than a throw: the caller wants facts, not a diagnosis.
7
+ */
8
+ async function refState(ctx, ref) {
9
+ const credentials = ctx.get("credentials");
10
+ if (credentials === void 0) return {
11
+ configured: false,
12
+ writable: false
13
+ };
14
+ const described = await credentials.describe(ref);
15
+ return described === void 0 ? {
16
+ configured: false,
17
+ writable: false
18
+ } : {
19
+ configured: described.configured,
20
+ writable: described.writable
21
+ };
22
+ }
23
+ /**
24
+ * Read the state a key store is about to operate against.
25
+ * @param ctx - the plugin context.
26
+ * @param providers - the currently resolved provider profiles.
27
+ * @returns the immutable snapshot for before/after comparison.
28
+ */
29
+ async function snapshotKeyring(ctx, providers) {
30
+ const settings = ctx.get("settings");
31
+ const sectionValue = settings === void 0 ? void 0 : settings.get(SETTINGS_NAMESPACE);
32
+ const refs = /* @__PURE__ */ new Map();
33
+ for (const { apiKeyEnv } of providers) if (!refs.has(apiKeyEnv)) refs.set(apiKeyEnv, await refState(ctx, apiKeyEnv));
34
+ return {
35
+ section: JSON.stringify(sectionValue ?? null),
36
+ refs
37
+ };
38
+ }
39
+ /**
40
+ * Verify a key store left no stray change. Compares the section snapshot and
41
+ * every reference the plugin knows with a stable JSON text, so a change in
42
+ * either is caught regardless of ordering.
43
+ * @param before - the snapshot taken before the store.
44
+ * @param after - the snapshot taken after the store.
45
+ * @param refs - the references the store wrote (the only ones allowed to move).
46
+ * @returns a failure line, or `undefined` when only the intended refs changed.
47
+ */
48
+ function verifyNoStrayChange(before, after, refs) {
49
+ if (before.section !== after.section) return `opencode-live: the settings section changed while storing the key (${SETTINGS_NAMESPACE})`;
50
+ for (const [ref, state] of before.refs) {
51
+ const next = after.refs.get(ref);
52
+ if (next === void 0) return `opencode-live: credential ${ref} vanished after storing the key`;
53
+ if (state.configured === next.configured && state.writable === next.writable) continue;
54
+ if (!refs.includes(ref)) return `opencode-live: an unrelated credential reference changed (${ref})`;
55
+ }
56
+ for (const [ref, next] of after.refs) {
57
+ if (before.refs.has(ref)) continue;
58
+ if (!refs.includes(ref)) return `opencode-live: an unrelated credential reference appeared (${ref})`;
59
+ if (!next.configured) return `opencode-live: the stored reference ${ref} did not become configured`;
60
+ }
61
+ }
62
+ /**
63
+ * Store one OpenCode key through the DSH credential seam.
64
+ *
65
+ * This is the whole write surface a command uses. Validation runs before any
66
+ * write, so a key that would be refused is never persisted. The write is
67
+ * fenced by `snapshotKeyring` before and after; a stray settings or unrelated
68
+ * credential change fails the call with a diagnostic instead of silently
69
+ * committing alongside the key.
70
+ * @param ctx - the plugin context.
71
+ * @param providers - the currently resolved provider profiles.
72
+ * @param value - the key typed into the command's input field.
73
+ * @returns `undefined` on success, or a failure line naming the exact problem.
74
+ */
75
+ async function storeApiKey(ctx, providers, value) {
76
+ try {
77
+ assertUsableApiKey(value, "opencode-live", "input");
78
+ } catch (error) {
79
+ return error.message;
80
+ }
81
+ const credentials = ctx.get("credentials");
82
+ if (credentials === void 0) return "opencode-live: no credentials service is mounted; export the API key in the launch environment instead";
83
+ const before = await snapshotKeyring(ctx, providers);
84
+ const targets = [...new Set([...providers].map(({ apiKeyEnv }) => apiKeyEnv))];
85
+ try {
86
+ for (const ref of targets) await credentials.set(ref, value);
87
+ } catch (error) {
88
+ return `opencode-live: could not store the API key (${error.message})`;
89
+ }
90
+ return verifyNoStrayChange(before, await snapshotKeyring(ctx, providers), targets);
91
+ }
92
+ /**
93
+ * Presence facts for the status path: whether a key is configured, without
94
+ * ever exposing a value.
95
+ * @param ctx - the plugin context.
96
+ * @param providers - the currently resolved provider profiles.
97
+ * @returns whether the shared reference is configured.
98
+ */
99
+ async function keyConfigured(ctx, providers) {
100
+ const refs = [...new Set([...providers].map(({ apiKeyEnv }) => apiKeyEnv))];
101
+ return (await Promise.all(refs.map((ref) => refState(ctx, ref)))).some((state) => state.configured);
102
+ }
103
+ /** Whether any reference is read-only, so the status can say so. */
104
+ async function keyReadonly(ctx, providers) {
105
+ const refs = [...new Set([...providers].map(({ apiKeyEnv }) => apiKeyEnv))];
106
+ return (await Promise.all(refs.map((ref) => refState(ctx, ref)))).some((state) => !state.writable);
107
+ }
108
+ //#endregion
109
+ export { keyConfigured, keyReadonly, snapshotKeyring, storeApiKey, verifyNoStrayChange };
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.2";
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. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-opencode",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Out-of-tree DSH bundle with live dynamic OpenCode Zen / Go model catalogs",
5
5
  "private": false,
6
6
  "license": "MIT",