@askdkc/dsh-cli 0.12.3 → 0.13.1

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.
Files changed (165) hide show
  1. package/cordis.patch.yml +12 -131
  2. package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
  3. package/lib/types/adapter/channel/session-projection.js +0 -7
  4. package/lib/types/adapter/ports/channel-view.d.ts +1 -1
  5. package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
  6. package/lib/types/dsh-adapter/bundled-presets.d.ts +3 -8
  7. package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -1
  8. package/lib/types/dsh-adapter/bundled-presets.js +5 -20
  9. package/lib/types/dsh-adapter/channel/binding-events.d.ts.map +1 -1
  10. package/lib/types/dsh-adapter/channel/binding-events.js +2 -9
  11. package/lib/types/dsh-adapter/channel/external-commands.d.ts.map +1 -1
  12. package/lib/types/dsh-adapter/channel/external-commands.js +9 -33
  13. package/lib/types/dsh-adapter/channel/local-actions.d.ts +2 -1
  14. package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
  15. package/lib/types/dsh-adapter/channel/local-actions.js +6 -2
  16. package/lib/types/dsh-adapter/channel/model-actions.js +3 -3
  17. package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
  18. package/lib/types/dsh-adapter/channel/model-switch.js +0 -1
  19. package/lib/types/dsh-adapter/channel/permissions.d.ts +1 -6
  20. package/lib/types/dsh-adapter/channel/permissions.d.ts.map +1 -1
  21. package/lib/types/dsh-adapter/channel/permissions.js +2 -54
  22. package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
  23. package/lib/types/dsh-adapter/channel/projection.js +15 -70
  24. package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
  25. package/lib/types/dsh-adapter/channel/session-fork.js +2 -5
  26. package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
  27. package/lib/types/dsh-adapter/channel/session-metadata.js +1 -4
  28. package/lib/types/dsh-adapter/channel/session-resume.js +2 -2
  29. package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
  30. package/lib/types/dsh-adapter/channel/session-rewind.js +0 -1
  31. package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
  32. package/lib/types/dsh-adapter/channel/session-tree-actions.js +1 -2
  33. package/lib/types/dsh-adapter/channel/session-tree.d.ts.map +1 -1
  34. package/lib/types/dsh-adapter/channel/session-tree.js +9 -62
  35. package/lib/types/dsh-adapter/channel/skill-catalog.js +1 -1
  36. package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
  37. package/lib/types/dsh-adapter/channel/subagent-projection.js +2 -8
  38. package/lib/types/dsh-adapter/channel/transcript.d.ts +0 -16
  39. package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
  40. package/lib/types/dsh-adapter/channel/transcript.js +0 -43
  41. package/lib/types/dsh-adapter/channel.js +2 -2
  42. package/lib/types/dsh-adapter/compat/index.d.ts +5 -25
  43. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  44. package/lib/types/dsh-adapter/compat/index.js +5 -25
  45. package/lib/types/dsh-adapter/compat/liveSession.d.ts +12 -23
  46. package/lib/types/dsh-adapter/compat/liveSession.d.ts.map +1 -1
  47. package/lib/types/dsh-adapter/compat/liveSession.js +15 -68
  48. package/lib/types/dsh-adapter/compat/messages.d.ts +1 -3
  49. package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -1
  50. package/lib/types/dsh-adapter/compat/messages.js +2 -10
  51. package/lib/types/dsh-adapter/compat/persistence.d.ts +2 -3
  52. package/lib/types/dsh-adapter/compat/persistence.d.ts.map +1 -1
  53. package/lib/types/dsh-adapter/compat/persistence.js +9 -14
  54. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +10 -144
  55. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  56. package/lib/types/dsh-adapter/compat/sessionLog.js +45 -275
  57. package/lib/types/dsh-adapter/compat/settings.d.ts +4 -10
  58. package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -1
  59. package/lib/types/dsh-adapter/compat/settings.js +6 -23
  60. package/lib/types/dsh-adapter/compat/shell.d.ts +2 -3
  61. package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -1
  62. package/lib/types/dsh-adapter/compat/shell.js +2 -7
  63. package/lib/types/dsh-adapter/contract.d.ts +4 -34
  64. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  65. package/lib/types/dsh-adapter/contract.js +8 -83
  66. package/lib/types/dsh-adapter/packaged-presets.d.ts +0 -19
  67. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -1
  68. package/lib/types/dsh-adapter/packaged-presets.js +1 -83
  69. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  70. package/lib/types/dsh-adapter/plugin.js +26 -183
  71. package/lib/types/dsh-adapter/preset-resolution.d.ts +1 -22
  72. package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
  73. package/lib/types/dsh-adapter/preset-resolution.js +0 -34
  74. package/lib/types/dsh-adapter/presets.d.ts +1 -4
  75. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  76. package/lib/types/dsh-adapter/presets.js +3 -6
  77. package/lib/types/dsh-adapter/questions-answerer.d.ts +5 -43
  78. package/lib/types/dsh-adapter/questions-answerer.d.ts.map +1 -1
  79. package/lib/types/dsh-adapter/questions-answerer.js +7 -63
  80. package/lib/types/dsh-adapter/sessionTree.d.ts +0 -19
  81. package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
  82. package/lib/types/dsh-adapter/sessionTree.js +2 -112
  83. package/lib/types/dsh-adapter/sessions/header.d.ts +2 -2
  84. package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -1
  85. package/lib/types/dsh-adapter/sessions/header.js +2 -8
  86. package/lib/types/dsh-adapter/sessions/list.d.ts +7 -28
  87. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -1
  88. package/lib/types/dsh-adapter/sessions/list.js +48 -103
  89. package/lib/types/dsh-adapter/sessions/store.d.ts +1 -2
  90. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -1
  91. package/lib/types/dsh-adapter/sessions/store.js +7 -10
  92. package/lib/types/dsh-adapter/sharp.d.ts.map +1 -1
  93. package/lib/types/dsh-adapter/sharp.js +0 -1
  94. package/lib/types/dsh-adapter/subagents.d.ts +1 -2
  95. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
  96. package/lib/types/dsh-adapter/subagents.js +1 -12
  97. package/lib/types/dsh-adapter/trajectory/aggregate.d.ts +1 -1
  98. package/lib/types/dsh-adapter/trajectory/aggregate.js +1 -1
  99. package/lib/types/dsh-adapter/trajectory/guards.d.ts +5 -22
  100. package/lib/types/dsh-adapter/trajectory/guards.d.ts.map +1 -1
  101. package/lib/types/dsh-adapter/trajectory/guards.js +5 -23
  102. package/lib/types/dsh-adapter/trajectory/projection.d.ts +5 -21
  103. package/lib/types/dsh-adapter/trajectory/projection.d.ts.map +1 -1
  104. package/lib/types/dsh-adapter/trajectory/projection.js +4 -38
  105. package/lib/types/dsh-adapter/trajectory/types.d.ts +1 -1
  106. package/lib/types/dsh-adapter/trajectory/types.d.ts.map +1 -1
  107. package/lib/types/i18n.d.ts +2 -22
  108. package/lib/types/i18n.d.ts.map +1 -1
  109. package/lib/types/i18n.js +2 -10
  110. package/lib/types/presetPrefs.d.ts +1 -3
  111. package/lib/types/presetPrefs.d.ts.map +1 -1
  112. package/lib/types/presetPrefs.js +2 -4
  113. package/lib/types/sessionHistory.d.ts +2 -0
  114. package/lib/types/sessionHistory.d.ts.map +1 -1
  115. package/lib/types/sessionHistory.js +8 -0
  116. package/node_modules/@askdkc/dsh-auth/README.ja.md +28 -9
  117. package/node_modules/@askdkc/dsh-auth/README.md +19 -8
  118. package/node_modules/@askdkc/dsh-auth/dsh-plugin.json +3 -3
  119. package/node_modules/@askdkc/dsh-auth/lib/THIRD_PARTY_NOTICES.txt +168 -0
  120. package/node_modules/@askdkc/dsh-auth/lib/abortable.d.ts +2 -0
  121. package/node_modules/@askdkc/dsh-auth/lib/abortable.js +9 -0
  122. package/node_modules/@askdkc/dsh-auth/lib/auth-contract.d.ts +66 -0
  123. package/node_modules/@askdkc/dsh-auth/lib/auth-contract.js +1 -0
  124. package/node_modules/@askdkc/dsh-auth/lib/command.js +15 -1
  125. package/node_modules/@askdkc/dsh-auth/lib/credentials.d.ts +8 -14
  126. package/node_modules/@askdkc/dsh-auth/lib/credentials.js +1 -1
  127. package/node_modules/@askdkc/dsh-auth/lib/index.d.ts +8 -28
  128. package/node_modules/@askdkc/dsh-auth/lib/index.js +67 -65
  129. package/node_modules/@askdkc/dsh-auth/lib/interaction.d.ts +5 -5
  130. package/node_modules/@askdkc/dsh-auth/lib/opencode-adapter.d.ts +37 -0
  131. package/node_modules/@askdkc/dsh-auth/lib/opencode-adapter.js +206 -0
  132. package/node_modules/@askdkc/dsh-auth/lib/opencode-catalog.d.ts +72 -0
  133. package/node_modules/@askdkc/dsh-auth/lib/opencode-catalog.js +252 -0
  134. package/node_modules/@askdkc/dsh-auth/lib/opencode-codec.d.ts +6 -0
  135. package/node_modules/@askdkc/dsh-auth/lib/opencode-codec.js +93 -0
  136. package/node_modules/@askdkc/dsh-auth/lib/opencode-owned.generated.d.ts +113 -0
  137. package/node_modules/@askdkc/dsh-auth/lib/opencode-owned.generated.js +2866 -0
  138. package/node_modules/@askdkc/dsh-auth/lib/opencode-replay.d.ts +15 -0
  139. package/node_modules/@askdkc/dsh-auth/lib/opencode-replay.js +141 -0
  140. package/node_modules/@askdkc/dsh-auth/lib/opencode-sdk.d.ts +5 -0
  141. package/node_modules/@askdkc/dsh-auth/lib/opencode-sdk.js +43465 -0
  142. package/node_modules/@askdkc/dsh-auth/lib/opencode-sdk.js.map +7 -0
  143. package/node_modules/@askdkc/dsh-auth/lib/opencode-transport.d.ts +5 -0
  144. package/node_modules/@askdkc/dsh-auth/lib/opencode-transport.js +69 -0
  145. package/node_modules/@askdkc/dsh-auth/lib/pi-routes.d.ts +32 -0
  146. package/node_modules/@askdkc/dsh-auth/lib/pi-routes.js +45 -0
  147. package/node_modules/@askdkc/dsh-auth/lib/profiles.d.ts +2 -20
  148. package/node_modules/@askdkc/dsh-auth/lib/profiles.js +6 -41
  149. package/node_modules/@askdkc/dsh-auth/lib/routes.d.ts +20 -0
  150. package/node_modules/@askdkc/dsh-auth/lib/routes.js +13 -0
  151. package/node_modules/@askdkc/dsh-auth/lib/service.d.ts +11 -2
  152. package/node_modules/@askdkc/dsh-auth/lib/service.js +26 -16
  153. package/node_modules/@askdkc/dsh-auth/package.json +26 -15
  154. package/package.json +100 -96
  155. package/lib/types/dsh-adapter/providerGuard.d.ts +0 -75
  156. package/lib/types/dsh-adapter/providerGuard.d.ts.map +0 -1
  157. package/lib/types/dsh-adapter/providerGuard.js +0 -104
  158. package/lib/types/migrationPrefs.d.ts +0 -60
  159. package/lib/types/migrationPrefs.d.ts.map +0 -1
  160. package/lib/types/migrationPrefs.js +0 -97
  161. package/node_modules/@askdkc/dsh-auth/lib/fresh-models.d.ts +0 -59
  162. package/node_modules/@askdkc/dsh-auth/lib/fresh-models.js +0 -246
  163. package/node_modules/@askdkc/dsh-auth/lib/opencode-snapshot.generated.d.ts +0 -321
  164. package/node_modules/@askdkc/dsh-auth/lib/opencode-snapshot.generated.js +0 -528
  165. package/presets/liangshen/.dsh-tui-managed.json +0 -5
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * dsh-auth — provider sign-in as LLM provider routes.
3
3
  *
