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.
Files changed (125) hide show
  1. package/AGENTS.md +27 -12
  2. package/README.md +37 -19
  3. package/apps/ui/AGENTS.md +18 -10
  4. package/apps/ui/README.md +17 -10
  5. package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
  6. package/apps/ui/src/components/antigravity-conversations-table.tsx +1 -0
  7. package/apps/ui/src/components/app-shell.tsx +45 -3
  8. package/apps/ui/src/components/data-table.tsx +17 -1
  9. package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
  10. package/apps/ui/src/components/export-dialog.tsx +77 -62
  11. package/apps/ui/src/components/projects-table.tsx +4 -1
  12. package/apps/ui/src/components/route-error-panel.tsx +13 -0
  13. package/apps/ui/src/components/thread-goals-panel.tsx +50 -0
  14. package/apps/ui/src/components/thread-tools-panel.tsx +142 -0
  15. package/apps/ui/src/components/threads-table.tsx +4 -1
  16. package/apps/ui/src/lib/antigravity-server.ts +70 -17
  17. package/apps/ui/src/lib/antigravity-transcript-events.ts +46 -70
  18. package/apps/ui/src/lib/claude-code-server.ts +14 -4
  19. package/apps/ui/src/lib/codex-queries.ts +3 -3
  20. package/apps/ui/src/lib/codex-server.ts +3 -3
  21. package/apps/ui/src/lib/cursor-server.ts +41 -69
  22. package/apps/ui/src/lib/cursor-transcript-events.ts +11 -86
  23. package/apps/ui/src/lib/delete-batch.ts +52 -0
  24. package/apps/ui/src/lib/error-presentation.ts +43 -0
  25. package/apps/ui/src/lib/export-mutation.ts +20 -0
  26. package/apps/ui/src/lib/export-options.ts +15 -0
  27. package/apps/ui/src/lib/grok-server.ts +2 -3
  28. package/apps/ui/src/lib/kiro-server.ts +14 -2
  29. package/apps/ui/src/lib/mutation-error.ts +15 -0
  30. package/apps/ui/src/lib/opencode-server.ts +13 -2
  31. package/apps/ui/src/lib/path-utils.ts +1 -1
  32. package/apps/ui/src/lib/qoder-queries.ts +15 -0
  33. package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
  34. package/apps/ui/src/lib/route-state-reset.tsx +10 -0
  35. package/apps/ui/src/lib/settings-server.ts +33 -0
  36. package/apps/ui/src/lib/settings-store.tsx +82 -38
  37. package/apps/ui/src/lib/settings.ts +65 -0
  38. package/apps/ui/src/lib/source-session-export-server.ts +1 -1
  39. package/apps/ui/src/lib/thread-metadata.ts +22 -0
  40. package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
  41. package/apps/ui/src/lib/workspace-delete-navigation.ts +8 -0
  42. package/apps/ui/src/routeTree.gen.ts +282 -282
  43. package/apps/ui/src/routes/__root.tsx +12 -15
  44. package/apps/ui/src/routes/analytics.tsx +10 -61
  45. package/apps/ui/src/routes/antigravity-conversations.$conversationId.tsx +73 -19
  46. package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +42 -60
  47. package/apps/ui/src/routes/antigravity.index.tsx +2 -2
  48. package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +25 -11
  49. package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +27 -32
  50. package/apps/ui/src/routes/claude-code.index.tsx +2 -2
  51. package/apps/ui/src/routes/codex.$project.tsx +33 -31
  52. package/apps/ui/src/routes/codex.index.tsx +5 -10
  53. package/apps/ui/src/routes/cursor-threads.$composerId.tsx +29 -16
  54. package/apps/ui/src/routes/cursor.$workspaceKey.tsx +33 -27
  55. package/apps/ui/src/routes/cursor.index.tsx +5 -2
  56. package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +25 -12
  57. package/apps/ui/src/routes/grok.$workspaceKey.tsx +20 -26
  58. package/apps/ui/src/routes/grok.index.tsx +2 -2
  59. package/apps/ui/src/routes/index.tsx +2 -18
  60. package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +25 -12
  61. package/apps/ui/src/routes/kiro.$workspaceKey.tsx +27 -32
  62. package/apps/ui/src/routes/kiro.index.tsx +2 -2
  63. package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +29 -36
  64. package/apps/ui/src/routes/opencode.$workspaceKey.tsx +45 -61
  65. package/apps/ui/src/routes/opencode.index.tsx +8 -17
  66. package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +12 -11
  67. package/apps/ui/src/routes/qoder.$workspaceKey.tsx +19 -25
  68. package/apps/ui/src/routes/qoder.index.tsx +2 -2
  69. package/apps/ui/src/routes/threads.$threadId.tsx +87 -56
  70. package/apps/ui/vite.config.ts +22 -12
  71. package/bin/spiracha.ts +28 -1
  72. package/package.json +26 -17
  73. package/src/client.ts +61 -16
  74. package/src/lib/antigravity-db.ts +236 -82
  75. package/src/lib/antigravity-keychain.ts +7 -8
  76. package/src/lib/antigravity-transcript-phase.ts +47 -0
  77. package/src/lib/claude-code-db.ts +45 -19
  78. package/src/lib/codex-analytics.ts +32 -5
  79. package/src/lib/codex-browser-db.ts +540 -137
  80. package/src/lib/codex-browser-export.ts +2 -7
  81. package/src/lib/codex-browser-types.ts +21 -1
  82. package/src/lib/codex-thread-cache.ts +88 -33
  83. package/src/lib/codex-thread-parser.ts +78 -35
  84. package/src/lib/codex-thread-recovery.ts +38 -11
  85. package/src/lib/codex-transcript-renderer.ts +49 -61
  86. package/src/lib/concurrency.ts +15 -3
  87. package/src/lib/conversation-api.ts +34 -31
  88. package/src/lib/conversation-data/adapter-helpers.ts +25 -3
  89. package/src/lib/conversation-data/antigravity-adapter.ts +54 -29
  90. package/src/lib/conversation-data/claude-code-adapter.ts +32 -6
  91. package/src/lib/conversation-data/codex-adapter.ts +91 -80
  92. package/src/lib/conversation-data/cursor-adapter.ts +48 -34
  93. package/src/lib/conversation-data/grok-adapter.ts +19 -6
  94. package/src/lib/conversation-data/index.ts +29 -10
  95. package/src/lib/conversation-data/kiro-adapter.ts +18 -5
  96. package/src/lib/conversation-data/message-selector.ts +2 -5
  97. package/src/lib/conversation-data/opencode-adapter.ts +43 -15
  98. package/src/lib/conversation-data/path-match.ts +1 -13
  99. package/src/lib/conversation-data/qoder-adapter.ts +23 -18
  100. package/src/lib/conversation-zip-export.ts +20 -2
  101. package/src/lib/cursor-db.ts +34 -23
  102. package/src/lib/cursor-id.ts +17 -0
  103. package/src/lib/cursor-recovery.ts +146 -35
  104. package/src/lib/cursor-transcript-phase.ts +40 -0
  105. package/src/lib/cursor-transcript.ts +21 -5
  106. package/src/lib/grok-db.ts +76 -34
  107. package/src/lib/grok-transcript-phase.ts +11 -2
  108. package/src/lib/grok-transcript.ts +15 -2
  109. package/src/lib/kiro-db.ts +161 -68
  110. package/src/lib/kiro-transcript.ts +0 -4
  111. package/src/lib/opencode-db.ts +4 -8
  112. package/src/lib/opencode-think-tags.ts +17 -3
  113. package/src/lib/portable-path.ts +9 -0
  114. package/src/lib/qoder-acp-client.ts +11 -2
  115. package/src/lib/qoder-db.ts +51 -17
  116. package/src/lib/qoder-transcript-phase.ts +64 -0
  117. package/src/lib/qoder-transcript.ts +30 -2
  118. package/src/lib/shared.ts +51 -15
  119. package/src/lib/sqlite-error.ts +31 -5
  120. package/src/lib/transcript-load-limiter.ts +8 -3
  121. package/src/lib/ui-cache.ts +152 -17
  122. package/src/lib/ui-export-archive.ts +14 -55
  123. package/src/lib/ui-export-files.ts +75 -15
  124. package/src/lib/ui-export-zip.ts +40 -0
  125. package/apps/ui/package.json +0 -65
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, rm, stat } from 'node:fs/promises';
1
+ import { chmod, mkdir, readdir, rm, stat } from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
@@ -7,6 +7,8 @@ export const UI_EXPORT_URL_PREFIX = '/__exports/';
7
7
 
