@toddzheng024/dscode-desktop 0.7.41 → 0.7.44

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 (40) hide show
  1. package/README.md +2 -2
  2. package/cordis.patch.yml +11 -2
  3. package/docs/CHANGELOG.md +39 -0
  4. package/docs/antigravity.md +41 -0
  5. package/docs/distribution.md +6 -4
  6. package/docs/dscode-ultra.md +16 -3
  7. package/docs/openai.md +64 -0
  8. package/docs/releases/0.7.41.md +5 -2
  9. package/docs/releases/0.7.42.md +23 -0
  10. package/docs/releases/0.7.43.md +41 -0
  11. package/docs/releases/0.7.44.md +43 -0
  12. package/docs/tui-commands.md +13 -8
  13. package/package.json +4 -1
  14. package/plugins/antigravity/NOTICE +25 -0
  15. package/plugins/antigravity/adapter.mjs +79 -0
  16. package/plugins/antigravity/client.mjs +73 -0
  17. package/plugins/antigravity/command.mjs +79 -0
  18. package/plugins/antigravity/index.mjs +49 -0
  19. package/plugins/antigravity/models.mjs +80 -0
  20. package/plugins/antigravity/oauth.mjs +130 -0
  21. package/plugins/antigravity/wire.mjs +155 -0
  22. package/plugins/compaction/engine.mjs +4 -3
  23. package/plugins/compaction/threshold.mjs +10 -2
  24. package/plugins/credentials/index.mjs +7 -0
  25. package/plugins/dscode/index.mjs +3 -2
  26. package/plugins/openai/adapter.mjs +52 -0
  27. package/plugins/openai/auth.mjs +81 -0
  28. package/plugins/openai/command.mjs +66 -0
  29. package/plugins/openai/http.mjs +29 -0
  30. package/plugins/openai/index.mjs +58 -0
  31. package/plugins/openai/models.mjs +59 -0
  32. package/plugins/openai/oauth.mjs +82 -0
  33. package/plugins/openai/speed.d.mts +2 -0
  34. package/plugins/openai/speed.mjs +48 -0
  35. package/plugins/openai/store.mjs +44 -0
  36. package/plugins/openai/wire.mjs +93 -0
  37. package/plugins/providers/catalog.mjs +2 -0
  38. package/plugins/providers/startup-commands.d.mts +6 -0
  39. package/plugins/providers/startup-commands.mjs +27 -0
  40. package/vendor/persistent/index.js +3 -20
