agentxchain 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,35 +19,63 @@ npx agentxchain init
19
19
  ```bash
20
20
  agentxchain init # create a project (template selection)
21
21
  cd my-project/
22
- export CURSOR_API_KEY=your_key # from cursor.com/settings
22
+ echo "CURSOR_API_KEY=your_key" >> .env # from cursor.com/settings -> Cloud Agents
23
23
  agentxchain start --ide cursor # launch agents
24
24
  agentxchain watch # coordinate turns automatically
25
25
  ```
26
26
 
27
+ > `CURSOR_API_KEY` is required for Cursor commands (`start/watch/stop/claim/release` when using Cursor sessions).
28
+
27
29
  ## Commands
28
30
 
29
31
  | Command | What it does |
30
32
  |---------|-------------|
31
33
  | `init` | Create project folder with agents, protocol files, and templates |
32
- | `start` | Launch agents in Cursor, Claude Code, or VS Code |
34
+ | `start` | Launch agents in Cursor, Claude Code, or VS Code (`CURSOR_API_KEY` required for Cursor) |
33
35
  | `watch` | The referee — coordinates turns, enforces TTL, wakes agents |
34
36
  | `status` | Show lock, phase, agents, Cursor session info |
35
37
  | `claim` | Human takes control (pauses Cursor agents) |
36
38
  | `release` | Hand lock back to agents |
37
39
  | `stop` | Terminate all running agents |
40
+ | `branch` | Show/set Cursor branch override (`cursor.ref`) |
38
41
  | `config` | View/edit config, add/remove agents, change rules |
39
42
  | `update` | Self-update CLI from npm |
40
43
 
44
+ ### Branch selection
45
+
46
+ By default, Cursor launches use your current local git branch. You can override this when needed.
47
+
48
+ ```bash
49
+ agentxchain branch # show current/effective branch
50
+ agentxchain branch develop # pin to a specific branch
51
+ agentxchain branch --use-current # pin to whatever branch you're on now
52
+ agentxchain branch --unset # remove pin; follow active git branch
53
+ ```
54
+
41
55
  ## Key features
42
56
 
43
57
  - **Claim-based coordination** — no fixed turn order; agents self-organize
44
58
  - **User-defined teams** — any number of agents, any roles
45
59
  - **Cursor Cloud Agents** — launch and manage agents via API
60
+ - **Branch-safe launching** — defaults to active git branch; optional `branch` override
61
+ - **Project `.env` loading** — CLI auto-reads `CURSOR_API_KEY` from project root `.env`
46
62
  - **Lock TTL** — stale locks auto-released after timeout
47
63
  - **Verify command** — agents must pass tests before releasing
48
64
  - **Human-in-the-loop** — claim/release to intervene anytime
49
65
  - **Team templates** — SaaS MVP, Landing Page, Bug Squad, API Builder, Refactor Team
50
66
 
67
+ ## Publish updates (maintainers)
68
+
69
+ ```bash
70
+ cd cli
71
+ bash scripts/publish-npm.sh # patch bump + publish
72
+ bash scripts/publish-npm.sh minor # minor bump + publish
73
+ bash scripts/publish-npm.sh 0.5.0 # explicit version + publish
74
+ bash scripts/publish-npm.sh patch --dry-run
75
+ ```
76
+
77
+ If `NPM_TOKEN` exists in `cli/.env`, the script uses it automatically.
78
+
51
79
  ## Links
52
80
 
