spiracha 1.0.0 → 1.1.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.
Files changed (92) hide show
  1. package/AGENTS.md +31 -1
  2. package/README.md +61 -7
  3. package/apps/ui/AGENTS.md +70 -0
  4. package/apps/ui/README.md +72 -0
  5. package/apps/ui/dist/client/assets/_threadId-CAIeH5mq.js +1 -0
  6. package/apps/ui/dist/client/assets/analytics-CqWZmyV6.js +1 -0
  7. package/apps/ui/dist/client/assets/checkbox-DXM4lkJq.js +1 -0
  8. package/apps/ui/dist/client/assets/data-table-DnPYMPCD.js +4 -0
  9. package/apps/ui/dist/client/assets/delete-confirm-dialog-CcZaRX33.js +11 -0
  10. package/apps/ui/dist/client/assets/download-DOwxk-cG.js +1 -0
  11. package/apps/ui/dist/client/assets/es2015-Bm0kEzx2.js +41 -0
  12. package/apps/ui/dist/client/assets/formatters-C12LmYaa.js +1 -0
  13. package/apps/ui/dist/client/assets/index-DdJ7ahIt.js +22 -0
  14. package/apps/ui/dist/client/assets/input-CEsI7EpI.js +1 -0
  15. package/apps/ui/dist/client/assets/metric-card-9jwBF7rG.js +1 -0
  16. package/apps/ui/dist/client/assets/page-header-Dr_h1CVv.js +1 -0
  17. package/apps/ui/dist/client/assets/projects._project-uyNGnpjH.js +1 -0
  18. package/apps/ui/dist/client/assets/projects._project-zoM8d2nH.js +1 -0
  19. package/apps/ui/dist/client/assets/projects.index-D1CWVN-O.js +1 -0
  20. package/apps/ui/dist/client/assets/projects.index-DukMuny6.js +1 -0
  21. package/apps/ui/dist/client/assets/routes-Gr2Wwh83.js +1 -0
  22. package/apps/ui/dist/client/assets/select-CFim44gT.js +1 -0
  23. package/apps/ui/dist/client/assets/settings-DqhyDxo2.js +1 -0
  24. package/apps/ui/dist/client/assets/styles-CMrP9Jb4.css +1 -0
  25. package/apps/ui/dist/client/assets/threads._threadId-DT75NiBa.js +1 -0
  26. package/apps/ui/dist/client/assets/threads._threadId-Df5VXIuZ.js +7 -0
  27. package/apps/ui/dist/client/favicon.ico +0 -0
  28. package/apps/ui/dist/client/logo192.png +0 -0
  29. package/apps/ui/dist/client/logo512.png +0 -0
  30. package/apps/ui/dist/client/manifest.json +25 -0
  31. package/apps/ui/dist/client/robots.txt +3 -0
  32. package/apps/ui/dist/server/assets/__23tanstack-start-plugin-adapters-BzCA6dXo.js +5 -0
  33. package/apps/ui/dist/server/assets/_tanstack-start-manifest_v-C0V305Nt.js +99 -0
  34. package/apps/ui/dist/server/assets/_threadId-B6SrBR9E.js +6 -0
  35. package/apps/ui/dist/server/assets/analytics-BMxW_bZL.js +139 -0
  36. package/apps/ui/dist/server/assets/button-CmTDnzOn.js +46 -0
  37. package/apps/ui/dist/server/assets/checkbox-C0hovF41.js +19 -0
  38. package/apps/ui/dist/server/assets/codex-queries-CAF6HYiG.js +109 -0
  39. package/apps/ui/dist/server/assets/codex-server-BFZq2Y2O.js +2062 -0
  40. package/apps/ui/dist/server/assets/data-table-Cdct823O.js +189 -0
  41. package/apps/ui/dist/server/assets/delete-confirm-dialog-CWqcTXTF.js +139 -0
  42. package/apps/ui/dist/server/assets/download-C5rkk_Bo.js +289 -0
  43. package/apps/ui/dist/server/assets/formatters-FJaGZgJk.js +91 -0
  44. package/apps/ui/dist/server/assets/input-B4tEzctc.js +46 -0
  45. package/apps/ui/dist/server/assets/loading-panel-DbLdvjtR.js +27 -0
  46. package/apps/ui/dist/server/assets/metric-card-ByEeLu0r.js +23 -0
  47. package/apps/ui/dist/server/assets/model-label-B1NWGc65.js +13 -0
  48. package/apps/ui/dist/server/assets/page-header-CxdZM86z.js +25 -0
  49. package/apps/ui/dist/server/assets/path-transforms-DL2IwtYd.js +31 -0
  50. package/apps/ui/dist/server/assets/projects._project-CJ7l0ynC.js +18 -0
  51. package/apps/ui/dist/server/assets/projects._project-CLSohrBp.js +26 -0
  52. package/apps/ui/dist/server/assets/projects._project-CcJLp_A8.js +337 -0
  53. package/apps/ui/dist/server/assets/projects.index-CaplpeMy.js +26 -0
  54. package/apps/ui/dist/server/assets/projects.index-srtogpuF.js +172 -0
  55. package/apps/ui/dist/server/assets/router-C_w-haH6.js +307 -0
  56. package/apps/ui/dist/server/assets/routes-BhbxvJE7.js +34 -0
  57. package/apps/ui/dist/server/assets/routes-CPe-ppmC.js +169 -0
  58. package/apps/ui/dist/server/assets/select-GW76p-ld.js +76 -0
  59. package/apps/ui/dist/server/assets/settings-MvWDgc1u.js +100 -0
  60. package/apps/ui/dist/server/assets/settings-store-DpEJEQ7M.js +52 -0
  61. package/apps/ui/dist/server/assets/sqlite-error-LZDrnxdd.js +13 -0
  62. package/apps/ui/dist/server/assets/start-HeKLHD9b.js +4 -0
  63. package/apps/ui/dist/server/assets/threads._threadId-BSSK4nkI.js +26 -0
  64. package/apps/ui/dist/server/assets/threads._threadId-Ba7vv6-K.js +18 -0
  65. package/apps/ui/dist/server/assets/threads._threadId-euyNckhj.js +1059 -0
  66. package/apps/ui/dist/server/assets/utils-C_uf36nf.js +8 -0
  67. package/apps/ui/dist/server/server.js +5678 -0
  68. package/package.json +53 -7
  69. package/src/export-chats.ts +4 -18
  70. package/src/lib/claude-exporter.ts +1 -1
  71. package/src/lib/codex-analytics.ts +100 -0
  72. package/src/lib/codex-browser-db.ts +605 -0
  73. package/src/lib/codex-browser-export.ts +429 -0
  74. package/src/lib/codex-browser-types.ts +224 -0
  75. package/src/lib/codex-exporter-cli.ts +6 -1
  76. package/src/lib/codex-exporter-db.ts +19 -20
  77. package/src/lib/codex-exporter-transcript.ts +158 -34
  78. package/src/lib/codex-exporter-types.ts +8 -0
  79. package/src/lib/codex-thread-cache.ts +58 -0
  80. package/src/lib/codex-thread-parser.ts +604 -0
  81. package/src/lib/interactive-cli.ts +10 -25
  82. package/src/lib/model-label.ts +24 -0
  83. package/src/lib/native-open.ts +54 -0
  84. package/src/lib/path-transforms.ts +46 -0
  85. package/src/lib/shared.ts +15 -1
  86. package/src/lib/sqlite-error.ts +14 -0
  87. package/src/lib/sqlite-retry.ts +53 -0
  88. package/src/lib/ui-cache.ts +96 -0
  89. package/src/lib/ui-export-files.ts +77 -0
  90. package/src/mcp-server.ts +1 -0
  91. package/src/spiracha.ts +16 -4
  92. package/src/ui-cli.ts +310 -0
