@agi-fans/dsh-tui 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/lib/CHANGELOG.md +249 -3
  2. package/lib/chrome/box.d.ts +47 -16
  3. package/lib/chrome/box.js +172 -130
  4. package/lib/chrome/box.js.map +1 -1
  5. package/lib/chrome/code-highlight.d.ts +15 -0
  6. package/lib/chrome/code-highlight.js +60 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +29 -2
  9. package/lib/chrome/command-output.js +100 -4
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/main-screen-renderer.d.ts +18 -0
  12. package/lib/chrome/main-screen-renderer.js +58 -5
  13. package/lib/chrome/main-screen-renderer.js.map +1 -1
  14. package/lib/chrome/markdown.js +30 -10
  15. package/lib/chrome/markdown.js.map +1 -1
  16. package/lib/chrome/relative-time.d.ts +17 -0
  17. package/lib/chrome/relative-time.js +36 -0
  18. package/lib/chrome/relative-time.js.map +1 -1
  19. package/lib/chrome/renderer.d.ts +9 -0
  20. package/lib/chrome/renderer.js +1 -1
  21. package/lib/chrome/renderer.js.map +1 -1
  22. package/lib/chrome/secret-mask.d.ts +35 -0
  23. package/lib/chrome/secret-mask.js +180 -0
  24. package/lib/chrome/secret-mask.js.map +1 -0
  25. package/lib/chrome/status-config.d.ts +161 -51
  26. package/lib/chrome/status-config.js +237 -126
  27. package/lib/chrome/status-config.js.map +1 -1
  28. package/lib/chrome/status-line.d.ts +3 -2
  29. package/lib/chrome/status-line.js +105 -63
  30. package/lib/chrome/status-line.js.map +1 -1
  31. package/lib/chrome/terminal-colors.d.ts +85 -0
  32. package/lib/chrome/terminal-colors.js +230 -0
  33. package/lib/chrome/terminal-colors.js.map +1 -0
  34. package/lib/chrome/terminal-images.d.ts +9 -1
  35. package/lib/chrome/terminal-images.js +27 -4
  36. package/lib/chrome/terminal-images.js.map +1 -1
  37. package/lib/chrome/theme.d.ts +32 -9
  38. package/lib/chrome/theme.js +616 -520
  39. package/lib/chrome/theme.js.map +1 -1
  40. package/lib/chrome/tool-args.d.ts +1 -1
  41. package/lib/chrome/tool-args.js +3 -1
  42. package/lib/chrome/tool-args.js.map +1 -1
  43. package/lib/chrome/tool-renderers.d.ts +2 -0
  44. package/lib/chrome/tool-renderers.js +69 -3
  45. package/lib/chrome/tool-renderers.js.map +1 -1
  46. package/lib/chrome/tools-list.d.ts +1 -4
  47. package/lib/chrome/tools-list.js +1 -41
  48. package/lib/chrome/tools-list.js.map +1 -1
  49. package/lib/chrome/welcome-tips.d.ts +20 -7
  50. package/lib/chrome/welcome-tips.js +22 -41
  51. package/lib/chrome/welcome-tips.js.map +1 -1
  52. package/lib/commands/auth-ids.d.ts +25 -0
  53. package/lib/commands/auth-ids.js +27 -0
  54. package/lib/commands/auth-ids.js.map +1 -0
  55. package/lib/commands/auth-input.d.ts +34 -0
  56. package/lib/commands/auth-input.js +96 -0
  57. package/lib/commands/auth-input.js.map +1 -0
  58. package/lib/commands/auth-providers.d.ts +39 -0
  59. package/lib/commands/auth-providers.js +125 -0
  60. package/lib/commands/auth-providers.js.map +1 -0
  61. package/lib/commands/auth.d.ts +16 -22
  62. package/lib/commands/auth.js +346 -299
  63. package/lib/commands/auth.js.map +1 -1
  64. package/lib/commands/btw.d.ts +27 -0
  65. package/lib/commands/btw.js +59 -0
  66. package/lib/commands/btw.js.map +1 -0
  67. package/lib/commands/diff.js +30 -5
  68. package/lib/commands/diff.js.map +1 -1
  69. package/lib/commands/export.d.ts +1 -1
  70. package/lib/commands/export.js +5 -3
  71. package/lib/commands/export.js.map +1 -1
  72. package/lib/commands/files.d.ts +17 -1
  73. package/lib/commands/files.js +69 -61
  74. package/lib/commands/files.js.map +1 -1
  75. package/lib/commands/integrations.js +18 -6
  76. package/lib/commands/integrations.js.map +1 -1
  77. package/lib/commands/jobs.d.ts +19 -4
  78. package/lib/commands/jobs.js +50 -7
  79. package/lib/commands/jobs.js.map +1 -1
  80. package/lib/commands/model.js +109 -76
  81. package/lib/commands/model.js.map +1 -1
  82. package/lib/commands/permission.js +46 -13
  83. package/lib/commands/permission.js.map +1 -1
  84. package/lib/commands/plugins.js +196 -42
  85. package/lib/commands/plugins.js.map +1 -1
  86. package/lib/commands/schedule.d.ts +29 -0
  87. package/lib/commands/schedule.js +228 -0
  88. package/lib/commands/schedule.js.map +1 -0
  89. package/lib/commands/session-configuration.d.ts +1 -1
  90. package/lib/commands/session-configuration.js +29 -28
  91. package/lib/commands/session-configuration.js.map +1 -1
  92. package/lib/commands/session.js +75 -80
  93. package/lib/commands/session.js.map +1 -1
  94. package/lib/commands/terminal.d.ts +24 -4
  95. package/lib/commands/terminal.js +38 -94
  96. package/lib/commands/terminal.js.map +1 -1
  97. package/lib/commands/trajectory.js +4 -1
  98. package/lib/commands/trajectory.js.map +1 -1
  99. package/lib/definition.d.ts +150 -11
  100. package/lib/index.d.ts +1 -0
  101. package/lib/index.js +1 -0
  102. package/lib/index.js.map +1 -1
  103. package/lib/input/actions.d.ts +51 -0
  104. package/lib/input/actions.js +120 -0
  105. package/lib/input/actions.js.map +1 -0
  106. package/lib/input/editor-discovery.js +2 -2
  107. package/lib/input/editor-discovery.js.map +1 -1
  108. package/lib/input/editor.d.ts +0 -10
  109. package/lib/input/editor.js +11 -9
  110. package/lib/input/editor.js.map +1 -1
  111. package/lib/input/image-paste.d.ts +19 -1
  112. package/lib/input/image-paste.js +82 -1
  113. package/lib/input/image-paste.js.map +1 -1
  114. package/lib/input/keybindings-config.d.ts +15 -4
  115. package/lib/input/keybindings-config.js +57 -35
  116. package/lib/input/keybindings-config.js.map +1 -1
  117. package/lib/input/keys.d.ts +18 -0
  118. package/lib/input/keys.js +48 -3
  119. package/lib/input/keys.js.map +1 -1
  120. package/lib/input/wheel-scroll.d.ts +46 -0
  121. package/lib/input/wheel-scroll.js +69 -0
  122. package/lib/input/wheel-scroll.js.map +1 -0
  123. package/lib/prompt-intents.d.ts +182 -0
  124. package/lib/prompt-intents.js +127 -0
  125. package/lib/prompt-intents.js.map +1 -0
  126. package/lib/runtime/agent-profile.d.ts +5 -0
  127. package/lib/runtime/agent-profile.js +12 -0
  128. package/lib/runtime/agent-profile.js.map +1 -1
  129. package/lib/runtime/approval-rules.d.ts +59 -0
  130. package/lib/runtime/approval-rules.js +179 -0
  131. package/lib/runtime/approval-rules.js.map +1 -0
  132. package/lib/runtime/btw-policy.d.ts +7 -0
  133. package/lib/runtime/btw-policy.js +18 -0
  134. package/lib/runtime/btw-policy.js.map +1 -0
  135. package/lib/runtime/btw.d.ts +47 -0
  136. package/lib/runtime/btw.js +200 -0
  137. package/lib/runtime/btw.js.map +1 -0
  138. package/lib/runtime/cancellable.d.ts +75 -0
  139. package/lib/runtime/cancellable.js +76 -0
  140. package/lib/runtime/cancellable.js.map +1 -0
  141. package/lib/runtime/composer-images.js +5 -4
  142. package/lib/runtime/composer-images.js.map +1 -1
  143. package/lib/runtime/composer-shell.d.ts +30 -0
  144. package/lib/runtime/composer-shell.js +150 -0
  145. package/lib/runtime/composer-shell.js.map +1 -0
  146. package/lib/runtime/file-recovery-local.d.ts +73 -0
  147. package/lib/runtime/file-recovery-local.js +351 -0
  148. package/lib/runtime/file-recovery-local.js.map +1 -0
  149. package/lib/runtime/file-recovery-recording.d.ts +23 -0
  150. package/lib/runtime/file-recovery-recording.js +131 -0
  151. package/lib/runtime/file-recovery-recording.js.map +1 -0
  152. package/lib/runtime/file-recovery-review.d.ts +3 -0
  153. package/lib/runtime/file-recovery-review.js +130 -0
  154. package/lib/runtime/file-recovery-review.js.map +1 -0
  155. package/lib/runtime/file-recovery-store.d.ts +83 -0
  156. package/lib/runtime/file-recovery-store.js +329 -0
  157. package/lib/runtime/file-recovery-store.js.map +1 -0
  158. package/lib/runtime/file-recovery-worker.d.ts +22 -0
  159. package/lib/runtime/file-recovery-worker.js +153 -0
  160. package/lib/runtime/file-recovery-worker.js.map +1 -0
  161. package/lib/runtime/file-review.d.ts +6 -3
  162. package/lib/runtime/file-review.js +44 -7
  163. package/lib/runtime/file-review.js.map +1 -1
  164. package/lib/runtime/herdr-agent.d.ts +4 -13
  165. package/lib/runtime/herdr-agent.js +19 -63
  166. package/lib/runtime/herdr-agent.js.map +1 -1
  167. package/lib/runtime/plain-tui.d.ts +2 -0
  168. package/lib/runtime/plain-tui.js +7 -1
  169. package/lib/runtime/plain-tui.js.map +1 -1
  170. package/lib/runtime/plugin-settings.d.ts +25 -2
  171. package/lib/runtime/plugin-settings.js +329 -31
  172. package/lib/runtime/plugin-settings.js.map +1 -1
  173. package/lib/runtime/provider-local.d.ts +81 -24
  174. package/lib/runtime/provider-local.js +1807 -608
  175. package/lib/runtime/provider-local.js.map +1 -1
  176. package/lib/runtime/runner.js +52 -12
  177. package/lib/runtime/runner.js.map +1 -1
  178. package/lib/runtime/session-archive.d.ts +6 -0
  179. package/lib/runtime/session-archive.js +129 -3
  180. package/lib/runtime/session-archive.js.map +1 -1
  181. package/lib/runtime/session-runtime.js +1 -1
  182. package/lib/runtime/session-runtime.js.map +1 -1
  183. package/lib/runtime/settings-preferences.d.ts +5 -0
  184. package/lib/runtime/settings-preferences.js +34 -0
  185. package/lib/runtime/settings-preferences.js.map +1 -0
  186. package/lib/runtime/startup-notices.js +2 -0
  187. package/lib/runtime/startup-notices.js.map +1 -1
  188. package/lib/runtime/terminal-modes.d.ts +47 -0
  189. package/lib/runtime/terminal-modes.js +86 -0
  190. package/lib/runtime/terminal-modes.js.map +1 -0
  191. package/lib/runtime/terminal-notifications.d.ts +3 -5
  192. package/lib/runtime/terminal-notifications.js +4 -8
  193. package/lib/runtime/terminal-notifications.js.map +1 -1
  194. package/lib/runtime/terminal-probe.d.ts +36 -0
  195. package/lib/runtime/terminal-probe.js +37 -0
  196. package/lib/runtime/terminal-probe.js.map +1 -0
  197. package/lib/runtime/terminal-session.d.ts +57 -0
  198. package/lib/runtime/terminal-session.js +164 -0
  199. package/lib/runtime/terminal-session.js.map +1 -0
  200. package/lib/runtime/terminal-state.d.ts +29 -0
  201. package/lib/runtime/terminal-state.js +62 -0
  202. package/lib/runtime/terminal-state.js.map +1 -0
  203. package/lib/runtime/tool-presentation.js +50 -14
  204. package/lib/runtime/tool-presentation.js.map +1 -1
  205. package/lib/session/external-agents.d.ts +5 -0
  206. package/lib/session/external-agents.js +27 -0
  207. package/lib/session/external-agents.js.map +1 -0
  208. package/lib/session/feature-toggles.d.ts +1 -1
  209. package/lib/session/feature-toggles.js +1 -1
  210. package/lib/session/interaction-adapter.d.ts +1 -6
  211. package/lib/session/interaction-adapter.js +95 -18
  212. package/lib/session/interaction-adapter.js.map +1 -1
  213. package/lib/session/message-queue.js +1 -1
  214. package/lib/session/message-queue.js.map +1 -1
  215. package/lib/session/model-readiness.d.ts +16 -0
  216. package/lib/session/model-readiness.js +43 -0
  217. package/lib/session/model-readiness.js.map +1 -0
  218. package/lib/session/package-metadata.d.ts +8 -0
  219. package/lib/session/package-metadata.js +8 -0
  220. package/lib/session/package-metadata.js.map +1 -1
  221. package/lib/session/project-context.d.ts +5 -0
  222. package/lib/session/project-context.js +36 -0
  223. package/lib/session/project-context.js.map +1 -1
  224. package/lib/session/session-controller.d.ts +10 -13
  225. package/lib/session/session-controller.js +183 -98
  226. package/lib/session/session-controller.js.map +1 -1
  227. package/lib/session/session-library.js +2 -0
  228. package/lib/session/session-library.js.map +1 -1
  229. package/lib/session/session-tree.d.ts +3 -0
  230. package/lib/session/session-tree.js +3 -1
  231. package/lib/session/session-tree.js.map +1 -1
  232. package/lib/session/subagent-roster.d.ts +2 -1
  233. package/lib/session/subagent-roster.js +24 -3
  234. package/lib/session/subagent-roster.js.map +1 -1
  235. package/lib/session/subagent-tracker.d.ts +6 -2
  236. package/lib/session/subagent-tracker.js +49 -2
  237. package/lib/session/subagent-tracker.js.map +1 -1
  238. package/lib/session/tui-settings.d.ts +47 -13
  239. package/lib/session/tui-settings.js +76 -34
  240. package/lib/session/tui-settings.js.map +1 -1
  241. package/lib/session/working-directory.d.ts +4 -0
  242. package/lib/session/working-directory.js +4 -0
  243. package/lib/session/working-directory.js.map +1 -0
  244. package/lib/session/worktrees.d.ts +13 -0
  245. package/lib/session/worktrees.js +49 -0
  246. package/lib/session/worktrees.js.map +1 -0
  247. package/lib/views/approval-rule.d.ts +3 -0
  248. package/lib/views/approval-rule.js +4 -0
  249. package/lib/views/approval-rule.js.map +1 -0
  250. package/lib/views/at-complete.d.ts +8 -0
  251. package/lib/views/at-complete.js +64 -2
  252. package/lib/views/at-complete.js.map +1 -1
  253. package/lib/views/autocomplete.d.ts +22 -2
  254. package/lib/views/autocomplete.js +70 -40
  255. package/lib/views/autocomplete.js.map +1 -1
  256. package/lib/views/copy-targets.d.ts +13 -14
  257. package/lib/views/copy-targets.js +42 -54
  258. package/lib/views/copy-targets.js.map +1 -1
  259. package/lib/views/document-reader.js +18 -5
  260. package/lib/views/document-reader.js.map +1 -1
  261. package/lib/views/event-views.d.ts +1 -1
  262. package/lib/views/event-views.js +60 -6
  263. package/lib/views/event-views.js.map +1 -1
  264. package/lib/views/file-actions.d.ts +12 -0
  265. package/lib/views/file-actions.js +72 -0
  266. package/lib/views/file-actions.js.map +1 -0
  267. package/lib/views/history-search.js +15 -21
  268. package/lib/views/history-search.js.map +1 -1
  269. package/lib/views/hotkeys.d.ts +8 -1
  270. package/lib/views/hotkeys.js +53 -26
  271. package/lib/views/hotkeys.js.map +1 -1
  272. package/lib/views/list-navigation.d.ts +62 -0
  273. package/lib/views/list-navigation.js +73 -0
  274. package/lib/views/list-navigation.js.map +1 -0
  275. package/lib/views/list-view.d.ts +99 -0
  276. package/lib/views/list-view.js +165 -0
  277. package/lib/views/list-view.js.map +1 -0
  278. package/lib/views/prompt-selector.d.ts +34 -4
  279. package/lib/views/prompt-selector.js +270 -134
  280. package/lib/views/prompt-selector.js.map +1 -1
  281. package/lib/views/session-files.d.ts +1 -2
  282. package/lib/views/session-files.js +1 -11
  283. package/lib/views/session-files.js.map +1 -1
  284. package/lib/views/settings-list.d.ts +51 -10
  285. package/lib/views/settings-list.js +267 -315
  286. package/lib/views/settings-list.js.map +1 -1
  287. package/lib/views/trajectory-timeline.d.ts +72 -0
  288. package/lib/views/trajectory-timeline.js +133 -0
  289. package/lib/views/trajectory-timeline.js.map +1 -0
  290. package/lib/views/trajectory.d.ts +15 -4
  291. package/lib/views/trajectory.js +557 -123
  292. package/lib/views/trajectory.js.map +1 -1
  293. package/lib/views/transcript-export.js +9 -0
  294. package/lib/views/transcript-export.js.map +1 -1
  295. package/lib/views/transcript-render.d.ts +97 -25
  296. package/lib/views/transcript-render.js +542 -170
  297. package/lib/views/transcript-render.js.map +1 -1
  298. package/lib/views/transcript-search.d.ts +39 -6
  299. package/lib/views/transcript-search.js +46 -11
  300. package/lib/views/transcript-search.js.map +1 -1
  301. package/lib/views/transcript-types.d.ts +63 -5
  302. package/lib/views/transcript-types.js +23 -11
  303. package/lib/views/transcript-types.js.map +1 -1
  304. package/package.json +95 -65
  305. package/lib/commands/loop.d.ts +0 -52
  306. package/lib/commands/loop.js +0 -307
  307. package/lib/commands/loop.js.map +0 -1
  308. package/lib/commands/steer.d.ts +0 -5
  309. package/lib/commands/steer.js +0 -24
  310. package/lib/commands/steer.js.map +0 -1
  311. package/lib/runtime/terminal-reader.d.ts +0 -11
  312. package/lib/runtime/terminal-reader.js +0 -87
  313. package/lib/runtime/terminal-reader.js.map +0 -1
  314. package/lib/views/agent-hub.d.ts +0 -32
  315. package/lib/views/agent-hub.js +0 -206
  316. package/lib/views/agent-hub.js.map +0 -1
  317. package/lib/views/copy-selector.d.ts +0 -51
  318. package/lib/views/copy-selector.js +0 -139
  319. package/lib/views/copy-selector.js.map +0 -1
