@contextgraph/agent 0.4.18 → 0.4.19

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 CHANGED
@@ -4,11 +4,11 @@
4
4
  import { Command } from "commander";
5
5
  import { readFileSync as readFileSync2 } from "fs";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
- import { dirname as dirname2, join as join5 } from "path";
7
+ import { dirname as dirname2, join as join6 } from "path";
8
8
 
9
9
  // src/callback-server.ts
10
10
  import http from "http";
11
- import { URL } from "url";
11
+ import { URL as URL2 } from "url";
12
12
  var MIN_PORT = 3e3;
13
13
  var MAX_PORT = 3100;
14
14
  async function findFreePort() {
@@ -38,7 +38,7 @@ async function startCallbackServer() {
38
38
  let callbackResolve = null;
39
39
  const connections = /* @__PURE__ */ new Set();
40
40
  const server = http.createServer((req, res) => {
41
- const url = new URL(req.url || "/", `http://localhost:${port}`);
41
+ const url = new URL2(req.url || "/", `http://localhost:${port}`);
42
42
  if (url.pathname === "/callback") {
43
43
  const token = url.searchParams.get("token");
44
44
  const userId = url.searchParams.get("userId");
@@ -859,6 +859,32 @@ async function executeClaude(options) {
859
859
  // Allow MCP tools to execute automatically
860
860
  maxTurns: 100,
861
861
  // Reasonable limit
862
+ // Enable skills: load from project .claude/skills/ and allow Skill tool
863
+ settingSources: ["project"],
864
+ allowedTools: [
865
+ // Core file operations
866
+ "Read",
867
+ "Write",
868
+ "Edit",
869
+ "MultiEdit",
870
+ // Search tools
871
+ "Grep",
872
+ "Glob",
873
+ // Execution
874
+ "Bash",
875
+ // Skills
876
+ "Skill",
877
+ // Agent orchestration
878
+ "Task",
879
+ // User interaction
880
+ "TodoWrite",
881
+ "AskUserQuestion",
882
+ // Web access
883
+ "WebFetch",
884
+ "WebSearch",
885
+ // MCP tools (pattern match for contextgraph MCP server)
886
+ "mcp__*"
887
+ ],
862
888
  env: {
863
889
  ...process.env,
864
890
  // Pass auth token through environment for MCP server
@@ -1310,208 +1336,219 @@ var HeartbeatManager = class {
1310
1336
  }
1311
1337
  };
1312
1338
 
1313
- // src/workflows/prepare.ts
1314
- var API_BASE_URL = "https://www.contextgraph.dev";
1315
- async function runPrepare(actionId, options) {
1316
- const credentials = await loadCredentials();
1317
- if (!credentials) {
1318
- console.error("\u274C Not authenticated. Run authentication first.");
1319
- process.exit(1);
1339
+ // src/workspace-setup.ts
1340
+ import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1341
+ import { tmpdir as tmpdir2 } from "os";
1342
+ import { join as join4 } from "path";
1343
+
1344
+ // src/workspace-prep.ts
1345
+ import { spawn as spawn2 } from "child_process";
1346
+ import { mkdtemp, rm } from "fs/promises";
1347
+ import { tmpdir } from "os";
1348
+ import { join as join3 } from "path";
1349
+
1350
+ // src/skill-injection.ts
1351
+ import { mkdir as mkdir2, writeFile } from "fs/promises";
1352
+ import { join as join2 } from "path";
1353
+ async function injectSkills(workspacePath, skills) {
1354
+ if (skills.length === 0) {
1355
+ console.log("\u{1F4DA} No skills to inject");
1356
+ return;
1320
1357
  }
1321
- if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1322
- console.error("\u274C Token expired. Re-authenticate to continue.");
1323
- process.exit(1);
1358
+ console.log(`\u{1F4DA} Injecting ${skills.length} skill(s) into workspace...`);
1359
+ for (const skill of skills) {
1360
+ try {
1361
+ const skillDir = join2(workspacePath, ".claude", "skills", skill.name);
1362
+ await mkdir2(skillDir, { recursive: true });
1363
+ const triggerSection = skill.trigger ? `trigger: |
1364
+ ${skill.trigger.split("\n").map((line) => ` ${line}`).join("\n")}
1365
+ ` : "";
1366
+ const skillContent = `---
1367
+ name: ${skill.name}
1368
+ description: ${skill.description}
1369
+ ${triggerSection}---
1370
+
1371
+ ${skill.content}
1372
+ `;
1373
+ const skillFilePath = join2(skillDir, "SKILL.md");
1374
+ await writeFile(skillFilePath, skillContent, "utf-8");
1375
+ console.log(` \u2705 Injected skill: ${skill.name}`);
1376
+ } catch (error) {
1377
+ console.error(` \u274C Failed to inject skill "${skill.name}":`, error);
1378
+ throw error;
1379
+ }
1324
1380
  }
1325
- const logTransport = new LogTransportService(API_BASE_URL, credentials.clerkToken);
1326
- let runId;
1327
- let heartbeatManager;
1328
- let logBuffer;
1381
+ console.log(`\u2705 Skills injected successfully`);
1382
+ }
1383
+
1384
+ // src/skills-library-fetch.ts
1385
+ var API_BASE_URL = "https://www.contextgraph.dev";
1386
+ async function fetchSkillsLibrary(options) {
1387
+ const { authToken, runId } = options;
1329
1388
  try {
1330
- console.log("[Log Streaming] Creating run for prepare phase...");
1331
- runId = await logTransport.createRun(actionId, "prepare", {
1332
- startingCommit: options?.startingCommit
1333
- });
1334
- console.log(`[Log Streaming] Run created: ${runId}`);
1335
- console.log(`Fetching preparation instructions for action ${actionId}...
1336
- `);
1389
+ const url = new URL(`${API_BASE_URL}/api/skills/library`);
1390
+ if (runId) {
1391
+ url.searchParams.set("runId", runId);
1392
+ }
1337
1393
  const response = await fetchWithRetry(
1338
- `${API_BASE_URL}/api/prompts/prepare`,
1394
+ url.toString(),
1339
1395
  {
1340
- method: "POST",
1341
1396
  headers: {
1342
- "Authorization": `Bearer ${credentials.clerkToken}`,
1397
+ "x-authorization": `Bearer ${authToken}`,
1343
1398
  "Content-Type": "application/json"
1344
- },
1345
- body: JSON.stringify({ actionId, runId })
1399
+ }
1400
+ },
1401
+ {
1402
+ maxRetries: 2,
1403
+ baseDelayMs: 500
1346
1404
  }
1347
1405
  );
1348
1406
  if (!response.ok) {
1349
- const errorText = await response.text();
1350
- throw new Error(`Failed to fetch prepare prompt: ${response.statusText}
1351
- ${errorText}`);
1407
+ console.warn(`\u26A0\uFE0F Skills library API returned ${response.status}: ${response.statusText}`);
1408
+ return [];
1352
1409
  }
1353
- const { prompt } = await response.json();
1354
- await logTransport.updateRunState("preparing");
1355
- heartbeatManager = new HeartbeatManager(API_BASE_URL, credentials.clerkToken, runId);
1356
- heartbeatManager.start();
1357
- console.log("[Log Streaming] Heartbeat started");
1358
- logBuffer = new LogBuffer(logTransport);
1359
- logBuffer.start();
1360
- console.log("Spawning Claude for preparation...\n");
1361
- const claudeResult = await executeClaude({
1362
- prompt,
1363
- cwd: options?.cwd || process.cwd(),
1364
- authToken: credentials.clerkToken,
1365
- model: options?.model || "claude-opus-4-5-20251101",
1366
- onLogEvent: (event) => {
1367
- logBuffer.push(event);
1368
- }
1369
- });
1370
- if (claudeResult.exitCode === 0) {
1371
- await logTransport.finishRun("success", {
1372
- exitCode: claudeResult.exitCode,
1373
- cost: claudeResult.cost,
1374
- usage: claudeResult.usage
1375
- });
1376
- console.log("\n\u2705 Preparation complete");
1377
- } else {
1378
- await logTransport.finishRun("error", {
1379
- exitCode: claudeResult.exitCode,
1380
- errorMessage: `Claude preparation failed with exit code ${claudeResult.exitCode}`
1381
- });
1382
- console.error(`
1383
- \u274C Claude preparation failed with exit code ${claudeResult.exitCode}`);
1384
- process.exit(1);
1410
+ const data = await response.json();
1411
+ if (!data.success || !data.data?.skills) {
1412
+ console.warn("\u26A0\uFE0F Skills library API returned unexpected format");
1413
+ return [];
1385
1414
  }
1415
+ const skills = data.data.skills.map((skill) => {
1416
+ const name = skill.filename.replace(/\.md$/, "");
1417
+ return {
1418
+ name,
1419
+ description: skill.description,
1420
+ trigger: skill.trigger,
1421
+ content: skill.content
1422
+ };
1423
+ });
1424
+ return skills;
1386
1425
  } catch (error) {
1387
- if (runId) {
1388
- try {
1389
- await logTransport.finishRun("error", {
1390
- errorMessage: error instanceof Error ? error.message : String(error)
1391
- });
1392
- } catch (stateError) {
1393
- console.error("[Log Streaming] Failed to update run state:", stateError);
1394
- }
1395
- }
1396
- throw error;
1397
- } finally {
1398
- if (heartbeatManager) {
1399
- heartbeatManager.stop();
1400
- console.log("[Log Streaming] Heartbeat stopped");
1401
- }
1402
- if (logBuffer) {
1403
- await logBuffer.stop();
1404
- console.log("[Log Streaming] Logs flushed");
1405
- }
1426
+ console.warn("\u26A0\uFE0F Failed to fetch skills library:", error instanceof Error ? error.message : error);
1427
+ return [];
1406
1428
  }
1407
1429
  }
1408
1430
 
1409
- // src/workflows/execute.ts
1431
+ // src/workspace-prep.ts
1410
1432
  var API_BASE_URL2 = "https://www.contextgraph.dev";
1411
- async function runExecute(actionId, options) {
1412
- const credentials = await loadCredentials();
1413
- if (!credentials) {
1414
- console.error("\u274C Not authenticated. Run authentication first.");
1415
- process.exit(1);
1433
+ async function fetchGitHubCredentials(authToken) {
1434
+ const response = await fetchWithRetry(`${API_BASE_URL2}/api/cli/credentials`, {
1435
+ headers: {
1436
+ "x-authorization": `Bearer ${authToken}`,
1437
+ "Content-Type": "application/json"
1438
+ }
1439
+ });
1440
+ if (response.status === 401) {
1441
+ throw new Error("Authentication failed. Please re-authenticate.");
1416
1442
  }
1417
- if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1418
- console.error("\u274C Token expired. Re-authenticate to continue.");
1419
- process.exit(1);
1443
+ if (response.status === 404) {
1444
+ throw new Error(
1445
+ "GitHub not connected. Please connect your GitHub account at https://contextgraph.dev/settings."
1446
+ );
1420
1447
  }
1421
- const logTransport = new LogTransportService(API_BASE_URL2, credentials.clerkToken);
1422
- let runId;
1423
- let heartbeatManager;
1424
- let logBuffer;
1425
- try {
1426
- console.log("[Log Streaming] Creating run...");
1427
- runId = await logTransport.createRun(actionId, "execute", {
1428
- startingCommit: options?.startingCommit
1448
+ if (!response.ok) {
1449
+ const errorText = await response.text();
1450
+ throw new Error(`Failed to fetch GitHub credentials: ${response.statusText}
1451
+ ${errorText}`);
1452
+ }
1453
+ return response.json();
1454
+ }
1455
+ function runGitCommand(args, cwd) {
1456
+ return new Promise((resolve, reject) => {
1457
+ const proc = spawn2("git", args, { cwd });
1458
+ let stdout = "";
1459
+ let stderr = "";
1460
+ proc.stdout.on("data", (data) => {
1461
+ stdout += data.toString();
1429
1462
  });
1430
- console.log(`[Log Streaming] Run created: ${runId}`);
1431
- console.log(`Fetching execution instructions for action ${actionId}...
1432
- `);
1433
- const response = await fetchWithRetry(
1434
- `${API_BASE_URL2}/api/prompts/execute`,
1435
- {
1436
- method: "POST",
1437
- headers: {
1438
- "Authorization": `Bearer ${credentials.clerkToken}`,
1439
- "Content-Type": "application/json"
1440
- },
1441
- body: JSON.stringify({ actionId, runId })
1463
+ proc.stderr.on("data", (data) => {
1464
+ stderr += data.toString();
1465
+ });
1466
+ proc.on("close", (code) => {
1467
+ if (code === 0) {
1468
+ resolve({ stdout, stderr });
1469
+ } else {
1470
+ reject(new Error(`git ${args[0]} failed (exit ${code}): ${stderr || stdout}`));
1442
1471
  }
1443
- );
1444
- if (!response.ok) {
1445
- const errorText = await response.text();
1446
- throw new Error(`Failed to fetch execute prompt: ${response.statusText}
1447
- ${errorText}`);
1472
+ });
1473
+ proc.on("error", (err) => {
1474
+ reject(new Error(`Failed to spawn git: ${err.message}`));
1475
+ });
1476
+ });
1477
+ }
1478
+ function buildAuthenticatedUrl(repoUrl, token) {
1479
+ if (repoUrl.startsWith("https://github.com/")) {
1480
+ return repoUrl.replace("https://github.com/", `https://${token}@github.com/`);
1481
+ }
1482
+ if (repoUrl.startsWith("https://github.com")) {
1483
+ return repoUrl.replace("https://github.com", `https://${token}@github.com`);
1484
+ }
1485
+ return repoUrl;
1486
+ }
1487
+ async function prepareWorkspace(repoUrl, options) {
1488
+ const { branch, authToken, runId, skipSkills } = options;
1489
+ const credentials = await fetchGitHubCredentials(authToken);
1490
+ const workspacePath = await mkdtemp(join3(tmpdir(), "cg-workspace-"));
1491
+ const cleanup = async () => {
1492
+ try {
1493
+ await rm(workspacePath, { recursive: true, force: true });
1494
+ } catch (error) {
1495
+ console.error(`Warning: Failed to cleanup workspace at ${workspacePath}:`, error);
1448
1496
  }
1449
- const { prompt } = await response.json();
1450
- await logTransport.updateRunState("executing");
1451
- heartbeatManager = new HeartbeatManager(API_BASE_URL2, credentials.clerkToken, runId);
1452
- heartbeatManager.start();
1453
- console.log("[Log Streaming] Heartbeat started");
1454
- logBuffer = new LogBuffer(logTransport);
1455
- logBuffer.start();
1456
- console.log("Spawning Claude for execution...\n");
1457
- const claudeResult = await executeClaude({
1458
- prompt,
1459
- cwd: options?.cwd || process.cwd(),
1460
- authToken: credentials.clerkToken,
1461
- ...options?.model ? { model: options.model } : {},
1462
- onLogEvent: (event) => {
1463
- logBuffer.push(event);
1497
+ };
1498
+ try {
1499
+ const cloneUrl = buildAuthenticatedUrl(repoUrl, credentials.githubToken);
1500
+ console.log(`\u{1F4C2} Cloning ${repoUrl}`);
1501
+ console.log(` \u2192 ${workspacePath}`);
1502
+ await runGitCommand(["clone", cloneUrl, workspacePath]);
1503
+ console.log(`\u2705 Repository cloned`);
1504
+ if (credentials.githubUsername) {
1505
+ await runGitCommand(["config", "user.name", credentials.githubUsername], workspacePath);
1506
+ }
1507
+ if (credentials.githubEmail) {
1508
+ await runGitCommand(["config", "user.email", credentials.githubEmail], workspacePath);
1509
+ }
1510
+ if (branch) {
1511
+ const { stdout } = await runGitCommand(
1512
+ ["ls-remote", "--heads", "origin", branch],
1513
+ workspacePath
1514
+ );
1515
+ const branchExists = stdout.trim().length > 0;
1516
+ if (branchExists) {
1517
+ console.log(`\u{1F33F} Checking out branch: ${branch}`);
1518
+ await runGitCommand(["checkout", branch], workspacePath);
1519
+ } else {
1520
+ console.log(`\u{1F331} Creating new branch: ${branch}`);
1521
+ await runGitCommand(["checkout", "-b", branch], workspacePath);
1464
1522
  }
1465
- });
1466
- if (claudeResult.exitCode === 0) {
1467
- await logTransport.finishRun("success", {
1468
- exitCode: claudeResult.exitCode,
1469
- cost: claudeResult.cost,
1470
- usage: claudeResult.usage
1471
- });
1472
- console.log("\n\u2705 Execution complete");
1473
- } else {
1474
- await logTransport.finishRun("error", {
1475
- exitCode: claudeResult.exitCode,
1476
- errorMessage: `Claude execution failed with exit code ${claudeResult.exitCode}`
1477
- });
1478
- throw new Error(`Claude execution failed with exit code ${claudeResult.exitCode}`);
1479
1523
  }
1480
- } catch (error) {
1481
- if (runId) {
1524
+ const { stdout: commitHash } = await runGitCommand(["rev-parse", "HEAD"], workspacePath);
1525
+ const startingCommit = commitHash.trim();
1526
+ console.log("");
1527
+ if (skipSkills) {
1528
+ console.log("\u{1F4DA} Skipping skill injection (--no-skills flag)");
1529
+ } else {
1482
1530
  try {
1483
- await logTransport.finishRun("error", {
1484
- errorMessage: error instanceof Error ? error.message : String(error)
1485
- });
1486
- } catch (stateError) {
1487
- console.error("[Log Streaming] Failed to update run state:", stateError);
1531
+ const librarySkills = await fetchSkillsLibrary({ authToken, runId });
1532
+ if (librarySkills.length > 0) {
1533
+ await injectSkills(workspacePath, librarySkills);
1534
+ } else {
1535
+ console.log("\u{1F4DA} No skills to inject (empty library)");
1536
+ }
1537
+ } catch (skillError) {
1538
+ console.warn("\u26A0\uFE0F Skill injection failed (agent will continue):", skillError);
1488
1539
  }
1489
1540
  }
1541
+ return { path: workspacePath, startingCommit, cleanup };
1542
+ } catch (error) {
1543
+ await cleanup();
1490
1544
  throw error;
1491
- } finally {
1492
- if (heartbeatManager) {
1493
- heartbeatManager.stop();
1494
- console.log("[Log Streaming] Heartbeat stopped");
1495
- }
1496
- if (logBuffer) {
1497
- await logBuffer.stop();
1498
- console.log("[Log Streaming] Logs flushed");
1499
- }
1500
1545
  }
1501
1546
  }
1502
1547
 
1503
- // src/workflows/agent.ts
1504
- import { randomUUID } from "crypto";
1505
- import { readFileSync } from "fs";
1506
- import { mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1507
- import { tmpdir as tmpdir2 } from "os";
1508
- import { fileURLToPath } from "url";
1509
- import { dirname, join as join4 } from "path";
1510
-
1511
1548
  // package.json
1512
1549
  var package_default = {
1513
1550
  name: "@contextgraph/agent",
1514
- version: "0.4.18",
1551
+ version: "0.4.19",
1515
1552
  description: "Autonomous agent for contextgraph action execution",
1516
1553
  type: "module",
1517
1554
  bin: {
@@ -1666,248 +1703,289 @@ var ApiClient = class {
1666
1703
  }
1667
1704
  };
1668
1705
 
1669
- // src/workspace-prep.ts
1670
- import { spawn as spawn2 } from "child_process";
1671
- import { mkdtemp, rm } from "fs/promises";
1672
- import { tmpdir } from "os";
1673
- import { join as join3 } from "path";
1674
-
1675
- // src/skill-injection.ts
1676
- import { mkdir as mkdir2, writeFile } from "fs/promises";
1677
- import { join as join2 } from "path";
1678
- async function injectSkills(workspacePath, skills) {
1679
- if (skills.length === 0) {
1680
- console.log("\u{1F4DA} No skills to inject");
1681
- return;
1682
- }
1683
- console.log(`\u{1F4DA} Injecting ${skills.length} skill(s) into workspace...`);
1684
- for (const skill of skills) {
1685
- try {
1686
- const skillDir = join2(workspacePath, ".claude", "skills", skill.name);
1687
- await mkdir2(skillDir, { recursive: true });
1688
- const skillContent = `---
1689
- name: ${skill.name}
1690
- description: ${skill.description}
1691
- ---
1692
-
1693
- ${skill.content}
1694
- `;
1695
- const skillFilePath = join2(skillDir, "SKILL.md");
1696
- await writeFile(skillFilePath, skillContent, "utf-8");
1697
- console.log(` \u2705 Injected skill: ${skill.name}`);
1698
- } catch (error) {
1699
- console.error(` \u274C Failed to inject skill "${skill.name}":`, error);
1700
- throw error;
1701
- }
1706
+ // src/workspace-setup.ts
1707
+ var API_BASE_URL3 = "https://www.contextgraph.dev";
1708
+ async function setupWorkspaceForAction(actionId, options) {
1709
+ const { authToken, phase, startingCommit: startingCommitOverride, skipSkills } = options;
1710
+ let actionDetail = options.actionDetail;
1711
+ if (!actionDetail) {
1712
+ const apiClient2 = new ApiClient();
1713
+ console.log(`Fetching action details for ${actionId}...`);
1714
+ actionDetail = await apiClient2.getActionDetail(actionId);
1715
+ }
1716
+ const logTransport = new LogTransportService(API_BASE_URL3, authToken);
1717
+ console.log(`[Log Streaming] Creating run for ${phase} phase...`);
1718
+ const runId = await logTransport.createRun(actionId, phase, {
1719
+ startingCommit: startingCommitOverride
1720
+ });
1721
+ console.log(`[Log Streaming] Run created: ${runId}`);
1722
+ const repoUrl = actionDetail.resolved_repository_url || actionDetail.repository_url;
1723
+ const branch = actionDetail.resolved_branch || actionDetail.branch;
1724
+ let workspacePath;
1725
+ let cleanup;
1726
+ let startingCommit = startingCommitOverride;
1727
+ if (repoUrl) {
1728
+ const workspace = await prepareWorkspace(repoUrl, {
1729
+ branch: branch || void 0,
1730
+ authToken,
1731
+ runId,
1732
+ skipSkills
1733
+ });
1734
+ workspacePath = workspace.path;
1735
+ cleanup = workspace.cleanup;
1736
+ startingCommit = workspace.startingCommit;
1737
+ } else {
1738
+ console.log(`\u{1F4C2} No repository configured - creating blank workspace`);
1739
+ workspacePath = await mkdtemp2(join4(tmpdir2(), "cg-workspace-"));
1740
+ console.log(` \u2192 ${workspacePath}`);
1741
+ cleanup = async () => {
1742
+ try {
1743
+ await rm2(workspacePath, { recursive: true, force: true });
1744
+ } catch (error) {
1745
+ console.error(`Warning: Failed to cleanup workspace at ${workspacePath}:`, error);
1746
+ }
1747
+ };
1702
1748
  }
1703
- console.log(`\u2705 Skills injected successfully`);
1704
- }
1705
-
1706
- // src/test-skills.ts
1707
- var VALIDATION_SKILL = {
1708
- name: "contextgraph-validation-marker",
1709
- description: "Marks agent runs to validate skill injection is working",
1710
- content: `# ContextGraph Skill Validation Marker
1711
-
1712
- ## Purpose
1713
- This skill validates that the ContextGraph skill injection mechanism is working correctly.
1714
-
1715
- ## Instructions
1716
-
1717
- **CRITICAL**: When you start execution, IMMEDIATELY include this exact marker in your first response:
1718
-
1719
- \`\`\`
1720
- \u{1F52C} SKILL_INJECTION_VALIDATED: contextgraph-validation-marker loaded
1721
- \`\`\`
1722
-
1723
- This marker proves that:
1724
- 1. The skill file was successfully written to the workspace
1725
- 2. Claude Code loaded the skill at startup
1726
- 3. The skill content is available during agent execution
1727
-
1728
- ## Usage
1729
-
1730
- This skill should be invoked automatically at the start of EVERY agent run to validate the injection mechanism. The marker should appear in the agent's output logs.
1731
-
1732
- ## Context
1733
-
1734
- This is a prototype validation skill for the ContextGraph learning and skills system. It's designed to have a high trigger frequency to reliably demonstrate skill loading mechanics.
1735
- `
1736
- };
1737
- function getValidationSkills() {
1738
- return [VALIDATION_SKILL];
1749
+ return {
1750
+ workspacePath,
1751
+ cleanup,
1752
+ startingCommit,
1753
+ runId,
1754
+ logTransport
1755
+ };
1739
1756
  }
1740
1757
 
1741
- // src/skills-library-fetch.ts
1742
- var API_BASE_URL3 = "https://www.contextgraph.dev";
1743
- async function fetchSkillsLibrary(authToken) {
1758
+ // src/workflows/prepare.ts
1759
+ var API_BASE_URL4 = "https://www.contextgraph.dev";
1760
+ async function runPrepare(actionId, options) {
1761
+ const credentials = await loadCredentials();
1762
+ if (!credentials) {
1763
+ console.error("\u274C Not authenticated. Run authentication first.");
1764
+ process.exit(1);
1765
+ }
1766
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1767
+ console.error("\u274C Token expired. Re-authenticate to continue.");
1768
+ process.exit(1);
1769
+ }
1770
+ let runId = options?.runId;
1771
+ let heartbeatManager;
1772
+ let logBuffer;
1773
+ let workspacePath;
1774
+ let cleanup;
1775
+ let logTransport;
1744
1776
  try {
1777
+ if (!runId) {
1778
+ const setup = await setupWorkspaceForAction(actionId, {
1779
+ authToken: credentials.clerkToken,
1780
+ phase: "prepare",
1781
+ startingCommit: options?.startingCommit,
1782
+ skipSkills: options?.skipSkills
1783
+ });
1784
+ workspacePath = setup.workspacePath;
1785
+ cleanup = setup.cleanup;
1786
+ runId = setup.runId;
1787
+ logTransport = setup.logTransport;
1788
+ } else {
1789
+ console.log(`[Log Streaming] Using pre-created run: ${runId}`);
1790
+ workspacePath = options?.cwd || process.cwd();
1791
+ logTransport = new LogTransportService(API_BASE_URL4, credentials.clerkToken, runId);
1792
+ }
1793
+ console.log(`Fetching preparation instructions for action ${actionId}...
1794
+ `);
1745
1795
  const response = await fetchWithRetry(
1746
- `${API_BASE_URL3}/api/skills/library`,
1796
+ `${API_BASE_URL4}/api/prompts/prepare`,
1747
1797
  {
1798
+ method: "POST",
1748
1799
  headers: {
1749
- "x-authorization": `Bearer ${authToken}`,
1800
+ "Authorization": `Bearer ${credentials.clerkToken}`,
1750
1801
  "Content-Type": "application/json"
1751
- }
1752
- },
1753
- {
1754
- maxRetries: 2,
1755
- baseDelayMs: 500
1802
+ },
1803
+ body: JSON.stringify({ actionId, runId })
1756
1804
  }
1757
1805
  );
1758
1806
  if (!response.ok) {
1759
- console.warn(`\u26A0\uFE0F Skills library API returned ${response.status}: ${response.statusText}`);
1760
- return [];
1761
- }
1762
- const data = await response.json();
1763
- if (!data.success || !data.data?.skills) {
1764
- console.warn("\u26A0\uFE0F Skills library API returned unexpected format");
1765
- return [];
1807
+ const errorText = await response.text();
1808
+ throw new Error(`Failed to fetch prepare prompt: ${response.statusText}
1809
+ ${errorText}`);
1766
1810
  }
1767
- const skills = data.data.skills.map((skill) => {
1768
- const name = skill.filename.replace(/\.md$/, "");
1769
- let description = "Skill from ContextGraph library";
1770
- const frontmatterMatch = skill.content.match(/^---\n([\s\S]*?)\n---/);
1771
- if (frontmatterMatch) {
1772
- const descMatch = frontmatterMatch[1].match(/description:\s*(.+)/);
1773
- if (descMatch) {
1774
- description = descMatch[1].trim();
1775
- }
1811
+ const { prompt } = await response.json();
1812
+ await logTransport.updateRunState("preparing");
1813
+ heartbeatManager = new HeartbeatManager(API_BASE_URL4, credentials.clerkToken, runId);
1814
+ heartbeatManager.start();
1815
+ console.log("[Log Streaming] Heartbeat started");
1816
+ logBuffer = new LogBuffer(logTransport);
1817
+ logBuffer.start();
1818
+ console.log("Spawning Claude for preparation...\n");
1819
+ const claudeResult = await executeClaude({
1820
+ prompt,
1821
+ cwd: workspacePath,
1822
+ authToken: credentials.clerkToken,
1823
+ model: options?.model || "claude-opus-4-5-20251101",
1824
+ onLogEvent: (event) => {
1825
+ logBuffer.push(event);
1776
1826
  }
1777
- const contentWithoutFrontmatter = skill.content.replace(/^---\n[\s\S]*?\n---\n/, "");
1778
- return {
1779
- name,
1780
- description,
1781
- content: contentWithoutFrontmatter
1782
- };
1783
1827
  });
1784
- return skills;
1828
+ if (claudeResult.exitCode === 0) {
1829
+ await logTransport.finishRun("success", {
1830
+ exitCode: claudeResult.exitCode,
1831
+ cost: claudeResult.cost,
1832
+ usage: claudeResult.usage
1833
+ });
1834
+ console.log("\n\u2705 Preparation complete");
1835
+ } else {
1836
+ await logTransport.finishRun("error", {
1837
+ exitCode: claudeResult.exitCode,
1838
+ errorMessage: `Claude preparation failed with exit code ${claudeResult.exitCode}`
1839
+ });
1840
+ console.error(`
1841
+ \u274C Claude preparation failed with exit code ${claudeResult.exitCode}`);
1842
+ process.exit(1);
1843
+ }
1785
1844
  } catch (error) {
1786
- console.warn("\u26A0\uFE0F Failed to fetch skills library:", error instanceof Error ? error.message : error);
1787
- return [];
1788
- }
1789
- }
1790
-
1791
- // src/workspace-prep.ts
1792
- var API_BASE_URL4 = "https://www.contextgraph.dev";
1793
- async function fetchGitHubCredentials(authToken) {
1794
- const response = await fetchWithRetry(`${API_BASE_URL4}/api/cli/credentials`, {
1795
- headers: {
1796
- "x-authorization": `Bearer ${authToken}`,
1797
- "Content-Type": "application/json"
1845
+ if (runId) {
1846
+ try {
1847
+ await logTransport.finishRun("error", {
1848
+ errorMessage: error instanceof Error ? error.message : String(error)
1849
+ });
1850
+ } catch (stateError) {
1851
+ console.error("[Log Streaming] Failed to update run state:", stateError);
1852
+ }
1853
+ }
1854
+ throw error;
1855
+ } finally {
1856
+ if (heartbeatManager) {
1857
+ heartbeatManager.stop();
1858
+ console.log("[Log Streaming] Heartbeat stopped");
1859
+ }
1860
+ if (logBuffer) {
1861
+ await logBuffer.stop();
1862
+ console.log("[Log Streaming] Logs flushed");
1863
+ }
1864
+ if (cleanup) {
1865
+ await cleanup();
1798
1866
  }
1799
- });
1800
- if (response.status === 401) {
1801
- throw new Error("Authentication failed. Please re-authenticate.");
1802
- }
1803
- if (response.status === 404) {
1804
- throw new Error(
1805
- "GitHub not connected. Please connect your GitHub account at https://contextgraph.dev/settings."
1806
- );
1807
- }
1808
- if (!response.ok) {
1809
- const errorText = await response.text();
1810
- throw new Error(`Failed to fetch GitHub credentials: ${response.statusText}
1811
- ${errorText}`);
1812
1867
  }
1813
- return response.json();
1814
- }
1815
- function runGitCommand(args, cwd) {
1816
- return new Promise((resolve, reject) => {
1817
- const proc = spawn2("git", args, { cwd });
1818
- let stdout = "";
1819
- let stderr = "";
1820
- proc.stdout.on("data", (data) => {
1821
- stdout += data.toString();
1822
- });
1823
- proc.stderr.on("data", (data) => {
1824
- stderr += data.toString();
1825
- });
1826
- proc.on("close", (code) => {
1827
- if (code === 0) {
1828
- resolve({ stdout, stderr });
1829
- } else {
1830
- reject(new Error(`git ${args[0]} failed (exit ${code}): ${stderr || stdout}`));
1831
- }
1832
- });
1833
- proc.on("error", (err) => {
1834
- reject(new Error(`Failed to spawn git: ${err.message}`));
1835
- });
1836
- });
1837
1868
  }
1838
- function buildAuthenticatedUrl(repoUrl, token) {
1839
- if (repoUrl.startsWith("https://github.com/")) {
1840
- return repoUrl.replace("https://github.com/", `https://${token}@github.com/`);
1869
+
1870
+ // src/workflows/execute.ts
1871
+ var API_BASE_URL5 = "https://www.contextgraph.dev";
1872
+ async function runExecute(actionId, options) {
1873
+ const credentials = await loadCredentials();
1874
+ if (!credentials) {
1875
+ console.error("\u274C Not authenticated. Run authentication first.");
1876
+ process.exit(1);
1841
1877
  }
1842
- if (repoUrl.startsWith("https://github.com")) {
1843
- return repoUrl.replace("https://github.com", `https://${token}@github.com`);
1878
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1879
+ console.error("\u274C Token expired. Re-authenticate to continue.");
1880
+ process.exit(1);
1844
1881
  }
1845
- return repoUrl;
1846
- }
1847
- async function prepareWorkspace(repoUrl, options) {
1848
- const { branch, authToken } = options;
1849
- const credentials = await fetchGitHubCredentials(authToken);
1850
- const workspacePath = await mkdtemp(join3(tmpdir(), "cg-workspace-"));
1851
- const cleanup = async () => {
1852
- try {
1853
- await rm(workspacePath, { recursive: true, force: true });
1854
- } catch (error) {
1855
- console.error(`Warning: Failed to cleanup workspace at ${workspacePath}:`, error);
1856
- }
1857
- };
1882
+ let runId = options?.runId;
1883
+ let heartbeatManager;
1884
+ let logBuffer;
1885
+ let workspacePath;
1886
+ let cleanup;
1887
+ let logTransport;
1858
1888
  try {
1859
- const cloneUrl = buildAuthenticatedUrl(repoUrl, credentials.githubToken);
1860
- console.log(`\u{1F4C2} Cloning ${repoUrl}`);
1861
- console.log(` \u2192 ${workspacePath}`);
1862
- await runGitCommand(["clone", cloneUrl, workspacePath]);
1863
- console.log(`\u2705 Repository cloned`);
1864
- if (credentials.githubUsername) {
1865
- await runGitCommand(["config", "user.name", credentials.githubUsername], workspacePath);
1866
- }
1867
- if (credentials.githubEmail) {
1868
- await runGitCommand(["config", "user.email", credentials.githubEmail], workspacePath);
1889
+ if (!runId) {
1890
+ const setup = await setupWorkspaceForAction(actionId, {
1891
+ authToken: credentials.clerkToken,
1892
+ phase: "execute",
1893
+ startingCommit: options?.startingCommit,
1894
+ skipSkills: options?.skipSkills
1895
+ });
1896
+ workspacePath = setup.workspacePath;
1897
+ cleanup = setup.cleanup;
1898
+ runId = setup.runId;
1899
+ logTransport = setup.logTransport;
1900
+ } else {
1901
+ console.log(`[Log Streaming] Using pre-created run: ${runId}`);
1902
+ workspacePath = options?.cwd || process.cwd();
1903
+ logTransport = new LogTransportService(API_BASE_URL5, credentials.clerkToken, runId);
1869
1904
  }
1870
- if (branch) {
1871
- const { stdout } = await runGitCommand(
1872
- ["ls-remote", "--heads", "origin", branch],
1873
- workspacePath
1874
- );
1875
- const branchExists = stdout.trim().length > 0;
1876
- if (branchExists) {
1877
- console.log(`\u{1F33F} Checking out branch: ${branch}`);
1878
- await runGitCommand(["checkout", branch], workspacePath);
1879
- } else {
1880
- console.log(`\u{1F331} Creating new branch: ${branch}`);
1881
- await runGitCommand(["checkout", "-b", branch], workspacePath);
1905
+ console.log(`Fetching execution instructions for action ${actionId}...
1906
+ `);
1907
+ const response = await fetchWithRetry(
1908
+ `${API_BASE_URL5}/api/prompts/execute`,
1909
+ {
1910
+ method: "POST",
1911
+ headers: {
1912
+ "Authorization": `Bearer ${credentials.clerkToken}`,
1913
+ "Content-Type": "application/json"
1914
+ },
1915
+ body: JSON.stringify({ actionId, runId })
1882
1916
  }
1917
+ );
1918
+ if (!response.ok) {
1919
+ const errorText = await response.text();
1920
+ throw new Error(`Failed to fetch execute prompt: ${response.statusText}
1921
+ ${errorText}`);
1883
1922
  }
1884
- const { stdout: commitHash } = await runGitCommand(["rev-parse", "HEAD"], workspacePath);
1885
- const startingCommit = commitHash.trim();
1886
- console.log("");
1887
- try {
1888
- const librarySkills = await fetchSkillsLibrary(authToken);
1889
- const validationSkills = getValidationSkills();
1890
- const allSkills = [...librarySkills, ...validationSkills];
1891
- if (allSkills.length > 0) {
1892
- await injectSkills(workspacePath, allSkills);
1893
- } else {
1894
- console.log("\u{1F4DA} No skills to inject (empty library)");
1923
+ const { prompt } = await response.json();
1924
+ await logTransport.updateRunState("executing");
1925
+ heartbeatManager = new HeartbeatManager(API_BASE_URL5, credentials.clerkToken, runId);
1926
+ heartbeatManager.start();
1927
+ console.log("[Log Streaming] Heartbeat started");
1928
+ logBuffer = new LogBuffer(logTransport);
1929
+ logBuffer.start();
1930
+ console.log("Spawning Claude for execution...\n");
1931
+ const claudeResult = await executeClaude({
1932
+ prompt,
1933
+ cwd: workspacePath,
1934
+ authToken: credentials.clerkToken,
1935
+ ...options?.model ? { model: options.model } : {},
1936
+ onLogEvent: (event) => {
1937
+ logBuffer.push(event);
1895
1938
  }
1896
- } catch (skillError) {
1897
- console.warn("\u26A0\uFE0F Skill injection failed (agent will continue):", skillError);
1939
+ });
1940
+ if (claudeResult.exitCode === 0) {
1941
+ await logTransport.finishRun("success", {
1942
+ exitCode: claudeResult.exitCode,
1943
+ cost: claudeResult.cost,
1944
+ usage: claudeResult.usage
1945
+ });
1946
+ console.log("\n\u2705 Execution complete");
1947
+ } else {
1948
+ await logTransport.finishRun("error", {
1949
+ exitCode: claudeResult.exitCode,
1950
+ errorMessage: `Claude execution failed with exit code ${claudeResult.exitCode}`
1951
+ });
1952
+ throw new Error(`Claude execution failed with exit code ${claudeResult.exitCode}`);
1898
1953
  }
1899
- return { path: workspacePath, startingCommit, cleanup };
1900
1954
  } catch (error) {
1901
- await cleanup();
1955
+ if (runId) {
1956
+ try {
1957
+ await logTransport.finishRun("error", {
1958
+ errorMessage: error instanceof Error ? error.message : String(error)
1959
+ });
1960
+ } catch (stateError) {
1961
+ console.error("[Log Streaming] Failed to update run state:", stateError);
1962
+ }
1963
+ }
1902
1964
  throw error;
1965
+ } finally {
1966
+ if (heartbeatManager) {
1967
+ heartbeatManager.stop();
1968
+ console.log("[Log Streaming] Heartbeat stopped");
1969
+ }
1970
+ if (logBuffer) {
1971
+ await logBuffer.stop();
1972
+ console.log("[Log Streaming] Logs flushed");
1973
+ }
1974
+ if (cleanup) {
1975
+ await cleanup();
1976
+ }
1903
1977
  }
1904
1978
  }
1905
1979
 
1906
1980
  // src/workflows/agent.ts
1981
+ import { randomUUID } from "crypto";
1982
+ import { readFileSync } from "fs";
1983
+ import { fileURLToPath } from "url";
1984
+ import { dirname, join as join5 } from "path";
1907
1985
  var __filename2 = fileURLToPath(import.meta.url);
1908
1986
  var __dirname2 = dirname(__filename2);
1909
1987
  var packageJson = JSON.parse(
1910
- readFileSync(join4(__dirname2, "../package.json"), "utf-8")
1988
+ readFileSync(join5(__dirname2, "../package.json"), "utf-8")
1911
1989
  );
1912
1990
  var INITIAL_POLL_INTERVAL = parseInt(process.env.WORKER_INITIAL_POLL_INTERVAL || "2000", 10);
1913
1991
  var MAX_POLL_INTERVAL = parseInt(process.env.WORKER_MAX_POLL_INTERVAL || "5000", 10);
@@ -2077,35 +2155,24 @@ async function runLocalAgent(options) {
2077
2155
  continue;
2078
2156
  }
2079
2157
  console.log(`Working: ${actionDetail.title}`);
2080
- const repoUrl = actionDetail.resolved_repository_url || actionDetail.repository_url;
2081
- const branch = actionDetail.resolved_branch || actionDetail.branch;
2082
2158
  let workspacePath;
2083
2159
  let cleanup;
2084
2160
  let startingCommit;
2085
- const needsRepo = repoUrl;
2161
+ let runId;
2086
2162
  try {
2087
- if (needsRepo) {
2088
- const workspace = await prepareWorkspace(repoUrl, {
2089
- branch: branch || void 0,
2090
- authToken: credentials.clerkToken
2091
- });
2092
- workspacePath = workspace.path;
2093
- cleanup = workspace.cleanup;
2094
- startingCommit = workspace.startingCommit;
2095
- } else {
2096
- console.log(`\u{1F4C2} No repository configured - creating blank workspace`);
2097
- workspacePath = await mkdtemp2(join4(tmpdir2(), "cg-workspace-"));
2098
- console.log(` \u2192 ${workspacePath}`);
2099
- cleanup = async () => {
2100
- try {
2101
- await rm2(workspacePath, { recursive: true, force: true });
2102
- } catch (error) {
2103
- console.error(`Warning: Failed to cleanup workspace at ${workspacePath}:`, error);
2104
- }
2105
- };
2106
- }
2163
+ const setup = await setupWorkspaceForAction(actionDetail.id, {
2164
+ authToken: credentials.clerkToken,
2165
+ phase,
2166
+ actionDetail,
2167
+ // Pass pre-fetched action detail to avoid redundant API call
2168
+ skipSkills: options?.skipSkills
2169
+ });
2170
+ workspacePath = setup.workspacePath;
2171
+ cleanup = setup.cleanup;
2172
+ startingCommit = setup.startingCommit;
2173
+ runId = setup.runId;
2107
2174
  if (phase === "prepare") {
2108
- await runPrepare(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel });
2175
+ await runPrepare(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel, runId });
2109
2176
  stats.prepared++;
2110
2177
  if (currentClaim && apiClient) {
2111
2178
  try {
@@ -2122,7 +2189,7 @@ async function runLocalAgent(options) {
2122
2189
  continue;
2123
2190
  }
2124
2191
  try {
2125
- await runExecute(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel });
2192
+ await runExecute(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel, runId });
2126
2193
  stats.executed++;
2127
2194
  console.log(`Completed: ${actionDetail.title}`);
2128
2195
  } catch (executeError) {
@@ -2171,14 +2238,15 @@ async function runLocalAgent(options) {
2171
2238
  var __filename3 = fileURLToPath2(import.meta.url);
2172
2239
  var __dirname3 = dirname2(__filename3);
2173
2240
  var packageJson2 = JSON.parse(
2174
- readFileSync2(join5(__dirname3, "../package.json"), "utf-8")
2241
+ readFileSync2(join6(__dirname3, "../package.json"), "utf-8")
2175
2242
  );
2176
2243
  var program = new Command();
2177
2244
  program.name("contextgraph-agent").description("Autonomous agent for contextgraph action execution").version(packageJson2.version);
2178
- program.command("run").description("Run continuous worker loop (claims and executes actions until Ctrl+C)").option("--force-haiku", "Force all workflows to use claude-haiku-4-5 instead of default models").action(async (options) => {
2245
+ program.command("run").description("Run continuous worker loop (claims and executes actions until Ctrl+C)").option("--force-haiku", "Force all workflows to use claude-haiku-4-5 instead of default models").option("--skip-skills", "Skip skill injection (for testing)").action(async (options) => {
2179
2246
  try {
2180
2247
  await runLocalAgent({
2181
- forceModel: options.forceHaiku ? "claude-haiku-4-5-20251001" : void 0
2248
+ forceModel: options.forceHaiku ? "claude-haiku-4-5-20251001" : void 0,
2249
+ skipSkills: options.skipSkills
2182
2250
  });
2183
2251
  } catch (error) {
2184
2252
  if (error instanceof Error) {
@@ -2201,17 +2269,17 @@ program.command("auth").description("Authenticate with contextgraph.dev").action
2201
2269
  process.exit(1);
2202
2270
  }
2203
2271
  });
2204
- program.command("prepare").argument("<action-id>", "Action ID to prepare").description("Prepare a single action").action(async (actionId) => {
2272
+ program.command("prepare").argument("<action-id>", "Action ID to prepare").description("Prepare a single action").option("--skip-skills", "Skip skill injection (for testing)").action(async (actionId, options) => {
2205
2273
  try {
2206
- await runPrepare(actionId);
2274
+ await runPrepare(actionId, { skipSkills: options.skipSkills });
2207
2275
  } catch (error) {
2208
2276
  console.error("Error preparing action:", error instanceof Error ? error.message : error);
2209
2277
  process.exit(1);
2210
2278
  }
2211
2279
  });
2212
- program.command("execute").argument("<action-id>", "Action ID to execute").description("Execute a single action").action(async (actionId) => {
2280
+ program.command("execute").argument("<action-id>", "Action ID to execute").description("Execute a single action").option("--skip-skills", "Skip skill injection (for testing)").action(async (actionId, options) => {
2213
2281
  try {
2214
- await runExecute(actionId);
2282
+ await runExecute(actionId, { skipSkills: options.skipSkills });
2215
2283
  } catch (error) {
2216
2284
  console.error("Error executing action:", error instanceof Error ? error.message : error);
2217
2285
  process.exit(1);