@tracemarketplace/cli 0.0.11 → 0.0.15

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 (84) hide show
  1. package/dist/api-client.d.ts +2 -2
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +2 -2
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/cli.js +45 -14
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/auto-submit.d.ts +2 -1
  8. package/dist/commands/auto-submit.d.ts.map +1 -1
  9. package/dist/commands/auto-submit.js +43 -56
  10. package/dist/commands/auto-submit.js.map +1 -1
  11. package/dist/commands/daemon.d.ts +8 -1
  12. package/dist/commands/daemon.d.ts.map +1 -1
  13. package/dist/commands/daemon.js +118 -62
  14. package/dist/commands/daemon.js.map +1 -1
  15. package/dist/commands/history.d.ts +3 -1
  16. package/dist/commands/history.d.ts.map +1 -1
  17. package/dist/commands/history.js +8 -4
  18. package/dist/commands/history.js.map +1 -1
  19. package/dist/commands/login.d.ts +5 -1
  20. package/dist/commands/login.d.ts.map +1 -1
  21. package/dist/commands/login.js +25 -9
  22. package/dist/commands/login.js.map +1 -1
  23. package/dist/commands/register.d.ts +1 -0
  24. package/dist/commands/register.d.ts.map +1 -1
  25. package/dist/commands/register.js +4 -39
  26. package/dist/commands/register.js.map +1 -1
  27. package/dist/commands/remove-hook.d.ts +6 -0
  28. package/dist/commands/remove-hook.d.ts.map +1 -0
  29. package/dist/commands/remove-hook.js +174 -0
  30. package/dist/commands/remove-hook.js.map +1 -0
  31. package/dist/commands/setup-hook.d.ts +2 -0
  32. package/dist/commands/setup-hook.d.ts.map +1 -1
  33. package/dist/commands/setup-hook.js +86 -42
  34. package/dist/commands/setup-hook.js.map +1 -1
  35. package/dist/commands/status.d.ts +3 -1
  36. package/dist/commands/status.d.ts.map +1 -1
  37. package/dist/commands/status.js +8 -4
  38. package/dist/commands/status.js.map +1 -1
  39. package/dist/commands/submit.d.ts +1 -0
  40. package/dist/commands/submit.d.ts.map +1 -1
  41. package/dist/commands/submit.js +136 -83
  42. package/dist/commands/submit.js.map +1 -1
  43. package/dist/commands/whoami.d.ts +3 -1
  44. package/dist/commands/whoami.d.ts.map +1 -1
  45. package/dist/commands/whoami.js +8 -4
  46. package/dist/commands/whoami.js.map +1 -1
  47. package/dist/config.d.ts +33 -6
  48. package/dist/config.d.ts.map +1 -1
  49. package/dist/config.js +163 -17
  50. package/dist/config.js.map +1 -1
  51. package/dist/constants.d.ts +8 -0
  52. package/dist/constants.d.ts.map +1 -0
  53. package/dist/constants.js +16 -0
  54. package/dist/constants.js.map +1 -0
  55. package/dist/flush.d.ts +46 -0
  56. package/dist/flush.d.ts.map +1 -0
  57. package/dist/flush.js +338 -0
  58. package/dist/flush.js.map +1 -0
  59. package/dist/flush.test.d.ts +2 -0
  60. package/dist/flush.test.d.ts.map +1 -0
  61. package/dist/flush.test.js +175 -0
  62. package/dist/flush.test.js.map +1 -0
  63. package/dist/submitter.d.ts.map +1 -1
  64. package/dist/submitter.js +5 -2
  65. package/dist/submitter.js.map +1 -1
  66. package/package.json +8 -7
  67. package/src/api-client.ts +3 -3
  68. package/src/cli.ts +51 -14
  69. package/src/commands/auto-submit.ts +80 -40
  70. package/src/commands/daemon.ts +166 -59
  71. package/src/commands/history.ts +9 -4
  72. package/src/commands/login.ts +37 -9
  73. package/src/commands/register.ts +5 -49
  74. package/src/commands/remove-hook.ts +194 -0
  75. package/src/commands/setup-hook.ts +94 -44
  76. package/src/commands/status.ts +8 -4
  77. package/src/commands/submit.ts +189 -83
  78. package/src/commands/whoami.ts +8 -4
  79. package/src/config.ts +223 -21
  80. package/src/constants.ts +18 -0
  81. package/src/flush.test.ts +214 -0
  82. package/src/flush.ts +505 -0
  83. package/vitest.config.ts +8 -0
  84. package/src/submitter.ts +0 -110