4
- * One cordis plugin mounts pi-ai catalog and custom provider profiles as
4
+ * One cordis plugin mounts owned OpenCode routes and legacy pi profiles as
5
5
  * `llm` registry routes, so
6
- * catalog models appear in every model picker after sign-in. `PiAiAdapter` runs with this
6
+ * catalog models appear in every model picker after sign-in. OpenCode owns
7
+ * its public catalog and bundled SDK transports. Other routes use `PiAiAdapter` with this
7
8
  * plugin's file-backed pi-ai `CredentialStore` injected
8
9
  * (`PiAiAuthInjection`): requests resolve stored credentials through the
9
10
  * provider's own auth or an explicit API key. Refreshes hold the store lock.
@@ -31,14 +32,12 @@
31
32
  *
32
33
  * @module dsh-auth
33
34
  */
34
- import { existsSync } from 'node:fs';
35
- import { homedir } from 'node:os';
36
- import { join } from 'node:path';
37
35
  import z from '@deepseek-ai/schemastery';
38
- import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai';
39
36
  import { CredentialFile, defaultCredentialsFile } from './credentials.js';
40
- import { buildOAuthProfile, AUTH_PROVIDER_IDS, canonicalProvider } from './profiles.js';
41
- import { createCustomProfile, CUSTOM_PROVIDER_IDS } from './custom-profiles.js';
37
+ import { AUTH_PROVIDER_IDS, canonicalProvider } from './routes.js';
38
+ import { OpenCodeAdapter } from './opencode-adapter.js';
39
+ import { OpenCodeCatalog, OPEN_CODE_ROUTES } from './opencode-catalog.js';
40
+ import { OPEN_CODE_SNAPSHOTS } from './opencode-owned.generated.js';
42
41
  import { refreshNous } from './nous-oauth.js';
