spiracha 2.1.0 → 2.2.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/AGENTS.md +27 -12
- package/README.md +37 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +17 -10
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
- package/apps/ui/src/components/app-shell.tsx +45 -3
- package/apps/ui/src/components/data-table.tsx +17 -1
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +77 -62
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
- package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
- package/apps/ui/src/components/threads-table.tsx +4 -1
- package/apps/ui/src/lib/antigravity-server.ts +70 -17
- package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
- package/apps/ui/src/lib/claude-code-server.ts +14 -4
- package/apps/ui/src/lib/codex-queries.ts +3 -3
- package/apps/ui/src/lib/codex-server.ts +3 -3
- package/apps/ui/src/lib/cursor-server.ts +41 -69
- package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
- package/apps/ui/src/lib/delete-batch.ts +52 -0
- package/apps/ui/src/lib/error-presentation.ts +43 -0
- package/apps/ui/src/lib/export-mutation.ts +20 -0
- package/apps/ui/src/lib/export-options.ts +15 -0
- package/apps/ui/src/lib/grok-server.ts +2 -3
- package/apps/ui/src/lib/kiro-server.ts +14 -2
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +13 -2
- package/apps/ui/src/lib/path-utils.ts +1 -1
- package/apps/ui/src/lib/qoder-queries.ts +15 -0
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-state-reset.tsx +10 -0
- package/apps/ui/src/lib/settings-server.ts +33 -0
- package/apps/ui/src/lib/settings-store.tsx +82 -38
- package/apps/ui/src/lib/settings.ts +65 -0
- package/apps/ui/src/lib/source-session-export-server.ts +1 -1
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
- package/apps/ui/src/routeTree.gen.ts +282 -282
- package/apps/ui/src/routes/__root.tsx +12 -15
- package/apps/ui/src/routes/analytics.tsx +10 -61
- package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/claude-code.index.tsx +2 -2
- package/apps/ui/src/routes/codex.$project.tsx +33 -31
- package/apps/ui/src/routes/codex.index.tsx +5 -10
- package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
- package/apps/ui/src/routes/grok.index.tsx +2 -2
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
- package/apps/ui/src/routes/opencode.index.tsx +8 -17
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +26 -17
- package/src/client.ts +61 -16
- package/src/lib/antigravity-db.ts +236 -82
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +45 -19
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +2 -7
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +88 -33
- package/src/lib/codex-thread-parser.ts +78 -35
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-renderer.ts +49 -61
- package/src/lib/concurrency.ts +15 -3
- package/src/lib/conversation-api.ts +34 -31
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
- package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
- package/src/lib/conversation-data/codex-adapter.ts +91 -80
- package/src/lib/conversation-data/cursor-adapter.ts +48 -34
- package/src/lib/conversation-data/grok-adapter.ts +19 -6
- package/src/lib/conversation-data/index.ts +29 -10
- package/src/lib/conversation-data/kiro-adapter.ts +18 -5
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +43 -15
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +23 -18
- package/src/lib/conversation-zip-export.ts +20 -2
- package/src/lib/cursor-db.ts +34 -23
- package/src/lib/cursor-id.ts +17 -0
- package/src/lib/cursor-recovery.ts +146 -35
- package/src/lib/cursor-transcript-phase.ts +40 -0
- package/src/lib/cursor-transcript.ts +21 -5
- package/src/lib/grok-db.ts +76 -34
- package/src/lib/grok-transcript-phase.ts +11 -2
- package/src/lib/grok-transcript.ts +15 -2
- package/src/lib/kiro-db.ts +161 -68
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/opencode-db.ts +4 -8
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/portable-path.ts +9 -0
- package/src/lib/qoder-acp-client.ts +11 -2
- package/src/lib/qoder-db.ts +51 -17
- package/src/lib/qoder-transcript-phase.ts +64 -0
- package/src/lib/qoder-transcript.ts +30 -2
- package/src/lib/shared.ts +51 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +8 -3
- package/src/lib/ui-cache.ts +152 -17
- package/src/lib/ui-export-archive.ts +14 -55
- package/src/lib/ui-export-files.ts +75 -15
- package/src/lib/ui-export-zip.ts +40 -0
- package/apps/ui/package.json +0 -65
package/apps/ui/vite.config.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { access, constants } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
5
4
|
import { devtools } from '@tanstack/devtools-vite';
|
|
6
5
|
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
|
7
6
|
import viteReact from '@vitejs/plugin-react';
|
|
8
7
|
import { defineConfig, type Plugin } from 'vite';
|
|
9
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildUiExportContentDisposition,
|
|
10
|
+
resolveReadableUiExportFileFromRequestPath,
|
|
11
|
+
UI_EXPORT_URL_PREFIX,
|
|
12
|
+
} from '../../src/lib/ui-export-files';
|
|
13
|
+
|
|
14
|
+
const uiRoot = __dirname;
|
|
10
15
|
|
|
11
16
|
const getExportContentType = (filePath: string) => {
|
|
12
17
|
if (filePath.endsWith('.zip')) {
|
|
@@ -33,17 +38,14 @@ const spirachaExportFiles = (): Plugin => {
|
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
if (!exportFilePath) {
|
|
41
|
+
const pathname = new URL(req.url, 'http://spiracha.local').pathname;
|
|
42
|
+
if (!pathname.startsWith(UI_EXPORT_URL_PREFIX)) {
|
|
40
43
|
next();
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} catch {
|
|
47
|
+
const exportFilePath = await resolveReadableUiExportFileFromRequestPath(pathname);
|
|
48
|
+
if (!exportFilePath) {
|
|
47
49
|
res.statusCode = 404;
|
|
48
50
|
res.end('Not Found');
|
|
49
51
|
return;
|
|
@@ -59,7 +61,14 @@ const spirachaExportFiles = (): Plugin => {
|
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
createReadStream(exportFilePath)
|
|
64
|
+
const stream = createReadStream(exportFilePath);
|
|
65
|
+
stream.once('error', () => {
|
|
66
|
+
if (!res.headersSent) {
|
|
67
|
+
res.statusCode = 500;
|
|
68
|
+
}
|
|
69
|
+
res.end('Internal Server Error');
|
|
70
|
+
});
|
|
71
|
+
stream.pipe(res);
|
|
63
72
|
});
|
|
64
73
|
},
|
|
65
74
|
name: 'spiracha-export-files',
|
|
@@ -70,13 +79,14 @@ const config = defineConfig({
|
|
|
70
79
|
plugins: [spirachaExportFiles(), devtools(), tailwindcss(), tanstackStart(), viteReact()],
|
|
71
80
|
resolve: {
|
|
72
81
|
alias: {
|
|
73
|
-
'@spiracha': path.resolve(
|
|
82
|
+
'@spiracha': path.resolve(uiRoot, '..', '..', 'src'),
|
|
74
83
|
},
|
|
75
84
|
tsconfigPaths: true,
|
|
76
85
|
},
|
|
86
|
+
root: uiRoot,
|
|
77
87
|
server: {
|
|
78
88
|
fs: {
|
|
79
|
-
allow: [path.resolve(
|
|
89
|
+
allow: [path.resolve(uiRoot, '..', '..')],
|
|
80
90
|
},
|
|
81
91
|
},
|
|
82
92
|
});
|
package/bin/spiracha.ts
CHANGED
|
@@ -14,6 +14,33 @@ export const buildSpirachaDevServerCommand = (packageRoot = resolveSpirachaPacka
|
|
|
14
14
|
cwd: path.join(packageRoot, 'apps', 'ui'),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
type SpirachaChildProcess = {
|
|
18
|
+
exited: Promise<number>;
|
|
19
|
+
kill: (signal?: NodeJS.Signals | number) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type SpirachaSignalEmitter = {
|
|
23
|
+
off: (event: NodeJS.Signals, listener: () => void) => unknown;
|
|
24
|
+
on: (event: NodeJS.Signals, listener: () => void) => unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const waitForSpirachaDevServer = async (
|
|
28
|
+
child: SpirachaChildProcess,
|
|
29
|
+
signalEmitter: SpirachaSignalEmitter = process,
|
|
30
|
+
): Promise<number> => {
|
|
31
|
+
const forwardSigint = () => child.kill('SIGINT');
|
|
32
|
+
const forwardSigterm = () => child.kill('SIGTERM');
|
|
33
|
+
signalEmitter.on('SIGINT', forwardSigint);
|
|
34
|
+
signalEmitter.on('SIGTERM', forwardSigterm);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return await child.exited;
|
|
38
|
+
} finally {
|
|
39
|
+
signalEmitter.off('SIGINT', forwardSigint);
|
|
40
|
+
signalEmitter.off('SIGTERM', forwardSigterm);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
17
44
|
export const runSpirachaDevServer = async (): Promise<number> => {
|
|
18
45
|
const command = buildSpirachaDevServerCommand();
|
|
19
46
|
const proc = Bun.spawn([process.execPath, ...command.args], {
|
|
@@ -24,7 +51,7 @@ export const runSpirachaDevServer = async (): Promise<number> => {
|
|
|
24
51
|
stdout: 'inherit',
|
|
25
52
|
});
|
|
26
53
|
|
|
27
|
-
return proc
|
|
54
|
+
return waitForSpirachaDevServer(proc);
|
|
28
55
|
};
|
|
29
56
|
|
|
30
57
|
if (import.meta.main) {
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@tailwindcss/typography": "0.5.20",
|
|
14
|
-
"@tailwindcss/vite": "4.3.
|
|
14
|
+
"@tailwindcss/vite": "4.3.3",
|
|
15
15
|
"@tanstack/devtools-vite": "0.8.1",
|
|
16
16
|
"@tanstack/match-sorter-utils": "8.19.4",
|
|
17
17
|
"@tanstack/react-devtools": "0.10.8",
|
|
@@ -20,28 +20,37 @@
|
|
|
20
20
|
"@tanstack/react-router": "1.170.18",
|
|
21
21
|
"@tanstack/react-router-devtools": "1.167.0",
|
|
22
22
|
"@tanstack/react-router-ssr-query": "1.167.1",
|
|
23
|
-
"@tanstack/react-start": "1.168.
|
|
23
|
+
"@tanstack/react-start": "1.168.30",
|
|
24
24
|
"@tanstack/react-table": "8.21.3",
|
|
25
25
|
"@tanstack/react-virtual": "3.14.6",
|
|
26
|
-
"@tanstack/router-plugin": "1.168.
|
|
26
|
+
"@tanstack/router-plugin": "1.168.22",
|
|
27
27
|
"@vitejs/plugin-react": "6.0.3",
|
|
28
28
|
"class-variance-authority": "0.7.1",
|
|
29
29
|
"clsx": "2.1.1",
|
|
30
|
-
"
|
|
30
|
+
"fflate": "0.8.3",
|
|
31
|
+
"lucide-react": "1.25.0",
|
|
31
32
|
"radix-ui": "1.6.2",
|
|
32
33
|
"react": "19.2.7",
|
|
33
34
|
"react-dom": "19.2.7",
|
|
34
35
|
"tailwind-merge": "3.6.0",
|
|
35
|
-
"tailwindcss": "4.3.
|
|
36
|
+
"tailwindcss": "4.3.3",
|
|
36
37
|
"tw-animate-css": "1.4.0",
|
|
37
|
-
"vite": "8.1.
|
|
38
|
+
"vite": "8.1.5",
|
|
38
39
|
"zod": "4.4.3"
|
|
39
40
|
},
|
|
40
41
|
"description": "Browse and expose local Codex, Claude Code, Grok, Kiro, Qoder, Cursor, Antigravity, and OpenCode conversation history through a local UI and stable data API.",
|
|
41
42
|
"devDependencies": {
|
|
43
|
+
"@biomejs/biome": "2.5.4",
|
|
44
|
+
"@testing-library/dom": "10.4.1",
|
|
45
|
+
"@testing-library/react": "16.3.2",
|
|
42
46
|
"@types/bun": "^1.3.14",
|
|
43
47
|
"@types/node": "^26.1.1",
|
|
44
|
-
"
|
|
48
|
+
"@types/react": "19.2.17",
|
|
49
|
+
"@types/react-dom": "19.2.3",
|
|
50
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
51
|
+
"jsdom": "29.1.1",
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
|
+
"vitest": "4.1.10"
|
|
45
54
|
},
|
|
46
55
|
"engines": {
|
|
47
56
|
"bun": ">=1.3.14"
|
|
@@ -63,7 +72,6 @@
|
|
|
63
72
|
"files": [
|
|
64
73
|
"bin/spiracha.ts",
|
|
65
74
|
"apps/ui/components.json",
|
|
66
|
-
"apps/ui/package.json",
|
|
67
75
|
"apps/ui/public/**/*",
|
|
68
76
|
"apps/ui/src/**/*",
|
|
69
77
|
"!apps/ui/src/**/*.vitest.ts",
|
|
@@ -78,6 +86,9 @@
|
|
|
78
86
|
"AGENTS.md"
|
|
79
87
|
],
|
|
80
88
|
"homepage": "https://github.com/ragaeeb/spiracha",
|
|
89
|
+
"imports": {
|
|
90
|
+
"#/*": "./apps/ui/src/*"
|
|
91
|
+
},
|
|
81
92
|
"keywords": [
|
|
82
93
|
"codex",
|
|
83
94
|
"claude",
|
|
@@ -100,21 +111,19 @@
|
|
|
100
111
|
"url": "git+https://github.com/ragaeeb/spiracha.git"
|
|
101
112
|
},
|
|
102
113
|
"scripts": {
|
|
103
|
-
"build": "bun run typecheck && bun
|
|
114
|
+
"build": "bun run typecheck && bun --cwd apps/ui --bun vite build",
|
|
104
115
|
"coverage": "bun run coverage:root && bun run coverage:ui",
|
|
105
116
|
"coverage:root": "bun test --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run ./src/coverage-check.ts root",
|
|
106
|
-
"coverage:ui": "
|
|
117
|
+
"coverage:ui": "vitest run --coverage --config apps/ui/vitest.config.ts && bun run ./src/coverage-check.ts ui",
|
|
107
118
|
"format": "biome check . --write && biome lint . --write",
|
|
108
119
|
"lint": "biome check .",
|
|
109
|
-
"start": "bun
|
|
120
|
+
"start": "bun --cwd apps/ui --bun vite dev --host 127.0.0.1 --port 3000",
|
|
121
|
+
"test:ui": "vitest run --config apps/ui/vitest.config.ts",
|
|
110
122
|
"typecheck": "bun run typecheck:root && bun run typecheck:ui",
|
|
111
123
|
"typecheck:root": "bunx tsc --noEmit",
|
|
112
|
-
"typecheck:ui": "
|
|
113
|
-
"ui:preview": "bun
|
|
124
|
+
"typecheck:ui": "bunx tsc --noEmit -p apps/ui/tsconfig.json",
|
|
125
|
+
"ui:preview": "bun --cwd apps/ui --bun vite preview"
|
|
114
126
|
},
|
|
115
127
|
"type": "module",
|
|
116
|
-
"version": "2.
|
|
117
|
-
"workspaces": [
|
|
118
|
-
"apps/*"
|
|
119
|
-
]
|
|
128
|
+
"version": "2.2.0"
|
|
120
129
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mapWithConcurrency } from './lib/concurrency';
|
|
1
2
|
import {
|
|
2
3
|
deleteConversation as deleteLocalConversation,
|
|
3
4
|
deleteConversations as deleteLocalConversations,
|
|
@@ -51,9 +52,11 @@ export type {
|
|
|
51
52
|
type HttpEnvelope<T> = {
|
|
52
53
|
data?: T;
|
|
53
54
|
error?: {
|
|
55
|
+
code?: string | null;
|
|
54
56
|
message?: string | null;
|
|
55
57
|
} | null;
|
|
56
58
|
meta?: {
|
|
59
|
+
has_next?: boolean | null;
|
|
57
60
|
hasNext?: boolean | null;
|
|
58
61
|
nextCursor?: string | null;
|
|
59
62
|
next_cursor?: string | null;
|
|
@@ -196,9 +199,22 @@ const fetchJson = async <T>(url: URL, init?: RequestInit): Promise<HttpEnvelope<
|
|
|
196
199
|
return readJsonEnvelope(response);
|
|
197
200
|
};
|
|
198
201
|
|
|
202
|
+
const isMissingConversationResponse = async (response: Response): Promise<boolean> => {
|
|
203
|
+
if (response.status !== 404) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const envelope = (await response.clone().json()) as HttpEnvelope<unknown>;
|
|
209
|
+
return envelope.error?.code === 'conversation_not_found';
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
199
215
|
const fetchJsonOrNull = async <T>(url: URL, init?: RequestInit): Promise<HttpEnvelope<T> | null> => {
|
|
200
216
|
const response = await fetchResponse(url, init);
|
|
201
|
-
if (response
|
|
217
|
+
if (await isMissingConversationResponse(response)) {
|
|
202
218
|
return null;
|
|
203
219
|
}
|
|
204
220
|
|
|
@@ -206,9 +222,30 @@ const fetchJsonOrNull = async <T>(url: URL, init?: RequestInit): Promise<HttpEnv
|
|
|
206
222
|
return readJsonEnvelope(response);
|
|
207
223
|
};
|
|
208
224
|
|
|
225
|
+
const fetchDeleteJsonOrNull = async <T>(url: URL, init?: RequestInit): Promise<HttpEnvelope<T> | null> => {
|
|
226
|
+
const response = await fetchResponse(url, init);
|
|
227
|
+
if (await isMissingConversationResponse(response)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (response.status === 405) {
|
|
232
|
+
try {
|
|
233
|
+
const envelope = (await response.clone().json()) as HttpEnvelope<T>;
|
|
234
|
+
if (envelope.error?.code === 'unsupported_operation') {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Let the normal error path report malformed error responses.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await assertOkResponse(response);
|
|
243
|
+
return readJsonEnvelope(response);
|
|
244
|
+
};
|
|
245
|
+
|
|
209
246
|
const fetchTextOrNull = async (url: URL, init?: RequestInit): Promise<string | null> => {
|
|
210
247
|
const response = await fetchResponse(url, init);
|
|
211
|
-
if (response
|
|
248
|
+
if (await isMissingConversationResponse(response)) {
|
|
212
249
|
return null;
|
|
213
250
|
}
|
|
214
251
|
|
|
@@ -232,7 +269,7 @@ const fileNameFromContentDisposition = (contentDisposition: string | null, fallb
|
|
|
232
269
|
|
|
233
270
|
const fetchZipOrNull = async (url: URL, init?: RequestInit): Promise<ConversationZipDownload | null> => {
|
|
234
271
|
const response = await fetchResponse(url, init);
|
|
235
|
-
if (response
|
|
272
|
+
if (await isMissingConversationResponse(response)) {
|
|
236
273
|
return null;
|
|
237
274
|
}
|
|
238
275
|
|
|
@@ -261,13 +298,20 @@ const normalizePage = (envelope: HttpEnvelope<ConversationDetail[]>): Conversati
|
|
|
261
298
|
return {
|
|
262
299
|
data,
|
|
263
300
|
meta: {
|
|
264
|
-
hasNext: envelope.meta?.hasNext === true,
|
|
301
|
+
hasNext: envelope.meta?.has_next === true || envelope.meta?.hasNext === true,
|
|
265
302
|
nextCursor: envelope.meta?.nextCursor ?? envelope.meta?.next_cursor ?? null,
|
|
266
303
|
},
|
|
267
304
|
};
|
|
268
305
|
};
|
|
269
306
|
|
|
270
|
-
const makeHttpUrl = (baseUrl: URL, pathname: string): URL =>
|
|
307
|
+
const makeHttpUrl = (baseUrl: URL, pathname: string): URL => {
|
|
308
|
+
const url = new URL(baseUrl);
|
|
309
|
+
const basePath = url.pathname.replace(/\/+$/u, '');
|
|
310
|
+
url.pathname = `${basePath}/${pathname.replace(/^\/+/, '')}`;
|
|
311
|
+
url.search = '';
|
|
312
|
+
url.hash = '';
|
|
313
|
+
return url;
|
|
314
|
+
};
|
|
271
315
|
|
|
272
316
|
const rejectHttpLocations = (locations: ConversationDataLocations | undefined): void => {
|
|
273
317
|
if (locations) {
|
|
@@ -287,15 +331,16 @@ const exportLocalConversationsZip = async (
|
|
|
287
331
|
locations: ConversationDataLocations | undefined,
|
|
288
332
|
) => {
|
|
289
333
|
const options = withDefaultLocations(exportOptions, locations);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
334
|
+
if (options.ids.length > 200) {
|
|
335
|
+
throw new SpirachaClientError('At most 200 conversation ids may be exported at once.');
|
|
336
|
+
}
|
|
337
|
+
const conversations = await mapWithConcurrency(options.ids, 4, (id) =>
|
|
338
|
+
getLocalConversation({
|
|
339
|
+
id,
|
|
340
|
+
locations: options.locations,
|
|
341
|
+
messageSelector: options.messageSelector ?? 'all',
|
|
342
|
+
source: options.source,
|
|
343
|
+
}),
|
|
299
344
|
);
|
|
300
345
|
|
|
301
346
|
if (conversations.some((conversation) => conversation === null)) {
|
|
@@ -346,7 +391,7 @@ const makeHttpClient = (options: HttpConversationClientOptions): ConversationCli
|
|
|
346
391
|
rejectHttpLocations(deleteOptions.locations);
|
|
347
392
|
const { id, source } = deleteOptions;
|
|
348
393
|
const url = makeHttpUrl(baseUrl, `/api/v1/conversations/${source}/${encodeURIComponent(id)}`);
|
|
349
|
-
const envelope = await
|
|
394
|
+
const envelope = await fetchDeleteJsonOrNull<DeleteConversationResult>(url, { method: 'DELETE' });
|
|
350
395
|
if (!envelope) {
|
|
351
396
|
return null;
|
|
352
397
|
}
|
|
@@ -354,7 +399,7 @@ const makeHttpClient = (options: HttpConversationClientOptions): ConversationCli
|
|
|
354
399
|
},
|
|
355
400
|
deleteConversations: async (deleteOptions) => {
|
|
356
401
|
rejectHttpLocations(deleteOptions.locations);
|
|
357
|
-
const envelope = await
|
|
402
|
+
const envelope = await fetchDeleteJsonOrNull<DeleteConversationsResult>(
|
|
358
403
|
makeHttpUrl(baseUrl, '/api/v1/conversations/delete'),
|
|
359
404
|
{
|
|
360
405
|
body: JSON.stringify({
|