@@ -2,98 +2,26 @@
2
2
  import { spawn } from 'node:child_process';
3
3
  import process from 'node:process';
4
4
  import { AuthorizationError, } from '@deepseek-ai/dsh-authorization';
5
- import { credentialKey, credentialKeyId, credentialKeyScope, credentialRef, isCredentialKeySegment, parseCredentialKey, } from '@deepseek-ai/dsh-credentials';
5
+ import { credentialRef, } from '@deepseek-ai/dsh-credentials';
6
6
  import { createAuthorizationInteraction } from "../session/authorization-interaction.js";
7
+ import { APP_VERSION } from "../session/package-metadata.js";
7
8
  import { registerCommands } from "./registration.js";
9
+ import { authorizationFlows, catalogForRecord, catalogProvidersWithoutFlows, fallbackDeepSeekEntry, flowChoiceValue, isAccountFlow, isDeepSeek, isPiAiDeepSeekFlow, loginAuthorizationFlows, logoutableProviders, parseFlowChoice, profileApiKeyEnv, providerListId, } from "./auth-providers.js";
10
+ import { askPrompt } from "../prompt-intents.js";
11
+ import { CUSTOM_PROTOCOLS, CUSTOM_PROVIDER_VALUE, DEEPSEEK_API_KEYS_URL, DEEPSEEK_MODELS_URL, managedProviderCredentialRef, normalizeApiKey, normalizeDeepSeekApiKey, parseBaseURL, parseModelIds, parseProviderId, validateDeepSeekApiKey, } from "./auth-input.js";
12
+ export { CUSTOM_PROTOCOLS, CUSTOM_PROVIDER_VALUE, DEEPSEEK_API_KEYS_URL, DEEPSEEK_MODELS_URL, managedProviderCredentialRef, normalizeApiKey, normalizeDeepSeekApiKey, parseBaseURL, parseModelIds, parseProviderId, validateDeepSeekApiKey, };
8
13
  export const name = 'omdsh-command-auth';
