@tracemarketplace/cli 0.0.13 → 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 +85 -41
  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 +93 -43
  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,16 +1,28 @@
1
1
  import chalk from "chalk";
2
2
  import ora from "ora";
3
3
  import open from "open";
4
- import { loadConfig, saveConfig } from "../config.js";
4
+ import { loadConfig, resolveProfile, saveConfig } from "../config.js";
5
5
  import { ApiClient } from "../api-client.js";
6
+ import {
7
+ DEFAULT_PROFILE,
8
+ defaultServerUrlForProfile,
9
+ inferProfileFromServerUrl,
10
+ loginCommandForProfile,
11
+ } from "../constants.js";
6
12
 
7
13
  const POLL_INTERVAL = 2000;
8
14
  const POLL_TIMEOUT = 10 * 60 * 1000; // 10 min
9
15
 
10
- export async function loginCommand() {
11
- const config = loadConfig();
12
- const serverUrl = config?.serverUrl ?? "https://trace-marketplace-api.fly.dev";
13
- const client = new ApiClient(serverUrl, config?.apiKey ?? "");
16
+ export interface LoginOptions {
17
+ profile?: string;
18
+ serverUrl?: string;
19
+ }
20
+
21
+ export async function loginCommand(opts: LoginOptions = {}) {
22
+ const profile = resolveLoginProfile(opts);
23
+ const config = loadConfig(profile);
24
+ const serverUrl = opts.serverUrl ?? config?.serverUrl ?? defaultServerUrlForProfile(profile);
25
+ const client = new ApiClient(serverUrl);
14
26
 
15
27
  // Step 1: init CLI session
16
28
  const spinner = ora("Initializing...").start();
@@ -53,23 +65,39 @@ export async function loginCommand() {
53
65
  const infoClient = new ApiClient(serverUrl, res.apiKey);
54
66
  const me = await infoClient.get("/api/v1/me") as { email: string };
55
67
 
56
- saveConfig({ apiKey: res.apiKey, serverUrl, email: me.email });
57
- console.log(chalk.green(`\n✓ Logged in as ${me.email}\n`));
68
+ const saved = saveConfig(
69
+ { apiKey: res.apiKey, serverUrl, email: me.email },
70
+ {
71
+ profile,
72
+ setDefault: profile === DEFAULT_PROFILE && !opts.profile && !process.env.TRACEMP_PROFILE,
73
+ }
74
+ );
75
+
76
+ console.log(chalk.green(`\n✓ Logged in as ${me.email}`));
77
+ console.log(chalk.gray(` Profile: ${saved.profile}`));
78
+ console.log(chalk.gray(` Server: ${saved.serverUrl}\n`));
58
79
  return;
59
80
  }
60
81
  } catch (e: any) {
61
82
  if (e.message?.includes("Expired")) {
62
- pollSpinner.fail("Login timed out. Run trace login again.");
83
+ pollSpinner.fail(`Login timed out. Run ${loginCommandForProfile(profile)} again.`);
63
84
  process.exit(1);
64
85
  }
65
86
  // Transient error — keep polling
66
87
  }
67
88
  }
68
89
 
69
- pollSpinner.fail("Timed out. Run trace login again.");
90
+ pollSpinner.fail(`Timed out. Run ${loginCommandForProfile(profile)} again.`);
70
91
  process.exit(1);
71
92
  }
72
93
 
73
94
  function sleep(ms: number) {
74
95
  return new Promise((r) => setTimeout(r, ms));
75
96
  }
97
+
98
+ function resolveLoginProfile(opts: LoginOptions): string {
99
+ if (opts.profile) return resolveProfile(opts.profile);
100
+ if (process.env.TRACEMP_PROFILE) return resolveProfile();
101
+ if (opts.serverUrl) return inferProfileFromServerUrl(opts.serverUrl);
102
+ return resolveProfile();
103
+ }
@@ -1,52 +1,8 @@
1
- import inquirer from "inquirer";
2
1
  import chalk from "chalk";
