@contextgraph/agent 0.4.18 → 0.4.20

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,220 @@ 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, appendFile } 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
+ await appendFile(join3(workspacePath, ".git", "info", "exclude"), "\n.claude/skills/\n");
1511
+ if (branch) {
1512
+ const { stdout } = await runGitCommand(
1513
+ ["ls-remote", "--heads", "origin", branch],
1514
+ workspacePath
1515
+ );
1516
+ const branchExists = stdout.trim().length > 0;
1517
+ if (branchExists) {
1518
+ console.log(`\u{1F33F} Checking out branch: ${branch}`);
1519
+ await runGitCommand(["checkout", branch], workspacePath);
1520
+ } else {
1521
+ console.log(`\u{1F331} Creating new branch: ${branch}`);
1522
+ await runGitCommand(["checkout", "-b", branch], workspacePath);
1464
1523
  }
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
1524
  }
1480
- } catch (error) {
1481
- if (runId) {
1525
+ const { stdout: commitHash } = await runGitCommand(["rev-parse", "HEAD"], workspacePath);
1526
+ const startingCommit = commitHash.trim();
1527
+ console.log("");
1528
+ if (skipSkills) {
1529
+ console.log("\u{1F4DA} Skipping skill injection (--no-skills flag)");
1530
+ } else {
1482
1531
  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);
1532
+ const librarySkills = await fetchSkillsLibrary({ authToken, runId });
1533
+ if (librarySkills.length > 0) {
1534
+ await injectSkills(workspacePath, librarySkills);
1535
+ } else {
1536
+ console.log("\u{1F4DA} No skills to inject (empty library)");
1537
+ }
1538
+ } catch (skillError) {
1539
+ console.warn("\u26A0\uFE0F Skill injection failed (agent will continue):", skillError);
1488
1540
  }
1489
1541
  }
1542
+ return { path: workspacePath, startingCommit, cleanup };
1543
+ } catch (error) {
1544
+ await cleanup();
1490
1545
  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
1546
  }
1501
1547
  }
1502
1548
 
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
1549
  // package.json
1512
1550
  var package_default = {
1513
1551
  name: "@contextgraph/agent",
1514
- version: "0.4.18",
1552
+ version: "0.4.20",
1515
1553
  description: "Autonomous agent for contextgraph action execution",
1516
1554
  type: "module",
1517
1555
  bin: {
@@ -1666,248 +1704,289 @@ var ApiClient = class {
1666
1704
  }
1667
1705
  };
1668
1706
 
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
- }
1707
+ // src/workspace-setup.ts
1708
+ var API_BASE_URL3 = "https://www.contextgraph.dev";
1709
+ async function setupWorkspaceForAction(actionId, options) {
1710
+ const { authToken, phase, startingCommit: startingCommitOverride, skipSkills } = options;
1711
+ let actionDetail = options.actionDetail;
1712
+ if (!actionDetail) {
1713
+ const apiClient2 = new ApiClient();
1714
+ console.log(`Fetching action details for ${actionId}...`);
1715
+ actionDetail = await apiClient2.getActionDetail(actionId);
1716
+ }
1717
+ const logTransport = new LogTransportService(API_BASE_URL3, authToken);
1718
+ console.log(`[Log Streaming] Creating run for ${phase} phase...`);
1719
+ const runId = await logTransport.createRun(actionId, phase, {
1720
+ startingCommit: startingCommitOverride
1721
+ });
1722
+ console.log(`[Log Streaming] Run created: ${runId}`);
1723
+ const repoUrl = actionDetail.resolved_repository_url || actionDetail.repository_url;
1724
+ const branch = actionDetail.resolved_branch || actionDetail.branch;
1725
+ let workspacePath;
1726
+ let cleanup;
1727
+ let startingCommit = startingCommitOverride;
1728
+ if (repoUrl) {
1729
+ const workspace = await prepareWorkspace(repoUrl, {
1730
+ branch: branch || void 0,
1731
+ authToken,
1732
+ runId,
1733
+ skipSkills
1734
+ });
1735
+ workspacePath = workspace.path;
1736
+ cleanup = workspace.cleanup;
1737
+ startingCommit = workspace.startingCommit;
1738
+ } else {
1739
+ console.log(`\u{1F4C2} No repository configured - creating blank workspace`);
1740
+ workspacePath = await mkdtemp2(join4(tmpdir2(), "cg-workspace-"));
1741
+ console.log(` \u2192 ${workspacePath}`);
1742
+ cleanup = async () => {
1743
+ try {
1744
+ await rm2(workspacePath, { recursive: true, force: true });
1745
+ } catch (error) {
1746
+ console.error(`Warning: Failed to cleanup workspace at ${workspacePath}:`, error);
1747
+ }
1748
+ };
1702
1749
  }
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];
1750
+ return {
1751
+ workspacePath,
1752
+ cleanup,
1753
+ startingCommit,
1754
+ runId,
1755
+ logTransport
1756
+ };
1739
1757
  }
