@tracemarketplace/cli 0.0.13 → 0.0.17
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/dist/api-client.d.ts +9 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +80 -15
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.d.ts +2 -0
- package/dist/api-client.test.d.ts.map +1 -0
- package/dist/api-client.test.js +34 -0
- package/dist/api-client.test.js.map +1 -0
- package/dist/cli.js +48 -18
- package/dist/cli.js.map +1 -1
- package/dist/commands/auto-submit.d.ts +2 -1
- package/dist/commands/auto-submit.d.ts.map +1 -1
- package/dist/commands/auto-submit.js +43 -56
- package/dist/commands/auto-submit.js.map +1 -1
- package/dist/commands/daemon.d.ts +8 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +184 -63
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/history.d.ts +3 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +8 -4
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/login.d.ts +5 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +25 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/register.d.ts +1 -0
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +4 -39
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/remove-daemon.d.ts +6 -0
- package/dist/commands/remove-daemon.d.ts.map +1 -0
- package/dist/commands/remove-daemon.js +66 -0
- package/dist/commands/remove-daemon.js.map +1 -0
- package/dist/commands/remove-hook.d.ts +6 -0
- package/dist/commands/remove-hook.d.ts.map +1 -0
- package/dist/commands/remove-hook.js +174 -0
- package/dist/commands/remove-hook.js.map +1 -0
- package/dist/commands/setup-hook.d.ts +2 -0
- package/dist/commands/setup-hook.d.ts.map +1 -1
- package/dist/commands/setup-hook.js +85 -41
- package/dist/commands/setup-hook.js.map +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +8 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +138 -83
- package/dist/commands/submit.js.map +1 -1
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +8 -4
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config.d.ts +38 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +175 -17
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/flush.d.ts +49 -0
- package/dist/flush.d.ts.map +1 -0
- package/dist/flush.js +405 -0
- package/dist/flush.js.map +1 -0
- package/dist/flush.test.d.ts +2 -0
- package/dist/flush.test.d.ts.map +1 -0
- package/dist/flush.test.js +330 -0
- package/dist/flush.test.js.map +1 -0
- package/dist/submitter.d.ts.map +1 -1
- package/dist/submitter.js +5 -2
- package/dist/submitter.js.map +1 -1
- package/package.json +8 -7
- package/src/api-client.test.ts +47 -0
- package/src/api-client.ts +100 -16
- package/src/cli.ts +55 -19
- package/src/commands/auto-submit.ts +80 -40
- package/src/commands/daemon.ts +243 -60
- package/src/commands/history.ts +9 -4
- package/src/commands/login.ts +37 -9
- package/src/commands/remove-daemon.ts +75 -0
- package/src/commands/remove-hook.ts +194 -0
- package/src/commands/setup-hook.ts +93 -43
- package/src/commands/status.ts +8 -4
- package/src/commands/submit.ts +191 -83
- package/src/commands/whoami.ts +8 -4
- package/src/config.ts +241 -21
- package/src/constants.ts +18 -0
- package/src/flush.test.ts +395 -0
- package/src/flush.ts +591 -0
- package/vitest.config.ts +8 -0
- package/src/commands/register.ts +0 -52
- package/src/submitter.ts +0 -110
package/src/flush.ts
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import {
|
|
4
|
+
chunkTrace,
|
|
5
|
+
extractClaudeCode,
|
|
6
|
+
extractCodex,
|
|
7
|
+
extractCursor,
|
|
8
|
+
redactTrace,
|
|
9
|
+
type NormalizedTrace,
|
|
10
|
+
} from "@tracemarketplace/shared";
|
|
11
|
+
import { ApiClient } from "./api-client.js";
|
|
12
|
+
import {
|
|
13
|
+
loadState,
|
|
14
|
+
migrateSessionUploadState,
|
|
15
|
+
saveState,
|
|
16
|
+
stateKey,
|
|
17
|
+
type Config,
|
|
18
|
+
type SessionUploadState,
|
|
19
|
+
type TrackedSessionTool,
|
|
20
|
+
} from "./config.js";
|
|
21
|
+
import { CURSOR_DB_PATH } from "./sessions.js";
|
|
22
|
+
|
|
23
|
+
const IDLE_FINALIZATION_MS = 2 * 24 * 60 * 60 * 1000;
|
|
24
|
+
const UNCONFIRMED_RESUBMIT_MS = 2 * 60 * 60 * 1000; // re-submit after 2hr with no confirmation
|
|
25
|
+
const INGEST_CONCURRENCY = 12;
|
|
26
|
+
|
|
27
|
+
export interface SessionSource {
|
|
28
|
+
tool: TrackedSessionTool;
|
|
29
|
+
locator: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PlannedUpload {
|
|
34
|
+
trace: NormalizedTrace;
|
|
35
|
+
nextState: SessionUploadState;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SessionPlan {
|
|
39
|
+
observedState: SessionUploadState;
|
|
40
|
+
uploads: PlannedUpload[];
|
|
41
|
+
pending: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SessionFlushResult {
|
|
45
|
+
source: SessionSource;
|
|
46
|
+
sessionKey: string | null;
|
|
47
|
+
uploadedChunks: number;
|
|
48
|
+
duplicateChunks: number;
|
|
49
|
+
pending: boolean;
|
|
50
|
+
payoutCents: number;
|
|
51
|
+
turnCount: number;
|
|
52
|
+
migratedLegacyState: boolean;
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FlushResult {
|
|
57
|
+
processedSessions: number;
|
|
58
|
+
uploadedChunks: number;
|
|
59
|
+
duplicateChunks: number;
|
|
60
|
+
pendingSessions: number;
|
|
61
|
+
payoutCents: number;
|
|
62
|
+
results: SessionFlushResult[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ChunkUploadResult {
|
|
66
|
+
duplicate: boolean;
|
|
67
|
+
payoutCents: number;
|
|
68
|
+
traceId: string | null;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface IngestResponse {
|
|
73
|
+
queued?: boolean;
|
|
74
|
+
duplicate?: boolean;
|
|
75
|
+
trace_id?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function collectIdleSessionSources(
|
|
79
|
+
sessions: Record<string, SessionUploadState>,
|
|
80
|
+
now = new Date()
|
|
81
|
+
): SessionSource[] {
|
|
82
|
+
return Object.values(sessions)
|
|
83
|
+
.filter((session) => isSessionIdlePending(session, now))
|
|
84
|
+
.map((session) => ({
|
|
85
|
+
tool: session.sourceTool,
|
|
86
|
+
locator: session.locator,
|
|
87
|
+
label: `${session.sourceTool}:${session.sourceSessionId}`,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ChunkExistsResponse {
|
|
92
|
+
exists: boolean;
|
|
93
|
+
trace_id?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function verifyUnconfirmedChunks(
|
|
97
|
+
state: ReturnType<typeof loadState>,
|
|
98
|
+
client: ApiClient,
|
|
99
|
+
now: Date,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
for (const [key, session] of Object.entries(state.sessions)) {
|
|
102
|
+
if (session.confirmedChunkIndex >= session.nextChunkIndex) continue;
|
|
103
|
+
|
|
104
|
+
// Check if timed out — reset to re-submit from last confirmed point
|
|
105
|
+
if (session.unconfirmedSince) {
|
|
106
|
+
const age = now.getTime() - Date.parse(session.unconfirmedSince);
|
|
107
|
+
if (age >= UNCONFIRMED_RESUBMIT_MS) {
|
|
108
|
+
state.sessions[key] = {
|
|
109
|
+
...session,
|
|
110
|
+
nextChunkIndex: session.confirmedChunkIndex,
|
|
111
|
+
openChunkStartTurn: session.confirmedOpenChunkStartTurn,
|
|
112
|
+
unconfirmedSince: null,
|
|
113
|
+
};
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check each unconfirmed chunk sequentially — stop at first missing one
|
|
119
|
+
for (let i = session.confirmedChunkIndex; i < session.nextChunkIndex; i++) {
|
|
120
|
+
try {
|
|
121
|
+
const params = new URLSearchParams({
|
|
122
|
+
source_tool: session.sourceTool,
|
|
123
|
+
source_session_id: session.sourceSessionId,
|
|
124
|
+
chunk_index: String(i),
|
|
125
|
+
});
|
|
126
|
+
const result = await client.get(`/api/v1/traces/exists?${params}`) as ChunkExistsResponse;
|
|
127
|
+
if (!result.exists) break;
|
|
128
|
+
state.sessions[key] = {
|
|
129
|
+
...state.sessions[key]!,
|
|
130
|
+
confirmedChunkIndex: i + 1,
|
|
131
|
+
confirmedOpenChunkStartTurn: state.sessions[key]!.openChunkStartTurn,
|
|
132
|
+
unconfirmedSince: i + 1 >= session.nextChunkIndex ? null : state.sessions[key]!.unconfirmedSince,
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function flushTrackedSessions(
|
|
142
|
+
config: Config,
|
|
143
|
+
sources: SessionSource[],
|
|
144
|
+
opts: { includeIdleTracked?: boolean; now?: Date; prefetchedTraces?: Map<string, NormalizedTrace> } = {}
|
|
145
|
+
): Promise<FlushResult> {
|
|
146
|
+
const now = opts.now ?? new Date();
|
|
147
|
+
const state = loadState(config.profile);
|
|
148
|
+
const client = new ApiClient(config.serverUrl, config.apiKey);
|
|
149
|
+
|
|
150
|
+
const allSources = dedupeSources([
|
|
151
|
+
...sources,
|
|
152
|
+
...(opts.includeIdleTracked ? collectIdleSessionSources(state.sessions, now) : []),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Migrate any existing sessions that predate the confirmation fields
|
|
156
|
+
for (const [key, session] of Object.entries(state.sessions)) {
|
|
157
|
+
state.sessions[key] = migrateSessionUploadState(session);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Verify previously queued chunks and handle 2hr re-submit timeout
|
|
161
|
+
await verifyUnconfirmedChunks(state, client, now);
|
|
162
|
+
|
|
163
|
+
// Process sessions concurrently — each session's chunks stay sequential internally
|
|
164
|
+
const results = await pLimit(
|
|
165
|
+
allSources.map((source) => () => processSessionSource(
|
|
166
|
+
source, state, config, client, now,
|
|
167
|
+
opts.prefetchedTraces?.get(`${source.tool}:${source.locator}`),
|
|
168
|
+
)),
|
|
169
|
+
INGEST_CONCURRENCY,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
saveState(state, config.profile);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
processedSessions: results.length,
|
|
176
|
+
uploadedChunks: results.reduce((sum, result) => sum + result.uploadedChunks, 0),
|
|
177
|
+
duplicateChunks: results.reduce((sum, result) => sum + result.duplicateChunks, 0),
|
|
178
|
+
pendingSessions: results.filter((result) => result.pending).length,
|
|
179
|
+
payoutCents: results.reduce((sum, result) => sum + result.payoutCents, 0),
|
|
180
|
+
results,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function planSessionUploads(
|
|
185
|
+
trace: NormalizedTrace,
|
|
186
|
+
cursor: SessionUploadState,
|
|
187
|
+
now = new Date()
|
|
188
|
+
): SessionPlan {
|
|
189
|
+
const observedState = observeTrace(cursor, trace);
|
|
190
|
+
|
|
191
|
+
if (observedState.openChunkStartTurn >= trace.turn_count) {
|
|
192
|
+
return {
|
|
193
|
+
observedState,
|
|
194
|
+
uploads: [],
|
|
195
|
+
pending: false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const suffixTrace = sliceTraceFromTurn(trace, observedState.openChunkStartTurn);
|
|
200
|
+
const relativeChunks = chunkTrace(suffixTrace);
|
|
201
|
+
const uploads: PlannedUpload[] = [];
|
|
202
|
+
const nowIso = now.toISOString();
|
|
203
|
+
let nextState = observedState;
|
|
204
|
+
|
|
205
|
+
for (let i = 0; i < relativeChunks.length; i++) {
|
|
206
|
+
const relativeChunk = relativeChunks[i];
|
|
207
|
+
const isLast = i === relativeChunks.length - 1;
|
|
208
|
+
const outputTokens = relativeChunk.total_output_tokens ?? 0;
|
|
209
|
+
const closeReason = !isLast
|
|
210
|
+
? "100k_tokens"
|
|
211
|
+
: outputTokens >= 100_000
|
|
212
|
+
? "100k_tokens"
|
|
213
|
+
: isTraceIdle(trace, now)
|
|
214
|
+
? "idle_2d"
|
|
215
|
+
: null;
|
|
216
|
+
|
|
217
|
+
if (!closeReason) {
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const finalizedChunk = finalizeChunk(relativeChunk, nextState, nowIso, closeReason);
|
|
222
|
+
nextState = applyUploadedChunk(nextState, trace, finalizedChunk);
|
|
223
|
+
uploads.push({ trace: finalizedChunk, nextState });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
observedState,
|
|
228
|
+
uploads,
|
|
229
|
+
pending: nextState.openChunkStartTurn < trace.turn_count,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function createFreshSessionState(
|
|
234
|
+
source: SessionSource,
|
|
235
|
+
trace: NormalizedTrace
|
|
236
|
+
): SessionUploadState {
|
|
237
|
+
return {
|
|
238
|
+
sourceTool: source.tool,
|
|
239
|
+
sourceSessionId: trace.source_session_id,
|
|
240
|
+
locator: source.locator,
|
|
241
|
+
nextChunkIndex: 0,
|
|
242
|
+
openChunkStartTurn: 0,
|
|
243
|
+
lastSeenTurnCount: 0,
|
|
244
|
+
lastActivityAt: null,
|
|
245
|
+
lastFlushedTurnId: null,
|
|
246
|
+
confirmedChunkIndex: 0,
|
|
247
|
+
confirmedOpenChunkStartTurn: 0,
|
|
248
|
+
unconfirmedSince: null,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function migrateLegacySessionState(
|
|
253
|
+
source: SessionSource,
|
|
254
|
+
trace: NormalizedTrace,
|
|
255
|
+
legacyChunkIndex: number
|
|
256
|
+
): SessionUploadState {
|
|
257
|
+
const nextChunkIndex = legacyChunkIndex + 1;
|
|
258
|
+
const openChunkStartTurn = trace.turn_count;
|
|
259
|
+
return {
|
|
260
|
+
sourceTool: source.tool,
|
|
261
|
+
sourceSessionId: trace.source_session_id,
|
|
262
|
+
locator: source.locator,
|
|
263
|
+
nextChunkIndex,
|
|
264
|
+
openChunkStartTurn,
|
|
265
|
+
lastSeenTurnCount: trace.turn_count,
|
|
266
|
+
lastActivityAt: getLastActivityAt(trace),
|
|
267
|
+
lastFlushedTurnId: trace.turns[trace.turn_count - 1]?.turn_id ?? null,
|
|
268
|
+
confirmedChunkIndex: nextChunkIndex,
|
|
269
|
+
confirmedOpenChunkStartTurn: openChunkStartTurn,
|
|
270
|
+
unconfirmedSince: null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function processSessionSource(
|
|
275
|
+
source: SessionSource,
|
|
276
|
+
state: ReturnType<typeof loadState>,
|
|
277
|
+
config: Config,
|
|
278
|
+
client: ApiClient,
|
|
279
|
+
now: Date,
|
|
280
|
+
prefetchedTrace?: NormalizedTrace,
|
|
281
|
+
): Promise<SessionFlushResult> {
|
|
282
|
+
let trace: NormalizedTrace;
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
trace = prefetchedTrace ?? await extractTraceFromSource(source, config.email);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
return {
|
|
288
|
+
source,
|
|
289
|
+
sessionKey: null,
|
|
290
|
+
uploadedChunks: 0,
|
|
291
|
+
duplicateChunks: 0,
|
|
292
|
+
pending: false,
|
|
293
|
+
payoutCents: 0,
|
|
294
|
+
turnCount: 0,
|
|
295
|
+
migratedLegacyState: false,
|
|
296
|
+
error: `Extraction failed: ${err}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (trace.turn_count === 0) {
|
|
301
|
+
return {
|
|
302
|
+
source,
|
|
303
|
+
sessionKey: stateKey(source.tool, trace.source_session_id),
|
|
304
|
+
uploadedChunks: 0,
|
|
305
|
+
duplicateChunks: 0,
|
|
306
|
+
pending: false,
|
|
307
|
+
payoutCents: 0,
|
|
308
|
+
turnCount: 0,
|
|
309
|
+
migratedLegacyState: false,
|
|
310
|
+
error: "Empty session",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const key = stateKey(trace.source_tool, trace.source_session_id);
|
|
315
|
+
const existing = state.sessions[key];
|
|
316
|
+
const legacyChunkIndex = state.chunks[key];
|
|
317
|
+
const migratedLegacyState = !existing && typeof legacyChunkIndex === "number";
|
|
318
|
+
const cursor = existing
|
|
319
|
+
? { ...existing, locator: source.locator, sourceTool: trace.source_tool, sourceSessionId: trace.source_session_id }
|
|
320
|
+
: migratedLegacyState
|
|
321
|
+
? migrateLegacySessionState(source, trace, legacyChunkIndex)
|
|
322
|
+
: createFreshSessionState(source, trace);
|
|
323
|
+
|
|
324
|
+
const plan = planSessionUploads(trace, cursor, now);
|
|
325
|
+
let workingState = plan.observedState;
|
|
326
|
+
let uploadedChunks = 0;
|
|
327
|
+
let duplicateChunks = 0;
|
|
328
|
+
let payoutCents = 0;
|
|
329
|
+
|
|
330
|
+
for (const upload of plan.uploads) {
|
|
331
|
+
const tUpload = Date.now();
|
|
332
|
+
const result = await uploadTraceChunk(upload.trace, client);
|
|
333
|
+
console.error(`[flush] ${key} chunk${upload.trace.chunk_index} done in ${Date.now()-tUpload}ms err=${result.error?.slice(0,60) ?? 'none'}`);
|
|
334
|
+
if (result.error) {
|
|
335
|
+
state.sessions[key] = workingState;
|
|
336
|
+
if (workingState.nextChunkIndex > 0) {
|
|
337
|
+
state.chunks[key] = workingState.nextChunkIndex - 1;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
source,
|
|
341
|
+
sessionKey: key,
|
|
342
|
+
uploadedChunks,
|
|
343
|
+
duplicateChunks,
|
|
344
|
+
pending: workingState.openChunkStartTurn < workingState.lastSeenTurnCount,
|
|
345
|
+
payoutCents,
|
|
346
|
+
turnCount: trace.turn_count,
|
|
347
|
+
migratedLegacyState,
|
|
348
|
+
error: result.error,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
workingState = upload.nextState;
|
|
353
|
+
payoutCents += result.payoutCents;
|
|
354
|
+
if (result.duplicate) {
|
|
355
|
+
duplicateChunks++;
|
|
356
|
+
} else {
|
|
357
|
+
uploadedChunks++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
state.sessions[key] = workingState;
|
|
362
|
+
if (workingState.nextChunkIndex > 0) {
|
|
363
|
+
state.chunks[key] = workingState.nextChunkIndex - 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
source,
|
|
368
|
+
sessionKey: key,
|
|
369
|
+
uploadedChunks,
|
|
370
|
+
duplicateChunks,
|
|
371
|
+
pending: workingState.openChunkStartTurn < workingState.lastSeenTurnCount,
|
|
372
|
+
payoutCents,
|
|
373
|
+
turnCount: trace.turn_count,
|
|
374
|
+
migratedLegacyState,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function extractTraceFromSource(
|
|
379
|
+
source: SessionSource,
|
|
380
|
+
email: string
|
|
381
|
+
): Promise<NormalizedTrace> {
|
|
382
|
+
switch (source.tool) {
|
|
383
|
+
case "claude_code":
|
|
384
|
+
return extractClaudeCode(source.locator, email);
|
|
385
|
+
case "codex_cli": {
|
|
386
|
+
const buf = await readFile(source.locator);
|
|
387
|
+
return extractCodex(buf, email);
|
|
388
|
+
}
|
|
389
|
+
case "cursor":
|
|
390
|
+
return extractCursor(CURSOR_DB_PATH, source.locator, email);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function uploadTraceChunk(
|
|
395
|
+
trace: NormalizedTrace,
|
|
396
|
+
client: ApiClient
|
|
397
|
+
): Promise<ChunkUploadResult> {
|
|
398
|
+
// Client-side regex redaction runs before transmission; Presidio runs server-side async.
|
|
399
|
+
const payloadTrace = redactTrace(trace, { homeDir: homedir() });
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const result = await client.post("/api/v1/traces/ingest", {
|
|
403
|
+
trace: payloadTrace,
|
|
404
|
+
source_tool: payloadTrace.source_tool,
|
|
405
|
+
}) as IngestResponse;
|
|
406
|
+
|
|
407
|
+
if (result.duplicate) {
|
|
408
|
+
return { duplicate: true, payoutCents: 0, traceId: result.trace_id ?? null };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 202 queued — payout credited asynchronously; show $0 until processed
|
|
412
|
+
return { duplicate: false, payoutCents: 0, traceId: null };
|
|
413
|
+
} catch (err) {
|
|
414
|
+
return {
|
|
415
|
+
duplicate: false,
|
|
416
|
+
payoutCents: 0,
|
|
417
|
+
traceId: null,
|
|
418
|
+
error: formatSubmitFailure(err),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function finalizeChunk(
|
|
424
|
+
trace: NormalizedTrace,
|
|
425
|
+
cursor: SessionUploadState,
|
|
426
|
+
nowIso: string,
|
|
427
|
+
closeReason: "100k_tokens" | "idle_2d"
|
|
428
|
+
): NormalizedTrace {
|
|
429
|
+
return {
|
|
430
|
+
...trace,
|
|
431
|
+
chunk_index: cursor.nextChunkIndex,
|
|
432
|
+
chunk_start_turn: cursor.openChunkStartTurn + (trace.chunk_start_turn ?? 0),
|
|
433
|
+
chunk_complete: true,
|
|
434
|
+
chunk_close_reason: closeReason,
|
|
435
|
+
chunk_closed_at: nowIso,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function applyUploadedChunk(
|
|
440
|
+
cursor: SessionUploadState,
|
|
441
|
+
trace: NormalizedTrace,
|
|
442
|
+
uploadedChunk: NormalizedTrace
|
|
443
|
+
): SessionUploadState {
|
|
444
|
+
const nextOpenChunkStartTurn = (uploadedChunk.chunk_start_turn ?? cursor.openChunkStartTurn)
|
|
445
|
+
+ (uploadedChunk.turn_count ?? uploadedChunk.turns.length);
|
|
446
|
+
|
|
447
|
+
const nowIso = new Date().toISOString();
|
|
448
|
+
return {
|
|
449
|
+
...cursor,
|
|
450
|
+
nextChunkIndex: (uploadedChunk.chunk_index ?? cursor.nextChunkIndex) + 1,
|
|
451
|
+
openChunkStartTurn: nextOpenChunkStartTurn,
|
|
452
|
+
lastSeenTurnCount: trace.turn_count,
|
|
453
|
+
lastActivityAt: getLastActivityAt(trace),
|
|
454
|
+
lastFlushedTurnId: trace.turns[nextOpenChunkStartTurn - 1]?.turn_id ?? cursor.lastFlushedTurnId,
|
|
455
|
+
unconfirmedSince: cursor.unconfirmedSince ?? nowIso,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function observeTrace(
|
|
460
|
+
cursor: SessionUploadState,
|
|
461
|
+
trace: NormalizedTrace
|
|
462
|
+
): SessionUploadState {
|
|
463
|
+
return {
|
|
464
|
+
...cursor,
|
|
465
|
+
sourceTool: trace.source_tool,
|
|
466
|
+
sourceSessionId: trace.source_session_id,
|
|
467
|
+
openChunkStartTurn: Math.min(cursor.openChunkStartTurn, trace.turn_count),
|
|
468
|
+
lastSeenTurnCount: trace.turn_count,
|
|
469
|
+
lastActivityAt: getLastActivityAt(trace),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function sliceTraceFromTurn(
|
|
474
|
+
trace: NormalizedTrace,
|
|
475
|
+
startTurn: number
|
|
476
|
+
): NormalizedTrace {
|
|
477
|
+
const turns = trace.turns.slice(startTurn);
|
|
478
|
+
const inputTokens = turns.reduce((sum, turn) => sum + (turn.usage?.input_tokens ?? 0), 0);
|
|
479
|
+
const outputTokens = turns.reduce((sum, turn) => sum + (turn.usage?.output_tokens ?? 0), 0);
|
|
480
|
+
const toolCallCount = turns.reduce(
|
|
481
|
+
(sum, turn) => sum + turn.content.filter((block) => block.type === "tool_use").length,
|
|
482
|
+
0
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
...trace,
|
|
487
|
+
turns,
|
|
488
|
+
chunk_index: 0,
|
|
489
|
+
chunk_start_turn: 0,
|
|
490
|
+
chunk_complete: false,
|
|
491
|
+
chunk_close_reason: undefined,
|
|
492
|
+
chunk_closed_at: null,
|
|
493
|
+
turn_count: turns.length,
|
|
494
|
+
tool_call_count: toolCallCount,
|
|
495
|
+
has_tool_calls: toolCallCount > 0,
|
|
496
|
+
has_thinking_blocks: turns.some((turn) => turn.content.some((block) => block.type === "thinking")),
|
|
497
|
+
has_file_changes: turns.some((turn) =>
|
|
498
|
+
turn.content.some((block) =>
|
|
499
|
+
block.type === "tool_use"
|
|
500
|
+
&& (block.tool_name === "Edit" || block.tool_name === "Write" || block.tool_name === "MultiEdit")
|
|
501
|
+
)
|
|
502
|
+
),
|
|
503
|
+
has_shell_commands: turns.some((turn) =>
|
|
504
|
+
turn.content.some((block) => block.type === "tool_use" && block.tool_name === "Bash")
|
|
505
|
+
),
|
|
506
|
+
total_input_tokens: inputTokens || null,
|
|
507
|
+
total_output_tokens: outputTokens || null,
|
|
508
|
+
started_at: turns.find((turn) => turn.timestamp)?.timestamp ?? trace.started_at,
|
|
509
|
+
ended_at: [...turns].reverse().find((turn) => turn.timestamp)?.timestamp ?? trace.ended_at,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function getLastActivityAt(trace: NormalizedTrace): string | null {
|
|
514
|
+
return trace.ended_at
|
|
515
|
+
?? trace.turns[trace.turns.length - 1]?.timestamp
|
|
516
|
+
?? trace.extracted_at
|
|
517
|
+
?? null;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function isTraceIdle(trace: NormalizedTrace, now: Date): boolean {
|
|
521
|
+
const lastActivityAt = getLastActivityAt(trace);
|
|
522
|
+
if (!lastActivityAt) return false;
|
|
523
|
+
|
|
524
|
+
const lastActivityMs = Date.parse(lastActivityAt);
|
|
525
|
+
if (Number.isNaN(lastActivityMs)) return false;
|
|
526
|
+
return now.getTime() - lastActivityMs >= IDLE_FINALIZATION_MS;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isSessionIdlePending(session: SessionUploadState, now: Date): boolean {
|
|
530
|
+
if (session.openChunkStartTurn >= session.lastSeenTurnCount) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (!session.lastActivityAt) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const lastActivityMs = Date.parse(session.lastActivityAt);
|
|
538
|
+
if (Number.isNaN(lastActivityMs)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return now.getTime() - lastActivityMs >= IDLE_FINALIZATION_MS;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function dedupeSources(sources: SessionSource[]): SessionSource[] {
|
|
546
|
+
const unique = new Map<string, SessionSource>();
|
|
547
|
+
|
|
548
|
+
for (const source of sources) {
|
|
549
|
+
unique.set(`${source.tool}:${source.locator}`, source);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return [...unique.values()];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function formatSubmitFailure(err: unknown): string {
|
|
556
|
+
return err instanceof Error ? `Submit failed: ${err.message}` : `Submit failed: ${String(err)}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function pLimit<T>(tasks: Array<() => Promise<T>>, concurrency: number): Promise<T[]> {
|
|
560
|
+
const results: T[] = new Array(tasks.length);
|
|
561
|
+
let index = 0;
|
|
562
|
+
|
|
563
|
+
async function worker() {
|
|
564
|
+
while (index < tasks.length) {
|
|
565
|
+
const i = index++;
|
|
566
|
+
results[i] = await tasks[i]!();
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, worker));
|
|
571
|
+
return results;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function buildFileSessionSource(
|
|
575
|
+
tool: "claude_code" | "codex_cli",
|
|
576
|
+
filePath: string
|
|
577
|
+
): SessionSource {
|
|
578
|
+
return {
|
|
579
|
+
tool,
|
|
580
|
+
locator: filePath,
|
|
581
|
+
label: filePath,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function buildCursorSessionSource(sessionId: string): SessionSource {
|
|
586
|
+
return {
|
|
587
|
+
tool: "cursor",
|
|
588
|
+
locator: sessionId,
|
|
589
|
+
label: sessionId,
|
|
590
|
+
};
|
|
591
|
+
}
|
package/vitest.config.ts
ADDED
package/src/commands/register.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import inquirer from "inquirer";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { saveConfig } from "../config.js";
|
|
4
|
-
|
|
5
|
-
export async function registerCommand(opts: { serverUrl?: string }): Promise<void> {
|
|
6
|
-
const { email } = await inquirer.prompt([
|
|
7
|
-
{
|
|
8
|
-
type: "input",
|
|
9
|
-
name: "email",
|
|
10
|
-
message: "Your email address:",
|
|
11
|
-
validate: (v: string) => v.includes("@") || "Enter a valid email",
|
|
12
|
-
},
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
const serverUrl =
|
|
16
|
-
opts.serverUrl ??
|
|
17
|
-
(
|
|
18
|
-
await inquirer.prompt([
|
|
19
|
-
{
|
|
20
|
-
type: "input",
|
|
21
|
-
name: "url",
|
|
22
|
-
message: "Server URL:",
|
|
23
|
-
default: "https://trace-marketplace-api.fly.dev",
|
|
24
|
-
},
|
|
25
|
-
])
|
|
26
|
-
).url;
|
|
27
|
-
|
|
28
|
-
const res = await fetch(`${serverUrl}/api/v1/register`, {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: { "Content-Type": "application/json" },
|
|
31
|
-
body: JSON.stringify({ email }),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const data = (await res.json()) as { api_key?: string; error?: string };
|
|
35
|
-
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
if (res.status === 409 && data.api_key) {
|
|
38
|
-
console.log(chalk.yellow("Email already registered."));
|
|
39
|
-
console.log(chalk.cyan("Your API key:"), chalk.bold(data.api_key));
|
|
40
|
-
saveConfig({ apiKey: data.api_key, serverUrl, email });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const apiKey = data.api_key!;
|
|
47
|
-
saveConfig({ apiKey, serverUrl, email });
|
|
48
|
-
|
|
49
|
-
console.log(chalk.green("Registered successfully!"));
|
|
50
|
-
console.log(chalk.cyan("Your API key:"), chalk.bold(apiKey));
|
|
51
|
-
console.log(chalk.gray("Config saved to ~/.config/tracemarketplace/config.json"));
|
|
52
|
-
}
|