@@ -1,22 +1,54 @@
1
1
  import { existsSync } from "fs";
2
- import { homedir } from "os";
3
- import { join } from "path";
2
+ import { readFile } from "fs/promises";
4
3
  import chalk from "chalk";
5
4
  import ora from "ora";
6
5
  import inquirer from "inquirer";
7
- import { extractClaudeCode, extractCodex, extractCursor, redactTrace } from "@tracemarketplace/shared";
8
- import { loadConfig } from "../config.js";
9
- import { ApiClient } from "../api-client.js";
10
- import { findFiles, CURSOR_DB_PATH } from "../sessions.js";
11
- import type { NormalizedTrace } from "@tracemarketplace/shared";
6
+ import { extractClaudeCode, extractCodex, extractCursor, type NormalizedTrace } from "@tracemarketplace/shared";
7
+ import { loadConfig, loadState, resolveProfile, stateKey } from "../config.js";
8
+ import { loginCommandForProfile } from "../constants.js";
9
+ import {
10
+ buildCursorSessionSource,
11
+ buildFileSessionSource,
12
+ createFreshSessionState,
13
+ flushTrackedSessions,
14
+ migrateLegacySessionState,
15
+ planSessionUploads,
16
+ type SessionSource,
17
+ } from "../flush.js";
18
+ import { CURSOR_DB_PATH, findFiles } from "../sessions.js";
12
19
 
13
20
  interface SubmitOptions {
21
+ profile?: string;
14
22
  tool?: string;
15
23
  session?: string;
16
24
  dryRun?: boolean;
17
25
  since?: string;
18
26
  }
19
27
 