package/package.json CHANGED
@@ -12,14 +12,27 @@
12
12
  "url": "https://github.com/ragaeeb/spiracha/issues"
13
13
  },
14
14
  "dependencies": {
15
- "@inquirer/prompts": "^8.4.2",
15
+ "@inquirer/prompts": "^8.4.3",
16
16
  "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "@tanstack/react-query": "5.100.14",
18
+ "@tanstack/react-router": "1.170.8",
19
+ "@tanstack/react-router-ssr-query": "1.167.0",
20
+ "@tanstack/react-table": "8.21.3",
21
+ "@tanstack/react-virtual": "3.13.25",
22
+ "class-variance-authority": "0.7.1",
23
+ "clsx": "2.1.1",
24
+ "iconv-lite": "^0.7.2",
25
+ "lucide-react": "1.16.0",
26
+ "radix-ui": "1.4.3",
27
+ "react": "19.2.6",
28
+ "react-dom": "19.2.6",
29
+ "tailwind-merge": "3.6.0",
17
30
  "zod": "^4.4.3"
18
31
  },
19
32
  "description": "Export local Codex chats and Claude Code transcripts to Markdown or plain text.",
20
33
  "devDependencies": {
21
- "@types/bun": "^1.3.13",
22
- "@types/node": "^25.6.2",
34
+ "@types/bun": "^1.3.14",
35
+ "@types/node": "^25.9.1",
23
36
  "typescript": "^6.0.3"
24
37
  },
