@agi-fans/dsh-tui 0.22.0 → 0.23.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.
- package/LICENSE +26 -0
- package/lib/CHANGELOG.md +219 -1
- package/lib/chrome/box.d.ts +1 -18
- package/lib/chrome/box.js +2 -45
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.js +4 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +0 -1
- package/lib/chrome/command-output.js +11 -5
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/goal-bar.js +1 -1
- package/lib/chrome/goal-bar.js.map +1 -1
- package/lib/chrome/main-screen-renderer.js +4 -3
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.d.ts +3 -3
- package/lib/chrome/markdown.js +85 -36
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/md-style.d.ts +5 -2
- package/lib/chrome/md-style.js +13 -4
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/renderer.d.ts +37 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -3
- package/lib/chrome/status-line.js +5 -5
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +8 -3
- package/lib/chrome/theme.js +51 -153
- package/lib/chrome/theme.js.map +1 -1
- package/lib/commands/auth-account.d.ts +9 -0
- package/lib/commands/auth-account.js +202 -0
- package/lib/commands/auth-account.js.map +1 -0
- package/lib/commands/auth.js +238 -414
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/diff.js +22 -16
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +0 -1
- package/lib/commands/export.js +99 -32
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.js +11 -9
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.d.ts +12 -1
- package/lib/commands/integrations.js +120 -23
- package/lib/commands/integrations.js.map +1 -1
- package/lib/commands/jobs-browser.d.ts +3 -0
- package/lib/commands/jobs-browser.js +152 -0
- package/lib/commands/jobs-browser.js.map +1 -0
- package/lib/commands/jobs.d.ts +3 -14
- package/lib/commands/jobs.js +26 -18
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.d.ts +3 -0
- package/lib/commands/model.js +212 -128
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/open-external.d.ts +2 -0
- package/lib/commands/open-external.js +19 -0
- package/lib/commands/open-external.js.map +1 -0
- package/lib/commands/permission.js +65 -54
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +381 -139
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule-catalog.d.ts +12 -0
- package/lib/commands/schedule-catalog.js +41 -0
- package/lib/commands/schedule-catalog.js.map +1 -0
- package/lib/commands/schedule-history.d.ts +4 -0
- package/lib/commands/schedule-history.js +62 -0
- package/lib/commands/schedule-history.js.map +1 -0
- package/lib/commands/schedule.d.ts +12 -15
- package/lib/commands/schedule.js +220 -187
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.d.ts +3 -0
- package/lib/commands/session-configuration.js +62 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +116 -66
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.js +14 -8
- package/lib/commands/terminal.js.map +1 -1
- package/lib/definition.d.ts +115 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +1 -1
- package/lib/input/actions.js +5 -2
- package/lib/input/actions.js.map +1 -1
- package/lib/input/composer-files.d.ts +2 -0
- package/lib/input/composer-files.js +13 -3
- package/lib/input/composer-files.js.map +1 -1
- package/lib/input/image-paste.d.ts +5 -12
- package/lib/input/image-paste.js +32 -28
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/key-test.d.ts +5 -0
- package/lib/input/key-test.js +19 -0
- package/lib/input/key-test.js.map +1 -0
- package/lib/input/keys.d.ts +2 -0
- package/lib/input/keys.js +4 -1
- package/lib/input/keys.js.map +1 -1
- package/lib/locale/index.d.ts +6 -0
- package/lib/locale/index.js +32 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/prompt.d.ts +4 -0
- package/lib/locale/prompt.js +24 -0
- package/lib/locale/prompt.js.map +1 -0
- package/lib/locale/zh.d.ts +1114 -0
- package/lib/locale/zh.js +1115 -0
- package/lib/locale/zh.js.map +1 -0
- package/lib/prompt-intents.d.ts +4 -0
- package/lib/prompt-intents.js +4 -0
- package/lib/prompt-intents.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +18 -0
- package/lib/runtime/account-settings.js +241 -0
- package/lib/runtime/account-settings.js.map +1 -0
- package/lib/runtime/agent-profile.d.ts +3 -3
- package/lib/runtime/agent-profile.js +4 -4
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/btw-policy.js +2 -1
- package/lib/runtime/btw-policy.js.map +1 -1
- package/lib/runtime/btw.js +1 -2
- package/lib/runtime/btw.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +2 -2
- package/lib/runtime/composer-images.js +4 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.js +27 -1
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-browser.d.ts +19 -0
- package/lib/runtime/file-browser.js +291 -0
- package/lib/runtime/file-browser.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -9
- package/lib/runtime/file-review.js +91 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/help-reader.d.ts +12 -0
- package/lib/runtime/help-reader.js +50 -0
- package/lib/runtime/help-reader.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/model-catalog-editor.d.ts +12 -0
- package/lib/runtime/model-catalog-editor.js +215 -0
- package/lib/runtime/model-catalog-editor.js.map +1 -0
- package/lib/runtime/model-settings.d.ts +11 -0
- package/lib/runtime/model-settings.js +404 -0
- package/lib/runtime/model-settings.js.map +1 -0
- package/lib/runtime/plugin-settings.d.ts +4 -1
- package/lib/runtime/plugin-settings.js +164 -88
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/prompt-browser.d.ts +9 -0
- package/lib/runtime/prompt-browser.js +19 -0
- package/lib/runtime/prompt-browser.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +27 -27
- package/lib/runtime/provider-local.js +1288 -308
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/reference-reader.d.ts +15 -0
- package/lib/runtime/reference-reader.js +78 -0
- package/lib/runtime/reference-reader.js.map +1 -0
- package/lib/runtime/runner.js +38 -29
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-runtime.js +1 -1
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/settings-draft.d.ts +21 -0
- package/lib/runtime/settings-draft.js +166 -0
- package/lib/runtime/settings-draft.js.map +1 -0
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.d.ts +0 -1
- package/lib/runtime/startup-notices.js +35 -6
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/subagent-model-selection.js +2 -1
- package/lib/runtime/subagent-model-selection.js.map +1 -1
- package/lib/runtime/subagent-settings.d.ts +14 -0
- package/lib/runtime/subagent-settings.js +288 -0
- package/lib/runtime/subagent-settings.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -0
- package/lib/runtime/terminal-notifications.js +13 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +2 -0
- package/lib/runtime/terminal-probe.js +9 -0
- package/lib/runtime/terminal-probe.js.map +1 -1
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/runtime/tool-reader.d.ts +5 -0
- package/lib/runtime/tool-reader.js +27 -0
- package/lib/runtime/tool-reader.js.map +1 -0
- package/lib/runtime/web-settings.d.ts +14 -0
- package/lib/runtime/web-settings.js +339 -0
- package/lib/runtime/web-settings.js.map +1 -0
- package/lib/session/command-attachments.d.ts +17 -0
- package/lib/session/command-attachments.js +50 -0
- package/lib/session/command-attachments.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +16 -0
- package/lib/session/context-diagnostics.js +15 -0
- package/lib/session/context-diagnostics.js.map +1 -1
- package/lib/session/external-agents.d.ts +3 -1
- package/lib/session/external-agents.js +35 -10
- package/lib/session/external-agents.js.map +1 -1
- package/lib/session/generation-continuation.d.ts +11 -0
- package/lib/session/generation-continuation.js +34 -0
- package/lib/session/generation-continuation.js.map +1 -0
- package/lib/session/goal-controls.d.ts +5 -0
- package/lib/session/goal-controls.js +81 -0
- package/lib/session/goal-controls.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +7 -3
- package/lib/session/interaction-adapter.js +121 -49
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +2 -0
- package/lib/session/message-queue.js +102 -17
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/pending-inputs.d.ts +30 -0
- package/lib/session/pending-inputs.js +183 -0
- package/lib/session/pending-inputs.js.map +1 -0
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +33 -20
- package/lib/session/session-controller.js +500 -129
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.d.ts +3 -1
- package/lib/session/session-library.js +15 -0
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +1 -0
- package/lib/session/subagent-roster.js +16 -5
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -0
- package/lib/session/subagent-tracker.js +18 -0
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +9 -5
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +2 -1
- package/lib/session/workflow-projection.js +34 -4
- package/lib/session/workflow-projection.js.map +1 -1
- package/lib/views/autocomplete.d.ts +12 -12
- package/lib/views/autocomplete.js +54 -45
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/context-overview.d.ts +4 -0
- package/lib/views/context-overview.js +43 -0
- package/lib/views/context-overview.js.map +1 -0
- package/lib/views/copy-targets.d.ts +6 -2
- package/lib/views/copy-targets.js +19 -14
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.d.ts +10 -1
- package/lib/views/document-reader.js +63 -6
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +29 -11
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.js +21 -0
- package/lib/views/file-actions.js.map +1 -1
- package/lib/views/file-browser.d.ts +98 -0
- package/lib/views/file-browser.js +120 -0
- package/lib/views/file-browser.js.map +1 -0
- package/lib/views/history-search.js +4 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +3 -2
- package/lib/views/hotkey-format.js +4 -3
- package/lib/views/hotkey-format.js.map +1 -1
- package/lib/views/hotkeys.js +10 -12
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-view.js +16 -11
- package/lib/views/list-view.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +23 -1
- package/lib/views/prompt-selector.js +123 -60
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +6 -1
- package/lib/views/settings-list.js +69 -46
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/tool-actions.d.ts +16 -0
- package/lib/views/tool-actions.js +29 -0
- package/lib/views/tool-actions.js.map +1 -0
- package/lib/views/trajectory-timeline.d.ts +5 -6
- package/lib/views/trajectory-timeline.js +25 -15
- package/lib/views/trajectory-timeline.js.map +1 -1
- package/lib/views/trajectory.d.ts +23 -2
- package/lib/views/trajectory.js +404 -186
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +39 -15
- package/lib/views/transcript-render.js +406 -208
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +3 -2
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workflow.d.ts +16 -0
- package/lib/views/workflow.js +42 -0
- package/lib/views/workflow.js.map +1 -0
- package/package.json +61 -58
- package/lib/chrome/welcome-tips.d.ts +0 -23
- package/lib/chrome/welcome-tips.js +0 -24
- package/lib/chrome/welcome-tips.js.map +0 -1
- package/lib/session/steering.d.ts +0 -3
- package/lib/session/steering.js +0 -9
- package/lib/session/steering.js.map +0 -1
package/lib/commands/auth.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** Interactive credential commands backed by the Harness credential and settings seams. */
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import process from 'node:process';
|
|
4
2
|
import { AuthorizationError, } from '@deepseek-ai/dsh-authorization';
|
|
5
3
|
import { credentialRef, } from '@deepseek-ai/dsh-credentials';
|
|
4
|
+
import { resolveUiLanguage, uiText } from "../locale/index.js";
|
|
5
|
+
import { createPromptBrowser } from "../runtime/prompt-browser.js";
|
|
6
6
|
import { createAuthorizationInteraction } from "../session/authorization-interaction.js";
|
|
7
|
-
import {
|
|
7
|
+
import { logoutDeepSeekAccount } from "./auth-account.js";
|
|
8
|
+
import { openExternal } from "./open-external.js";
|
|
9
|
+
import { openAccountSettings, bindAccountNotices } from "../runtime/account-settings.js";
|
|
8
10
|
import { registerCommands } from "./registration.js";
|
|
9
11
|
import { authorizationFlows, catalogForRecord, catalogProvidersWithoutFlows, fallbackDeepSeekEntry, flowChoiceValue, isAccountFlow, isDeepSeek, isPiAiDeepSeekFlow, loginAuthorizationFlows, logoutableProviders, parseFlowChoice, profileApiKeyEnv, providerListId, } from "./auth-providers.js";
|
|
10
12
|
import { askPrompt } from "../prompt-intents.js";
|
|
@@ -87,7 +89,7 @@ async function configuredFlowRecords(ctx) {
|
|
|
87
89
|
async function logoutableChoices(ctx) {
|
|
88
90
|
const providers = logoutableProviders(ctx);
|
|
89
91
|
const records = await configuredFlowRecords(ctx);
|
|
90
|
-
const account =
|
|
92
|
+
const account = ctx.get('deepseekAccount');
|
|
91
93
|
const accountState = account === undefined ? undefined : await account.getState().catch(() => undefined);
|
|
92
94
|
const accountChoice = accountState?.status === 'credential-stored' ? [DEEPSEEK_ACCOUNT_CHOICE] : [];
|
|
93
95
|
const actionable = [];
|
|
@@ -128,11 +130,9 @@ function credentialOwnershipBadge(info) {
|
|
|
128
130
|
return { label: 'environment', tone: 'muted' };
|
|
129
131
|
return { label: 'configured externally', tone: 'muted' };
|
|
130
132
|
}
|
|
131
|
-
async function pickLoginChoice(ctx, invocation) {
|
|
133
|
+
async function pickLoginChoice(ctx, invocation, prompt) {
|
|
132
134
|
const entries = catalogProvidersWithoutFlows(ctx);
|
|
133
135
|
const flows = loginAuthorizationFlows(ctx);
|
|
134
|
-
if (entries.length === 0 && flows.length === 0)
|
|
135
|
-
return CUSTOM_PROVIDER_VALUE;
|
|
136
136
|
const entryRows = await Promise.all(entries.map(async (entry) => {
|
|
137
137
|
const ref = profileApiKeyEnv(ctx, entry);
|
|
138
138
|
const info = ref === undefined ? UNCONFIGURED : await ctx.credentials.describe(credentialRef(ref));
|
|
@@ -151,7 +151,7 @@ async function pickLoginChoice(ctx, invocation) {
|
|
|
151
151
|
const accountState = account?.getState === undefined ? undefined : await account.getState().catch(() => undefined);
|
|
152
152
|
const includeAccount = account?.getState !== undefined;
|
|
153
153
|
const values = loginChoiceValues(entries, orderedFlows, true, includeAccount);
|
|
154
|
-
const raw = await
|
|
154
|
+
const raw = await prompt({ localize: true,
|
|
155
155
|
presentation: 'card',
|
|
156
156
|
filterable: true,
|
|
157
157
|
title: 'Provider',
|
|
@@ -193,8 +193,7 @@ async function pickLoginChoice(ctx, invocation) {
|
|
|
193
193
|
],
|
|
194
194
|
allowCustom: false,
|
|
195
195
|
notify: false,
|
|
196
|
-
signal: invocation.signal,
|
|
197
|
-
});
|
|
196
|
+
signal: invocation.signal, });
|
|
198
197
|
if (raw === null)
|
|
199
198
|
return undefined;
|
|
200
199
|
if (raw.trim().toLowerCase() === 'custom' || selected(raw, values) === CUSTOM_PROVIDER_VALUE) {
|
|
@@ -217,7 +216,7 @@ async function pickAuthorizationMethod(ctx, invocation, flow) {
|
|
|
217
216
|
if (flow.methods.length === 1)
|
|
218
217
|
return flow.methods[0]?.id;
|
|
219
218
|
const values = flow.methods.map(method => method.id);
|
|
220
|
-
const raw = await ctx.tui.prompt({
|
|
219
|
+
const raw = await ctx.tui.prompt({ localize: true,
|
|
221
220
|
title: `Login to ${flow.label}`,
|
|
222
221
|
question: 'Choose a sign-in method',
|
|
223
222
|
options: flow.methods.map(method => ({
|
|
@@ -272,59 +271,62 @@ async function loginAuthorization(ctx, invocation, flow) {
|
|
|
272
271
|
async function resetDeepSeekCredentialRoute(ctx) {
|
|
273
272
|
await ctx.settings.mutate(DEEPSEEK_SETTINGS, [{ op: 'unset', path: ['apiKeyEnv'] }]);
|
|
274
273
|
}
|
|
275
|
-
/** Best-effort browser launch; callers always retain the full URL as a fallback. */
|
|
276
|
-
function openExternal(url) {
|
|
277
|
-
const command = process.platform === 'darwin'
|
|
278
|
-
? { file: 'open', args: [url] }
|
|
279
|
-
: process.platform === 'win32'
|
|
280
|
-
? { file: 'cmd.exe', args: ['/d', '/s', '/c', 'start', '', url] }
|
|
281
|
-
: { file: 'xdg-open', args: [url] };
|
|
282
|
-
try {
|
|
283
|
-
const child = spawn(command.file, command.args, { detached: true, stdio: 'ignore' });
|
|
284
|
-
child.once('error', () => undefined);
|
|
285
|
-
child.unref();
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// Headless and remote terminals still receive the visible URL.
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
274
|
/** Best-effort browser launch for the DeepSeek key dashboard. */
|
|
292
275
|
export function openDeepSeekDashboard() {
|
|
293
276
|
openExternal(DEEPSEEK_API_KEYS_URL);
|
|
294
277
|
}
|
|
295
|
-
async function loginDeepSeek(ctx, invocation, config) {
|
|
278
|
+
async function loginDeepSeek(ctx, invocation, config, prompt) {
|
|
296
279
|
const current = await ctx.credentials.describe(DEEPSEEK_API_KEY);
|
|
297
280
|
if (current.configured && !current.writable) {
|
|
298
281
|
return { kind: 'error', text: unmanagedCredentialMessage(DEEPSEEK_API_KEY, current, 'replace') };
|
|
299
282
|
}
|
|
300
283
|
if (config.openDashboard !== false)
|
|
301
284
|
openDeepSeekDashboard();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return { kind: 'success' };
|
|
312
|
-
let apiKey;
|
|
313
|
-
try {
|
|
314
|
-
apiKey = normalizeDeepSeekApiKey(raw);
|
|
315
|
-
await validateDeepSeekApiKey(apiKey, invocation.signal);
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
if (invocation.signal.aborted)
|
|
285
|
+
let stored = false;
|
|
286
|
+
while (!invocation.signal.aborted && !stored) {
|
|
287
|
+
const apiKey = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
|
|
288
|
+
title: 'Login to DeepSeek',
|
|
289
|
+
question: current.configured ? 'Paste a new DeepSeek API key' : 'Paste your DeepSeek API key',
|
|
290
|
+
detail: `Create or copy a key at ${DEEPSEEK_API_KEYS_URL}. It will be validated and stored locally as ${DEEPSEEK_API_KEY}.`,
|
|
291
|
+
secret: true, submitLabel: 'validate', signal: invocation.signal,
|
|
292
|
+
}), normalizeDeepSeekApiKey);
|
|
293
|
+
if (apiKey === null)
|
|
319
294
|
return { kind: 'success' };
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
295
|
+
let validated = false;
|
|
296
|
+
while (!invocation.signal.aborted) {
|
|
297
|
+
try {
|
|
298
|
+
if (!validated)
|
|
299
|
+
await validateDeepSeekApiKey(apiKey, invocation.signal);
|
|
300
|
+
validated = true;
|
|
301
|
+
invocation.signal.throwIfAborted();
|
|
302
|
+
await ctx.credentials.set(DEEPSEEK_API_KEY, apiKey);
|
|
303
|
+
stored = true;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (invocation.signal.aborted)
|
|
308
|
+
return { kind: 'success' };
|
|
309
|
+
const diagnostic = validated
|
|
310
|
+
? 'The API key is valid, but it could not be saved to the Harness credential store.'
|
|
311
|
+
: error instanceof Error ? error.message : 'DeepSeek login failed.';
|
|
312
|
+
const action = await prompt({ localize: ['title', 'question', 'options'],
|
|
313
|
+
title: 'Login to DeepSeek', question: validated ? 'Could not save the key' : 'Could not validate the key',
|
|
314
|
+
detail: diagnostic, allowCustom: false, notify: false, signal: invocation.signal,
|
|
315
|
+
options: [
|
|
316
|
+
{ label: validated ? 'Retry saving' : 'Retry validation', value: 'retry' },
|
|
317
|
+
{ label: 'Enter another key', value: 'edit' },
|
|
318
|
+
{ label: 'Back', value: 'back' },
|
|
319
|
+
],
|
|
320
|
+
});
|
|
321
|
+
if (action === null || action === 'back')
|
|
322
|
+
return { kind: 'success' };
|
|
323
|
+
if (action !== 'retry')
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
327
|
}
|
|
328
|
+
if (invocation.signal.aborted)
|
|
329
|
+
return { kind: 'success' };
|
|
328
330
|
// A profile may still pin the old product-owned reference. Reset it so the
|
|
329
331
|
// upstream default resolves the key that was just stored.
|
|
330
332
|
if (profileApiKeyEnv(ctx, fallbackDeepSeekEntry()) !== String(DEEPSEEK_API_KEY)) {
|
|
@@ -343,30 +345,23 @@ async function loginDeepSeek(ctx, invocation, config) {
|
|
|
343
345
|
text: 'Logged in to DeepSeek. The stored key is used by the deepseek-official route on the next model request.',
|
|
344
346
|
};
|
|
345
347
|
}
|
|
346
|
-
async function loginCatalog(ctx, invocation, entry) {
|
|
348
|
+
async function loginCatalog(ctx, invocation, entry, prompt) {
|
|
347
349
|
const ref = managedProviderCredentialRef(entry.provider);
|
|
348
350
|
const current = await ctx.credentials.describe(ref);
|
|
349
351
|
if (current.configured && !current.writable) {
|
|
350
352
|
return { kind: 'error', text: unmanagedCredentialMessage(ref, current, 'replace') };
|
|
351
353
|
}
|
|
352
354
|
const label = entry.displayName;
|
|
353
|
-
const
|
|
355
|
+
const apiKey = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
|
|
354
356
|
title: `Login to ${label}`,
|
|
355
357
|
question: current.configured ? `Paste a new ${label} API key` : `Paste your ${label} API key`,
|
|
356
358
|
detail: `The key is stored locally and activates the ${entry.provider} route for the next model request. Use /model to select it.`,
|
|
357
359
|
secret: true,
|
|
358
360
|
submitLabel: 'save',
|
|
359
361
|
signal: invocation.signal,
|
|
360
|
-
}));
|
|
361
|
-
if (
|
|
362
|
+
}), value => normalizeApiKey(value, label));
|
|
363
|
+
if (apiKey === null)
|
|
362
364
|
return { kind: 'success' };
|
|
363
|
-
let apiKey;
|
|
364
|
-
try {
|
|
365
|
-
apiKey = normalizeApiKey(raw, label);
|
|
366
|
-
}
|
|
367
|
-
catch (error) {
|
|
368
|
-
return { kind: 'error', text: error instanceof Error ? error.message : `${label} login failed.` };
|
|
369
|
-
}
|
|
370
365
|
try {
|
|
371
366
|
await ctx.credentials.set(ref, apiKey);
|
|
372
367
|
}
|
|
@@ -398,168 +393,163 @@ function reservedProviderIds(ctx) {
|
|
|
398
393
|
.map(entry => entry.provider),
|
|
399
394
|
]);
|
|
400
395
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
});
|
|
414
|
-
if (source === null)
|
|
415
|
-
return undefined;
|
|
416
|
-
if (source === 'fetch') {
|
|
396
|
+
/** Retry a field in place, keeping only non-secret text in its editor. */
|
|
397
|
+
async function readLoginField(prompt, request, validate) {
|
|
398
|
+
let input = request.initialInput ?? '', diagnostic = '';
|
|
399
|
+
while (!request.signal?.aborted) {
|
|
400
|
+
const raw = await prompt({ ...request,
|
|
401
|
+
...(request.secret ? {} : { initialInput: input }),
|
|
402
|
+
detail: [diagnostic, request.detail].filter(Boolean).join('\n\n'),
|
|
403
|
+
});
|
|
404
|
+
if (raw === null || request.signal?.aborted)
|
|
405
|
+
return null;
|
|
406
|
+
if (!request.secret)
|
|
407
|
+
input = raw;
|
|
417
408
|
try {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
409
|
+
return await validate(raw);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
diagnostic = error instanceof Error ? error.message : 'Invalid value.';
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
async function collectCustomModels(ctx, invocation, draft, prompt) {
|
|
418
|
+
let diagnostic = '', typed = '';
|
|
419
|
+
while (!invocation.signal.aborted) {
|
|
420
|
+
const source = await prompt({ localize: true,
|
|
421
|
+
title: 'Custom models', question: 'How should omdsh learn the model ids?', detail: diagnostic,
|
|
422
|
+
options: [
|
|
423
|
+
{ label: 'Enter model ids', value: 'enter', description: 'Type one or more ids, separated by commas.' },
|
|
424
|
+
{ label: 'Fetch from endpoint', value: 'fetch', description: 'Ask the base URL for GET /models.' },
|
|
425
|
+
],
|
|
426
|
+
initialValue: 'enter', allowCustom: false, notify: false, signal: invocation.signal,
|
|
427
|
+
});
|
|
428
|
+
if (source === null || invocation.signal.aborted)
|
|
429
|
+
return undefined;
|
|
430
|
+
if (source === 'fetch') {
|
|
431
|
+
try {
|
|
432
|
+
const discovered = await ctx.llm.discoverModels('llm-pi-ai', draft, invocation.signal);
|
|
433
|
+
if (invocation.signal.aborted)
|
|
434
|
+
return undefined;
|
|
435
|
+
if (discovered.length === 1)
|
|
436
|
+
return [discovered[0].id];
|
|
437
|
+
if (discovered.length === 0) {
|
|
438
|
+
diagnostic = 'No models were returned. Try again or enter model ids.';
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
const picked = await prompt({ localize: ['title', 'question', 'actions'],
|
|
442
|
+
title: 'Custom models', question: 'Choose models to enable',
|
|
443
|
+
options: discovered.map(model => ({ label: model.id, value: model.id,
|
|
444
|
+
...(model.name === undefined || model.name === model.id ? {} : { description: model.name }) })),
|
|
445
|
+
multiSelect: true, allowCustom: false, notify: false, filterable: true,
|
|
446
|
+
presentation: 'card', signal: invocation.signal,
|
|
440
447
|
});
|
|
441
448
|
if (picked === null)
|
|
442
|
-
|
|
449
|
+
continue;
|
|
443
450
|
return parseModelIds(picked);
|
|
444
451
|
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
diagnostic = error instanceof Error ? error.message : 'Could not fetch models. Try again or enter model ids.';
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
445
456
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
detail: `Could not fetch models (${detail}). Type one or more ids, separated by commas.`,
|
|
454
|
-
signal: invocation.signal,
|
|
455
|
-
}));
|
|
456
|
-
if (fallback === null)
|
|
457
|
-
return undefined;
|
|
458
|
-
return parseModelIds(fallback);
|
|
459
|
-
}
|
|
457
|
+
const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
|
|
458
|
+
title: 'Custom models', question: 'Enter model ids',
|
|
459
|
+
detail: 'Separate multiple ids with commas. Example: llama3.1, qwen2.5-coder',
|
|
460
|
+
initialInput: typed, onCancelDraft: value => { typed = value; }, signal: invocation.signal,
|
|
461
|
+
}), value => parseModelIds(value).join(', '));
|
|
462
|
+
if (value !== null)
|
|
463
|
+
return parseModelIds(value);
|
|
460
464
|
}
|
|
461
|
-
|
|
462
|
-
title: 'Custom models',
|
|
463
|
-
question: 'Enter model ids',
|
|
464
|
-
detail: 'Separate multiple ids with commas. Example: llama3.1, qwen2.5-coder',
|
|
465
|
-
signal: invocation.signal,
|
|
466
|
-
}));
|
|
467
|
-
if (typed === null)
|
|
468
|
-
return undefined;
|
|
469
|
-
return parseModelIds(typed);
|
|
465
|
+
return undefined;
|
|
470
466
|
}
|
|
471
|
-
async function loginCustom(ctx, invocation) {
|
|
472
|
-
|
|
473
|
-
title: 'Custom provider',
|
|
474
|
-
question: 'Provider id',
|
|
475
|
-
detail: 'Lowercase letters, digits, and hyphens. This id is permanent.',
|
|
476
|
-
signal: invocation.signal,
|
|
477
|
-
}));
|
|
478
|
-
if (idRaw === null)
|
|
479
|
-
return { kind: 'success' };
|
|
480
|
-
let id;
|
|
481
|
-
try {
|
|
482
|
-
id = parseProviderId(idRaw);
|
|
483
|
-
}
|
|
484
|
-
catch (error) {
|
|
485
|
-
return { kind: 'error', text: error instanceof Error ? error.message : 'Invalid provider id.' };
|
|
486
|
-
}
|
|
487
|
-
if (reservedProviderIds(ctx).has(id)) {
|
|
488
|
-
return { kind: 'error', text: `${id} is a catalog provider. Use /login and choose it from the list.` };
|
|
489
|
-
}
|
|
490
|
-
const urlRaw = await ctx.tui.prompt(askPrompt({
|
|
491
|
-
title: 'Custom provider',
|
|
492
|
-
question: 'Base URL',
|
|
493
|
-
detail: 'Absolute http(s) prefix, for example http://127.0.0.1:11434/v1',
|
|
494
|
-
signal: invocation.signal,
|
|
495
|
-
}));
|
|
496
|
-
if (urlRaw === null)
|
|
497
|
-
return { kind: 'success' };
|
|
498
|
-
let baseURL;
|
|
499
|
-
try {
|
|
500
|
-
baseURL = parseBaseURL(urlRaw);
|
|
501
|
-
}
|
|
502
|
-
catch (error) {
|
|
503
|
-
return { kind: 'error', text: error instanceof Error ? error.message : 'Invalid base URL.' };
|
|
504
|
-
}
|
|
505
|
-
const api = await ctx.tui.prompt({
|
|
506
|
-
title: 'Custom provider',
|
|
507
|
-
question: 'API protocol',
|
|
508
|
-
options: CUSTOM_PROTOCOLS.map(protocol => ({
|
|
509
|
-
label: protocol,
|
|
510
|
-
value: protocol,
|
|
511
|
-
description: protocol === 'openai-completions'
|
|
512
|
-
? 'Chat Completions — Ollama, vLLM, LM Studio, and most gateways'
|
|
513
|
-
: protocol === 'openai-responses' ? 'OpenAI Responses API' : 'Anthropic Messages API',
|
|
514
|
-
})),
|
|
515
|
-
initialValue: 'openai-completions',
|
|
516
|
-
allowCustom: false,
|
|
517
|
-
notify: false,
|
|
518
|
-
signal: invocation.signal,
|
|
519
|
-
});
|
|
520
|
-
if (api === null)
|
|
521
|
-
return { kind: 'success' };
|
|
522
|
-
if (!CUSTOM_PROTOCOLS.includes(api)) {
|
|
523
|
-
return { kind: 'error', text: `Unknown API protocol: ${api}` };
|
|
524
|
-
}
|
|
525
|
-
const keyRaw = await ctx.tui.prompt(askPrompt({
|
|
526
|
-
title: `Login to ${id}`,
|
|
527
|
-
question: `Paste your ${id} API key`,
|
|
528
|
-
detail: 'Leave empty if the endpoint does not need a key.',
|
|
529
|
-
secret: true,
|
|
530
|
-
submitLabel: 'continue',
|
|
531
|
-
signal: invocation.signal,
|
|
532
|
-
}));
|
|
533
|
-
if (keyRaw === null)
|
|
534
|
-
return { kind: 'success' };
|
|
467
|
+
async function loginCustom(ctx, invocation, prompt) {
|
|
468
|
+
let step = 0, id = '', baseURL = '', api = 'openai-completions';
|
|
535
469
|
let apiKey;
|
|
536
|
-
if (keyRaw.trim() !== '') {
|
|
537
|
-
try {
|
|
538
|
-
apiKey = normalizeApiKey(keyRaw, id);
|
|
539
|
-
}
|
|
540
|
-
catch (error) {
|
|
541
|
-
return { kind: 'error', text: error instanceof Error ? error.message : `${id} login failed.` };
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
470
|
let modelIds;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
471
|
+
while (!invocation.signal.aborted && modelIds === undefined) {
|
|
472
|
+
switch (step) {
|
|
473
|
+
case 0: {
|
|
474
|
+
const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
|
|
475
|
+
title: 'Custom provider', question: 'Provider id',
|
|
476
|
+
detail: 'Lowercase letters, digits, and hyphens. This id is permanent.',
|
|
477
|
+
initialInput: id, onCancelDraft: value => { id = value; }, signal: invocation.signal,
|
|
478
|
+
}), value => {
|
|
479
|
+
const parsed = parseProviderId(value);
|
|
480
|
+
if (reservedProviderIds(ctx).has(parsed))
|
|
481
|
+
throw new Error(uiText(resolveUiLanguage(ctx.tui.prefs?.().uiLanguage), '{name} is a catalog provider. Choose it from the provider list.', { name: parsed }));
|
|
482
|
+
return parsed;
|
|
483
|
+
});
|
|
484
|
+
if (value === null)
|
|
485
|
+
return undefined;
|
|
486
|
+
id = value;
|
|
487
|
+
step = 1;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case 1: {
|
|
491
|
+
const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
|
|
492
|
+
title: 'Custom provider', question: 'Base URL',
|
|
493
|
+
detail: 'Absolute http(s) prefix, for example http://127.0.0.1:11434/v1',
|
|
494
|
+
initialInput: baseURL, onCancelDraft: value => { baseURL = value; }, signal: invocation.signal,
|
|
495
|
+
}), parseBaseURL);
|
|
496
|
+
if (value === null) {
|
|
497
|
+
step = 0;
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
baseURL = value;
|
|
501
|
+
step = 2;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
case 2: {
|
|
505
|
+
const value = await prompt({ localize: true, title: 'Custom provider', question: 'API protocol',
|
|
506
|
+
options: CUSTOM_PROTOCOLS.map(protocol => ({ label: protocol, value: protocol,
|
|
507
|
+
description: protocol === 'openai-completions'
|
|
508
|
+
? 'Chat Completions — Ollama, vLLM, LM Studio, and most gateways'
|
|
509
|
+
: protocol === 'openai-responses' ? 'OpenAI Responses API' : 'Anthropic Messages API' })),
|
|
510
|
+
initialValue: api, allowCustom: false, notify: false, signal: invocation.signal,
|
|
511
|
+
});
|
|
512
|
+
if (value === null) {
|
|
513
|
+
step = 1;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
if (!CUSTOM_PROTOCOLS.includes(value))
|
|
517
|
+
break;
|
|
518
|
+
api = value;
|
|
519
|
+
step = 3;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
case 3: {
|
|
523
|
+
apiKey = undefined;
|
|
524
|
+
const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
|
|
525
|
+
title: `Login to ${id}`, question: `Paste your ${id} API key`,
|
|
526
|
+
detail: 'Leave empty if the endpoint does not need a key.', secret: true,
|
|
527
|
+
submitLabel: 'continue', signal: invocation.signal,
|
|
528
|
+
}), value => value.trim() === '' ? '' : normalizeApiKey(value, id));
|
|
529
|
+
if (value === null) {
|
|
530
|
+
step = 2;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
apiKey = value === '' ? undefined : value;
|
|
534
|
+
step = 4;
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
case 4:
|
|
538
|
+
modelIds = await collectCustomModels(ctx, invocation, { baseURL, api,
|
|
539
|
+
...(apiKey === undefined ? {} : { apiKey }),
|
|
540
|
+
}, prompt);
|
|
541
|
+
if (modelIds === undefined)
|
|
542
|
+
step = 3;
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
559
545
|
}
|
|
546
|
+
if (invocation.signal.aborted || modelIds === undefined)
|
|
547
|
+
return undefined;
|
|
560
548
|
const ref = managedProviderCredentialRef(id);
|
|
561
549
|
if (apiKey !== undefined) {
|
|
562
550
|
const current = await ctx.credentials.describe(ref);
|
|
551
|
+
if (invocation.signal.aborted)
|
|
552
|
+
return undefined;
|
|
563
553
|
if (current.configured && !current.writable) {
|
|
564
554
|
return { kind: 'error', text: unmanagedCredentialMessage(ref, current, 'replace') };
|
|
565
555
|
}
|
|
@@ -570,6 +560,8 @@ async function loginCustom(ctx, invocation) {
|
|
|
570
560
|
return { kind: 'error', text: 'The API key could not be saved to the Harness credential store.' };
|
|
571
561
|
}
|
|
572
562
|
}
|
|
563
|
+
if (invocation.signal.aborted)
|
|
564
|
+
return undefined;
|
|
573
565
|
try {
|
|
574
566
|
await ctx.settings.mutate(PI_AI_SETTINGS, [{
|
|
575
567
|
op: 'set',
|
|
@@ -610,7 +602,7 @@ async function logoutDeepSeek(ctx, invocation) {
|
|
|
610
602
|
if (current.source !== 'file') {
|
|
611
603
|
return { kind: 'success', text: `DeepSeek is configured by ${credentialSource(current)}; the credential store cannot remove it.` };
|
|
612
604
|
}
|
|
613
|
-
const answer = await ctx.tui.prompt({
|
|
605
|
+
const answer = await ctx.tui.prompt({ localize: true,
|
|
614
606
|
title: 'Logout from DeepSeek',
|
|
615
607
|
question: 'Remove the stored DeepSeek API key?',
|
|
616
608
|
detail: 'The next model request will use DEEPSEEK_API_KEY from the launch environment when one is available.',
|
|
@@ -641,43 +633,6 @@ async function logoutDeepSeek(ctx, invocation) {
|
|
|
641
633
|
}
|
|
642
634
|
return { kind: 'success', text: 'Logged out from DeepSeek. The next model request falls back to the environment when available.' };
|
|
643
635
|
}
|
|
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.' };
|
|
680
|
-
}
|
|
681
636
|
async function pickLogoutChoice(ctx, invocation) {
|
|
682
637
|
const choices = await logoutableChoices(ctx);
|
|
683
638
|
if (choices.length === 0)
|
|
@@ -687,7 +642,7 @@ async function pickLogoutChoice(ctx, invocation) {
|
|
|
687
642
|
const values = choices.map(choice => choice === DEEPSEEK_ACCOUNT_CHOICE
|
|
688
643
|
? DEEPSEEK_ACCOUNT_CHOICE
|
|
689
644
|
: isFlowRecordLogout(choice) ? flowChoiceValue(choice.key) : choice.provider);
|
|
690
|
-
const raw = await ctx.tui.prompt({
|
|
645
|
+
const raw = await ctx.tui.prompt({ localize: true,
|
|
691
646
|
presentation: 'card',
|
|
692
647
|
filterable: true,
|
|
693
648
|
title: 'Provider',
|
|
@@ -732,7 +687,7 @@ async function logoutRecord(ctx, invocation, record) {
|
|
|
732
687
|
text: `The stored ${record.label} sign-in is not removable from this session. Update that credential source directly and restart omdsh.`,
|
|
733
688
|
};
|
|
734
689
|
}
|
|
735
|
-
const answer = await ctx.tui.prompt({
|
|
690
|
+
const answer = await ctx.tui.prompt({ localize: true,
|
|
736
691
|
title: `Logout from ${record.label}`,
|
|
737
692
|
question: `Remove the stored ${record.label} sign-in?`,
|
|
738
693
|
detail: 'The local credential record is deleted. Settings profiles and environment credentials stay in place. The issuer is not notified.',
|
|
@@ -773,7 +728,7 @@ async function logoutCatalog(ctx, invocation, entry) {
|
|
|
773
728
|
return { kind: 'error', text: unmanagedCredentialMessage(managedRef, current, 'remove') };
|
|
774
729
|
}
|
|
775
730
|
const removesStoredKey = usesManaged && current.source === 'file';
|
|
776
|
-
const answer = await ctx.tui.prompt({
|
|
731
|
+
const answer = await ctx.tui.prompt({ localize: true,
|
|
777
732
|
title: `Logout from ${label}`,
|
|
778
733
|
question: removesStoredKey
|
|
779
734
|
? `Remove the stored ${label} API key and deactivate the route?`
|
|
@@ -810,172 +765,40 @@ async function logoutCatalog(ctx, invocation, entry) {
|
|
|
810
765
|
}
|
|
811
766
|
return { kind: 'success', text: `Logged out from ${label}.` };
|
|
812
767
|
}
|
|
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
|
-
}
|
|
963
768
|
async function login(ctx, invocation, config) {
|
|
964
769
|
if (invocation.rawInput.trim() !== '') {
|
|
965
770
|
return { kind: 'error', text: 'Usage: /login (paste the key only into the protected prompt)' };
|
|
966
771
|
}
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
772
|
+
const abort = new AbortController();
|
|
773
|
+
invocation = { ...invocation, signal: AbortSignal.any([invocation.signal, abort.signal]) };
|
|
774
|
+
const prompt = request => ctx.tui.prompt({ dismissLabel: 'Back', ...request,
|
|
775
|
+
...(request.detail === undefined ? {} : { detail: request.detail.split('\n').map(line => uiText(resolveUiLanguage(ctx.tui.prefs?.().uiLanguage), line)).join('\n') }),
|
|
776
|
+
signal: invocation.signal, onSuperseded: () => abort.abort(),
|
|
777
|
+
});
|
|
778
|
+
const browse = createPromptBrowser(request => prompt({ ...request, dismissLabel: 'Cancel' }));
|
|
779
|
+
while (!invocation.signal.aborted) {
|
|
780
|
+
const choice = await pickLoginChoice(ctx, invocation, browse);
|
|
781
|
+
if (choice === undefined || invocation.signal.aborted)
|
|
782
|
+
return { kind: 'success' };
|
|
783
|
+
if (choice === DEEPSEEK_ACCOUNT_CHOICE) {
|
|
784
|
+
await openAccountSettings(ctx, invocation.signal, { ...config, signIn: true });
|
|
785
|
+
return { kind: 'success' };
|
|
786
|
+
}
|
|
787
|
+
if (choice === CUSTOM_PROVIDER_VALUE) {
|
|
788
|
+
const result = await loginCustom(ctx, invocation, prompt);
|
|
789
|
+
if (result !== undefined)
|
|
790
|
+
return result;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
if (isAuthorizationFlow(choice))
|
|
794
|
+
return loginAuthorization(ctx, invocation, choice);
|
|
795
|
+
const result = isDeepSeek(choice)
|
|
796
|
+
? await loginDeepSeek(ctx, invocation, config, prompt)
|
|
797
|
+
: await loginCatalog(ctx, invocation, choice, prompt);
|
|
798
|
+
if (result.kind !== 'success' || result.text !== undefined)
|
|
799
|
+
return result;
|
|
800
|
+
}
|
|
801
|
+
return { kind: 'success' };
|
|
979
802
|
}
|
|
980
803
|
async function logout(ctx, invocation) {
|
|
981
804
|
if (invocation.rawInput.trim() !== '')
|
|
@@ -992,6 +815,7 @@ async function logout(ctx, invocation) {
|
|
|
992
815
|
: logoutCatalog(ctx, invocation, choice);
|
|
993
816
|
}
|
|
994
817
|
export function apply(ctx, config = {}) {
|
|
818
|
+
ctx.inject(['deepseekAccount'], accountCtx => { bindAccountNotices(accountCtx); });
|
|
995
819
|
registerCommands(ctx, [
|
|
996
820
|
{
|
|
997
821
|
name: 'login',
|