ReviewOS

also looking at this

stacks/uplink

feat(engine): answer texts with Codex as well as Claude Code

#7
Merged glennmichael123 wants to merge feat/codex-engine into main
18 files +1080 -142
app/Commands/Uplink.tsmodified+18-18
Changes to app/Commands/Uplink.ts
@@ -1,9 +1,11 @@
1import type { EngineId } from '../Uplink/engine'
12import { copyFileSync, existsSync, watch } from 'node:fs'
23import process from 'node:process'
34import { defineCommand } from '@stacksjs/cli'
4import { loadConfig, systemPrompt } from '../Uplink/config'
5import { runChecks, writeHeartbeat } from '../Uplink/doctor'
6import { ClaudeEngine } from '../Uplink/engine'
5import { loadConfig } from '../Uplink/config'
6import { checksPass, runChecks, writeHeartbeat } from '../Uplink/doctor'
7import { ENGINE_IDS, isEngineId } from '../Uplink/engine'
8import { engineFor, selectedEngine } from '../Uplink/engines'
79import { formatDuration, toPlainText } from '../Uplink/format'
810import { MessagesAccessError, MessagesDb } from '../Uplink/messages-db'
911import { ModelStore } from '../Uplink/model-store'
@@ -18,16 +20,6 @@ import { detectWorkdir } from '../Uplink/workdir'
1820const HEARTBEAT_MS = 15_000
1921const RETRY_OPEN_MS = 30_000
2022
21function 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
3123async function openMessages(config: ReturnType<typeof loadConfig>, appDir: string, startedAt: number): Promise<MessagesDb> {
3224 // Without Full Disk Access this fails. Under launchd, exiting would only
3325 // restart-loop, so wait for the grant instead and say why in the heartbeat.
@@ -85,7 +77,7 @@ export default defineCommand((cli) => {
8577 config,
8678 messages,
8779 sender: new AppleScriptSender(),
88 engine: engineFor(config),
80 engine: selectedEngine(config),
8981 store: new ModelStore(`${appDir}/storage/uplink/cursor`),
9082 })
9183 await uplink.start()
@@ -134,11 +126,19 @@ export default defineCommand((cli) => {
134126 cli
135127 .command('uplink:ask <prompt>', 'Run one prompt through the agent exactly as a text would, and print the reply')
136128 .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 }) => {
138131 const config = loadConfig()
139132 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({
142142 prompt,
143143 cwd,
144144 onEvent: (event) => {
@@ -161,7 +161,7 @@ export default defineCommand((cli) => {
161161 if (!check.ok && check.fix)
162162 console.log(` → ${check.fix}`)
163163 }
164 process.exit(checks.every(c => c.ok) ? 0 : 1)
164 process.exit(checksPass(checks) ? 0 : 1)
165165 })
166166
167167 cli