1740
1758
 
1741
- // src/skills-library-fetch.ts
1742
- var API_BASE_URL3 = "https://www.contextgraph.dev";
1743
- async function fetchSkillsLibrary(authToken) {
1759
+ // src/workflows/prepare.ts
1760
+ var API_BASE_URL4 = "https://www.contextgraph.dev";
1761
+ async function runPrepare(actionId, options) {
1762
+ const credentials = await loadCredentials();
1763
+ if (!credentials) {
1764
+ console.error("\u274C Not authenticated. Run authentication first.");
1765
+ process.exit(1);
1766
+ }
1767
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1768
+ console.error("\u274C Token expired. Re-authenticate to continue.");
1769
+ process.exit(1);
1770
+ }
1771
+ let runId = options?.runId;
1772
+ let heartbeatManager;
1773
+ let logBuffer;
1774
+ let workspacePath;
1775
+ let cleanup;
1776
+ let logTransport;
1744
1777
  try {
1778
+ if (!runId) {
1779
+ const setup = await setupWorkspaceForAction(actionId, {
1780
+ authToken: credentials.clerkToken,
1781
+ phase: "prepare",
1782
+ startingCommit: options?.startingCommit,
1783
+ skipSkills: options?.skipSkills
1784
+ });
1785
+ workspacePath = setup.workspacePath;
1786
+ cleanup = setup.cleanup;
1787
+ runId = setup.runId;
1788
+ logTransport = setup.logTransport;
1789
+ } else {
1790
+ console.log(`[Log Streaming] Using pre-created run: ${runId}`);
1791
+ workspacePath = options?.cwd || process.cwd();
1792
+ logTransport = new LogTransportService(API_BASE_URL4, credentials.clerkToken, runId);
1793
+ }
1794
+ console.log(`Fetching preparation instructions for action ${actionId}...
1795
+ `);
1745
1796
  const response = await fetchWithRetry(
1746
- `${API_BASE_URL3}/api/skills/library`,
1797
+ `${API_BASE_URL4}/api/prompts/prepare`,
1747
1798
  {
1799
+ method: "POST",
1748
1800
  headers: {
1749
- "x-authorization": `Bearer ${authToken}`,
1801
+ "Authorization": `Bearer ${credentials.clerkToken}`,
1750
1802
  "Content-Type": "application/json"
1751
- }
1752
- },
1753
- {
1754
- maxRetries: 2,
1755
- baseDelayMs: 500
1803
+ },
1804
+ body: JSON.stringify({ actionId, runId })
1756
1805
  }
1757
1806
  );
1758
1807
  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 [];
1808
+ const errorText = await response.text();
1809
+ throw new Error(`Failed to fetch prepare prompt: ${response.statusText}
1810
+ ${errorText}`);
1766
1811
  }
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
- }
1812
+ const { prompt } = await response.json();
1813
+ await logTransport.updateRunState("preparing");
1814
+ heartbeatManager = new HeartbeatManager(API_BASE_URL4, credentials.clerkToken, runId);
1815
+ heartbeatManager.start();
1816
+ console.log("[Log Streaming] Heartbeat started");
1817
+ logBuffer = new LogBuffer(logTransport);
1818
+ logBuffer.start();
1819
+ console.log("Spawning Claude for preparation...\n");
1820
+ const claudeResult = await executeClaude({
1821
+ prompt,
1822
+ cwd: workspacePath,
1823
+ authToken: credentials.clerkToken,
1824
+ model: options?.model || "claude-opus-4-5-20251101",
1825
+ onLogEvent: (event) => {
1826
+ logBuffer.push(event);
1776
1827
  }
1777
- const contentWithoutFrontmatter = skill.content.replace(/^---\n[\s\S]*?\n---\n/, "");
1778
- return {
1779
- name,
1780
- description,
1781
- content: contentWithoutFrontmatter
1782
- };
1783
1828
  });
