@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.
- package/dist/api-client.d.ts +8 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +34 -0
- package/dist/api-client.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/auto-submit.d.ts +9 -0
- package/dist/commands/auto-submit.d.ts.map +1 -0
- package/dist/commands/auto-submit.js +111 -0
- package/dist/commands/auto-submit.js.map +1 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +125 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +32 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +69 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/register.d.ts +4 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +43 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/setup-hook.d.ts +6 -0
- package/dist/commands/setup-hook.d.ts.map +1 -0
- package/dist/commands/setup-hook.js +148 -0
- package/dist/commands/setup-hook.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +23 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/submit.d.ts +8 -0
- package/dist/commands/submit.d.ts.map +1 -0
- package/dist/commands/submit.js +149 -0
- package/dist/commands/submit.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +16 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +23 -0
- package/dist/config.js.map +1 -0
- package/dist/sessions.d.ts +16 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +107 -0
- package/dist/sessions.js.map +1 -0
- package/dist/submitter.d.ts +19 -0
- package/dist/submitter.d.ts.map +1 -0
- package/dist/submitter.js +65 -0
- package/dist/submitter.js.map +1 -0
- package/package.json +31 -0
- package/src/api-client.ts +33 -0
- package/src/cli.ts +82 -0
- package/src/commands/auto-submit.ts +95 -0
- package/src/commands/daemon.ts +128 -0
- package/src/commands/history.ts +50 -0
- package/src/commands/login.ts +75 -0
- package/src/commands/register.ts +52 -0
- package/src/commands/setup-hook.ts +175 -0
- package/src/commands/status.ts +26 -0
- package/src/commands/submit.ts +184 -0
- package/src/commands/whoami.ts +22 -0
- package/src/config.ts +29 -0
- package/src/sessions.ts +105 -0
- package/src/submitter.ts +89 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup-hook — installs trace auto-submit as a per-turn hook for AI coding tools.
|
|
3
|
+
* Run once; sessions are then captured automatically with no user action.
|
|
4
|
+
*
|
|
5
|
+
* Supported tools:
|
|
6
|
+
* --tool claude-code → writes ~/.claude/settings.json Stop hook (per-turn)
|
|
7
|
+
* --tool cursor → writes ~/.cursor/hooks.json stop hook (per-turn)
|
|
8
|
+
* --tool codex → writes ~/.codex/config.toml [[hooks]] after_agent (per-turn)
|
|
9
|
+
* (no flag) → installs for all detected tools
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { join } from "path";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
|
|
16
|
+
interface SetupHookOptions {
|
|
17
|
+
tool?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The command that hooks invoke — must be on PATH after `npm install -g`
|
|
21
|
+
const HOOK_COMMAND = "trace auto-submit";
|
|
22
|
+
|
|
23
|
+
export async function setupHookCommand(opts: SetupHookOptions): Promise<void> {
|
|
24
|
+
const tools = opts.tool ? [opts.tool] : detectInstalledTools();
|
|
25
|
+
|
|
26
|
+
if (tools.length === 0) {
|
|
27
|
+
console.log(chalk.yellow("No supported AI coding tools detected."));
|
|
28
|
+
console.log(chalk.gray("Install Claude Code, Cursor, or Codex CLI, then run setup-hook again."));
|
|
29
|
+
console.log(chalk.gray("Or specify: trace setup-hook --tool claude-code"));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const tool of tools) {
|
|
34
|
+
try {
|
|
35
|
+
switch (tool) {
|
|
36
|
+
case "claude-code":
|
|
37
|
+
setupClaudeCode();
|
|
38
|
+
break;
|
|
39
|
+
case "cursor":
|
|
40
|
+
setupCursor();
|
|
41
|
+
break;
|
|
42
|
+
case "codex":
|
|
43
|
+
setupCodex();
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
console.log(chalk.yellow(`Unknown tool: ${tool}. Supported: claude-code, cursor`));
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(chalk.red(`Failed to set up hook for ${tool}: ${err}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(chalk.gray(`\nHook logs: ~/.config/tracemarketplace/auto-submit.log`));
|
|
54
|
+
console.log(chalk.gray(`Remove hooks: trace remove-hook`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Claude Code ────────────────────────────────────────────────────────────
|
|
58
|
+
// Hook config: ~/.claude/settings.json
|
|
59
|
+
// Stop hook fires at session end with stdin: { session_id, transcript_path }
|
|
60
|
+
|
|
61
|
+
function setupClaudeCode() {
|
|
62
|
+
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
63
|
+
mkdirSync(join(homedir(), ".claude"), { recursive: true });
|
|
64
|
+
|
|
65
|
+
let settings: Record<string, unknown> = {};
|
|
66
|
+
if (existsSync(settingsPath)) {
|
|
67
|
+
try { settings = JSON.parse(readFileSync(settingsPath, "utf-8")); } catch {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const hooks = (settings.hooks as Record<string, unknown> | undefined) ?? {};
|
|
71
|
+
const stopHooks = (hooks["Stop"] as unknown[] | undefined) ?? [];
|
|
72
|
+
|
|
73
|
+
// Don't add if already present
|
|
74
|
+
const alreadyInstalled = stopHooks.some((h: unknown) => {
|
|
75
|
+
const entry = h as Record<string, unknown>;
|
|
76
|
+
const innerHooks = entry.hooks as Array<Record<string, unknown>> | undefined;
|
|
77
|
+
return innerHooks?.some((ih) => String(ih.command ?? "").includes("trace auto-submit"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!alreadyInstalled) {
|
|
81
|
+
stopHooks.push({
|
|
82
|
+
matcher: "",
|
|
83
|
+
hooks: [{ type: "command", command: HOOK_COMMAND }],
|
|
84
|
+
});
|
|
85
|
+
hooks["Stop"] = stopHooks;
|
|
86
|
+
settings.hooks = hooks;
|
|
87
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(chalk.green("✓ Claude Code") + chalk.gray(` — Stop hook installed in ${settingsPath}`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Cursor ─────────────────────────────────────────────────────────────────
|
|
94
|
+
// Hook config: ~/.cursor/hooks.json
|
|
95
|
+
// stop fires after each assistant turn with stdin: { sessionId, terminationReason, duration }
|
|
96
|
+
|
|
97
|
+
function setupCursor() {
|
|
98
|
+
const hooksPath = join(homedir(), ".cursor", "hooks.json");
|
|
99
|
+
mkdirSync(join(homedir(), ".cursor"), { recursive: true });
|
|
100
|
+
|
|
101
|
+
let hooks: unknown[] = [];
|
|
102
|
+
if (existsSync(hooksPath)) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(readFileSync(hooksPath, "utf-8"));
|
|
105
|
+
hooks = Array.isArray(parsed) ? parsed : (parsed.hooks ?? []);
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove any old sessionEnd entry for trace auto-submit (migration)
|
|
110
|
+
hooks = hooks.filter((h: unknown) => {
|
|
111
|
+
const entry = h as Record<string, unknown>;
|
|
112
|
+
return !(entry.event === "sessionEnd" && String(entry.command ?? "").includes("trace auto-submit"));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const alreadyInstalled = hooks.some((h: unknown) => {
|
|
116
|
+
const entry = h as Record<string, unknown>;
|
|
117
|
+
return entry.event === "stop" && String(entry.command ?? "").includes("trace auto-submit");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!alreadyInstalled) {
|
|
121
|
+
hooks.push({ event: "stop", command: HOOK_COMMAND, timeout: 30 });
|
|
122
|
+
writeFileSync(hooksPath, JSON.stringify(hooks, null, 2), "utf-8");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.green("✓ Cursor") + chalk.gray(` — stop hook installed in ${hooksPath}`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Codex CLI ───────────────────────────────────────────────────────────────
|
|
129
|
+
// Hook config: ~/.codex/config.toml
|
|
130
|
+
// after_agent fires after each agent turn with stdin:
|
|
131
|
+
// { "thread-id": "...", "turn-id": "...", "cwd": "...", "last-assistant-message": "..." }
|
|
132
|
+
// Uses TOML [[hooks]] array-of-tables format.
|
|
133
|
+
|
|
134
|
+
const CODEX_HOOK_MARKER = "# trace-marketplace-hook";
|
|
135
|
+
|
|
136
|
+
function setupCodex() {
|
|
137
|
+
const configPath = join(homedir(), ".codex", "config.toml");
|
|
138
|
+
mkdirSync(join(homedir(), ".codex"), { recursive: true });
|
|
139
|
+
|
|
140
|
+
let existing = "";
|
|
141
|
+
if (existsSync(configPath)) {
|
|
142
|
+
try { existing = readFileSync(configPath, "utf-8"); } catch {}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (existing.includes("trace auto-submit")) {
|
|
146
|
+
console.log(chalk.green("✓ Codex") + chalk.gray(` — after_agent hook already installed in ${configPath}`));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hookEntry = `\n${CODEX_HOOK_MARKER}\n[[hooks]]\nevent = "after_agent"\ncommand = "${HOOK_COMMAND} --tool codex"\n`;
|
|
151
|
+
writeFileSync(configPath, existing + hookEntry, "utf-8");
|
|
152
|
+
|
|
153
|
+
console.log(chalk.green("✓ Codex") + chalk.gray(` — after_agent hook installed in ${configPath}`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── Tool detection ──────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
function detectInstalledTools(): string[] {
|
|
159
|
+
const found: string[] = [];
|
|
160
|
+
|
|
161
|
+
if (existsSync(join(homedir(), ".claude"))) {
|
|
162
|
+
found.push("claude-code");
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
existsSync(join(homedir(), "Library", "Application Support", "Cursor")) ||
|
|
166
|
+
existsSync(join(homedir(), ".cursor"))
|
|
167
|
+
) {
|
|
168
|
+
found.push("cursor");
|
|
169
|
+
}
|
|
170
|
+
if (existsSync(join(homedir(), ".codex"))) {
|
|
171
|
+
found.push("codex");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return found;
|
|
175
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { ApiClient } from "../api-client.js";
|
|
4
|
+
|
|
5
|
+
export async function statusCommand(): 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 [me, subs] = await Promise.all([
|
|
14
|
+
client.get("/api/v1/me") as Promise<{ email: string; balanceCents?: number; balance_cents?: number }>,
|
|
15
|
+
client.get("/api/v1/submissions") as Promise<any[]>,
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const balance = ((me as any).balanceCents ?? (me as any).balance_cents ?? 0) / 100;
|
|
19
|
+
console.log(chalk.bold("Balance:"), chalk.green(`$${balance.toFixed(2)}`));
|
|
20
|
+
console.log(chalk.bold("Submissions:"), (subs as any[]).length);
|
|
21
|
+
|
|
22
|
+
const pending = (subs as any[]).filter((s) => s.acceptedCount === null);
|
|
23
|
+
if (pending.length > 0) {
|
|
24
|
+
console.log(chalk.yellow(`\n${pending.length} pending submission(s)`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import inquirer from "inquirer";
|
|
7
|
+
import { extractClaudeCode, extractCodex, extractCursor } 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";
|
|
12
|
+
|
|
13
|
+
interface SubmitOptions {
|
|
14
|
+
tool?: string;
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
since?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseSinceDays(since: string): number {
|
|
20
|
+
const match = since.match(/^(\d+)d$/);
|
|
21
|
+
return match ? parseInt(match[1], 10) : 30;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function submitCommand(opts: SubmitOptions): Promise<void> {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
if (!config) {
|
|
27
|
+
console.error(chalk.red("Not registered. Run: trace register"));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sinceDays = parseSinceDays(opts.since ?? "30d");
|
|
32
|
+
const spinner = ora("Discovering sessions...").start();
|
|
33
|
+
|
|
34
|
+
const traces: NormalizedTrace[] = [];
|
|
35
|
+
const errors: string[] = [];
|
|
36
|
+
|
|
37
|
+
const toolAlias: Record<string, string> = {
|
|
38
|
+
"claude-code": "claude_code",
|
|
39
|
+
"codex": "codex_cli",
|
|
40
|
+
};
|
|
41
|
+
const normalizedTool = opts.tool ? (toolAlias[opts.tool] ?? opts.tool) : null;
|
|
42
|
+
const tools = normalizedTool ? [normalizedTool] : ["claude_code", "codex_cli", "cursor"];
|
|
43
|
+
|
|
44
|
+
if (tools.includes("claude_code")) {
|
|
45
|
+
const files = findFiles("claude_code", sinceDays);
|
|
46
|
+
spinner.text = `Found ${files.length} Claude Code sessions`;
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
try {
|
|
49
|
+
traces.push(await extractClaudeCode(f, config.email));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
errors.push(`Claude Code ${f}: ${e}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (tools.includes("codex_cli")) {
|
|
57
|
+
const files = findFiles("codex_cli", sinceDays);
|
|
58
|
+
spinner.text = `Found ${files.length} Codex sessions`;
|
|
59
|
+
for (const f of files) {
|
|
60
|
+
try {
|
|
61
|
+
const { readFile } = await import("fs/promises");
|
|
62
|
+
const trace = await extractCodex(await readFile(f), config.email);
|
|
63
|
+
if (trace.turn_count > 0) traces.push(trace);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
errors.push(`Codex ${f}: ${e}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (tools.includes("cursor")) {
|
|
71
|
+
if (existsSync(CURSOR_DB_PATH)) {
|
|
72
|
+
try {
|
|
73
|
+
const { default: Database } = await import("better-sqlite3");
|
|
74
|
+
const db = new Database(CURSOR_DB_PATH, { readonly: true });
|
|
75
|
+
const rows = db.prepare("SELECT key FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all() as Array<{ key: string }>;
|
|
76
|
+
db.close();
|
|
77
|
+
spinner.text = `Found ${rows.length} Cursor sessions`;
|
|
78
|
+
for (const { key } of rows) {
|
|
79
|
+
const sessionId = key.replace("composerData:", "");
|
|
80
|
+
try {
|
|
81
|
+
traces.push(await extractCursor(CURSOR_DB_PATH, sessionId, config.email));
|
|
82
|
+
} catch (e) {
|
|
83
|
+
errors.push(`Cursor ${sessionId}: ${e}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
errors.push(`Cursor DB: ${e}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
spinner.stop();
|
|
93
|
+
|
|
94
|
+
if (errors.length > 0) {
|
|
95
|
+
console.log(chalk.yellow(`\n${errors.length} extraction error(s):`));
|
|
96
|
+
errors.slice(0, 3).forEach((e) => console.log(chalk.gray(` ${e}`)));
|
|
97
|
+
if (errors.length > 3) console.log(chalk.gray(` ...and ${errors.length - 3} more`));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (traces.length === 0) {
|
|
101
|
+
console.log(chalk.yellow("No sessions found in the last " + sinceDays + " days."));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Preview table
|
|
106
|
+
console.log(`\n${chalk.bold("Sessions to submit:")} (${traces.length} total)\n`);
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.gray(
|
|
109
|
+
" Tool".padEnd(16) +
|
|
110
|
+
"Session ID".padEnd(20) +
|
|
111
|
+
"Turns".padEnd(8) +
|
|
112
|
+
"Tokens".padEnd(12) +
|
|
113
|
+
"Est. payout"
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
console.log(chalk.gray(" " + "─".repeat(60)));
|
|
117
|
+
|
|
118
|
+
for (const t of traces.slice(0, 20)) {
|
|
119
|
+
const tokens = (t.total_input_tokens ?? 0) + (t.total_output_tokens ?? 0);
|
|
120
|
+
const estPayout = t.content_fidelity === "full" ? "$0.04" : "$0.02";
|
|
121
|
+
console.log(
|
|
122
|
+
" " +
|
|
123
|
+
t.source_tool.padEnd(16) +
|
|
124
|
+
t.source_session_id.slice(0, 18).padEnd(20) +
|
|
125
|
+
String(t.turn_count).padEnd(8) +
|
|
126
|
+
String(tokens || "—").padEnd(12) +
|
|
127
|
+
estPayout
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (traces.length > 20) {
|
|
131
|
+
console.log(chalk.gray(` ... and ${traces.length - 20} more`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (opts.dryRun) {
|
|
135
|
+
console.log(chalk.cyan("\nDry run — nothing submitted."));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const { confirm } = await inquirer.prompt([
|
|
140
|
+
{
|
|
141
|
+
type: "confirm",
|
|
142
|
+
name: "confirm",
|
|
143
|
+
message: `Submit ${traces.length} traces to ${config.serverUrl}?`,
|
|
144
|
+
default: true,
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
if (!confirm) {
|
|
149
|
+
console.log(chalk.gray("Cancelled."));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const uploadSpinner = ora(`Submitting ${traces.length} traces...`).start();
|
|
154
|
+
const client = new ApiClient(config.serverUrl, config.apiKey);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const result = (await client.post("/api/v1/traces/batch", {
|
|
158
|
+
traces,
|
|
159
|
+
source_tool: tools[0] ?? "mixed",
|
|
160
|
+
})) as {
|
|
161
|
+
submission_id: string;
|
|
162
|
+
accepted: number;
|
|
163
|
+
duplicate: number;
|
|
164
|
+
total: number;
|
|
165
|
+
traces: Array<{ payout_cents?: number }>;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
uploadSpinner.stop();
|
|
169
|
+
|
|
170
|
+
const totalPayout =
|
|
171
|
+
result.traces?.reduce((s, t) => s + (t.payout_cents ?? 0), 0) ?? 0;
|
|
172
|
+
|
|
173
|
+
console.log(chalk.green("\nSubmission complete!"));
|
|
174
|
+
console.log(` Accepted: ${chalk.bold(result.accepted)}`);
|
|
175
|
+
console.log(` Duplicates: ${chalk.gray(result.duplicate)}`);
|
|
176
|
+
console.log(` Total: ${result.total}`);
|
|
177
|
+
console.log(` Payout: ${chalk.green("$" + (totalPayout / 100).toFixed(2))}`);
|
|
178
|
+
console.log(chalk.gray(`\nSubmission ID: ${result.submission_id}`));
|
|
179
|
+
} catch (e) {
|
|
180
|
+
uploadSpinner.fail("Submission failed");
|
|
181
|
+
console.error(chalk.red(String(e)));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadConfig } from "../config.js";
|
|
3
|
+
import { ApiClient } from "../api-client.js";
|
|
4
|
+
|
|
5
|
+
export async function whoamiCommand(): 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 user = (await client.get("/api/v1/me")) as {
|
|
14
|
+
email: string;
|
|
15
|
+
balance_cents?: number;
|
|
16
|
+
balanceCents?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const balance = (user.balance_cents ?? user.balanceCents ?? 0) / 100;
|
|
20
|
+
console.log(chalk.bold(user.email));
|
|
21
|
+
console.log(chalk.gray("Balance:"), chalk.green(`$${balance.toFixed(2)}`));
|
|
22
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
serverUrl: string;
|
|
8
|
+
email: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getConfigPath(): string {
|
|
12
|
+
return join(homedir(), ".config", "tracemarketplace", "config.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadConfig(): Config | null {
|
|
16
|
+
const p = getConfigPath();
|
|
17
|
+
if (!existsSync(p)) return null;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(readFileSync(p, "utf-8")) as Config;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function saveConfig(config: Config): void {
|
|
26
|
+
const p = getConfigPath();
|
|
27
|
+
mkdirSync(join(homedir(), ".config", "tracemarketplace"), { recursive: true });
|
|
28
|
+
writeFileSync(p, JSON.stringify(config, null, 2), "utf-8");
|
|
29
|
+
}
|
package/src/sessions.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessions.ts — knows where each tool stores session files.
|
|
3
|
+
* Used by submit (batch discovery), auto-submit (hook path resolution), and daemon (watching).
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, statSync, existsSync, watch } from "fs";
|
|
6
|
+
import type { FSWatcher } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
export type ToolName = "claude_code" | "codex_cli" | "cursor";
|
|
11
|
+
|
|
12
|
+
// Root directories to watch per tool
|
|
13
|
+
export const SESSION_DIRS: Record<ToolName, string> = {
|
|
14
|
+
claude_code: join(homedir(), ".claude", "projects"),
|
|
15
|
+
codex_cli: join(homedir(), ".codex", "sessions"),
|
|
16
|
+
cursor: join(homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage"),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const CURSOR_DB_PATH = join(
|
|
20
|
+
homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb"
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** Walk a directory and return all .jsonl files modified within the cutoff. */
|
|
24
|
+
function walkJsonl(dir: string, cutoff = 0): string[] {
|
|
25
|
+
const results: string[] = [];
|
|
26
|
+
try {
|
|
27
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
const full = join(dir, entry.name);
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
results.push(...walkJsonl(full, cutoff));
|
|
31
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
32
|
+
try {
|
|
33
|
+
if (statSync(full).mtimeMs >= cutoff) results.push(full);
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
return results;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Find session files for a file-based tool (claude_code, codex_cli) within the last N days. */
|
|
42
|
+
export function findFiles(tool: "claude_code" | "codex_cli", sinceDays = 30): string[] {
|
|
43
|
+
const dir = SESSION_DIRS[tool];
|
|
44
|
+
if (!existsSync(dir)) return [];
|
|
45
|
+
const cutoff = Date.now() - sinceDays * 86400000;
|
|
46
|
+
return walkJsonl(dir, cutoff);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Find the most recently modified .jsonl file for claude_code (used when hook doesn't give path). */
|
|
50
|
+
export function findLatestFile(tool: "claude_code" | "codex_cli"): string | null {
|
|
51
|
+
const dir = SESSION_DIRS[tool];
|
|
52
|
+
if (!existsSync(dir)) return null;
|
|
53
|
+
const all = walkJsonl(dir, 0);
|
|
54
|
+
if (all.length === 0) return null;
|
|
55
|
+
return all.reduce((a, b) => {
|
|
56
|
+
try { return statSync(a).mtimeMs >= statSync(b).mtimeMs ? a : b; } catch { return a; }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Find a specific codex session file by UUID (the UUID appears in the filename). */
|
|
61
|
+
export function findCodexFileById(sessionId: string): string | null {
|
|
62
|
+
const dir = SESSION_DIRS["codex_cli"];
|
|
63
|
+
if (!existsSync(dir)) return null;
|
|
64
|
+
const files = walkJsonl(dir, 0);
|
|
65
|
+
return files.find(f => f.includes(sessionId)) ?? null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Watch session directories for new/changed .jsonl files.
|
|
70
|
+
* Calls onChange(tool, filePath) after a 5s debounce (lets writes settle).
|
|
71
|
+
* Returns a cleanup function.
|
|
72
|
+
*/
|
|
73
|
+
export function watchDirs(
|
|
74
|
+
tools: Array<"claude_code" | "codex_cli">,
|
|
75
|
+
onChange: (tool: "claude_code" | "codex_cli", filePath: string) => void
|
|
76
|
+
): () => void {
|
|
77
|
+
const watchers: FSWatcher[] = [];
|
|
78
|
+
const debounce = new Map<string, ReturnType<typeof setTimeout>>();
|
|
79
|
+
|
|
80
|
+
for (const tool of tools) {
|
|
81
|
+
const dir = SESSION_DIRS[tool];
|
|
82
|
+
if (!existsSync(dir)) continue;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const watcher = watch(dir, { recursive: true }, (_event, filename) => {
|
|
86
|
+
if (!filename || !filename.endsWith(".jsonl")) return;
|
|
87
|
+
const full = join(dir, filename);
|
|
88
|
+
|
|
89
|
+
// Debounce: wait 5s of no writes before treating the file as settled
|
|
90
|
+
const existing = debounce.get(full);
|
|
91
|
+
if (existing) clearTimeout(existing);
|
|
92
|
+
debounce.set(full, setTimeout(() => {
|
|
93
|
+
debounce.delete(full);
|
|
94
|
+
if (existsSync(full)) onChange(tool, full);
|
|
95
|
+
}, 5000));
|
|
96
|
+
});
|
|
97
|
+
watchers.push(watcher);
|
|
98
|
+
} catch {}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
for (const w of watchers) w.close();
|
|
103
|
+
for (const t of debounce.values()) clearTimeout(t);
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/submitter.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
import type { Config } from "./config.js";
|
|
10
|
+
|
|
11
|
+
export interface SubmitResult {
|
|
12
|
+
accepted: boolean;
|
|
13
|
+
superseded: boolean;
|
|
14
|
+
duplicate: boolean;
|
|
15
|
+
turnCount: number;
|
|
16
|
+
payoutCents: number;
|
|
17
|
+
traceId: string | null;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract a claude_code or codex_cli session from a file path and submit it.
|
|
23
|
+
*/
|
|
24
|
+
export async function submitFile(
|
|
25
|
+
tool: "claude_code" | "codex_cli",
|
|
26
|
+
filePath: string,
|
|
27
|
+
config: Config
|
|
28
|
+
): Promise<SubmitResult> {
|
|
29
|
+
let trace;
|
|
30
|
+
try {
|
|
31
|
+
if (tool === "claude_code") {
|
|
32
|
+
trace = await extractClaudeCode(filePath, config.email);
|
|
33
|
+
} else {
|
|
34
|
+
const buf = await readFile(filePath);
|
|
35
|
+
trace = await extractCodex(buf, config.email);
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { accepted: false, superseded: false, duplicate: false, turnCount: 0, payoutCents: 0, traceId: null, error: `Extraction failed: ${err}` };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (trace.turn_count === 0) {
|
|
42
|
+
return { accepted: false, superseded: false, duplicate: false, turnCount: 0, payoutCents: 0, traceId: null, error: "Empty session" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return submitTrace(trace, config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract a Cursor session by session ID and submit it.
|
|
50
|
+
*/
|
|
51
|
+
export async function submitCursorSession(
|
|
52
|
+
sessionId: string,
|
|
53
|
+
config: Config
|
|
54
|
+
): Promise<SubmitResult> {
|
|
55
|
+
let trace;
|
|
56
|
+
try {
|
|
57
|
+
trace = await extractCursor(CURSOR_DB_PATH, sessionId, config.email);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return { accepted: false, superseded: false, duplicate: false, turnCount: 0, payoutCents: 0, traceId: null, error: `Cursor extraction failed: ${err}` };
|
|
60
|
+
}
|
|
61
|
+
return submitTrace(trace, config);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function submitTrace(trace: Awaited<ReturnType<typeof extractClaudeCode>>, config: Config): Promise<SubmitResult> {
|
|
65
|
+
const client = new ApiClient(config.serverUrl, config.apiKey);
|
|
66
|
+
try {
|
|
67
|
+
const result = await client.post("/api/v1/traces/batch", {
|
|
68
|
+
traces: [trace],
|
|
69
|
+
source_tool: trace.source_tool,
|
|
70
|
+
}) as {
|
|
71
|
+
accepted: number;
|
|
72
|
+
duplicate: number;
|
|
73
|
+
superseded: number;
|
|
74
|
+
traces: Array<{ trace_id?: string; payout_cents?: number }>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const first = result.traces?.[0];
|
|
78
|
+
return {
|
|
79
|
+
accepted: result.accepted > 0,
|
|
80
|
+
superseded: result.superseded > 0,
|
|
81
|
+
duplicate: result.duplicate > 0,
|
|
82
|
+
turnCount: trace.turn_count,
|
|
83
|
+
payoutCents: first?.payout_cents ?? 0,
|
|
84
|
+
traceId: first?.trace_id ?? null,
|
|
85
|
+
};
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return { accepted: false, superseded: false, duplicate: false, turnCount: trace.turn_count, payoutCents: 0, traceId: null, error: `Submit failed: ${err}` };
|
|
88
|
+
}
|
|
89
|
+
}
|