| ... | @@ -14,6 +14,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson"; | ... | @@ -14,6 +14,7 @@ import { decodeJsonResult } from "@t3tools/shared/schemaJson"; |
| 14 | import { | 14 | import { |
| 15 | query as claudeQuery, | 15 | query as claudeQuery, |
| 16 | type SlashCommand as ClaudeSlashCommand, | 16 | type SlashCommand as ClaudeSlashCommand, |
| | 17 | type SDKUserMessage, |
| 17 | } from "@anthropic-ai/claude-agent-sdk"; | 18 | } from "@anthropic-ai/claude-agent-sdk"; |
| 18 | | 19 | |
| 19 | import { | 20 | import { |
| ... | @@ -480,13 +481,24 @@ function dedupeSlashCommands( | ... | @@ -480,13 +481,24 @@ function dedupeSlashCommands( |
| 480 | return [...commandsByName.values()]; | 481 | return [...commandsByName.values()]; |
| 481 | } | 482 | } |
| 482 | | 483 | |
| | 484 | function waitForAbortSignal(signal: AbortSignal): Promise<void> { |
| | 485 | if (signal.aborted) { |
| | 486 | return Promise.resolve(); |
| | 487 | } |
| | 488 | return new Promise((resolve) => { |
| | 489 | signal.addEventListener("abort", () => resolve(), { once: true }); |
| | 490 | }); |
| | 491 | } |
| | 492 | |
| 483 | /** | 493 | /** |
| 484 | * Probe account information by spawning a lightweight Claude Agent SDK | 494 | * Probe account information by spawning a lightweight Claude Agent SDK |
| 485 | * session and reading the initialization result. | 495 | * session and reading the initialization result. |
| 486 | * | 496 | * |
| 487 | * The prompt is never sent to the Anthropic API — we abort immediately | 497 | * We pass a never-yielding AsyncIterable as the prompt so that no user |
| 488 | * after the local initialization phase completes. This gives us the | 498 | * message is ever written to the subprocess stdin. This means the Claude |
| 489 | * user's subscription type without incurring any token cost. | 499 | * Code subprocess completes its local initialization IPC (returning |
| | 500 | * account info and slash commands) but never starts an API request to |
| | 501 | * Anthropic. We read the init data and then abort the subprocess. |
| 490 | * | 502 | * |
| 491 | * This is used as a fallback when `claude auth status` does not include | 503 | * This is used as a fallback when `claude auth status` does not include |
| 492 | * subscription type information. | 504 | * subscription type information. |
| ... | @@ -495,12 +507,16 @@ const probeClaudeCapabilities = (binaryPath: string) => { | ... | @@ -495,12 +507,16 @@ const probeClaudeCapabilities = (binaryPath: string) => { |
| 495 | const abort = new AbortController(); | 507 | const abort = new AbortController(); |
| 496 | return Effect.tryPromise(async () => { | 508 | return Effect.tryPromise(async () => { |
| 497 | const q = claudeQuery({ | 509 | const q = claudeQuery({ |
| 498 | prompt: ".", | 510 | // Never yield — we only need initialization data, not a conversation. |
| | 511 | // This prevents any prompt from reaching the Anthropic API. |
| | 512 | // oxlint-disable-next-line require-yield |
| | 513 | prompt: (async function* (): AsyncGenerator<SDKUserMessage> { |
| | 514 | await waitForAbortSignal(abort.signal); |
| | 515 | })(), |
| 499 | options: { | 516 | options: { |
| 500 | persistSession: false, | 517 | persistSession: false, |
| 501 | pathToClaudeCodeExecutable: binaryPath, | 518 | pathToClaudeCodeExecutable: binaryPath, |
| 502 | abortController: abort, | 519 | abortController: abort, |
| 503 | maxTurns: 0, | | |
| 504 | settingSources: ["user", "project", "local"], | 520 | settingSources: ["user", "project", "local"], |
| 505 | allowedTools: [], | 521 | allowedTools: [], |
| 506 | stderr: () => {}, | 522 | stderr: () => {}, |