53
81
  - [agentxchain.dev](https://agentxchain.dev)
@@ -9,13 +9,14 @@ import { configCommand } from '../src/commands/config.js';
9
9
  import { updateCommand } from '../src/commands/update.js';
10
10
  import { watchCommand } from '../src/commands/watch.js';
11
11
  import { claimCommand, releaseCommand } from '../src/commands/claim.js';
12
+ import { branchCommand } from '../src/commands/branch.js';
12
13
 
13
14
  const program = new Command();
14
15
 
15
16
  program
16
17
  .name('agentxchain')
17
18
  .description('Multi-agent coordination in your IDE')
18
- .version('0.1.1');
19
+ .version('0.4.0');
19
20
 
20
21
  program
21
22
  .command('init')
@@ -51,6 +52,13 @@ program
51
52
  .option('-j, --json', 'Output config as JSON')
52
53
  .action(configCommand);
53
54
 
55
+ program
56
+ .command('branch [name]')
57
+ .description('Show or set the Cursor branch used for agent launches')
58
+ .option('--use-current', 'Set override to the current local git branch')
59
+ .option('--unset', 'Remove override and follow active git branch automatically')
60
+ .action(branchCommand);
61
+
54
62
  program
55
63
  .command('watch')
56
64
  .description('Watch lock.json and coordinate agent turns (the referee)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,8 @@
14
14
  "scripts": {
15
15
  "dev": "node bin/agentxchain.js",
16
16
  "build:macos": "bun build bin/agentxchain.js --compile --target=bun-darwin-arm64 --outfile=dist/agentxchain-macos-arm64",
17
- "build:linux": "bun build bin/agentxchain.js --compile --target=bun-linux-x64 --outfile=dist/agentxchain-linux-x64"
17
+ "build:linux": "bun build bin/agentxchain.js --compile --target=bun-linux-x64 --outfile=dist/agentxchain-linux-x64",
18
+ "publish:npm": "bash scripts/publish-npm.sh"
18
19
  },
19
20
  "keywords": [
20
21
  "ai",
@@ -2,7 +2,8 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { generateSeedPrompt } from '../lib/seed-prompt.js';
5
- import { getRepoUrl } from '../lib/repo.js';
5
+ import { getRepoUrl, getCurrentBranch } from '../lib/repo.js';
6
+ import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
6
7
 
7
8
  const API_BASE = 'https://api.cursor.com/v0';
8
9
 
@@ -16,11 +17,11 @@ function authHeaders(apiKey) {
16
17
  // --- Public API ---
17
18
 
18
19
  export async function launchCursorAgents(config, root, opts) {
19
- const apiKey = process.env.CURSOR_API_KEY;
20
+ const apiKey = getCursorApiKey(root);
20
21
 
21
22
  if (!apiKey) {
22
- printApiKeyHelp();
23
- return fallbackPromptOutput(config, opts);
23
+ printCursorApiKeyRequired('`agentxchain start --ide cursor`');
24
+ return [];
24
25
  }
25
26
 
26
27
  const repoUrl = await getRepoUrl(root);
@@ -32,9 +33,11 @@ export async function launchCursorAgents(config, root, opts) {
32
33
  }
33
34
 
34
35
  const model = config.cursor?.model || 'default';
35
- const ref = config.cursor?.ref || 'main';
36
+ const ref = config.cursor?.ref || getCurrentBranch(root) || 'main';
37
+ console.log(chalk.dim(` Cursor source: ${repoUrl} @ ${ref}`));
36
38
  const agents = filterAgents(config, opts.agent);
37
39
  const launched = [];
40
+ let branchErrorCount = 0;
38
41
 
39
42
  for (const [id, agent] of Object.entries(agents)) {
40
43
  const prompt = generateSeedPrompt(id, agent, config);
@@ -56,6 +59,9 @@ export async function launchCursorAgents(config, root, opts) {
56
59
  if (!res.ok) {
57
60
  const errBody = await res.text();
58
61
  console.log(chalk.red(` ✗ ${id}: ${res.status} ${errBody}`));
62
+ if (errBody.includes('Failed to verify existence of branch')) {
63
+ branchErrorCount += 1;
64
+ }
59
65
  continue;
60
66
  }
61
67
 
@@ -76,7 +82,16 @@ export async function launchCursorAgents(config, root, opts) {
76
82
  }
77
83
 
78
84
  if (launched.length > 0) {
79
- saveSession(root, launched, repoUrl);
85
+ saveSession(root, launched, repoUrl, ref);
86
+ }
87
+
88
+ if (launched.length === 0 && branchErrorCount > 0) {
89
+ console.log('');
90
+ console.log(chalk.yellow(' Launch failed because the branch ref is invalid for this repository.'));
91
+ console.log(chalk.dim(' Fix by setting the branch explicitly in agentxchain.json:'));
92
+ console.log(chalk.bold(' "cursor": { "ref": "your-default-branch" }'));
93
+ console.log(chalk.dim(' Or switch to the target branch locally, then re-run start.'));
94
+ console.log('');
80
95
  }
81
96
 
82
97
  return launched;
@@ -137,12 +152,13 @@ export function loadSession(root) {
137
152
 
138
153
  // --- Internal ---
139
154
 
140
- function saveSession(root, launched, repoUrl) {
155
+ function saveSession(root, launched, repoUrl, ref) {
141
156
  const session = {
142
157
  launched,
143
158
  started_at: new Date().toISOString(),
144
159
  ide: 'cursor',
145
- repo: repoUrl
160
+ repo: repoUrl,
161
+ ref
146
162
  };
147
163
  const sessionPath = join(root, '.agentxchain-session.json');
148
164
  writeFileSync(sessionPath, JSON.stringify(session, null, 2) + '\n');
@@ -160,29 +176,5 @@ function filterAgents(config, specificId) {
160
176
  return config.agents;
161
177
  }
162
178
 
163
- function fallbackPromptOutput(config, opts) {
164
- const agents = filterAgents(config, opts.agent);
165
- console.log(chalk.bold(' No API key. Printing seed prompts for manual use:'));
166
- console.log('');
167
- for (const [id, agent] of Object.entries(agents)) {
168
- const prompt = generateSeedPrompt(id, agent, config);
169
- console.log(chalk.dim(' ' + '─'.repeat(50)));
170
- console.log(chalk.cyan(` Agent: ${chalk.bold(id)} (${agent.name})`));
171
- console.log(chalk.dim(' ' + '─'.repeat(50)));
172
- console.log('');
173
- console.log(prompt);
174
- console.log('');
175
- }
176
- return [];
177
- }
178
-
179
- function printApiKeyHelp() {
180
- console.log('');
181
- console.log(chalk.yellow(' CURSOR_API_KEY not found.'));
182
- console.log('');
183
- console.log(` 1. Go to ${chalk.cyan('cursor.com/settings')} → Cloud Agents`);
184
- console.log(' 2. Create an API key');
185
- console.log(` 3. Add to .env: ${chalk.bold('CURSOR_API_KEY=your_key')}`);
186
- console.log(` 4. Run: ${chalk.bold('source .env && agentxchain start')}`);
187
- console.log('');
188
- }
179
+ // No prompt fallback here by design.
180
+ // Cursor mode is strict: an API key is required.
@@ -0,0 +1,98 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import { loadConfig, CONFIG_FILE } from '../lib/config.js';
5
+ import { getCurrentBranch } from '../lib/repo.js';
6
+
7
+ export async function branchCommand(name, opts) {
8
+ const result = loadConfig();
9
+ if (!result) {
10
+ console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
11
+ process.exit(1);
12
+ }
13
+
14
+ const { root, config } = result;
15
+ const configPath = join(root, CONFIG_FILE);
16
+ const currentGitBranch = getCurrentBranch(root) || 'main';
17
+ const hasOverride = !!config.cursor?.ref;
18
+ const overrideBranch = hasOverride ? config.cursor.ref : null;
19
+
20
+ if (opts.unset && (name || opts.useCurrent)) {
21
+ console.log(chalk.red(' Use either --unset OR a branch value, not both.'));
22
+ process.exit(1);
23
+ }
24
+
25
+ if (name && opts.useCurrent) {
26
+ console.log(chalk.red(' Use either --use-current OR a branch value, not both.'));
27
+ process.exit(1);
28
+ }
29
+
30
+ if (opts.unset) {
31
+ if (config.cursor?.ref) {
32
+ delete config.cursor.ref;
33
+ if (config.cursor && Object.keys(config.cursor).length === 0) delete config.cursor;
34
+ saveConfig(configPath, config);
35
+ }
36
+
37
+ console.log('');
38
+ console.log(chalk.green(' ✓ Cleared branch override.'));
39
+ console.log(chalk.dim(` Effective branch now follows git: ${currentGitBranch}`));
40
+ console.log('');
41
+ return;
42
+ }
43
+
44
+ if (opts.useCurrent) {
45
+ setBranchOverride(config, configPath, currentGitBranch);
46
+ console.log('');
47
+ console.log(chalk.green(` ✓ Set Cursor branch override to current git branch: ${chalk.bold(currentGitBranch)}`));
48
+ console.log('');
49
+ return;
50
+ }
51
+
52
+ if (name) {
53
+ const branch = String(name).trim();
54
+ if (!branch) {
55
+ console.log(chalk.red(' Branch cannot be empty.'));
56
+ process.exit(1);
57
+ }
58
+ if (!/^[A-Za-z0-9._/-]+$/.test(branch)) {
59
+ console.log(chalk.red(' Invalid branch name. Use letters, numbers, ., _, -, /.'));
60
+ process.exit(1);
61
+ }
62
+
63
+ setBranchOverride(config, configPath, branch);
64
+ console.log('');
65
+ console.log(chalk.green(` ✓ Set Cursor branch override: ${chalk.bold(branch)}`));
66
+ if (branch !== currentGitBranch) {
67
+ console.log(chalk.dim(` (current git branch is ${currentGitBranch})`));
68
+ }
69
+ console.log('');
70
+ return;
71
+ }
72
+
73
+ const effective = overrideBranch || currentGitBranch;
74
+ console.log('');
75
+ console.log(chalk.bold(' AgentXchain Branch'));
76
+ console.log(chalk.dim(' ' + '─'.repeat(36)));
77
+ console.log('');
78
+ console.log(` ${chalk.dim('Current git branch:')} ${currentGitBranch}`);
79
+ console.log(` ${chalk.dim('Cursor override:')} ${overrideBranch || chalk.dim('none')}`);
80
+ console.log(` ${chalk.dim('Effective branch:')} ${chalk.bold(effective)}`);
81
+ console.log('');
82
+ console.log(chalk.dim(' Commands:'));
83
+ console.log(` ${chalk.bold('agentxchain branch')} Show effective branch`);
84
+ console.log(` ${chalk.bold('agentxchain branch <name>')} Set branch override`);
85
+ console.log(` ${chalk.bold('agentxchain branch --use-current')} Set override to current git branch`);
86
+ console.log(` ${chalk.bold('agentxchain branch --unset')} Follow git branch automatically`);
87
+ console.log('');
88
+ }
89
+
90
+ function setBranchOverride(config, configPath, branch) {
91
+ if (!config.cursor) config.cursor = {};
92
+ config.cursor.ref = branch;
93
+ saveConfig(configPath, config);
94
+ }
95
+
96
+ function saveConfig(configPath, config) {
97
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
98
+ }
@@ -3,6 +3,7 @@ import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
5
5
  import { stopAgent, sendFollowup, loadSession } from '../adapters/cursor.js';
6
+ import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
6
7
 
7
8
  export async function claimCommand(opts) {
8
9
  const result = loadConfig();
@@ -12,9 +13,17 @@ export async function claimCommand(opts) {
12
13
  const lock = loadLock(root);
13
14
  if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
14
15
 
15
- const apiKey = process.env.CURSOR_API_KEY;
16
+ const apiKey = getCursorApiKey(root);
16
17
  const session = loadSession(root);
17
- const hasCursor = session?.ide === 'cursor' && apiKey;
18
+ const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
19
+ const hasCursor = hasCursorSession && apiKey;
20
+
21
+ if (hasCursorSession && !apiKey) {
22
+ printCursorApiKeyRequired('`agentxchain claim` with Cursor agents');
23
+ console.log(chalk.dim(' Claim aborted so agents are not left running unexpectedly.'));
24
+ console.log('');
25
+ process.exit(1);
26
+ }
18
27
 
19
28
  if (lock.holder === 'human') {
20
29
  console.log('');
@@ -90,8 +99,17 @@ export async function releaseCommand() {
90
99
 
91
100
  // If releasing from human and Cursor session exists, wake the next agent
92
101
  if (who === 'human') {
93
- const apiKey = process.env.CURSOR_API_KEY;
102
+ const apiKey = getCursorApiKey(root);
94
103
  const session = loadSession(root);
104
+ const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
105
+
106
+ if (hasCursorSession && !apiKey) {
107
+ printCursorApiKeyRequired('`agentxchain release` with Cursor agents');
108
+ console.log(chalk.dim(' Lock released, but wake-up followup was skipped due to missing key.'));
109
+ console.log(chalk.dim(' Start `agentxchain watch` after setting the key.'));
110
+ console.log('');
111
+ return;
112
+ }
95
113
 
96
114
  if (session?.ide === 'cursor' && apiKey) {
97
115
  const agentIds = Object.keys(config.agents);
@@ -205,6 +205,25 @@ export async function initCommand(opts) {
205
205
  writeFileSync(join(dir, 'history.jsonl'), '');
206
206
  writeFileSync(join(dir, 'log.md'), `# ${project} — Agent Log\n\n## COMPRESSED CONTEXT\n\n(No compressed context yet.)\n\n## MESSAGE LOG\n\n(Agents append messages below this line.)\n`);
207
207
  writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
208
+ writeFileSync(join(dir, '.env.example'), 'CURSOR_API_KEY=\n');
209
+ if (!existsSync(join(dir, '.env'))) {
210
+ writeFileSync(
211
+ join(dir, '.env'),
212
+ '# Required for Cursor commands: start/watch/stop/claim/release\nCURSOR_API_KEY=\n'
213
+ );
214
+ }
215
+ const gitignorePath = join(dir, '.gitignore');
216
+ const requiredIgnores = ['.env', '.agentxchain-session.json', '.agentxchain-trigger.json'];
217
+ if (!existsSync(gitignorePath)) {
218
+ writeFileSync(gitignorePath, requiredIgnores.join('\n') + '\n');
219
+ } else {
220
+ const existingIgnore = readFileSync(gitignorePath, 'utf8');
221
+ const missing = requiredIgnores.filter(entry => !existingIgnore.split(/\r?\n/).includes(entry));
222
+ if (missing.length > 0) {
223
+ const prefix = existingIgnore.endsWith('\n') ? '' : '\n';
224
+ writeFileSync(gitignorePath, existingIgnore + prefix + missing.join('\n') + '\n');
225
+ }
226
+ }
208
227
 
209
228
  // .planning/ structure
210
229
  mkdirSync(join(dir, '.planning', 'research'), { recursive: true });
@@ -245,7 +264,8 @@ export async function initCommand(opts) {
245
264
  console.log('');
246
265
  console.log(` ${chalk.cyan('Next:')}`);
247
266
  console.log(` ${chalk.bold(`cd ${folderName}`)}`);
267
+ console.log(` ${chalk.bold('edit .env')} ${chalk.dim('# set CURSOR_API_KEY (required for Cursor mode)')}`);
268
+ console.log(` ${chalk.bold('agentxchain start')} ${chalk.dim('# launch agents in Cursor')}`);
248
269
  console.log(` ${chalk.bold('agentxchain watch')} ${chalk.dim('# start the referee')}`);
249
- console.log(` ${chalk.bold('agentxchain start')} ${chalk.dim('# launch agents in your IDE')}`);
250
270
  console.log('');
251
271
  }
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import ora from 'ora';
3
2
  import { loadConfig } from '../lib/config.js';
4
3
  import { generateSeedPrompt } from '../lib/seed-prompt.js';
4
+ import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
5
5
 
6
6
  export async function startCommand(opts) {
7
7
  const result = loadConfig();
@@ -33,6 +33,11 @@ export async function startCommand(opts) {
33
33
 
34
34
  switch (ide) {
35
35
  case 'cursor': {
36
+ const apiKey = getCursorApiKey(root);
37
+ if (!apiKey) {
38
+ printCursorApiKeyRequired('`agentxchain start --ide cursor`');
39
+ process.exit(1);
40
+ }
36
41
  const { launchCursorAgents } = await import('../adapters/cursor.js');
37
42
  await launchCursorAgents(config, root, opts);
38
43
  break;
@@ -1,6 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import { loadConfig, loadLock, loadState } from '../lib/config.js';
3
3
  import { getAgentStatus, loadSession } from '../adapters/cursor.js';
4
+ import { getCursorApiKey } from '../lib/cursor-api-key.js';
4
5
 
5
6
  export async function statusCommand(opts) {
6
7
  const result = loadConfig();
@@ -52,13 +53,16 @@ export async function statusCommand(opts) {
52
53
 
53
54
  // Cursor session info
54
55
  const session = loadSession(root);
55
- const apiKey = process.env.CURSOR_API_KEY;
56
+ const apiKey = getCursorApiKey(root);
56
57
  const hasCursor = session?.ide === 'cursor' && session?.launched?.length > 0;
57
58
 
58
59
  if (hasCursor) {
59
60
  console.log(` ${chalk.dim('Cursor:')} ${chalk.cyan('Active session')} (${session.launched.length} agents)`);
60
61
  console.log(` ${chalk.dim('Started:')} ${session.started_at}`);
61
62
  if (session.repo) console.log(` ${chalk.dim('Repo:')} ${session.repo}`);
63
+ if (!apiKey) {
64
+ console.log(` ${chalk.dim('API key:')} ${chalk.red('Missing')} (set CURSOR_API_KEY in .env for live statuses)`);
65
+ }
62
66
  console.log('');
63
67
  }
64
68
 
@@ -2,7 +2,8 @@ import { readFileSync, existsSync, unlinkSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import { loadConfig } from '../lib/config.js';
5
- import { deleteAgent, stopAgent, loadSession } from '../adapters/cursor.js';
5
+ import { deleteAgent, loadSession } from '../adapters/cursor.js';
6
+ import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
6
7
 
7
8
  const SESSION_FILE = '.agentxchain-session.json';
8
9
 
@@ -24,22 +25,24 @@ export async function stopCommand() {
24
25
  console.log('');
25
26
 
26
27
  if (session.ide === 'cursor') {
27
- const apiKey = process.env.CURSOR_API_KEY;
28
+ const apiKey = getCursorApiKey(root);
28
29
  if (!apiKey) {
29
- console.log(chalk.yellow(' CURSOR_API_KEY not set. Cannot stop agents via API.'));
30
- console.log(chalk.dim(' Stop them manually at cursor.com/agents'));
31
- } else {
32
- for (const agent of session.launched) {
33
- try {
34
- const deleted = await deleteAgent(apiKey, agent.cloudId);
35
- if (deleted) {
36
- console.log(chalk.green(` ✓ Deleted ${chalk.bold(agent.id)} (${agent.cloudId})`));
37
- } else {
38
- console.log(chalk.yellow(` ⚠ Could not delete ${agent.id} — may already be gone`));
39
- }
40
- } catch (err) {
41
- console.log(chalk.red(` ${agent.id}: ${err.message}`));
30
+ printCursorApiKeyRequired('`agentxchain stop` for Cursor agents');
31
+ console.log(chalk.dim(' Session file was kept so you can retry after setting the key.'));
32
+ console.log('');
33
+ return;
34
+ }
35
+
36
+ for (const agent of session.launched) {
37
+ try {
38
+ const deleted = await deleteAgent(apiKey, agent.cloudId);
39
+ if (deleted) {
40
+ console.log(chalk.green(` ✓ Deleted ${chalk.bold(agent.id)} (${agent.cloudId})`));
41
+ } else {
42
+ console.log(chalk.yellow(` Could not delete ${agent.id} — may already be gone`));
42
43
  }
44
+ } catch (err) {
45
+ console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
43
46
  }
44
47
  }
45
48
  } else if (session.ide === 'claude-code') {
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
5
5
  import { notifyHuman as sendNotification } from '../lib/notify.js';
6
6
  import { sendFollowup, getAgentStatus, stopAgent, loadSession } from '../adapters/cursor.js';
7
+ import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
7
8
 
8
9
  export async function watchCommand(opts) {
9
10
  const result = loadConfig();
@@ -16,10 +17,15 @@ export async function watchCommand(opts) {
16
17
  const interval = config.rules?.watch_interval_ms || 5000;
17
18
  const ttlMinutes = config.rules?.ttl_minutes || 10;
18
19
  const agentIds = Object.keys(config.agents);
19
- const apiKey = process.env.CURSOR_API_KEY;
20
+ const apiKey = getCursorApiKey(root);
20
21
  const session = loadSession(root);
21
22
  const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
22
23
 
24
+ if (hasCursorSession && !apiKey) {
25
+ printCursorApiKeyRequired('`agentxchain watch` with a Cursor session');
26
+ process.exit(1);
27
+ }
28
+
23
29
  console.log('');
24
30
  console.log(chalk.bold(' AgentXchain Watch'));
25
31
  console.log(chalk.dim(` Project: ${config.project}`));
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ function parseEnvFile(raw) {
6
+ const out = {};
7
+ const lines = raw.split(/\r?\n/);
8
+
9
+ for (const line of lines) {
10
+ const trimmed = line.trim();
11
+ if (!trimmed || trimmed.startsWith('#')) continue;
12
+
13
+ const eq = trimmed.indexOf('=');
14
+ if (eq === -1) continue;
15
+
16
+ const key = trimmed.slice(0, eq).trim();
17
+ let value = trimmed.slice(eq + 1).trim();
18
+
19
+ if (
20
+ (value.startsWith('"') && value.endsWith('"')) ||
21
+ (value.startsWith("'") && value.endsWith("'"))
22
+ ) {
23
+ value = value.slice(1, -1);
24
+ }
25
+
26
+ if (key) out[key] = value;
27
+ }
28
+
29
+ return out;
30
+ }
31
+
32
+ export function hydrateEnvFromProject(root) {
33
+ if (!root) return;
34
+
35
+ const envPath = join(root, '.env');
36
+ if (!existsSync(envPath)) return;
37
+
38
+ try {
39
+ const parsed = parseEnvFile(readFileSync(envPath, 'utf8'));
40
+ for (const [k, v] of Object.entries(parsed)) {
41
+ if (!process.env[k] && v !== undefined) process.env[k] = v;
42
+ }
43
+ } catch {
44
+ // Non-fatal: commands still work with shell env vars.
45
+ }
46
+ }
47
+
48
+ export function getCursorApiKey(root) {
49
+ hydrateEnvFromProject(root);
50
+ const key = process.env.CURSOR_API_KEY?.trim();
51
+ return key || null;
52
+ }
53
+
54
+ export function printCursorApiKeyRequired(commandName = 'this command') {
55
+ console.log('');
56
+ console.log(chalk.red(` CURSOR_API_KEY is required for ${commandName}.`));
57
+ console.log(chalk.dim(' Set it once in your project root .env file:'));
58
+ console.log(` ${chalk.bold('CURSOR_API_KEY=your_key')}`);
59
+ console.log(chalk.dim(' You can get a key from: cursor.com/settings -> Cloud Agents'));
60
+ console.log('');
61
+ }
package/src/lib/repo.js CHANGED
@@ -30,8 +30,16 @@ export async function getRepoUrl(root) {
30
30
 
31
31
  export function getCurrentBranch(root) {
32
32
  try {
33
- return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, encoding: 'utf8' }).trim();
33
+ const current = execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, encoding: 'utf8' }).trim();
34
+ if (current && current !== 'HEAD') return current;
35
+ } catch {}
36
+
37
+ try {
38
+ const remoteHead = execSync('git symbolic-ref --short refs/remotes/origin/HEAD', { cwd: root, encoding: 'utf8' }).trim();
39
+ if (remoteHead.includes('/')) return remoteHead.split('/').pop();
34
40
  } catch {
35
41
  return 'main';
36
42
  }
43
+
44
+ return 'main';
37
45
  }