@contextgraph/agent 0.4.0

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/index.js ADDED
@@ -0,0 +1,1793 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+ import { readFileSync as readFileSync2 } from "fs";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
+ import { dirname as dirname2, join as join4 } from "path";
8
+
9
+ // src/callback-server.ts
10
+ import http from "http";
11
+ import { URL } from "url";
12
+ var MIN_PORT = 3e3;
13
+ var MAX_PORT = 3100;
14
+ async function findFreePort() {
15
+ for (let port = MIN_PORT; port <= MAX_PORT; port++) {
16
+ const isAvailable = await checkPortAvailable(port);
17
+ if (isAvailable) {
18
+ return port;
19
+ }
20
+ }
21
+ throw new Error(`No free ports found between ${MIN_PORT} and ${MAX_PORT}`);
22
+ }
23
+ function checkPortAvailable(port) {
24
+ return new Promise((resolve) => {
25
+ const server = http.createServer();
26
+ server.once("error", () => {
27
+ resolve(false);
28
+ });
29
+ server.once("listening", () => {
30
+ server.close();
31
+ resolve(true);
32
+ });
33
+ server.listen(port);
34
+ });
35
+ }
36
+ async function startCallbackServer() {
37
+ const port = await findFreePort();
38
+ let callbackResolve = null;
39
+ const server = http.createServer((req, res) => {
40
+ const url = new URL(req.url || "/", `http://localhost:${port}`);
41
+ if (url.pathname === "/callback") {
42
+ const token = url.searchParams.get("token");
43
+ const userId = url.searchParams.get("userId");
44
+ if (!token) {
45
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
46
+ res.end(getErrorPage("Missing token parameter"));
47
+ return;
48
+ }
49
+ if (!userId) {
50
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
51
+ res.end(getErrorPage("Missing userId parameter"));
52
+ return;
53
+ }
54
+ if (callbackResolve) {
55
+ callbackResolve({ token, userId });
56
+ }
57
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
58
+ res.end(getSuccessPage());
59
+ } else {
60
+ res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
61
+ res.end(getNotFoundPage());
62
+ }
63
+ });
64
+ await new Promise((resolve) => {
65
+ server.listen(port, resolve);
66
+ });
67
+ return {
68
+ port,
69
+ waitForCallback: () => {
70
+ return new Promise((resolve) => {
71
+ callbackResolve = resolve;
72
+ });
73
+ },
74
+ close: () => {
75
+ return new Promise((resolve, reject) => {
76
+ server.close((err) => {
77
+ if (err) {
78
+ reject(err);
79
+ } else {
80
+ resolve();
81
+ }
82
+ });
83
+ });
84
+ }
85
+ };
86
+ }
87
+ function getSuccessPage() {
88
+ return `
89
+ <!DOCTYPE html>
90
+ <html>
91
+ <head>
92
+ <meta charset="utf-8">
93
+ <title>Authentication Successful</title>
94
+ <style>
95
+ body {
96
+ font-family: system-ui, -apple-system, sans-serif;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ min-height: 100vh;
101
+ margin: 0;
102
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
103
+ }
104
+ .container {
105
+ background: white;
106
+ padding: 3rem;
107
+ border-radius: 1rem;
108
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
109
+ text-align: center;
110
+ max-width: 400px;
111
+ }
112
+ .icon {
113
+ font-size: 4rem;
114
+ margin-bottom: 1rem;
115
+ }
116
+ h1 {
117
+ color: #667eea;
118
+ margin: 0 0 1rem 0;
119
+ font-size: 1.5rem;
120
+ }
121
+ p {
122
+ color: #666;
123
+ margin: 0;
124
+ }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div class="container">
129
+ <div class="icon">\u2705</div>
130
+ <h1>Authentication successful!</h1>
131
+ <p>You can close this window and return to your terminal.</p>
132
+ </div>
133
+ </body>
134
+ </html>
135
+ `.trim();
136
+ }
137
+ function getErrorPage(message) {
138
+ return `
139
+ <!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <meta charset="utf-8">
143
+ <title>Authentication Error</title>
144
+ <style>
145
+ body {
146
+ font-family: system-ui, -apple-system, sans-serif;
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: center;
150
+ min-height: 100vh;
151
+ margin: 0;
152
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
153
+ }
154
+ .container {
155
+ background: white;
156
+ padding: 3rem;
157
+ border-radius: 1rem;
158
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
159
+ text-align: center;
160
+ max-width: 400px;
161
+ }
162
+ .icon {
163
+ font-size: 4rem;
164
+ margin-bottom: 1rem;
165
+ }
166
+ h1 {
167
+ color: #f5576c;
168
+ margin: 0 0 1rem 0;
169
+ font-size: 1.5rem;
170
+ }
171
+ p {
172
+ color: #666;
173
+ margin: 0;
174
+ }
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <div class="container">
179
+ <div class="icon">\u274C</div>
180
+ <h1>Authentication error</h1>
181
+ <p>${message}</p>
182
+ </div>
183
+ </body>
184
+ </html>
185
+ `.trim();
186
+ }
187
+ function getNotFoundPage() {
188
+ return `
189
+ <!DOCTYPE html>
190
+ <html>
191
+ <head>
192
+ <meta charset="utf-8">
193
+ <title>Not Found</title>
194
+ <style>
195
+ body {
196
+ font-family: system-ui, -apple-system, sans-serif;
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ min-height: 100vh;
201
+ margin: 0;
202
+ background: #f0f0f0;
203
+ }
204
+ .container {
205
+ background: white;
206
+ padding: 3rem;
207
+ border-radius: 1rem;
208
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
209
+ text-align: center;
210
+ max-width: 400px;
211
+ }
212
+ h1 {
213
+ color: #666;
214
+ margin: 0;
215
+ }
216
+ </style>
217
+ </head>
218
+ <body>
219
+ <div class="container">
220
+ <h1>404 Not Found</h1>
221
+ </div>
222
+ </body>
223
+ </html>
224
+ `.trim();
225
+ }
226
+
227
+ // src/credentials.ts
228
+ import fs from "fs/promises";
229
+ import path from "path";
230
+ import os from "os";
231
+ function getCredentialsDir() {
232
+ return process.env.CONTEXTGRAPH_CREDENTIALS_DIR || path.join(os.homedir(), ".contextgraph");
233
+ }
234
+ function getCredentialsPath() {
235
+ return path.join(getCredentialsDir(), "credentials.json");
236
+ }
237
+ var CREDENTIALS_DIR = getCredentialsDir();
238
+ var CREDENTIALS_PATH = getCredentialsPath();
239
+ async function saveCredentials(credentials) {
240
+ const dir = getCredentialsDir();
241
+ const filePath = getCredentialsPath();
242
+ await fs.mkdir(dir, { recursive: true, mode: 448 });
243
+ const content = JSON.stringify(credentials, null, 2);
244
+ await fs.writeFile(filePath, content, { mode: 384 });
245
+ }
246
+ async function loadCredentials() {
247
+ const apiToken = process.env.CONTEXTGRAPH_API_TOKEN;
248
+ if (apiToken) {
249
+ const farFuture = new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toISOString();
250
+ return {
251
+ clerkToken: apiToken,
252
+ userId: "api-token-user",
253
+ // Placeholder - server will resolve actual user
254
+ expiresAt: farFuture,
255
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
256
+ };
257
+ }
258
+ const filePath = getCredentialsPath();
259
+ try {
260
+ const content = await fs.readFile(filePath, "utf-8");
261
+ return JSON.parse(content);
262
+ } catch (error) {
263
+ if (error.code === "ENOENT") {
264
+ return null;
265
+ }
266
+ console.error("Error loading credentials:", error);
267
+ return null;
268
+ }
269
+ }
270
+ function isExpired(credentials) {
271
+ return new Date(credentials.expiresAt) <= /* @__PURE__ */ new Date();
272
+ }
273
+ function isTokenExpired(token) {
274
+ try {
275
+ const parts = token.split(".");
276
+ if (parts.length !== 3) {
277
+ return false;
278
+ }
279
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
280
+ const now = Math.floor(Date.now() / 1e3);
281
+ if (!payload.exp) {
282
+ return true;
283
+ }
284
+ if (payload.exp <= now) {
285
+ return true;
286
+ }
287
+ if (payload.nbf && payload.nbf > now) {
288
+ return true;
289
+ }
290
+ return false;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+
296
+ // src/auth-flow.ts
297
+ var DEFAULT_TIMEOUT = 5 * 60 * 1e3;
298
+ var DEFAULT_BASE_URL = "https://www.contextgraph.dev";
299
+ async function defaultOpenBrowser(url) {
300
+ const open = (await import("open")).default;
301
+ await open(url);
302
+ }
303
+ async function authenticateAgent(options = {}) {
304
+ const {
305
+ baseUrl = DEFAULT_BASE_URL,
306
+ timeout = DEFAULT_TIMEOUT,
307
+ openBrowser = defaultOpenBrowser
308
+ } = options;
309
+ let server;
310
+ try {
311
+ server = await startCallbackServer();
312
+ const { port, waitForCallback, close } = server;
313
+ const authUrl = `${baseUrl}/auth/cli-callback?port=${port}`;
314
+ console.log(`Opening browser to: ${authUrl}`);
315
+ await openBrowser(authUrl);
316
+ const timeoutPromise = new Promise((_, reject) => {
317
+ setTimeout(() => reject(new Error("Authentication timeout")), timeout);
318
+ });
319
+ const result = await Promise.race([waitForCallback(), timeoutPromise]);
320
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString();
321
+ await saveCredentials({
322
+ clerkToken: result.token,
323
+ userId: result.userId,
324
+ expiresAt,
325
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
326
+ });
327
+ await close();
328
+ return {
329
+ success: true,
330
+ credentials: {
331
+ token: result.token,
332
+ userId: result.userId
333
+ }
334
+ };
335
+ } catch (error) {
336
+ if (server) {
337
+ await server.close();
338
+ }
339
+ return {
340
+ success: false,
341
+ error: error instanceof Error ? error.message : "Unknown error"
342
+ };
343
+ }
344
+ }
345
+
346
+ // src/workflows/auth.ts
347
+ async function runAuth() {
348
+ console.log("Starting authentication flow...\n");
349
+ const result = await authenticateAgent();
350
+ if (result.success) {
351
+ console.log("\n\u2705 Authentication successful!");
352
+ console.log(`User ID: ${result.credentials.userId}`);
353
+ } else {
354
+ console.error("\n\u274C Authentication failed:", result.error);
355
+ process.exit(1);
356
+ }
357
+ }
358
+
359
+ // src/claude-sdk.ts
360
+ import { query } from "@anthropic-ai/claude-agent-sdk";
361
+
362
+ // src/plugin-setup.ts
363
+ import { spawn } from "child_process";
364
+ import { access, mkdir } from "fs/promises";
365
+ import { join } from "path";
366
+ import { homedir } from "os";
367
+ var PLUGIN_REPO = "https://github.com/contextgraph/claude-code-plugin.git";
368
+ var PLUGIN_DIR = join(homedir(), ".contextgraph", "claude-code-plugin");
369
+ var PLUGIN_PATH = join(PLUGIN_DIR, "plugins", "contextgraph");
370
+ async function ensurePlugin() {
371
+ try {
372
+ await access(PLUGIN_PATH);
373
+ console.log(`\u{1F4E6} Using plugin: ${PLUGIN_PATH}`);
374
+ return PLUGIN_PATH;
375
+ } catch {
376
+ }
377
+ let repoDirExists = false;
378
+ try {
379
+ await access(PLUGIN_DIR);
380
+ repoDirExists = true;
381
+ } catch {
382
+ }
383
+ if (repoDirExists) {
384
+ console.log("\u{1F4E6} Plugin directory exists but incomplete, pulling latest...");
385
+ await runCommand("git", ["pull"], PLUGIN_DIR);
386
+ try {
387
+ await access(PLUGIN_PATH);
388
+ console.log(`\u{1F4E6} Plugin ready: ${PLUGIN_PATH}`);
389
+ return PLUGIN_PATH;
390
+ } catch {
391
+ throw new Error(`Plugin not found at ${PLUGIN_PATH} even after git pull. Check repository structure.`);
392
+ }
393
+ }
394
+ console.log(`\u{1F4E6} Cloning plugin from ${PLUGIN_REPO}...`);
395
+ const contextgraphDir = join(homedir(), ".contextgraph");
396
+ try {
397
+ await mkdir(contextgraphDir, { recursive: true });
398
+ } catch {
399
+ }
400
+ await runCommand("git", ["clone", PLUGIN_REPO, PLUGIN_DIR]);
401
+ try {
402
+ await access(PLUGIN_PATH);
403
+ console.log(`\u{1F4E6} Plugin installed: ${PLUGIN_PATH}`);
404
+ return PLUGIN_PATH;
405
+ } catch {
406
+ throw new Error(`Plugin clone succeeded but plugin path not found at ${PLUGIN_PATH}`);
407
+ }
408
+ }
409
+ function runCommand(command, args, cwd) {
410
+ return new Promise((resolve, reject) => {
411
+ const proc = spawn(command, args, { cwd, stdio: "inherit" });
412
+ proc.on("close", (code) => {
413
+ if (code === 0) {
414
+ resolve();
415
+ } else {
416
+ reject(new Error(`${command} ${args[0]} failed with exit code ${code}`));
417
+ }
418
+ });
419
+ proc.on("error", (err) => {
420
+ reject(new Error(`Failed to spawn ${command}: ${err.message}`));
421
+ });
422
+ });
423
+ }
424
+
425
+ // src/sdk-event-transformer.ts
426
+ function transformSDKMessage(message) {
427
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
428
+ switch (message.type) {
429
+ case "system":
430
+ return transformSystemMessage(message, timestamp);
431
+ case "assistant":
432
+ return transformAssistantMessage(message, timestamp);
433
+ case "result":
434
+ return transformResultMessage(message, timestamp);
435
+ case "user":
436
+ return transformUserMessage(message, timestamp);
437
+ default:
438
+ return null;
439
+ }
440
+ }
441
+ function transformSystemMessage(message, timestamp) {
442
+ return {
443
+ eventType: "claude_message",
444
+ content: message.content || `System: ${message.subtype || "initialization"}`,
445
+ data: {
446
+ type: "system",
447
+ subtype: message.subtype,
448
+ content: message.content,
449
+ session_id: message.session_id
450
+ },
451
+ timestamp
452
+ };
453
+ }
454
+ function transformAssistantMessage(message, timestamp) {
455
+ const content = message.message?.content;
456
+ if (!content || !Array.isArray(content)) {
457
+ return null;
458
+ }
459
+ const contentSummary = generateContentSummary(content);
460
+ return {
461
+ eventType: "claude_message",
462
+ content: contentSummary,
463
+ data: {
464
+ type: "assistant",
465
+ message: message.message,
466
+ session_id: message.session_id,
467
+ parent_tool_use_id: message.parent_tool_use_id
468
+ },
469
+ timestamp
470
+ };
471
+ }
472
+ function transformResultMessage(message, timestamp) {
473
+ const isSuccess = message.subtype === "success";
474
+ const durationSec = message.duration_ms ? (message.duration_ms / 1e3).toFixed(1) : "unknown";
475
+ return {
476
+ eventType: "claude_message",
477
+ content: isSuccess ? `Completed successfully in ${durationSec}s` : `Execution ${message.subtype}: ${durationSec}s`,
478
+ data: {
479
+ type: "result",
480
+ subtype: message.subtype,
481
+ duration_ms: message.duration_ms,
482
+ total_cost_usd: message.total_cost_usd,
483
+ num_turns: message.num_turns,
484
+ usage: message.usage,
485
+ session_id: message.session_id
486
+ },
487
+ timestamp
488
+ };
489
+ }
490
+ function transformUserMessage(message, timestamp) {
491
+ const content = message.message?.content;
492
+ if (!content || !Array.isArray(content)) {
493
+ return null;
494
+ }
495
+ const hasToolResults = content.some(
496
+ (block) => block.type === "tool_result"
497
+ );
498
+ if (!hasToolResults) {
499
+ return null;
500
+ }
501
+ const summaries = content.filter((block) => block.type === "tool_result").map((block) => {
502
+ const prefix = block.is_error ? "\u274C" : "\u2713";
503
+ const resultText = extractToolResultText(block.content);
504
+ return `${prefix} ${resultText.substring(0, 100)}${resultText.length > 100 ? "..." : ""}`;
505
+ });
506
+ return {
507
+ eventType: "claude_message",
508
+ content: summaries.join("\n"),
509
+ data: {
510
+ type: "user",
511
+ message: message.message,
512
+ session_id: message.session_id
513
+ },
514
+ timestamp
515
+ };
516
+ }
517
+ function generateContentSummary(content) {
518
+ const parts = [];
519
+ for (const block of content) {
520
+ if (block.type === "text" && block.text) {
521
+ const text = block.text.length > 200 ? block.text.substring(0, 200) + "..." : block.text;
522
+ parts.push(text);
523
+ } else if (block.type === "tool_use") {
524
+ parts.push(`\u{1F527} ${block.name}`);
525
+ } else if (block.type === "thinking") {
526
+ parts.push("\u{1F4AD} [thinking]");
527
+ }
528
+ }
529
+ return parts.join(" | ") || "[no content]";
530
+ }
531
+ function extractToolResultText(content) {
532
+ if (!content) return "";
533
+ if (typeof content === "string") {
534
+ return content;
535
+ }
536
+ if (Array.isArray(content)) {
537
+ return content.filter((block) => block.type === "text" && block.text).map((block) => block.text).join("\n");
538
+ }
539
+ return "";
540
+ }
541
+
542
+ // src/claude-sdk.ts
543
+ var EXECUTION_TIMEOUT_MS = 20 * 60 * 1e3;
544
+ var THINKING_TRUNCATE_LENGTH = 100;
545
+ var COMMAND_TRUNCATE_LENGTH = 60;
546
+ function formatToolUse(content) {
547
+ if (content.type === "tool_use") {
548
+ const name = content.name || "unknown";
549
+ const summary = formatToolInput(name, content.input);
550
+ return ` \u{1F527} ${name}${summary}`;
551
+ }
552
+ if (content.type === "thinking" && content.thinking) {
553
+ const truncated = content.thinking.length > THINKING_TRUNCATE_LENGTH ? content.thinking.substring(0, THINKING_TRUNCATE_LENGTH) + "..." : content.thinking;
554
+ return ` \u{1F4AD} ${truncated}`;
555
+ }
556
+ return "";
557
+ }
558
+ function formatToolInput(toolName, input) {
559
+ if (!input) return "";
560
+ switch (toolName) {
561
+ case "Read":
562
+ return `: ${input.file_path}`;
563
+ case "Edit":
564
+ case "Write":
565
+ return `: ${input.file_path}`;
566
+ case "Bash":
567
+ const cmd = input.command || "";
568
+ const truncated = cmd.length > COMMAND_TRUNCATE_LENGTH ? cmd.substring(0, COMMAND_TRUNCATE_LENGTH) + "..." : cmd;
569
+ return `: ${truncated}`;
570
+ case "Grep":
571
+ return `: "${input.pattern}"`;
572
+ case "Glob":
573
+ return `: ${input.pattern}`;
574
+ default:
575
+ return "";
576
+ }
577
+ }
578
+ function formatAssistantMessage(content) {
579
+ const lines = [];
580
+ for (const item of content) {
581
+ if (item.type === "text" && item.text) {
582
+ lines.push(` ${item.text}`);
583
+ } else if (item.type === "tool_use" || item.type === "thinking") {
584
+ const formatted = formatToolUse(item);
585
+ if (formatted) lines.push(formatted);
586
+ }
587
+ }
588
+ return lines.join("\n");
589
+ }
590
+ function formatMessage(message) {
591
+ switch (message.type) {
592
+ case "system":
593
+ if (message.subtype === "init") {
594
+ return "\u{1F680} Claude session initialized";
595
+ }
596
+ return null;
597
+ case "assistant":
598
+ const assistantMsg = message;
599
+ if (assistantMsg.message?.content && Array.isArray(assistantMsg.message.content)) {
600
+ return formatAssistantMessage(assistantMsg.message.content);
601
+ }
602
+ return null;
603
+ case "result":
604
+ const resultMsg = message;
605
+ if (resultMsg.subtype === "success") {
606
+ const duration = resultMsg.duration_ms ? `${(resultMsg.duration_ms / 1e3).toFixed(1)}s` : "unknown";
607
+ return `\u2705 Completed in ${duration}`;
608
+ } else if (resultMsg.subtype.startsWith("error_")) {
609
+ return "\u274C Execution failed";
610
+ }
611
+ return null;
612
+ default:
613
+ return null;
614
+ }
615
+ }
616
+ async function executeClaude(options) {
617
+ let sessionId;
618
+ let totalCost = 0;
619
+ let usage;
620
+ const abortController = new AbortController();
621
+ const timeout = setTimeout(() => {
622
+ abortController.abort();
623
+ }, EXECUTION_TIMEOUT_MS);
624
+ try {
625
+ const pluginPath = await ensurePlugin();
626
+ console.log("[Agent SDK] Loading plugin from:", pluginPath);
627
+ console.log("[Agent SDK] Auth token available:", !!options.authToken);
628
+ console.log("[Agent SDK] Anthropic API key available:", !!process.env.ANTHROPIC_API_KEY);
629
+ console.log("[Agent SDK] Claude OAuth token available:", !!process.env.CLAUDE_CODE_OAUTH_TOKEN);
630
+ const iterator = query({
631
+ prompt: options.prompt,
632
+ options: {
633
+ cwd: options.cwd,
634
+ abortController,
635
+ permissionMode: "bypassPermissions",
636
+ // Allow MCP tools to execute automatically
637
+ maxTurns: 100,
638
+ // Reasonable limit
639
+ env: {
640
+ ...process.env,
641
+ // Pass auth token through environment for MCP server
642
+ CONTEXTGRAPH_AUTH_TOKEN: options.authToken || "",
643
+ // Pass Anthropic API key for SDK authentication
644
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "",
645
+ // Pass Claude OAuth token for SDK authentication (alternative to API key)
646
+ CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || ""
647
+ },
648
+ // Load the contextgraph plugin (provides MCP server URL and other config)
649
+ plugins: [
650
+ {
651
+ type: "local",
652
+ path: pluginPath
653
+ }
654
+ ]
655
+ // Note: Auth is passed via CONTEXTGRAPH_AUTH_TOKEN environment variable above
656
+ }
657
+ });
658
+ for await (const message of iterator) {
659
+ if (!sessionId && message.session_id) {
660
+ sessionId = message.session_id;
661
+ }
662
+ const formatted = formatMessage(message);
663
+ if (formatted) {
664
+ console.log(formatted);
665
+ }
666
+ if (options.onLogEvent) {
667
+ try {
668
+ const logEvent = transformSDKMessage(message);
669
+ if (logEvent) {
670
+ options.onLogEvent(logEvent);
671
+ }
672
+ } catch (error) {
673
+ console.error("[Log Transform]", error instanceof Error ? error.message : String(error));
674
+ }
675
+ }
676
+ if (message.type === "result") {
677
+ const resultMsg = message;
678
+ totalCost = resultMsg.total_cost_usd || 0;
679
+ usage = resultMsg.usage;
680
+ if (resultMsg.subtype.startsWith("error_")) {
681
+ clearTimeout(timeout);
682
+ return {
683
+ exitCode: 1,
684
+ sessionId,
685
+ usage,
686
+ cost: totalCost
687
+ };
688
+ }
689
+ }
690
+ }
691
+ clearTimeout(timeout);
692
+ return {
693
+ exitCode: 0,
694
+ sessionId,
695
+ usage,
696
+ cost: totalCost
697
+ };
698
+ } catch (error) {
699
+ clearTimeout(timeout);
700
+ if (abortController.signal.aborted) {
701
+ const timeoutMinutes = EXECUTION_TIMEOUT_MS / (60 * 1e3);
702
+ throw new Error(`Claude SDK execution timed out after ${timeoutMinutes} minutes`);
703
+ }
704
+ throw new Error(`Failed to execute Claude SDK: ${error.message}`);
705
+ }
706
+ }
707
+
708
+ // src/fetch-with-retry.ts
709
+ async function fetchWithRetry(url, options, retryOptions = {}) {
710
+ const { maxRetries = 3, baseDelayMs = 1e3, maxDelayMs = 1e4 } = retryOptions;
711
+ let lastError = null;
712
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
713
+ try {
714
+ const response = await fetch(url, options);
715
+ if (response.ok || response.status >= 400 && response.status < 500) {
716
+ return response;
717
+ }
718
+ lastError = new Error(`HTTP ${response.status}`);
719
+ } catch (error) {
720
+ lastError = error instanceof Error ? error : new Error(String(error));
721
+ }
722
+ if (attempt < maxRetries) {
723
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
724
+ const jitter = delay * 0.1 * Math.random();
725
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
726
+ }
727
+ }
728
+ throw lastError ?? new Error("Request failed after retries");
729
+ }
730
+
731
+ // src/log-transport.ts
732
+ var DEFAULT_RETRY_CONFIG = {
733
+ maxRetries: 3,
734
+ initialDelayMs: 100,
735
+ backoffFactor: 2
736
+ };
737
+ var LogTransportService = class {
738
+ constructor(baseUrl, authToken, runId, retryConfig) {
739
+ this.baseUrl = baseUrl;
740
+ this.authToken = authToken;
741
+ this.runId = runId ?? null;
742
+ this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig };
743
+ }
744
+ runId = null;
745
+ retryConfig;
746
+ /**
747
+ * Get the current run ID
748
+ */
749
+ getRunId() {
750
+ return this.runId;
751
+ }
752
+ /**
753
+ * Create a new run for an action
754
+ * @param actionId - The action ID this run is executing
755
+ * @returns The created run ID
756
+ */
757
+ async createRun(actionId) {
758
+ const response = await this.makeRequest("/api/runs", {
759
+ method: "POST",
760
+ body: JSON.stringify({
761
+ actionId,
762
+ state: "queued"
763
+ })
764
+ });
765
+ const result = await response.json();
766
+ if (!result.success) {
767
+ throw new Error(result.error || "Failed to create run");
768
+ }
769
+ this.runId = result.data.runId;
770
+ return this.runId;
771
+ }
772
+ /**
773
+ * Start the run (transition to running state)
774
+ * Called when execution begins
775
+ */
776
+ async startRun() {
777
+ if (!this.runId) {
778
+ throw new Error("No run ID set. Call createRun() first.");
779
+ }
780
+ const response = await this.makeRequest(`/api/runs/${this.runId}/start`, {
781
+ method: "POST",
782
+ body: JSON.stringify({})
783
+ });
784
+ const result = await response.json();
785
+ if (!result.success) {
786
+ throw new Error(result.error || "Failed to start run");
787
+ }
788
+ }
789
+ /**
790
+ * Finish the run with an outcome
791
+ * @param outcome - 'success' | 'error' | 'timeout' | 'incomplete'
792
+ * @param metadata - Optional metadata (exitCode, errorMessage, cost, usage)
793
+ */
794
+ async finishRun(outcome, metadata) {
795
+ if (!this.runId) {
796
+ throw new Error("No run ID set. Call createRun() first.");
797
+ }
798
+ const response = await this.makeRequest(`/api/runs/${this.runId}/finish`, {
799
+ method: "POST",
800
+ body: JSON.stringify({
801
+ outcome,
802
+ exitCode: metadata?.exitCode?.toString(),
803
+ errorMessage: metadata?.errorMessage
804
+ })
805
+ });
806
+ const result = await response.json();
807
+ if (!result.success) {
808
+ const error = result.error || "Failed to finish run";
809
+ if (error.includes("summarizing") || error.includes("finished")) {
810
+ console.log("[LogTransport] Run is already being finished by server, skipping client finish");
811
+ return;
812
+ }
813
+ throw new Error(error);
814
+ }
815
+ }
816
+ /**
817
+ * Update the state of the current run
818
+ * @deprecated Use startRun() and finishRun() instead
819
+ * @param state - New state for the run
820
+ * @param metadata - Optional metadata to include with the state update
821
+ */
822
+ async updateRunState(state, metadata) {
823
+ if (!this.runId) {
824
+ throw new Error("No run ID set. Call createRun() first.");
825
+ }
826
+ if (state === "executing" || state === "preparing" || state === "running") {
827
+ await this.startRun();
828
+ } else if (state === "completed" || state === "failed") {
829
+ const outcome = state === "completed" ? "success" : "error";
830
+ await this.finishRun(outcome, {
831
+ exitCode: metadata?.exitCode,
832
+ errorMessage: metadata?.error,
833
+ cost: metadata?.cost,
834
+ usage: metadata?.usage
835
+ });
836
+ } else {
837
+ console.warn(`[LogTransport] Unknown state '${state}' - no API call made`);
838
+ }
839
+ }
840
+ /**
841
+ * Send a batch of log events to the platform
842
+ * @param events - Array of log events to send
843
+ * @param workerId - Optional worker ID
844
+ * @returns Success status and number of events received
845
+ */
846
+ async sendBatch(events, workerId) {
847
+ if (!this.runId) {
848
+ throw new Error("No run ID set. Call createRun() first.");
849
+ }
850
+ if (events.length === 0) {
851
+ return { success: true, eventsReceived: 0 };
852
+ }
853
+ const response = await this.makeRequest("/api/agents/log/event", {
854
+ method: "POST",
855
+ body: JSON.stringify({
856
+ runId: this.runId,
857
+ events,
858
+ ...workerId && { workerId }
859
+ })
860
+ });
861
+ const result = await response.json();
862
+ if (!result.success) {
863
+ throw new Error(result.error || "Failed to send log batch");
864
+ }
865
+ return {
866
+ success: true,
867
+ eventsReceived: result.data?.eventsReceived ?? events.length
868
+ };
869
+ }
870
+ /**
871
+ * Make an HTTP request with retry logic
872
+ */
873
+ async makeRequest(path2, options) {
874
+ const url = `${this.baseUrl}${path2}`;
875
+ const headers = {
876
+ "x-authorization": `Bearer ${this.authToken}`,
877
+ "Content-Type": "application/json"
878
+ };
879
+ let lastError = null;
880
+ let delay = this.retryConfig.initialDelayMs;
881
+ for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
882
+ try {
883
+ const response = await fetch(url, {
884
+ ...options,
885
+ headers: {
886
+ ...headers,
887
+ ...options.headers || {}
888
+ }
889
+ });
890
+ if (response.status >= 400 && response.status < 500) {
891
+ return response;
892
+ }
893
+ if (!response.ok) {
894
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
895
+ }
896
+ return response;
897
+ } catch (error) {
898
+ lastError = error instanceof Error ? error : new Error(String(error));
899
+ if (attempt < this.retryConfig.maxRetries) {
900
+ await this.sleep(delay);
901
+ delay *= this.retryConfig.backoffFactor;
902
+ }
903
+ }
904
+ }
905
+ throw new Error(
906
+ `Request failed after ${this.retryConfig.maxRetries + 1} attempts: ${lastError?.message}`
907
+ );
908
+ }
909
+ /**
910
+ * Sleep for a given number of milliseconds
911
+ */
912
+ sleep(ms) {
913
+ return new Promise((resolve) => setTimeout(resolve, ms));
914
+ }
915
+ };
916
+
917
+ // src/log-buffer.ts
918
+ var LOG_BUFFER_FLUSH_INTERVAL_MS = 500;
919
+ var LOG_BUFFER_MAX_SIZE = 50;
920
+ var LOG_BUFFER_MAX_QUEUE_SIZE = 1e3;
921
+ var LogBuffer = class {
922
+ constructor(transport, flushIntervalMs = LOG_BUFFER_FLUSH_INTERVAL_MS, maxBufferSize = LOG_BUFFER_MAX_SIZE, maxQueueSize = LOG_BUFFER_MAX_QUEUE_SIZE) {
923
+ this.transport = transport;
924
+ this.flushIntervalMs = flushIntervalMs;
925
+ this.maxBufferSize = maxBufferSize;
926
+ this.maxQueueSize = maxQueueSize;
927
+ }
928
+ buffer = [];
929
+ flushIntervalId = null;
930
+ isFlushing = false;
931
+ /**
932
+ * Add an event to the buffer (fire-and-forget)
933
+ * Handles backpressure by dropping oldest events if queue is full.
934
+ */
935
+ push(event) {
936
+ if (this.buffer.length >= this.maxQueueSize) {
937
+ this.buffer.shift();
938
+ }
939
+ this.buffer.push(event);
940
+ if (this.buffer.length >= this.maxBufferSize) {
941
+ this.flushAsync();
942
+ }
943
+ }
944
+ /**
945
+ * Start periodic flushing
946
+ */
947
+ start() {
948
+ if (this.flushIntervalId !== null) return;
949
+ this.flushIntervalId = setInterval(() => {
950
+ this.flushAsync();
951
+ }, this.flushIntervalMs);
952
+ }
953
+ /**
954
+ * Stop periodic flushing and flush remaining events
955
+ */
956
+ async stop() {
957
+ if (this.flushIntervalId !== null) {
958
+ clearInterval(this.flushIntervalId);
959
+ this.flushIntervalId = null;
960
+ }
961
+ await this.flush();
962
+ }
963
+ /**
964
+ * Async flush (fire-and-forget, non-blocking)
965
+ */
966
+ flushAsync() {
967
+ if (this.isFlushing || this.buffer.length === 0) return;
968
+ this.flush().catch((error) => {
969
+ console.error("[LogBuffer] Flush error:", error instanceof Error ? error.message : String(error));
970
+ });
971
+ }
972
+ /**
973
+ * Flush current buffer contents to transport
974
+ */
975
+ async flush() {
976
+ if (this.isFlushing || this.buffer.length === 0) return;
977
+ this.isFlushing = true;
978
+ const eventsToSend = [...this.buffer];
979
+ this.buffer = [];
980
+ try {
981
+ await this.transport.sendBatch(eventsToSend);
982
+ } catch (error) {
983
+ console.error("[LogBuffer] Failed to send batch:", error instanceof Error ? error.message : String(error));
984
+ } finally {
985
+ this.isFlushing = false;
986
+ }
987
+ }
988
+ };
989
+
990
+ // src/heartbeat-manager.ts
991
+ var HeartbeatManager = class {
992
+ constructor(baseUrl, authToken, runId) {
993
+ this.baseUrl = baseUrl;
994
+ this.authToken = authToken;
995
+ this.runId = runId;
996
+ }
997
+ intervalId = null;
998
+ currentPhase = "executing";
999
+ currentProgress = void 0;
1000
+ /**
1001
+ * Start sending periodic heartbeats
1002
+ * @param intervalMs - Time between heartbeats in milliseconds (default: 30000)
1003
+ */
1004
+ start(intervalMs = 3e4) {
1005
+ this.stop();
1006
+ this.sendHeartbeat();
1007
+ this.intervalId = setInterval(() => {
1008
+ this.sendHeartbeat();
1009
+ }, intervalMs);
1010
+ }
1011
+ /**
1012
+ * Stop sending heartbeats
1013
+ */
1014
+ stop() {
1015
+ if (this.intervalId !== null) {
1016
+ clearInterval(this.intervalId);
1017
+ this.intervalId = null;
1018
+ }
1019
+ }
1020
+ /**
1021
+ * Update the current phase and optional progress
1022
+ * @param phase - Current execution phase
1023
+ * @param progress - Optional progress percentage (0-100)
1024
+ */
1025
+ updatePhase(phase, progress) {
1026
+ this.currentPhase = phase;
1027
+ this.currentProgress = progress;
1028
+ }
1029
+ /**
1030
+ * Check if heartbeat manager is currently running
1031
+ */
1032
+ isRunning() {
1033
+ return this.intervalId !== null;
1034
+ }
1035
+ /**
1036
+ * Send a heartbeat to the platform (internal method)
1037
+ * Errors are logged but not thrown to avoid blocking execution.
1038
+ * Includes one retry attempt for transient network failures.
1039
+ */
1040
+ async sendHeartbeat() {
1041
+ const payload = {
1042
+ phase: this.currentPhase,
1043
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1044
+ };
1045
+ if (this.currentProgress !== void 0) {
1046
+ payload.progress = this.currentProgress;
1047
+ }
1048
+ const url = `${this.baseUrl}/api/runs/${this.runId}/heartbeat`;
1049
+ const requestOptions = {
1050
+ method: "POST",
1051
+ headers: {
1052
+ "x-authorization": `Bearer ${this.authToken}`,
1053
+ "Content-Type": "application/json"
1054
+ },
1055
+ body: JSON.stringify(payload)
1056
+ };
1057
+ for (let attempt = 0; attempt < 2; attempt++) {
1058
+ try {
1059
+ const response = await fetch(url, requestOptions);
1060
+ if (response.ok) {
1061
+ return;
1062
+ }
1063
+ if (response.status >= 400 && response.status < 500) {
1064
+ console.error(
1065
+ `Heartbeat failed: HTTP ${response.status} ${response.statusText}`
1066
+ );
1067
+ return;
1068
+ }
1069
+ if (attempt === 0) {
1070
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1071
+ }
1072
+ } catch (error) {
1073
+ if (attempt === 0) {
1074
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1075
+ } else {
1076
+ console.error(
1077
+ "Heartbeat error:",
1078
+ error instanceof Error ? error.message : String(error)
1079
+ );
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ };
1085
+
1086
+ // src/workflows/prepare.ts
1087
+ var API_BASE_URL = "https://www.contextgraph.dev";
1088
+ async function runPrepare(actionId, options) {
1089
+ const credentials = await loadCredentials();
1090
+ if (!credentials) {
1091
+ console.error("\u274C Not authenticated. Run authentication first.");
1092
+ process.exit(1);
1093
+ }
1094
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1095
+ console.error("\u274C Token expired. Re-authenticate to continue.");
1096
+ process.exit(1);
1097
+ }
1098
+ console.log(`Fetching preparation instructions for action ${actionId}...
1099
+ `);
1100
+ const response = await fetchWithRetry(
1101
+ `${API_BASE_URL}/api/prompts/prepare`,
1102
+ {
1103
+ method: "POST",
1104
+ headers: {
1105
+ "Authorization": `Bearer ${credentials.clerkToken}`,
1106
+ "Content-Type": "application/json"
1107
+ },
1108
+ body: JSON.stringify({ actionId })
1109
+ }
1110
+ );
1111
+ if (!response.ok) {
1112
+ const errorText = await response.text();
1113
+ throw new Error(`Failed to fetch prepare prompt: ${response.statusText}
1114
+ ${errorText}`);
1115
+ }
1116
+ const { prompt } = await response.json();
1117
+ const logTransport = new LogTransportService(API_BASE_URL, credentials.clerkToken);
1118
+ let runId;
1119
+ let heartbeatManager;
1120
+ let logBuffer;
1121
+ try {
1122
+ console.log("[Log Streaming] Creating run for prepare phase...");
1123
+ runId = await logTransport.createRun(actionId);
1124
+ console.log(`[Log Streaming] Run created: ${runId}`);
1125
+ await logTransport.updateRunState("preparing");
1126
+ heartbeatManager = new HeartbeatManager(API_BASE_URL, credentials.clerkToken, runId);
1127
+ heartbeatManager.start();
1128
+ console.log("[Log Streaming] Heartbeat started");
1129
+ logBuffer = new LogBuffer(logTransport);
1130
+ logBuffer.start();
1131
+ console.log("Spawning Claude for preparation...\n");
1132
+ const claudeResult = await executeClaude({
1133
+ prompt,
1134
+ cwd: options?.cwd || process.cwd(),
1135
+ authToken: credentials.clerkToken,
1136
+ onLogEvent: (event) => {
1137
+ logBuffer.push(event);
1138
+ }
1139
+ });
1140
+ if (claudeResult.exitCode === 0) {
1141
+ await logTransport.finishRun("success", {
1142
+ exitCode: claudeResult.exitCode,
1143
+ cost: claudeResult.cost,
1144
+ usage: claudeResult.usage
1145
+ });
1146
+ console.log("\n\u2705 Preparation complete");
1147
+ } else {
1148
+ await logTransport.finishRun("error", {
1149
+ exitCode: claudeResult.exitCode,
1150
+ errorMessage: `Claude preparation failed with exit code ${claudeResult.exitCode}`
1151
+ });
1152
+ console.error(`
1153
+ \u274C Claude preparation failed with exit code ${claudeResult.exitCode}`);
1154
+ process.exit(1);
1155
+ }
1156
+ } catch (error) {
1157
+ if (runId) {
1158
+ try {
1159
+ await logTransport.finishRun("error", {
1160
+ errorMessage: error instanceof Error ? error.message : String(error)
1161
+ });
1162
+ } catch (stateError) {
1163
+ console.error("[Log Streaming] Failed to update run state:", stateError);
1164
+ }
1165
+ }
1166
+ throw error;
1167
+ } finally {
1168
+ if (heartbeatManager) {
1169
+ heartbeatManager.stop();
1170
+ console.log("[Log Streaming] Heartbeat stopped");
1171
+ }
1172
+ if (logBuffer) {
1173
+ await logBuffer.stop();
1174
+ console.log("[Log Streaming] Logs flushed");
1175
+ }
1176
+ }
1177
+ }
1178
+
1179
+ // src/workflows/execute.ts
1180
+ var API_BASE_URL2 = "https://www.contextgraph.dev";
1181
+ async function runExecute(actionId, options) {
1182
+ const credentials = await loadCredentials();
1183
+ if (!credentials) {
1184
+ console.error("\u274C Not authenticated. Run authentication first.");
1185
+ process.exit(1);
1186
+ }
1187
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1188
+ console.error("\u274C Token expired. Re-authenticate to continue.");
1189
+ process.exit(1);
1190
+ }
1191
+ console.log(`Fetching execution instructions for action ${actionId}...
1192
+ `);
1193
+ const response = await fetchWithRetry(
1194
+ `${API_BASE_URL2}/api/prompts/execute`,
1195
+ {
1196
+ method: "POST",
1197
+ headers: {
1198
+ "Authorization": `Bearer ${credentials.clerkToken}`,
1199
+ "Content-Type": "application/json"
1200
+ },
1201
+ body: JSON.stringify({ actionId })
1202
+ }
1203
+ );
1204
+ if (!response.ok) {
1205
+ const errorText = await response.text();
1206
+ throw new Error(`Failed to fetch execute prompt: ${response.statusText}
1207
+ ${errorText}`);
1208
+ }
1209
+ const { prompt } = await response.json();
1210
+ const logTransport = new LogTransportService(API_BASE_URL2, credentials.clerkToken);
1211
+ let runId;
1212
+ let heartbeatManager;
1213
+ let logBuffer;
1214
+ try {
1215
+ console.log("[Log Streaming] Creating run...");
1216
+ runId = await logTransport.createRun(actionId);
1217
+ console.log(`[Log Streaming] Run created: ${runId}`);
1218
+ await logTransport.updateRunState("executing");
1219
+ heartbeatManager = new HeartbeatManager(API_BASE_URL2, credentials.clerkToken, runId);
1220
+ heartbeatManager.start();
1221
+ console.log("[Log Streaming] Heartbeat started");
1222
+ logBuffer = new LogBuffer(logTransport);
1223
+ logBuffer.start();
1224
+ console.log("Spawning Claude for execution...\n");
1225
+ const claudeResult = await executeClaude({
1226
+ prompt,
1227
+ cwd: options?.cwd || process.cwd(),
1228
+ authToken: credentials.clerkToken,
1229
+ onLogEvent: (event) => {
1230
+ logBuffer.push(event);
1231
+ }
1232
+ });
1233
+ if (claudeResult.exitCode === 0) {
1234
+ await logTransport.finishRun("success", {
1235
+ exitCode: claudeResult.exitCode,
1236
+ cost: claudeResult.cost,
1237
+ usage: claudeResult.usage
1238
+ });
1239
+ console.log("\n\u2705 Execution complete");
1240
+ } else {
1241
+ await logTransport.finishRun("error", {
1242
+ exitCode: claudeResult.exitCode,
1243
+ errorMessage: `Claude execution failed with exit code ${claudeResult.exitCode}`
1244
+ });
1245
+ throw new Error(`Claude execution failed with exit code ${claudeResult.exitCode}`);
1246
+ }
1247
+ } catch (error) {
1248
+ if (runId) {
1249
+ try {
1250
+ await logTransport.finishRun("error", {
1251
+ errorMessage: error instanceof Error ? error.message : String(error)
1252
+ });
1253
+ } catch (stateError) {
1254
+ console.error("[Log Streaming] Failed to update run state:", stateError);
1255
+ }
1256
+ }
1257
+ throw error;
1258
+ } finally {
1259
+ if (heartbeatManager) {
1260
+ heartbeatManager.stop();
1261
+ console.log("[Log Streaming] Heartbeat stopped");
1262
+ }
1263
+ if (logBuffer) {
1264
+ await logBuffer.stop();
1265
+ console.log("[Log Streaming] Logs flushed");
1266
+ }
1267
+ }
1268
+ }
1269
+
1270
+ // src/workflows/agent.ts
1271
+ import { randomUUID } from "crypto";
1272
+ import { readFileSync } from "fs";
1273
+ import { fileURLToPath } from "url";
1274
+ import { dirname, join as join3 } from "path";
1275
+
1276
+ // src/api-client.ts
1277
+ var ApiClient = class {
1278
+ constructor(baseUrl = "https://www.contextgraph.dev") {
1279
+ this.baseUrl = baseUrl;
1280
+ }
1281
+ async getAuthToken() {
1282
+ const credentials = await loadCredentials();
1283
+ if (!credentials) {
1284
+ throw new Error("Not authenticated. Run authentication first.");
1285
+ }
1286
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1287
+ throw new Error("Token expired. Re-authenticate to continue.");
1288
+ }
1289
+ return credentials.clerkToken;
1290
+ }
1291
+ async getActionDetail(actionId) {
1292
+ const token = await this.getAuthToken();
1293
+ const response = await fetchWithRetry(
1294
+ `${this.baseUrl}/api/actions/${actionId}?token=${encodeURIComponent(token)}`,
1295
+ {
1296
+ headers: {
1297
+ "x-authorization": `Bearer ${token}`,
1298
+ "Content-Type": "application/json"
1299
+ }
1300
+ }
1301
+ );
1302
+ if (!response.ok) {
1303
+ throw new Error(`API error: ${response.status}`);
1304
+ }
1305
+ const result = await response.json();
1306
+ if (!result.success) {
1307
+ throw new Error(result.error);
1308
+ }
1309
+ return result.data;
1310
+ }
1311
+ async fetchTree(rootActionId, includeCompleted = false) {
1312
+ const token = await this.getAuthToken();
1313
+ const response = await fetchWithRetry(
1314
+ `${this.baseUrl}/api/tree/${rootActionId}?includeCompleted=${includeCompleted}&token=${encodeURIComponent(token)}`,
1315
+ {
1316
+ headers: {
1317
+ "x-authorization": `Bearer ${token}`,
1318
+ "Content-Type": "application/json"
1319
+ }
1320
+ }
1321
+ );
1322
+ if (!response.ok) {
1323
+ const errorText = await response.text();
1324
+ throw new Error(`Failed to fetch tree: ${response.status} ${errorText}`);
1325
+ }
1326
+ const result = await response.json();
1327
+ if (!result.success) {
1328
+ throw new Error("Failed to fetch tree: API returned unsuccessful response");
1329
+ }
1330
+ if (!result.data.rootActions?.[0]) {
1331
+ return { id: rootActionId, title: "", done: true, dependencies: [], children: [] };
1332
+ }
1333
+ return result.data.rootActions[0];
1334
+ }
1335
+ async claimNextAction(workerId) {
1336
+ const token = await this.getAuthToken();
1337
+ const response = await fetchWithRetry(
1338
+ `${this.baseUrl}/api/worker/next?token=${encodeURIComponent(token)}`,
1339
+ {
1340
+ method: "POST",
1341
+ headers: {
1342
+ "x-authorization": `Bearer ${token}`,
1343
+ "Content-Type": "application/json"
1344
+ },
1345
+ body: JSON.stringify({ worker_id: workerId })
1346
+ }
1347
+ );
1348
+ if (!response.ok) {
1349
+ const errorText = await response.text();
1350
+ throw new Error(`API error ${response.status}: ${errorText}`);
1351
+ }
1352
+ const result = await response.json();
1353
+ if (!result.success) {
1354
+ throw new Error(result.error || "API returned unsuccessful response");
1355
+ }
1356
+ return result.data;
1357
+ }
1358
+ async releaseClaim(params) {
1359
+ const token = await this.getAuthToken();
1360
+ const response = await fetchWithRetry(
1361
+ `${this.baseUrl}/api/worker/release?token=${encodeURIComponent(token)}`,
1362
+ {
1363
+ method: "POST",
1364
+ headers: {
1365
+ "x-authorization": `Bearer ${token}`,
1366
+ "Content-Type": "application/json"
1367
+ },
1368
+ body: JSON.stringify(params)
1369
+ }
1370
+ );
1371
+ if (!response.ok) {
1372
+ const errorText = await response.text();
1373
+ throw new Error(`API error ${response.status}: ${errorText}`);
1374
+ }
1375
+ const result = await response.json();
1376
+ if (!result.success) {
1377
+ throw new Error(result.error || "API returned unsuccessful response");
1378
+ }
1379
+ }
1380
+ };
1381
+
1382
+ // src/workspace-prep.ts
1383
+ import { spawn as spawn2 } from "child_process";
1384
+ import { mkdtemp, rm } from "fs/promises";
1385
+ import { tmpdir } from "os";
1386
+ import { join as join2 } from "path";
1387
+ var API_BASE_URL3 = "https://www.contextgraph.dev";
1388
+ async function fetchGitHubCredentials(authToken) {
1389
+ const response = await fetchWithRetry(`${API_BASE_URL3}/api/cli/credentials`, {
1390
+ headers: {
1391
+ "x-authorization": `Bearer ${authToken}`,
1392
+ "Content-Type": "application/json"
1393
+ }
1394
+ });
1395
+ if (response.status === 401) {
1396
+ throw new Error("Authentication failed. Please re-authenticate.");
1397
+ }
1398
+ if (response.status === 404) {
1399
+ throw new Error(
1400
+ "GitHub not connected. Please connect your GitHub account at https://contextgraph.dev/settings."
1401
+ );
1402
+ }
1403
+ if (!response.ok) {
1404
+ const errorText = await response.text();
1405
+ throw new Error(`Failed to fetch GitHub credentials: ${response.statusText}
1406
+ ${errorText}`);
1407
+ }
1408
+ return response.json();
1409
+ }
1410
+ function runGitCommand(args, cwd) {
1411
+ return new Promise((resolve, reject) => {
1412
+ const proc = spawn2("git", args, { cwd });
1413
+ let stdout = "";
1414
+ let stderr = "";
1415
+ proc.stdout.on("data", (data) => {
1416
+ stdout += data.toString();
1417
+ });
1418
+ proc.stderr.on("data", (data) => {
1419
+ stderr += data.toString();
1420
+ });
1421
+ proc.on("close", (code) => {
1422
+ if (code === 0) {
1423
+ resolve({ stdout, stderr });
1424
+ } else {
1425
+ reject(new Error(`git ${args[0]} failed (exit ${code}): ${stderr || stdout}`));
1426
+ }
1427
+ });
1428
+ proc.on("error", (err) => {
1429
+ reject(new Error(`Failed to spawn git: ${err.message}`));
1430
+ });
1431
+ });
1432
+ }
1433
+ function buildAuthenticatedUrl(repoUrl, token) {
1434
+ if (repoUrl.startsWith("https://github.com/")) {
1435
+ return repoUrl.replace("https://github.com/", `https://${token}@github.com/`);
1436
+ }
1437
+ if (repoUrl.startsWith("https://github.com")) {
1438
+ return repoUrl.replace("https://github.com", `https://${token}@github.com`);
1439
+ }
1440
+ return repoUrl;
1441
+ }
1442
+ async function prepareWorkspace(repoUrl, options) {
1443
+ const { branch, authToken } = options;
1444
+ const credentials = await fetchGitHubCredentials(authToken);
1445
+ const workspacePath = await mkdtemp(join2(tmpdir(), "cg-workspace-"));
1446
+ const cleanup = async () => {
1447
+ try {
1448
+ await rm(workspacePath, { recursive: true, force: true });
1449
+ } catch (error) {
1450
+ console.error(`Warning: Failed to cleanup workspace at ${workspacePath}:`, error);
1451
+ }
1452
+ };
1453
+ try {
1454
+ const cloneUrl = buildAuthenticatedUrl(repoUrl, credentials.githubToken);
1455
+ console.log(`\u{1F4C2} Cloning ${repoUrl}`);
1456
+ console.log(` \u2192 ${workspacePath}`);
1457
+ await runGitCommand(["clone", cloneUrl, workspacePath]);
1458
+ console.log(`\u2705 Repository cloned`);
1459
+ if (credentials.githubUsername) {
1460
+ await runGitCommand(["config", "user.name", credentials.githubUsername], workspacePath);
1461
+ }
1462
+ if (credentials.githubEmail) {
1463
+ await runGitCommand(["config", "user.email", credentials.githubEmail], workspacePath);
1464
+ }
1465
+ if (branch) {
1466
+ const { stdout } = await runGitCommand(
1467
+ ["ls-remote", "--heads", "origin", branch],
1468
+ workspacePath
1469
+ );
1470
+ const branchExists = stdout.trim().length > 0;
1471
+ if (branchExists) {
1472
+ console.log(`\u{1F33F} Checking out branch: ${branch}`);
1473
+ await runGitCommand(["checkout", branch], workspacePath);
1474
+ } else {
1475
+ console.log(`\u{1F331} Creating new branch: ${branch}`);
1476
+ await runGitCommand(["checkout", "-b", branch], workspacePath);
1477
+ }
1478
+ }
1479
+ return { path: workspacePath, cleanup };
1480
+ } catch (error) {
1481
+ await cleanup();
1482
+ throw error;
1483
+ }
1484
+ }
1485
+
1486
+ // src/workflows/agent.ts
1487
+ var __filename2 = fileURLToPath(import.meta.url);
1488
+ var __dirname2 = dirname(__filename2);
1489
+ var packageJson = JSON.parse(
1490
+ readFileSync(join3(__dirname2, "../package.json"), "utf-8")
1491
+ );
1492
+ var INITIAL_POLL_INTERVAL = parseInt(process.env.WORKER_INITIAL_POLL_INTERVAL || "2000", 10);
1493
+ var MAX_POLL_INTERVAL = parseInt(process.env.WORKER_MAX_POLL_INTERVAL || "5000", 10);
1494
+ var BACKOFF_MULTIPLIER = 1.5;
1495
+ var STATUS_INTERVAL_MS = 3e4;
1496
+ var INITIAL_RETRY_DELAY = 1e3;
1497
+ var MAX_RETRY_DELAY = 6e4;
1498
+ var OUTAGE_WARNING_THRESHOLD = 5;
1499
+ var running = true;
1500
+ var currentClaim = null;
1501
+ var apiClient = null;
1502
+ var stats = {
1503
+ startTime: Date.now(),
1504
+ prepared: 0,
1505
+ executed: 0,
1506
+ errors: 0
1507
+ };
1508
+ function formatDuration(ms) {
1509
+ const seconds = Math.floor(ms / 1e3);
1510
+ const minutes = Math.floor(seconds / 60);
1511
+ const hours = Math.floor(minutes / 60);
1512
+ if (hours > 0) {
1513
+ return `${hours}h ${minutes % 60}m`;
1514
+ } else if (minutes > 0) {
1515
+ return `${minutes}m ${seconds % 60}s`;
1516
+ }
1517
+ return `${seconds}s`;
1518
+ }
1519
+ function printStatus() {
1520
+ const uptime = formatDuration(Date.now() - stats.startTime);
1521
+ const total = stats.prepared + stats.executed;
1522
+ console.log(`Status: ${total} actions (${stats.prepared} prepared, ${stats.executed} executed, ${stats.errors} errors) | Uptime: ${uptime}`);
1523
+ }
1524
+ async function cleanupAndExit() {
1525
+ if (currentClaim && apiClient) {
1526
+ try {
1527
+ console.log(`
1528
+ \u{1F9F9} Releasing claim on action ${currentClaim.actionId}...`);
1529
+ await apiClient.releaseClaim({
1530
+ action_id: currentClaim.actionId,
1531
+ worker_id: currentClaim.workerId,
1532
+ claim_id: currentClaim.claimId
1533
+ });
1534
+ console.log("\u2705 Claim released successfully");
1535
+ } catch (error) {
1536
+ console.error("\u26A0\uFE0F Failed to release claim:", error.message);
1537
+ }
1538
+ }
1539
+ console.log("\u{1F44B} Shutdown complete");
1540
+ process.exit(0);
1541
+ }
1542
+ function setupSignalHandlers() {
1543
+ process.on("SIGINT", async () => {
1544
+ console.log("\n\n\u26A0\uFE0F Received SIGINT (Ctrl+C). Shutting down gracefully...");
1545
+ running = false;
1546
+ await cleanupAndExit();
1547
+ });
1548
+ process.on("SIGTERM", async () => {
1549
+ console.log("\n\n\u26A0\uFE0F Received SIGTERM. Shutting down gracefully...");
1550
+ running = false;
1551
+ await cleanupAndExit();
1552
+ });
1553
+ }
1554
+ function sleep(ms) {
1555
+ return new Promise((resolve) => setTimeout(resolve, ms));
1556
+ }
1557
+ function isRetryableError(error) {
1558
+ const message = error.message.toLowerCase();
1559
+ return message.includes("api error 5") || message.includes("500") || message.includes("502") || message.includes("503") || message.includes("504") || message.includes("network") || message.includes("timeout") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("socket hang up") || message.includes("failed query");
1560
+ }
1561
+ async function runLocalAgent() {
1562
+ apiClient = new ApiClient();
1563
+ setupSignalHandlers();
1564
+ const credentials = await loadCredentials();
1565
+ if (!credentials) {
1566
+ console.error("\u274C Not authenticated.");
1567
+ console.error(" Set CONTEXTGRAPH_API_TOKEN environment variable or run `contextgraph-agent auth`");
1568
+ process.exit(1);
1569
+ }
1570
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1571
+ console.error("\u274C Token expired. Run `contextgraph-agent auth` to re-authenticate.");
1572
+ process.exit(1);
1573
+ }
1574
+ const usingApiToken = !!process.env.CONTEXTGRAPH_API_TOKEN;
1575
+ if (usingApiToken) {
1576
+ console.log("\u{1F510} Authenticated via CONTEXTGRAPH_API_TOKEN");
1577
+ }
1578
+ const workerId = randomUUID();
1579
+ console.log(`\u{1F916} ContextGraph Agent v${packageJson.version}`);
1580
+ console.log(`\u{1F477} Worker ID: ${workerId}`);
1581
+ console.log(`\u{1F504} Starting continuous worker loop...
1582
+ `);
1583
+ console.log(`\u{1F4A1} Press Ctrl+C to gracefully shutdown and release any claimed work
1584
+ `);
1585
+ let currentPollInterval = INITIAL_POLL_INTERVAL;
1586
+ let lastStatusTime = Date.now();
1587
+ let consecutiveApiErrors = 0;
1588
+ let apiRetryDelay = INITIAL_RETRY_DELAY;
1589
+ while (running) {
1590
+ let actionDetail;
1591
+ try {
1592
+ actionDetail = await apiClient.claimNextAction(workerId);
1593
+ consecutiveApiErrors = 0;
1594
+ apiRetryDelay = INITIAL_RETRY_DELAY;
1595
+ } catch (error) {
1596
+ const err = error;
1597
+ if (isRetryableError(err)) {
1598
+ consecutiveApiErrors++;
1599
+ if (consecutiveApiErrors === OUTAGE_WARNING_THRESHOLD) {
1600
+ console.warn(`
1601
+ \u26A0\uFE0F API appears to be experiencing an outage.`);
1602
+ console.warn(` Will continue retrying indefinitely (every ${MAX_RETRY_DELAY / 1e3}s max).`);
1603
+ console.warn(` Press Ctrl+C to stop.
1604
+ `);
1605
+ }
1606
+ if (consecutiveApiErrors < OUTAGE_WARNING_THRESHOLD) {
1607
+ console.warn(`\u26A0\uFE0F API error (attempt ${consecutiveApiErrors}): ${err.message}`);
1608
+ } else if (consecutiveApiErrors % 10 === 0) {
1609
+ console.warn(`\u26A0\uFE0F Still retrying... (attempt ${consecutiveApiErrors}, last error: ${err.message})`);
1610
+ }
1611
+ const delaySeconds = Math.round(apiRetryDelay / 1e3);
1612
+ if (consecutiveApiErrors < OUTAGE_WARNING_THRESHOLD) {
1613
+ console.warn(` Retrying in ${delaySeconds}s...`);
1614
+ }
1615
+ await sleep(apiRetryDelay);
1616
+ apiRetryDelay = Math.min(apiRetryDelay * 2, MAX_RETRY_DELAY);
1617
+ continue;
1618
+ }
1619
+ throw err;
1620
+ }
1621
+ if (!actionDetail) {
1622
+ if (Date.now() - lastStatusTime >= STATUS_INTERVAL_MS) {
1623
+ printStatus();
1624
+ lastStatusTime = Date.now();
1625
+ }
1626
+ await sleep(currentPollInterval);
1627
+ currentPollInterval = Math.min(currentPollInterval * BACKOFF_MULTIPLIER, MAX_POLL_INTERVAL);
1628
+ continue;
1629
+ }
1630
+ currentPollInterval = INITIAL_POLL_INTERVAL;
1631
+ console.log(`Working: ${actionDetail.title}`);
1632
+ if (actionDetail.claim_id) {
1633
+ currentClaim = {
1634
+ actionId: actionDetail.id,
1635
+ claimId: actionDetail.claim_id,
1636
+ workerId
1637
+ };
1638
+ }
1639
+ const isPrepared = actionDetail.prepared !== false;
1640
+ const repoUrl = actionDetail.resolved_repository_url || actionDetail.repository_url;
1641
+ const branch = actionDetail.resolved_branch || actionDetail.branch;
1642
+ if (!repoUrl) {
1643
+ console.error(`
1644
+ \u274C Action "${actionDetail.title}" has no repository_url set.`);
1645
+ console.error(` Actions must have a repository_url (directly or inherited from parent).`);
1646
+ console.error(` Action ID: ${actionDetail.id}`);
1647
+ console.error(` resolved_repository_url: ${actionDetail.resolved_repository_url}`);
1648
+ console.error(` repository_url: ${actionDetail.repository_url}`);
1649
+ process.exit(1);
1650
+ }
1651
+ let workspacePath;
1652
+ let cleanup;
1653
+ try {
1654
+ const workspace = await prepareWorkspace(repoUrl, {
1655
+ branch: branch || void 0,
1656
+ authToken: credentials.clerkToken
1657
+ });
1658
+ workspacePath = workspace.path;
1659
+ cleanup = workspace.cleanup;
1660
+ if (!isPrepared) {
1661
+ await runPrepare(actionDetail.id, { cwd: workspacePath });
1662
+ stats.prepared++;
1663
+ if (currentClaim && apiClient) {
1664
+ try {
1665
+ await apiClient.releaseClaim({
1666
+ action_id: currentClaim.actionId,
1667
+ worker_id: currentClaim.workerId,
1668
+ claim_id: currentClaim.claimId
1669
+ });
1670
+ } catch (releaseError) {
1671
+ console.error("\u26A0\uFE0F Failed to release claim after preparation:", releaseError.message);
1672
+ }
1673
+ }
1674
+ currentClaim = null;
1675
+ continue;
1676
+ }
1677
+ try {
1678
+ await runExecute(actionDetail.id, { cwd: workspacePath });
1679
+ stats.executed++;
1680
+ console.log(`Completed: ${actionDetail.title}`);
1681
+ } catch (executeError) {
1682
+ stats.errors++;
1683
+ console.error(`Error: ${executeError.message}. Continuing...`);
1684
+ } finally {
1685
+ if (currentClaim && apiClient) {
1686
+ try {
1687
+ await apiClient.releaseClaim({
1688
+ action_id: currentClaim.actionId,
1689
+ worker_id: currentClaim.workerId,
1690
+ claim_id: currentClaim.claimId
1691
+ });
1692
+ } catch (releaseError) {
1693
+ console.error("\u26A0\uFE0F Failed to release claim:", releaseError.message);
1694
+ }
1695
+ }
1696
+ currentClaim = null;
1697
+ }
1698
+ } catch (workspaceError) {
1699
+ stats.errors++;
1700
+ console.error(`Error preparing workspace: ${workspaceError.message}. Continuing...`);
1701
+ if (currentClaim && apiClient) {
1702
+ try {
1703
+ console.log(`\u{1F9F9} Releasing claim due to workspace error...`);
1704
+ await apiClient.releaseClaim({
1705
+ action_id: currentClaim.actionId,
1706
+ worker_id: currentClaim.workerId,
1707
+ claim_id: currentClaim.claimId
1708
+ });
1709
+ console.log("\u2705 Claim released");
1710
+ } catch (releaseError) {
1711
+ console.error("\u26A0\uFE0F Failed to release claim:", releaseError.message);
1712
+ }
1713
+ }
1714
+ currentClaim = null;
1715
+ } finally {
1716
+ if (cleanup) {
1717
+ await cleanup();
1718
+ }
1719
+ }
1720
+ }
1721
+ }
1722
+
1723
+ // src/cli/index.ts
1724
+ var __filename3 = fileURLToPath2(import.meta.url);
1725
+ var __dirname3 = dirname2(__filename3);
1726
+ var packageJson2 = JSON.parse(
1727
+ readFileSync2(join4(__dirname3, "../package.json"), "utf-8")
1728
+ );
1729
+ var program = new Command();
1730
+ program.name("contextgraph-agent").description("Autonomous agent for contextgraph action execution").version(packageJson2.version);
1731
+ program.command("run").description("Run continuous worker loop (claims and executes actions until Ctrl+C)").action(async () => {
1732
+ try {
1733
+ await runLocalAgent();
1734
+ } catch (error) {
1735
+ if (error instanceof Error) {
1736
+ console.error("Error running agent:", error.message || "(no message)");
1737
+ if (error.stack) {
1738
+ console.error("\nStack trace:");
1739
+ console.error(error.stack);
1740
+ }
1741
+ } else {
1742
+ console.error("Error running agent:", error);
1743
+ }
1744
+ process.exit(1);
1745
+ }
1746
+ });
1747
+ program.command("auth").description("Authenticate with contextgraph.dev").action(async () => {
1748
+ try {
1749
+ await runAuth();
1750
+ } catch (error) {
1751
+ console.error("Error during authentication:", error instanceof Error ? error.message : error);
1752
+ process.exit(1);
1753
+ }
1754
+ });
1755
+ program.command("prepare").argument("<action-id>", "Action ID to prepare").description("Prepare a single action").action(async (actionId) => {
1756
+ try {
1757
+ await runPrepare(actionId);
1758
+ } catch (error) {
1759
+ console.error("Error preparing action:", error instanceof Error ? error.message : error);
1760
+ process.exit(1);
1761
+ }
1762
+ });
1763
+ program.command("execute").argument("<action-id>", "Action ID to execute").description("Execute a single action").action(async (actionId) => {
1764
+ try {
1765
+ await runExecute(actionId);
1766
+ } catch (error) {
1767
+ console.error("Error executing action:", error instanceof Error ? error.message : error);
1768
+ process.exit(1);
1769
+ }
1770
+ });
1771
+ program.command("whoami").description("Show current authentication status").action(async () => {
1772
+ try {
1773
+ const credentials = await loadCredentials();
1774
+ if (!credentials) {
1775
+ console.log("Not authenticated. Run `contextgraph-agent auth` to authenticate.");
1776
+ process.exit(1);
1777
+ }
1778
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1779
+ console.log("\u26A0\uFE0F Token expired. Run `contextgraph-agent auth` to re-authenticate.");
1780
+ console.log(`User ID: ${credentials.userId}`);
1781
+ console.log(`Expired at: ${credentials.expiresAt}`);
1782
+ process.exit(1);
1783
+ }
1784
+ console.log("\u2705 Authenticated");
1785
+ console.log(`User ID: ${credentials.userId}`);
1786
+ console.log(`Expires at: ${credentials.expiresAt}`);
1787
+ } catch (error) {
1788
+ console.error("Error checking authentication:", error instanceof Error ? error.message : error);
1789
+ process.exit(1);
1790
+ }
1791
+ });
1792
+ program.parse();
1793
+ //# sourceMappingURL=index.js.map