28
+ interface DiscoveredSession {
29
+ source: SessionSource;
30
+ trace: NormalizedTrace;
31
+ }
32
+
33
+ interface PlannedSession extends DiscoveredSession {
34
+ readyChunks: number;
35
+ pending: boolean;
36
+ }
37
+
38
+ function pushSessionIfNonEmpty(
39
+ sessions: DiscoveredSession[],
40
+ source: SessionSource,
41
+ trace: NormalizedTrace,
42
+ emptyLabel: string,
43
+ skippedEmpty: string[],
44
+ ): void {
45
+ if (trace.turn_count > 0) {
46
+ sessions.push({ source, trace });
47
+ } else {
48
+ skippedEmpty.push(emptyLabel);
49
+ }
50
+ }
51
+
20
52
  function parseSinceMs(since: string): number {
21
53
  const m = since.match(/^(\d+)(m|h|d)$/);
22
54
  if (!m) return 30 * 24 * 60 * 60 * 1000;
@@ -27,9 +59,10 @@ function parseSinceMs(since: string): number {
27
59
  }
28
60
 
29
61
  export async function submitCommand(opts: SubmitOptions): Promise<void> {
30
- const config = loadConfig();
62
+ const profile = resolveProfile(opts.profile);
63
+ const config = loadConfig(profile);
31
64
  if (!config) {
32
- console.error(chalk.red("Not registered. Run: trace register"));
65
+ console.error(chalk.red(`Not authenticated for profile '${profile}'. Run: ${loginCommandForProfile(profile)}`));
33
66
  process.exit(1);
34
67
  }
35
68
 
@@ -37,8 +70,9 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
37
70
  const sinceDays = sinceMs / (24 * 60 * 60 * 1000);
38
71
  const spinner = ora("Discovering sessions...").start();
39
72
 
40
- const traces: NormalizedTrace[] = [];
73
+ const discovered: DiscoveredSession[] = [];
41
74
  const errors: string[] = [];
75
+ const skippedEmpty: string[] = [];
42
76
 
43
77
  const toolAlias: Record<string, string> = {
44
78
  "claude-code": "claude_code",
@@ -50,11 +84,17 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
50
84
  if (tools.includes("claude_code")) {
51
85
  const files = findFiles("claude_code", sinceDays);
52
86
  spinner.text = `Found ${files.length} Claude Code sessions`;
53
- for (const f of files) {
87
+ for (const filePath of files) {
54
88
  try {
55
- traces.push(await extractClaudeCode(f, config.email));
89
+ pushSessionIfNonEmpty(
90
+ discovered,
91
+ buildFileSessionSource("claude_code", filePath),
92
+ await extractClaudeCode(filePath, config.email),
93
+ `Claude Code ${filePath}`,
94
+ skippedEmpty,
95
+ );
56
96
  } catch (e) {
57
- errors.push(`Claude Code ${f}: ${e}`);
97
+ errors.push(`Claude Code ${filePath}: ${e}`);
58
98
  }
59
99
  }
60
100
  }
@@ -62,105 +102,163 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
62
102
  if (tools.includes("codex_cli")) {
63
103
  const files = findFiles("codex_cli", sinceDays);
64
104
  spinner.text = `Found ${files.length} Codex sessions`;
65
- for (const f of files) {
105
+ for (const filePath of files) {
66
106
  try {
67
- const { readFile } = await import("fs/promises");
68
- const trace = await extractCodex(await readFile(f), config.email);
69
- if (trace.turn_count > 0) traces.push(trace);
107
+ pushSessionIfNonEmpty(
108
+ discovered,
109
+ buildFileSessionSource("codex_cli", filePath),
110
+ await extractCodex(await readFile(filePath), config.email),
111
+ `Codex ${filePath}`,
112
+ skippedEmpty,
113
+ );
70
114
  } catch (e) {
71
- errors.push(`Codex ${f}: ${e}`);
115
+ errors.push(`Codex ${filePath}: ${e}`);
72
116
  }
73
117
  }
74
118
  }
75
119
 
76
- if (tools.includes("cursor")) {
77
- if (existsSync(CURSOR_DB_PATH)) {
78
- try {
79
- const { default: Database } = await import("better-sqlite3");
80
- const db = new Database(CURSOR_DB_PATH, { readonly: true });
81
- const cutoff = Date.now() - sinceMs;
82
- const allRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all() as Array<{ key: string; value: string }>;
83
- const rows = allRows.filter(r => {
84
- try { const d = JSON.parse(r.value); return (d.createdAt ?? 0) >= cutoff; } catch { return false; }
85
- });
86
- db.close();
87
- spinner.text = `Found ${rows.length} Cursor sessions`;
88
- for (const { key } of rows) {
89
- const sessionId = key.replace("composerData:", "");
90
- try {
91
- traces.push(await extractCursor(CURSOR_DB_PATH, sessionId, config.email));
92
- } catch (e) {
93
- errors.push(`Cursor ${sessionId}: ${e}`);
94
- }
120
+ if (tools.includes("cursor") && existsSync(CURSOR_DB_PATH)) {
121
+ try {
122
+ const { default: Database } = await import("better-sqlite3");
123
+ const db = new Database(CURSOR_DB_PATH, { readonly: true });
124
+ const cutoff = Date.now() - sinceMs;
125
+ const allRows = db.prepare("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all() as Array<{ key: string; value: string }>;
126
+ const rows = allRows.filter((row) => {
127
+ try {
128
+ const data = JSON.parse(row.value);
129
+ return (data.createdAt ?? 0) >= cutoff;
130
+ } catch {
131
+ return false;
132
+ }
133
+ });
134
+ db.close();
135
+ spinner.text = `Found ${rows.length} Cursor sessions`;
136
+ for (const { key } of rows) {
137
+ const sessionId = key.replace("composerData:", "");
138
+ try {
139
+ pushSessionIfNonEmpty(
140
+ discovered,
141
+ buildCursorSessionSource(sessionId),
142
+ await extractCursor(CURSOR_DB_PATH, sessionId, config.email),
143
+ `Cursor ${sessionId}`,
144
+ skippedEmpty,
145
+ );
146
+ } catch (e) {
147
+ errors.push(`Cursor ${sessionId}: ${e}`);
95
148
  }
96
- } catch (e) {
97
- errors.push(`Cursor DB: ${e}`);
98
149
  }
150
+ } catch (e) {
151
+ errors.push(`Cursor DB: ${e}`);
99
152
  }
100
153
  }
101
154
 
102
155
  spinner.stop();
103
156
 
104
- // Filter to specific session if requested
105
- const filteredTraces = opts.session
106
- ? traces.filter((t) => t.source_session_id.startsWith(opts.session!))
107
- : traces;
157
+ const filtered = opts.session
158
+ ? discovered.filter((session) => session.trace.source_session_id.startsWith(opts.session!))
159
+ : discovered;
108
160
 
109
- if (opts.session && filteredTraces.length === 0) {
161
+ if (opts.session && filtered.length === 0) {
110
162
  console.log(chalk.yellow(`No session found matching ID: ${opts.session}`));
111
163
  return;
112
164
  }
113
165
 
114
166
  if (errors.length > 0) {
115
167
  console.log(chalk.yellow(`\n${errors.length} extraction error(s):`));
116
- errors.slice(0, 3).forEach((e) => console.log(chalk.gray(` ${e}`)));
168
+ errors.slice(0, 3).forEach((error) => console.log(chalk.gray(` ${error}`)));
117
169
  if (errors.length > 3) console.log(chalk.gray(` ...and ${errors.length - 3} more`));
118
170
  }
119
171
 
120
- if (filteredTraces.length === 0) {
121
- console.log(chalk.yellow("No sessions found in the last " + sinceDays + " days."));
172
+ if (skippedEmpty.length > 0) {
173
+ console.log(chalk.gray(`\nSkipped ${skippedEmpty.length} empty/in-progress session(s).`));
174
+ skippedEmpty.slice(0, 3).forEach((label) => console.log(chalk.gray(` ${label}`)));
175
+ if (skippedEmpty.length > 3) {
176
+ console.log(chalk.gray(` ...and ${skippedEmpty.length - 3} more`));
177
+ }
178
+ }
179
+
180
+ if (filtered.length === 0) {
181
+ console.log(chalk.yellow(`No sessions found in the last ${sinceDays} days.`));
122
182
  return;
123
183
  }
124
184
 
125
- // Preview table
126
- console.log(`\n${chalk.bold("Sessions to submit:")} (${filteredTraces.length} total)\n`);
185
+ const state = loadState(config.profile);
186
+ const now = new Date();
187
+ const plannedSessions = filtered.map((session) => {
188
+ const key = stateKey(session.trace.source_tool, session.trace.source_session_id);
189
+ const existing = state.sessions[key];
190
+ const legacyChunkIndex = state.chunks[key];
191
+ const cursor = existing
192
+ ? {
193
+ ...existing,
194
+ locator: session.source.locator,
195
+ sourceTool: session.trace.source_tool,
196
+ sourceSessionId: session.trace.source_session_id,
197
+ }
198
+ : typeof legacyChunkIndex === "number"
199
+ ? migrateLegacySessionState(session.source, session.trace, legacyChunkIndex)
200
+ : createFreshSessionState(session.source, session.trace);
201
+ const plan = planSessionUploads(session.trace, cursor, now);
202
+
203
+ return {
204
+ ...session,
205
+ readyChunks: plan.uploads.length,
206
+ pending: plan.pending,
207
+ } satisfies PlannedSession;
208
+ });
209
+
210
+ const readyChunkCount = plannedSessions.reduce((sum, session) => sum + session.readyChunks, 0);
211
+ const readySessionCount = plannedSessions.filter((session) => session.readyChunks > 0).length;
212
+ const pendingSessionCount = plannedSessions.filter((session) => session.pending).length;
213
+
214
+ console.log(chalk.gray(`Target: ${config.profile} (${config.serverUrl})`));
215
+ console.log(`\n${chalk.bold("Sessions scanned:")} (${plannedSessions.length} total)\n`);
127
216
  console.log(
128
217
  chalk.gray(
129
218
  " Tool".padEnd(16) +
130
219
  "Session ID".padEnd(20) +
131
220
  "Turns".padEnd(8) +
132
- "Tokens".padEnd(12) +
133
- "Est. payout"
221
+ "Ready".padEnd(8) +
222
+ "Status"
134
223
  )
135
224
  );
136
- console.log(chalk.gray(" " + "─".repeat(60)));
225
+ console.log(chalk.gray(" " + "─".repeat(62)));
137
226
 
138
- for (const t of filteredTraces.slice(0, 20)) {
139
- const tokens = (t.total_input_tokens ?? 0) + (t.total_output_tokens ?? 0);
140
- const estPayout = t.content_fidelity === "full" ? "$0.04" : "$0.02";
227
+ for (const session of plannedSessions.slice(0, 20)) {
141
228
  console.log(
142
229
  " " +
143
- t.source_tool.padEnd(16) +
144
- t.source_session_id.slice(0, 18).padEnd(20) +
145
- String(t.turn_count).padEnd(8) +
146
- String(tokens || "—").padEnd(12) +
147
- estPayout
230
+ session.trace.source_tool.padEnd(16) +
231
+ session.trace.source_session_id.slice(0, 18).padEnd(20) +
232
+ String(session.trace.turn_count).padEnd(8) +
233
+ String(session.readyChunks).padEnd(8) +
234
+ describeSessionStatus(session)
148
235
  );
149
236
  }
150
- if (filteredTraces.length > 20) {
151
- console.log(chalk.gray(` ... and ${filteredTraces.length - 20} more`));
237
+ if (plannedSessions.length > 20) {
238
+ console.log(chalk.gray(` ... and ${plannedSessions.length - 20} more`));
152
239
  }
153
240
 
241
+ console.log(
242
+ chalk.gray(
243
+ `\nReady chunks: ${readyChunkCount} across ${readySessionCount} session(s); pending sessions: ${pendingSessionCount}`
244
+ )
245
+ );
246
+
154
247
  if (opts.dryRun) {
155
248
  console.log(chalk.cyan("\nDry run — nothing submitted."));
156
249
  return;
157
250
  }
158
251
 
252
+ if (readyChunkCount === 0) {
253
+ console.log(chalk.yellow("\nNo finalized chunks are ready to submit yet."));
254
+ return;
255
+ }
256
+
159
257
  const { confirm } = await inquirer.prompt([
160
258
  {
161
259
  type: "confirm",
162
260
  name: "confirm",
163
- message: `Submit ${filteredTraces.length} traces to ${config.serverUrl}?`,
261
+ message: `Submit ${readyChunkCount} finalized chunk(s) from ${readySessionCount} session(s) to ${config.profile} (${config.serverUrl})?`,
164
262
  default: true,
165
263
  },
166
264
  ]);
@@ -170,36 +268,44 @@ export async function submitCommand(opts: SubmitOptions): Promise<void> {
170
268
  return;
171
269
  }
172
270
 
173
- const uploadSpinner = ora(`Submitting ${traces.length} traces...`).start();
174
- const client = new ApiClient(config.serverUrl, config.apiKey);
271
+ const uploadSpinner = ora(`Submitting ${readyChunkCount} finalized chunk(s) to ${config.profile}...`).start();
175
272
 
176
273
  try {
177
- const home = homedir();
178
- const result = (await client.post("/api/v1/traces/batch", {
179
- traces: filteredTraces.map((t) => redactTrace(t, { homeDir: home })),
180
- source_tool: tools[0] ?? "mixed",
181
- })) as {
182
- submission_id: string;
183
- accepted: number;
184
- duplicate: number;
185
- total: number;
186
- traces: Array<{ payout_cents?: number }>;
187
- };
274
+ const result = await flushTrackedSessions(
275
+ config,
276
+ plannedSessions.map((session) => session.source),
277
+ { includeIdleTracked: false }
278
+ );
188
279
 
189
280
  uploadSpinner.stop();
190
281
 
191
- const totalPayout =
192
- result.traces?.reduce((s, t) => s + (t.payout_cents ?? 0), 0) ?? 0;
282
+ const failedSessions = result.results.filter((session) => session.error && session.error !== "Empty session");
193
283
 
194
284
  console.log(chalk.green("\nSubmission complete!"));
195
- console.log(` Accepted: ${chalk.bold(result.accepted)}`);
196
- console.log(` Duplicates: ${chalk.gray(result.duplicate)}`);
197
- console.log(` Total: ${result.total}`);
198
- console.log(` Payout: ${chalk.green("$" + (totalPayout / 100).toFixed(2))}`);
199
- console.log(chalk.gray(`\nSubmission ID: ${result.submission_id}`));
285
+ console.log(` Uploaded chunks: ${chalk.bold(result.uploadedChunks)}`);
286
+ console.log(` Duplicate chunks: ${chalk.gray(result.duplicateChunks)}`);
287
+ console.log(` Pending sessions: ${result.pendingSessions}`);
288
+ console.log(` Payout: ${chalk.green("$" + (result.payoutCents / 100).toFixed(2))}`);
289
+
290
+ if (failedSessions.length > 0) {
291
+ console.log(chalk.yellow(`\n${failedSessions.length} session(s) failed during submit:`));
292
+ failedSessions.slice(0, 3).forEach((session) => {
293
+ console.log(chalk.gray(` ${session.source.label}: ${session.error}`));
294
+ });
295
+ if (failedSessions.length > 3) {
296
+ console.log(chalk.gray(` ...and ${failedSessions.length - 3} more`));
297
+ }
298
+ }
200
299
  } catch (e) {
201
300
  uploadSpinner.fail("Submission failed");
202
301
  console.error(chalk.red(String(e)));
203
302
  process.exit(1);
204
303
  }
205
304
  }
305
+
306
+ function describeSessionStatus(session: PlannedSession): string {
307
+ if (session.readyChunks > 0 && session.pending) return "ready + pending";
308
+ if (session.readyChunks > 0) return "ready";
309
+ if (session.pending) return "pending";
310
+ return "up-to-date";
311
+ }
@@ -1,11 +1,13 @@
1
1
  import chalk from "chalk";
2
- import { loadConfig } from "../config.js";
2
+ import { loadConfig, resolveProfile } from "../config.js";
3
3
  import { ApiClient } from "../api-client.js";
4
+ import { loginCommandForProfile } from "../constants.js";
4
5
 
5
- export async function whoamiCommand(): Promise<void> {
6
- const config = loadConfig();
6
+ export async function whoamiCommand(opts: { profile?: string } = {}): Promise<void> {
7
+ const profile = resolveProfile(opts.profile);
8
+ const config = loadConfig(profile);
7
9
  if (!config) {
8
- console.error(chalk.red("Not registered. Run: trace register"));
10
+ console.error(chalk.red(`Not authenticated for profile '${profile}'. Run: ${loginCommandForProfile(profile)}`));
9
11
  process.exit(1);
10
12
  }
11
13
 
@@ -18,5 +20,7 @@ export async function whoamiCommand(): Promise<void> {
18
20
 
19
21
  const balance = (user.balance_cents ?? user.balanceCents ?? 0) / 100;
20
22
  console.log(chalk.bold(user.email));
23
+ console.log(chalk.gray("Profile:"), config.profile);
24
+ console.log(chalk.gray("Server:"), config.serverUrl);
21
25
  console.log(chalk.gray("Balance:"), chalk.green(`$${balance.toFixed(2)}`));
22
26
  }