9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
145 lines
6.8 KiB
JavaScript
145 lines
6.8 KiB
JavaScript
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
import {
|
|
CLAUDE_DOMAIN, CLAUDE_URL, COMPOSER_SELECTOR, MESSAGE_SELECTOR,
|
|
ensureOnClaude, selectModel, setAdaptiveThinking,
|
|
sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
|
|
ensureClaudeComposer, requireNonEmptyPrompt, requirePositiveInt,
|
|
} from './utils.js';
|
|
|
|
export const askCommand = cli({
|
|
site: 'claude',
|
|
name: 'ask',
|
|
access: 'write',
|
|
description: 'Send a prompt to Claude and get the response',
|
|
domain: CLAUDE_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: 'sonnet', choices: ['sonnet', 'opus', 'haiku'], help: 'Model to use: sonnet, opus, or haiku' },
|
|
{ name: 'think', type: 'boolean', default: false, help: 'Enable Adaptive thinking' },
|
|
{ name: 'file', help: 'Attach a file (image, PDF, text) with the prompt' },
|
|
],
|
|
columns: ['response'],
|
|
|
|
func: async (page, kwargs) => {
|
|
const prompt = requireNonEmptyPrompt(kwargs.prompt, 'claude ask');
|
|
const timeoutSeconds = requirePositiveInt(
|
|
Number(kwargs.timeout ?? 120),
|
|
'claude ask --timeout',
|
|
'Example: opencli claude ask "hello" --timeout 120',
|
|
);
|
|
const timeoutMs = timeoutSeconds * 1000;
|
|
const wantThink = parseBoolFlag(kwargs.think);
|
|
|
|
if (parseBoolFlag(kwargs.new)) {
|
|
await page.goto(CLAUDE_URL);
|
|
try {
|
|
await page.wait({ selector: COMPOSER_SELECTOR, timeout: 8 });
|
|
} catch {
|
|
// Composer didn't mount; ensureClaudeComposer below surfaces a typed error.
|
|
}
|
|
} else {
|
|
const navigated = await ensureOnClaude(page);
|
|
if (navigated) {
|
|
// Workspace was recycled; try to resume the most recent
|
|
// conversation instead of starting a new one.
|
|
await page.evaluate(`(() => {
|
|
var link = document.querySelector('a[href*="/chat/"]');
|
|
if (link) link.click();
|
|
})()`);
|
|
// Wait for the resumed conversation to render messages, or
|
|
// fall through if the link click had no effect (no recents).
|
|
try {
|
|
await page.wait({ selector: MESSAGE_SELECTOR, timeout: 5 });
|
|
} catch {
|
|
// No prior conversation; ensureClaudeComposer still requires composer below.
|
|
}
|
|
}
|
|
}
|
|
|
|
// ensureClaudeComposer reads composer presence directly via getPageState,
|
|
// so the previous standalone 2 s settle is redundant.
|
|
await withRetry(() => ensureClaudeComposer(page, 'Claude ask requires a visible composer on the current page.'));
|
|
|
|
// 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('/chat/');
|
|
const modelExplicit = kwargs.__opencliOptionSources?.model === 'cli';
|
|
|
|
const wantModel = kwargs.model || 'sonnet';
|
|
if (inConversation && modelExplicit) {
|
|
throw new ArgumentError(
|
|
`Cannot switch to ${wantModel} model inside an existing conversation.`,
|
|
'Re-run with --new to start a fresh chat before selecting a model.',
|
|
);
|
|
}
|
|
|
|
if (!inConversation) {
|
|
const modelResult = await withRetry(() => selectModel(page, wantModel));
|
|
if (!modelResult?.ok) {
|
|
if (modelResult?.upgrade) {
|
|
throw new ArgumentError(
|
|
`${wantModel} model requires a paid Claude plan.`,
|
|
'Pick --model sonnet or --model haiku, or upgrade your account.',
|
|
);
|
|
}
|
|
throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
|
|
}
|
|
// Post-toggle settle dropped — the next CDP eval (setAdaptiveThinking) gives
|
|
// React enough time to flush aria-checked updates between rountrips.
|
|
}
|
|
|
|
const thinkResult = await withRetry(() => setAdaptiveThinking(page, wantThink));
|
|
if (!thinkResult?.ok && wantThink) {
|
|
throw new CommandExecutionError('Could not enable Adaptive thinking');
|
|
}
|
|
// Post-toggle settle dropped — the next CDP eval (sendMessage / sendWithFile)
|
|
// gives React enough time to flush aria-checked updates.
|
|
|
|
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;
|
|
}
|
|
// Pre-waitForResponse settle dropped — waitForResponse's first 3 s polling
|
|
// tick covers the same window without an unconditional sleep.
|
|
const result = await waitForResponse(page, baseline, prompt, timeoutMs);
|
|
if (!result) {
|
|
throw new EmptyResultError(
|
|
'claude ask',
|
|
`No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
|
|
);
|
|
}
|
|
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);
|
|
if (!result) {
|
|
throw new EmptyResultError(
|
|
'claude ask',
|
|
`No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
|
|
);
|
|
}
|
|
return [{ response: result }];
|
|
},
|
|
});
|