@tracemarketplace/cli 0.0.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 (72) hide show
  1. package/dist/api-client.d.ts +8 -0
  2. package/dist/api-client.d.ts.map +1 -0
  3. package/dist/api-client.js +34 -0
  4. package/dist/api-client.js.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +69 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/auto-submit.d.ts +9 -0
  10. package/dist/commands/auto-submit.d.ts.map +1 -0
  11. package/dist/commands/auto-submit.js +111 -0
  12. package/dist/commands/auto-submit.js.map +1 -0
  13. package/dist/commands/daemon.d.ts +2 -0
  14. package/dist/commands/daemon.d.ts.map +1 -0
  15. package/dist/commands/daemon.js +125 -0
  16. package/dist/commands/daemon.js.map +1 -0
  17. package/dist/commands/history.d.ts +2 -0
  18. package/dist/commands/history.d.ts.map +1 -0
  19. package/dist/commands/history.js +32 -0
  20. package/dist/commands/history.js.map +1 -0
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.d.ts.map +1 -0
  23. package/dist/commands/login.js +69 -0
  24. package/dist/commands/login.js.map +1 -0
  25. package/dist/commands/register.d.ts +4 -0
  26. package/dist/commands/register.d.ts.map +1 -0
  27. package/dist/commands/register.js +43 -0
  28. package/dist/commands/register.js.map +1 -0
  29. package/dist/commands/setup-hook.d.ts +6 -0
  30. package/dist/commands/setup-hook.d.ts.map +1 -0
  31. package/dist/commands/setup-hook.js +148 -0
  32. package/dist/commands/setup-hook.js.map +1 -0
  33. package/dist/commands/status.d.ts +2 -0
  34. package/dist/commands/status.d.ts.map +1 -0
  35. package/dist/commands/status.js +23 -0
  36. package/dist/commands/status.js.map +1 -0
  37. package/dist/commands/submit.d.ts +8 -0
  38. package/dist/commands/submit.d.ts.map +1 -0
  39. package/dist/commands/submit.js +149 -0
  40. package/dist/commands/submit.js.map +1 -0
  41. package/dist/commands/whoami.d.ts +2 -0
  42. package/dist/commands/whoami.d.ts.map +1 -0
  43. package/dist/commands/whoami.js +16 -0
  44. package/dist/commands/whoami.js.map +1 -0
  45. package/dist/config.d.ts +9 -0
  46. package/dist/config.d.ts.map +1 -0
  47. package/dist/config.js +23 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/sessions.d.ts +16 -0
  50. package/dist/sessions.d.ts.map +1 -0
  51. package/dist/sessions.js +107 -0
  52. package/dist/sessions.js.map +1 -0
  53. package/dist/submitter.d.ts +19 -0
  54. package/dist/submitter.d.ts.map +1 -0
  55. package/dist/submitter.js +65 -0
  56. package/dist/submitter.js.map +1 -0
  57. package/package.json +31 -0
  58. package/src/api-client.ts +33 -0
  59. package/src/cli.ts +82 -0
  60. package/src/commands/auto-submit.ts +95 -0
  61. package/src/commands/daemon.ts +128 -0
  62. package/src/commands/history.ts +50 -0
  63. package/src/commands/login.ts +75 -0
  64. package/src/commands/register.ts +52 -0
  65. package/src/commands/setup-hook.ts +175 -0
  66. package/src/commands/status.ts +26 -0
  67. package/src/commands/submit.ts +184 -0
  68. package/src/commands/whoami.ts +22 -0
  69. package/src/config.ts +29 -0
  70. package/src/sessions.ts +105 -0
  71. package/src/submitter.ts +89 -0
  72. package/tsconfig.json +8 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * submitter.ts — extract one session file and submit it to the API.