25
38
  "engines": {
@@ -30,14 +43,35 @@
30
43
  "src/export-claude.ts",
31
44
  "src/mcp-server.ts",
32
45
  "src/spiracha.ts",
46
+ "src/ui-cli.ts",
33
47
  "src/lib/claude-exporter.ts",
34
48
  "src/lib/codex-exporter-cli.ts",
35
49
  "src/lib/codex-exporter-db.ts",
36
50
  "src/lib/codex-exporter-transcript.ts",
37
51
  "src/lib/codex-exporter-types.ts",
38
52
  "src/lib/codex-exporter.ts",
53
+ "src/lib/codex-browser-db.ts",
54
+ "src/lib/codex-browser-export.ts",
55
+ "src/lib/codex-browser-types.ts",
56
+ "src/lib/codex-thread-cache.ts",
57
+ "src/lib/codex-thread-parser.ts",
58
+ "src/lib/codex-analytics.ts",
59
+ "src/lib/path-transforms.ts",
60
+ "src/lib/sqlite-error.ts",
61
+ "src/lib/sqlite-retry.ts",
62
+ "src/lib/ui-export-files.ts",
63
+ "src/lib/ui-cache.ts",
39
64
  "src/lib/interactive-cli.ts",
65
+ "src/lib/model-label.ts",
66
+ "src/lib/native-open.ts",
40
67
  "src/lib/shared.ts",
68
+ "apps/ui/dist/client/assets/**/*",
69
+ "apps/ui/dist/client/favicon.ico",
70
+ "apps/ui/dist/client/logo192.png",
71
+ "apps/ui/dist/client/logo512.png",
72
+ "apps/ui/dist/client/manifest.json",
73
+ "apps/ui/dist/client/robots.txt",
74
+ "apps/ui/dist/server/**/*",
41
75
  "README.md",
42
76
  "AGENTS.md"
43
77
  ],
@@ -52,19 +86,31 @@
52
86
  ],
53
87
  "license": "MIT",
54
88
  "name": "spiracha",
55
- "packageManager": "bun@1.3.13",
89
+ "packageManager": "bun@1.3.14",
56
90
  "repository": {
57
91
  "type": "git",
58
92
  "url": "git+https://github.com/ragaeeb/spiracha.git"
59
93
  },
60
94
  "scripts": {
61
- "build": "bun run typecheck",
95
+ "build": "bun run typecheck && bun run --cwd apps/ui build",
96
+ "coverage": "bun run coverage:root && bun run coverage:ui",
97
+ "coverage:root": "bun test --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run ./src/coverage-check.ts root",
98
+ "coverage:ui": "bun run --cwd apps/ui coverage && bun run ./src/coverage-check.ts ui",
62
99
  "export:claude": "bun run ./src/export-claude.ts",
63
100
  "format": "biome check . --write && biome lint . --write",
101
+ "lint": "biome check .",
64
102
  "mcp": "bun run ./src/mcp-server.ts",
103
+ "smoke:package-ui": "bun run ./src/package-ui-smoke.ts",
65
104
  "start": "bun run ./src/export-chats.ts",
66
- "typecheck": "bunx tsc --noEmit"
105
+ "typecheck": "bun run typecheck:root && bun run typecheck:ui",
106
+ "typecheck:root": "bunx tsc --noEmit",
107
+ "typecheck:ui": "bun run --cwd apps/ui typecheck",
108
+ "ui:dev": "bun run --cwd apps/ui dev",
109
+ "ui:preview": "bun run --cwd apps/ui preview"
67
110
  },
