@inixiative/archive 0.2.0 → 0.2.1
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 +4 -0
- package/package.json +7 -6
- package/src/chatgpt.ts +74 -0
- package/src/cli.ts +23 -9
- package/src/index.ts +1 -1
- package/src/server.ts +6 -1
package/README.md
CHANGED
|
@@ -98,3 +98,7 @@ bun run typecheck
|
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
See [provenance](docs/PROVENANCE.md), [license](LICENSE), and [goals](tickets/README.md). The historical tickets describe earlier scope; this implementation now includes explicit provider collection and authenticated standalone hosting.
|
|
101
|
+
|
|
102
|
+
### ChatGPT conversation text
|
|
103
|
+
|
|
104
|
+
`archive import --file conversation.json --source chatgpt --project-id personal` accepts the JSON result of the desktop ChatGPT conversation reader (`thread`, `page`, and `turns`). This is a partial text capture, not a ChatGPT account export: tools, attachments, alternate branches and resolved citation targets are unavailable. ChatGPT conversations retain their own source identity.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inixiative/archive",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"./import-file": "./src/import-file.ts",
|
|
13
13
|
"./preview": "./src/preview.ts",
|
|
14
14
|
"./cli": "./src/cli.ts",
|
|
15
|
-
"./collector": "./src/collector.ts"
|
|
15
|
+
"./collector": "./src/collector.ts",
|
|
16
|
+
"./chatgpt": "./src/chatgpt.ts"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"src"
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
"zod": "^4.3.6"
|
|
33
34
|
},
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@types/bun": "1.4.2",
|
|
36
|
-
"typescript": "6.0.3",
|
|
37
36
|
"@biomejs/biome": "2.5.2",
|
|
38
|
-
"@inixiative/config": "^0.5.
|
|
39
|
-
"
|
|
37
|
+
"@inixiative/config": "^0.5.34",
|
|
38
|
+
"@types/bun": "1.4.2",
|
|
39
|
+
"lefthook": "2.1.9",
|
|
40
|
+
"typescript": "6.0.3"
|
|
40
41
|
},
|
|
41
42
|
"bin": {
|
|
42
43
|
"archive": "src/cli.ts"
|
package/src/chatgpt.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ArchiveEntry, archiveSnapshotSchema } from './index';
|
|
3
|
+
|
|
4
|
+
const exportSchema = z.object({
|
|
5
|
+
thread: z.object({ id: z.string(), kind: z.literal('chatgpt'), title: z.string() }),
|
|
6
|
+
page: z.object({ order: z.literal('newest_first'), hasMore: z.boolean() }),
|
|
7
|
+
turns: z.array(
|
|
8
|
+
z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
startedAt: z.number().nullable(),
|
|
11
|
+
completedAt: z.number().nullable(),
|
|
12
|
+
items: z.array(
|
|
13
|
+
z.object({
|
|
14
|
+
type: z.string(),
|
|
15
|
+
id: z.string(),
|
|
16
|
+
text: z.string().optional(),
|
|
17
|
+
content: z.array(z.object({ type: z.string(), text: z.string().optional() })).optional(),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
}),
|
|
21
|
+
),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Import the text returned by the desktop ChatGPT conversation reader, not a native JSONL log. */
|
|
25
|
+
export function importChatGPTThread(
|
|
26
|
+
input: unknown,
|
|
27
|
+
options: { sourceId: string; projectId?: string; title?: string },
|
|
28
|
+
) {
|
|
29
|
+
const data = exportSchema.parse(input);
|
|
30
|
+
const entries: ArchiveEntry[] = [];
|
|
31
|
+
for (const turn of [...data.turns].reverse()) {
|
|
32
|
+
for (const item of turn.items) {
|
|
33
|
+
if (!['userMessage', 'agentMessage'].includes(item.type)) continue;
|
|
34
|
+
const text =
|
|
35
|
+
item.text ??
|
|
36
|
+
item.content
|
|
37
|
+
?.filter((b) => b.type === 'text')
|
|
38
|
+
.map((b) => b.text ?? '')
|
|
39
|
+
.join('\n');
|
|
40
|
+
if (!text) continue;
|
|
41
|
+
entries.push({
|
|
42
|
+
id: item.id,
|
|
43
|
+
turnId: turn.id,
|
|
44
|
+
kind: item.type === 'userMessage' ? 'user' : 'assistant',
|
|
45
|
+
text,
|
|
46
|
+
timestamp:
|
|
47
|
+
(item.type === 'userMessage' ? turn.startedAt : turn.completedAt) === null
|
|
48
|
+
? null
|
|
49
|
+
: (item.type === 'userMessage' ? turn.startedAt! : turn.completedAt!) * 1000,
|
|
50
|
+
sourceRef: `chatgpt:${data.thread.id}:${item.id}`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!entries.length) throw new Error('ChatGPT export contains no readable messages');
|
|
55
|
+
return archiveSnapshotSchema.parse({
|
|
56
|
+
schemaVersion: 1,
|
|
57
|
+
source: 'chatgpt',
|
|
58
|
+
sourceId: options.sourceId,
|
|
59
|
+
sessionId: data.thread.id,
|
|
60
|
+
title: options.title ?? data.thread.title,
|
|
61
|
+
projectId: options.projectId,
|
|
62
|
+
tags: [],
|
|
63
|
+
capturedAt: Date.now(),
|
|
64
|
+
entries,
|
|
65
|
+
coverage: {
|
|
66
|
+
reasoning: 'unavailable',
|
|
67
|
+
completeness: 'partial',
|
|
68
|
+
omissions: [
|
|
69
|
+
'Desktop conversation text only; tool activity, attachments, alternate branches and citation targets are not exported. Reader text may be truncated.',
|
|
70
|
+
...(data.page.hasMore ? ['Older conversation turns are not included in this page.'] : []),
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'n
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
|
+
import { importChatGPTThread } from './chatgpt';
|
|
7
8
|
import {
|
|
8
9
|
archiveRequest,
|
|
9
10
|
publishArchive,
|
|
@@ -142,15 +143,28 @@ export async function runCli(args = Bun.argv.slice(2)) {
|
|
|
142
143
|
const store = new LocalArchiveStore(storePath);
|
|
143
144
|
try {
|
|
144
145
|
if (command === 'import') {
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
if (
|
|
147
|
+
!v.file ||
|
|
148
|
+
!['codex', 'claude-code', 'chatgpt'].includes(v.source ?? '') ||
|
|
149
|
+
!v['project-id']
|
|
150
|
+
)
|
|
151
|
+
throw new Error(
|
|
152
|
+
'Import requires --file, --source codex|claude-code|chatgpt and --project-id',
|
|
153
|
+
);
|
|
154
|
+
const snapshot =
|
|
155
|
+
v.source === 'chatgpt'
|
|
156
|
+
? importChatGPTThread(JSON.parse(readFileSync(v.file, 'utf8')), {
|
|
157
|
+
sourceId: store.sourceId,
|
|
158
|
+
projectId: v['project-id'],
|
|
159
|
+
title: v.title,
|
|
160
|
+
})
|
|
161
|
+
: importTranscriptFile(v.file, {
|
|
162
|
+
source: v.source as 'codex' | 'claude-code',
|
|
163
|
+
sourceId: store.sourceId,
|
|
164
|
+
projectId: v['project-id'],
|
|
165
|
+
sessionId: v['session-id'],
|
|
166
|
+
title: v.title,
|
|
167
|
+
});
|
|
154
168
|
snapshot.tags = [
|
|
155
169
|
...new Set([...(store.read(archiveKey(snapshot))?.snapshot.tags ?? []), ...(v.tag ?? [])]),
|
|
156
170
|
];
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ export const archiveSnapshotSchema = z
|
|
|
16
16
|
.strictObject({
|
|
17
17
|
schemaVersion: z.literal(1),
|
|
18
18
|
sourceId: z.uuid(),
|
|
19
|
-
source: z.enum(['foundry', 'claude-code', 'codex']),
|
|
19
|
+
source: z.enum(['foundry', 'claude-code', 'codex', 'chatgpt']),
|
|
20
20
|
sessionId: identifier,
|
|
21
21
|
title: z.string().min(1).max(500),
|
|
22
22
|
projectId: identifier.optional(),
|
package/src/server.ts
CHANGED
|
@@ -12,6 +12,7 @@ const ingestSchema = z.strictObject({
|
|
|
12
12
|
});
|
|
13
13
|
const searchSchema = z.strictObject({
|
|
14
14
|
query: z.string().max(1000).default(''),
|
|
15
|
+
tag: z.string().min(1).max(120).optional(),
|
|
15
16
|
budget: z.number().int().min(16).max(32768).default(2048),
|
|
16
17
|
projectId: z.string().max(256).optional(),
|
|
17
18
|
limit: z.number().int().min(1).max(100).default(20),
|
|
@@ -75,7 +76,11 @@ export function createArchiveHandler(store: LocalArchiveStore, token: string) {
|
|
|
75
76
|
let remaining = input.budget;
|
|
76
77
|
const matching = store
|
|
77
78
|
.list()
|
|
78
|
-
.filter(
|
|
79
|
+
.filter(
|
|
80
|
+
(a) =>
|
|
81
|
+
(!input.projectId || a.projectId === input.projectId) &&
|
|
82
|
+
(!input.tag || a.tags.includes(input.tag)),
|
|
83
|
+
)
|
|
79
84
|
.map((a) => ({
|
|
80
85
|
a,
|
|
81
86
|
selected: selectChunks(store.read(a.id)!.chunks, input.query, input.budget),
|