1784
- return skills;
1829
+ if (claudeResult.exitCode === 0) {
1830
+ await logTransport.finishRun("success", {
1831
+ exitCode: claudeResult.exitCode,
1832
+ cost: claudeResult.cost,
1833
+ usage: claudeResult.usage
1834
+ });
1835
+ console.log("\n\u2705 Preparation complete");
1836
+ } else {
1837
+ await logTransport.finishRun("error", {
1838
+ exitCode: claudeResult.exitCode,
1839
+ errorMessage: `Claude preparation failed with exit code ${claudeResult.exitCode}`
1840
+ });
1841
+ console.error(`
1842
+ \u274C Claude preparation failed with exit code ${claudeResult.exitCode}`);
1843
+ process.exit(1);
1844
+ }
1785
1845
  } 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"
1846
+ if (runId) {
1847
+ try {
1848
+ await logTransport.finishRun("error", {
1849
+ errorMessage: error instanceof Error ? error.message : String(error)
1850
+ });
1851
+ } catch (stateError) {
1852
+ console.error("[Log Streaming] Failed to update run state:", stateError);
1853
+ }
1854
+ }
1855
+ throw error;
1856
+ } finally {
1857
+ if (heartbeatManager) {
1858
+ heartbeatManager.stop();
1859
+ console.log("[Log Streaming] Heartbeat stopped");
1860
+ }
1861
+ if (logBuffer) {
1862
+ await logBuffer.stop();
1863
+ console.log("[Log Streaming] Logs flushed");
1864
+ }
1865
+ if (cleanup) {
1866
+ await cleanup();
1798
1867
  }
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
1868
  }
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
1869
  }
