also looking at this
feat(engine): answer texts with Codex as well as Claude Code
#7
18 files
+1080
-142
| @@ -1,9 +1,11 @@ | ||
| 1 | import type { EngineId } from '../Uplink/engine' | |
| 1 | 2 | import { copyFileSync, existsSync, watch } from 'node:fs' |
| 2 | 3 | import process from 'node:process' |
| 3 | 4 | import { defineCommand } from '@stacksjs/cli' |
| 4 | import { loadConfig, systemPrompt } from '../Uplink/config' | |
| 5 | import { runChecks, writeHeartbeat } from '../Uplink/doctor' | |
| 6 | import { ClaudeEngine } from '../Uplink/engine' | |
| 5 | import { loadConfig } from '../Uplink/config' | |
| 6 | import { checksPass, runChecks, writeHeartbeat } from '../Uplink/doctor' | |
| 7 | import { ENGINE_IDS, isEngineId } from '../Uplink/engine' | |
| 8 | import { engineFor, selectedEngine } from '../Uplink/engines' | |
| 7 | 9 | import { formatDuration, toPlainText } from '../Uplink/format' |
| 8 | 10 | import { MessagesAccessError, MessagesDb } from '../Uplink/messages-db' |
| 9 | 11 | import { ModelStore } from '../Uplink/model-store' |
| @@ -18,16 +20,6 @@ import { detectWorkdir } from '../Uplink/workdir' | ||
| 18 | 20 | const HEARTBEAT_MS = 15_000 |
| 19 | 21 | const RETRY_OPEN_MS = 30_000 |
| 20 | 22 | |
| 21 | function engineFor(config: ReturnType<typeof loadConfig>): ClaudeEngine { | |
| 22 | return new ClaudeEngine({ | |
| 23 | bin: config.claudeBin, | |
| 24 | model: config.model, | |
| 25 | permissionMode: config.permissionMode, | |
| 26 | systemPrompt: systemPrompt(config), | |
| 27 | timeoutMs: config.timeoutMs, | |
| 28 | }) | |
| 29 | } | |
| 30 | ||
| 31 | 23 | async function openMessages(config: ReturnType<typeof loadConfig>, appDir: string, startedAt: number): Promise<MessagesDb> { |
| 32 | 24 | // Without Full Disk Access this fails. Under launchd, exiting would only |
| 33 | 25 | // restart-loop, so wait for the grant instead and say why in the heartbeat. |
| @@ -85,7 +77,7 @@ export default defineCommand((cli) => { | ||
| 85 | 77 | config, |
| 86 | 78 | messages, |
| 87 | 79 | sender: new AppleScriptSender(), |
| 88 | engine: engineFor(config), | |
| 80 | engine: selectedEngine(config), | |
| 89 | 81 | store: new ModelStore(`${appDir}/storage/uplink/cursor`), |
| 90 | 82 | }) |
| 91 | 83 | await uplink.start() |
| @@ -134,11 +126,19 @@ export default defineCommand((cli) => { | ||
| 134 | 126 | cli |
| 135 | 127 | .command('uplink:ask <prompt>', 'Run one prompt through the agent exactly as a text would, and print the reply') |
| 136 | 128 | .option('--cwd <dir>', 'Working directory (default: detected from the prompt)') |
| 137 | .action(async (prompt: string, options: { cwd?: string }) => { | |
| 129 | .option('--engine <name>', 'claude or codex (default: UPLINK_ENGINE)') | |
| 130 | .action(async (prompt: string, options: { cwd?: string, engine?: string }) => { | |
| 138 | 131 | const config = loadConfig() |
| 139 | 132 | const cwd = options.cwd ?? detectWorkdir(prompt, config.workdir) |
| 140 | console.log(`[uplink] running in ${cwd}`) | |
| 141 | const run = engineFor(config).run({ | |
| 133 | // Naming an engine here is how the other one gets tried without editing | |
| 134 | // .env, which is the whole point of this command. | |
| 135 | if (options.engine && !isEngineId(options.engine)) { | |
| 136 | console.error(`[uplink] unknown engine "${options.engine}". Use ${ENGINE_IDS.join(' or ')}.`) | |
| 137 | process.exit(1) | |
| 138 | } | |
| 139 | const engine = options.engine ? engineFor(options.engine as EngineId, config) : selectedEngine(config) | |
| 140 | console.log(`[uplink] running in ${cwd} on ${engine.label}`) | |
| 141 | const run = engine.run({ | |
| 142 | 142 | prompt, |
| 143 | 143 | cwd, |
| 144 | 144 | onEvent: (event) => { |
| @@ -161,7 +161,7 @@ export default defineCommand((cli) => { | ||
| 161 | 161 | if (!check.ok && check.fix) |
| 162 | 162 | console.log(` → ${check.fix}`) |
| 163 | 163 | } |
| 164 | process.exit(checks.every(c => c.ok) ? 0 : 1) | |
| 164 | process.exit(checksPass(checks) ? 0 : 1) | |
| 165 | 165 | }) |
| 166 | 166 | |
| 167 | 167 | cli |