@inixiative/archive 0.6.0 → 0.8.0
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 +51 -47
- package/compose.yaml +24 -3
- package/package.json +11 -5
- package/prisma/migrations/20261001022030_init/migration.sql +119 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +125 -0
- package/src/agents.ts +5 -15
- package/src/cli.ts +239 -180
- package/src/client.ts +79 -51
- package/src/collector.ts +11 -11
- package/src/config.ts +33 -44
- package/src/kingdom.ts +103 -0
- package/src/remote.ts +170 -0
- package/src/schemas.ts +40 -0
- package/src/server.ts +56 -63
- package/src/store.ts +438 -0
- package/src/local.ts +0 -355
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
4
5
|
import { join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
5
7
|
import { parseArgs } from 'node:util';
|
|
6
8
|
import {
|
|
7
9
|
agentStatus,
|
|
@@ -14,21 +16,25 @@ import {
|
|
|
14
16
|
writeAgents,
|
|
15
17
|
} from './agents';
|
|
16
18
|
import { importChatGPTThread } from './chatgpt';
|
|
17
|
-
import {
|
|
18
|
-
archiveRequest,
|
|
19
|
-
kingdomFields,
|
|
20
|
-
publishArchive,
|
|
21
|
-
routingPreview,
|
|
22
|
-
searchRemotes,
|
|
23
|
-
syncArchives,
|
|
24
|
-
} from './client';
|
|
19
|
+
import { archiveRequest, routingPreview, searchRemotes, syncArchives } from './client';
|
|
25
20
|
import { collectSessions } from './collector';
|
|
26
21
|
import { archiveDestinationSchema, connectDestination, readDestinations } from './config';
|
|
27
22
|
import { importTranscriptFile } from './import-file';
|
|
28
|
-
import {
|
|
29
|
-
import { LocalArchiveStore } from './local';
|
|
23
|
+
import { pairWithKingdom, verifyKingdomDestination } from './kingdom';
|
|
30
24
|
import { previewImports } from './preview';
|
|
25
|
+
import { ArchiveClient, DEFAULT_URL } from './remote';
|
|
31
26
|
import { startArchiveServer } from './server';
|
|
27
|
+
import { ArchiveStore } from './store';
|
|
28
|
+
|
|
29
|
+
/** Applies pending schema migrations to the archive database. */
|
|
30
|
+
export function migrate(databaseUrl: string) {
|
|
31
|
+
const schema = fileURLToPath(new URL('../prisma/schema.prisma', import.meta.url));
|
|
32
|
+
const run = Bun.spawnSync(
|
|
33
|
+
[process.execPath, 'x', 'prisma', 'migrate', 'deploy', '--schema', schema],
|
|
34
|
+
{ env: { ...process.env, DATABASE_URL: databaseUrl }, stdout: 'ignore', stderr: 'pipe' },
|
|
35
|
+
);
|
|
36
|
+
if (!run.success) throw new Error(`Archive migration failed: ${run.stderr.toString().trim()}`);
|
|
37
|
+
}
|
|
32
38
|
|
|
33
39
|
export async function runCli(args = Bun.argv.slice(2)) {
|
|
34
40
|
const { values: v, positionals } = parseArgs({
|
|
@@ -36,15 +42,14 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
36
42
|
allowPositionals: true,
|
|
37
43
|
options: {
|
|
38
44
|
home: { type: 'string', default: defaultHome() },
|
|
39
|
-
store: { type: 'string' },
|
|
40
45
|
config: { type: 'string' },
|
|
41
46
|
url: { type: 'string' },
|
|
42
47
|
kind: { type: 'string', default: 'archive' },
|
|
43
48
|
'project-id': { type: 'string' },
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
|
|
49
|
+
'integration-id': { type: 'string' },
|
|
50
|
+
'resource-id': { type: 'string' },
|
|
51
|
+
'credential-file': { type: 'string' },
|
|
52
|
+
kingdom: { type: 'string' },
|
|
48
53
|
'token-env': { type: 'string' },
|
|
49
54
|
'token-file': { type: 'string' },
|
|
50
55
|
file: { type: 'string' },
|
|
@@ -66,61 +71,97 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
66
71
|
watch: { type: 'boolean', default: false },
|
|
67
72
|
sync: { type: 'boolean', default: false },
|
|
68
73
|
remote: { type: 'boolean', default: false },
|
|
69
|
-
destination: { type: 'string' },
|
|
70
74
|
help: { type: 'boolean' },
|
|
71
75
|
},
|
|
72
76
|
});
|
|
73
77
|
const home = resolve(v.home!),
|
|
74
|
-
config = v.config ?? join(home, 'destinations.json')
|
|
75
|
-
|
|
78
|
+
config = v.config ?? join(home, 'destinations.json'),
|
|
79
|
+
tokenFile = v['token-file'] ? resolve(v['token-file']) : join(home, 'server.token');
|
|
76
80
|
const output = (value: unknown) => console.log(JSON.stringify(value, null, 2));
|
|
77
81
|
const command = positionals[0];
|
|
78
82
|
if (v.help || !command) {
|
|
79
83
|
console.log(
|
|
80
|
-
'Archive: init | serve | connect (setup) | preview | import | collect | list | export | tag | routes | sync | search | agents\n' +
|
|
81
|
-
'
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
+
'Archive: init | up | down | serve | connect (setup) | preview | import | collect | list | export | tag | routes | sync | search | agents\n' +
|
|
85
|
+
'up | down (the local Archive: bundled compose.yaml with Postgres, data in --home)\n' +
|
|
86
|
+
'serve [--port 4700 --hostname 127.0.0.1] [--sync] (DATABASE_URL; token from ARCHIVE_SERVER_TOKEN or --token-file)\n' +
|
|
87
|
+
'import --file PATH --source codex|claude-code|chatgpt --project-id ID [--tag TAG]\n' +
|
|
84
88
|
'collect --directory HISTORY --source codex|claude-code --project-root EXACT_CWD [--project-root ...] [--worktrees] [--atlas] --project-id ID [--watch]\n' +
|
|
85
|
-
'
|
|
86
|
-
'
|
|
89
|
+
'list | export --id ID | tag --id ID [--tag TAG ...] [--untag TAG ...] | search --query TEXT [--remote]\n' +
|
|
90
|
+
'pair --kingdom KINGDOM_API_URL [--name NAME] (pair this Archive with Kingdom; approve the review code there)\n' +
|
|
91
|
+
'connect --url HTTPS_URL --project-id ID (--token-env ENV | --token-file PATH) (a remote Archive, for serve --sync)\n' +
|
|
92
|
+
'connect --kind kingdom --url KINGDOM_API_URL --credential-file PATH --integration-id UUID --resource-id UUID --project-id ID (a hosted Archive through Kingdom)\n' +
|
|
93
|
+
'sync | routes (DATABASE_URL: publish to, or preview, destinations once)\n' +
|
|
87
94
|
'agents add-collector --name N --source codex|claude-code --project-id ID --project-root DIR [--project-root ...] [--directory HISTORY] [--worktrees] [--atlas]\n' +
|
|
88
|
-
'agents remove-collector --name N |
|
|
89
|
-
|
|
95
|
+
'agents remove-collector --name N | server --url URL | install | uninstall | status\n' +
|
|
96
|
+
`Data commands talk to the Archive server at --url (default ${DEFAULT_URL}) with the token in --token-file (default <home>/server.token) or ARCHIVE_TOKEN.`,
|
|
90
97
|
);
|
|
91
98
|
return;
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
const readToken = () =>
|
|
101
|
+
existsSync(tokenFile) ? readFileSync(tokenFile, 'utf8').trim() : undefined;
|
|
102
|
+
const init = () => {
|
|
94
103
|
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
95
|
-
const tokenFile = join(home, 'server.token');
|
|
96
104
|
if (!existsSync(tokenFile))
|
|
97
|
-
writeFileSync(tokenFile, randomBytes(32).toString('hex')
|
|
105
|
+
writeFileSync(tokenFile, `${randomBytes(32).toString('hex')}\n`, { mode: 0o600, flag: 'wx' });
|
|
98
106
|
chmodSync(tokenFile, 0o600);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
};
|
|
108
|
+
if (command === 'init') {
|
|
109
|
+
init();
|
|
110
|
+
output({ initialized: true, home, tokenFile });
|
|
102
111
|
return;
|
|
103
112
|
}
|
|
113
|
+
if (command === 'up' || command === 'down') {
|
|
114
|
+
// The machine's one local Archive: the bundled compose file, with its data in the home.
|
|
115
|
+
if (command === 'up') init();
|
|
116
|
+
const compose = fileURLToPath(new URL('../compose.yaml', import.meta.url));
|
|
117
|
+
const run = Bun.spawnSync(
|
|
118
|
+
[
|
|
119
|
+
'docker',
|
|
120
|
+
'compose',
|
|
121
|
+
'--project-name',
|
|
122
|
+
'archive',
|
|
123
|
+
'--file',
|
|
124
|
+
compose,
|
|
125
|
+
...(command === 'up' ? ['up', '--detach', '--wait'] : ['down']),
|
|
126
|
+
],
|
|
127
|
+
{
|
|
128
|
+
env: {
|
|
129
|
+
...process.env,
|
|
130
|
+
ARCHIVE_DATA_DIR: home,
|
|
131
|
+
// Compose requires the variable even to stop; down never needs the real token.
|
|
132
|
+
ARCHIVE_SERVER_TOKEN: readToken() ?? 'x'.repeat(32),
|
|
133
|
+
},
|
|
134
|
+
stdout: 'inherit',
|
|
135
|
+
stderr: 'inherit',
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
if (!run.success) throw new Error(`docker compose ${command} failed`);
|
|
139
|
+
output(command === 'up' ? { running: DEFAULT_URL, home } : { stopped: true });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const databaseUrl = () => {
|
|
143
|
+
const url = process.env.DATABASE_URL;
|
|
144
|
+
if (!url) throw new Error('DATABASE_URL is required');
|
|
145
|
+
return url;
|
|
146
|
+
};
|
|
104
147
|
if (command === 'serve') {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
token,
|
|
112
|
-
port: Number(v.port ?? process.env.PORT ?? 4411),
|
|
148
|
+
const url = databaseUrl();
|
|
149
|
+
migrate(url);
|
|
150
|
+
const instance = await startArchiveServer({
|
|
151
|
+
databaseUrl: url,
|
|
152
|
+
token: process.env.ARCHIVE_SERVER_TOKEN ?? readToken() ?? '',
|
|
153
|
+
port: Number(v.port ?? process.env.PORT ?? 4700),
|
|
113
154
|
hostname: v.hostname,
|
|
114
155
|
});
|
|
115
|
-
output({ listening: instance.server.url.href,
|
|
156
|
+
output({ listening: instance.server.url.href, sync: v.sync });
|
|
116
157
|
let stopped = false;
|
|
117
|
-
// One process owns the
|
|
158
|
+
// One process owns the database connection: the server and its sync share it.
|
|
118
159
|
const sync = (async () => {
|
|
119
160
|
while (v.sync && !stopped) {
|
|
120
161
|
try {
|
|
121
162
|
const results = await syncArchives(instance.store, readDestinations(config));
|
|
122
|
-
const failed = results.filter((r) => r.status.startsWith('failed'))
|
|
123
|
-
if (failed) console.error(JSON.stringify({ sync: 'failed', archives: failed }));
|
|
163
|
+
const failed = results.filter((r) => r.status.startsWith('failed'));
|
|
164
|
+
if (failed.length) console.error(JSON.stringify({ sync: 'failed', archives: failed }));
|
|
124
165
|
} catch {
|
|
125
166
|
console.error(JSON.stringify({ sync: 'unavailable' }));
|
|
126
167
|
}
|
|
@@ -137,45 +178,61 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
137
178
|
process.once('SIGINT', stop);
|
|
138
179
|
return;
|
|
139
180
|
}
|
|
181
|
+
if (command === 'pair') {
|
|
182
|
+
if (!v.kingdom) throw new Error('pair requires --kingdom KINGDOM_API_URL');
|
|
183
|
+
const local = new ArchiveClient({
|
|
184
|
+
url: v.url ?? DEFAULT_URL,
|
|
185
|
+
token: process.env.ARCHIVE_TOKEN ?? readToken() ?? '',
|
|
186
|
+
});
|
|
187
|
+
output(
|
|
188
|
+
await pairWithKingdom({
|
|
189
|
+
kingdom: v.kingdom,
|
|
190
|
+
name: v.name ?? `${hostname()} Archive`,
|
|
191
|
+
sourceId: await local.sourceId(),
|
|
192
|
+
directory: join(home, 'kingdom'),
|
|
193
|
+
onReview: (review) => console.error(JSON.stringify(review)),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
140
198
|
if (command === 'connect' || command === 'setup') {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
projectId: v['project-id'],
|
|
144
|
-
url: v.url,
|
|
145
|
-
tokenEnv: v['token-env'] ?? (tokenFile ? undefined : 'ARCHIVE_TOKEN'),
|
|
146
|
-
tokenFile,
|
|
147
|
-
kind: v.kind,
|
|
148
|
-
...(v.kind === 'kingdom'
|
|
199
|
+
const destination = archiveDestinationSchema.parse(
|
|
200
|
+
v.kind === 'kingdom'
|
|
149
201
|
? {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
202
|
+
kind: 'kingdom',
|
|
203
|
+
projectId: v['project-id'],
|
|
204
|
+
url: v.url,
|
|
205
|
+
credentialFile: v['credential-file'] && resolve(v['credential-file']),
|
|
206
|
+
integrationId: v['integration-id'],
|
|
207
|
+
resourceId: v['resource-id'],
|
|
154
208
|
}
|
|
155
|
-
: {
|
|
156
|
-
|
|
209
|
+
: {
|
|
210
|
+
kind: 'archive',
|
|
211
|
+
projectId: v['project-id'],
|
|
212
|
+
url: v.url,
|
|
213
|
+
tokenEnv: v['token-env'] ?? (v['token-file'] ? undefined : 'ARCHIVE_TOKEN'),
|
|
214
|
+
tokenFile: v['token-file'] && resolve(v['token-file']),
|
|
215
|
+
},
|
|
216
|
+
);
|
|
157
217
|
// Verify credentials and protocol before saving; no records uploaded by setup.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
218
|
+
if (destination.kind === 'kingdom') await verifyKingdomDestination(destination);
|
|
219
|
+
else {
|
|
220
|
+
const probe = await archiveRequest(destination, 'search', {
|
|
221
|
+
query: '',
|
|
222
|
+
budget: 16,
|
|
223
|
+
limit: 1,
|
|
224
|
+
projectId: destination.projectId,
|
|
225
|
+
});
|
|
226
|
+
if (!Array.isArray(probe.data?.archives))
|
|
227
|
+
throw new Error('Destination is not an Archive-compatible endpoint');
|
|
228
|
+
}
|
|
168
229
|
output(connectDestination(config, destination));
|
|
169
230
|
return;
|
|
170
231
|
}
|
|
171
232
|
if (command === 'agents') {
|
|
172
233
|
const file = join(home, 'agents.json');
|
|
173
234
|
const current = readAgents(file);
|
|
174
|
-
const
|
|
175
|
-
const toggle = () => {
|
|
176
|
-
if (state !== 'on' && state !== 'off') throw new Error(`agents ${action} requires on|off`);
|
|
177
|
-
return state === 'on';
|
|
178
|
-
};
|
|
235
|
+
const action = positionals[1];
|
|
179
236
|
const base = { collectors: [], ...current };
|
|
180
237
|
if (action === 'add-collector') {
|
|
181
238
|
if (!v.name || !v['project-id'] || !v['project-root'] || !v.source)
|
|
@@ -203,16 +260,10 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
203
260
|
collectors: base.collectors.filter((c) => c.name !== v.name),
|
|
204
261
|
}),
|
|
205
262
|
);
|
|
206
|
-
} else if (action === '
|
|
207
|
-
|
|
208
|
-
output(
|
|
209
|
-
|
|
210
|
-
file,
|
|
211
|
-
toggle() ? { ...rest, serve: v.port ? { port: Number(v.port) } : {} } : rest,
|
|
212
|
-
),
|
|
213
|
-
);
|
|
214
|
-
} else if (action === 'sync') output(writeAgents(file, { ...base, sync: toggle() }));
|
|
215
|
-
else if (action === 'install') {
|
|
263
|
+
} else if (action === 'server') {
|
|
264
|
+
if (!v.url) throw new Error('agents server requires --url');
|
|
265
|
+
output(writeAgents(file, { ...base, server: { url: v.url } }));
|
|
266
|
+
} else if (action === 'install') {
|
|
216
267
|
if (!current) throw new Error('No agents.json; declare agents first');
|
|
217
268
|
output(installAgents(current, { home }));
|
|
218
269
|
} else if (action === 'uninstall') output(uninstallAgents({ home }));
|
|
@@ -237,110 +288,118 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
237
288
|
if (results.some((r) => 'error' in r)) process.exitCode = 1;
|
|
238
289
|
return;
|
|
239
290
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
!v.file ||
|
|
245
|
-
!['codex', 'claude-code', 'chatgpt'].includes(v.source ?? '') ||
|
|
246
|
-
!v['project-id']
|
|
247
|
-
)
|
|
248
|
-
throw new Error(
|
|
249
|
-
'Import requires --file, --source codex|claude-code|chatgpt and --project-id',
|
|
250
|
-
);
|
|
251
|
-
const snapshot =
|
|
252
|
-
v.source === 'chatgpt'
|
|
253
|
-
? importChatGPTThread(JSON.parse(readFileSync(v.file, 'utf8')), {
|
|
254
|
-
sourceId: store.sourceId,
|
|
255
|
-
projectId: v['project-id'],
|
|
256
|
-
title: v.title,
|
|
257
|
-
})
|
|
258
|
-
: importTranscriptFile(v.file, {
|
|
259
|
-
source: v.source as 'codex' | 'claude-code',
|
|
260
|
-
sourceId: store.sourceId,
|
|
261
|
-
projectId: v['project-id'],
|
|
262
|
-
sessionId: v['session-id'],
|
|
263
|
-
title: v.title,
|
|
264
|
-
});
|
|
265
|
-
snapshot.tags = [
|
|
266
|
-
...new Set([...(store.read(archiveKey(snapshot))?.snapshot.tags ?? []), ...(v.tag ?? [])]),
|
|
267
|
-
];
|
|
268
|
-
output({
|
|
269
|
-
...store.capture(snapshot),
|
|
270
|
-
entries: snapshot.entries.length,
|
|
271
|
-
coverage: snapshot.coverage,
|
|
272
|
-
});
|
|
273
|
-
} else if (command === 'list') output(store.list());
|
|
274
|
-
else if (command === 'routes') output(routingPreview(store, readDestinations(config)));
|
|
275
|
-
else if (command === 'sync' || command === 'collect') {
|
|
276
|
-
if (
|
|
277
|
-
command === 'collect' &&
|
|
278
|
-
(!v.directory ||
|
|
279
|
-
!v['project-root'] ||
|
|
280
|
-
!v['project-id'] ||
|
|
281
|
-
!['codex', 'claude-code'].includes(v.source ?? ''))
|
|
282
|
-
)
|
|
283
|
-
throw new Error('Collect requires --directory, --source, --project-root and --project-id');
|
|
284
|
-
let stopped = false;
|
|
285
|
-
const stop = () => {
|
|
286
|
-
stopped = true;
|
|
287
|
-
};
|
|
288
|
-
process.once('SIGINT', stop);
|
|
289
|
-
process.once('SIGTERM', stop);
|
|
290
|
-
do {
|
|
291
|
-
if (command === 'collect') {
|
|
292
|
-
const result = await collectSessions(store, {
|
|
293
|
-
directory: v.directory!,
|
|
294
|
-
source: v.source as 'codex' | 'claude-code',
|
|
295
|
-
projectRoots: v['project-root']!,
|
|
296
|
-
worktrees: v.worktrees,
|
|
297
|
-
atlas: v.atlas,
|
|
298
|
-
projectId: v['project-id']!,
|
|
299
|
-
tags: v.tag,
|
|
300
|
-
});
|
|
301
|
-
output(result);
|
|
302
|
-
if (!v.watch && result.failed) process.exitCode = 1;
|
|
303
|
-
} else {
|
|
304
|
-
const results = await syncArchives(store, readDestinations(config));
|
|
305
|
-
output(results);
|
|
306
|
-
if (!v.watch && results.some((r) => r.status.startsWith('failed'))) process.exitCode = 1;
|
|
307
|
-
}
|
|
308
|
-
if (v.watch && !stopped) await Bun.sleep(1000);
|
|
309
|
-
// Short waits allow prompt shutdown; network requests have their own timeout.
|
|
310
|
-
for (let i = 1; v.watch && !stopped && i < 30; i++) await Bun.sleep(1000);
|
|
311
|
-
} while (v.watch && !stopped);
|
|
312
|
-
process.removeListener('SIGINT', stop);
|
|
313
|
-
process.removeListener('SIGTERM', stop);
|
|
314
|
-
} else if (command === 'publish') {
|
|
315
|
-
if (!v.id || !v.destination) throw new Error('Publish requires --id and --destination');
|
|
316
|
-
output(
|
|
317
|
-
await publishArchive(
|
|
318
|
-
store,
|
|
319
|
-
v.id,
|
|
320
|
-
archiveDestinationSchema.parse(JSON.parse(readFileSync(v.destination, 'utf8'))),
|
|
321
|
-
),
|
|
322
|
-
);
|
|
323
|
-
} else if (command === 'search')
|
|
324
|
-
output(store.search(v.id ? [v.id] : store.list().map((a) => a.id), v.query!));
|
|
325
|
-
else if (command === 'export' || command === 'tag') {
|
|
326
|
-
const archive = v.id ? store.read(v.id) : undefined;
|
|
327
|
-
if (!archive) throw new Error('Valid --id required');
|
|
328
|
-
if (command === 'export') output(archive.snapshot);
|
|
291
|
+
if (command === 'sync' || command === 'routes') {
|
|
292
|
+
const store = new ArchiveStore(databaseUrl());
|
|
293
|
+
try {
|
|
294
|
+
if (command === 'routes') output(await routingPreview(store, readDestinations(config)));
|
|
329
295
|
else {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
296
|
+
const results = await syncArchives(store, readDestinations(config));
|
|
297
|
+
output(results);
|
|
298
|
+
if (results.some((r) => r.status.startsWith('failed'))) process.exitCode = 1;
|
|
333
299
|
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
|
|
300
|
+
} finally {
|
|
301
|
+
await store.close();
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
337
304
|
}
|
|
305
|
+
const token = process.env.ARCHIVE_TOKEN ?? readToken();
|
|
306
|
+
if (!token) throw new Error('No Archive token: run `archive init` or set ARCHIVE_TOKEN');
|
|
307
|
+
const archive = new ArchiveClient({ url: v.url ?? DEFAULT_URL, token });
|
|
308
|
+
if (command === 'import') {
|
|
309
|
+
if (
|
|
310
|
+
!v.file ||
|
|
311
|
+
!['codex', 'claude-code', 'chatgpt'].includes(v.source ?? '') ||
|
|
312
|
+
!v['project-id']
|
|
313
|
+
)
|
|
314
|
+
throw new Error(
|
|
315
|
+
'Import requires --file, --source codex|claude-code|chatgpt and --project-id',
|
|
316
|
+
);
|
|
317
|
+
const sourceId = await archive.sourceId();
|
|
318
|
+
const snapshot =
|
|
319
|
+
v.source === 'chatgpt'
|
|
320
|
+
? importChatGPTThread(JSON.parse(readFileSync(v.file, 'utf8')), {
|
|
321
|
+
sourceId,
|
|
322
|
+
projectId: v['project-id'],
|
|
323
|
+
title: v.title,
|
|
324
|
+
})
|
|
325
|
+
: importTranscriptFile(v.file, {
|
|
326
|
+
source: v.source as 'codex' | 'claude-code',
|
|
327
|
+
sourceId,
|
|
328
|
+
projectId: v['project-id'],
|
|
329
|
+
sessionId: v['session-id'],
|
|
330
|
+
title: v.title,
|
|
331
|
+
});
|
|
332
|
+
snapshot.tags = [...new Set(v.tag ?? [])];
|
|
333
|
+
output({
|
|
334
|
+
...(await archive.capture(snapshot)),
|
|
335
|
+
entries: snapshot.entries.length,
|
|
336
|
+
coverage: snapshot.coverage,
|
|
337
|
+
});
|
|
338
|
+
} else if (command === 'list') output(await archive.list());
|
|
339
|
+
else if (command === 'collect') {
|
|
340
|
+
if (
|
|
341
|
+
!v.directory ||
|
|
342
|
+
!v['project-root'] ||
|
|
343
|
+
!v['project-id'] ||
|
|
344
|
+
!['codex', 'claude-code'].includes(v.source ?? '')
|
|
345
|
+
)
|
|
346
|
+
throw new Error('Collect requires --directory, --source, --project-root and --project-id');
|
|
347
|
+
let stopped = false;
|
|
348
|
+
const stop = () => {
|
|
349
|
+
stopped = true;
|
|
350
|
+
};
|
|
351
|
+
process.once('SIGINT', stop);
|
|
352
|
+
process.once('SIGTERM', stop);
|
|
353
|
+
do {
|
|
354
|
+
try {
|
|
355
|
+
const result = await collectSessions(archive, {
|
|
356
|
+
directory: v.directory,
|
|
357
|
+
source: v.source as 'codex' | 'claude-code',
|
|
358
|
+
projectRoots: v['project-root'],
|
|
359
|
+
worktrees: v.worktrees,
|
|
360
|
+
atlas: v.atlas,
|
|
361
|
+
projectId: v['project-id'],
|
|
362
|
+
tags: v.tag,
|
|
363
|
+
});
|
|
364
|
+
output(result);
|
|
365
|
+
if (!v.watch && result.failed) process.exitCode = 1;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
// The server may be starting; a watching collector retries.
|
|
368
|
+
if (!v.watch) throw error;
|
|
369
|
+
console.error(JSON.stringify({ collect: 'server unavailable' }));
|
|
370
|
+
}
|
|
371
|
+
// Short waits allow prompt shutdown; network requests have their own timeout.
|
|
372
|
+
for (let i = 0; v.watch && !stopped && i < 30; i++) await Bun.sleep(1000);
|
|
373
|
+
} while (v.watch && !stopped);
|
|
374
|
+
process.removeListener('SIGINT', stop);
|
|
375
|
+
process.removeListener('SIGTERM', stop);
|
|
376
|
+
} else if (command === 'search')
|
|
377
|
+
output(
|
|
378
|
+
await archive.search({
|
|
379
|
+
query: v.query,
|
|
380
|
+
limit: Number(v.limit) > 100 ? 100 : Number(v.limit),
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
else if (command === 'export' || command === 'tag') {
|
|
384
|
+
if (!v.id) throw new Error('Valid --id required');
|
|
385
|
+
if (command === 'export') {
|
|
386
|
+
const read = await archive.read(v.id);
|
|
387
|
+
if (!read) throw new Error('Valid --id required');
|
|
388
|
+
output(read.snapshot);
|
|
389
|
+
} else {
|
|
390
|
+
if (!v.tag?.length && !v.untag?.length)
|
|
391
|
+
throw new Error('Tag requires at least one --tag or --untag');
|
|
392
|
+
output(await archive.tag(v.id, { add: v.tag, remove: v.untag }));
|
|
393
|
+
}
|
|
394
|
+
} else throw new Error('Unknown command; use --help');
|
|
338
395
|
}
|
|
339
396
|
|
|
340
397
|
if (import.meta.main)
|
|
341
|
-
runCli().catch(() => {
|
|
398
|
+
runCli().catch((error) => {
|
|
342
399
|
console.error(
|
|
343
|
-
|
|
400
|
+
process.env.ARCHIVE_DEBUG
|
|
401
|
+
? error
|
|
402
|
+
: 'Archive command failed. Check arguments, server and destination access, and local paths. Use --help for usage. Set ARCHIVE_DEBUG=1 for details.',
|
|
344
403
|
);
|
|
345
404
|
process.exitCode = 1;
|
|
346
405
|
});
|