3
+ * Shared by auto-submit (hook), submit (batch), and daemon (watch).
4
+ */
5
+ import { readFile } from "fs/promises";
6
+ import { extractClaudeCode, extractCodex, extractCursor } from "@tracemarketplace/shared";
7
+ import { ApiClient } from "./api-client.js";
8
+ import { CURSOR_DB_PATH } from "./sessions.js";
9
+ /**
10
+ * Extract a claude_code or codex_cli session from a file path and submit it.
11
+ */
12
+ export async function submitFile(tool, filePath, config) {
13
+ let trace;
14
+ try {
15
+ if (tool === "claude_code") {
16
+ trace = await extractClaudeCode(filePath, config.email);
17
+ }
18
+ else {
19
+ const buf = await readFile(filePath);
20
+ trace = await extractCodex(buf, config.email);
21
+ }
22
+ }
23
+ catch (err) {
24
+ return { accepted: false, superseded: false, duplicate: false, turnCount: 0, payoutCents: 0, traceId: null, error: `Extraction failed: ${err}` };
25
+ }
26
+ if (trace.turn_count === 0) {
27
+ return { accepted: false, superseded: false, duplicate: false, turnCount: 0, payoutCents: 0, traceId: null, error: "Empty session" };
28
+ }
29
+ return submitTrace(trace, config);
30
+ }
31
+ /**
32
+ * Extract a Cursor session by session ID and submit it.
33
+ */
34
+ export async function submitCursorSession(sessionId, config) {
35
+ let trace;
36
+ try {
37
+ trace = await extractCursor(CURSOR_DB_PATH, sessionId, config.email);
38
+ }
39
+ catch (err) {
40
+ return { accepted: false, superseded: false, duplicate: false, turnCount: 0, payoutCents: 0, traceId: null, error: `Cursor extraction failed: ${err}` };
41
+ }
42
+ return submitTrace(trace, config);
43
+ }
44
+ async function submitTrace(trace, config) {
45
+ const client = new ApiClient(config.serverUrl, config.apiKey);
46
+ try {
47
+ const result = await client.post("/api/v1/traces/batch", {
48
+ traces: [trace],
49
+ source_tool: trace.source_tool,
50
+ });
51
+ const first = result.traces?.[0];
52
+ return {
53
+ accepted: result.accepted > 0,
54
+ superseded: result.superseded > 0,
55
+ duplicate: result.duplicate > 0,
56
+ turnCount: trace.turn_count,
57
+ payoutCents: first?.payout_cents ?? 0,
58
+ traceId: first?.trace_id ?? null,
59
+ };
60
+ }
61
+ catch (err) {
62
+ return { accepted: false, superseded: false, duplicate: false, turnCount: trace.turn_count, payoutCents: 0, traceId: null, error: `Submit failed: ${err}` };
63
+ }
64
+ }
65
+ //# sourceMappingURL=submitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submitter.js","sourceRoot":"","sources":["../src/submitter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAa/C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAiC,EACjC,QAAgB,EAChB,MAAc;IAEd,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,IAAI,IAAI,KAAK,aAAa,EAAE,CAAC;YAC3B,KAAK,GAAG,MAAM,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1D,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACrC,KAAK,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,sBAAsB,GAAG,EAAE,EAAE,CAAC;IACnJ,CAAC;IAED,IAAI,KAAK,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IACvI,CAAC;IAED,OAAO,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,SAAiB,EACjB,MAAc;IAEd,IAAI,KAAK,CAAC;IACV,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,aAAa,CAAC,cAAc,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,6BAA6B,GAAG,EAAE,EAAE,CAAC;IAC1J,CAAC;IACD,OAAO,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,KAAoD,EAAE,MAAc;IAC7F,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE;YACvD,MAAM,EAAE,CAAC,KAAK,CAAC;YACf,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B,CAKA,CAAC;QAEF,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;QACjC,OAAO;YACL,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,CAAC;YAC7B,UAAU,EAAE,MAAM,CAAC,UAAU,GAAG,CAAC;YACjC,SAAS,EAAE,MAAM,CAAC,SAAS,GAAG,CAAC;YAC/B,SAAS,EAAE,KAAK,CAAC,UAAU;YAC3B,WAAW,EAAE,KAAK,EAAE,YAAY,IAAI,CAAC;YACrC,OAAO,EAAE,KAAK,EAAE,QAAQ,IAAI,IAAI;SACjC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,UAAU,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,kBAAkB,GAAG,EAAE,EAAE,CAAC;IAC9J,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@tracemarketplace/cli",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "trace": "dist/cli.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsx src/cli.ts",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@tracemarketplace/shared": "workspace:*",
15
+ "better-sqlite3": "^9.4.0",
16
+ "chalk": "^5.3.0",
17
+ "commander": "^12.0.0",
18
+ "fzstd": "^0.1.1",
19
+ "inquirer": "^9.2.0",
20
+ "open": "^11.0.0",
21
+ "ora": "^8.0.0",
22
+ "p-limit": "^5.0.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/better-sqlite3": "^7.6.0",
26
+ "@types/inquirer": "^9.0.7",
27
+ "@types/node": "^20.0.0",
28
+ "tsx": "^4.7.0",
29
+ "typescript": "^5.4.0"
30
+ }
31
+ }
@@ -0,0 +1,33 @@
1
+ export class ApiClient {
2
+ constructor(
3
+ private baseUrl: string,
4
+ private apiKey: string
5
+ ) {}
6
+
7
+ async get(path: string): Promise<unknown> {
8
+ const res = await fetch(`${this.baseUrl}${path}`, {
9
+ headers: { "X-Api-Key": this.apiKey },
10
+ });
11
+ if (!res.ok) {
12
+ const text = await res.text();
13
+ throw new Error(`API error ${res.status}: ${text}`);
14
+ }
15
+ return res.json();
16
+ }
17
+
18
+ async post(path: string, body: unknown): Promise<unknown> {
19
+ const res = await fetch(`${this.baseUrl}${path}`, {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ "X-Api-Key": this.apiKey,
24
+ },
25
+ body: JSON.stringify(body),
26
+ });
27
+ if (!res.ok) {
28
+ const text = await res.text();
29
+ throw new Error(`API error ${res.status}: ${text}`);
30
+ }
31
+ return res.json();
32
+ }
33
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ import { program } from "commander";
3
+ import { loginCommand } from "./commands/login.js";
4
+ import { registerCommand } from "./commands/register.js";
5
+ import { whoamiCommand } from "./commands/whoami.js";
6
+ import { submitCommand } from "./commands/submit.js";
7
+ import { statusCommand } from "./commands/status.js";
8
+ import { historyCommand } from "./commands/history.js";
9
+ import { autoSubmitCommand } from "./commands/auto-submit.js";
10
+ import { setupHookCommand } from "./commands/setup-hook.js";
11
+ import { daemonCommand } from "./commands/daemon.js";
12
+
13
+ program
14
+ .name("trace")
15
+ .description("Trace Marketplace CLI — submit AI coding sessions, get paid")
16
+ .version("0.0.1");
17
+
18
+ program
19
+ .command("login")
20
+ .description("Sign in via browser — opens tracemarketplace.dev, no key needed")
21
+ .action(() => loginCommand().catch(handleError));
22
+
23
+ program
24
+ .command("register")
25
+ .description("Register your email and get an API key")
26
+ .option("--server-url <url>", "Server URL (default: https://api.tracemarketplace.dev)")
27
+ .action((opts) => registerCommand({ serverUrl: opts.serverUrl }).catch(handleError));
28
+
29
+ program
30
+ .command("whoami")
31
+ .description("Show your account info and balance")
32
+ .action(() => whoamiCommand().catch(handleError));
33
+
34
+ program
35
+ .command("submit")
36
+ .description("Auto-detect and submit traces from Claude Code, Codex CLI, and Cursor")
37
+ .option("--tool <tool>", "Only submit from specific tool (claude-code|codex|cursor)")
38
+ .option("--dry-run", "Preview without submitting")
39
+ .option("--since <duration>", "Only include sessions from last N days (e.g. 30d)", "30d")
40
+ .action((opts) =>
41
+ submitCommand({
42
+ tool: opts.tool,
43
+ dryRun: opts.dryRun,
44
+ since: opts.since,
45
+ }).catch(handleError)
46
+ );
47
+
48
+ program
49
+ .command("status")
50
+ .description("Show pending submissions and balance")
51
+ .action(() => statusCommand().catch(handleError));
52
+
53
+ program
54
+ .command("history")
55
+ .description("Show submission history")
56
+ .action(() => historyCommand().catch(handleError));
57
+
58
+ program
59
+ .command("auto-submit")
60
+ .description("Submit the current session (called by tool hooks — do not run manually)")
61
+ .option("--tool <tool>", "Tool that triggered the hook (claude-code|cursor|codex)")
62
+ .option("--session <id>", "Session ID (for cursor/codex hooks)")
63
+ .option("--file <path>", "Direct path to session file (for claude-code)")
64
+ .action((opts) => autoSubmitCommand({ tool: opts.tool, session: opts.session, file: opts.file }));
65
+
66
+ program
67
+ .command("daemon")
68
+ .description("Watch session directories and auto-submit new sessions as they appear")
69
+ .action(() => daemonCommand().catch(handleError));
70
+
71
+ program
72
+ .command("setup-hook")
73
+ .description("Install session-end hooks for Claude Code, Cursor, and Codex CLI")
74
+ .option("--tool <tool>", "Only set up hook for specific tool (claude-code|cursor|codex)")
75
+ .action((opts) => setupHookCommand({ tool: opts.tool }).catch(handleError));
76
+
77
+ function handleError(e: unknown) {
78
+ console.error((e as Error).message ?? String(e));
79
+ process.exit(1);
80
+ }
81
+
82
+ program.parse();
@@ -0,0 +1,95 @@
1
+ /**
2
+ * auto-submit — called by tool hooks (Claude Code Stop, Cursor sessionEnd).
3
+ * Non-interactive: never prompts, always exits 0 so it never blocks the user's tool.
4
+ * Logs results to ~/.config/tracemarketplace/auto-submit.log
5
+ */
6
+ import { readFileSync, appendFileSync, mkdirSync } from "fs";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ import { loadConfig } from "../config.js";
10
+ import { findLatestFile, findCodexFileById } from "../sessions.js";
11
+ import { submitFile, submitCursorSession } from "../submitter.js";
12
+
13
+ interface AutoSubmitOptions {
14
+ tool?: string;
15
+ session?: string; // session ID (for cursor)
16
+ file?: string; // direct path (for claude-code when not via hook)
17
+ }
18
+
19
+ export function log(msg: string) {
20
+ const dir = join(homedir(), ".config", "tracemarketplace");
21
+ mkdirSync(dir, { recursive: true });
22
+ try {
23
+ appendFileSync(join(dir, "auto-submit.log"), `[${new Date().toISOString()}] ${msg}\n`);
24
+ } catch {}
25
+ }
26
+
27
+ export async function autoSubmitCommand(opts: AutoSubmitOptions): Promise<void> {
28
+ try { await run(opts); } catch (err) { log(`ERROR: ${String(err)}`); }
29
+ }
30
+
31
+ async function run(opts: AutoSubmitOptions): Promise<void> {
32
+ const config = loadConfig();
33
+ if (!config) { log("Not registered — run: trace register"); return; }
34
+
35
+ // Read hook payload from stdin
36
+ let hookPayload: Record<string, unknown> = {};
37
+ try {
38
+ const raw = readFileSync("/dev/stdin", "utf-8").trim();
39
+ if (raw) hookPayload = JSON.parse(raw);
40
+ } catch {}
41
+
42
+ const tool = opts.tool ?? inferTool(hookPayload);
43
+ if (!tool) { log("Could not determine tool"); return; }
44
+
45
+ log(`auto-submit triggered for tool=${tool}`);
46
+
47
+ if (tool === "claude-code" || tool === "claude_code") {
48
+ // Claude Code Stop hook sends { session_id, transcript_path }
49
+ const filePath = opts.file
50
+ ?? hookPayload["transcript_path"] as string
51
+ ?? findLatestFile("claude_code");
52
+
53
+ if (!filePath) { log("Claude Code: no session file found"); return; }
54
+
55
+ const result = await submitFile("claude_code", filePath, config);
56
+ logResult(result, filePath);
57
+
58
+ } else if (tool === "cursor") {
59
+ const sessionId = opts.session ?? hookPayload["sessionId"] as string;
60
+ if (!sessionId) { log("Cursor: no sessionId"); return; }
61
+
62
+ const result = await submitCursorSession(sessionId, config);
63
+ logResult(result, sessionId);
64
+
65
+ } else if (tool === "codex" || tool === "codex_cli") {
66
+ // after_agent payload: { "thread-id": "...", "turn-id": "...", "cwd": "...", "last-assistant-message": "..." }
67
+ // Legacy / manual: session_path or session_id
68
+ const threadId = hookPayload["thread-id"] as string ?? "";
69
+ const filePath = opts.file
70
+ ?? hookPayload["session_path"] as string
71
+ ?? findCodexFileById(opts.session ?? hookPayload["session_id"] as string ?? threadId ?? "");
72
+
73
+ if (!filePath) { log("Codex: no session file found"); return; }
74
+
75
+ const result = await submitFile("codex_cli", filePath, config);
76
+ logResult(result, filePath);
77
+
78
+ } else {
79
+ log(`Unknown tool: ${tool}`);
80
+ }
81
+ }
82
+
83
+ function logResult(result: Awaited<ReturnType<typeof submitFile>>, label: string) {
84
+ if (result.error) { log(`${label}: ${result.error}`); return; }
85
+ if (result.duplicate) { log(`${label}: already captured — skipped`); return; }
86
+ if (result.superseded) { log(`${label}: updated (${result.turnCount} turns) — $${(result.payoutCents / 100).toFixed(2)}`); return; }
87
+ log(`${label}: accepted (${result.turnCount} turns) — $${(result.payoutCents / 100).toFixed(2)}`);
88
+ }
89
+
90
+ function inferTool(payload: Record<string, unknown>): string | null {
91
+ if (payload["transcript_path"]) return "claude-code";
92
+ if (payload["terminationReason"] || payload["sessionId"]) return "cursor";
93
+ if (payload["session_path"] || payload["thread-id"]) return "codex";
94
+ return null;
95
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * trace daemon — watches ~/.claude and ~/.codex session dirs and auto-submits on change.
3
+ * Keeps a state file so already-submitted unchanged files are skipped on restart.
4
+ *
5
+ * State: ~/.config/tracemarketplace/daemon-state.json
6
+ * { [filePath]: { mtime: number, size: number } }
7
+ *
8
+ * On startup: backfills any files newer than last run.
9
+ * While running: submits files 5s after they stop changing (debounce in watchDirs).
10
+ * Server handles dedup/supersession — daemon just fires on change.
11
+ */
12
+ import { readFileSync, writeFileSync, mkdirSync, statSync, existsSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { join } from "path";
15
+ import chalk from "chalk";
16
+ import { loadConfig } from "../config.js";
17
+ import { findFiles, watchDirs } from "../sessions.js";
18
+ import { submitFile } from "../submitter.js";
19
+ import { log } from "./auto-submit.js";
20
+
21
+ const STATE_PATH = join(homedir(), ".config", "tracemarketplace", "daemon-state.json");
22
+
23
+ type DaemonState = Record<string, { mtime: number; size: number }>;
24
+
25
+ function loadState(): DaemonState {
26
+ try { return JSON.parse(readFileSync(STATE_PATH, "utf-8")); } catch { return {}; }
27
+ }
28
+
29
+ function saveState(state: DaemonState) {
30
+ mkdirSync(join(homedir(), ".config", "tracemarketplace"), { recursive: true });
31
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
32
+ }
33
+
34
+ function hasChanged(filePath: string, state: DaemonState): boolean {
35
+ try {
36
+ const { mtimeMs, size } = statSync(filePath);
37
+ const recorded = state[filePath];
38
+ if (!recorded) return true;
39
+ return mtimeMs !== recorded.mtime || size !== recorded.size;
40
+ } catch { return false; }
41
+ }
42
+
43
+ function recordFile(filePath: string, state: DaemonState): DaemonState {
44
+ try {
45
+ const { mtimeMs, size } = statSync(filePath);
46
+ return { ...state, [filePath]: { mtime: mtimeMs, size } };
47
+ } catch { return state; }
48
+ }
49
+
50
+ async function processFile(
51
+ tool: "claude_code" | "codex_cli",
52
+ filePath: string,
53
+ state: DaemonState
54
+ ): Promise<DaemonState> {
55
+ const config = loadConfig();
56
+ if (!config) return state;
57
+
58
+ const result = await submitFile(tool, filePath, config);
59
+ const updated = recordFile(filePath, state);
60
+
61
+ if (result.error && result.error !== "Empty session") {
62
+ log(`daemon: ${filePath}: ${result.error}`);
63
+ } else if (result.duplicate) {
64
+ // Already current — no log noise
65
+ } else if (result.superseded) {
66
+ log(`daemon: updated ${filePath} (${result.turnCount} turns) +$${(result.payoutCents / 100).toFixed(2)}`);
67
+ console.log(chalk.cyan(` ↑ updated`), chalk.gray(filePath.split("/").slice(-2).join("/")), chalk.green(`+$${(result.payoutCents / 100).toFixed(2)}`));
68
+ } else if (result.accepted) {
69
+ log(`daemon: accepted ${filePath} (${result.turnCount} turns) +$${(result.payoutCents / 100).toFixed(2)}`);
70
+ console.log(chalk.green(` ✓ new`), chalk.gray(filePath.split("/").slice(-2).join("/")), chalk.green(`+$${(result.payoutCents / 100).toFixed(2)}`));
71
+ }
72
+
73
+ return updated;
74
+ }
75
+
76
+ export async function daemonCommand(): Promise<void> {
77
+ const config = loadConfig();
78
+ if (!config) {
79
+ console.error(chalk.red("Not registered. Run: trace register"));
80
+ process.exit(1);
81
+ }
82
+
83
+ const tools: Array<"claude_code" | "codex_cli"> = [];
84
+ if (existsSync(join(homedir(), ".claude"))) tools.push("claude_code");
85
+ if (existsSync(join(homedir(), ".codex", "sessions"))) tools.push("codex_cli");
86
+
87
+ if (tools.length === 0) {
88
+ console.log(chalk.yellow("No supported tools detected (Claude Code, Codex)."));
89
+ return;
90
+ }
91
+
92
+ console.log(chalk.bold("Trace daemon starting"));
93
+ console.log(chalk.gray(`Watching: ${tools.join(", ")}`));
94
+ console.log(chalk.gray("Press Ctrl+C to stop\n"));
95
+
96
+ let state = loadState();
97
+
98
+ // Backfill: submit any files from the last 7 days that have changed since last run
99
+ console.log(chalk.gray("Backfilling new/changed sessions..."));
100
+ let backfilled = 0;
101
+ for (const tool of tools) {
102
+ for (const filePath of findFiles(tool, 7)) {
103
+ if (hasChanged(filePath, state)) {
104
+ state = await processFile(tool, filePath, state);
105
+ backfilled++;
106
+ }
107
+ }
108
+ }
109
+ saveState(state);
110
+ if (backfilled === 0) console.log(chalk.gray(" Nothing new.\n"));
111
+ else console.log();
112
+
113
+ // Watch for ongoing changes
114
+ const stop = watchDirs(tools, async (tool, filePath) => {
115
+ if (!hasChanged(filePath, state)) return;
116
+ state = await processFile(tool, filePath, state);
117
+ saveState(state);
118
+ });
119
+
120
+ console.log(chalk.gray("Watching for new sessions...\n"));
121
+
122
+ // Graceful shutdown
123
+ process.on("SIGINT", () => { stop(); console.log(chalk.gray("\nDaemon stopped.")); process.exit(0); });
124
+ process.on("SIGTERM", () => { stop(); process.exit(0); });
125
+
126
+ // Keep alive
127
+ await new Promise(() => {});
128
+ }
@@ -0,0 +1,50 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig } from "../config.js";
3
+ import { ApiClient } from "../api-client.js";
4
+
5
+ export async function historyCommand(): Promise<void> {
6
+ const config = loadConfig();
7
+ if (!config) {
8
+ console.error(chalk.red("Not registered. Run: trace register"));
9
+ process.exit(1);
10
+ }
11
+
12
+ const client = new ApiClient(config.serverUrl, config.apiKey);
13
+ const subs = (await client.get("/api/v1/submissions")) as Array<{
14
+ id: string;
15
+ sourceTool: string;
16
+ submittedAt: string | number;
17
+ sessionCount: number;
18
+ acceptedCount: number | null;
19
+ totalPayoutCents: number | null;
20
+ }>;
21
+
22
+ if (subs.length === 0) {
23
+ console.log(chalk.gray("No submissions yet."));
24
+ return;
25
+ }
26
+
27
+ console.log(
28
+ chalk.gray(
29
+ "Date".padEnd(14) +
30
+ "Tool".padEnd(16) +
31
+ "Accepted".padEnd(12) +
32
+ "Payout"
33
+ )
34
+ );
35
+ console.log(chalk.gray("─".repeat(50)));
36
+
37
+ for (const s of subs) {
38
+ const date = new Date(s.submittedAt).toLocaleDateString();
39
+ const payout =
40
+ s.totalPayoutCents !== null
41
+ ? "$" + (s.totalPayoutCents / 100).toFixed(2)
42
+ : "pending";
43
+ console.log(
44
+ date.padEnd(14) +
45
+ s.sourceTool.padEnd(16) +
46
+ `${s.acceptedCount ?? "—"}/${s.sessionCount}`.padEnd(12) +
47
+ chalk.green(payout)
48
+ );
49
+ }
50
+ }
@@ -0,0 +1,75 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import open from "open";
4
+ import { loadConfig, saveConfig } from "../config.js";
5
+ import { ApiClient } from "../api-client.js";
6
+
7
+ const POLL_INTERVAL = 2000;
8
+ const POLL_TIMEOUT = 10 * 60 * 1000; // 10 min
9
+
10
+ export async function loginCommand() {
11
+ const config = loadConfig();
12
+ const serverUrl = config?.serverUrl ?? "https://api.tracemarketplace.dev";
13
+ const client = new ApiClient(serverUrl, config?.apiKey ?? "");
14
+
15
+ // Step 1: init CLI session
16
+ const spinner = ora("Initializing...").start();
17
+ let token: string;
18
+ let loginUrl: string;
19
+ try {
20
+ const res = await client.post("/api/v1/auth/cli-init", {}) as { token: string; loginUrl: string };
21
+ token = res.token;
22
+ loginUrl = res.loginUrl;
23
+ spinner.stop();
24
+ } catch (e: any) {
25
+ spinner.fail("Failed to connect to server");
26
+ throw e;
27
+ }
28
+
29
+ // Step 2: open browser
30
+ console.log(chalk.dim("\nOpening browser to sign in...\n"));
31
+ console.log(chalk.dim(` ${loginUrl}\n`));
32
+ console.log(chalk.dim("If the browser didn't open, copy the URL above.\n"));
33
+
34
+ try {
35
+ await open(loginUrl);
36
+ } catch {
37
+ // Browser open failed — user can copy the URL
38
+ }
39
+
40
+ // Step 3: poll for completion
41
+ const pollSpinner = ora("Waiting for authentication...").start();
42
+ const deadline = Date.now() + POLL_TIMEOUT;
43
+
44
+ while (Date.now() < deadline) {
45
+ await sleep(POLL_INTERVAL);
46
+ try {
47
+ const res = await client.get(`/api/v1/auth/cli-poll?token=${token}`) as { pending?: boolean; apiKey?: string };
48
+ if (res.pending) continue;
49
+ if (res.apiKey) {
50
+ pollSpinner.succeed("Authenticated");
51
+
52
+ // Get user info to confirm
53
+ const infoClient = new ApiClient(serverUrl, res.apiKey);
54
+ const me = await infoClient.get("/api/v1/me") as { email: string };
55
+
56
+ saveConfig({ apiKey: res.apiKey, serverUrl, email: me.email });
57
+ console.log(chalk.green(`\n✓ Logged in as ${me.email}\n`));
58
+ return;
59
+ }
60
+ } catch (e: any) {
61
+ if (e.message?.includes("Expired")) {
62
+ pollSpinner.fail("Login timed out. Run trace login again.");
63
+ process.exit(1);
64
+ }
65
+ // Transient error — keep polling
66
+ }
67
+ }
68
+
69
+ pollSpinner.fail("Timed out. Run trace login again.");
70
+ process.exit(1);
71
+ }
72
+
73
+ function sleep(ms: number) {
74
+ return new Promise((r) => setTimeout(r, ms));
75
+ }
@@ -0,0 +1,52 @@
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://api.tracemarketplace.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
+ }