spiracha 2.0.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 +42 -19
- package/apps/ui/AGENTS.md +18 -10
- package/apps/ui/README.md +36 -11
- package/apps/ui/src/components/analytics-breakdowns.tsx +106 -0
- package/apps/ui/src/components/antigravity-conversations-table.tsx +51 -11
- package/apps/ui/src/components/antigravity-workspaces-table.tsx +1 -1
- package/apps/ui/src/components/app-shell.tsx +46 -3
- package/apps/ui/src/components/breadcrumbs.tsx +21 -3
- package/apps/ui/src/components/claude-code-sessions-table.tsx +42 -6
- package/apps/ui/src/components/claude-code-workspaces-table.tsx +3 -1
- package/apps/ui/src/components/cursor-threads-table.tsx +10 -47
- package/apps/ui/src/components/data-table.tsx +23 -5
- package/apps/ui/src/components/delete-confirm-dialog.tsx +4 -3
- package/apps/ui/src/components/export-dialog.tsx +79 -74
- package/apps/ui/src/components/grok-sessions-table.tsx +146 -0
- package/apps/ui/src/components/grok-workspaces-table.tsx +53 -0
- package/apps/ui/src/components/kiro-sessions-table.tsx +40 -4
- package/apps/ui/src/components/opencode-sessions-table.tsx +40 -4
- package/apps/ui/src/components/page-header.tsx +15 -9
- package/apps/ui/src/components/projects-table.tsx +4 -1
- package/apps/ui/src/components/qoder-sessions-table.tsx +23 -2
- package/apps/ui/src/components/qoder-workspaces-table.tsx +5 -1
- package/apps/ui/src/components/route-error-panel.tsx +13 -0
- package/apps/ui/src/components/selection-actions-toolbar.tsx +80 -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 +13 -47
- package/apps/ui/src/components/transcript-view.tsx +148 -52
- package/apps/ui/src/components/ui/select.tsx +1 -1
- package/apps/ui/src/lib/antigravity-conversation-state.ts +17 -4
- package/apps/ui/src/lib/antigravity-server.ts +209 -11
- package/apps/ui/src/lib/antigravity-transcript-events.ts +57 -73
- package/apps/ui/src/lib/claude-code-queries.ts +8 -0
- package/apps/ui/src/lib/claude-code-server.ts +185 -7
- package/apps/ui/src/lib/claude-code-transcript-events.ts +8 -1
- package/apps/ui/src/lib/codex-queries.ts +16 -0
- package/apps/ui/src/lib/codex-server.ts +134 -52
- package/apps/ui/src/lib/cursor-server.ts +68 -53
- 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/formatters.ts +3 -5
- package/apps/ui/src/lib/grok-queries.ts +22 -0
- package/apps/ui/src/lib/grok-server.ts +168 -0
- package/apps/ui/src/lib/grok-transcript-events.ts +149 -0
- package/apps/ui/src/lib/kiro-server.ts +97 -7
- package/apps/ui/src/lib/mutation-error.ts +15 -0
- package/apps/ui/src/lib/opencode-server.ts +96 -7
- 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-server.ts +67 -11
- package/apps/ui/src/lib/qoder-transcript-events.ts +1 -1
- package/apps/ui/src/lib/route-search.ts +114 -0
- 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 +86 -3
- package/apps/ui/src/lib/thread-metadata.ts +22 -0
- package/apps/ui/src/lib/thread-transcript-load.ts +15 -0
- package/apps/ui/src/lib/thread-transcript-stats.ts +10 -1
- package/apps/ui/src/lib/workspace-delete-navigation.ts +20 -0
- package/apps/ui/src/routeTree.gen.ts +341 -234
- 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 +149 -29
- package/apps/ui/src/routes/antigravity.$workspaceKey.tsx +262 -42
- package/apps/ui/src/routes/antigravity.index.tsx +2 -2
- package/apps/ui/src/routes/api.v1.conversations.$source.$id.ts +4 -0
- package/apps/ui/src/routes/api.v1.conversations.delete.ts +12 -0
- package/apps/ui/src/routes/api.v1.conversations.export.ts +12 -0
- package/apps/ui/src/routes/claude-code-sessions.$sessionId.tsx +168 -28
- package/apps/ui/src/routes/claude-code.$workspaceKey.tsx +179 -45
- 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 +31 -18
- package/apps/ui/src/routes/cursor.$workspaceKey.tsx +44 -28
- package/apps/ui/src/routes/cursor.index.tsx +5 -2
- package/apps/ui/src/routes/grok-sessions.$sessionId.tsx +401 -0
- package/apps/ui/src/routes/grok.$workspaceKey.tsx +295 -0
- package/apps/ui/src/routes/grok.index.tsx +48 -0
- package/apps/ui/src/routes/index.tsx +2 -18
- package/apps/ui/src/routes/kiro-sessions.$sessionId.tsx +91 -27
- package/apps/ui/src/routes/kiro.$workspaceKey.tsx +213 -51
- package/apps/ui/src/routes/kiro.index.tsx +2 -2
- package/apps/ui/src/routes/opencode-sessions.$sessionId.tsx +108 -29
- package/apps/ui/src/routes/opencode.$workspaceKey.tsx +231 -55
- package/apps/ui/src/routes/opencode.index.tsx +6 -2
- package/apps/ui/src/routes/qoder-sessions.$sessionId.tsx +19 -15
- package/apps/ui/src/routes/qoder.$workspaceKey.tsx +89 -48
- package/apps/ui/src/routes/qoder.index.tsx +2 -2
- package/apps/ui/src/routes/threads.$threadId.tsx +620 -105
- package/apps/ui/vite.config.ts +22 -12
- package/bin/spiracha.ts +28 -1
- package/package.json +37 -27
- package/src/client.ts +183 -5
- package/src/lib/antigravity-db.ts +424 -94
- package/src/lib/antigravity-exporter-types.ts +3 -0
- package/src/lib/antigravity-keychain.ts +7 -8
- package/src/lib/antigravity-projects.ts +201 -0
- package/src/lib/antigravity-transcript-phase.ts +47 -0
- package/src/lib/claude-code-db.ts +534 -42
- package/src/lib/claude-code-exporter-types.ts +5 -0
- package/src/lib/claude-code-transcript-phase.ts +60 -1
- package/src/lib/claude-code-transcript.ts +8 -5
- package/src/lib/codex-analytics.ts +32 -5
- package/src/lib/codex-browser-db.ts +540 -137
- package/src/lib/codex-browser-export.ts +18 -27
- package/src/lib/codex-browser-types.ts +21 -1
- package/src/lib/codex-thread-cache.ts +106 -23
- package/src/lib/codex-thread-parser.ts +149 -55
- package/src/lib/codex-thread-recovery.ts +38 -11
- package/src/lib/codex-transcript-filter.ts +31 -0
- package/src/lib/codex-transcript-renderer.ts +87 -79
- package/src/lib/concurrency.ts +56 -3
- package/src/lib/conversation-api.ts +410 -27
- package/src/lib/conversation-data/adapter-helpers.ts +25 -3
- package/src/lib/conversation-data/antigravity-adapter.ts +70 -26
- package/src/lib/conversation-data/claude-code-adapter.ts +69 -12
- package/src/lib/conversation-data/codex-adapter.ts +108 -73
- package/src/lib/conversation-data/cursor-adapter.ts +77 -29
- package/src/lib/conversation-data/grok-adapter.ts +223 -0
- package/src/lib/conversation-data/index.ts +105 -11
- package/src/lib/conversation-data/kiro-adapter.ts +59 -12
- package/src/lib/conversation-data/message-selector.ts +2 -5
- package/src/lib/conversation-data/opencode-adapter.ts +67 -17
- package/src/lib/conversation-data/path-match.ts +1 -13
- package/src/lib/conversation-data/qoder-adapter.ts +67 -37
- package/src/lib/conversation-data/types.ts +43 -0
- package/src/lib/conversation-zip-export.ts +75 -0
- package/src/lib/cursor-db.ts +68 -28
- 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 +1068 -0
- package/src/lib/grok-exporter-types.ts +110 -0
- package/src/lib/grok-transcript-phase.ts +61 -0
- package/src/lib/grok-transcript.ts +167 -0
- package/src/lib/kiro-db.ts +187 -43
- package/src/lib/kiro-transcript.ts +0 -4
- package/src/lib/model-label.ts +11 -3
- package/src/lib/opencode-db.ts +599 -60
- package/src/lib/opencode-think-tags.ts +17 -3
- package/src/lib/opencode-transcript.ts +8 -5
- 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 +63 -15
- package/src/lib/sqlite-error.ts +31 -5
- package/src/lib/transcript-load-limiter.ts +87 -0
- package/src/lib/ui-cache.ts +185 -24
- package/src/lib/ui-export-archive.ts +58 -39
- 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
|
@@ -27,9 +27,23 @@ const findClosingBackticks = (text: string, start: number, runLength: number): n
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const findThinkCloseIndex = (text: string, start: number): number => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
let cursor = start;
|
|
31
|
+
while (cursor < text.length) {
|
|
32
|
+
THINK_CLOSE_PATTERN.lastIndex = cursor;
|
|
33
|
+
const match = THINK_CLOSE_PATTERN.exec(text);
|
|
34
|
+
if (!match) {
|
|
35
|
+
return -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const backtickIndex = text.indexOf('`', cursor);
|
|
39
|
+
if (backtickIndex === -1 || backtickIndex >= match.index) {
|
|
40
|
+
return match.index;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cursor = findClosingBackticks(text, backtickIndex, getBacktickRunLength(text, backtickIndex));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return -1;
|
|
33
47
|
};
|
|
34
48
|
|
|
35
49
|
const getLastVisibleChar = (parts: string[]): string => {
|
|
@@ -88,10 +88,11 @@ const renderTextPart = (
|
|
|
88
88
|
options: OpenCodeExportOptions,
|
|
89
89
|
finalAssistantTextPartIds: Set<string>,
|
|
90
90
|
): string => {
|
|
91
|
+
const rawText = part.text ?? '';
|
|
91
92
|
const { reasoningBlocks, visibleText } =
|
|
92
93
|
part.role === 'assistant'
|
|
93
|
-
? splitOpenCodeThinkTaggedText(
|
|
94
|
-
: { reasoningBlocks: [], visibleText:
|
|
94
|
+
? splitOpenCodeThinkTaggedText(rawText)
|
|
95
|
+
: { reasoningBlocks: [], visibleText: rawText };
|
|
95
96
|
const sections: string[] = [];
|
|
96
97
|
|
|
97
98
|
if (options.includeCommentary) {
|
|
@@ -119,7 +120,8 @@ const renderReasoningPart = (part: OpenCodeTranscriptPart, options: OpenCodeExpo
|
|
|
119
120
|
return '';
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
const
|
|
123
|
+
const rawText = part.text ?? '';
|
|
124
|
+
const { reasoningBlocks, visibleText } = splitOpenCodeThinkTaggedText(rawText);
|
|
123
125
|
const text = cleanExtractedText([...reasoningBlocks, visibleText].filter(Boolean).join('\n\n')).trim();
|
|
124
126
|
return text ? renderSection('Reasoning', text, options.outputFormat) : '';
|
|
125
127
|
};
|
|
@@ -143,8 +145,9 @@ const renderToolPart = (part: OpenCodeTranscriptPart, options: OpenCodeExportOpt
|
|
|
143
145
|
if (part.argumentsText?.trim()) {
|
|
144
146
|
lines.push('', 'Input:', '', renderCodeBlock(part.argumentsText.trim(), options.outputFormat));
|
|
145
147
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
const outputText = part.outputText ?? '';
|
|
149
|
+
if (outputText.trim()) {
|
|
150
|
+
lines.push('', 'Output:', '', renderCodeBlock(truncateOutput(outputText.trim()), options.outputFormat));
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
return renderSection('Tool Call', lines.join('\n'), options.outputFormat);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const getPortablePathBasename = (value: string): string => {
|
|
2
|
+
const trimmed = value.replace(/[\\/]+$/u, '');
|
|
3
|
+
if (!trimmed) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const separatorIndex = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'));
|
|
8
|
+
return separatorIndex === -1 ? trimmed : trimmed.slice(separatorIndex + 1);
|
|
9
|
+
};
|
|
@@ -9,6 +9,7 @@ const INITIALIZE_REQUEST_ID = 1;
|
|
|
9
9
|
const LOAD_REQUEST_ID = 2;
|
|
10
10
|
|
|
11
11
|
type JsonRpcMessage = {
|
|
12
|
+
error?: JsonValue;
|
|
12
13
|
id?: number;
|
|
13
14
|
jsonrpc?: '2.0';
|
|
14
15
|
method?: string;
|
|
@@ -16,6 +17,14 @@ type JsonRpcMessage = {
|
|
|
16
17
|
result?: JsonValue;
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
export const isQoderAcpResponse = (message: JsonRpcMessage, requestId: number) => {
|
|
21
|
+
return (
|
|
22
|
+
message.id === requestId &&
|
|
23
|
+
message.method === undefined &&
|
|
24
|
+
(Object.hasOwn(message, 'result') || Object.hasOwn(message, 'error'))
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
19
28
|
export type QoderAcpSessionUpdate = {
|
|
20
29
|
requestId: string | null;
|
|
21
30
|
sessionId: string;
|
|
@@ -207,7 +216,7 @@ export const loadQoderAcpSession = async (
|
|
|
207
216
|
};
|
|
208
217
|
|
|
209
218
|
const markLoadCompleted = (message: JsonRpcMessage) => {
|
|
210
|
-
if (message
|
|
219
|
+
if (!isQoderAcpResponse(message, LOAD_REQUEST_ID) || loadCompleted) {
|
|
211
220
|
return false;
|
|
212
221
|
}
|
|
213
222
|
|
|
@@ -221,7 +230,7 @@ export const loadQoderAcpSession = async (
|
|
|
221
230
|
};
|
|
222
231
|
|
|
223
232
|
const handleMessage = (message: JsonRpcMessage) => {
|
|
224
|
-
if (message
|
|
233
|
+
if (isQoderAcpResponse(message, INITIALIZE_REQUEST_ID)) {
|
|
225
234
|
sendLoadRequest();
|
|
226
235
|
return;
|
|
227
236
|
}
|
package/src/lib/qoder-db.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Database } from 'bun:sqlite';
|
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { mapWithConcurrency } from './concurrency';
|
|
5
|
+
import { getPortablePathBasename } from './portable-path';
|
|
5
6
|
import { loadQoderAcpSession, type QoderAcpSessionUpdate, resolveQoderAcpSocketPath } from './qoder-acp-client';
|
|
6
7
|
import {
|
|
7
8
|
getDefaultQoderUserDir,
|
|
@@ -14,16 +15,18 @@ import {
|
|
|
14
15
|
resolveQoderGlobalStateDb,
|
|
15
16
|
resolveQoderWorkspaceStorageDir,
|
|
16
17
|
} from './qoder-exporter-types';
|
|
18
|
+
import { coalesceQoderMessageChunks } from './qoder-transcript-phase';
|
|
17
19
|
import {
|
|
18
20
|
asObject,
|
|
19
21
|
asString,
|
|
20
22
|
cleanExtractedText,
|
|
21
23
|
cleanInlineTitle,
|
|
22
|
-
getPortablePathBasename,
|
|
23
24
|
isWorkspacePathQuery,
|
|
24
25
|
type JsonValue,
|
|
26
|
+
toFileUri,
|
|
25
27
|
workspacePathMatchesQuery,
|
|
26
28
|
} from './shared';
|
|
29
|
+
import { runWithSqliteRetry } from './sqlite-retry';
|
|
27
30
|
|
|
28
31
|
export {
|
|
29
32
|
getDefaultQoderUserDir,
|
|
@@ -202,7 +205,7 @@ const getWorkspaceLabel = (worktree: string): string => {
|
|
|
202
205
|
};
|
|
203
206
|
|
|
204
207
|
const getWorkspaceUri = (worktree: string): string => {
|
|
205
|
-
return worktree.startsWith(path.sep) ?
|
|
208
|
+
return worktree.startsWith(path.sep) ? toFileUri(worktree) : worktree;
|
|
206
209
|
};
|
|
207
210
|
|
|
208
211
|
const parseJsonValue = (value: string): JsonValue | null => {
|
|
@@ -213,23 +216,50 @@ const parseJsonValue = (value: string): JsonValue | null => {
|
|
|
213
216
|
}
|
|
214
217
|
};
|
|
215
218
|
|
|
219
|
+
export const isUnavailableQoderGlobalStateError = (error: unknown): boolean => {
|
|
220
|
+
if (!(error instanceof Error)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const code = 'code' in error && typeof error.code === 'string' ? error.code : '';
|
|
225
|
+
return (
|
|
226
|
+
code === 'SQLITE_CANTOPEN' ||
|
|
227
|
+
code === 'SQLITE_IOERR_READ' ||
|
|
228
|
+
/^(?:SQLite operation failed after \d+ attempts?:\s*)?(?:SQLITE_CANTOPEN:.*|unable to open database file)$/iu.test(
|
|
229
|
+
error.message,
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
|
|
216
234
|
const readGlobalRows = async (globalStateDb = resolveQoderGlobalStateDb()): Promise<ItemTableRow[]> => {
|
|
217
|
-
|
|
235
|
+
const isDatabaseFile = await stat(globalStateDb)
|
|
236
|
+
.then((metadata) => metadata.isFile())
|
|
237
|
+
.catch(() => false);
|
|
238
|
+
if (!isDatabaseFile) {
|
|
218
239
|
return [];
|
|
219
240
|
}
|
|
220
241
|
|
|
221
|
-
let db: Database | null = null;
|
|
222
242
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
return runWithSqliteRetry({
|
|
244
|
+
action: () => {
|
|
245
|
+
const db = new Database(globalStateDb, { readonly: true, strict: true });
|
|
246
|
+
try {
|
|
247
|
+
db.exec('PRAGMA busy_timeout = 1000');
|
|
248
|
+
return db
|
|
249
|
+
.query(
|
|
250
|
+
"select key, value from ItemTable where key like 'lingma.chat.localHistory.%.quest' or key = 'aicoding.questTaskListSnapshot' or key in ('aicoding.modelConfigs.cache.assistant', 'aicoding.modelConfigs.cache.quest')",
|
|
251
|
+
)
|
|
252
|
+
.all() as ItemTableRow[];
|
|
253
|
+
} finally {
|
|
254
|
+
db.close();
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if (isUnavailableQoderGlobalStateError(error)) {
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
throw error;
|
|
233
263
|
}
|
|
234
264
|
};
|
|
235
265
|
|
|
@@ -800,6 +830,7 @@ const readRecordSummary = async (
|
|
|
800
830
|
modelFallback: string | null,
|
|
801
831
|
): Promise<QoderSessionSummary> => {
|
|
802
832
|
const state = await readStateData(workspaceStorageDir, workspaceStorageIds, record);
|
|
833
|
+
// List stats describe persisted Qoder state only; detail reads may additionally hydrate CLI or live ACP messages.
|
|
803
834
|
const entries = buildLocalTranscriptEntries(record, state);
|
|
804
835
|
const stats = createStatsFromEntries(entries, state.snapshotFileCount);
|
|
805
836
|
return toSessionSummary(record, state, stats, modelFallback);
|
|
@@ -1128,6 +1159,7 @@ const cliToolCallPartToTranscriptPart = (part: Record<string, JsonValue>): Qoder
|
|
|
1128
1159
|
raw: {
|
|
1129
1160
|
...part,
|
|
1130
1161
|
command: text,
|
|
1162
|
+
toolCallId: asString(data.id ?? data.tool_use_id ?? part.id ?? part.tool_use_id ?? null),
|
|
1131
1163
|
toolName: getCliToolName(part, data),
|
|
1132
1164
|
},
|
|
1133
1165
|
role: 'tool',
|
|
@@ -1494,9 +1526,11 @@ const readAcpTranscriptEntries = async (
|
|
|
1494
1526
|
}
|
|
1495
1527
|
|
|
1496
1528
|
return {
|
|
1497
|
-
entries:
|
|
1498
|
-
.
|
|
1499
|
-
|
|
1529
|
+
entries: coalesceQoderMessageChunks(
|
|
1530
|
+
loaded.events
|
|
1531
|
+
.map((event, index) => acpUpdateToEntry(event, index))
|
|
1532
|
+
.filter((entry): entry is QoderTranscriptEntry => Boolean(entry)),
|
|
1533
|
+
),
|
|
1500
1534
|
model: getAcpModel(loaded.events),
|
|
1501
1535
|
socketPath: loaded.socketPath,
|
|
1502
1536
|
};
|
|
@@ -2,6 +2,59 @@ import type { QoderTranscriptEntry } from './qoder-exporter-types';
|
|
|
2
2
|
|
|
3
3
|
export type QoderMessagePhase = 'commentary' | 'final_answer' | null;
|
|
4
4
|
|
|
5
|
+
const getMessageChunkType = (entry: QoderTranscriptEntry): string | null => {
|
|
6
|
+
if (entry.entryType !== 'message' || (entry.role !== 'assistant' && entry.role !== 'user')) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sessionUpdate = entry.parts[0]?.raw.sessionUpdate;
|
|
11
|
+
return typeof sessionUpdate === 'string' && sessionUpdate.endsWith('_message_chunk') ? sessionUpdate : null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const mergeMessageChunks = (first: QoderTranscriptEntry, second: QoderTranscriptEntry): QoderTranscriptEntry => {
|
|
15
|
+
const firstPart = first.parts[0]!;
|
|
16
|
+
const secondPart = second.parts[0]!;
|
|
17
|
+
return {
|
|
18
|
+
...first,
|
|
19
|
+
parts: [
|
|
20
|
+
{
|
|
21
|
+
...firstPart,
|
|
22
|
+
raw: {
|
|
23
|
+
...firstPart.raw,
|
|
24
|
+
coalescedChunkCount:
|
|
25
|
+
(typeof firstPart.raw.coalescedChunkCount === 'number'
|
|
26
|
+
? firstPart.raw.coalescedChunkCount
|
|
27
|
+
: 1) + 1,
|
|
28
|
+
},
|
|
29
|
+
text: `${firstPart.text ?? ''}${secondPart.text ?? ''}`,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
raw: second.raw,
|
|
33
|
+
timestamp: second.timestamp ?? first.timestamp,
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const coalesceQoderMessageChunks = (entries: QoderTranscriptEntry[]): QoderTranscriptEntry[] => {
|
|
38
|
+
const coalesced: QoderTranscriptEntry[] = [];
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const previous = coalesced.at(-1);
|
|
41
|
+
const chunkType = getMessageChunkType(entry);
|
|
42
|
+
if (
|
|
43
|
+
previous &&
|
|
44
|
+
chunkType &&
|
|
45
|
+
chunkType === getMessageChunkType(previous) &&
|
|
46
|
+
previous.requestId === entry.requestId &&
|
|
47
|
+
previous.parts.length === 1 &&
|
|
48
|
+
entry.parts.length === 1
|
|
49
|
+
) {
|
|
50
|
+
coalesced[coalesced.length - 1] = mergeMessageChunks(previous, entry);
|
|
51
|
+
} else {
|
|
52
|
+
coalesced.push(entry);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return coalesced;
|
|
56
|
+
};
|
|
57
|
+
|
|
5
58
|
export const getFinalQoderAssistantMessageEntryIds = (entries: QoderTranscriptEntry[]): Set<string> => {
|
|
6
59
|
const finalEntryIds = new Set<string>();
|
|
7
60
|
let latestAssistantMessageEntryId: string | null = null;
|
|
@@ -37,5 +90,16 @@ export const getQoderMessagePhase = (entry: QoderTranscriptEntry, finalAssistant
|
|
|
37
90
|
return null;
|
|
38
91
|
}
|
|
39
92
|
|
|
93
|
+
const isExplicitReasoning = entry.parts.some((part) => {
|
|
94
|
+
return (
|
|
95
|
+
part.raw.sourceType === 'reasoning' ||
|
|
96
|
+
part.raw.sourceType === 'thinking' ||
|
|
97
|
+
part.raw.sessionUpdate === 'agent_thought_chunk'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
if (isExplicitReasoning) {
|
|
101
|
+
return 'commentary';
|
|
102
|
+
}
|
|
103
|
+
|
|
40
104
|
return finalAssistantMessageEntryIds.has(entry.entryId) ? 'final_answer' : 'commentary';
|
|
41
105
|
};
|
|
@@ -9,6 +9,7 @@ import { getFinalQoderAssistantMessageEntryIds, getQoderMessagePhase } from './q
|
|
|
9
9
|
import {
|
|
10
10
|
cleanExtractedText,
|
|
11
11
|
cleanInlineTitle,
|
|
12
|
+
formatInlineLiteral,
|
|
12
13
|
type MetadataEntry,
|
|
13
14
|
renderDocumentTitle,
|
|
14
15
|
renderMetadataBlock,
|
|
@@ -61,6 +62,33 @@ const renderTextPart = (part: QoderTranscriptPart, title: string, options: Qoder
|
|
|
61
62
|
return text ? renderSection(title, text, options.outputFormat) : '';
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
const getPartString = (part: QoderTranscriptPart, key: string): string | null => {
|
|
66
|
+
const value = part.raw[key];
|
|
67
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const renderToolPart = (part: QoderTranscriptPart, title: string, options: QoderExportOptions): string => {
|
|
71
|
+
const text = cleanExtractedText(part.text ?? '').trim();
|
|
72
|
+
if (!text) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const lines: string[] = [];
|
|
77
|
+
const toolName = getPartString(part, 'toolName');
|
|
78
|
+
const toolCallId = getPartString(part, 'toolCallId');
|
|
79
|
+
if (toolName) {
|
|
80
|
+
lines.push(`Tool: ${formatInlineLiteral(toolName, options.outputFormat)}`);
|
|
81
|
+
}
|
|
82
|
+
if (toolCallId) {
|
|
83
|
+
lines.push(`Call ID: ${toolCallId}`);
|
|
84
|
+
}
|
|
85
|
+
if (lines.length > 0) {
|
|
86
|
+
lines.push('');
|
|
87
|
+
}
|
|
88
|
+
lines.push(text);
|
|
89
|
+
return renderSection(title, lines.join('\n'), options.outputFormat);
|
|
90
|
+
};
|
|
91
|
+
|
|
64
92
|
const renderPart = (
|
|
65
93
|
entry: QoderTranscriptEntry,
|
|
66
94
|
part: QoderTranscriptPart,
|
|
@@ -68,11 +96,11 @@ const renderPart = (
|
|
|
68
96
|
finalAssistantMessageEntryIds: Set<string>,
|
|
69
97
|
): string => {
|
|
70
98
|
if (entry.entryType === 'tool_call') {
|
|
71
|
-
return options.includeTools && part.type === 'text' ?
|
|
99
|
+
return options.includeTools && part.type === 'text' ? renderToolPart(part, 'Tool call', options) : '';
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
if (entry.entryType === 'tool_output') {
|
|
75
|
-
return options.includeTools && part.type === 'text' ?
|
|
103
|
+
return options.includeTools && part.type === 'text' ? renderToolPart(part, 'Tool output', options) : '';
|
|
76
104
|
}
|
|
77
105
|
|
|
78
106
|
if (getQoderMessagePhase(entry, finalAssistantMessageEntryIds) === 'commentary' && !options.includeCommentary) {
|
package/src/lib/shared.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
-
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { finished } from 'node:stream/promises';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
7
8
|
import { formatModelLabel as formatSharedModelLabel } from './model-label';
|
|
8
9
|
|
|
9
10
|
export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
@@ -17,6 +18,19 @@ export type MetadataEntry = {
|
|
|
17
18
|
|
|
18
19
|
export class CliUsageError extends Error {}
|
|
19
20
|
|
|
21
|
+
export const readDirectoryEntriesIfExists = async (directoryPath: string) => {
|
|
22
|
+
try {
|
|
23
|
+
return await readdir(directoryPath, { withFileTypes: true });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if ((error as { code?: unknown }).code === 'ENOENT') {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const toFileUri = (filePath: string): string => pathToFileURL(filePath).href;
|
|
33
|
+
|
|
20
34
|
export const expandHome = (value: string): string => {
|
|
21
35
|
if (!value) {
|
|
22
36
|
return value;
|
|
@@ -26,20 +40,26 @@ export const expandHome = (value: string): string => {
|
|
|
26
40
|
return os.homedir();
|
|
27
41
|
}
|
|
28
42
|
|
|
29
|
-
if (value.startsWith('~/')) {
|
|
30
|
-
return path.join(
|
|
43
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) {
|
|
44
|
+
return path.join(
|
|
45
|
+
os.homedir(),
|
|
46
|
+
...value
|
|
47
|
+
.slice(2)
|
|
48
|
+
.split(/[\\/]+/u)
|
|
49
|
+
.filter(Boolean),
|
|
50
|
+
);
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
return value;
|
|
34
54
|
};
|
|
35
55
|
|
|
36
|
-
export const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
56
|
+
export const pathExists = async (target: string): Promise<boolean> => {
|
|
57
|
+
try {
|
|
58
|
+
await stat(target);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
40
62
|
}
|
|
41
|
-
|
|
42
|
-
return path.win32.basename(path.posix.basename(trimmed));
|
|
43
63
|
};
|
|
44
64
|
|
|
45
65
|
export const isWorkspacePathQuery = (value: string): boolean => {
|
|
@@ -86,6 +106,18 @@ export const cleanExtractedText = (text: string): string => {
|
|
|
86
106
|
return text.replace(/^\s*<\/?image>\s*$/gm, '').replace(/\n{3,}/g, '\n\n');
|
|
87
107
|
};
|
|
88
108
|
|
|
109
|
+
const CODEX_APP_DIRECTIVE_PATTERN =
|
|
110
|
+
/^::(?:code-comment|created-thread|git-commit|git-create-branch|git-create-pr|git-push|git-stage)\{.*\}\s*$/u;
|
|
111
|
+
|
|
112
|
+
export const stripCodexAppDirectiveLines = (text: string): string => {
|
|
113
|
+
return text
|
|
114
|
+
.split('\n')
|
|
115
|
+
.filter((line) => !CODEX_APP_DIRECTIVE_PATTERN.test(line.trim()))
|
|
116
|
+
.join('\n')
|
|
117
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
118
|
+
.trim();
|
|
119
|
+
};
|
|
120
|
+
|
|
89
121
|
export const formatModelLabel = formatSharedModelLabel;
|
|
90
122
|
|
|
91
123
|
export const asObject = (value: JsonValue): Record<string, JsonValue> | null => {
|
|
@@ -116,6 +148,7 @@ export const readJsonlObjects = (filePath: string): AsyncIterableIterator<Record
|
|
|
116
148
|
});
|
|
117
149
|
const lineIterator = lines[Symbol.asyncIterator]();
|
|
118
150
|
let closed = false;
|
|
151
|
+
let lineNumber = 0;
|
|
119
152
|
|
|
120
153
|
const close = () => {
|
|
121
154
|
if (closed) {
|
|
@@ -135,17 +168,27 @@ export const readJsonlObjects = (filePath: string): AsyncIterableIterator<Record
|
|
|
135
168
|
return { done: true, value: undefined as never };
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
lineNumber += 1;
|
|
138
172
|
const trimmed = nextLine.value.trim();
|
|
139
173
|
if (!trimmed) {
|
|
140
174
|
continue;
|
|
141
175
|
}
|
|
142
176
|
|
|
143
177
|
try {
|
|
178
|
+
const parsed = JSON.parse(trimmed) as JsonValue;
|
|
179
|
+
const object = asObject(parsed);
|
|
180
|
+
if (!object) {
|
|
181
|
+
console.warn('[spiracha:jsonl] invalid_json_line', { filePath, lineNumber });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
144
185
|
return {
|
|
145
186
|
done: false,
|
|
146
|
-
value:
|
|
187
|
+
value: object,
|
|
147
188
|
};
|
|
148
|
-
} catch {
|
|
189
|
+
} catch {
|
|
190
|
+
console.warn('[spiracha:jsonl] invalid_json_line', { filePath, lineNumber });
|
|
191
|
+
}
|
|
149
192
|
}
|
|
150
193
|
};
|
|
151
194
|
|
|
@@ -213,7 +256,8 @@ export const renderSection = (title: string, body: string, format: ExportFormat)
|
|
|
213
256
|
|
|
214
257
|
export const renderCodeBlock = (text: string, format: ExportFormat): string => {
|
|
215
258
|
if (format === 'md') {
|
|
216
|
-
|
|
259
|
+
const fence = getBacktickFence(text, 3);
|
|
260
|
+
return `${fence}text\n${text}\n${fence}`;
|
|
217
261
|
}
|
|
218
262
|
|
|
219
263
|
return text;
|
|
@@ -223,10 +267,14 @@ export const formatInlineLiteral = (value: string, format: ExportFormat): string
|
|
|
223
267
|
return format === 'md' ? inlineCode(value) : value;
|
|
224
268
|
};
|
|
225
269
|
|
|
226
|
-
|
|
227
|
-
const backtickRuns = value.match(/`+/
|
|
270
|
+
const getBacktickFence = (value: string, minimumLength: number): string => {
|
|
271
|
+
const backtickRuns = value.match(/`+/gu) ?? [];
|
|
228
272
|
const maxRunLength = backtickRuns.reduce((max, run) => Math.max(max, run.length), 0);
|
|
229
|
-
|
|
273
|
+
return '`'.repeat(Math.max(minimumLength, maxRunLength + 1));
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const inlineCode = (value: string): string => {
|
|
277
|
+
const fence = getBacktickFence(value, 1);
|
|
230
278
|
const padded = value.startsWith('`') || value.endsWith('`') ? ` ${value} ` : value;
|
|
231
279
|
return `${fence}${padded}${fence}`;
|
|
232
280
|
};
|
package/src/lib/sqlite-error.ts
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
|
+
const SQLITE_RETRYABLE_CODES = new Set(['SQLITE_BUSY', 'SQLITE_BUSY_SNAPSHOT', 'SQLITE_CANTOPEN', 'SQLITE_LOCKED']);
|
|
2
|
+
const SQLITE_OPERATION_PREFIX = String.raw`(?:SQLite operation failed after \d+ attempts?:\s*)?`;
|
|
1
3
|
const SQLITE_RETRYABLE_PATTERNS = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/
|
|
5
|
-
/SQLITE_CANTOPEN/iu,
|
|
4
|
+
new RegExp(`^${SQLITE_OPERATION_PREFIX}unable to open database(?: file)?(?:$|:)`, 'iu'),
|
|
5
|
+
new RegExp(`^${SQLITE_OPERATION_PREFIX}database(?: table)? is locked(?:$|:)`, 'iu'),
|
|
6
|
+
/^SQLITE_(?:BUSY|CANTOPEN|LOCKED)(?:_[A-Z_]+)?(?:$|:)/u,
|
|
6
7
|
];
|
|
8
|
+
const SQLITE_DATABASE_PATTERNS = [
|
|
9
|
+
...SQLITE_RETRYABLE_PATTERNS,
|
|
10
|
+
/^SQLITE_[A-Z_]+(?:$|:)/u,
|
|
11
|
+
new RegExp(
|
|
12
|
+
`^${SQLITE_OPERATION_PREFIX}(?:attempt to write a readonly database|database disk image is malformed|disk I/O error)(?:$|:)`,
|
|
13
|
+
'iu',
|
|
14
|
+
),
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const getSqliteCode = (error: Error): string | null => {
|
|
18
|
+
const code = 'code' in error ? error.code : null;
|
|
19
|
+
return typeof code === 'string' && code.startsWith('SQLITE_') ? code.toUpperCase() : null;
|
|
20
|
+
};
|
|
7
21
|
|
|
8
22
|
export const isRetryableSqliteError = (error: unknown) => {
|
|
9
23
|
if (!(error instanceof Error)) {
|
|
10
24
|
return false;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
|
-
|
|
27
|
+
const code = getSqliteCode(error);
|
|
28
|
+
return (
|
|
29
|
+
Boolean(code && SQLITE_RETRYABLE_CODES.has(code)) ||
|
|
30
|
+
SQLITE_RETRYABLE_PATTERNS.some((pattern) => pattern.test(error.message))
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const isSqliteDatabaseError = (error: unknown): boolean => {
|
|
35
|
+
if (!(error instanceof Error)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return getSqliteCode(error) !== null || SQLITE_DATABASE_PATTERNS.some((pattern) => pattern.test(error.message));
|
|
14
40
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createConcurrencyLimiter } from './concurrency';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TRANSCRIPT_LOAD_CONCURRENCY = 3;
|
|
4
|
+
const MAX_TRANSCRIPT_LOAD_CONCURRENCY = 16;
|
|
5
|
+
type TranscriptLoadLogContext = {
|
|
6
|
+
id?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
source?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let nextTranscriptLoadId = 1;
|
|
12
|
+
let activeTranscriptLoads = 0;
|
|
13
|
+
let queuedTranscriptLoads = 0;
|
|
14
|
+
|
|
15
|
+
export const resolveTranscriptLoadConcurrency = (value = process.env.SPIRACHA_TRANSCRIPT_LOAD_CONCURRENCY): number => {
|
|
16
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
17
|
+
return Number.isFinite(parsed) && parsed > 0
|
|
18
|
+
? Math.min(parsed, MAX_TRANSCRIPT_LOAD_CONCURRENCY)
|
|
19
|
+
: DEFAULT_TRANSCRIPT_LOAD_CONCURRENCY;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let transcriptLoadLimiter: ReturnType<typeof createConcurrencyLimiter> | null = null;
|
|
23
|
+
|
|
24
|
+
const getTranscriptLoadLimiter = () => {
|
|
25
|
+
transcriptLoadLimiter ??= createConcurrencyLimiter(resolveTranscriptLoadConcurrency());
|
|
26
|
+
return transcriptLoadLimiter;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const shouldLogTranscriptLoads = () => process.env.SPIRACHA_TRANSCRIPT_LOAD_LOGS === '1';
|
|
30
|
+
|
|
31
|
+
const logTranscriptLoad = (event: string, details: Record<string, unknown>) => {
|
|
32
|
+
if (shouldLogTranscriptLoads()) {
|
|
33
|
+
console.info(`[spiracha:transcript-load] ${event}`, details);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const runWithTranscriptLoadLimit = async <T>(
|
|
38
|
+
loader: () => Promise<T>,
|
|
39
|
+
context: TranscriptLoadLogContext = {},
|
|
40
|
+
): Promise<T> => {
|
|
41
|
+
const loadId = nextTranscriptLoadId;
|
|
42
|
+
nextTranscriptLoadId += 1;
|
|
43
|
+
queuedTranscriptLoads += 1;
|
|
44
|
+
const queuedAt = Date.now();
|
|
45
|
+
|
|
46
|
+
return getTranscriptLoadLimiter()(async () => {
|
|
47
|
+
queuedTranscriptLoads -= 1;
|
|
48
|
+
activeTranscriptLoads += 1;
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
logTranscriptLoad('start', {
|
|
51
|
+
active: activeTranscriptLoads,
|
|
52
|
+
id: context.id,
|
|
53
|
+
loadId,
|
|
54
|
+
path: context.path,
|
|
55
|
+
queued: queuedTranscriptLoads,
|
|
56
|
+
source: context.source,
|
|
57
|
+
waitMs: startedAt - queuedAt,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
return await loader();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logTranscriptLoad('error', {
|
|
64
|
+
active: activeTranscriptLoads,
|
|
65
|
+
durationMs: Date.now() - startedAt,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
id: context.id,
|
|
68
|
+
loadId,
|
|
69
|
+
path: context.path,
|
|
70
|
+
queued: queuedTranscriptLoads,
|
|
71
|
+
source: context.source,
|
|
72
|
+
});
|
|
73
|
+
throw error;
|
|
74
|
+
} finally {
|
|
75
|
+
activeTranscriptLoads -= 1;
|
|
76
|
+
logTranscriptLoad('finish', {
|
|
77
|
+
active: activeTranscriptLoads,
|
|
78
|
+
durationMs: Date.now() - startedAt,
|
|
79
|
+
id: context.id,
|
|
80
|
+
loadId,
|
|
81
|
+
path: context.path,
|
|
82
|
+
queued: queuedTranscriptLoads,
|
|
83
|
+
source: context.source,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
};
|