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.
@@ -0,0 +1,5 @@
1
+ export interface InitOptions {
2
+ url?: string;
3
+ dev?: boolean;
4
+ }
5
+ export declare function initCommand(options?: InitOptions): Promise<void>;
@@ -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,4 @@
1
+ export interface OnOptions {
2
+ dev?: boolean;
3
+ }
4
+ export declare function onCommand(options?: OnOptions): Promise<void>;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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,6 @@
1
+ export interface OpenBrowserResult {
2
+ success: boolean;
3
+ message: string;
4
+ }
5
+ export declare function openBrowser(url: string): Promise<OpenBrowserResult>;
6
+ export declare function buildSetupUrl(serverUrl: string, token: string, callbackUrl: string): string;
@@ -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
+ }