43
42
  import { createDshAuthApi, DshAuthService } from './service.js';
44
43
  import { createAuthCommandHandler } from './command.js';
@@ -67,49 +66,11 @@ export { createDshAuthApi } from './service.js';
67
66
  export { QuestionBridge, describeEvent } from './interaction.js';
68
67
  export { copyToClipboard, openInBrowser, openerFor } from './opener.js';
69
68
  export { CredentialFile, defaultCredentialsFile } from './credentials.js';
70
- export { OAUTH_PROVIDER_IDS, CATALOG_PROVIDER_IDS, AUTH_PROVIDER_IDS, canonicalProvider, buildOAuthProfile } from './profiles.js';
71
- export { freshRosterIds } from './fresh-models.js';
72
- /**
73
- * The ambient auth context providers may consult while resolving their own
74
- * auth. `env()` answers from the process environment; `fileExists()` answers
75
- * about the host process's filesystem (the paths a provider asks about —
76
- * `~/.aws/credentials` and friends — are facts about where this process
77
- * runs, not about the project under edit).
78
- */
79
- function hostAuthContext() {
80
- return {
81
- env: async (name) => process.env[name],
82
- fileExists: path => Promise.resolve(path.startsWith('~/') ? existsSync(join(homedir(), path.slice(2))) : existsSync(path)),
83
- };
84
- }
85
- /**
86
- * The adapter the plugin registers: a {@link PiAiAdapter} whose *advisory
87
- * catalog* is credential-gated. A provider with no stored credential
88
- * lists no models — its rows never reach any model picker, which is the
89
- * whole point: picking a model that would only fail with "not signed in"
90
- * is noise. The gate only shapes `listModels`; `resolveModel` and requests
91
- * are untouched, so a model id already saved in a session (or named
92
- * explicitly) keeps resolving exactly as the registry contract promises
93
- * ("advisory and never changes routing").
94
- *
95
- * An expired-but-stored credential still lists: token refresh runs on the
96
- * next request, and a picker that hid a refreshable provider would look
97
- * signed-out when it is not.
98
- */
99
- export class CredentialGatedAdapter extends PiAiAdapter {
100
- hasCredential;
101
- constructor(options, hasCredential) {
102
- super(options);
103
- this.hasCredential = hasCredential;
104
- }
105
- async listModels(provider) {
106
- if (!(await this.hasCredential(provider)))
107
- return [];
108
- return super.listModels(provider);
109
- }
110
- }
69
+ export { OAUTH_PROVIDER_IDS, CATALOG_PROVIDER_IDS, AUTH_PROVIDER_IDS, canonicalProvider } from './routes.js';
70
+ export { OpenCodeAdapter } from './opencode-adapter.js';
71
+ export { OpenCodeCatalog } from './opencode-catalog.js';
111
72
  /** Mount the routes, the service, and the command. */
