@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/.claude/settings.local.json +32 -0
- package/CHANGELOG.md +32 -0
- package/README.md +258 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1793 -0
- package/dist/index.js.map +1 -0
- package/examples/completion-extraction.ts +243 -0
- package/package.json +47 -0
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
|