1838
- function buildAuthenticatedUrl(repoUrl, token) {
1839
- if (repoUrl.startsWith("https://github.com/")) {
1840
- return repoUrl.replace("https://github.com/", `https://${token}@github.com/`);
1870
+
1871
+ // src/workflows/execute.ts
1872
+ var API_BASE_URL5 = "https://www.contextgraph.dev";
1873
+ async function runExecute(actionId, options) {
1874
+ const credentials = await loadCredentials();
1875
+ if (!credentials) {
1876
+ console.error("\u274C Not authenticated. Run authentication first.");
1877
+ process.exit(1);
1841
1878
  }
1842
- if (repoUrl.startsWith("https://github.com")) {
1843
- return repoUrl.replace("https://github.com", `https://${token}@github.com`);
1879
+ if (isExpired(credentials) || isTokenExpired(credentials.clerkToken)) {
1880
+ console.error("\u274C Token expired. Re-authenticate to continue.");
1881
+ process.exit(1);
1844
1882
  }
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
- };
1883
+ let runId = options?.runId;
1884
+ let heartbeatManager;
1885
+ let logBuffer;
1886
+ let workspacePath;
1887
+ let cleanup;
1888
+ let logTransport;
1858
1889
  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);
1890
+ if (!runId) {
1891
+ const setup = await setupWorkspaceForAction(actionId, {
1892
+ authToken: credentials.clerkToken,
1893
+ phase: "execute",
1894
+ startingCommit: options?.startingCommit,
1895
+ skipSkills: options?.skipSkills
1896
+ });
1897
+ workspacePath = setup.workspacePath;
1898
+ cleanup = setup.cleanup;
1899
+ runId = setup.runId;
1900
+ logTransport = setup.logTransport;
1901
+ } else {
1902
+ console.log(`[Log Streaming] Using pre-created run: ${runId}`);
1903
+ workspacePath = options?.cwd || process.cwd();
1904
+ logTransport = new LogTransportService(API_BASE_URL5, credentials.clerkToken, runId);
1869
1905
  }
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);
1906
+ console.log(`Fetching execution instructions for action ${actionId}...
1907
+ `);
1908
+ const response = await fetchWithRetry(
1909
+ `${API_BASE_URL5}/api/prompts/execute`,
1910
+ {
1911
+ method: "POST",
1912
+ headers: {
1913
+ "Authorization": `Bearer ${credentials.clerkToken}`,
1914
+ "Content-Type": "application/json"
1915
+ },
1916
+ body: JSON.stringify({ actionId, runId })
1882
1917
  }
1918
+ );
1919
+ if (!response.ok) {
1920
+ const errorText = await response.text();
1921
+ throw new Error(`Failed to fetch execute prompt: ${response.statusText}
1922
+ ${errorText}`);
1883
1923
  }
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)");
1924
+ const { prompt } = await response.json();
1925
+ await logTransport.updateRunState("executing");
1926
+ heartbeatManager = new HeartbeatManager(API_BASE_URL5, credentials.clerkToken, runId);
1927
+ heartbeatManager.start();
1928
+ console.log("[Log Streaming] Heartbeat started");
1929
+ logBuffer = new LogBuffer(logTransport);
1930
+ logBuffer.start();
1931
+ console.log("Spawning Claude for execution...\n");
1932
+ const claudeResult = await executeClaude({
1933
+ prompt,
1934
+ cwd: workspacePath,
1935
+ authToken: credentials.clerkToken,
1936
+ ...options?.model ? { model: options.model } : {},
1937
+ onLogEvent: (event) => {
1938
+ logBuffer.push(event);
1895
1939
  }
1896
- } catch (skillError) {
1897
- console.warn("\u26A0\uFE0F Skill injection failed (agent will continue):", skillError);
1940
+ });
1941
+ if (claudeResult.exitCode === 0) {
1942
+ await logTransport.finishRun("success", {
1943
+ exitCode: claudeResult.exitCode,
1944
+ cost: claudeResult.cost,
1945
+ usage: claudeResult.usage
1946
+ });
1947
+ console.log("\n\u2705 Execution complete");
1948
+ } else {
1949
+ await logTransport.finishRun("error", {
1950
+ exitCode: claudeResult.exitCode,
1951
+ errorMessage: `Claude execution failed with exit code ${claudeResult.exitCode}`
1952
+ });
1953
+ throw new Error(`Claude execution failed with exit code ${claudeResult.exitCode}`);
1898
1954
  }
1899
- return { path: workspacePath, startingCommit, cleanup };
1900
1955
  } catch (error) {
1901
- await cleanup();
1956
+ if (runId) {
1957
+ try {
1958
+ await logTransport.finishRun("error", {
1959
+ errorMessage: error instanceof Error ? error.message : String(error)
1960
+ });
1961
+ } catch (stateError) {
1962
+ console.error("[Log Streaming] Failed to update run state:", stateError);
1963
+ }
1964
+ }
1902
1965
  throw error;
1966
+ } finally {
1967
+ if (heartbeatManager) {
1968
+ heartbeatManager.stop();
1969
+ console.log("[Log Streaming] Heartbeat stopped");
1970
+ }
1971
+ if (logBuffer) {
1972
+ await logBuffer.stop();
1973
+ console.log("[Log Streaming] Logs flushed");
1974
+ }
1975
+ if (cleanup) {
1976
+ await cleanup();
1977
+ }
1903
1978
  }
1904
1979
  }
1905
1980
 
1906
1981
  // src/workflows/agent.ts
1982
+ import { randomUUID } from "crypto";
1983
+ import { readFileSync } from "fs";
1984
+ import { fileURLToPath } from "url";
1985
+ import { dirname, join as join5 } from "path";
1907
1986
  var __filename2 = fileURLToPath(import.meta.url);
1908
1987
  var __dirname2 = dirname(__filename2);
1909
1988
  var packageJson = JSON.parse(
1910
- readFileSync(join4(__dirname2, "../package.json"), "utf-8")
1989
+ readFileSync(join5(__dirname2, "../package.json"), "utf-8")
1911
1990
  );
