Spaces:
Running
Running
| import { execFile as defaultExecFile } from 'node:child_process'; | |
| import fs from 'node:fs/promises'; | |
| import path from 'node:path'; | |
| import { pathToFileURL } from 'node:url'; | |
| import { CODEX_HOME } from './codex-config.js'; | |
| export const CODEX_APP_STATE_DB = path.join(CODEX_HOME, 'state_5.sqlite'); | |
| export const MAX_CODEX_APP_TITLE_LENGTH = 120; | |
| export const MAX_CODEX_APP_PREVIEW_LENGTH = 500; | |
| function normalizeText(value, maxLength) { | |
| const text = String(value || '').replaceAll('\0', '').replace(/\s+/g, ' ').trim(); | |
| if (text.length <= maxLength) { | |
| return text; | |
| } | |
| return text.slice(0, maxLength).trimEnd(); | |
| } | |
| function sqlString(value) { | |
| return `'${String(value).replaceAll("'", "''")}'`; | |
| } | |
| function timestampParts(updatedAt) { | |
| const date = updatedAt ? new Date(updatedAt) : new Date(); | |
| const millis = Number.isFinite(date.getTime()) ? date.getTime() : Date.now(); | |
| return { | |
| seconds: Math.floor(millis / 1000), | |
| millis | |
| }; | |
| } | |
| function execFileAsync(execFile, command, args, options) { | |
| return new Promise((resolve, reject) => { | |
| execFile(command, args, options, (error, stdout, stderr) => { | |
| if (error) { | |
| error.stderr = stderr; | |
| reject(error); | |
| return; | |
| } | |
| resolve({ stdout, stderr }); | |
| }); | |
| }); | |
| } | |
| function sqliteReadWriteUri(filePath) { | |
| const url = pathToFileURL(path.resolve(filePath)); | |
| url.searchParams.set('mode', 'rw'); | |
| return url.href; | |
| } | |
| export function buildCodexAppThreadMetadataSql({ threadId, title, preview, updatedAt, cwd }) { | |
| const id = String(threadId || '').trim(); | |
| if (!id) { | |
| throw new TypeError('threadId is required.'); | |
| } | |
| const nextTitle = normalizeText(title, MAX_CODEX_APP_TITLE_LENGTH); | |
| const nextPreview = normalizeText(preview, MAX_CODEX_APP_PREVIEW_LENGTH); | |
| const { seconds, millis } = timestampParts(updatedAt); | |
| const titleSql = nextTitle ? sqlString(nextTitle) : 'title'; | |
| const previewSql = nextPreview ? sqlString(nextPreview) : 'preview'; | |
| const cwdGuard = cwd ? ` AND cwd = ${sqlString(path.resolve(cwd))}` : ''; | |
| const freshnessGuard = ` AND (updated_at_ms IS NULL OR updated_at_ms <= ${millis})`; | |
| return [ | |
| 'PRAGMA busy_timeout = 1000;', | |
| 'UPDATE threads', | |
| 'SET', | |
| ` title = ${titleSql},`, | |
| ` preview = ${previewSql},`, | |
| ` updated_at = CASE WHEN updated_at IS NULL OR updated_at < ${seconds} THEN ${seconds} ELSE updated_at END,`, | |
| ` updated_at_ms = CASE WHEN updated_at_ms IS NULL OR updated_at_ms < ${millis} THEN ${millis} ELSE updated_at_ms END`, | |
| `WHERE id = ${sqlString(id)}${cwdGuard}${freshnessGuard};`, | |
| 'SELECT changes();' | |
| ].join('\n'); | |
| } | |
| export async function syncCodexAppThreadMetadata(params, options = {}) { | |
| const stateDbPath = options.stateDbPath || CODEX_APP_STATE_DB; | |
| const execFile = options.execFile || defaultExecFile; | |
| const sqliteCommand = options.sqliteCommand || process.env.CODEXMOBILE_SQLITE3 || 'sqlite3'; | |
| if (!await pathExists(stateDbPath)) { | |
| return { updated: false, skipped: true, reason: 'state-db-missing' }; | |
| } | |
| const sql = buildCodexAppThreadMetadataSql(params); | |
| let stdout; | |
| try { | |
| ({ stdout } = await execFileAsync(execFile, sqliteCommand, [sqliteReadWriteUri(stateDbPath), sql], { | |
| timeout: options.timeoutMs || 5000, | |
| maxBuffer: 1024 * 1024 | |
| })); | |
| } catch (error) { | |
| if (!await pathExists(stateDbPath)) { | |
| return { updated: false, skipped: true, reason: 'state-db-missing' }; | |
| } | |
| throw error; | |
| } | |
| const changes = Number.parseInt(String(stdout || '').trim().split(/\s+/).pop() || '0', 10); | |
| return { | |
| updated: Number.isFinite(changes) && changes > 0, | |
| skipped: false, | |
| changes: Number.isFinite(changes) ? changes : 0 | |
| }; | |
| } | |
| async function pathExists(filePath) { | |
| try { | |
| await fs.access(filePath); | |
| return true; | |
| } catch (error) { | |
| if (error.code === 'ENOENT') { | |
| return false; | |
| } | |
| throw error; | |
| } | |
| } | |