agentrace 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/commands/init.d.ts +5 -0
- package/dist/commands/init.js +95 -0
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +40 -0
- package/dist/commands/off.d.ts +1 -0
- package/dist/commands/off.js +18 -0
- package/dist/commands/on.d.ts +4 -0
- package/dist/commands/on.js +29 -0
- package/dist/commands/send.d.ts +1 -0
- package/dist/commands/send.js +128 -0
- package/dist/commands/uninstall.d.ts +1 -0
- package/dist/commands/uninstall.js +22 -0
- package/dist/config/cursor.d.ts +8 -0
- package/dist/config/cursor.js +57 -0
- package/dist/config/manager.d.ts +8 -0
- package/dist/config/manager.js +38 -0
- package/dist/hooks/installer.d.ts +12 -0
- package/dist/hooks/installer.js +100 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +50 -0
- package/dist/utils/browser.d.ts +6 -0
- package/dist/utils/browser.js +40 -0
- package/dist/utils/callback-server.d.ts +10 -0
- package/dist/utils/callback-server.js +81 -0
- package/dist/utils/http.d.ts +24 -0
- package/dist/utils/http.js +56 -0
- package/package.json +48 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { saveConfig, getConfigPath } from "../config/manager.js";
|
|
4
|
+
import { installHooks } from "../hooks/installer.js";
|
|
5
|
+
import { startCallbackServer, getRandomPort, generateToken, } from "../utils/callback-server.js";
|
|
6
|
+
import { openBrowser, buildSetupUrl } from "../utils/browser.js";
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
const CALLBACK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
10
|
+
export async function initCommand(options = {}) {
|
|
11
|
+
// --url is required
|
|
12
|
+
if (!options.url) {
|
|
13
|
+
console.error("Error: --url option is required");
|
|
14
|
+
console.error("");
|
|
15
|
+
console.error("Usage: npx agentrace init --url <server-url>");
|
|
16
|
+
console.error("Example: npx agentrace init --url http://localhost:8080");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
// Validate URL
|
|
20
|
+
let serverUrl;
|
|
21
|
+
try {
|
|
22
|
+
serverUrl = new URL(options.url);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error("Error: Invalid URL format");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log("Agentrace Setup\n");
|
|
29
|
+
if (options.dev) {
|
|
30
|
+
console.log("[Dev Mode] Using local CLI for hooks\n");
|
|
31
|
+
}
|
|
32
|
+
// Generate token and start callback server
|
|
33
|
+
const token = generateToken();
|
|
34
|
+
const port = getRandomPort();
|
|
35
|
+
const callbackUrl = `http://127.0.0.1:${port}/callback`;
|
|
36
|
+
console.log("Starting local callback server...");
|
|
37
|
+
// Start callback server (returns promise that resolves when callback is received)
|
|
38
|
+
const callbackPromise = startCallbackServer(port, {
|
|
39
|
+
token,
|
|
40
|
+
timeout: CALLBACK_TIMEOUT,
|
|
41
|
+
});
|
|
42
|
+
// Build setup URL and open browser
|
|
43
|
+
const setupUrl = buildSetupUrl(serverUrl.toString(), token, callbackUrl);
|
|
44
|
+
console.log(`Opening browser for authentication...`);
|
|
45
|
+
const browserResult = await openBrowser(setupUrl);
|
|
46
|
+
if (!browserResult.success) {
|
|
47
|
+
console.log("");
|
|
48
|
+
console.log("Could not open browser automatically.");
|
|
49
|
+
console.log("Please open this URL manually:");
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log(` ${setupUrl}`);
|
|
52
|
+
console.log("");
|
|
53
|
+
}
|
|
54
|
+
console.log("Waiting for setup to complete...");
|
|
55
|
+
console.log("(This will timeout in 5 minutes)\n");
|
|
56
|
+
try {
|
|
57
|
+
// Wait for callback
|
|
58
|
+
const result = await callbackPromise;
|
|
59
|
+
// Save config (remove trailing slash from URL)
|
|
60
|
+
const serverUrlStr = serverUrl.toString().replace(/\/+$/, '');
|
|
61
|
+
saveConfig({
|
|
62
|
+
server_url: serverUrlStr,
|
|
63
|
+
api_key: result.apiKey,
|
|
64
|
+
});
|
|
65
|
+
console.log(`✓ Config saved to ${getConfigPath()}`);
|
|
66
|
+
// Determine hook command
|
|
67
|
+
let hookCommand;
|
|
68
|
+
if (options.dev) {
|
|
69
|
+
// Use local CLI path for development
|
|
70
|
+
const cliRoot = path.resolve(__dirname, "../..");
|
|
71
|
+
const indexPath = path.join(cliRoot, "src/index.ts");
|
|
72
|
+
hookCommand = `npx tsx ${indexPath} send`;
|
|
73
|
+
console.log(` Hook command: ${hookCommand}`);
|
|
74
|
+
}
|
|
75
|
+
// Install hooks
|
|
76
|
+
const hookResult = installHooks({ command: hookCommand });
|
|
77
|
+
if (hookResult.success) {
|
|
78
|
+
console.log(`✓ ${hookResult.message}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
console.error(`✗ ${hookResult.message}`);
|
|
82
|
+
}
|
|
83
|
+
console.log("\n✓ Setup complete!");
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof Error && error.message.includes("Timeout")) {
|
|
87
|
+
console.error("\n✗ Setup timed out.");
|
|
88
|
+
console.error("Please try again with: npx agentrace init --url " + options.url);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error("\n✗ Setup failed:", error instanceof Error ? error.message : error);
|
|
92
|
+
}
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loginCommand(): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createWebSession } from "../utils/http.js";
|
|
2
|
+
import * as readline from "node:readline";
|
|
3
|
+
export async function loginCommand() {
|
|
4
|
+
console.log("Creating login session...\n");
|
|
5
|
+
const result = await createWebSession();
|
|
6
|
+
if (!result.ok) {
|
|
7
|
+
console.error(`Error: ${result.error}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
console.log(`Login URL: ${result.data.url}\n`);
|
|
11
|
+
console.log("Press Enter to open in browser, or Ctrl+C to cancel.\n");
|
|
12
|
+
// Wait for Enter key
|
|
13
|
+
const rl = readline.createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
await new Promise((resolve) => {
|
|
18
|
+
rl.question("", () => {
|
|
19
|
+
rl.close();
|
|
20
|
+
resolve();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
// Open browser
|
|
24
|
+
const { exec } = await import("node:child_process");
|
|
25
|
+
const url = result.data.url;
|
|
26
|
+
const command = process.platform === "darwin"
|
|
27
|
+
? `open "${url}"`
|
|
28
|
+
: process.platform === "win32"
|
|
29
|
+
? `start "${url}"`
|
|
30
|
+
: `xdg-open "${url}"`;
|
|
31
|
+
exec(command, (error) => {
|
|
32
|
+
if (error) {
|
|
33
|
+
console.error(`Failed to open browser: ${error.message}`);
|
|
34
|
+
console.log(`Please open this URL manually: ${url}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log("Opened in browser");
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function offCommand(): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { uninstallHooks } from "../hooks/installer.js";
|
|
2
|
+
import { loadConfig } from "../config/manager.js";
|
|
3
|
+
export async function offCommand() {
|
|
4
|
+
// Check if config exists
|
|
5
|
+
const config = loadConfig();
|
|
6
|
+
if (!config) {
|
|
7
|
+
console.log("Agentrace is not configured. Run 'npx agentrace init' first.");
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const result = uninstallHooks();
|
|
11
|
+
if (result.success) {
|
|
12
|
+
console.log(`✓ Hooks disabled. Your credentials are still saved.`);
|
|
13
|
+
console.log(` Run 'npx agentrace on' to re-enable.`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.error(`✗ ${result.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { installHooks } from "../hooks/installer.js";
|
|
4
|
+
import { loadConfig } from "../config/manager.js";
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
export async function onCommand(options = {}) {
|
|
8
|
+
// Check if config exists
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
if (!config) {
|
|
11
|
+
console.log("Agentrace is not configured. Run 'npx agentrace init' first.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Determine hook command
|
|
15
|
+
let hookCommand;
|
|
16
|
+
if (options.dev) {
|
|
17
|
+
// Use local CLI path for development
|
|
18
|
+
const cliRoot = path.resolve(__dirname, "../..");
|
|
19
|
+
const indexPath = path.join(cliRoot, "src/index.ts");
|
|
20
|
+
hookCommand = `npx tsx ${indexPath} send`;
|
|
21
|
+
}
|
|
22
|
+
const result = installHooks({ command: hookCommand });
|
|
23
|
+
if (result.success) {
|
|
24
|
+
console.log(`✓ Hooks enabled. Session data will be sent to ${config.server_url}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error(`✗ ${result.message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sendCommand(): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { loadConfig } from "../config/manager.js";
|
|
3
|
+
import { getNewLines, saveCursor, hasCursor } from "../config/cursor.js";
|
|
4
|
+
import { sendIngest } from "../utils/http.js";
|
|
5
|
+
function getGitRemoteUrl(cwd) {
|
|
6
|
+
try {
|
|
7
|
+
const url = execSync("git remote get-url origin", {
|
|
8
|
+
cwd,
|
|
9
|
+
encoding: "utf-8",
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
11
|
+
}).trim();
|
|
12
|
+
return url || null;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null; // Not a git repo or no remote
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function getGitBranch(cwd) {
|
|
19
|
+
try {
|
|
20
|
+
const branch = execSync("git branch --show-current", {
|
|
21
|
+
cwd,
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
24
|
+
}).trim();
|
|
25
|
+
return branch || null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function sendCommand() {
|
|
32
|
+
// Check if config exists
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
if (!config) {
|
|
35
|
+
console.error("[agentrace] Warning: Config not found. Run 'npx agentrace init' first.");
|
|
36
|
+
process.exit(0); // Exit 0 to not block hooks
|
|
37
|
+
}
|
|
38
|
+
// Read stdin
|
|
39
|
+
let input = "";
|
|
40
|
+
try {
|
|
41
|
+
input = await readStdin();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
console.error("[agentrace] Warning: Failed to read stdin");
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
if (!input.trim()) {
|
|
48
|
+
console.error("[agentrace] Warning: Empty input");
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
// Parse JSON
|
|
52
|
+
let data;
|
|
53
|
+
try {
|
|
54
|
+
data = JSON.parse(input);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
console.error("[agentrace] Warning: Invalid JSON input");
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
const sessionId = data.session_id;
|
|
61
|
+
const transcriptPath = data.transcript_path;
|
|
62
|
+
if (!sessionId || !transcriptPath) {
|
|
63
|
+
console.error("[agentrace] Warning: Missing session_id or transcript_path");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
// Get new lines from transcript
|
|
67
|
+
const { lines, totalLineCount } = getNewLines(transcriptPath, sessionId);
|
|
68
|
+
if (lines.length === 0) {
|
|
69
|
+
// No new lines to send
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
// Parse JSONL lines
|
|
73
|
+
const transcriptLines = [];
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
try {
|
|
76
|
+
transcriptLines.push(JSON.parse(line));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Skip invalid JSON lines
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (transcriptLines.length === 0) {
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
// Use CLAUDE_PROJECT_DIR (stable project root) instead of cwd (can change during builds)
|
|
86
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || data.cwd;
|
|
87
|
+
// Extract git info only on first send (when cursor doesn't exist yet)
|
|
88
|
+
let gitRemoteUrl;
|
|
89
|
+
let gitBranch;
|
|
90
|
+
if (projectDir && !hasCursor(sessionId)) {
|
|
91
|
+
gitRemoteUrl = getGitRemoteUrl(projectDir) ?? undefined;
|
|
92
|
+
gitBranch = getGitBranch(projectDir) ?? undefined;
|
|
93
|
+
}
|
|
94
|
+
// Send to server
|
|
95
|
+
const result = await sendIngest({
|
|
96
|
+
session_id: sessionId,
|
|
97
|
+
transcript_lines: transcriptLines,
|
|
98
|
+
cwd: projectDir,
|
|
99
|
+
git_remote_url: gitRemoteUrl,
|
|
100
|
+
git_branch: gitBranch,
|
|
101
|
+
});
|
|
102
|
+
if (result.ok) {
|
|
103
|
+
// Update cursor on success
|
|
104
|
+
saveCursor(sessionId, totalLineCount);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.error(`[agentrace] Warning: ${result.error}`);
|
|
108
|
+
}
|
|
109
|
+
// Always exit 0 to not block hooks
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
function readStdin() {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
let data = "";
|
|
115
|
+
process.stdin.setEncoding("utf8");
|
|
116
|
+
process.stdin.on("data", (chunk) => {
|
|
117
|
+
data += chunk;
|
|
118
|
+
});
|
|
119
|
+
process.stdin.on("end", () => {
|
|
120
|
+
resolve(data);
|
|
121
|
+
});
|
|
122
|
+
process.stdin.on("error", reject);
|
|
123
|
+
// Set timeout to avoid hanging
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
resolve(data);
|
|
126
|
+
}, 5000);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function uninstallCommand(): Promise<void>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { deleteConfig } from "../config/manager.js";
|
|
2
|
+
import { uninstallHooks } from "../hooks/installer.js";
|
|
3
|
+
export async function uninstallCommand() {
|
|
4
|
+
console.log("Uninstalling Agentrace...\n");
|
|
5
|
+
// Remove hooks
|
|
6
|
+
const hookResult = uninstallHooks();
|
|
7
|
+
if (hookResult.success) {
|
|
8
|
+
console.log(`✓ ${hookResult.message}`);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
console.error(`✗ ${hookResult.message}`);
|
|
12
|
+
}
|
|
13
|
+
// Remove config
|
|
14
|
+
const configRemoved = deleteConfig();
|
|
15
|
+
if (configRemoved) {
|
|
16
|
+
console.log("✓ Config removed");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log("✓ No config to remove");
|
|
20
|
+
}
|
|
21
|
+
console.log("\nUninstall complete!");
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function getCursor(sessionId: string): number;
|
|
2
|
+
export declare function hasCursor(sessionId: string): boolean;
|
|
3
|
+
export declare function saveCursor(sessionId: string, lineCount: number): void;
|
|
4
|
+
export declare function readTranscriptLines(transcriptPath: string): string[];
|
|
5
|
+
export declare function getNewLines(transcriptPath: string, sessionId: string): {
|
|
6
|
+
lines: string[];
|
|
7
|
+
totalLineCount: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
const CURSORS_DIR = path.join(os.homedir(), ".agentrace", "cursors");
|
|
5
|
+
function getCursorPath(sessionId) {
|
|
6
|
+
return path.join(CURSORS_DIR, `${sessionId}.json`);
|
|
7
|
+
}
|
|
8
|
+
export function getCursor(sessionId) {
|
|
9
|
+
try {
|
|
10
|
+
const cursorPath = getCursorPath(sessionId);
|
|
11
|
+
if (!fs.existsSync(cursorPath)) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
const content = fs.readFileSync(cursorPath, "utf-8");
|
|
15
|
+
const data = JSON.parse(content);
|
|
16
|
+
return data.lineCount;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function hasCursor(sessionId) {
|
|
23
|
+
const cursorPath = getCursorPath(sessionId);
|
|
24
|
+
return fs.existsSync(cursorPath);
|
|
25
|
+
}
|
|
26
|
+
export function saveCursor(sessionId, lineCount) {
|
|
27
|
+
if (!fs.existsSync(CURSORS_DIR)) {
|
|
28
|
+
fs.mkdirSync(CURSORS_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
const data = {
|
|
31
|
+
lineCount,
|
|
32
|
+
lastUpdated: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
const cursorPath = getCursorPath(sessionId);
|
|
35
|
+
fs.writeFileSync(cursorPath, JSON.stringify(data, null, 2));
|
|
36
|
+
}
|
|
37
|
+
export function readTranscriptLines(transcriptPath) {
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const content = fs.readFileSync(transcriptPath, "utf-8");
|
|
43
|
+
return content.split("\n").filter((line) => line.trim() !== "");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function getNewLines(transcriptPath, sessionId) {
|
|
50
|
+
const allLines = readTranscriptLines(transcriptPath);
|
|
51
|
+
const cursor = getCursor(sessionId);
|
|
52
|
+
const newLines = allLines.slice(cursor);
|
|
53
|
+
return {
|
|
54
|
+
lines: newLines,
|
|
55
|
+
totalLineCount: allLines.length,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface AgentraceConfig {
|
|
2
|
+
server_url: string;
|
|
3
|
+
api_key: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getConfigPath(): string;
|
|
6
|
+
export declare function loadConfig(): AgentraceConfig | null;
|
|
7
|
+
export declare function saveConfig(config: AgentraceConfig): void;
|
|
8
|
+
export declare function deleteConfig(): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), ".agentrace");
|
|
5
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function getConfigPath() {
|
|
7
|
+
return CONFIG_FILE;
|
|
8
|
+
}
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const content = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
15
|
+
return JSON.parse(content);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveConfig(config) {
|
|
22
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
23
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
26
|
+
}
|
|
27
|
+
export function deleteConfig() {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
30
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface InstallHooksOptions {
|
|
2
|
+
command?: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function installHooks(options?: InstallHooksOptions): {
|
|
5
|
+
success: boolean;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function uninstallHooks(): {
|
|
9
|
+
success: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function checkHooksInstalled(): boolean;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
5
|
+
const DEFAULT_COMMAND = "npx agentrace send";
|
|
6
|
+
function createAgentraceHook(command) {
|
|
7
|
+
return {
|
|
8
|
+
type: "command",
|
|
9
|
+
command,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function isAgentraceHook(hook) {
|
|
13
|
+
// Match both production ("agentrace send") and dev mode ("index.ts send")
|
|
14
|
+
return hook.command?.includes("agentrace send") || hook.command?.includes("index.ts send");
|
|
15
|
+
}
|
|
16
|
+
export function installHooks(options = {}) {
|
|
17
|
+
const command = options.command || DEFAULT_COMMAND;
|
|
18
|
+
const agentraceHook = createAgentraceHook(command);
|
|
19
|
+
try {
|
|
20
|
+
let settings = {};
|
|
21
|
+
// Load existing settings if file exists
|
|
22
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
23
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
24
|
+
settings = JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
// Initialize hooks structure if not present
|
|
27
|
+
if (!settings.hooks) {
|
|
28
|
+
settings.hooks = {};
|
|
29
|
+
}
|
|
30
|
+
// Add Stop hook only (transcript diff is sent on each Stop)
|
|
31
|
+
if (!settings.hooks.Stop) {
|
|
32
|
+
settings.hooks.Stop = [];
|
|
33
|
+
}
|
|
34
|
+
const hasStopHook = settings.hooks.Stop.some((matcher) => matcher.hooks?.some(isAgentraceHook));
|
|
35
|
+
if (hasStopHook) {
|
|
36
|
+
return { success: true, message: "Hooks already installed (skipped)" };
|
|
37
|
+
}
|
|
38
|
+
settings.hooks.Stop.push({
|
|
39
|
+
hooks: [agentraceHook],
|
|
40
|
+
});
|
|
41
|
+
// Ensure directory exists
|
|
42
|
+
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
// Write settings
|
|
47
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
48
|
+
return { success: true, message: `Hooks added to ${CLAUDE_SETTINGS_PATH}` };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
return { success: false, message: `Failed to install hooks: ${message}` };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function uninstallHooks() {
|
|
56
|
+
try {
|
|
57
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
58
|
+
return { success: true, message: "No settings file found" };
|
|
59
|
+
}
|
|
60
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
61
|
+
const settings = JSON.parse(content);
|
|
62
|
+
if (!settings.hooks) {
|
|
63
|
+
return { success: true, message: "No hooks configured" };
|
|
64
|
+
}
|
|
65
|
+
// Remove agentrace hooks from Stop
|
|
66
|
+
if (settings.hooks.Stop) {
|
|
67
|
+
settings.hooks.Stop = settings.hooks.Stop.filter((matcher) => !matcher.hooks?.some(isAgentraceHook));
|
|
68
|
+
if (settings.hooks.Stop.length === 0) {
|
|
69
|
+
delete settings.hooks.Stop;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Clean up empty hooks object
|
|
73
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
74
|
+
delete settings.hooks;
|
|
75
|
+
}
|
|
76
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
message: `Removed hooks from ${CLAUDE_SETTINGS_PATH}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
return { success: false, message: `Failed to uninstall hooks: ${message}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function checkHooksInstalled() {
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
93
|
+
const settings = JSON.parse(content);
|
|
94
|
+
const hasStopHook = settings.hooks?.Stop?.some((matcher) => matcher.hooks?.some(isAgentraceHook));
|
|
95
|
+
return !!hasStopHook;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCommand } from "./commands/init.js";
|
|
4
|
+
import { loginCommand } from "./commands/login.js";
|
|
5
|
+
import { sendCommand } from "./commands/send.js";
|
|
6
|
+
import { uninstallCommand } from "./commands/uninstall.js";
|
|
7
|
+
import { onCommand } from "./commands/on.js";
|
|
8
|
+
import { offCommand } from "./commands/off.js";
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program.name("agentrace").description("CLI for Agentrace").version("0.1.0");
|
|
11
|
+
program
|
|
12
|
+
.command("init")
|
|
13
|
+
.description("Initialize agentrace configuration and hooks")
|
|
14
|
+
.requiredOption("--url <url>", "Server URL (required)")
|
|
15
|
+
.option("--dev", "Use local CLI path for development")
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
await initCommand({ url: options.url, dev: options.dev });
|
|
18
|
+
});
|
|
19
|
+
program
|
|
20
|
+
.command("login")
|
|
21
|
+
.description("Open web dashboard in browser")
|
|
22
|
+
.action(async () => {
|
|
23
|
+
await loginCommand();
|
|
24
|
+
});
|
|
25
|
+
program
|
|
26
|
+
.command("send")
|
|
27
|
+
.description("Send event to server (used by hooks)")
|
|
28
|
+
.action(async () => {
|
|
29
|
+
await sendCommand();
|
|
30
|
+
});
|
|
31
|
+
program
|
|
32
|
+
.command("uninstall")
|
|
33
|
+
.description("Remove agentrace hooks and config")
|
|
34
|
+
.action(async () => {
|
|
35
|
+
await uninstallCommand();
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command("on")
|
|
39
|
+
.description("Enable agentrace hooks (credentials preserved)")
|
|
40
|
+
.option("--dev", "Use local CLI path for development")
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
await onCommand({ dev: options.dev });
|
|
43
|
+
});
|
|
44
|
+
program
|
|
45
|
+
.command("off")
|
|
46
|
+
.description("Disable agentrace hooks (credentials preserved)")
|
|
47
|
+
.action(async () => {
|
|
48
|
+
await offCommand();
|
|
49
|
+
});
|
|
50
|
+
program.parse();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
export async function openBrowser(url) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const os = platform();
|
|
6
|
+
let command;
|
|
7
|
+
switch (os) {
|
|
8
|
+
case "darwin":
|
|
9
|
+
command = `open "${url}"`;
|
|
10
|
+
break;
|
|
11
|
+
case "win32":
|
|
12
|
+
command = `start "" "${url}"`;
|
|
13
|
+
break;
|
|
14
|
+
default:
|
|
15
|
+
// Linux and others
|
|
16
|
+
command = `xdg-open "${url}"`;
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
exec(command, (error) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
resolve({
|
|
22
|
+
success: false,
|
|
23
|
+
message: `Failed to open browser: ${error.message}`,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
resolve({
|
|
28
|
+
success: true,
|
|
29
|
+
message: "Browser opened",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function buildSetupUrl(serverUrl, token, callbackUrl) {
|
|
36
|
+
const url = new URL("/setup", serverUrl);
|
|
37
|
+
url.searchParams.set("token", token);
|
|
38
|
+
url.searchParams.set("callback", callbackUrl);
|
|
39
|
+
return url.toString();
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CallbackResult {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
}
|
|
4
|
+
export interface CallbackServerOptions {
|
|
5
|
+
token: string;
|
|
6
|
+
timeout: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function getRandomPort(): number;
|
|
9
|
+
export declare function generateToken(): string;
|
|
10
|
+
export declare function startCallbackServer(port: number, options: CallbackServerOptions): Promise<CallbackResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
import * as crypto from "node:crypto";
|
|
3
|
+
export function getRandomPort() {
|
|
4
|
+
// Use dynamic port range (49152-65535)
|
|
5
|
+
return Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152;
|
|
6
|
+
}
|
|
7
|
+
export function generateToken() {
|
|
8
|
+
return crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
export function startCallbackServer(port, options) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
let resolved = false;
|
|
13
|
+
const server = http.createServer((req, res) => {
|
|
14
|
+
// Set CORS headers
|
|
15
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
16
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
17
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
18
|
+
// Handle preflight
|
|
19
|
+
if (req.method === "OPTIONS") {
|
|
20
|
+
res.writeHead(204);
|
|
21
|
+
res.end();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Only accept POST to /callback
|
|
25
|
+
if (req.method !== "POST" || req.url !== "/callback") {
|
|
26
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
27
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let body = "";
|
|
31
|
+
req.on("data", (chunk) => {
|
|
32
|
+
body += chunk.toString();
|
|
33
|
+
});
|
|
34
|
+
req.on("end", () => {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(body);
|
|
37
|
+
// Validate token
|
|
38
|
+
if (data.token !== options.token) {
|
|
39
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
40
|
+
res.end(JSON.stringify({ error: "Invalid token" }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Validate api_key
|
|
44
|
+
if (!data.api_key || typeof data.api_key !== "string") {
|
|
45
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
46
|
+
res.end(JSON.stringify({ error: "Invalid api_key" }));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Success response
|
|
50
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
51
|
+
res.end(JSON.stringify({ success: true }));
|
|
52
|
+
resolved = true;
|
|
53
|
+
server.close();
|
|
54
|
+
resolve({ apiKey: data.api_key });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
58
|
+
res.end(JSON.stringify({ error: "Invalid JSON" }));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
server.on("error", (err) => {
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
reject(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Timeout handling
|
|
68
|
+
const timeoutId = setTimeout(() => {
|
|
69
|
+
if (!resolved) {
|
|
70
|
+
server.close();
|
|
71
|
+
reject(new Error("Timeout: No callback received"));
|
|
72
|
+
}
|
|
73
|
+
}, options.timeout);
|
|
74
|
+
server.on("close", () => {
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
});
|
|
77
|
+
server.listen(port, "127.0.0.1", () => {
|
|
78
|
+
// Server started successfully
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface IngestPayload {
|
|
2
|
+
session_id: string;
|
|
3
|
+
transcript_lines: unknown[];
|
|
4
|
+
cwd?: string;
|
|
5
|
+
git_remote_url?: string;
|
|
6
|
+
git_branch?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IngestResponse {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
events_created?: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface WebSessionResponse {
|
|
14
|
+
url: string;
|
|
15
|
+
expires_at: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function sendIngest(payload: IngestPayload): Promise<IngestResponse>;
|
|
18
|
+
export declare function createWebSession(): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
data: WebSessionResponse;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
error: string;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { loadConfig } from "../config/manager.js";
|
|
2
|
+
function getBaseUrl(config) {
|
|
3
|
+
return config.server_url.replace(/\/+$/, '');
|
|
4
|
+
}
|
|
5
|
+
export async function sendIngest(payload) {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
if (!config) {
|
|
8
|
+
return { ok: false, error: "Config not found" };
|
|
9
|
+
}
|
|
10
|
+
const url = `${getBaseUrl(config)}/api/ingest`;
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(url, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(payload),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
const text = await response.text();
|
|
22
|
+
return { ok: false, error: `HTTP ${response.status}: ${text}` };
|
|
23
|
+
}
|
|
24
|
+
return (await response.json());
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return { ok: false, error: message };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function createWebSession() {
|
|
32
|
+
const config = loadConfig();
|
|
33
|
+
if (!config) {
|
|
34
|
+
return { ok: false, error: "Config not found. Run 'agentrace init' first." };
|
|
35
|
+
}
|
|
36
|
+
const url = `${getBaseUrl(config)}/api/auth/web-session`;
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(url, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
return { ok: false, error: `HTTP ${response.status}: ${text}` };
|
|
48
|
+
}
|
|
49
|
+
const data = (await response.json());
|
|
50
|
+
return { ok: true, data };
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return { ok: false, error: message };
|
|
55
|
+
}
|
|
56
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentrace",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for Agentrace - Claude Code session tracker",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agentrace": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"claude",
|
|
20
|
+
"claude-code",
|
|
21
|
+
"agentrace",
|
|
22
|
+
"session",
|
|
23
|
+
"tracker",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"author": "satetsu888",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/satetsu888/agentrace.git",
|
|
31
|
+
"directory": "cli"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/satetsu888/agentrace",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/satetsu888/agentrace/issues"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^12.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.0.0",
|
|
42
|
+
"tsx": "^4.0.0",
|
|
43
|
+
"typescript": "^5.0.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|