8
8
  const DEFAULT_UI_EXPORT_DIR = path.join(os.tmpdir(), 'spiracha-ui-exports');
9
9
  const DEFAULT_EXPORT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
10
+ const DEFAULT_EXPORT_MAX_BYTES = 1024 * 1024 * 1024;
11
+ const MAX_EXPORT_FILE_NAME_BYTES = 200;
10
12
 
11
13
  const decodeExportFileName = (value: string) => {
12
14
  try {
@@ -17,7 +19,15 @@ const decodeExportFileName = (value: string) => {
17
19
  };
18
20
 
19
21
  const isSafeExportFileName = (value: string) => {
20
- return value.length > 0 && value === path.basename(value) && !/[\\/]/u.test(value) && !value.includes('\0');
22
+ return (
23
+ value.length > 0 &&
24
+ Buffer.byteLength(value) <= MAX_EXPORT_FILE_NAME_BYTES &&
25
+ value !== '.' &&
26
+ value !== '..' &&
27
+ value === path.basename(value) &&
28
+ !/[\\/]/u.test(value) &&
29
+ !value.includes('\0')
30
+ );
21
31
  };
22
32
 
23
33
  export const getUiExportDir = () => {
@@ -26,7 +36,8 @@ export const getUiExportDir = () => {
26
36
 
27
37
  export const ensureUiExportDir = async () => {
28
38
  const exportDir = getUiExportDir();
29
- await mkdir(exportDir, { recursive: true });
39
+ await mkdir(exportDir, { mode: 0o700, recursive: true });
40
+ await chmod(exportDir, 0o700);
30
41
  await purgeStaleUiExports(exportDir);
31
42
  return exportDir;
32
43
  };
@@ -40,26 +51,61 @@ export const buildUiExportContentDisposition = (filePath: string) => {
40
51
  return `attachment; filename*=UTF-8''${encodeURIComponent(fileName)}`;
41
52
  };
42
53
 
54
+ export const purgeStaleUiExportFile = async (filePath: string, cutoff: number) => {
55
+ let metadata: Awaited<ReturnType<typeof stat>>;
56
+ try {
57
+ metadata = await stat(filePath);
58
+ } catch (error) {
59
+ if ((error as { code?: unknown }).code === 'ENOENT') {
60
+ return;
61
+ }
62
+ throw error;
63
+ }
64
+ if (metadata.mtimeMs < cutoff) {
65
+ await rm(filePath, { force: true });
66
+ }
67
+ };
68
+
43
69
  export const purgeStaleUiExports = async (
44
70
  exportDir: string = getUiExportDir(),
45
71
  maxAgeMs: number = DEFAULT_EXPORT_MAX_AGE_MS,
72
+ maxBytes: number = DEFAULT_EXPORT_MAX_BYTES,
46
73
  ) => {
47
74
  const entries = await readdir(exportDir, { withFileTypes: true });
48
75
  const cutoff = Date.now() - maxAgeMs;
76
+ const files = (
77
+ await Promise.all(
78
+ entries
79
+ .filter((entry) => entry.isFile())
80
+ .map(async (entry) => {
81
+ const filePath = path.join(exportDir, entry.name);
82
+ try {
83
+ const metadata = await stat(filePath);
84
+ return { filePath, mtimeMs: metadata.mtimeMs, name: entry.name, size: metadata.size };
85
+ } catch (error) {
86
+ if ((error as { code?: unknown }).code === 'ENOENT') {
87
+ return null;
88
+ }
89
+ throw error;
90
+ }
91
+ }),
92
+ )
93
+ ).filter((file): file is NonNullable<typeof file> => file !== null);
94
+ const staleFiles = files.filter((file) => file.mtimeMs < cutoff);
95
+ await Promise.all(staleFiles.map((file) => rm(file.filePath, { force: true })));
49
96
 
50
- await Promise.all(
51
- entries
52
- .filter((entry) => entry.isFile())
53
- .map(async (entry) => {
54
- const filePath = path.join(exportDir, entry.name);
55
- const metadata = await stat(filePath);
56
- if (metadata.mtimeMs >= cutoff) {
57
- return;
58
- }
59
-
60
- await rm(filePath, { force: true });
61
- }),
97
+ const retainedFiles = files.filter((file) => file.mtimeMs >= cutoff);
98
+ let retainedBytes = retainedFiles.reduce((total, file) => total + file.size, 0);
99
+ const oldestFirst = retainedFiles.sort(
100
+ (left, right) => left.mtimeMs - right.mtimeMs || left.name.localeCompare(right.name),
62
101
  );
102
+ for (const file of oldestFirst) {
103
+ if (retainedBytes <= maxBytes) {
104
+ break;
105
+ }
106
+ await rm(file.filePath, { force: true });
107
+ retainedBytes -= file.size;
108
+ }
63
109
  };
64
110
 
65
111
  export const resolveUiExportFilePathFromRequestPath = (pathname: string) => {
@@ -75,3 +121,17 @@ export const resolveUiExportFilePathFromRequestPath = (pathname: string) => {
75
121
 
76
122
  return path.join(getUiExportDir(), fileName);
77
123
  };
124
+
125
+ export const resolveReadableUiExportFileFromRequestPath = async (pathname: string) => {
126
+ const filePath = resolveUiExportFilePathFromRequestPath(pathname);
127
+ if (!filePath) {
128
+ return null;
129
+ }
130
+
131
+ try {
132
+ const metadata = await stat(filePath);
133
+ return metadata.isFile() ? filePath : null;
134
+ } catch {
135
+ return null;
136
+ }
137
+ };
@@ -0,0 +1,40 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { type Zippable, zipSync } from 'fflate';
4
+
5
+ const readZipFile = async (filePath: string) => {
6
+ return new Uint8Array(await Bun.file(filePath).arrayBuffer());
7
+ };
8
+
9
+ const createZip = async (files: Zippable) => zipSync(files, { level: 3 });
10
+
11
+ const readZipDirectory = async (
12
+ rootDirectory: string,
13
+ currentDirectory = rootDirectory,
14
+ files: Zippable = {},
15
+ ): Promise<Zippable> => {
16
+ const entries = await readdir(currentDirectory, { withFileTypes: true });
17
+ await Promise.all(
18
+ entries.map(async (entry) => {
19
+ const entryPath = path.join(currentDirectory, entry.name);
20
+ if (entry.isDirectory()) {
21
+ await readZipDirectory(rootDirectory, entryPath, files);
22
+ return;
23
+ }
24
+
25
+ if (entry.isFile()) {
26
+ const archivePath = path.relative(rootDirectory, entryPath).split(path.sep).join('/');
27
+ files[archivePath] = await readZipFile(entryPath);
28
+ }
29
+ }),
30
+ );
31
+ return files;
32
+ };
33
+
34
+ export const zipExportFile = async (sourcePath: string, zipPath: string) => {
35
+ await Bun.write(zipPath, await createZip({ [path.basename(sourcePath)]: await readZipFile(sourcePath) }));
36
+ };
37
+
38
+ export const zipExportDirectory = async (sourceDirectory: string, zipPath: string) => {
39
+ await Bun.write(zipPath, await createZip(await readZipDirectory(sourceDirectory)));
40
+ };
@@ -1,65 +0,0 @@
1
- {
2
- "dependencies": {
3
- "@tailwindcss/vite": "4.3.1",
4
- "@tanstack/match-sorter-utils": "8.19.4",
5
- "@tanstack/react-devtools": "0.10.5",
6
- "@tanstack/react-query": "5.101.2",
7
- "@tanstack/react-query-devtools": "5.101.2",
8
- "@tanstack/react-router": "1.170.15",
9
- "@tanstack/react-router-devtools": "1.167.0",
10
- "@tanstack/react-router-ssr-query": "1.167.1",
11
- "@tanstack/react-start": "1.168.25",
12
- "@tanstack/react-table": "8.21.3",
13
- "@tanstack/react-virtual": "3.14.2",
14
- "@tanstack/router-plugin": "1.168.18",
15
- "class-variance-authority": "0.7.1",
16
- "clsx": "2.1.1",
17
- "lucide-react": "1.18.0",
18
- "radix-ui": "1.5.0",
19
- "react": "19.2.7",
20
- "react-dom": "19.2.7",
21
- "tailwind-merge": "3.6.0",
22
- "tailwindcss": "4.3.1",
23
- "tw-animate-css": "1.4.0",
24
- "zod": "4.4.3"
25
- },
26
- "devDependencies": {
27
- "@biomejs/biome": "2.5.0",
28
- "@tailwindcss/typography": "0.5.20",
29
- "@tanstack/devtools-vite": "0.7.0",
30
- "@testing-library/dom": "10.4.1",
31
- "@testing-library/react": "16.3.2",
32
- "@types/node": "25.9.3",
33
- "@types/react": "19.2.17",
34
- "@types/react-dom": "19.2.3",
35
- "@vitejs/plugin-react": "6.0.2",
36
- "@vitest/coverage-v8": "4.1.8",
37
- "jsdom": "29.1.1",
38
- "typescript": "6.0.3",
39
- "vite": "8.0.16",
40
- "vitest": "4.1.8"
41
- },
42
- "imports": {
43
- "#/*": "./src/*"
44
- },
45
- "name": "ui",
46
- "pnpm": {
47
- "onlyBuiltDependencies": [
48
- "esbuild",
49
- "lightningcss"
50
- ]
51
- },
52
- "private": true,
53
- "scripts": {
54
- "build": "bun --bun vite build",
55
- "check": "biome check",
56
- "coverage": "vitest run --coverage",
57
- "dev": "bun --bun vite dev --host 127.0.0.1 --port 3000",
58
- "format": "biome format",
59
- "lint": "biome lint",
60
- "preview": "bun --bun vite preview",
61
- "test": "vitest run",
62
- "typecheck": "tsc --noEmit"
63
- },
64
- "type": "module"
65
- }