@@ -0,0 +1,82 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { createLocalJWKSet, jwtVerify } from 'jose';
4
+ import { LlmError } from '@deepseek-ai/dsh-llm';
5
+ import { API, ISSUER, jsonRequest, tokenRequest } from './http.mjs';
6
+ export const SCOPES = 'openid profile email offline_access resource.invoke chatgpt.tokens.use.direct';
7
+ const random = () => randomBytes(32).toString('base64url');
8
+ export function transaction(data, account, consent = false) {
9
+ return { state: random(), nonce: random(), verifier: random(), hostId: data.hostId, revision: data.revision,
10
+ clientId: account?.clientId ?? 'dynamic_agent_client', subject: account?.subject, consent };
11
+ }
12
+ export function authorizationUrl(tx) {
13
+ const params = new URLSearchParams({ client_id: tx.clientId, ext_agent_host_id: tx.hostId, response_type: 'code', redirect_uri: tx.redirectUri,
14
+ scope: SCOPES, resource: API, state: tx.state, nonce: tx.nonce, code_challenge_method: 'S256', code_challenge: createHash('sha256').update(tx.verifier).digest('base64url') });
15
+ if (tx.clientId === 'dynamic_agent_client') params.set('agent_name_hint', 'DSCODE');
16
+ if (tx.consent) params.set('prompt', 'consent');
17
+ // Deliberately omit id_token_hint: a safe fallback URL must contain no tokens.
18
+ return ISSUER + '/api/accounts/authorize?' + params;
19
+ }
20
+ export async function callbackServer(tx, { port = 0, timeoutMs = 300000, signal } = {}) {
21
+ let resolve, reject, timer, settled = false;
22
+ const result = new Promise((a,b) => { resolve = a; reject = b; }); result.catch(() => {});
23
+ const finish = (error, value) => {
24
+ if (settled) return; settled = true; clearTimeout(timer); signal?.removeEventListener('abort', cancel);
25
+ server.close(); server.closeAllConnections?.(); if (error) reject(error); else resolve(value);
26
+ };
27
+ const cancel = () => finish(new Error('OpenAI login cancelled'));
28
+ const server = createServer((req, res) => {
29
+ res.setHeader('content-type', 'text/plain; charset=utf-8'); res.setHeader('cache-control', 'no-store');
30
+ res.setHeader('referrer-policy', 'no-referrer'); res.setHeader('connection', 'close'); res.setHeader('content-security-policy', "default-src 'none'");
31
+ let url; try { url = new URL(req.url, 'http://127.0.0.1'); } catch { res.writeHead(400); res.end(); return; }
32
+ if (req.method !== 'GET' || url.pathname !== '/auth/callback') { res.writeHead(404); res.end(); return; }
33
+ if (url.searchParams.get('state') !== tx.state) { res.writeHead(400); res.end('Invalid state'); return; }
34
+ const code = url.searchParams.get('code'), clientId = url.searchParams.get('client_id') ?? tx.clientId;
35
+ const valid = !url.searchParams.has('error') && code && clientId !== 'dynamic_agent_client'
36
+ && /^[a-zA-Z0-9_-]{1,200}$/.test(clientId) && (tx.clientId === 'dynamic_agent_client' || clientId === tx.clientId);
37
+ res.writeHead(valid ? 200 : 400); res.end(valid ? 'Authorization received. Return to DSCODE and run /openai status.' : 'Authorization failed. Return to DSCODE.');
38
+ res.once('finish', () => finish(valid ? undefined : new Error('OpenAI authorization declined or invalid'), { code, clientId }));
39
+ });
40
+ server.requestTimeout = 5000; server.headersTimeout = 5000;
41
+ signal?.throwIfAborted();
42
+ await new Promise((a,b) => { server.once('error', b); server.listen(port, '127.0.0.1', a); });
43
+ server.on('error', () => finish(new Error('OpenAI callback failed')));
44
+ tx.redirectUri = `http://127.0.0.1:${server.address().port}/auth/callback`;
45
+ timer = setTimeout(() => finish(new Error('OpenAI login expired')), timeoutMs); timer.unref?.();
46
+ signal?.addEventListener('abort', cancel, { once: true }); if (signal?.aborted) cancel();
47
+ return { result, close: cancel, url: authorizationUrl(tx) };
48
+ }
49
+ export class IdentityVerifier {
50
+ constructor(fetcher = globalThis.fetch) { this.fetch = fetcher; }
51
+ async discovery() {
52
+ if (!this.config) {
53
+ const config = await jsonRequest(ISSUER + '/.well-known/openid-configuration', {}, this.fetch);
54
+ if (config.issuer !== ISSUER || ![config.jwks_uri, config.revocation_endpoint].every(u => typeof u === 'string' && new URL(u).origin === ISSUER)) throw new LlmError('Invalid OpenAI discovery document', 'AUTH');
55
+ this.config = config;
56
+ }
57
+ return this.config;
58
+ }
59
+ async verify(token, tx, { refresh = false } = {}) {
60
+ const config = await this.discovery();
61
+ for (let attempt = 0; attempt < 2; attempt++) {
62
+ if (!this.keys || Date.now() - this.fetched > 3600000 || attempt) { this.keys = createLocalJWKSet(await jsonRequest(config.jwks_uri, {}, this.fetch)); this.fetched = Date.now(); }
63
+ try {
64
+ const { payload } = await jwtVerify(token, this.keys, { issuer: ISSUER, audience: tx.clientId, algorithms: ['RS256', 'ES256'], requiredClaims: ['sub','exp','iat'], clockTolerance: 5 });
65
+ if ((!refresh && payload.nonce !== tx.nonce) || typeof payload.sub !== 'string' || !payload.sub || (tx.subject && payload.sub !== tx.subject)) throw Error('Identity mismatch');
66
+ return payload;
67
+ } catch (error) { if (error.code === 'ERR_JWKS_NO_MATCHING_KEY' && !attempt) continue; throw new LlmError('OpenAI identity validation failed', 'AUTH'); }
68
+ }
69
+ }
70
+ }
71
+ export function tokenFields(token, previous = {}) {
72
+ if (typeof token.access_token !== 'string' || !token.access_token || typeof token.refresh_token !== 'string' || !token.refresh_token
73
+ || String(token.token_type).toLowerCase() !== 'bearer' || !Number.isFinite(token.expires_in) || token.expires_in <= 0) throw new LlmError('OpenAI returned incomplete credentials', 'AUTH');
74
+ const scopes = typeof token.scope === 'string' ? token.scope.split(/\s+/) : previous.scopes;
75
+ if (!Array.isArray(scopes)) throw new LlmError('OpenAI did not return granted scopes', 'AUTH');
76
+ return { access: token.access_token, refresh: token.refresh_token, idToken: token.id_token ?? previous.idToken, expires: Date.now() + token.expires_in * 1000, scopes };
77
+ }
78
+ export async function exchangeCode(callback, tx, verifier, fetcher, signal) {
79
+ const token = await tokenRequest({ grant_type: 'authorization_code', client_id: callback.clientId, code: callback.code, code_verifier: tx.verifier, redirect_uri: tx.redirectUri }, fetcher, signal);
80
+ const identity = await verifier.verify(token.id_token, { ...tx, clientId: callback.clientId });
81
+ return { clientId: callback.clientId, subject: identity.sub, email: typeof identity.email === 'string' ? identity.email : undefined, ...tokenFields(token) };
82
+ }
@@ -0,0 +1,2 @@
1
+ /** Speed display shared with the terminal; implementation lives in speed.mjs. */
2
+ export function speedLabel(sessionId: string, modelLabel?: string): string;
@@ -0,0 +1,48 @@
1
+ import { resolveSpeed } from './models.mjs';
2
+ let source;
3
+ export function setSpeedSource(next) { source = next; return () => { if (source === next) source = undefined; }; }
4
+ export function speedLabel(sessionId, modelLabel) { return source?.(sessionId, modelLabel) ?? ''; }
5
+ // Reuse the native command lifecycle, which the persisted session vocabulary
6
+ // understands. Failed commands and status-only queries never select a tier.
7
+ function selection(events) {
8
+ const runs = new Map(); let selected;
9
+ for (const event of events) {
10
+ if (event.type === 'command/run' && event.data.name === 'speed') {
11
+ const tier = event.data.args?.trim();
12
+ if (['standard','default','fast','priority','ultrafast'].includes(tier)) runs.set(event.data.commandId, tier === 'standard' ? 'default' : tier);
13
+ } else if (event.type === 'command/done' && event.data.kind === 'success' && runs.has(event.data.commandId)) {
14
+ selected = { tier: runs.get(event.data.commandId), seq: event.seq };
15
+ }
16
+ }
17
+ return selected;
18
+ }
19
+ export function selectedSpeed(session, fallback = 'default') {
20
+ return selection(session?.snapshotEvents() ?? [])?.tier ?? fallback;
21
+ }
22
+ export const canonicalSpeed = tier => tier === 'priority' ? 'fast' : tier;
23
+ export function speedState(session, model, fallback = 'default') {
24
+ const events = session?.snapshotEvents() ?? [], selected = selection(events);
25
+ const requested = selected?.tier ?? fallback;
26
+ const used = events.findLast(e => e.type === 'assistant/message' && !e.data.interrupted
27
+ && e.data.message?.source?.provider === 'openai' && e.data.message.source.model === model
28
+ && e.data.message.source.replayState?.response?.kind === 'dscode-openai');
29
+ const replay = used?.data.message.source.replayState.response;
30
+ return { requested, actual: used && (!selected || used.seq > selected.seq) && canonicalSpeed(replay.requestedTier) === canonicalSpeed(requested) ? replay.actualTier : undefined };
31
+ }
32
+ export function speedCommand(service) {
33
+ return { name: 'speed', description: 'OpenAI service speed, independent of reasoning effort', input: { hint: '[standard|fast|ultrafast]' }, handler: async ({ agent, rawInput }) => {
34
+ const route = agent.session.requestHeader()?.config ?? agent.options;
35
+ if (route.provider !== 'openai') return { kind: 'error', text: '/speed currently applies to /provider openai.' };
36
+ try {
37
+ const model = await service.models.get(route.model);
38
+ const requested = String(rawInput ?? '').trim();
39
+ if (!requested) {
40
+ const state = speedState(agent.session, route.model, service.options().serviceTier);
41
+ return { kind: 'success', text: `Speed requested: ${state.requested}; last confirmed: ${state.actual ?? 'not yet confirmed'}. Available: ${model.tiers.map(t => t.name).join(', ')}. Fast and Ultrafast consume more usage.` };
42
+ }
43
+ if (agent.status === 'running') return { kind: 'error', text: 'Stop the current turn before changing speed.' };
44
+ const tier = resolveSpeed(model, requested);
45
+ return { kind: 'success', text: `OpenAI speed: ${canonicalSpeed(tier)}. This selection is saved for this session. Higher speeds consume more usage; /effort remains independent.` };
46
+ } catch (error) { return { kind: 'error', text: error.code ? error.message : 'Could not change OpenAI speed.' }; }
47
+ } };
48
+ }
@@ -0,0 +1,44 @@
1
+ import { mkdirSync, openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { tryLockExclusive } from '@deepseek-ai/node-addon-system/flock';
5
+
6
+ /** One stable host identity and atomic account records, serialized across Hosts. */
7
+ export class OpenAIStore {
8
+ constructor(directory) { this.directory = directory; this.path = join(directory, 'accounts.json'); }
9
+ read() {
10
+ try {
11
+ const data = JSON.parse(readFileSync(this.path, 'utf8'));
12
+ if (data.version !== 1 || !Array.isArray(data.accounts) || typeof data.hostId !== 'string') throw Error();
13
+ return data;
14
+ } catch (error) {
15
+ if (error.code === 'ENOENT') return undefined;
16
+ throw new Error('Cannot read OpenAI account storage', { cause: error });
17
+ }
18
+ }
19
+ write(data) {
20
+ mkdirSync(this.directory, { recursive: true, mode: 0o700 });
21
+ const temp = join(this.directory, '.' + randomUUID());
22
+ try { writeFileSync(temp, JSON.stringify(data), { mode: 0o600, flag: 'wx' }); renameSync(temp, this.path); }
23
+ finally { try { unlinkSync(temp); } catch { /* renamed or not created */ } }
24
+ }
25
+ async locked(action, signal) {
26
+ mkdirSync(this.directory, { recursive: true, mode: 0o700 });
27
+ const fd = openSync(join(this.directory, '.guard'), 'a', 0o600), deadline = Date.now() + 45000;
28
+ try {
29
+ for (;;) {
30
+ signal?.throwIfAborted();
31
+ try { await tryLockExclusive(fd); break; }
32
+ catch (error) {
33
+ if (!['EAGAIN', 'EWOULDBLOCK'].includes(error.code)) throw error;
34
+ if (Date.now() >= deadline) throw Error('OpenAI account storage is busy; retry', { cause: error });
35
+ await new Promise(resolve => setTimeout(resolve, 25));
36
+ }
37
+ }
38
+ signal?.throwIfAborted();
39
+ const data = this.read() ?? { version: 1, hostId: 'urn:uuid:' + randomUUID(), mode: 'chatgpt', revision: 0, accounts: [] };
40
+ return await action(data);
41
+ } finally { closeSync(fd); }
42
+ }
43
+ initialize() { return this.locked(data => { this.write(data); return data; }); }
44
+ }
@@ -0,0 +1,93 @@
1
+ import { LlmError } from '@deepseek-ai/dsh-llm';
2
+ import { serializeMessages } from '../providers/chat-messages.mjs';
3
+ import { resolveSpeed } from './models.mjs';
4
+ import { responseError } from './http.mjs';
5
+ export const REPLAY_KIND = 'dscode-openai';
6
+ export function requestBody(options, model, auth, requested, images) {
7
+ const input = [], system = [options.system].filter(Boolean);
8
+ for (const message of options.messages) {
9
+ if (message.role === 'system') { system.push(message.content.filter(b => b.type === 'text').map(b => b.text).join('\n')); continue; }
10
+ const replay = message.source?.replayState?.response;
11
+ if (message.role === 'assistant' && message.source?.provider === 'openai' && message.source.model === model.id && replay?.kind === REPLAY_KIND && replay.account === auth.key && Array.isArray(replay.items)) { input.push(...replay.items); continue; }
12
+ for (const wire of serializeMessages([message], { model: model.id, images, dialect: { provider: 'openai', reasoningContent: () => false } })) {
13
+ if (wire.role === 'tool') { input.push({ type: 'function_call_output', call_id: wire.tool_call_id, output: wire.content }); continue; }
14
+ const content = typeof wire.content === 'string' ? wire.content ? [{ type: wire.role === 'assistant' ? 'output_text' : 'input_text', text: wire.content }] : []
15
+ : wire.content.map(p => p.type === 'text' ? { type: 'input_text', text: p.text } : { type: 'input_image', image_url: p.image_url.url, detail: 'auto' });
16
+ if (content.length) input.push({ role: wire.role === 'system' ? 'developer' : wire.role, content });
17
+ for (const call of wire.tool_calls ?? []) input.push({ type: 'function_call', namespace: 'dscode', call_id: call.id, name: call.function.name, arguments: call.function.arguments });
18
+ }
19
+ }
20
+ const effort = options.reasoningEffort ?? model.defaultEffort;
21
+ if (effort !== undefined && !model.efforts.includes(effort)) throw new LlmError('This OpenAI model does not advertise that reasoning effort', 'UNSUPPORTED_REASONING_EFFORT');
22
+ const tools = (options.tools ?? []).map(t => ({ type: 'function', name: t.name, description: t.description, parameters: t.parameters, strict: false }));
23
+ return { model: model.id, stream: true, store: false, input, include: ['reasoning.encrypted_content'], service_tier: resolveSpeed(model, requested),
24
+ ...(system.length ? { instructions: system.join('\n\n') } : {}), ...(effort ? { reasoning: { effort, summary: 'auto' } } : {}),
25
+ ...(tools.length ? { tools: [{ type: 'namespace', name: 'dscode', description: 'DSCODE local tools', tools }] } : {}),
26
+ ...(auth.mode === 'api-key' ? { max_output_tokens: Math.min(options.maxTokens ?? model.maxTokens, model.maxTokens), ...(options.temperature !== undefined && !effort ? { temperature: options.temperature } : {}) } : {}) };
27
+ }
28
+ const usageOf = u => u && Number.isFinite(u.input_tokens) && Number.isFinite(u.output_tokens) ? {
29
+ inputTokens: Math.max(0, u.input_tokens - (u.input_tokens_details?.cached_tokens ?? 0)), outputTokens: u.output_tokens,
30
+ cacheReadTokens: u.input_tokens_details?.cached_tokens ?? 0, reasoningTokens: u.output_tokens_details?.reasoning_tokens ?? 0,
31
+ ...(Number.isFinite(u.total_tokens) ? { totalTokens: u.total_tokens } : {}) } : undefined;
32
+ /** Stream visible deltas, but commit tool calls only after response.completed. */
33
+ export async function* translate(payloads, { model, account, requested }) {
34
+ let completed;
35
+ const completedItems = new Map();
36
+ const started = new Map(); let nextIndex = 0;
37
+ for await (const raw of payloads) {
38
+ if (raw === '[DONE]') break;
39
+ let e; try { e = JSON.parse(raw); } catch { throw new LlmError('Malformed OpenAI stream', 'MALFORMED_RESPONSE'); }
40
+ if (e.type === 'error' || e.type === 'response.failed') throw responseError(e.response?.error?.code?.includes('usage_limit') ? 429 : 400, { error: e.error ?? e.response?.error ?? e }, e.response?.id);
41
+ if (e.type === 'response.incomplete') throw new LlmError('OpenAI response was incomplete; no tool calls were committed', 'INCOMPLETE_RESPONSE');
42
+ if (e.type === 'response.completed') { completed = e.response; continue; }
43
+ if (completed && /delta|output_item/.test(e.type)) throw new LlmError('OpenAI emitted content after completion', 'MALFORMED_RESPONSE');
44
+ if (e.type === 'response.output_item.done') {
45
+ if (!Number.isSafeInteger(e.output_index) || e.output_index < 0 || !e.item || typeof e.item !== 'object'
46
+ || completedItems.has(e.output_index) || e.item.status && e.item.status !== 'completed') throw new LlmError('Invalid completed OpenAI output item', 'MALFORMED_RESPONSE');
47
+ completedItems.set(e.output_index, e.item);
48
+ continue;
49
+ }
50
+ const kind = e.type === 'response.output_text.delta' ? 'text' : e.type === 'response.reasoning_summary_text.delta' ? 'reasoning' : undefined;
51
+ if (kind) {
52
+ const key = `${e.output_index}:${kind}`;
53
+ if (!started.has(key)) { const index = nextIndex++; started.set(key, index); yield { type: 'block-start', index, blockType: kind }; }
54
+ yield { type: kind === 'text' ? 'text-delta' : 'reasoning-delta', index: started.get(key), text: e.delta };
55
+ }
56
+ }
57
+ if (!completed || completed.status !== 'completed' || !Array.isArray(completed.output)) throw new LlmError('OpenAI stream ended before completion', 'TRANSPORT');
58
+ // The direct route may send an empty output array in the terminal envelope.
59
+ // In that case use fully completed items, never argument/text deltas. The
60
+ // response.completed gate above still controls all tool commits.
61
+ const itemEntries = [...completedItems].sort(([a], [b]) => a - b);
62
+ if (!completed.output.length && itemEntries.some(([index], position) => index !== position)) throw new LlmError('OpenAI completion omitted output items', 'MALFORMED_RESPONSE');
63
+ const output = completed.output.length ? completed.output : itemEntries.map(([, item]) => item), blocks = [];
64
+ let hasTools = false;
65
+ for (let i = 0; i < output.length; i++) {
66
+ const item = output[i]; let block;
67
+ if (item.type === 'message') {
68
+ if (item.content?.some(p => p.type === 'refusal')) throw new LlmError('OpenAI declined the response', 'CONTENT_FILTER');
69
+ if (!Array.isArray(item.content) || item.content.some(p => p.type !== 'output_text')) throw new LlmError('Unsupported OpenAI output content', 'UNSUPPORTED_CONTENT');
70
+ block = { type: 'text', text: item.content.map(p => p.text).join('') };
71
+ } else if (item.type === 'reasoning') block = { type: 'reasoning', text: (item.summary ?? []).map(p => p.text ?? '').join('') };
72
+ else if (item.type === 'function_call') {
73
+ if (item.namespace && item.namespace !== 'dscode') throw new LlmError('Unknown OpenAI tool namespace', 'MALFORMED_RESPONSE');
74
+ let args; try { args = JSON.parse(item.arguments); } catch { throw new LlmError('Invalid OpenAI tool arguments', 'MALFORMED_RESPONSE'); }
75
+ if (!args || typeof args !== 'object' || Array.isArray(args) || typeof item.name !== 'string' || !item.name || typeof item.call_id !== 'string' || !item.call_id) throw new LlmError('Invalid OpenAI tool call', 'MALFORMED_RESPONSE');
76
+ block = { type: 'tool-call', id: item.call_id, name: item.name, arguments: item.arguments }; hasTools = true;
77
+ } else throw new LlmError('Unsupported OpenAI output item', 'UNSUPPORTED_CONTENT');
78
+ const key = `${i}:${block.type}`;
79
+ let index = started.get(key);
80
+ const needsStart = index === undefined;
81
+ if (needsStart) index = nextIndex++;
82
+ blocks.push({ index, block, needsStart });
83
+ }
84
+ if (!blocks.length || !blocks.some(b => b.block.type === 'tool-call' || b.block.text)) throw new LlmError('OpenAI returned no content', 'EMPTY_RESPONSE');
85
+ // Validate every returned item before emitting any committed block.
86
+ if ([...started.values()].some(index => !blocks.some(b => b.index === index))) throw new LlmError('OpenAI completion omitted streamed content', 'MALFORMED_RESPONSE');
87
+ for (const { index, block, needsStart } of blocks.sort((a,b) => a.index-b.index)) {
88
+ if (needsStart) yield { type: 'block-start', index, blockType: block.type };
89
+ yield { type: 'block-end', index, block };
90
+ }
91
+ const usage = usageOf(completed.usage); if (usage) yield { type: 'usage', usage };
92
+ yield { type: 'finish', reason: { kind: hasTools ? 'tool-calls' : 'stop' }, replayState: { response: { kind: REPLAY_KIND, model, account, items: output, requestedTier: requested, actualTier: completed.service_tier }, blocks: blocks.sort((a,b) => a.index-b.index).map(b => ({ type: b.block.type })) } };
93
+ }
@@ -12,6 +12,8 @@ export const PROVIDERS = Object.freeze([
12
12
  // The OpenCode Go subscription: its chat-completions models, authorised only by an OpenCode
13
13
  // account login. `login` names the command that sets the credential up; there is no key to paste.
14
14
  { id: 'opencode-go', name: 'OpenCode Go', aliases: ['opencode-go', 'opencode', 'go'], credentialRef: 'OPENCODE_OAUTH', login: '/opencode login', defaultModel: 'kimi-k3' },
15
+ { id: 'antigravity', name: 'Antigravity', aliases: ['antigravity', 'google-antigravity', 'agy'], credentialRef: 'ANTIGRAVITY_OAUTH', login: '/antigravity login', defaultModel: 'gemini-3.8-flash' },
16
+ { id: 'openai', name: 'OpenAI / ChatGPT', aliases: ['openai', 'codex', 'chatgpt'], credentialRef: 'OPENAI_API_KEY', defaultModel: 'gpt-6.1-sol' },
15
17
  ]);
16
18
 
17
19
  // The pi-ai adapter served OpenRouter until 0.7.6, from this settings section.
@@ -0,0 +1,6 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { CommandDefinition, CommandDescriptor } from '@deepseek-ai/dsh-commands';
3
+ /** Register a global Host command whose metadata is safe to show before a session exists. */
4
+ export function registerStartupCommand(ctx: Context, definition: CommandDefinition): () => void;
5
+ export function startupCommands(ctx: Context): readonly CommandDescriptor[];
6
+ export function watchStartupCommands(ctx: Context, listener: () => void): () => void;
@@ -0,0 +1,27 @@
1
+ // Discovery for Host commands before the terminal has a receiving Agent.
2
+ // Execution remains in the native registry; only immutable descriptors live here.
3
+ const catalogs = new WeakMap();
4
+ function catalog(ctx) {
5
+ let value = catalogs.get(ctx.root);
6
+ if (!value) { value = { entries: new Set(), listeners: new Set() }; catalogs.set(ctx.root, value); }
7
+ return value;
8
+ }
9
+ function notify(value) { for (const listener of value.listeners) listener(); }
10
+ export function registerStartupCommand(ctx, definition) {
11
+ const unregister = ctx.commands.register(definition);
12
+ const value = catalog(ctx);
13
+ const descriptor = Object.freeze({ name: definition.name, description: definition.description,
14
+ ...(definition.input ? { input: Object.freeze({ ...definition.input }) } : {}) });
15
+ value.entries.add(descriptor); notify(value);
16
+ const remove = () => { if (value.entries.delete(descriptor)) notify(value); };
17
+ ctx.effect(() => remove);
18
+ return () => { remove(); unregister(); };
19
+ }
20
+ export function startupCommands(ctx) {
21
+ return [...new Map([...catalog(ctx).entries].map(d => [d.name, d])).values()].sort((a, b) => a.name.localeCompare(b.name));
22
+ }
23
+ export function watchStartupCommands(ctx, listener) {
24
+ const value = catalog(ctx);
25
+ value.listeners.add(listener);
26
+ return () => value.listeners.delete(listener);
27
+ }
@@ -1,7 +1,5 @@
1
+ // dscode-noninteractive-bash-v1
1
2
  // dscode-shell-capture-v1
2
- // dscode-stdin-stall-v2
3
- const STDIN_STALL_MS = 5000;
4
- const STDIN_STALL_NOTE = "\n[Interrupted after 5s with no output: the command held the terminal without producing anything, and this tool cannot supply terminal input. Give it stdin (a heredoc, `< file` or `< /dev/null`) or use a non-interactive form.]";
5
3
  // dscode-shell-reset-v1
6
4
  import { randomUUID } from "node:crypto";
7
5
  import z from "@deepseek-ai/schemastery";
@@ -94,7 +92,7 @@ function quoteForBash(value) {
94
92
  return `$'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'").replaceAll("\r", "\\r").replaceAll("\n", "\\n")}'`;
95
93
  }
96
94
  function wrapCommand(command, marker) {
97
- return `printf '%s\\n' ${quoteForBash(marker.start)}; eval -- ${quoteForBash(command)}; __dsh_persistent_bash_status=$?; printf '%s%s\\n' ${quoteForBash(marker.end)} "$__dsh_persistent_bash_status"`;
95
+ return `printf '%s\\n' ${quoteForBash(marker.start)}; eval -- ${quoteForBash(command)} < /dev/null; __dsh_persistent_bash_status=$?; printf '%s%s\\n' ${quoteForBash(marker.end)} "$__dsh_persistent_bash_status"`;
98
96
  }
99
97
  function trimTrailingNewline(text) {
100
98
  return text.replace(/(?:\r?\n)+$/, "");
@@ -267,9 +265,6 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
267
265
  }
268
266
  const marker = markers();
269
267
  const wrapped = wrapCommand(command, marker);
270
- const startedAt = Date.now();
271
- let stdinWaited = false;
272
- let stdinStalled = false;
273
268
  let first = true;
274
269
  let fallback = "";
275
270
  let fallbackTruncated = false;
@@ -312,21 +307,9 @@ async function executeCommand(ctx, shells, owner, command, config, upstream) {
312
307
  }
313
308
  if (captured.text.includes(marker.end)) {
314
309
  const complete = commandOutput(captured, marker);
315
- if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars) + (stdinStalled ? STDIN_STALL_NOTE : "");
310
+ if (complete !== void 0) return renderCaptured(complete, config.maxOutputChars);
316
311
  }
317
312
  if (result.sessionStatus.kind === "exited") return await respondToSessionExit(ctx, shells, owner, id, result.sessionStatus, marker, fallback, fallbackTruncated, config);
318
- const partial = renderCaptured(partialOutput(captured, marker, fallback, fallbackTruncated), config.maxOutputChars);
319
- if (result.waitReason === "stdin_read") stdinWaited = true;
320
- if (result.waitReason === "stdin_read" && partial.length > 0) return partial;
321
- if (stdinWaited && partial.length === 0 && !stdinStalled && Date.now() - startedAt >= STDIN_STALL_MS) {
322
- stdinStalled = true;
323
- try {
324
- await ctx.terminals.signal(owner, id, "SIGINT");
325
- } catch (_unsignallableForeground) {
326
- stdinStalled = false;
327
- return partial;
328
- }
329
- }
330
313
  await pause();
331
314
  }
332
315
  } catch (e_1) {