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/src/ui-cli.ts ADDED
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { access, constants } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { openUrlNatively } from './lib/native-open';
6
+ import { CliUsageError } from './lib/shared';
7
+ import {
8
+ buildUiExportContentDisposition,
9
+ ensureUiExportDir,
10
+ resolveUiExportFilePathFromRequestPath,
11
+ } from './lib/ui-export-files';
12
+
13
+ type UiCliOptions = {
14
+ dbPath: string | null;
15
+ host: string;
16
+ openBrowser: boolean;
17
+ port: number;
18
+ };
19
+
20
+ const DEFAULT_UI_HOST = '127.0.0.1';
21
+ const DEFAULT_UI_PORT = 3000;
22
+ const MAX_PORT_ATTEMPTS = 10;
23
+
24
+ const resolveUiDistPaths = () => {
25
+ const distRoot = path.resolve(import.meta.dir, '..', 'apps', 'ui', 'dist');
26
+ return {
27
+ clientDir: path.join(distRoot, 'client'),
28
+ serverEntryPath: path.join(distRoot, 'server', 'server.js'),
29
+ };
30
+ };
31
+
32
+ const ensureUiBuildExists = async () => {
33
+ const { clientDir, serverEntryPath } = resolveUiDistPaths();
34
+ const serverEntry = Bun.file(serverEntryPath);
35
+ const clientAssetExists = await Bun.file(path.join(clientDir, 'favicon.ico')).exists();
36
+ const serverEntryExists = await serverEntry.exists();
37
+
38
+ if (!clientAssetExists || !serverEntryExists) {
39
+ throw new Error('UI build artifacts are missing. Run `bun run build` before launching `spiracha ui`.');
40
+ }
41
+
42
+ await ensureUiExportDir();
43
+ };
44
+
45
+ const parsePort = (value: string) => {
46
+ const parsed = Number.parseInt(value, 10);
47
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
48
+ throw new CliUsageError(`Invalid port: ${value}`);
49
+ }
50
+
51
+ return parsed;
52
+ };
53
+
54
+ const requireUiArgValue = (value: string | undefined, flag: string) => {
55
+ if (!value) {
56
+ throw new CliUsageError(`Missing value for ${flag}`);
57
+ }
58
+
59
+ return value;
60
+ };
61
+
62
+ const applyUiCliArg = (
63
+ argv: string[],
64
+ index: number,
65
+ state: UiCliOptions,
66
+ ): {
67
+ index: number;
68
+ state: UiCliOptions;
69
+ } => {
70
+ const arg = argv[index];
71
+
72
+ if (arg === '--no-open') {
73
+ return {
74
+ index,
75
+ state: {
76
+ ...state,
77
+ openBrowser: false,
78
+ },
79
+ };
80
+ }
81
+
82
+ if (arg === '--port') {
83
+ return {
84
+ index: index + 1,
85
+ state: {
86
+ ...state,
87
+ port: parsePort(requireUiArgValue(argv[index + 1], '--port')),
88
+ },
89
+ };
90
+ }
91
+
92
+ if (arg === '--host') {
93
+ return {
94
+ index: index + 1,
95
+ state: {
96
+ ...state,
97
+ host: requireUiArgValue(argv[index + 1], '--host'),
98
+ },
99
+ };
100
+ }
101
+
102
+ if (arg === '--db') {
103
+ return {
104
+ index: index + 1,
105
+ state: {
106
+ ...state,
107
+ dbPath: requireUiArgValue(argv[index + 1], '--db'),
108
+ },
109
+ };
110
+ }
111
+
112
+ throw new CliUsageError(`Unknown UI argument: ${arg}`);
113
+ };
114
+
115
+ export const parseUiCliArgs = (argv: string[]): UiCliOptions => {
116
+ let state: UiCliOptions = {
117
+ dbPath: null,
118
+ host: DEFAULT_UI_HOST,
119
+ openBrowser: true,
120
+ port: DEFAULT_UI_PORT,
121
+ };
122
+
123
+ for (let index = 0; index < argv.length; index += 1) {
124
+ const next = applyUiCliArg(argv, index, state);
125
+ state = next.state;
126
+ index = next.index;
127
+ }
128
+
129
+ return state;
130
+ };
131
+
132
+ export const getUiHelpText = (): string => {
133
+ return [
134
+ 'Launch the Spiracha browser UI.',
135
+ '',
136
+ 'Usage:',
137
+ ' spiracha ui [--port 3000] [--host 127.0.0.1] [--db FILE] [--no-open]',
138
+ '',
139
+ 'Options:',
140
+ ` --port HTTP port to bind (default: ${DEFAULT_UI_PORT})`,
141
+ ` --host Hostname to bind (default: ${DEFAULT_UI_HOST})`,
142
+ ' --db Override the Codex SQLite database path for the UI',
143
+ ' --no-open Do not open the browser automatically',
144
+ ' --help,-h Show this help text',
145
+ '',
146
+ 'Stop the UI with Ctrl+C.',
147
+ ].join('\n');
148
+ };
149
+
150
+ const toPublicFilePath = (clientDir: string, pathname: string) => {
151
+ const normalizedPath = pathname === '/' ? '' : pathname.replace(/^\/+/u, '');
152
+ const resolved = path.resolve(clientDir, normalizedPath);
153
+ const relative = path.relative(clientDir, resolved);
154
+
155
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
156
+ return null;
157
+ }
158
+
159
+ return resolved;
160
+ };
161
+
162
+ export const getUiStaticResponse = async (clientDir: string, pathname: string): Promise<Response | null> => {
163
+ if (pathname === '/') {
164
+ return null;
165
+ }
166
+
167
+ const exportFilePath = resolveUiExportFilePathFromRequestPath(pathname);
168
+ if (exportFilePath) {
169
+ try {
170
+ await access(exportFilePath, constants.R_OK);
171
+ } catch {
172
+ return new Response('Not Found', { status: 404 });
173
+ }
174
+
175
+ return new Response(Bun.file(exportFilePath), {
176
+ headers: {
177
+ 'cache-control': 'no-store',
178
+ 'content-disposition': buildUiExportContentDisposition(exportFilePath),
179
+ },
180
+ });
181
+ }
182
+
183
+ const targetPath = toPublicFilePath(clientDir, pathname);
184
+ if (!targetPath) {
185
+ return new Response('Forbidden', { status: 403 });
186
+ }
187
+
188
+ const file = Bun.file(targetPath);
189
+ if (!(await file.exists())) {
190
+ return null;
191
+ }
192
+
193
+ return new Response(file);
194
+ };
195
+
196
+ const openBrowserIfRequested = async (url: string, openBrowser: boolean) => {
197
+ if (!openBrowser) {
198
+ return;
199
+ }
200
+
201
+ try {
202
+ await openUrlNatively(url);
203
+ } catch (error) {
204
+ const message = error instanceof Error ? error.message : String(error);
205
+ console.warn(`Could not open the browser automatically: ${message}`);
206
+ }
207
+ };
208
+
209
+ const startUiServer = async (options: UiCliOptions) => {
210
+ await ensureUiBuildExists();
211
+ const { clientDir, serverEntryPath } = resolveUiDistPaths();
212
+ const serverModule = (await import(serverEntryPath)) as {
213
+ default: {
214
+ fetch: (request: Request) => Promise<Response> | Response;
215
+ };
216
+ };
217
+
218
+ if (options.dbPath) {
219
+ process.env.SPIRACHA_CODEX_DB = options.dbPath;
220
+ }
221
+
222
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt += 1) {
223
+ const port = options.port + attempt;
224
+
225
+ try {
226
+ const server = Bun.serve({
227
+ fetch: async (request) => {
228
+ const url = new URL(request.url);
229
+ const staticResponse = await getUiStaticResponse(clientDir, url.pathname);
230
+ if (staticResponse) {
231
+ return staticResponse;
232
+ }
233
+
234
+ return serverModule.default.fetch(request);
235
+ },
236
+ hostname: options.host,
237
+ idleTimeout: 30,
238
+ port,
239
+ });
240
+
241
+ return {
242
+ port,
243
+ server,
244
+ };
245
+ } catch (error) {
246
+ if (
247
+ attempt < MAX_PORT_ATTEMPTS - 1 &&
248
+ error instanceof Error &&
249
+ /address already in use|EADDRINUSE/i.test(error.message)
250
+ ) {
251
+ continue;
252
+ }
253
+
254
+ throw error;
255
+ }
256
+ }
257
+
258
+ throw new Error(`Unable to bind the UI server after ${MAX_PORT_ATTEMPTS} port attempts.`);
259
+ };
260
+
261
+ export const runUiCli = async (argv = process.argv.slice(2)) => {
262
+ try {
263
+ if (argv.includes('--help') || argv.includes('-h')) {
264
+ console.log(getUiHelpText());
265
+ return;
266
+ }
267
+ const options = parseUiCliArgs(argv);
268
+ const { port, server } = await startUiServer(options);
269
+ const url = `http://${options.host}:${port}`;
270
+
271
+ console.log(`Spiracha UI running at ${url}`);
272
+ console.log('Press Ctrl+C to stop.');
273
+
274
+ await openBrowserIfRequested(url, options.openBrowser);
275
+
276
+ let shuttingDown = false;
277
+ const shutdown = () => {
278
+ if (shuttingDown) {
279
+ return;
280
+ }
281
+ shuttingDown = true;
282
+ server.stop(true);
283
+ console.log('Spiracha UI stopped.');
284
+ process.exit(0);
285
+ };
286
+
287
+ process.once('SIGINT', shutdown);
288
+ process.once('SIGTERM', shutdown);
289
+
290
+ await new Promise<void>(() => {});
291
+ } catch (error) {
292
+ if (error instanceof CliUsageError) {
293
+ console.error(error.message);
294
+ console.error('');
295
+ console.error(getUiHelpText());
296
+ process.exit(1);
297
+ }
298
+
299
+ if (error instanceof Error) {
300
+ console.error(error.message);
301
+ process.exit(1);
302
+ }
303
+
304
+ throw error;
305
+ }
306
+ };
307
+
308
+ if (import.meta.main) {
309
+ await runUiCli();
310
+ }