@linzumi/cli 0.0.50-beta → 0.0.52-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +746 -353
- package/package.json +4 -2
- package/scripts/codex_history_table.mjs +628 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linzumi/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.52-beta",
|
|
4
4
|
"description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
"README.md",
|
|
11
11
|
"bin",
|
|
12
12
|
"dist",
|
|
13
|
-
"docs/images"
|
|
13
|
+
"docs/images",
|
|
14
|
+
"scripts"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "rm -rf dist && bun build src/index.ts --target=node --format=esm --outfile=dist/index.js --external ws --external undici && mkdir -p dist/assets && cp src/assets/linzumi-logo.svg dist/assets/linzumi-logo.svg",
|
|
18
|
+
"codex:history-table": "node scripts/codex_history_table.mjs",
|
|
17
19
|
"test": "bun test",
|
|
18
20
|
"prepack": "bun run build",
|
|
19
21
|
"pack:dry-run": "npm pack --dry-run"
|
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createServer } from 'node:net';
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const NON_INTERACTIVE_SOURCE_KINDS = [
|
|
9
|
+
'exec',
|
|
10
|
+
'appServer',
|
|
11
|
+
'subAgent',
|
|
12
|
+
'subAgentReview',
|
|
13
|
+
'subAgentCompact',
|
|
14
|
+
'subAgentThreadSpawn',
|
|
15
|
+
'subAgentOther',
|
|
16
|
+
'unknown',
|
|
17
|
+
];
|
|
18
|
+
const SOURCE_KIND_PASSES = [
|
|
19
|
+
{
|
|
20
|
+
label: 'interactive-default',
|
|
21
|
+
sourceKinds: null,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: 'explicit-non-interactive',
|
|
25
|
+
sourceKinds: NON_INTERACTIVE_SOURCE_KINDS,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
30
|
+
const DEFAULT_TURN_PAGE_SIZE = 100;
|
|
31
|
+
const DEFAULT_RPC_TIMEOUT_MS = 30_000;
|
|
32
|
+
|
|
33
|
+
function parseArgs(rawArgv) {
|
|
34
|
+
const argv = rawArgv[0] === '--' ? rawArgv.slice(1) : rawArgv;
|
|
35
|
+
const options = {
|
|
36
|
+
codexBin: 'codex',
|
|
37
|
+
format: 'markdown',
|
|
38
|
+
outputPath: null,
|
|
39
|
+
pageSize: DEFAULT_PAGE_SIZE,
|
|
40
|
+
maxRows: null,
|
|
41
|
+
includeArchived: true,
|
|
42
|
+
includeTurnCount: false,
|
|
43
|
+
useStateDbOnly: true,
|
|
44
|
+
cwdFilters: [],
|
|
45
|
+
rpcTimeoutMs: DEFAULT_RPC_TIMEOUT_MS,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
49
|
+
const arg = argv[index];
|
|
50
|
+
|
|
51
|
+
switch (arg) {
|
|
52
|
+
case '--help':
|
|
53
|
+
case '-h':
|
|
54
|
+
return { kind: 'help' };
|
|
55
|
+
|
|
56
|
+
case '--codex-bin':
|
|
57
|
+
options.codexBin = requiredValue(argv, index, arg);
|
|
58
|
+
index += 1;
|
|
59
|
+
break;
|
|
60
|
+
|
|
61
|
+
case '--format':
|
|
62
|
+
options.format = requiredValue(argv, index, arg);
|
|
63
|
+
index += 1;
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case '--out':
|
|
67
|
+
options.outputPath = requiredValue(argv, index, arg);
|
|
68
|
+
index += 1;
|
|
69
|
+
break;
|
|
70
|
+
|
|
71
|
+
case '--page-size':
|
|
72
|
+
options.pageSize = parsePositiveInteger(requiredValue(argv, index, arg), arg);
|
|
73
|
+
index += 1;
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case '--max':
|
|
77
|
+
options.maxRows = parsePositiveInteger(requiredValue(argv, index, arg), arg);
|
|
78
|
+
index += 1;
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case '--cwd':
|
|
82
|
+
options.cwdFilters.push(requiredValue(argv, index, arg));
|
|
83
|
+
index += 1;
|
|
84
|
+
break;
|
|
85
|
+
|
|
86
|
+
case '--rpc-timeout-ms':
|
|
87
|
+
options.rpcTimeoutMs = parsePositiveInteger(requiredValue(argv, index, arg), arg);
|
|
88
|
+
index += 1;
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case '--include-turn-count':
|
|
92
|
+
options.includeTurnCount = true;
|
|
93
|
+
break;
|
|
94
|
+
|
|
95
|
+
case '--exclude-archived':
|
|
96
|
+
options.includeArchived = false;
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case '--scan-rollouts':
|
|
100
|
+
options.useStateDbOnly = false;
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!['markdown', 'tsv', 'csv', 'json'].includes(options.format)) {
|
|
109
|
+
throw new Error(`--format must be markdown, tsv, csv, or json: ${options.format}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { kind: 'run', options };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function requiredValue(argv, index, flag) {
|
|
116
|
+
const value = argv[index + 1];
|
|
117
|
+
|
|
118
|
+
if (value === undefined || value.startsWith('--')) {
|
|
119
|
+
throw new Error(`${flag} requires a value`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parsePositiveInteger(value, flag) {
|
|
126
|
+
const parsed = Number.parseInt(value, 10);
|
|
127
|
+
|
|
128
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
129
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return parsed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function usage() {
|
|
136
|
+
return [
|
|
137
|
+
'Usage: node packages/linzumi-cli/scripts/codex_history_table.mjs [options]',
|
|
138
|
+
'',
|
|
139
|
+
'Lists Codex conversations from the default CODEX_HOME through codex app-server.',
|
|
140
|
+
'',
|
|
141
|
+
'Options:',
|
|
142
|
+
' --out <path> Write the table/report to a file instead of stdout.',
|
|
143
|
+
' --format <kind> markdown, tsv, csv, or json. Default: markdown.',
|
|
144
|
+
' --page-size <n> thread/list page size. Default: 25.',
|
|
145
|
+
' --max <n> Stop after n rows for a quick smoke test.',
|
|
146
|
+
' --cwd <path> Optional exact cwd filter. Repeatable.',
|
|
147
|
+
' --include-turn-count Count turns with thread/turns/list. Slower.',
|
|
148
|
+
' --exclude-archived Skip archived conversations.',
|
|
149
|
+
' --scan-rollouts Let app-server scan/repair rollout metadata.',
|
|
150
|
+
' --rpc-timeout-ms <n> Per-request timeout. Default: 30000.',
|
|
151
|
+
' --codex-bin <path> Codex executable. Default: codex.',
|
|
152
|
+
'',
|
|
153
|
+
'Examples:',
|
|
154
|
+
' node packages/linzumi-cli/scripts/codex_history_table.mjs --max 20',
|
|
155
|
+
' node packages/linzumi-cli/scripts/codex_history_table.mjs --out output/codex-conversations.md',
|
|
156
|
+
].join('\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function chooseLoopbackPort() {
|
|
160
|
+
return await new Promise((resolve, reject) => {
|
|
161
|
+
const server = createServer();
|
|
162
|
+
|
|
163
|
+
server.on('error', reject);
|
|
164
|
+
server.listen(0, '127.0.0.1', () => {
|
|
165
|
+
const address = server.address();
|
|
166
|
+
|
|
167
|
+
if (typeof address === 'object' && address !== null) {
|
|
168
|
+
const port = address.port;
|
|
169
|
+
server.close((error) => (error === undefined ? resolve(port) : reject(error)));
|
|
170
|
+
} else {
|
|
171
|
+
server.close();
|
|
172
|
+
reject(new Error('failed to allocate loopback port'));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function startCodexAppServer(codexBin) {
|
|
179
|
+
const port = await chooseLoopbackPort();
|
|
180
|
+
const url = `ws://127.0.0.1:${port}`;
|
|
181
|
+
const child = spawn(codexBin, ['app-server', '--listen', url], {
|
|
182
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
detached: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
let stderr = '';
|
|
187
|
+
|
|
188
|
+
const ready = new Promise((resolve, reject) => {
|
|
189
|
+
let output = '';
|
|
190
|
+
const handleOutput = (chunk) => {
|
|
191
|
+
const text = String(chunk);
|
|
192
|
+
output += text;
|
|
193
|
+
|
|
194
|
+
if (output.includes('listening on:')) {
|
|
195
|
+
resolve();
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
child.stdout.on('data', handleOutput);
|
|
200
|
+
child.stderr.on('data', (chunk) => {
|
|
201
|
+
stderr += String(chunk);
|
|
202
|
+
handleOutput(chunk);
|
|
203
|
+
});
|
|
204
|
+
child.once('exit', (code, signal) => {
|
|
205
|
+
reject(
|
|
206
|
+
new Error(
|
|
207
|
+
`codex app-server exited before ready: ${code ?? signal ?? 'unknown'}\n${stderr}`
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await ready;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
url,
|
|
217
|
+
stop: () => stopProcessGroup(child),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function stopProcessGroup(child) {
|
|
222
|
+
if (child.pid !== undefined) {
|
|
223
|
+
try {
|
|
224
|
+
process.kill(-child.pid, 'SIGINT');
|
|
225
|
+
return;
|
|
226
|
+
} catch (_error) {
|
|
227
|
+
child.kill('SIGINT');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
child.kill('SIGINT');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function connectWebSocket(url) {
|
|
236
|
+
const websocket = new WebSocket(url);
|
|
237
|
+
|
|
238
|
+
await new Promise((resolve, reject) => {
|
|
239
|
+
websocket.addEventListener('open', resolve, { once: true });
|
|
240
|
+
websocket.addEventListener('error', reject, { once: true });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return websocket;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function createJsonRpcClient(websocket, timeoutMs) {
|
|
247
|
+
let nextId = 1;
|
|
248
|
+
const pending = new Map();
|
|
249
|
+
|
|
250
|
+
websocket.addEventListener('message', (event) => {
|
|
251
|
+
const message = JSON.parse(String(event.data));
|
|
252
|
+
const request = pending.get(message.id);
|
|
253
|
+
|
|
254
|
+
if (request !== undefined) {
|
|
255
|
+
pending.delete(message.id);
|
|
256
|
+
clearTimeout(request.timer);
|
|
257
|
+
request.resolve(message);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
websocket.addEventListener('close', () => {
|
|
262
|
+
for (const request of pending.values()) {
|
|
263
|
+
clearTimeout(request.timer);
|
|
264
|
+
request.reject(new Error('codex app-server websocket closed'));
|
|
265
|
+
}
|
|
266
|
+
pending.clear();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
request: (method, params) => {
|
|
271
|
+
const id = nextId;
|
|
272
|
+
nextId += 1;
|
|
273
|
+
websocket.send(JSON.stringify({ jsonrpc: '2.0', id, method, params }));
|
|
274
|
+
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
pending.delete(id);
|
|
278
|
+
reject(new Error(`timeout waiting for ${method}`));
|
|
279
|
+
}, timeoutMs);
|
|
280
|
+
pending.set(id, { resolve, reject, timer });
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
notify: (method, params) => {
|
|
284
|
+
websocket.send(JSON.stringify({ jsonrpc: '2.0', method, params }));
|
|
285
|
+
},
|
|
286
|
+
close: () => websocket.close(),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function initialize(client) {
|
|
291
|
+
const response = await client.request('initialize', {
|
|
292
|
+
clientInfo: {
|
|
293
|
+
name: 'linzumi-codex-history-table',
|
|
294
|
+
version: '0.0.0',
|
|
295
|
+
},
|
|
296
|
+
capabilities: {
|
|
297
|
+
experimentalApi: true,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (response.error !== undefined) {
|
|
302
|
+
throw new Error(`initialize failed: ${response.error.message}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
client.notify('initialized', {});
|
|
306
|
+
|
|
307
|
+
return response.result ?? {};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function listAllThreads(client, options) {
|
|
311
|
+
const rowsByKey = new Map();
|
|
312
|
+
const archivedModes = options.includeArchived ? [false, true] : [false];
|
|
313
|
+
|
|
314
|
+
for (const archived of archivedModes) {
|
|
315
|
+
for (const sourceKindPass of SOURCE_KIND_PASSES) {
|
|
316
|
+
const passRows = await listThreadPass(client, options, archived, sourceKindPass);
|
|
317
|
+
|
|
318
|
+
for (const row of passRows) {
|
|
319
|
+
rowsByKey.set(row.id || row.path, row);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const rows = Array.from(rowsByKey.values()).sort(compareRowsByUpdatedAtDesc);
|
|
325
|
+
|
|
326
|
+
return options.maxRows === null ? rows : rows.slice(0, options.maxRows);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function listThreadPass(client, options, archived, sourceKindPass) {
|
|
330
|
+
const rows = [];
|
|
331
|
+
let cursor = null;
|
|
332
|
+
|
|
333
|
+
while (true) {
|
|
334
|
+
const response = await client.request('thread/list', {
|
|
335
|
+
limit: options.pageSize,
|
|
336
|
+
...(cursor === null ? {} : { cursor }),
|
|
337
|
+
sortKey: 'updated_at',
|
|
338
|
+
sortDirection: 'desc',
|
|
339
|
+
...(sourceKindPass.sourceKinds === null
|
|
340
|
+
? {}
|
|
341
|
+
: { sourceKinds: sourceKindPass.sourceKinds }),
|
|
342
|
+
archived,
|
|
343
|
+
useStateDbOnly: options.useStateDbOnly,
|
|
344
|
+
...(options.cwdFilters.length === 0
|
|
345
|
+
? {}
|
|
346
|
+
: {
|
|
347
|
+
cwd:
|
|
348
|
+
options.cwdFilters.length === 1
|
|
349
|
+
? options.cwdFilters[0]
|
|
350
|
+
: options.cwdFilters,
|
|
351
|
+
}),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (response.error !== undefined) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`thread/list failed for ${sourceKindPass.label}: ${response.error.message}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result = response.result ?? {};
|
|
361
|
+
const data = Array.isArray(result.data) ? result.data : [];
|
|
362
|
+
|
|
363
|
+
for (const thread of data) {
|
|
364
|
+
rows.push(threadToRow(thread, archived));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
|
|
368
|
+
|
|
369
|
+
if (
|
|
370
|
+
cursor === null ||
|
|
371
|
+
data.length === 0 ||
|
|
372
|
+
(options.maxRows !== null && rows.length >= options.maxRows)
|
|
373
|
+
) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return rows;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function compareRowsByUpdatedAtDesc(left, right) {
|
|
382
|
+
return (
|
|
383
|
+
Date.parse(right.updatedAt || right.createdAt || '') -
|
|
384
|
+
Date.parse(left.updatedAt || left.createdAt || '')
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function threadToRow(thread, archived) {
|
|
389
|
+
const gitInfo = asRecord(thread.gitInfo);
|
|
390
|
+
const preview = stringValue(thread.preview);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
id: stringValue(thread.id),
|
|
394
|
+
updatedAt: timestampFromSeconds(thread.updatedAt),
|
|
395
|
+
createdAt: timestampFromSeconds(thread.createdAt),
|
|
396
|
+
source: stringValue(thread.source),
|
|
397
|
+
archived,
|
|
398
|
+
turnCount: '',
|
|
399
|
+
previewChars: preview.length,
|
|
400
|
+
modelProvider: stringValue(thread.modelProvider),
|
|
401
|
+
cliVersion: stringValue(thread.cliVersion),
|
|
402
|
+
branch: stringValue(gitInfo?.branch),
|
|
403
|
+
origin: stringValue(gitInfo?.originUrl),
|
|
404
|
+
cwd: stringValue(thread.cwd),
|
|
405
|
+
name: stringValue(thread.name),
|
|
406
|
+
preview: preview.replace(/\s+/g, ' ').trim(),
|
|
407
|
+
path: stringValue(thread.path),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function addTurnCounts(client, rows) {
|
|
412
|
+
for (const row of rows) {
|
|
413
|
+
row.turnCount = String(await countThreadTurns(client, row.id));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function countThreadTurns(client, threadId) {
|
|
418
|
+
let cursor = null;
|
|
419
|
+
let count = 0;
|
|
420
|
+
|
|
421
|
+
while (true) {
|
|
422
|
+
const response = await client.request('thread/turns/list', {
|
|
423
|
+
threadId,
|
|
424
|
+
...(cursor === null ? {} : { cursor }),
|
|
425
|
+
limit: DEFAULT_TURN_PAGE_SIZE,
|
|
426
|
+
sortDirection: 'asc',
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (response.error !== undefined) {
|
|
430
|
+
throw new Error(`thread/turns/list failed for ${threadId}: ${response.error.message}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = response.result ?? {};
|
|
434
|
+
const data = Array.isArray(result.data) ? result.data : [];
|
|
435
|
+
count += data.length;
|
|
436
|
+
cursor = typeof result.nextCursor === 'string' ? result.nextCursor : null;
|
|
437
|
+
|
|
438
|
+
if (cursor === null || data.length === 0) {
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return count;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function stringValue(value) {
|
|
447
|
+
return typeof value === 'string' ? value : '';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function asRecord(value) {
|
|
451
|
+
return typeof value === 'object' && value !== null ? value : undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function timestampFromSeconds(value) {
|
|
455
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
456
|
+
return '';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return new Date(value * 1000).toISOString();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function buildReport(rows, metadata, format) {
|
|
463
|
+
switch (format) {
|
|
464
|
+
case 'json':
|
|
465
|
+
return `${JSON.stringify({ metadata, rows }, null, 2)}\n`;
|
|
466
|
+
|
|
467
|
+
case 'csv':
|
|
468
|
+
return tableAsDelimited(rows, ',');
|
|
469
|
+
|
|
470
|
+
case 'tsv':
|
|
471
|
+
return tableAsDelimited(rows, '\t');
|
|
472
|
+
|
|
473
|
+
case 'markdown':
|
|
474
|
+
return tableAsMarkdown(rows, metadata);
|
|
475
|
+
|
|
476
|
+
default:
|
|
477
|
+
throw new Error(`unsupported format: ${format}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function tableColumns() {
|
|
482
|
+
return [
|
|
483
|
+
'updatedAt',
|
|
484
|
+
'createdAt',
|
|
485
|
+
'source',
|
|
486
|
+
'archived',
|
|
487
|
+
'turnCount',
|
|
488
|
+
'previewChars',
|
|
489
|
+
'modelProvider',
|
|
490
|
+
'branch',
|
|
491
|
+
'cwd',
|
|
492
|
+
'name',
|
|
493
|
+
'preview',
|
|
494
|
+
'id',
|
|
495
|
+
'path',
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function tableAsMarkdown(rows, metadata) {
|
|
500
|
+
const columns = tableColumns();
|
|
501
|
+
const header = [
|
|
502
|
+
`Codex home: ${metadata.codexHome || 'unknown'}`,
|
|
503
|
+
`Rows: ${rows.length}`,
|
|
504
|
+
`Generated at: ${metadata.generatedAt}`,
|
|
505
|
+
'',
|
|
506
|
+
`| ${columns.join(' | ')} |`,
|
|
507
|
+
`| ${columns.map(() => '---').join(' | ')} |`,
|
|
508
|
+
];
|
|
509
|
+
|
|
510
|
+
const body = rows.map((row) => {
|
|
511
|
+
const values = columns.map((column) => markdownCell(formatCell(row[column], column)));
|
|
512
|
+
return `| ${values.join(' | ')} |`;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
return `${[...header, ...body].join('\n')}\n`;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function tableAsDelimited(rows, delimiter) {
|
|
519
|
+
const columns = tableColumns();
|
|
520
|
+
const body = rows.map((row) =>
|
|
521
|
+
columns.map((column) => delimitedCell(row[column], delimiter)).join(delimiter)
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
return `${columns.join(delimiter)}\n${body.join('\n')}\n`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function formatCell(value, column) {
|
|
528
|
+
const text = String(value ?? '');
|
|
529
|
+
|
|
530
|
+
switch (column) {
|
|
531
|
+
case 'id':
|
|
532
|
+
return text.slice(0, 8);
|
|
533
|
+
|
|
534
|
+
case 'preview':
|
|
535
|
+
return truncate(text, 120);
|
|
536
|
+
|
|
537
|
+
case 'path':
|
|
538
|
+
case 'origin':
|
|
539
|
+
return truncate(text, 90);
|
|
540
|
+
|
|
541
|
+
default:
|
|
542
|
+
return text;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function truncate(value, maxLength) {
|
|
547
|
+
if (value.length <= maxLength) {
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function markdownCell(value) {
|
|
555
|
+
return String(value).replaceAll('|', '\\|').replace(/\n/g, ' ');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function delimitedCell(value, delimiter) {
|
|
559
|
+
const text = String(value ?? '');
|
|
560
|
+
|
|
561
|
+
if (delimiter === '\t') {
|
|
562
|
+
return text.replace(/\t/g, ' ').replace(/\n/g, ' ');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const escaped = text.replaceAll('"', '""');
|
|
566
|
+
return `"${escaped}"`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function run(options) {
|
|
570
|
+
const server = await startCodexAppServer(options.codexBin);
|
|
571
|
+
let client;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const websocket = await connectWebSocket(server.url);
|
|
575
|
+
client = createJsonRpcClient(websocket, options.rpcTimeoutMs);
|
|
576
|
+
const init = await initialize(client);
|
|
577
|
+
const rows = await listAllThreads(client, options);
|
|
578
|
+
|
|
579
|
+
if (options.includeTurnCount) {
|
|
580
|
+
await addTurnCounts(client, rows);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const metadata = {
|
|
584
|
+
codexHome: stringValue(init.codexHome),
|
|
585
|
+
generatedAt: new Date().toISOString(),
|
|
586
|
+
rowCount: rows.length,
|
|
587
|
+
sourceKindStrategy:
|
|
588
|
+
'default interactive pass plus explicit non-interactive sourceKinds pass',
|
|
589
|
+
nonInteractiveSourceKinds: NON_INTERACTIVE_SOURCE_KINDS,
|
|
590
|
+
includeArchived: options.includeArchived,
|
|
591
|
+
includeTurnCount: options.includeTurnCount,
|
|
592
|
+
useStateDbOnly: options.useStateDbOnly,
|
|
593
|
+
cwdFilters: options.cwdFilters,
|
|
594
|
+
};
|
|
595
|
+
const report = buildReport(rows, metadata, options.format);
|
|
596
|
+
|
|
597
|
+
if (options.outputPath === null) {
|
|
598
|
+
process.stdout.write(report);
|
|
599
|
+
} else {
|
|
600
|
+
const resolvedOutputPath = resolveOutputPath(options.outputPath);
|
|
601
|
+
await mkdir(dirname(resolvedOutputPath), { recursive: true });
|
|
602
|
+
await writeFile(resolvedOutputPath, report, 'utf8');
|
|
603
|
+
process.stderr.write(`wrote ${rows.length} rows to ${resolvedOutputPath}\n`);
|
|
604
|
+
}
|
|
605
|
+
} finally {
|
|
606
|
+
client?.close();
|
|
607
|
+
server.stop();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function resolveOutputPath(outputPath) {
|
|
612
|
+
if (isAbsolute(outputPath)) {
|
|
613
|
+
return outputPath;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return resolve(process.env.INIT_CWD ?? process.cwd(), outputPath);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
620
|
+
|
|
621
|
+
if (parsed.kind === 'help') {
|
|
622
|
+
process.stdout.write(`${usage()}\n`);
|
|
623
|
+
} else {
|
|
624
|
+
run(parsed.options).catch((error) => {
|
|
625
|
+
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
|
626
|
+
process.exitCode = 1;
|
|
627
|
+
});
|
|
628
|
+
}
|