3
- import { saveConfig } from "../config.js";
2
+ import { CLI_NAME } from "../constants.js";
3
+ import { loginCommand } from "./login.js";
4
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"));
5
+ export async function registerCommand(opts: { profile?: string; serverUrl?: string }): Promise<void> {
6
+ console.log(chalk.yellow(`\`${CLI_NAME} register\` is now an alias for \`${CLI_NAME} login\`.`));
7
+ await loginCommand({ profile: opts.profile, serverUrl: opts.serverUrl });
52
8
  }
@@ -0,0 +1,194 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import chalk from "chalk";
5
+ import { CODEX_HOOK_MARKER } from "./setup-hook.js";
6
+
7
+ interface RemoveHookOptions {
8
+ tool?: string;
9
+ }
10
+
11
+ const LEGACY_CODEX_HOOK_MARKER = "# trace-marketplace-hook";
12
+
13
+ export async function removeHookCommand(opts: RemoveHookOptions): Promise<void> {
14
+ const tools = opts.tool ? [opts.tool] : ["claude-code", "cursor", "codex"];
15
+ let removedAny = false;
16
+
17
+ for (const tool of tools) {
18
+ try {
19
+ let removed = false;
20
+ switch (tool) {
21
+ case "claude-code":
22
+ removed = removeClaudeCode();
23
+ break;
24
+ case "cursor":
25
+ removed = removeCursor();
26
+ break;
27
+ case "codex":
28
+ removed = removeCodex();
29
+ break;
30
+ default:
31
+ console.log(chalk.yellow(`Unknown tool: ${tool}. Supported: claude-code, cursor, codex`));
32
+ continue;
33
+ }
34
+
35
+ removedAny = removedAny || removed;
36
+ } catch (err) {
37
+ console.error(chalk.red(`Failed to remove hook for ${tool}: ${err}`));
38
+ }
39
+ }
40
+
41
+ if (!removedAny) {
42
+ console.log(chalk.gray("No tracemp hooks found."));
43
+ }
44
+ }
45
+
46
+ function removeClaudeCode(): boolean {
47
+ const settingsPath = join(homedir(), ".claude", "settings.json");
48
+ if (!existsSync(settingsPath)) {
49
+ console.log(chalk.gray(`Claude Code — no settings file at ${settingsPath}`));
50
+ return false;
51
+ }
52
+
53
+ let settings: Record<string, unknown>;
54
+ try {
55
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
56
+ } catch {
57
+ console.log(chalk.yellow(`Claude Code — could not parse ${settingsPath}`));
58
+ return false;
59
+ }
60
+
61
+ const hooks = (settings.hooks as Record<string, unknown> | undefined) ?? {};
62
+ const stopHooks = (hooks["Stop"] as unknown[] | undefined) ?? [];
63
+ let removed = false;
64
+
65
+ const nextStopHooks = stopHooks
66
+ .map((h) => {
67
+ const entry = { ...(h as Record<string, unknown>) };
68
+ const innerHooks = Array.isArray(entry.hooks) ? entry.hooks as Array<Record<string, unknown>> : [];
69
+ const nextInnerHooks = innerHooks.filter((ih) => !String(ih.command ?? "").includes("auto-submit"));
70
+ if (nextInnerHooks.length !== innerHooks.length) removed = true;
71
+ entry.hooks = nextInnerHooks;
72
+ return entry;
73
+ })
74
+ .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
75
+
76
+ if (!removed) {
77
+ console.log(chalk.gray("Claude Code — no tracemp hook found"));
78
+ return false;
79
+ }
80
+
81
+ if (nextStopHooks.length > 0) {
82
+ hooks["Stop"] = nextStopHooks;
83
+ settings.hooks = hooks;
84
+ } else {
85
+ delete hooks["Stop"];
86
+ if (Object.keys(hooks).length > 0) {
87
+ settings.hooks = hooks;
88
+ } else {
89
+ delete settings.hooks;
90
+ }
91
+ }
92
+
93
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
94
+ console.log(chalk.green("✓ Claude Code") + chalk.gray(` — tracemp hook removed from ${settingsPath}`));
95
+ return true;
96
+ }
97
+
98
+ function removeCursor(): boolean {
99
+ const hooksPath = join(homedir(), ".cursor", "hooks.json");
100
+ if (!existsSync(hooksPath)) {
101
+ console.log(chalk.gray(`Cursor — no hooks file at ${hooksPath}`));
102
+ return false;
103
+ }
104
+
105
+ let hooks: unknown[] = [];
106
+ try {
107
+ const parsed = JSON.parse(readFileSync(hooksPath, "utf-8"));
108
+ hooks = Array.isArray(parsed) ? parsed : (parsed.hooks ?? []);
109
+ } catch {
110
+ console.log(chalk.yellow(`Cursor — could not parse ${hooksPath}`));
111
+ return false;
112
+ }
113
+
114
+ const nextHooks = hooks.filter((h) => !String((h as Record<string, unknown>).command ?? "").includes("auto-submit"));
115
+ if (nextHooks.length === hooks.length) {
116
+ console.log(chalk.gray("Cursor — no tracemp hook found"));
117
+ return false;
118
+ }
119
+
120
+ writeFileSync(hooksPath, JSON.stringify(nextHooks, null, 2) + "\n", "utf-8");
121
+ console.log(chalk.green("✓ Cursor") + chalk.gray(` — tracemp hook removed from ${hooksPath}`));
122
+ return true;
123
+ }
124
+
125
+ function removeCodex(): boolean {
126
+ const configPath = join(homedir(), ".codex", "config.toml");
127
+ if (!existsSync(configPath)) {
128
+ console.log(chalk.gray(`Codex — no config file at ${configPath}`));
129
+ return false;
130
+ }
131
+
132
+ let existing = "";
133
+ try {
134
+ existing = readFileSync(configPath, "utf-8");
135
+ } catch {
136
+ console.log(chalk.yellow(`Codex — could not read ${configPath}`));
137
+ return false;
138
+ }
139
+
140
+ const next = removeCodexHookBlocks(existing);
141
+ if (next === existing) {
142
+ console.log(chalk.gray("Codex — no tracemp hook found"));
143
+ return false;
144
+ }
145
+
146
+ writeFileSync(configPath, next, "utf-8");
147
+ console.log(chalk.green("✓ Codex") + chalk.gray(` — tracemp hook removed from ${configPath}`));
148
+ return true;
149
+ }
150
+
151
+ function removeCodexHookBlocks(content: string): string {
152
+ const markers = new Set([CODEX_HOOK_MARKER, LEGACY_CODEX_HOOK_MARKER]);
153
+ const lines = content.split("\n");
154
+ const out: string[] = [];
155
+ let i = 0;
156
+
157
+ while (i < lines.length) {
158
+ const line = lines[i];
159
+ const trimmed = line.trim();
160
+
161
+ if (markers.has(trimmed)) {
162
+ i++;
163
+ while (i < lines.length && lines[i].trim() === "") i++;
164
+ if (i < lines.length && lines[i].trim() === "[[hooks]]") {
165
+ i = skipHookBlock(lines, i);
166
+ while (i < lines.length && lines[i].trim() === "") i++;
167
+ }
168
+ continue;
169
+ }
170
+
171
+ if (trimmed === "[[hooks]]") {
172
+ const blockEnd = skipHookBlock(lines, i);
173
+ const blockText = lines.slice(i, blockEnd).join("\n");
174
+ if (blockText.includes("auto-submit")) {
175
+ i = blockEnd;
176
+ while (i < lines.length && lines[i].trim() === "") i++;
177
+ continue;
178
+ }
179
+ }
180
+
181
+ out.push(line);
182
+ i++;
183
+ }
184
+
185
+ return out.join("\n").replace(/\n{3,}/g, "\n\n");
186
+ }
187
+
188
+ function skipHookBlock(lines: string[], start: number): number {
189
+ let i = start + 1;
190
+ while (i < lines.length && !lines[i].trim().startsWith("[")) {
191
+ i++;
192
+ }
193
+ return i;
194
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * setup-hook — installs trace auto-submit as a per-turn hook for AI coding tools.
2
+ * setup-hook — installs tracemp auto-submit as a per-turn hook for AI coding tools.
3
3
  * Run once; sessions are then captured automatically with no user action.
4
4
  *
5
5
  * Supported tools:
@@ -12,21 +12,22 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
12
12
  import { homedir } from "os";
13
13
  import { join } from "path";
14
14
  import chalk from "chalk";
15
+ import { getAutoSubmitLogPath, resolveProfile } from "../config.js";
15
16
 
16
17
  interface SetupHookOptions {
18
+ profile?: string;
17
19
  tool?: string;
18
20
  }
19
21
 
20
- // The command that hooks invoke — must be on PATH after `npm install -g`
21
- const HOOK_COMMAND = "tracemp auto-submit";
22
-
23
22
  export async function setupHookCommand(opts: SetupHookOptions): Promise<void> {
23
+ const profile = resolveProfile(opts.profile);
24
24
  const tools = opts.tool ? [opts.tool] : detectInstalledTools();
25
+ const hookCommand = buildHookCommand(profile);
25
26
 
26
27
  if (tools.length === 0) {
27
28
  console.log(chalk.yellow("No supported AI coding tools detected."));
28
29
  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
+ console.log(chalk.gray("Or specify: tracemp setup-hook --tool claude-code"));
30
31
  return;
31
32
  }
32
33
 
@@ -34,31 +35,32 @@ export async function setupHookCommand(opts: SetupHookOptions): Promise<void> {
34
35
  try {
35
36
  switch (tool) {
36
37
  case "claude-code":
37
- setupClaudeCode();
38
+ setupClaudeCode(hookCommand);
38
39
  break;
39
40
  case "cursor":
40
- setupCursor();
41
+ setupCursor(hookCommand);
41
42
  break;
42
43
  case "codex":
43
- setupCodex();
44
+ setupCodex(hookCommand);
44
45
  break;
45
46
  default:
46
- console.log(chalk.yellow(`Unknown tool: ${tool}. Supported: claude-code, cursor`));
47
+ console.log(chalk.yellow(`Unknown tool: ${tool}. Supported: claude-code, cursor, codex`));
47
48
  }
48
49
  } catch (err) {
49
50
  console.error(chalk.red(`Failed to set up hook for ${tool}: ${err}`));
50
51
  }
51
52
  }
52
53
 
53
- console.log(chalk.gray(`\nHook logs: ~/.config/tracemarketplace/auto-submit.log`));
54
- console.log(chalk.gray(`Remove hooks: trace remove-hook`));
54
+ console.log(chalk.gray(`\nProfile: ${profile}`));
55
+ console.log(chalk.gray(`Hook logs: ${getAutoSubmitLogPath(profile)}`));
56
+ console.log(chalk.gray("Remove hooks: tracemp remove-hook"));
55
57
  }
56
58
 
57
59
  // ─── Claude Code ────────────────────────────────────────────────────────────
58
60
  // Hook config: ~/.claude/settings.json
59
61
  // Stop hook fires at session end with stdin: { session_id, transcript_path }
60
62
 
61
- function setupClaudeCode() {
63
+ function setupClaudeCode(hookCommand: string) {
62
64
  const settingsPath = join(homedir(), ".claude", "settings.json");
63
65
  mkdirSync(join(homedir(), ".claude"), { recursive: true });
64
66
 
@@ -70,22 +72,23 @@ function setupClaudeCode() {
70
72
  const hooks = (settings.hooks as Record<string, unknown> | undefined) ?? {};
71
73
  const stopHooks = (hooks["Stop"] as unknown[] | undefined) ?? [];
72
74
 
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("auto-submit"));
75
+ const nextStopHooks = stopHooks
76
+ .map((h: unknown) => {
77
+ const entry = { ...(h as Record<string, unknown>) };
78
+ const innerHooks = Array.isArray(entry.hooks) ? entry.hooks as Array<Record<string, unknown>> : [];
79
+ entry.hooks = innerHooks.filter((ih) => !String(ih.command ?? "").includes("auto-submit"));
80
+ return entry;
81
+ })
82
+ .filter((entry) => Array.isArray(entry.hooks) && entry.hooks.length > 0);
83
+
84
+ nextStopHooks.push({
85
+ matcher: "",
86
+ hooks: [{ type: "command", command: hookCommand, async: true }],
78
87
  });
79
88
 
80
- if (!alreadyInstalled) {
81
- stopHooks.push({
82
- matcher: "",
83
- hooks: [{ type: "command", command: HOOK_COMMAND, async: true }],
84
- });
85
- hooks["Stop"] = stopHooks;
86
- settings.hooks = hooks;
87
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
88
- }
89
+ hooks["Stop"] = nextStopHooks;
90
+ settings.hooks = hooks;
91
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
89
92
 
90
93
  console.log(chalk.green("✓ Claude Code") + chalk.gray(` — Stop hook installed in ${settingsPath}`));
91
94
  }
@@ -94,7 +97,7 @@ function setupClaudeCode() {
94
97
  // Hook config: ~/.cursor/hooks.json
95
98
  // stop fires after each assistant turn with stdin: { sessionId, terminationReason, duration }
96
99
 
97
- function setupCursor() {
100
+ function setupCursor(hookCommand: string) {
98
101
  const hooksPath = join(homedir(), ".cursor", "hooks.json");
99
102
  mkdirSync(join(homedir(), ".cursor"), { recursive: true });
100
103
 
@@ -106,21 +109,19 @@ function setupCursor() {
106
109
  } catch {}
107
110
  }
108
111
 
109
- // Remove any old sessionEnd entry for trace auto-submit (migration)
112
+ // Remove any old sessionEnd entry for tracemp auto-submit (migration)
110
113
  hooks = hooks.filter((h: unknown) => {
111
114
  const entry = h as Record<string, unknown>;
112
115
  return !(entry.event === "sessionEnd" && String(entry.command ?? "").includes("auto-submit"));
113
116
  });
114
117
 
115
- const alreadyInstalled = hooks.some((h: unknown) => {
118
+ hooks = hooks.filter((h: unknown) => {
116
119
  const entry = h as Record<string, unknown>;
117
- return entry.event === "stop" && String(entry.command ?? "").includes("auto-submit");
120
+ return !(entry.event === "stop" && String(entry.command ?? "").includes("auto-submit"));
118
121
  });
119
122
 
120
- if (!alreadyInstalled) {
121
- hooks.push({ event: "stop", command: HOOK_COMMAND, timeout: 30 });
122
- writeFileSync(hooksPath, JSON.stringify(hooks, null, 2), "utf-8");
123
- }
123
+ hooks.push({ event: "stop", command: hookCommand, timeout: 30 });
124
+ writeFileSync(hooksPath, JSON.stringify(hooks, null, 2) + "\n", "utf-8");
124
125
 
125
126
  console.log(chalk.green("✓ Cursor") + chalk.gray(` — stop hook installed in ${hooksPath}`));
126
127
  }
@@ -131,9 +132,9 @@ function setupCursor() {
131
132
  // { "thread-id": "...", "turn-id": "...", "cwd": "...", "last-assistant-message": "..." }
132
133
  // Uses TOML [[hooks]] array-of-tables format.
133
134
 
134
- const CODEX_HOOK_MARKER = "# trace-marketplace-hook";
135
+ export const CODEX_HOOK_MARKER = "# tracemp-hook";
135
136
 
136
- function setupCodex() {
137
+ function setupCodex(hookCommand: string) {
137
138
  const configPath = join(homedir(), ".codex", "config.toml");
138
139
  mkdirSync(join(homedir(), ".codex"), { recursive: true });
139
140
 
@@ -142,13 +143,8 @@ function setupCodex() {
142
143
  try { existing = readFileSync(configPath, "utf-8"); } catch {}
143
144
  }
144
145
 
145
- if (existing.includes("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");
146
+ const next = appendCodexHook(existing, `${hookCommand} --tool codex`);
147
+ writeFileSync(configPath, next, "utf-8");
152
148
 
153
149
  console.log(chalk.green("✓ Codex") + chalk.gray(` — after_agent hook installed in ${configPath}`));
154
150
  }
@@ -173,3 +169,57 @@ function detectInstalledTools(): string[] {
173
169
 
174
170
  return found;
175
171
  }
172
+
173
+ function buildHookCommand(profile: string): string {
174
+ return `tracemp auto-submit --profile ${profile}`;
175
+ }
176
+
177
+ function appendCodexHook(content: string, hookCommand: string): string {
178
+ const base = removeCodexHookBlocks(content).trimEnd();
179
+ const hookBlock = `${CODEX_HOOK_MARKER}\n[[hooks]]\nevent = "after_agent"\ncommand = "${hookCommand}"`;
180
+ return base ? `${base}\n\n${hookBlock}\n` : `${hookBlock}\n`;
181
+ }
182
+
183
+ function removeCodexHookBlocks(content: string): string {
184
+ const lines = content.split("\n");
185
+ const out: string[] = [];
186
+ let i = 0;
187
+
188
+ while (i < lines.length) {
189
+ const line = lines[i];
190
+ const trimmed = line.trim();
191
+
192
+ if (trimmed === CODEX_HOOK_MARKER) {
193
+ i++;
194
+ while (i < lines.length && lines[i].trim() === "") i++;
195
+ if (i < lines.length && lines[i].trim() === "[[hooks]]") {
196
+ i = skipHookBlock(lines, i);
197
+ while (i < lines.length && lines[i].trim() === "") i++;
198
+ }
199
+ continue;
200
+ }
201
+
202
+ if (trimmed === "[[hooks]]") {
203
+ const blockEnd = skipHookBlock(lines, i);
204
+ const blockText = lines.slice(i, blockEnd).join("\n");
205
+ if (blockText.includes("auto-submit")) {
206
+ i = blockEnd;
207
+ while (i < lines.length && lines[i].trim() === "") i++;
208
+ continue;
209
+ }
210
+ }
211
+
212
+ out.push(line);
213
+ i++;
214
+ }
215
+
216
+ return out.join("\n").replace(/\n{3,}/g, "\n\n");
217
+ }
218
+
219
+ function skipHookBlock(lines: string[], start: number): number {
220
+ let i = start + 1;
221
+ while (i < lines.length && !lines[i].trim().startsWith("[")) {
222
+ i++;
223
+ }
224
+ return i;
225
+ }
@@ -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 statusCommand(): Promise<void> {
6
- const config = loadConfig();
6
+ export async function statusCommand(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
 
@@ -16,6 +18,8 @@ export async function statusCommand(): Promise<void> {
16
18
  ]);
17
19
 
18
20
  const balance = ((me as any).balanceCents ?? (me as any).balance_cents ?? 0) / 100;
21
+ console.log(chalk.gray("Profile:"), config.profile);
22
+ console.log(chalk.gray("Server:"), config.serverUrl);
19
23
  console.log(chalk.bold("Balance:"), chalk.green(`$${balance.toFixed(2)}`));
20
24
  console.log(chalk.bold("Submissions:"), (subs as any[]).length);
21
25