112
- export function apply(ctx, config) {
73
+ export async function apply(ctx, config) {
113
74
  const configured = (config.providers ?? [...AUTH_PROVIDER_IDS]).map(canonicalProvider);
114
75
  const unknown = configured.filter(id => !AUTH_PROVIDER_IDS.includes(id));
115
76
  if (unknown.length > 0 || configured.length === 0) {
@@ -123,14 +84,35 @@ export function apply(ctx, config) {
123
84
  if (unknownOverridden.length > 0) {
124
85
  throw new Error(`dsh-auth: modelOverrides names provider "${unknownOverridden[0]}", which is not among the mounted providers [${configured.join(', ')}]`);
125
86
  }
126
- // Profile construction validates the installed catalog loudly: a pi-ai
127
- // downgrade that dropped a provider fails the boot that asked for it, and
128
- // a per-model miss is refused per route (see buildOAuthProfile).
129
- const custom = new Map(CUSTOM_PROVIDER_IDS.filter(id => configured.includes(id))
130
- .map(id => [id, createCustomProfile(id, overrides[id])]));
131
- const profiles = new Map(configured.map(id => [id,
132
- custom.get(id)?.profile ?? buildOAuthProfile(id, overrides[id]),
133
- ]));
87
+ const piIds = configured.filter(id => !OPEN_CODE_ROUTES.includes(id));
88
+ const custom = new Map();
89
+ const piProfiles = new Map();
90
+ const profiles = new Map();
91
+ let pi;
92
+ if (piIds.length) {
93
+ try {
94
+ pi = await import('./pi-routes.js');
95
+ }
96
+ catch {
97
+ ctx.logger.warn('dsh-auth: pi routes unavailable; OpenCode routes remain available');
98
+ }
99
+ if (pi)
100
+ for (const id of piIds) {
101
+ try {
102
+ const entry = pi.CUSTOM_PROVIDER_IDS.includes(id) ? pi.createCustomProfile(id, overrides[id]) : undefined;
103
+ if (entry)
104
+ custom.set(id, entry);
105
+ const profile = entry?.profile ?? pi.buildOAuthProfile(id, overrides[id]);
106
+ piProfiles.set(id, profile);
107
+ profiles.set(id, { provider: id, displayName: profile.displayName, oauth: profile.piProvider?.auth.oauth });
108
+ }
109
+ catch {
110
+ ctx.logger.warn(`dsh-auth: route ${id} unavailable; remaining routes stay mounted`);
111
+ }
112
+ }
113
+ }
114
+ for (const id of configured.filter(id => OPEN_CODE_ROUTES.includes(id)))
115
+ profiles.set(id, { provider: id, displayName: id === 'opencode' ? 'OpenCode Zen' : 'OpenCode Go' });
134
116
  const store = new CredentialFile(config.credentialsFile ?? defaultCredentialsFile());
135
117
  const credentialKey = async (provider) => {
136
118
  const current = await store.read(provider);
@@ -161,28 +143,34 @@ export function apply(ctx, config) {
161
143
  return false;
162
144
  }
163
145
  };
164
- const adapter = new CredentialGatedAdapter({
146
+ const adapter = pi ? new pi.CredentialGatedAdapter({
165
147
  // One immutable map for the plugin's lifetime: the snapshot memoizes on
166
148
  // identity, and route changes here always mean a plugin remount anyway.
167
- profiles: () => profiles,
149
+ profiles: () => piProfiles,
168
150
  // Explicit API keys cover catalog routes too. OAuth catalog routes still
169
151
  // resolve their grants through pi-ai's injected credential store.
170
152
  resolveApiKey: credentialKey,
171
153
  auth: {
172
154
  credentials: store,
173
- authContext: hostAuthContext(),
155
+ authContext: pi.hostAuthContext(),
174
156
  },
175
157
  resolveAttachments: () => ctx.get('attachments'),
176
158
  onReplayDegrade: ({ provider, model, reason }) => {
177
159
  ctx.logger.warn(`dsh-auth: replay state on assistant history for ${provider}/${model} degraded: ${reason}`);
178
160
  },
179
- }, hasCredential);
161
+ }, hasCredential) : undefined;
162
+ const catalogs = new Map();
163
+ for (const id of OPEN_CODE_ROUTES)
164
+ if (configured.includes(id))
165
+ catalogs.set(id, new OpenCodeCatalog(id, OPEN_CODE_SNAPSHOTS[id], { changed: () => service.api?.notifyModelsChanged?.(id) }));
166
+ const native = new OpenCodeAdapter({ catalogs, store, overrides, attachments: () => ctx.get('attachments'), warn: reason => ctx.logger.warn(reason) });
180
167
  // The service is a thin holder so UIs can find the api without importing
181
168
  // the plugin module; the defensive get-then-create matches how the TUI
182
169
  // mounts userQuestions.
183
170
  const service = ctx.get('dshAuth') ?? new DshAuthService(ctx);
184
171
  const api = createDshAuthApi({
185
172
  profiles,
173
+ catalogs,
186
174
  store,
187
175
  resolveAsk: () => {
188
176
  const questions = ctx.get('userQuestions');
@@ -191,6 +179,14 @@ export function apply(ctx, config) {
191
179
  logger: ctx.logger,
192
180
  nousClientId: config.nous?.clientId,
193
181
  credentialChanged: async (provider, credential) => {
182
+ if (catalogs.has(provider)) {
183
+ if (credential) {
184
+ const status = await catalogs.get(provider).refresh();
185
+ if (status.warning)
186
+ throw new Error(status.warning);
187
+ }
188
+ return;
189
+ }
194
190
  const entry = custom.get(provider);
195
191
  if (entry === undefined)
196
192
  return;
@@ -207,6 +203,8 @@ export function apply(ctx, config) {
207
203
  },
208
204
  });
209
205
  service.api = api;
206
+ for (const catalog of catalogs.values())
207
+ void catalog.start();
210
208
  for (const [provider, entry] of custom) {
211
209
  void credentialKey(provider).then(key => key === undefined ? undefined : entry.refresh(key))
212
210
  .then(count => { if (count !== undefined)
@@ -226,9 +224,11 @@ export function apply(ctx, config) {
226
224
  else {
227
225
  // Individual registrations: one conflicting route must not strand the
228
226
  // rest (the registry keeps the previous owner serving).
229
- for (const id of configured) {
227
+ for (const id of profiles.keys()) {
230
228
  try {
231
- releases.push(llm.registerAdapter([id], adapter));
229
+ const owned = catalogs.has(id) ? native : adapter;
230
+ if (owned)
231
+ releases.push(llm.registerAdapter([id], owned));
232
232
  }
233
233
  catch (error) {
234
234
  ctx.logger.error(`dsh-auth: route "${id}" was not registered: ${error instanceof Error ? error.message : String(error)} `
@@ -244,7 +244,7 @@ export function apply(ctx, config) {
244
244
  const handler = createAuthCommandHandler(api);
245
245
  releases.push(commands.register({
246
246
  name: 'auth',
247
- description: 'Provider authentication: status, login, logout',
247
+ description: 'Provider authentication and catalogs: status, login, logout, models, refresh',
248
248
  handler: invocation => {
249
249
  const operation = handler(invocation);
250
250
  active.add(operation);
@@ -257,6 +257,8 @@ export function apply(ctx, config) {
257
257
  // enter while already-started logins finish their final write.
258
258
  yield async () => { await Promise.allSettled([...active]); };
259
259
  yield () => {
260
+ for (const catalog of catalogs.values())
261
+ catalog.dispose();
260
262
  for (const release of releases)
261
263
  release();
262
264
  };
@@ -29,7 +29,7 @@
29
29
  * @module dsh-auth/interaction
30
30
  */
31
31
  import type { AskUserQuestionItem, AskUserQuestionAnswer } from '@deepseek-ai/dsh-user-questions';
32
- import type { PiAiAuthEvent, PiAiAuthInteraction, PiAiAuthPrompt } from './pi-ai.js';
32
+ import type { AuthEvent, AuthInteraction, AuthPrompt } from './auth-contract.js';
33
33
  /** The ask surface this bridge drives (usually `ctx.userQuestions.ask`). */
34
34
  export type AskFn = (request: {
35
35
  questions: AskUserQuestionItem[];
@@ -41,13 +41,13 @@ export interface QuestionBridgeHelpers {
41
41
  copyText?: (text: string) => Promise<boolean>;
42
42
  }
43
43
  /** Human-readable copy for one notify event. */
44
- export declare function describeEvent(event: PiAiAuthEvent): string;
44
+ export declare function describeEvent(event: AuthEvent): string;
45
45
  /**
46
46
  * The bridge for one login run. At most one waiting panel exists at a time;
47
47
  * `settle()` retires it when the flow ends (any outcome), so a completed or
48
48
  * failed login never leaves an interactive dead end on screen.
49
49
  */
50
- export declare class QuestionBridge implements PiAiAuthInteraction {
50
+ export declare class QuestionBridge implements AuthInteraction {
51
51
  private readonly ask;
52
52
  private readonly runAbort;
53
53
  readonly signal: AbortSignal;
@@ -56,8 +56,8 @@ export declare class QuestionBridge implements PiAiAuthInteraction {
56
56
  private waiting;
57
57
  private waitingClosed;
58
58
  constructor(ask: AskFn, runAbort: AbortController, helpers?: QuestionBridgeHelpers);
59
- prompt(prompt: PiAiAuthPrompt): Promise<string>;
60
- notify(event: PiAiAuthEvent): void;
59
+ prompt(prompt: AuthPrompt): Promise<string>;
60
+ notify(event: AuthEvent): void;
61
61
  /** Close the waiting panel (if any) and stop answering for this run. */
62
62
  settle(): Promise<void>;
63
63
  /**
@@ -0,0 +1,37 @@
1
+ /** Native DSH adapter for Zen/Go; catalog and wire dispatch never consult pi. */
2
+ import { LlmAdapter, type GenerateOptions, type StreamChunk, type PreparedAdapterCall, type LlmResolvedModelInfo } from '@deepseek-ai/dsh-llm';
3
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
4
+ import { type OpenCodeCatalog, type OpenCodeRoute } from './opencode-catalog.js';
5
+ import type { CredentialFile } from './credentials.js';
6
+ interface Options {
7
+ catalogs: ReadonlyMap<OpenCodeRoute, OpenCodeCatalog>;
8
+ store: CredentialFile;
9
+ attachments?: () => AttachmentStore | undefined;
10
+ fetcher?: typeof fetch;
11
+ overrides?: Record<string, Record<string, {
12
+ contextWindow?: number;
13
+ maxTokens?: number;
14
+ }>>;
15
+ warn?: (reason: string) => void;
16
+ }
17
+ export declare class OpenCodeAdapter extends LlmAdapter {
18
+ private readonly options;
19
+ constructor(options: Options);
20
+ providerInfo(provider: string): {
21
+ id: string;
22
+ name: string;
23
+ };
24
+ private descriptor;
25
+ listModels(provider: string): Promise<{
26
+ provider: string;
27
+ id: string;
28
+ name: string;
29
+ inputModalities: ("text" | "image")[];
30
+ }[]>;
31
+ private resolved;
32
+ resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
33
+ prepareCall(provider: string, model: string, signal?: AbortSignal): Promise<PreparedAdapterCall>;
34
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
35
+ private dispatch;
36
+ }
37
+ export {};
@@ -0,0 +1,206 @@
1
+ /** Native DSH adapter for Zen/Go; catalog and wire dispatch never consult pi. */
2
+ import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm';
3
+ import { encodePrompt } from './opencode-codec.js';
4
+ import { createTransport } from './opencode-transport.js';
5
+ import { abortable } from './abortable.js';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { record, mergeMetadata } from './opencode-replay.js';
8
+ export class OpenCodeAdapter extends LlmAdapter {
9
+ options;
10
+ constructor(options) {
11
+ super();
12
+ this.options = options;
13
+ for (const [provider, models] of Object.entries(options.overrides ?? {}))
14
+ for (const [id, override] of Object.entries(models)) {
15
+ if (!options.catalogs.has(provider))
16
+ continue;
17
+ this.descriptor(provider, id);
18
+ for (const value of Object.values(override))
19
+ if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0))
20
+ throw new Error(`Invalid model override for ${provider}/${id}`);
21
+ }
22
+ }
23
+ providerInfo(provider) { return { id: provider, name: provider === 'opencode' ? 'OpenCode Zen' : 'OpenCode Go' }; }
24
+ descriptor(provider, id) {
25
+ const catalog = this.options.catalogs.get(provider);
26
+ const found = catalog?.models().find(model => model.id === id);
27
+ if (!found)
28
+ throw new LlmError(`OpenCode model ${provider}/${id} unavailable: ${catalog?.status().excluded.find(model => model.id === id)?.reason ?? 'not on the verified roster'}`, 'MODEL_NOT_FOUND');
29
+ const override = this.options.overrides?.[provider]?.[id];
30
+ return { ...found, ...override };
31
+ }
32
+ async listModels(provider) {
33
+ if (!(await this.options.store.read(provider)))
34
+ return [];
35
+ return (this.options.catalogs.get(provider)?.models() ?? []).map(model => ({ provider, id: model.id, name: model.name, inputModalities: model.inputModalities }));
36
+ }
37
+ resolved(provider, descriptor) {
38
+ return { provider, id: descriptor.id, name: descriptor.name, inputModalities: descriptor.inputModalities, context: { contextWindow: descriptor.contextWindow }, defaultMaxTokens: descriptor.maxTokens,
39
+ ...(descriptor.efforts.length ? { reasoning: { efforts: descriptor.efforts.map(id => ({ id: id, name: id })) } } : {}) };
40
+ }
41
+ async resolveModel(provider, model, signal) { signal?.throwIfAborted(); return this.resolved(provider, this.descriptor(provider, model)); }
42
+ async prepareCall(provider, model, signal) {
43
+ signal?.throwIfAborted();
44
+ const descriptor = this.descriptor(provider, model);
45
+ return { model: this.resolved(provider, descriptor), stream: options => {
46
+ if (options.provider !== provider || options.model !== model)
47
+ throw new LlmError('Prepared OpenCode call identity mismatch', 'INVALID_PREPARED_CALL');
48
+ return this.dispatch(options, descriptor);
49
+ } };
50
+ }
51
+ stream(options) { return this.dispatch(options, this.descriptor(options.provider, options.model)); }
52
+ async *dispatch(options, descriptor) {
53
+ options.signal?.throwIfAborted();
54
+ if (options.tools?.length && !descriptor.toolCall)
55
+ throw new LlmError('This OpenCode model does not support tool calls', 'UNSUPPORTED_CAPABILITY');
56
+ if (options.reasoningEffort && !descriptor.efforts.includes(options.reasoningEffort))
57
+ throw new LlmError('Unsupported reasoning effort for this model', 'UNSUPPORTED_CAPABILITY');
58
+ const prompt = await encodePrompt(options, descriptor, this.options.attachments, this.options.warn);
59
+ const credential = await this.options.store.read(options.provider);
60
+ if (credential?.type !== 'api_key' || !credential.key?.trim())
61
+ throw new LlmError(`Run /auth login ${options.provider}`, 'AUTH');
62
+ const headers = { ...attributionHeaders(), 'x-opencode-session': options.sessionId ?? randomUUID() };
63
+ const model = createTransport(descriptor, options, credential.key, headers, prompt, this.options.fetcher);
64
+ const abort = new AbortController();
65
+ const signal = AbortSignal.any([abort.signal, ...(options.signal ? [options.signal] : [])]);
66
+ let timer = setTimeout(() => abort.abort(new Error('OpenCode stream idle timeout')), 300_000);
67
+ timer.unref();
68
+ let reader;
69
+ try {
70
+ const result = await abortable(model.doStream({ prompt, abortSignal: signal, maxOutputTokens: options.maxTokens ?? descriptor.maxTokens,
71
+ ...(descriptor.temperature ? { temperature: options.temperature } : {}), stopSequences: options.stop,
72
+ tools: options.tools?.map(tool => ({ type: 'function', name: tool.name, description: tool.description, inputSchema: tool.parameters })),
73
+ providerOptions: descriptor.protocol === 'responses' ? { openai: { store: false } } : undefined }), signal);
74
+ reader = result.stream.getReader();
75
+ const blocks = new Map();
76
+ let responseId;
77
+ while (true) {
78
+ const item = await abortable(reader.read(), signal);
79
+ if (item.done)
80
+ throw new LlmError('OpenCode stream ended without a finish event', 'PROTOCOL_ERROR');
81
+ clearTimeout(timer);
82
+ timer = setTimeout(() => abort.abort(new Error('OpenCode stream idle timeout')), 300_000);
83
+ timer.unref();
84
+ const part = item.value;
85
+ if (part.type === 'error')
86
+ throw part.error;
87
+ if (part.type === 'response-metadata') {
88
+ responseId = part.id;
89
+ continue;
90
+ }
91
+ if (part.type === 'text-start' || part.type === 'reasoning-start' || part.type === 'tool-input-start') {
92
+ const block = part.type === 'tool-input-start' ? { type: 'tool-call', id: part.id, name: part.toolName, arguments: '' } : { type: part.type === 'text-start' ? 'text' : 'reasoning', text: '' };
93
+ const entry = { index: blocks.size, block, ended: false, providerOptions: part.providerMetadata };
94
+ blocks.set(part.id, entry);
95
+ yield { type: 'block-start', index: entry.index, blockType: block.type };
96
+ continue;
97
+ }
98
+ if (part.type === 'text-delta' || part.type === 'reasoning-delta' || part.type === 'tool-input-delta') {
99
+ const entry = blocks.get(part.id);
100
+ if (!entry || entry.ended || entry.inputEnded)
101
+ throw new LlmError('Unmatched stream delta', 'PROTOCOL_ERROR');
102
+ if (entry.block.type === 'tool-call') {
103
+ entry.block.arguments += part.delta;
104
+ yield { type: 'tool-call-delta', index: entry.index, id: entry.block.id, argumentsDelta: part.delta };
105
+ }
106
+ else if (entry.block.type === 'text' || entry.block.type === 'reasoning') {
107
+ entry.block.text += part.delta;
108
+ yield { type: entry.block.type === 'text' ? 'text-delta' : 'reasoning-delta', index: entry.index, text: part.delta };
109
+ }
110
+ if (part.providerMetadata)
111
+ entry.providerOptions = mergeMetadata(entry.providerOptions, part.providerMetadata);
112
+ continue;
113
+ }
114
+ if (part.type === 'text-end' || part.type === 'reasoning-end' || part.type === 'tool-input-end') {
115
+ const entry = blocks.get(part.id);
116
+ if (!entry || entry.ended)
117
+ throw new LlmError('Unmatched stream end', 'PROTOCOL_ERROR');
118
+ if (part.providerMetadata)
119
+ entry.providerOptions = mergeMetadata(entry.providerOptions, part.providerMetadata);
120
+ if (entry.block.type === 'tool-call') {
121
+ entry.inputEnded = true;
122
+ continue;
123
+ }
124
+ entry.ended = true;
125
+ yield { type: 'block-end', index: entry.index, block: entry.block };
126
+ continue;
127
+ }
128
+ if (part.type === 'tool-call') {
129
+ if (part.providerExecuted)
130
+ throw new LlmError('Unexpected provider-executed tool', 'PROTOCOL_ERROR');
131
+ if (!blocks.has(part.toolCallId)) {
132
+ const block = { type: 'tool-call', id: part.toolCallId, name: part.toolName, arguments: part.input };
133
+ const index = blocks.size;
134
+ blocks.set(part.toolCallId, { index, block, ended: true, providerOptions: part.providerMetadata });
135
+ yield { type: 'block-start', index, blockType: 'tool-call' };
136
+ yield { type: 'block-end', index, block };
137
+ }
138
+ else {
139
+ const entry = blocks.get(part.toolCallId);
140
+ if (entry.block.type !== 'tool-call' || entry.ended || entry.block.name !== part.toolName)
141
+ throw new LlmError('Mismatched final tool call', 'PROTOCOL_ERROR');
142
+ // Some providers send the complete arguments only in the final
143
+ // tool event. Preserve streamed raw JSON when it was supplied.
144
+ if (!entry.block.arguments)
145
+ entry.block.arguments = part.input;
146
+ if (part.providerMetadata)
147
+ entry.providerOptions = mergeMetadata(entry.providerOptions, part.providerMetadata);
148
+ entry.ended = true;
149
+ yield { type: 'block-end', index: entry.index, block: entry.block };
150
+ }
151
+ continue;
152
+ }
153
+ if (part.type === 'finish') {
154
+ for (const entry of blocks.values())
155
+ if (!entry.ended) {
156
+ entry.ended = true;
157
+ yield { type: 'block-end', index: entry.index, block: entry.block };
158
+ }
159
+ const input = part.usage.inputTokens;
160
+ const output = part.usage.outputTokens;
161
+ // DSH snapshots chunks losslessly: optional SDK values must be absent,
162
+ // not own properties containing undefined. Preserve reported zeroes.
163
+ yield { type: 'usage', usage: {
164
+ inputTokens: input.noCache ?? Math.max(0, (input.total ?? 0) - (input.cacheRead ?? 0) - (input.cacheWrite ?? 0)),
165
+ outputTokens: output.total ?? 0,
166
+ ...(input.cacheRead !== undefined ? { cacheReadTokens: input.cacheRead } : {}),
167
+ ...(input.cacheWrite !== undefined ? { cacheWriteTokens: input.cacheWrite } : {}),
168
+ ...(output.reasoning !== undefined ? { reasoningTokens: output.reasoning } : {}),
169
+ ...(input.total !== undefined && output.total !== undefined ? { totalTokens: input.total + output.total } : {}),
170
+ } };
171
+ if (!blocks.size)
172
+ throw new LlmError('OpenCode returned no content', 'EMPTY_RESPONSE');
173
+ const finish = part.finishReason.unified;
174
+ if (finish === 'error' || finish === 'content-filter' || finish === 'other')
175
+ throw new LlmError(`OpenCode generation ended: ${finish}`, 'PROVIDER_ERROR');
176
+ yield { type: 'finish', reason: { kind: finish === 'tool-calls' ? 'tool-calls' : finish === 'length' ? 'max-tokens' : 'stop' }, replayState: { response: { kind: 'dsh-opencode', version: 1, protocol: descriptor.protocol, provider: options.provider, model: descriptor.id, ...(responseId !== undefined ? { responseId } : {}) }, blocks: [...blocks.values()].map(entry => ({ type: entry.block.type, ...(entry.providerOptions ? { providerOptions: entry.providerOptions } : {}) })) } };
177
+ return;
178
+ }
179
+ }
180
+ }
181
+ catch (error) {
182
+ if (options.signal?.aborted)
183
+ throw options.signal.reason;
184
+ if (signal.aborted)
185
+ throw new LlmError('OpenCode stream idle timeout', 'TIMEOUT');
186
+ if (error instanceof LlmError)
187
+ throw error;
188
+ const raw = record(error);
189
+ const status = typeof raw?.statusCode === 'number' ? raw.statusCode : undefined;
190
+ const responseHeaders = record(raw?.responseHeaders);
191
+ const retryHeader = responseHeaders?.['retry-after'];
192
+ const seconds = Number(retryHeader);
193
+ const retry = typeof retryHeader === 'string' ? Number.isFinite(seconds) ? seconds * 1000 : Date.parse(retryHeader) - Date.now() : undefined;
194
+ const requestId = responseHeaders?.['x-request-id'] ?? responseHeaders?.['request-id'];
195
+ throw new LlmError(`OpenCode request failed${status ? `: HTTP ${status}` : ''}`, status === 401 || status === 403 ? 'AUTH' : status === 429 ? 'RATE_LIMIT' : status && status >= 500 ? 'SERVER' : status ? 'PROVIDER_ERROR' : 'TRANSPORT', { status, ...(retry !== undefined && retry > 0 ? { providerRetryAfterMs: retry } : {}), ...(typeof requestId === 'string' && requestId.trim() ? { requestId: requestId } : {}) });
196
+ }
197
+ finally {
198
+ clearTimeout(timer);
199
+ abort.abort();
200
+ if (reader) {
201
+ void reader.cancel().catch(() => undefined);
202
+ reader.releaseLock();
203
+ }
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,72 @@
1
+ export declare const OPEN_CODE_ROUTES: readonly ["opencode", "opencode-go"];
2
+ export type OpenCodeRoute = typeof OPEN_CODE_ROUTES[number];
3
+ export type OpenCodeProtocol = 'messages' | 'responses' | 'chat' | 'google';
4
+ export declare const OPEN_CODE_URLS: Record<OpenCodeRoute, string>;
5
+ export interface OpenCodeModel {
6
+ id: string;
7
+ name: string;
8
+ protocol: OpenCodeProtocol;
9
+ contextWindow: number;
10
+ maxTokens: number;
11
+ inputModalities: ('text' | 'image')[];
12
+ toolCall: boolean;
13
+ reasoning: boolean;
14
+ temperature: boolean;
15
+ efforts: string[];
16
+ cost?: Record<string, number>;
17
+ }
18
+ export interface OpenCodeSnapshot {
19
+ version: 1;
20
+ provider: OpenCodeRoute;
21
+ fetchedAt: number;
22
+ roster: string[];
23
+ models: OpenCodeModel[];
24
+ excluded: {
25
+ id: string;
26
+ reason: string;
27
+ }[];
28
+ }
29
+ export interface OpenCodeCatalogStatus {
30
+ provider: OpenCodeRoute;
31
+ source: 'bundled' | 'cache' | 'live';
32
+ fetchedAt: number;
33
+ available: number;
34
+ excluded: readonly {
35
+ id: string;
36
+ reason: string;
37
+ }[];
38
+ warning?: string;
39
+ }
40
+ export declare function rosterIds(payload: unknown): string[];
41
+ /** Normalize only exact-route metadata; every roster id has an explicit outcome. */
42
+ export declare function normalizeCatalog(provider: OpenCodeRoute, roster: readonly string[], metadata: unknown, fetchedAt: number, previous?: OpenCodeSnapshot): OpenCodeSnapshot;
43
+ /** Validate cache/snapshot data using the same normalizer as fetched metadata. */
44
+ export declare function parseSnapshot(value: unknown, provider: OpenCodeRoute): OpenCodeSnapshot;
45
+ export declare function fetchCatalogJson(url: string, fetcher: typeof fetch, signal?: AbortSignal): Promise<unknown>;
46
+ export declare class OpenCodeCatalog {
47
+ readonly provider: OpenCodeRoute;
48
+ private readonly options;
49
+ private snapshot;
50
+ private source;
51
+ private warning?;
52
+ private pending?;
53
+ private disposed;
54
+ private abort;
55
+ private timer?;
56
+ private started;
57
+ constructor(provider: OpenCodeRoute, bundled: unknown, options?: {
58
+ directory?: string;
59
+ fetcher?: typeof fetch;
60
+ changed?: () => void;
61
+ now?: () => number;
62
+ });
63
+ private get path();
64
+ models(): readonly OpenCodeModel[];
65
+ status(): OpenCodeCatalogStatus;
66
+ start(): Promise<void>;
67
+ refresh(): Promise<OpenCodeCatalogStatus>;
68
+ private update;
69
+ /** Compare and replace under a bounded cross-process lock; failed writes retain the old cache. */
70
+ private save;
71
+ dispose(): void;
72
+ }