9
14
  export const inject = ['commands', 'credentials', 'settings', 'tui', 'llm'];
10
15
  export const DEEPSEEK_PROVIDER = 'deepseek-official';
11
16
  export const DEEPSEEK_API_KEY = credentialRef('DEEPSEEK_API_KEY');
12
- export const OMDSH_DEEPSEEK_API_KEY = credentialRef('OMDSH_DEEPSEEK_API_KEY');
13
17
  export const DEEPSEEK_SETTINGS = 'llm-deepseek';
14
18
  export const PI_AI_SETTINGS = 'llm-pi-ai';
15
- export const DEEPSEEK_API_KEYS_URL = 'https://platform.deepseek.com/api_keys';
16
- export const DEEPSEEK_MODELS_URL = 'https://api.deepseek.com/v1/models';
17
- export const CUSTOM_PROVIDER_VALUE = '__omdsh_custom__';
19
+ /** Sentinel the provider picker returns for "sign in to the DeepSeek account". */
20
+ export const DEEPSEEK_ACCOUNT_CHOICE = '__omdsh_deepseek_account__';
21
+ /** Prefix a picker value carries when it names an authorization flow. */
18
22
  export const FLOW_VALUE_PREFIX = '__omdsh_flow__:';
19
- const PI_AI_RECORD_SCOPE = 'llm-pi-ai';
20
- const FULLSCREEN_CHOICE_THRESHOLD = 8;
23
+ /** Default when the credential store has nothing to say about a provider. */
21
24
  const UNCONFIGURED = { configured: false, writable: true };
