@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 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.0",
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.33",
39
- "lefthook": "2.1.9"
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 (!v.file || !['codex', 'claude-code'].includes(v.source ?? '') || !v['project-id'])
146
- throw new Error('Import requires --file, --source codex|claude-code and --project-id');
147
- const snapshot = importTranscriptFile(v.file, {
148
- source: v.source as 'codex' | 'claude-code',
149
- sourceId: store.sourceId,
150
- projectId: v['project-id'],
151
- sessionId: v['session-id'],
152
- title: v.title,
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((a) => !input.projectId || a.projectId === input.projectId)
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),