1912
1991
  var INITIAL_POLL_INTERVAL = parseInt(process.env.WORKER_INITIAL_POLL_INTERVAL || "2000", 10);
1913
1992
  var MAX_POLL_INTERVAL = parseInt(process.env.WORKER_MAX_POLL_INTERVAL || "5000", 10);
@@ -2077,35 +2156,24 @@ async function runLocalAgent(options) {
2077
2156
  continue;
2078
2157
  }
2079
2158
  console.log(`Working: ${actionDetail.title}`);
2080
- const repoUrl = actionDetail.resolved_repository_url || actionDetail.repository_url;
2081
- const branch = actionDetail.resolved_branch || actionDetail.branch;
2082
2159
  let workspacePath;
2083
2160
  let cleanup;
2084
2161
  let startingCommit;
2085
- const needsRepo = repoUrl;
2162
+ let runId;
2086
2163
  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
- }
2164
+ const setup = await setupWorkspaceForAction(actionDetail.id, {
2165
+ authToken: credentials.clerkToken,
2166
+ phase,
2167
+ actionDetail,
2168
+ // Pass pre-fetched action detail to avoid redundant API call
2169
+ skipSkills: options?.skipSkills
2170
+ });
2171
+ workspacePath = setup.workspacePath;
2172
+ cleanup = setup.cleanup;
2173
+ startingCommit = setup.startingCommit;
2174
+ runId = setup.runId;
2107
2175
  if (phase === "prepare") {
2108
- await runPrepare(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel });
2176
+ await runPrepare(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel, runId });
2109
2177
  stats.prepared++;
2110
2178
  if (currentClaim && apiClient) {
2111
2179
  try {
@@ -2122,7 +2190,7 @@ async function runLocalAgent(options) {
2122
2190
  continue;
2123
2191
  }
2124
2192
  try {
2125
- await runExecute(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel });
2193
+ await runExecute(actionDetail.id, { cwd: workspacePath, startingCommit, model: options?.forceModel, runId });
2126
2194
  stats.executed++;
2127
2195
  console.log(`Completed: ${actionDetail.title}`);
2128
2196
  } catch (executeError) {
@@ -2171,14 +2239,15 @@ async function runLocalAgent(options) {
2171
2239
  var __filename3 = fileURLToPath2(import.meta.url);
2172
2240
  var __dirname3 = dirname2(__filename3);
2173
2241
  var packageJson2 = JSON.parse(
2174
- readFileSync2(join5(__dirname3, "../package.json"), "utf-8")
2242
+ readFileSync2(join6(__dirname3, "../package.json"), "utf-8")
2175
2243
  );
2176
2244
  var program = new Command();
2177
2245
  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) => {
2246
+ 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
2247
  try {
2180
2248
  await runLocalAgent({
2181
- forceModel: options.forceHaiku ? "claude-haiku-4-5-20251001" : void 0
2249
+ forceModel: options.forceHaiku ? "claude-haiku-4-5-20251001" : void 0,
2250
+ skipSkills: options.skipSkills
2182
2251
  });
2183
2252
  } catch (error) {
2184
2253
  if (error instanceof Error) {
@@ -2201,17 +2270,17 @@ program.command("auth").description("Authenticate with contextgraph.dev").action
2201
2270
  process.exit(1);
2202
2271
  }
2203
2272
  });
2204
- program.command("prepare").argument("<action-id>", "Action ID to prepare").description("Prepare a single action").action(async (actionId) => {
2273
+ 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
2274
  try {
2206
- await runPrepare(actionId);
2275
+ await runPrepare(actionId, { skipSkills: options.skipSkills });
2207
2276
  } catch (error) {
2208
2277
  console.error("Error preparing action:", error instanceof Error ? error.message : error);
2209
2278
  process.exit(1);
2210
2279
  }
2211
2280
  });
2212
- program.command("execute").argument("<action-id>", "Action ID to execute").description("Execute a single action").action(async (actionId) => {
2281
+ 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
2282
  try {
2214
- await runExecute(actionId);
2283
+ await runExecute(actionId, { skipSkills: options.skipSkills });
2215
2284
  } catch (error) {
2216
2285
  console.error("Error executing action:", error instanceof Error ? error.message : error);
2217
2286
  process.exit(1);