@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.
- package/dist/api-client.d.ts +2 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +2 -2
- package/dist/api-client.js.map +1 -1
- package/dist/cli.js +45 -14
- package/dist/cli.js.map +1 -1
- package/dist/commands/auto-submit.d.ts +2 -1
- package/dist/commands/auto-submit.d.ts.map +1 -1
- package/dist/commands/auto-submit.js +43 -56
- package/dist/commands/auto-submit.js.map +1 -1
- package/dist/commands/daemon.d.ts +8 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +118 -62
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/history.d.ts +3 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +8 -4
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/login.d.ts +5 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +25 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/register.d.ts +1 -0
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +4 -39
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/remove-hook.d.ts +6 -0
- package/dist/commands/remove-hook.d.ts.map +1 -0
- package/dist/commands/remove-hook.js +174 -0
- package/dist/commands/remove-hook.js.map +1 -0
- package/dist/commands/setup-hook.d.ts +2 -0
- package/dist/commands/setup-hook.d.ts.map +1 -1
- package/dist/commands/setup-hook.js +86 -42
- package/dist/commands/setup-hook.js.map +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +8 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +136 -83
- package/dist/commands/submit.js.map +1 -1
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +8 -4
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config.d.ts +33 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +163 -17
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/flush.d.ts +46 -0
- package/dist/flush.d.ts.map +1 -0
- package/dist/flush.js +338 -0
- package/dist/flush.js.map +1 -0
- package/dist/flush.test.d.ts +2 -0
- package/dist/flush.test.d.ts.map +1 -0
- package/dist/flush.test.js +175 -0
- package/dist/flush.test.js.map +1 -0
- package/dist/submitter.d.ts.map +1 -1
- package/dist/submitter.js +5 -2
- package/dist/submitter.js.map +1 -1
- package/package.json +8 -7
- package/src/api-client.ts +3 -3
- package/src/cli.ts +51 -14
- package/src/commands/auto-submit.ts +80 -40
- package/src/commands/daemon.ts +166 -59
- package/src/commands/history.ts +9 -4
- package/src/commands/login.ts +37 -9
- package/src/commands/register.ts +5 -49
- package/src/commands/remove-hook.ts +194 -0
- package/src/commands/setup-hook.ts +94 -44
- package/src/commands/status.ts +8 -4
- package/src/commands/submit.ts +189 -83
- package/src/commands/whoami.ts +8 -4
- package/src/config.ts +223 -21
- package/src/constants.ts +18 -0
- package/src/flush.test.ts +214 -0
- package/src/flush.ts +505 -0
- package/vitest.config.ts +8 -0
- package/src/submitter.ts +0 -110
package/src/commands/login.ts
CHANGED
|
@@ -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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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(
|
|
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(
|
|
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
|
+
}
|
package/src/commands/register.ts
CHANGED
|
@@ -1,52 +1,8 @@
|
|
|
1
|
-
import inquirer from "inquirer";
|
|
2
1
|
import chalk from "chalk";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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 = "trace 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:
|
|
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(`\
|
|
54
|
-
console.log(chalk.gray(`
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
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
|
|
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
|
-
return !(entry.event === "sessionEnd" && String(entry.command ?? "").includes("
|
|
115
|
+
return !(entry.event === "sessionEnd" && String(entry.command ?? "").includes("auto-submit"));
|
|
113
116
|
});
|
|
114
117
|
|
|
115
|
-
|
|
118
|
+
hooks = hooks.filter((h: unknown) => {
|
|
116
119
|
const entry = h as Record<string, unknown>;
|
|
117
|
-
return entry.event === "stop" && String(entry.command ?? "").includes("
|
|
120
|
+
return !(entry.event === "stop" && String(entry.command ?? "").includes("auto-submit"));
|
|
118
121
|
});
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
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 = "#
|
|
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
|
-
|
|
146
|
-
|
|
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
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|