68
111
  "type": "module",
69
- "version": "1.0.0"
112
+ "version": "1.1.1",
113
+ "workspaces": [
114
+ "apps/*"
115
+ ]
70
116
  }
@@ -5,7 +5,8 @@ import path from 'node:path';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
6
  import { createInterface } from 'node:readline/promises';
7
7
  import { getCodexHelpText, parseCodexCliArgs, runCodexExport } from './lib/codex-exporter';
8
- import { runInteractiveExport } from './lib/interactive-cli';
8
+ import type { InteractiveExportResult } from './lib/interactive-cli';
9
+ import { openPathNatively } from './lib/native-open';
9
10
  import { CliUsageError } from './lib/shared';
10
11
 
11
12
  export const runExportChatsCli = async (argv = process.argv.slice(2)): Promise<void> => {
@@ -43,6 +44,7 @@ const shouldRunInteractive = (argv: string[]): boolean => {
43
44
  };
44
45
 
45
46
  const runInteractiveCliFlow = async (): Promise<void> => {
47
+ const { runInteractiveExport } = await import('./lib/interactive-cli');
46
48
  const result = await runInteractiveExport();
47
49
  const targetFolder = await printInteractiveExportResult(result);
48
50
  await maybeOpenExportFolder(targetFolder);
@@ -54,9 +56,7 @@ const runCodexCliFlow = async (argv: string[]): Promise<void> => {
54
56
  printCodexExportResult(result);
55
57
  };
56
58
 
57
- const printInteractiveExportResult = async (
58
- result: Awaited<ReturnType<typeof runInteractiveExport>>,
59
- ): Promise<string> => {
59
+ const printInteractiveExportResult = async (result: InteractiveExportResult): Promise<string> => {
60
60
  if (result.mode === 'claude') {
61
61
  console.log(`Exported ${result.sourcePath} -> ${result.outputPath}`);
62
62
  return resolveExportFolder(result.outputPath);
@@ -115,20 +115,6 @@ const resolveExportFolder = async (targetPath: string): Promise<string> => {
115
115
  return path.dirname(targetPath);
116
116
  };
117
117
 
118
- const openPathNatively = async (targetPath: string): Promise<void> => {
119
- const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
120
-
121
- const proc = Bun.spawn([command, targetPath], {
122
- stderr: 'pipe',
123
- stdout: 'ignore',
124
- });
125
- const exitCode = await proc.exited;
126
- if (exitCode !== 0) {
127
- const errorText = await new Response(proc.stderr).text();
128
- throw new Error(`Failed to open ${targetPath} with ${command}: ${errorText.trim() || `exit code ${exitCode}`}`);
129
- }
130
- };
131
-
132
118
  if (import.meta.main) {
133
119
  await runExportChatsCli();
134
120
  }
@@ -848,7 +848,7 @@ const fileExists = async (targetPath: string): Promise<boolean> => {
848
848
  };
849
849
 
850
850
  const requireValue = (value: string | undefined, flag: string): string => {
851
- if (!value || value.startsWith('--')) {
851
+ if (!value || (value.startsWith('-') && value !== '-')) {
852
852
  throw new CliUsageError(`Missing value for ${flag}`);
853
853
  }
854
854
 
@@ -0,0 +1,100 @@
1
+ import { listScopedThreads } from './codex-browser-db';
2
+ import type { CodexAnalytics, DistributionItem, ModelTokenSummary } from './codex-browser-types';
3
+ import type { ThreadRow } from './codex-exporter-types';
4
+ import { getCachedParsedCodexTranscript } from './codex-thread-cache';
5
+ import { getPortablePathBasename } from './shared';
6
+ import { getFileFingerprint, hashCacheKeyParts, withCachedJson } from './ui-cache';
7
+
8
+ export type CodexAnalyticsInput = {
9
+ dbPath: string;
10
+ project: string | null;
11
+ };
12
+
13
+ const roundToTwoDecimals = (value: number) => {
14
+ return Number(value.toFixed(2));
15
+ };
16
+
17
+ const incrementCount = (counts: Map<string, number>, key: string) => {
18
+ counts.set(key, (counts.get(key) ?? 0) + 1);
19
+ };
20
+
21
+ const toDistribution = (counts: Map<string, number>): DistributionItem[] => {
22
+ return [...counts.entries()]
23
+ .map(([label, count]) => ({ count, label }))
24
+ .sort((left, right) => {
25
+ if (left.count !== right.count) {
26
+ return right.count - left.count;
27
+ }
28
+
29
+ return left.label.localeCompare(right.label);
30
+ });
31
+ };
32
+
33
+ const buildModelsByTokens = (threads: ThreadRow[]): ModelTokenSummary[] => {
34
+ const models = new Map<string, { threadCount: number; totalTokens: number }>();
35
+
36
+ for (const thread of threads) {
37
+ const model = thread.model ?? 'unknown';
38
+ const current = models.get(model) ?? { threadCount: 0, totalTokens: 0 };
39
+ current.threadCount += 1;
40
+ current.totalTokens += thread.tokens_used;
41
+ models.set(model, current);
42
+ }
43
+
44
+ return [...models.entries()]
45
+ .map(([model, value]) => ({ model, ...value }))
46
+ .sort((left, right) => {
47
+ if (left.totalTokens !== right.totalTokens) {
48
+ return right.totalTokens - left.totalTokens;
49
+ }
50
+
51
+ return left.model.localeCompare(right.model);
52
+ });
53
+ };
54
+
55
+ const buildAnalyticsCacheKey = async (dbPath: string, threads: ThreadRow[], project: string | null) => {
56
+ const dbFingerprint = await getFileFingerprint(dbPath);
57
+ const rolloutFingerprints = await Promise.all(threads.map((thread) => getFileFingerprint(thread.rollout_path)));
58
+ return `analytics-${hashCacheKeyParts(dbFingerprint, project ?? 'all', ...rolloutFingerprints)}`;
59
+ };
60
+
61
+ const computeCodexAnalytics = async (threads: ThreadRow[]): Promise<CodexAnalytics> => {
62
+ const totalTokens = threads.reduce((sum, thread) => sum + thread.tokens_used, 0);
63
+ const projectNames = new Set(threads.map((thread) => getPortablePathBasename(thread.cwd)).filter(Boolean));
64
+ const toolUsage = new Map<string, number>();
65
+ const transcripts = await Promise.all(threads.map((thread) => getCachedParsedCodexTranscript(thread.rollout_path)));
66
+ let threadsWithWebSearch = 0;
67
+
68
+ for (const transcript of transcripts) {
69
+ if (transcript.stats.webSearchEventCount > 0) {
70
+ threadsWithWebSearch += 1;
71
+ }
72
+
73
+ for (const event of transcript.events) {
74
+ if (event.kind === 'tool_call') {
75
+ incrementCount(toolUsage, event.name);
76
+ }
77
+ }
78
+ }
79
+
80
+ return {
81
+ modelsByTokens: buildModelsByTokens(threads),
82
+ summary: {
83
+ archivedThreads: threads.filter((thread) => Boolean(thread.archived)).length,
84
+ averageTokensPerThread: threads.length === 0 ? 0 : roundToTwoDecimals(totalTokens / threads.length),
85
+ distinctToolNames: toolUsage.size,
86
+ threadsWithWebSearch,
87
+ totalProjects: projectNames.size,
88
+ totalThreads: threads.length,
89
+ totalTokens,
90
+ },
91
+ toolUsage: toDistribution(toolUsage).map((item) => ({ count: item.count, name: item.label })),
92
+ };
93
+ };
94
+
95
+ export const getCodexAnalytics = async (input: CodexAnalyticsInput): Promise<CodexAnalytics> => {
96
+ const threads = listScopedThreads(input.dbPath, input.project);
97
+ const key = await buildAnalyticsCacheKey(input.dbPath, threads, input.project);
98
+
99
+ return withCachedJson(key, async () => computeCodexAnalytics(threads));
100
+ };