22
- const PROVIDER_ID = /^[a-z][a-z0-9-]*$/u;
23
- const CUSTOM_PROTOCOLS = ['openai-completions', 'openai-responses', 'anthropic-messages'];
24
- /** Match DeepSeek's own login normalization without accepting an empty Bearer value. */
25
- export function normalizeDeepSeekApiKey(raw) {
26
- return normalizeApiKey(raw, 'DeepSeek');
27
- }
28
- /** Trim a pasted key and reject an empty Bearer value. */
29
- export function normalizeApiKey(raw, providerLabel) {
30
- const normalized = raw.trim().replace(/^bearer\b\s*/iu, '');
31
- if (normalized === '')
32
- throw new Error(`Paste a non-empty ${providerLabel} API key.`);
33
- return normalized;
34
- }
35
- /** omdsh-managed credential reference for one catalog provider route. */
36
- export function managedProviderCredentialRef(provider) {
37
- return credentialRef(`OMDSH_${provider.toUpperCase().replace(/[^A-Z0-9]+/gu, '_')}_API_KEY`);
38
- }
39
- /** Lowercase kebab-case route id used as the permanent provider key. */
40
- export function parseProviderId(raw) {
41
- const id = raw.trim().toLowerCase();
42
- if (!PROVIDER_ID.test(id)) {
43
- throw new Error('Provider id must start with a letter and use only lowercase letters, digits, and hyphens.');
44
- }
45
- if (id === 'custom' || id === CUSTOM_PROVIDER_VALUE) {
46
- throw new Error('Choose a different provider id.');
47
- }
48
- return id;
49
- }
50
- /** Absolute http(s) prefix the adapter will call. */
51
- export function parseBaseURL(raw) {
52
- const value = raw.trim().replace(/\/+$/u, '');
53
- let parsed;
54
- try {
55
- parsed = new URL(value);
56
- }
57
- catch {
58
- throw new Error('Base URL must be an absolute http or https URL.');
59
- }
60
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
61
- throw new Error('Base URL must be an http or https URL.');
62
- }
63
- return value;
64
- }
65
- /** One or more model ids from a comma- or whitespace-separated list. */
66
- export function parseModelIds(raw) {
67
- const ids = [...new Set(raw.split(/[,\s]+/u).map(item => item.trim()).filter(item => item !== ''))];
68
- if (ids.length === 0)
69
- throw new Error('Enter at least one model id.');
70
- return ids;
71
- }
72
- /** Validate a candidate without ever reading or returning a response body. */
73
- export async function validateDeepSeekApiKey(apiKey, signal, fetchImpl = globalThis.fetch) {
74
- let response;
75
- try {
76
- response = await fetchImpl(DEEPSEEK_MODELS_URL, {
77
- method: 'GET',
78
- headers: {
79
- accept: 'application/json',
80
- authorization: `Bearer ${apiKey}`,
81
- },
82
- signal,
83
- });
84
- }
85
- catch {
86
- if (signal.aborted)
87
- throw new Error('DeepSeek login was cancelled.');
88
- throw new Error('Could not reach DeepSeek to validate the API key. Check your network and try again.');
89
- }
90
- if (response.ok)
91
- return;
92
- if (response.status === 401 || response.status === 403) {
93
- throw new Error('DeepSeek rejected this API key. Copy a valid key from the DeepSeek dashboard and try again.');
94
- }
95
- throw new Error(`DeepSeek could not validate the API key (HTTP ${response.status}). Try again later.`);
96
- }
97
25
  function credentialSource(info) {
98
26
  if (info.source === 'env')
99
27
  return 'the current process environment';
@@ -115,123 +43,24 @@ function unmanagedCredentialMessage(ref, info, action) {
115
43
  : 'Update that source directly and restart omdsh.';
116
44
  return `${String(ref)} is supplied by ${credentialSource(info)} and /${action === 'replace' ? 'login' : 'logout'} cannot ${action} it. ${instruction}`;
117
45
  }
118
- function isDeepSeek(entry) {
119
- return entry.provider === DEEPSEEK_PROVIDER;
120
- }
121
- function isPiAiLoginable(entry) {
122
- return entry.settingsNs === 'llm-pi-ai' && entry.provider !== 'deepseek';
123
- }
124
- /** Picker labels are route ids; the official adapter keeps the DeepSeek product name. */
125
- export function providerListId(entry) {
126
- return isDeepSeek(entry) ? 'DeepSeek' : entry.provider;
127
- }
128
- function fallbackDeepSeekEntry() {
129
- return {
130
- provider: DEEPSEEK_PROVIDER,
131
- displayName: 'DeepSeek',
132
- settingsNs: 'llm-deepseek',
133
- settingsPath: [],
134
- };
135
- }
136
- function loginableProviders(ctx) {
137
- const listed = ctx.llm.listConfigurableProviders();
138
- const loginable = listed.filter(entry => isDeepSeek(entry) || isPiAiLoginable(entry));
139
- return loginable.length > 0 ? loginable : [fallbackDeepSeekEntry()];
140
- }
141
- function authorizationFlows(ctx) {
142
- return ctx.get('authorization')?.list() ?? [];
143
- }
144
- function isPiAiDeepSeekFlow(flow) {
145
- return credentialKeyScope(flow.key) === PI_AI_RECORD_SCOPE && credentialKeyId(flow.key) === 'deepseek';
146
- }
147
- function loginAuthorizationFlows(ctx) {
148
- return authorizationFlows(ctx).flatMap(flow => {
149
- if (!isPiAiDeepSeekFlow(flow))
150
- return [flow];
151
- const profile = walkPath(sectionValue(ctx, PI_AI_SETTINGS), ['providers', 'deepseek']);
152
- if (typeof profile !== 'object' || profile === null || Array.isArray(profile))
153
- return [];
154
- return [{ ...flow, label: 'DeepSeek (pi-ai compatibility)' }];
155
- });
156
- }
157
- function catalogAuthorizationFlow(ctx, provider) {
158
- const authorization = ctx.get('authorization');
159
- if (authorization === undefined || !isCredentialKeySegment(provider))
160
- return undefined;
161
- return authorization.describe(credentialKey(PI_AI_RECORD_SCOPE, provider));
162
- }
163
- function catalogProvidersWithoutFlows(ctx) {
164
- return loginableProviders(ctx).filter(entry => isDeepSeek(entry) || catalogAuthorizationFlow(ctx, entry.provider) === undefined);
165
- }
166
- function flowChoiceValue(key) {
167
- return FLOW_VALUE_PREFIX + key;
168
- }
169
- function parseFlowChoice(raw) {
170
- if (!raw.startsWith(FLOW_VALUE_PREFIX))
171
- return undefined;
172
- try {
173
- return parseCredentialKey(raw.slice(FLOW_VALUE_PREFIX.length));
174
- }
175
- catch {
176
- return undefined;
177
- }
178
- }
179
- function loginChoiceValues(entries, flows, includeCustom) {
46
+ function loginChoiceValues(entries, flows, includeCustom, includeAccount = false) {
180
47
  return [
48
+ ...(includeAccount ? [DEEPSEEK_ACCOUNT_CHOICE] : []),
181
49
  ...entries.map(entry => entry.provider),
182
50
  ...flows.map(flow => flowChoiceValue(flow.key)),
183
51
  ...(includeCustom ? [CUSTOM_PROVIDER_VALUE] : []),
184
52
  ];
185
53
  }
186
- function sectionValue(ctx, namespace) {
187
- // A provider's configured fields are the live volatile config of its own
188
- // profile entry, so the roster reads the same values a form would show.
189
- return ctx.settings.describe().find(row => String(row.ns) === namespace)?.value;
190
- }
191
- function walkPath(root, path) {
192
- let cursor = root;
193
- for (const key of path) {
194
- if (typeof cursor !== 'object' || cursor === null || Array.isArray(cursor))
195
- return undefined;
196
- cursor = cursor[key];
197
- }
198
- return cursor;
199
- }
200
- function profileApiKeyEnv(ctx, entry) {
201
- const profile = walkPath(sectionValue(ctx, entry.settingsNs), entry.settingsPath);
202
- if (isDeepSeek(entry)) {
203
- if (typeof profile !== 'object' || profile === null || Array.isArray(profile))
204
- return String(DEEPSEEK_API_KEY);
205
- const apiKeyEnv = profile['apiKeyEnv'];
206
- return typeof apiKeyEnv === 'string' && apiKeyEnv.trim() !== '' ? apiKeyEnv : String(DEEPSEEK_API_KEY);
207
- }
208
- if (typeof profile !== 'object' || profile === null || Array.isArray(profile))
209
- return undefined;
210
- const apiKeyEnv = profile['apiKeyEnv'];
211
- return typeof apiKeyEnv === 'string' && apiKeyEnv.trim() !== '' ? apiKeyEnv : undefined;
212
- }
213
- function hasStoredProfile(ctx, entry) {
214
- if (isDeepSeek(entry))
215
- return true;
216
- const profile = walkPath(sectionValue(ctx, entry.settingsNs), entry.settingsPath);
217
- return typeof profile === 'object' && profile !== null && !Array.isArray(profile);
218
- }
219
- function logoutableProviders(ctx) {
220
- return loginableProviders(ctx).filter(entry => isDeepSeek(entry) || hasStoredProfile(ctx, entry));
221
- }
222
- function catalogForRecord(ctx, key) {
223
- const id = credentialKeyId(key);
224
- const scope = credentialKeyScope(key);
225
- return loginableProviders(ctx).find(entry => entry.provider === id && entry.settingsNs === scope);
226
- }
227
- function isFlowRecordLogout(choice) {
228
- return 'key' in choice && 'writable' in choice && !('provider' in choice);
54
+ export function isFlowRecordLogout(choice) {
55
+ return typeof choice !== 'string' && 'key' in choice && 'writable' in choice && !('provider' in choice);
229
56
  }
230
57
  async function configuredFlowRecords(ctx) {
231
58
  const credentials = ctx.credentials;
232
59
  const rows = [];
233
60
  const claimed = new Set();
234
61
  for (const flow of authorizationFlows(ctx)) {
62
+ if (isAccountFlow(flow))
63
+ continue;
235
64
  const info = await credentials.describeRecord(flow.key);
236
65
  if (!info.configured)
237
66
  continue;
@@ -258,6 +87,9 @@ async function configuredFlowRecords(ctx) {
258
87
  async function logoutableChoices(ctx) {
259
88
  const providers = logoutableProviders(ctx);
260
89
  const records = await configuredFlowRecords(ctx);
90
+ const account = accountClient(ctx);
91
+ const accountState = account === undefined ? undefined : await account.getState().catch(() => undefined);
92
+ const accountChoice = accountState?.status === 'credential-stored' ? [DEEPSEEK_ACCOUNT_CHOICE] : [];
261
93
  const actionable = [];
262
94
  let inactiveDeepSeek;
263
95
  for (const entry of providers) {
@@ -266,19 +98,18 @@ async function logoutableChoices(ctx) {
266
98
  continue;
267
99
  }
268
100
  const activeRef = profileApiKeyEnv(ctx, entry) ?? String(DEEPSEEK_API_KEY);
269
- const managed = activeRef === String(OMDSH_DEEPSEEK_API_KEY);
270
- const current = await ctx.credentials.describe(managed ? OMDSH_DEEPSEEK_API_KEY : DEEPSEEK_API_KEY);
271
- if (managed || (activeRef === String(DEEPSEEK_API_KEY) && current.source === 'file')) {
101
+ const current = await ctx.credentials.describe(credentialRef(activeRef));
102
+ if (current.configured && current.source === 'file')
272
103
  actionable.push(entry);
273
- }
274
- else {
104
+ else
275
105
  inactiveDeepSeek = entry;
276
- }
277
106
  }
278
- const choices = [...actionable, ...records];
107
+ const choices = [...accountChoice, ...actionable, ...records];
279
108
  // Preserve the direct explanatory response when the environment-owned or
280
109
  // already-logged-out official route is the only thing the command can see.
281
- return choices.length === 0 && inactiveDeepSeek !== undefined ? [inactiveDeepSeek] : choices;
110
+ if (choices.length === 0 && inactiveDeepSeek !== undefined)
111
+ return [inactiveDeepSeek];
112
+ return choices;
282
113
  }
283
114
  function selected(raw, values) {
284
115
  const index = /^\d+$/u.test(raw) ? Number(raw) - 1 : -1;
@@ -316,15 +147,26 @@ async function pickLoginChoice(ctx, invocation) {
316
147
  return configured !== 0 ? configured : left.flow.label.localeCompare(right.flow.label);
317
148
  });
318
149
  const orderedFlows = flowRows.map(row => row.flow);
319
- const values = loginChoiceValues(entries, orderedFlows, true);
320
- const fullscreen = values.length > FULLSCREEN_CHOICE_THRESHOLD;
150
+ const account = ctx.get('deepseekAccount');
151
+ const accountState = account?.getState === undefined ? undefined : await account.getState().catch(() => undefined);
152
+ const includeAccount = account?.getState !== undefined;
153
+ const values = loginChoiceValues(entries, orderedFlows, true, includeAccount);
321
154
  const raw = await ctx.tui.prompt({
322
- ...(fullscreen ? { presentation: 'fullscreen-list' } : {}),
323
- optionLayout: 'compact',
324
- filterable: fullscreen,
155
+ presentation: 'card',
156
+ filterable: true,
325
157
  title: 'Provider',
326
158
  question: 'Choose a provider to sign in or update',
327
159
  options: [
160
+ ...(includeAccount ? [{
161
+ label: 'DeepSeek account',
162
+ value: DEEPSEEK_ACCOUNT_CHOICE,
163
+ description: accountState?.status === 'credential-stored'
164
+ ? 'signed in · shared with DSH; choose deepseek-account in /model'
165
+ : 'signed out · opens the DeepSeek browser sign-in',
166
+ ...(accountState?.status === 'credential-stored'
167
+ ? { badge: { label: 'signed in', tone: 'success' } }
168
+ : {}),
169
+ }] : []),
328
170
  ...entryRows.map(({ entry, badge }) => ({
329
171
  label: providerListId(entry),
330
172
  value: entry.provider,
@@ -350,6 +192,7 @@ async function pickLoginChoice(ctx, invocation) {
350
192
  },
351
193
  ],
352
194
  allowCustom: false,
195
+ notify: false,
353
196
  signal: invocation.signal,
354
197
  });
355
198
  if (raw === null)
@@ -360,6 +203,8 @@ async function pickLoginChoice(ctx, invocation) {
360
203
  const chosen = selected(raw, values);
361
204
  if (chosen === undefined)
362
205
  return undefined;
206
+ if (chosen === DEEPSEEK_ACCOUNT_CHOICE)
207
+ return DEEPSEEK_ACCOUNT_CHOICE;
363
208
  const flowKey = parseFlowChoice(chosen);
364
209
  if (flowKey !== undefined)
365
210
  return orderedFlows.find(flow => flow.key === flowKey);
@@ -380,6 +225,7 @@ async function pickAuthorizationMethod(ctx, invocation, flow) {
380
225
  value: method.id,
381
226
  })),
382
227
  allowCustom: false,
228
+ notify: false,
383
229
  signal: invocation.signal,
384
230
  });
385
231
  if (raw === null)
@@ -401,7 +247,10 @@ async function loginAuthorization(ctx, invocation, flow) {
401
247
  const outcome = await authorization.begin({
402
248
  key: flow.key,
403
249
  method,
404
- interaction: createAuthorizationInteraction(ctx.tui, `Login to ${flow.label}`),
250
+ interaction: createAuthorizationInteraction({
251
+ notice: message => { ctx.tui.notice(message); },
252
+ prompt: request => ctx.tui.prompt({ ...request, notify: false }),
253
+ }, `Login to ${flow.label}`),
405
254
  signal: invocation.signal,
406
255
  });
407
256
  if (outcome.status === 'cancelled')
@@ -423,48 +272,41 @@ async function loginAuthorization(ctx, invocation, flow) {
423
272
  async function resetDeepSeekCredentialRoute(ctx) {
424
273
  await ctx.settings.mutate(DEEPSEEK_SETTINGS, [{ op: 'unset', path: ['apiKeyEnv'] }]);
425
274
  }
426
- async function logoutSuccess(ctx, managed) {
427
- const fallback = await ctx.credentials.describe(DEEPSEEK_API_KEY);
428
- if (fallback.configured) {
429
- return {
430
- kind: 'success',
431
- text: `${managed ? 'Logged out from the omdsh-managed DeepSeek credential.' : 'Logged out from DeepSeek.'} Falling back to DEEPSEEK_API_KEY from ${credentialSource(fallback)}.`,
432
- };
433
- }
434
- return { kind: 'success', text: 'Logged out from DeepSeek.' };
435
- }
436
- /** Best-effort browser launch; the prompt always retains the full URL as a fallback. */
437
- export function openDeepSeekDashboard() {
275
+ /** Best-effort browser launch; callers always retain the full URL as a fallback. */
276
+ function openExternal(url) {
438
277
  const command = process.platform === 'darwin'
439
- ? { file: 'open', args: [DEEPSEEK_API_KEYS_URL] }
278
+ ? { file: 'open', args: [url] }
440
279
  : process.platform === 'win32'
441
- ? { file: 'cmd.exe', args: ['/d', '/s', '/c', 'start', '', DEEPSEEK_API_KEYS_URL] }
442
- : { file: 'xdg-open', args: [DEEPSEEK_API_KEYS_URL] };
280
+ ? { file: 'cmd.exe', args: ['/d', '/s', '/c', 'start', '', url] }
281
+ : { file: 'xdg-open', args: [url] };
443
282
  try {
444
283
  const child = spawn(command.file, command.args, { detached: true, stdio: 'ignore' });
445
284
  child.once('error', () => undefined);
446
285
  child.unref();
447
286
  }
448
287
  catch {
449
- // Headless and remote terminals still receive the visible dashboard URL.
288
+ // Headless and remote terminals still receive the visible URL.
450
289
  }
451
290
  }
291
+ /** Best-effort browser launch for the DeepSeek key dashboard. */
292
+ export function openDeepSeekDashboard() {
293
+ openExternal(DEEPSEEK_API_KEYS_URL);
294
+ }
452
295
  async function loginDeepSeek(ctx, invocation, config) {
453
- const current = await ctx.credentials.describe(OMDSH_DEEPSEEK_API_KEY);
296
+ const current = await ctx.credentials.describe(DEEPSEEK_API_KEY);
454
297
  if (current.configured && !current.writable) {
455
- return { kind: 'error', text: unmanagedCredentialMessage(OMDSH_DEEPSEEK_API_KEY, current, 'replace') };
298
+ return { kind: 'error', text: unmanagedCredentialMessage(DEEPSEEK_API_KEY, current, 'replace') };
456
299
  }
457
300
  if (config.openDashboard !== false)
458
301
  openDeepSeekDashboard();
459
- const raw = await ctx.tui.prompt({
302
+ const raw = await ctx.tui.prompt(askPrompt({
460
303
  title: 'Login to DeepSeek',
461
304
  question: current.configured ? 'Paste a new DeepSeek API key' : 'Paste your DeepSeek API key',
462
- detail: `Create or copy a key at ${DEEPSEEK_API_KEYS_URL}. It will be validated, stored locally, and preferred over DEEPSEEK_API_KEY.`,
463
- allowCustom: true,
305
+ detail: `Create or copy a key at ${DEEPSEEK_API_KEYS_URL}. It will be validated and stored locally as ${DEEPSEEK_API_KEY}.`,
464
306
  secret: true,
465
307
  submitLabel: 'validate',
466
308
  signal: invocation.signal,
467
- });
309
+ }));
468
310
  if (raw === null)
469
311
  return { kind: 'success' };
470
312
  let apiKey;
@@ -478,23 +320,27 @@ async function loginDeepSeek(ctx, invocation, config) {
478
320
  return { kind: 'error', text: error instanceof Error ? error.message : 'DeepSeek login failed.' };
479
321
  }
480
322
  try {
481
- await ctx.credentials.set(OMDSH_DEEPSEEK_API_KEY, apiKey);
323
+ await ctx.credentials.set(DEEPSEEK_API_KEY, apiKey);
482
324
  }
483
325
  catch {
484
326
  return { kind: 'error', text: 'The API key is valid, but it could not be saved to the Harness credential store.' };
485
327
  }
486
- try {
487
- await ctx.settings.update(DEEPSEEK_SETTINGS, { apiKeyEnv: String(OMDSH_DEEPSEEK_API_KEY) });
488
- }
489
- catch {
490
- return {
491
- kind: 'error',
492
- text: 'The API key is valid and saved, but omdsh could not activate it for the DeepSeek provider. Run /login again after checking your settings file.',
493
- };
328
+ // A profile may still pin the old product-owned reference. Reset it so the
329
+ // upstream default resolves the key that was just stored.
330
+ if (profileApiKeyEnv(ctx, fallbackDeepSeekEntry()) !== String(DEEPSEEK_API_KEY)) {
331
+ try {
332
+ await resetDeepSeekCredentialRoute(ctx);
333
+ }
334
+ catch {
335
+ return {
336
+ kind: 'error',
337
+ text: 'The API key is valid and saved, but the DeepSeek credential reference could not be reset. Run /login again after checking your settings file.',
338
+ };
339
+ }
494
340
  }
495
341
  return {
496
342
  kind: 'success',
497
- text: 'Logged in to DeepSeek. Your omdsh credential takes priority on the next model request.',
343
+ text: 'Logged in to DeepSeek. The stored key is used by the deepseek-official route on the next model request.',
498
344
  };
499
345
  }
500
346
  async function loginCatalog(ctx, invocation, entry) {
@@ -504,15 +350,14 @@ async function loginCatalog(ctx, invocation, entry) {
504
350
  return { kind: 'error', text: unmanagedCredentialMessage(ref, current, 'replace') };
505
351
  }
506
352
  const label = entry.displayName;
507
- const raw = await ctx.tui.prompt({
353
+ const raw = await ctx.tui.prompt(askPrompt({
508
354
  title: `Login to ${label}`,
509
355
  question: current.configured ? `Paste a new ${label} API key` : `Paste your ${label} API key`,
510
356
  detail: `The key is stored locally and activates the ${entry.provider} route for the next model request. Use /model to select it.`,
511
- allowCustom: true,
512
357
  secret: true,
513
358
  submitLabel: 'save',
514
359
  signal: invocation.signal,
515
- });
360
+ }));
516
361
  if (raw === null)
517
362
  return { kind: 'success' };
518
363
  let apiKey;
@@ -563,6 +408,7 @@ async function collectCustomModels(ctx, invocation, draft) {
563
408
  ],
564
409
  initialValue: 'enter',
565
410
  allowCustom: false,
411
+ notify: false,
566
412
  signal: invocation.signal,
567
413
  });
568
414
  if (source === null)
@@ -587,8 +433,9 @@ async function collectCustomModels(ctx, invocation, draft) {
587
433
  })),
588
434
  multiSelect: true,
589
435
  allowCustom: false,
590
- filterable: discovered.length > FULLSCREEN_CHOICE_THRESHOLD,
591
- ...(discovered.length > FULLSCREEN_CHOICE_THRESHOLD ? { presentation: 'fullscreen-list' } : {}),
436
+ notify: false,
437
+ filterable: true,
438
+ presentation: 'card',
592
439
  signal: invocation.signal,
593
440
  });
594
441
  if (picked === null)
@@ -600,37 +447,34 @@ async function collectCustomModels(ctx, invocation, draft) {
600
447
  if (invocation.signal.aborted)
601
448
  return undefined;
602
449
  const detail = error instanceof Error ? error.message : 'the endpoint did not list models';
603
- const fallback = await ctx.tui.prompt({
450
+ const fallback = await ctx.tui.prompt(askPrompt({
604
451
  title: 'Custom models',
605
452
  question: 'Enter model ids',
606
453
  detail: `Could not fetch models (${detail}). Type one or more ids, separated by commas.`,
607
- allowCustom: true,
608
454
  signal: invocation.signal,
609
- });
455
+ }));
610
456
  if (fallback === null)
611
457
  return undefined;
612
458
  return parseModelIds(fallback);
613
459
  }
614
460
  }
615
- const typed = await ctx.tui.prompt({
461
+ const typed = await ctx.tui.prompt(askPrompt({
616
462
  title: 'Custom models',
617
463
  question: 'Enter model ids',
618
464
  detail: 'Separate multiple ids with commas. Example: llama3.1, qwen2.5-coder',
619
- allowCustom: true,
620
465
  signal: invocation.signal,
621
- });
466
+ }));
622
467
  if (typed === null)
623
468
  return undefined;
624
469
  return parseModelIds(typed);
625
470
  }
626
471
  async function loginCustom(ctx, invocation) {
627
- const idRaw = await ctx.tui.prompt({
472
+ const idRaw = await ctx.tui.prompt(askPrompt({
628
473
  title: 'Custom provider',
629
474
  question: 'Provider id',
630
475
  detail: 'Lowercase letters, digits, and hyphens. This id is permanent.',
631
- allowCustom: true,
632
476
  signal: invocation.signal,
633
- });
477
+ }));
634
478
  if (idRaw === null)
635
479
  return { kind: 'success' };
636
480
  let id;
@@ -643,13 +487,12 @@ async function loginCustom(ctx, invocation) {
643
487
  if (reservedProviderIds(ctx).has(id)) {
644
488
  return { kind: 'error', text: `${id} is a catalog provider. Use /login and choose it from the list.` };
645
489
  }
646
- const urlRaw = await ctx.tui.prompt({
490
+ const urlRaw = await ctx.tui.prompt(askPrompt({
647
491
  title: 'Custom provider',
648
492
  question: 'Base URL',
649
493
  detail: 'Absolute http(s) prefix, for example http://127.0.0.1:11434/v1',
650
- allowCustom: true,
651
494
  signal: invocation.signal,
652
- });
495
+ }));
653
496
  if (urlRaw === null)
654
497
  return { kind: 'success' };
655
498
  let baseURL;
@@ -671,6 +514,7 @@ async function loginCustom(ctx, invocation) {
671
514
  })),
672
515
  initialValue: 'openai-completions',
673
516
  allowCustom: false,
517
+ notify: false,
674
518
  signal: invocation.signal,
675
519
  });
676
520
  if (api === null)
@@ -678,15 +522,14 @@ async function loginCustom(ctx, invocation) {
678
522
  if (!CUSTOM_PROTOCOLS.includes(api)) {
679
523
  return { kind: 'error', text: `Unknown API protocol: ${api}` };
680
524
  }
681
- const keyRaw = await ctx.tui.prompt({
525
+ const keyRaw = await ctx.tui.prompt(askPrompt({
682
526
  title: `Login to ${id}`,
683
527
  question: `Paste your ${id} API key`,
684
528
  detail: 'Leave empty if the endpoint does not need a key.',
685
- allowCustom: true,
686
529
  secret: true,
687
530
  submitLabel: 'continue',
688
531
  signal: invocation.signal,
689
- });
532
+ }));
690
533
  if (keyRaw === null)
691
534
  return { kind: 'success' };
692
535
  let apiKey;
@@ -752,60 +595,88 @@ async function loginCustom(ctx, invocation) {
752
595
  }
753
596
  async function logoutDeepSeek(ctx, invocation) {
754
597
  const activeRef = profileApiKeyEnv(ctx, fallbackDeepSeekEntry()) ?? String(DEEPSEEK_API_KEY);
755
- const usesOmdshCredential = activeRef === String(OMDSH_DEEPSEEK_API_KEY);
756
- const activeCredential = usesOmdshCredential ? OMDSH_DEEPSEEK_API_KEY : DEEPSEEK_API_KEY;
757
- const current = await ctx.credentials.describe(activeCredential);
758
- const legacyManagedDefault = activeRef === String(DEEPSEEK_API_KEY) && current.source === 'file';
759
- if (!usesOmdshCredential && !legacyManagedDefault) {
760
- if (!current.configured)
761
- return { kind: 'success', text: 'DeepSeek is already logged out.' };
762
- return {
763
- kind: 'success',
764
- text: `No omdsh-managed DeepSeek login is active. DeepSeek is configured by ${credentialSource(current)}.`,
765
- };
766
- }
767
- if (usesOmdshCredential && !current.configured) {
768
- try {
769
- await resetDeepSeekCredentialRoute(ctx);
770
- }
771
- catch {
772
- return { kind: 'error', text: 'Could not reset the DeepSeek credential setting.' };
598
+ const current = await ctx.credentials.describe(credentialRef(activeRef));
599
+ if (!current.configured) {
600
+ if (activeRef !== String(DEEPSEEK_API_KEY)) {
601
+ try {
602
+ await resetDeepSeekCredentialRoute(ctx);
603
+ }
604
+ catch {
605
+ return { kind: 'error', text: 'Could not reset the DeepSeek credential setting.' };
606
+ }
773
607
  }
774
- return logoutSuccess(ctx, true);
608
+ return { kind: 'success', text: 'DeepSeek is already logged out.' };
609
+ }
610
+ if (current.source !== 'file') {
611
+ return { kind: 'success', text: `DeepSeek is configured by ${credentialSource(current)}; the credential store cannot remove it.` };
775
612
  }
776
- const removesStoredKey = current.source === 'file';
777
613
  const answer = await ctx.tui.prompt({
778
614
  title: 'Logout from DeepSeek',
779
- question: removesStoredKey ? 'Remove the stored DeepSeek API key?' : 'Stop using the omdsh DeepSeek credential?',
780
- detail: 'The next model request will fall back to DEEPSEEK_API_KEY when one is available.',
615
+ question: 'Remove the stored DeepSeek API key?',
616
+ detail: 'The next model request will use DEEPSEEK_API_KEY from the launch environment when one is available.',
781
617
  options: [
782
618
  { label: 'Cancel', value: 'cancel', description: 'Keep the current credential.' },
783
619
  {
784
620
  label: 'Log out',
785
621
  value: 'logout',
786
- description: removesStoredKey
787
- ? 'Remove the key from the local Harness credential store.'
788
- : 'Return the provider to its default credential source.',
622
+ description: 'Remove the key from the local Harness credential store.',
789
623
  badge: { label: 'removes credential', tone: 'warning' },
790
624
  },
791
625
  ],
792
626
  initialValue: 'cancel',
793
627
  allowCustom: false,
628
+ notify: false,
794
629
  submitLabel: 'apply',
795
630
  signal: invocation.signal,
796
631
  });
797
632
  if (answer !== 'logout')
798
633
  return { kind: 'success' };
799
634
  try {
800
- if (usesOmdshCredential)
635
+ await ctx.credentials.unset(credentialRef(activeRef));
636
+ if (activeRef !== String(DEEPSEEK_API_KEY))
801
637
  await resetDeepSeekCredentialRoute(ctx);
802
- if (removesStoredKey)
803
- await ctx.credentials.unset(activeCredential);
804
638
  }
805
639
  catch {
806
640
  return { kind: 'error', text: 'Could not remove the DeepSeek API key from the Harness credential store.' };
807
641
  }
808
- return logoutSuccess(ctx, usesOmdshCredential);
642
+ return { kind: 'success', text: 'Logged out from DeepSeek. The next model request falls back to the environment when available.' };
643
+ }
644
+ async function logoutDeepSeekAccount(ctx, invocation) {
645
+ const account = accountClient(ctx);
646
+ if (account === undefined)
647
+ return { kind: 'error', text: 'DeepSeek account integration is not mounted in this build.' };
648
+ let state;
649
+ try {
650
+ state = await account.getState();
651
+ }
652
+ catch {
653
+ return { kind: 'error', text: 'Could not read the shared DeepSeek account state.' };
654
+ }
655
+ if (state.status !== 'credential-stored') {
656
+ return { kind: 'success', text: 'No DeepSeek account grant is stored.' };
657
+ }
658
+ const answer = await ctx.tui.prompt({
659
+ title: 'Logout from DeepSeek account',
660
+ question: 'Sign out of the shared DeepSeek account?',
661
+ detail: 'Removes the local grant and revokes it in the background. The browser session is not signed out.',
662
+ options: [
663
+ { label: 'Cancel', value: 'cancel', description: 'Keep the account grant.' },
664
+ { label: 'Sign out', value: 'logout', description: 'Remove the local grant and revoke it in the background.' },
665
+ ],
666
+ initialValue: 'cancel',
667
+ allowCustom: false,
668
+ notify: false,
669
+ signal: invocation.signal,
670
+ });
671
+ if (answer !== 'logout')
672
+ return { kind: 'success' };
673
+ try {
674
+ await account.signOut(accountClientMetadata());
675
+ }
676
+ catch {
677
+ return { kind: 'error', text: 'Could not sign out of the DeepSeek account. Try again from DSH Web/Desktop.' };
678
+ }
679
+ return { kind: 'success', text: 'Signed out of the DeepSeek account. The local grant was removed; remote revocation continues in the background.' };
809
680
  }
810
681
  async function pickLogoutChoice(ctx, invocation) {
811
682
  const choices = await logoutableChoices(ctx);
@@ -813,26 +684,33 @@ async function pickLogoutChoice(ctx, invocation) {
813
684
  return undefined;
814
685
  if (choices.length === 1)
815
686
  return choices[0];
816
- const values = choices.map(choice => isFlowRecordLogout(choice) ? flowChoiceValue(choice.key) : choice.provider);
817
- const fullscreen = values.length > FULLSCREEN_CHOICE_THRESHOLD;
687
+ const values = choices.map(choice => choice === DEEPSEEK_ACCOUNT_CHOICE
688
+ ? DEEPSEEK_ACCOUNT_CHOICE
689
+ : isFlowRecordLogout(choice) ? flowChoiceValue(choice.key) : choice.provider);
818
690
  const raw = await ctx.tui.prompt({
819
- ...(fullscreen ? { presentation: 'fullscreen-list' } : {}),
820
- optionLayout: 'compact',
821
- filterable: fullscreen,
691
+ presentation: 'card',
692
+ filterable: true,
822
693
  title: 'Provider',
823
694
  question: 'Choose a provider to log out from',
824
- options: choices.map(choice => isFlowRecordLogout(choice)
695
+ options: choices.map(choice => choice === DEEPSEEK_ACCOUNT_CHOICE
825
696
  ? {
826
- label: choice.label,
827
- value: flowChoiceValue(choice.key),
828
- description: choice.writable ? 'signed in' : 'not removable here',
697
+ label: 'DeepSeek account',
698
+ value: DEEPSEEK_ACCOUNT_CHOICE,
699
+ description: 'shared with DSH · signed in',
829
700
  }
830
- : {
831
- label: providerListId(choice),
832
- value: choice.provider,
833
- description: 'signed in',
834
- }),
701
+ : isFlowRecordLogout(choice)
702
+ ? {
703
+ label: choice.label,
704
+ value: flowChoiceValue(choice.key),
705
+ description: choice.writable ? 'signed in' : 'not removable here',
706
+ }
707
+ : {
708
+ label: providerListId(choice),
709
+ value: choice.provider,
710
+ description: 'signed in',
711
+ }),
835
712
  allowCustom: false,
713
+ notify: false,
836
714
  signal: invocation.signal,
837
715
  });
838
716
  if (raw === null)
@@ -840,10 +718,12 @@ async function pickLogoutChoice(ctx, invocation) {
840
718
  const chosen = selected(raw, values);
841
719
  if (chosen === undefined)
842
720
  return undefined;
721
+ if (chosen === DEEPSEEK_ACCOUNT_CHOICE)
722
+ return DEEPSEEK_ACCOUNT_CHOICE;
843
723
  const flowKey = parseFlowChoice(chosen);
844
724
  if (flowKey !== undefined)
845
725
  return choices.find(choice => isFlowRecordLogout(choice) && choice.key === flowKey);
846
- return choices.find(choice => !isFlowRecordLogout(choice) && choice.provider === chosen);
726
+ return choices.find(choice => typeof choice !== 'string' && !isFlowRecordLogout(choice) && choice.provider === chosen);
847
727
  }
848
728
  async function logoutRecord(ctx, invocation, record) {
849
729
  if (!record.writable) {
@@ -867,6 +747,7 @@ async function logoutRecord(ctx, invocation, record) {
867
747
  ],
868
748
  initialValue: 'cancel',
869
749
  allowCustom: false,
750
+ notify: false,
870
751
  submitLabel: 'apply',
871
752
  signal: invocation.signal,
872
753
  });
@@ -911,6 +792,7 @@ async function logoutCatalog(ctx, invocation, entry) {
911
792
  ],
912
793
  initialValue: 'cancel',
913
794
  allowCustom: false,
795
+ notify: false,
914
796
  submitLabel: 'apply',
915
797
  signal: invocation.signal,
916
798
  });
@@ -928,6 +810,156 @@ async function logoutCatalog(ctx, invocation, entry) {
928
810
  }
929
811
  return { kind: 'success', text: `Logged out from ${label}.` };
930
812
  }
813
+ function accountClient(ctx) {
814
+ return ctx.get('deepseekAccount');
815
+ }
816
+ function accountClientMetadata() {
817
+ return {
818
+ version: APP_VERSION,
819
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
820
+ timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
821
+ };
822
+ }
823
+ function accountCallbackOrigin(ctx) {
824
+ const server = ctx.get('webServer');
825
+ if (server === undefined || typeof server.port !== 'number' || server.port === 0)
826
+ return undefined;
827
+ return `${server.protocol}//${server.host}:${server.port}`;
828
+ }
829
+ function accountFailure(attempt) {
830
+ if (attempt?.phase === 'expired')
831
+ return 'The DeepSeek sign-in expired. Run /login to try again.';
832
+ if (attempt?.phase === 'failed')
833
+ return `DeepSeek sign-in failed${attempt.errorCode === undefined ? '' : ` (${attempt.errorCode})`}. Run /login to try again.`;
834
+ if (attempt?.phase === 'cancelled')
835
+ return 'DeepSeek sign-in was cancelled.';
836
+ return undefined;
837
+ }
838
+ async function waitForAccount(invocation, account, timeoutMs) {
839
+ const deadline = Date.now() + timeoutMs;
840
+ while (Date.now() < deadline) {
841
+ if (invocation.signal.aborted) {
842
+ const state = await account.getState().catch(() => undefined);
843
+ const id = state?.attempt?.id;
844
+ if (id !== undefined)
845
+ await account.cancelSignIn(id).catch(() => undefined);
846
+ return { kind: 'success' };
847
+ }
848
+ const state = await account.getState().catch(() => undefined);
849
+ if (state?.status === 'credential-stored' && state.attempt?.phase === 'succeeded') {
850
+ return { kind: 'success', text: 'Signed in to the DeepSeek account. Use /model to select the deepseek-account route; web search and fetch use the account token there.' };
851
+ }
852
+ const failure = accountFailure(state?.attempt);
853
+ if (failure !== undefined)
854
+ return { kind: 'error', text: failure };
855
+ await new Promise(resolve => setTimeout(resolve, 400));
856
+ }
857
+ const state = await account.getState().catch(() => undefined);
858
+ const id = state?.attempt?.id;
859
+ if (id !== undefined)
860
+ await account.cancelSignIn(id).catch(() => undefined);
861
+ return { kind: 'error', text: 'DeepSeek sign-in timed out before the browser completed. Run /login to try again.' };
862
+ }
863
+ async function loginDeepSeekAccount(ctx, invocation, config) {
864
+ const account = accountClient(ctx);
865
+ if (account === undefined)
866
+ return { kind: 'error', text: 'DeepSeek account integration is not mounted in this build.' };
867
+ let state;
868
+ try {
869
+ state = await account.getState();
870
+ }
871
+ catch {
872
+ return { kind: 'error', text: 'Could not read the shared DeepSeek account state.' };
873
+ }
874
+ if (state.status === 'credential-stored') {
875
+ const choice = await ctx.tui.prompt({
876
+ title: 'DeepSeek account',
877
+ question: 'The account is signed in and shared with DSH.',
878
+ detail: 'Use /model to select the deepseek-account route. Signing out removes the local grant; the DSH browser session is revoked in the background.',
879
+ options: [
880
+ { label: 'Use account route', value: 'use', description: 'Close this prompt; select the route in /model.' },
881
+ { label: 'Sign out', value: 'out', description: 'Remove the local grant and revoke it in the background.' },
882
+ { label: 'Cancel', value: 'cancel' },
883
+ ],
884
+ initialValue: 'use',
885
+ allowCustom: false,
886
+ notify: false,
887
+ signal: invocation.signal,
888
+ });
889
+ if (choice === null || choice === 'cancel' || choice === 'use') {
890
+ return { kind: 'success', text: 'Use /model to select the deepseek-account route.' };
891
+ }
892
+ try {
893
+ await account.signOut(accountClientMetadata());
894
+ }
895
+ catch {
896
+ return { kind: 'error', text: 'Could not sign out of the DeepSeek account. Try again from DSH Web/Desktop.' };
897
+ }
898
+ return { kind: 'success', text: 'Signed out of the DeepSeek account. The next account request requires sign-in again.' };
899
+ }
900
+ const server = ctx.get('webServer');
901
+ if (server === undefined || ctx.tui.interactive === false) {
902
+ return {
903
+ kind: 'error',
904
+ text: 'The loopback sign-in host is unavailable. Start omdsh with its normal terminal host, or sign in through DSH Web/Desktop and rerun /login.',
905
+ };
906
+ }
907
+ const origin = accountCallbackOrigin(ctx);
908
+ if (origin === undefined)
909
+ return { kind: 'error', text: 'The loopback sign-in host did not expose a port.' };
910
+ let initial;
911
+ try {
912
+ initial = await account.startSignIn(accountClientMetadata(), origin, 'web');
913
+ }
914
+ catch (error) {
915
+ return { kind: 'error', text: error instanceof Error ? error.message : 'Could not start DeepSeek sign-in.' };
916
+ }
917
+ const attempt = initial.attempt;
918
+ const failure = accountFailure(attempt);
919
+ if (failure !== undefined)
920
+ return { kind: 'error', text: failure };
921
+ let url = attempt?.authorizeUrl;
922
+ for (let i = 0; i < 40 && url === undefined; i += 1) {
923
+ if (invocation.signal.aborted)
924
+ break;
925
+ await new Promise(resolve => setTimeout(resolve, 100));
926
+ const next = await account.getState().catch(() => undefined);
927
+ const nextFailure = accountFailure(next?.attempt);
928
+ if (nextFailure !== undefined)
929
+ return { kind: 'error', text: nextFailure };
930
+ url = next?.attempt?.authorizeUrl;
931
+ }
932
+ if (url === undefined) {
933
+ const id = attempt?.id ?? (await account.getState().catch(() => undefined))?.attempt?.id;
934
+ if (id !== undefined)
935
+ await account.cancelSignIn(id).catch(() => undefined);
936
+ return { kind: 'error', text: 'DeepSeek did not return a browser URL for this sign-in attempt.' };
937
+ }
938
+ if (config.openBrowser !== false)
939
+ openExternal(url);
940
+ const answer = await ctx.tui.prompt({
941
+ title: 'DeepSeek account',
942
+ question: 'Complete sign-in in your browser, then return here.',
943
+ detail: `${url}
944
+
945
+ This terminal is listening on ${origin}/oauth/callback until the attempt ends.`,
946
+ options: [
947
+ { label: 'I completed sign-in', value: 'done' },
948
+ { label: 'Cancel', value: 'cancel', description: 'Withdraw this sign-in attempt.' },
949
+ ],
950
+ initialValue: 'done',
951
+ allowCustom: false,
952
+ notify: false,
953
+ signal: invocation.signal,
954
+ });
955
+ if (answer !== 'done') {
956
+ const id = attempt?.id ?? (await account.getState().catch(() => undefined))?.attempt?.id;
957
+ if (id !== undefined)
958
+ await account.cancelSignIn(id).catch(() => undefined);
959
+ return { kind: 'success', text: 'DeepSeek sign-in cancelled.' };
960
+ }
961
+ return waitForAccount(invocation, account, 120_000);
962
+ }
931
963
  async function login(ctx, invocation, config) {
932
964
  if (invocation.rawInput.trim() !== '') {
933
965
  return { kind: 'error', text: 'Usage: /login (paste the key only into the protected prompt)' };
@@ -935,6 +967,8 @@ async function login(ctx, invocation, config) {
935
967
  const choice = await pickLoginChoice(ctx, invocation);
936
968
  if (choice === undefined)
937
969
  return { kind: 'success' };
970
+ if (choice === DEEPSEEK_ACCOUNT_CHOICE)
971
+ return loginDeepSeekAccount(ctx, invocation, config);
938
972
  if (choice === CUSTOM_PROVIDER_VALUE)
939
973
  return loginCustom(ctx, invocation);
940
974
  if (isAuthorizationFlow(choice))
@@ -949,6 +983,8 @@ async function logout(ctx, invocation) {
949
983
  const choice = await pickLogoutChoice(ctx, invocation);
950
984
  if (choice === undefined)
951
985
  return { kind: 'success' };
986
+ if (choice === DEEPSEEK_ACCOUNT_CHOICE)
987
+ return logoutDeepSeekAccount(ctx, invocation);
952
988
  if (isFlowRecordLogout(choice))
953
989
  return logoutRecord(ctx, invocation, choice);
954
990
  return isDeepSeek(choice)
@@ -957,8 +993,19 @@ async function logout(ctx, invocation) {
957
993
  }
958
994
  export function apply(ctx, config = {}) {
959
995
  registerCommands(ctx, [
960
- { name: 'login', description: 'Sign in to a provider', handler: invocation => login(ctx, invocation, config) },
961
- { name: 'logout', description: 'Sign out of a stored provider', handler: invocation => logout(ctx, invocation) },
996
+ {
997
+ name: 'login',
998
+ description: 'Sign in to a provider',
999
+ handler: invocation => login(ctx, invocation, config),
1000
+ },
1001
+ {
1002
+ // Its own command: signing in and signing out are two intentions, and
1003
+ // folding the second into `/login logout` made the reader look for a verb
1004
+ // about leaving inside the one about entering.
1005
+ name: 'logout',
1006
+ description: 'Sign out of a stored provider or account',
1007
+ handler: invocation => logout(ctx, invocation),
1008
+ },
962
1009
  ], 'omdsh authentication commands');
963
1010
  }
964
1011
  //# sourceMappingURL=auth.js.map