import { cli, Strategy } from '@jackwener/opencli/registry'; import { CliError, CommandExecutionError, EXIT_CODES, TimeoutError } from '@jackwener/opencli/errors'; import { DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature, sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry, pickResumeUrl, TEXTAREA_SELECTOR, } from './utils.js'; export const askCommand = cli({ site: 'deepseek', name: 'ask', access: 'write', description: 'Send a prompt to DeepSeek and get the response', domain: DEEPSEEK_DOMAIN, strategy: Strategy.COOKIE, browser: true, siteSession: 'persistent', navigateBefore: false, args: [ { name: 'prompt', positional: true, required: true, help: 'Prompt to send' }, { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' }, { name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' }, { name: 'model', default: 'instant', choices: ['instant', 'expert', 'vision'], help: 'Model to use: instant, expert, or vision' }, { name: 'think', type: 'boolean', default: false, help: 'Enable DeepThink mode' }, { name: 'search', type: 'boolean', default: false, help: 'Enable web search' }, { name: 'file', help: 'Attach a file (PDF, image, text) with the prompt' }, ], // columns omitted: derived from row keys so non-think output shows only 'response' func: async (page, kwargs) => { const prompt = kwargs.prompt; const timeoutMs = (kwargs.timeout || 120) * 1000; const wantThink = parseBoolFlag(kwargs.think); const wantSearch = parseBoolFlag(kwargs.search); const wantModel = kwargs.model || 'instant'; if ((wantModel === 'vision' || wantModel === 'expert') && wantSearch) { throw new CliError( 'ARGUMENT', `DeepSeek ${wantModel} mode does not support --search.`, 'Run without --search, or use --model instant for web search.', EXIT_CODES.USAGE_ERROR, ); } if (parseBoolFlag(kwargs.new)) { await page.goto(DEEPSEEK_URL); // Wait for the composer to mount instead of a fixed 3 s sleep. try { await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 8 }); } catch { // Selector still missing → downstream selectModel/sendMessage // will surface the failure with a typed error. } } else { const navigated = await ensureOnDeepSeek(page); if (navigated) { // Pinned conversations sit in their own DOM section and are // skipped so the resume never lands on a topped chat. const resumeUrl = await pickResumeUrl(page); if (!resumeUrl) { throw new CommandExecutionError( 'Workspace was recycled but no prior conversation could be loaded', 'Pass --new to start a fresh chat, or wait for the sidebar to populate before retrying.', ); } await page.goto(resumeUrl); try { await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 5 }); } catch { // Conversation page may still be loading; subsequent steps // will retry or report. } } } // Model selector is only available on the new-chat page, not inside // an existing conversation. Skip it when we resumed a prior thread. const currentUrl = await page.evaluate('window.location.href') || ''; const inConversation = currentUrl.includes('/a/chat/s/'); const modelExplicit = kwargs.__opencliOptionSources?.model === 'cli'; if (inConversation && modelExplicit) { throw new CliError( 'ARGUMENT', `Cannot switch to ${wantModel} model inside an existing conversation.`, 'Re-run with --new to start a fresh chat before selecting a model.', EXIT_CODES.USAGE_ERROR, ); } if (!inConversation) { const modelResult = await withRetry(() => selectModel(page, wantModel)); if (!modelResult?.ok) { throw new CommandExecutionError(`Could not switch to ${wantModel} model`); } // The 0.5 s settle previously here was redundant: each subsequent // step (setFeature, sendMessage) issues a fresh CDP eval, giving // React more than enough time to flush the toggle's state update. } const thinkResult = await withRetry(() => setFeature(page, 'DeepThink', wantThink)); if (!thinkResult?.ok && wantThink) { throw new CommandExecutionError('Could not enable DeepThink'); } // Only instant mode has the Search toggle in the DeepSeek UI. let searchResult; if (wantModel !== 'vision' && wantModel !== 'expert') { searchResult = await withRetry(() => setFeature(page, 'Search', wantSearch)); if (!searchResult?.ok && wantSearch) { throw new CommandExecutionError('Could not enable Search'); } } // No settle wait after toggles: the next CDP eval below already gives // React time to flush the aria-checked state. if (kwargs.file) { const baseline = await withRetry(() => getBubbleCount(page)); try { const fileResult = await sendWithFile(page, kwargs.file, prompt); if (fileResult && !fileResult.ok) { throw new CommandExecutionError(fileResult.reason || 'Failed to attach file'); } } catch (err) { // SPA navigates after send; "Promise was collected" means send succeeded if (!String(err?.message || err).includes('Promise was collected')) throw err; } // waitForResponse polls every 3 s for new bubbles, so the previous // 3 s settle here was a redundant sleep on top of the first poll. const result = await waitForResponse(page, baseline, prompt, timeoutMs, wantThink); if (!result) { throw new TimeoutError('deepseek ask', kwargs.timeout, 'No DeepSeek reply observed before timeout. Retry with --timeout increased.'); } if (wantThink && typeof result === 'object' && result.response !== undefined) { return [result]; } return [{ response: result }]; } const baseline = await withRetry(() => getBubbleCount(page)); const sendResult = await withRetry(() => sendMessage(page, prompt)); if (!sendResult?.ok) { throw new CommandExecutionError(sendResult?.reason || 'Failed to send message'); } const result = await waitForResponse(page, baseline, prompt, timeoutMs, wantThink); if (!result) { throw new TimeoutError('deepseek ask', kwargs.timeout, 'No DeepSeek reply observed before timeout. Retry with --timeout increased.'); } if (wantThink && typeof result === 'object' && result.response !== undefined) { return [result]; } return [{ response: result }]; }, });