cxs is a local-first CLI for searching Codex session logs. It is designed for progressive retrieval: find the right session first, then read
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(cli): 友好提示缺失索引

cat 38460231 5417bbd2

+113 -59
+105 -58
cli.ts
··· 9 9 migrateLegacyCacheDirIfNeeded, 10 10 resolveCodexDir, 11 11 } from "./env"; 12 + import { IndexUnavailableError } from "./db"; 12 13 13 14 // One-shot migration from legacy ~/.cache/cxs/ to ~/.local/state/cxs/. Runs 14 15 // before any subcommand so `cxs stats` etc. see the migrated db, not just ··· 121 122 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 122 123 .option("--json", "输出 JSON") 123 124 .action((query, options) => { 124 - const limit = parsePositiveInt(options.limit, 10); 125 - const result = findSessions(options.db, query, limit); 126 - if (options.json) { 127 - console.log(JSON.stringify(result, null, 2)); 128 - return; 129 - } 130 - printFindResults(result.query, result.results); 125 + runReadCommand(Boolean(options.json), () => { 126 + const limit = parsePositiveInt(options.limit, 10); 127 + const result = findSessions(options.db, query, limit); 128 + if (options.json) { 129 + console.log(JSON.stringify(result, null, 2)); 130 + return; 131 + } 132 + printFindResults(result.query, result.results); 133 + }); 131 134 }); 132 135 133 136 program ··· 140 143 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 141 144 .option("--json", "输出 JSON") 142 145 .action((sessionUuid, options) => { 143 - const result = getMessageRange(options.db, sessionUuid, { 144 - seq: optionalInt(options.seq), 145 - query: options.query, 146 - before: parsePositiveInt(options.before, 2), 147 - after: parsePositiveInt(options.after, 2), 146 + runReadCommand(Boolean(options.json), () => { 147 + const result = getMessageRange(options.db, sessionUuid, { 148 + seq: optionalInt(options.seq), 149 + query: options.query, 150 + before: parsePositiveInt(options.before, 2), 151 + after: parsePositiveInt(options.after, 2), 152 + }); 153 + if (options.json) { 154 + console.log(JSON.stringify(result, null, 2)); 155 + return; 156 + } 157 + printReadRangeResult( 158 + result.session, 159 + result.anchorSeq, 160 + result.messages, 161 + result.rangeStartSeq, 162 + result.rangeEndSeq, 163 + ); 148 164 }); 149 - if (options.json) { 150 - console.log(JSON.stringify(result, null, 2)); 151 - return; 152 - } 153 - printReadRangeResult( 154 - result.session, 155 - result.anchorSeq, 156 - result.messages, 157 - result.rangeStartSeq, 158 - result.rangeEndSeq, 159 - ); 160 165 }); 161 166 162 167 program ··· 167 172 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 168 173 .option("--json", "输出 JSON") 169 174 .action((sessionUuid, options) => { 170 - const result = getMessagePage( 171 - options.db, 172 - sessionUuid, 173 - parseNonNegativeInt(options.offset, 0), 174 - parsePositiveInt(options.limit, 20), 175 - ); 176 - if (options.json) { 177 - console.log(JSON.stringify(result, null, 2)); 178 - return; 179 - } 180 - printReadPage( 181 - result.session, 182 - result.offset, 183 - result.limit, 184 - result.totalCount, 185 - result.hasMore, 186 - result.messages, 187 - ); 175 + runReadCommand(Boolean(options.json), () => { 176 + const result = getMessagePage( 177 + options.db, 178 + sessionUuid, 179 + parseNonNegativeInt(options.offset, 0), 180 + parsePositiveInt(options.limit, 20), 181 + ); 182 + if (options.json) { 183 + console.log(JSON.stringify(result, null, 2)); 184 + return; 185 + } 186 + printReadPage( 187 + result.session, 188 + result.offset, 189 + result.limit, 190 + result.totalCount, 191 + result.hasMore, 192 + result.messages, 193 + ); 194 + }); 188 195 }); 189 196 190 197 program ··· 197 204 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 198 205 .option("--json", "输出 JSON") 199 206 .action((options) => { 200 - const sort = normalizeListSort(options.sort); 201 - const result = listSessionSummaries(options.db, { 202 - cwd: options.cwd, 203 - since: options.since, 204 - sort, 205 - limit: parsePositiveInt(options.limit, 20), 207 + runReadCommand(Boolean(options.json), () => { 208 + const sort = normalizeListSort(options.sort); 209 + const result = listSessionSummaries(options.db, { 210 + cwd: options.cwd, 211 + since: options.since, 212 + sort, 213 + limit: parsePositiveInt(options.limit, 20), 214 + }); 215 + if (options.json) { 216 + console.log(JSON.stringify(result, null, 2)); 217 + return; 218 + } 219 + printSessionList(result.results); 206 220 }); 207 - if (options.json) { 208 - console.log(JSON.stringify(result, null, 2)); 209 - return; 210 - } 211 - printSessionList(result.results); 212 221 }); 213 222 214 223 program ··· 217 226 .option("--db <path>", "覆盖默认数据库路径", DEFAULT_DB_PATH) 218 227 .option("--json", "输出 JSON") 219 228 .action((options) => { 220 - const summary = collectStats(options.db); 221 - if (options.json) { 222 - console.log(JSON.stringify(summary, null, 2)); 223 - return; 224 - } 225 - printStats(summary); 229 + runReadCommand(Boolean(options.json), () => { 230 + const summary = collectStats(options.db); 231 + if (options.json) { 232 + console.log(JSON.stringify(summary, null, 2)); 233 + return; 234 + } 235 + printStats(summary); 236 + }); 226 237 }); 227 238 228 239 program.parse(); ··· 245 256 function normalizeListSort(value: string | undefined): SessionListSort { 246 257 if (value === "started" || value === "messages") return value; 247 258 return "ended"; 259 + } 260 + 261 + function runReadCommand(jsonMode: boolean, action: () => void): void { 262 + try { 263 + action(); 264 + } catch (error) { 265 + if (error instanceof IndexUnavailableError) { 266 + emitIndexUnavailableError(error, jsonMode); 267 + return; 268 + } 269 + throw error; 270 + } 271 + } 272 + 273 + function emitIndexUnavailableError(error: IndexUnavailableError, jsonMode: boolean): void { 274 + const hint = 275 + "Run `cxs sync` first to create the index. No separate init command is needed; sync initializes and updates it."; 276 + if (jsonMode) { 277 + console.log( 278 + JSON.stringify( 279 + { 280 + error: { 281 + code: "index_unavailable", 282 + message: error.message, 283 + dbPath: error.dbPath, 284 + hint, 285 + }, 286 + }, 287 + null, 288 + 2, 289 + ), 290 + ); 291 + } else { 292 + console.error(`${error.message}\n${hint}`); 293 + } 294 + process.exitCode = 1; 248 295 } 249 296 250 297 function emitCurrentError(error: CurrentStateDbError, jsonMode: boolean): void {
+8 -1
db.ts
··· 15 15 16 16 const BUSY_TIMEOUT_MS = 5000; 17 17 18 + export class IndexUnavailableError extends Error { 19 + constructor(public readonly dbPath: string) { 20 + super(`index not found: ${dbPath}`); 21 + this.name = "IndexUnavailableError"; 22 + } 23 + } 24 + 18 25 export function openReadDb(dbPath: string): Db { 19 26 if (!existsSync(dbPath)) { 20 - throw new Error(`index not found: ${dbPath}; run cxs sync first`); 27 + throw new IndexUnavailableError(dbPath); 21 28 } 22 29 23 30 const db = new Database(dbPath, { readonly: true });