agentxchain 0.4.1 → 0.4.3

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
@@ -20,11 +20,13 @@ npx agentxchain init
20
20
  agentxchain init # create a project (template selection)
21
21
  cd my-project/
22
22
  echo "CURSOR_API_KEY=your_key" >> .env # from cursor.com/settings -> Cloud Agents
23
+ # In Cursor, connect GitHub account (needed for private repos)
24
+ # Cursor Settings -> GitHub integration
23
25
  agentxchain start --ide cursor # launch agents
24
26
  agentxchain watch # coordinate turns automatically
25
27
  ```
26
28
 
27
- > `CURSOR_API_KEY` is required for Cursor commands (`start/watch/stop/claim/release` when using Cursor sessions).
29
+ > `CURSOR_API_KEY` is required for Cursor commands (`start/watch/stop/claim/release` when using Cursor sessions). Your Cursor account also needs GitHub access to the target repository.
28
30
 
29
31
  ## Commands
30
32
 
@@ -52,6 +54,13 @@ agentxchain branch --use-current # pin to whatever branch you're on now
52
54
  agentxchain branch --unset # remove pin; follow active git branch
53
55
  ```
54
56
 
57
+ ### Additional command flags
58
+
59
+ ```bash
60
+ agentxchain watch --daemon # run watch in background
61
+ agentxchain release --force # force-release non-human holder lock
62
+ ```
63
+
55
64
  ## Key features
56
65
 
57
66
  - **Claim-based coordination** — no fixed turn order; agents self-organize
@@ -74,7 +83,7 @@ bash scripts/publish-npm.sh 0.5.0 # explicit version + publish
74
83
  bash scripts/publish-npm.sh patch --dry-run
75
84
  ```
76
85
 
77
- If `NPM_TOKEN` exists in `cli/.env`, the script uses it automatically.
86
+ If `NPM_TOKEN` exists in `agentXchain.dev/.env` (project root), the script uses it automatically.
78
87
 
79
88
  ## Links
80
89
 
@@ -16,7 +16,7 @@ const program = new Command();
16
16
  program
17
17
  .name('agentxchain')
18
18
  .description('Multi-agent coordination in your IDE')
19
- .version('0.4.0');
19
+ .version('0.4.1');
20
20
 
21
21
  program
22
22
  .command('init')
@@ -74,6 +74,7 @@ program
74
74
  program
75
75
  .command('release')
76
76
  .description('Release the lock (hand back to agents)')
77
+ .option('--force', 'Force release even if a non-human holder has the lock')
77
78
  .action(releaseCommand);
78
79
 
79
80
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,6 +91,8 @@ export async function launchCursorAgents(config, root, opts) {
91
91
  console.log(chalk.dim(' Fix by setting the branch explicitly in agentxchain.json:'));
92
92
  console.log(chalk.bold(' "cursor": { "ref": "your-default-branch" }'));
93
93
  console.log(chalk.dim(' Or switch to the target branch locally, then re-run start.'));
94
+ console.log(chalk.dim(' If the branch exists on GitHub, verify your Cursor account has GitHub access'));
95
+ console.log(chalk.dim(' to this repository (Cursor Settings -> GitHub integration).'));
94
96
  console.log('');
95
97
  }
96
98
 
@@ -71,7 +71,7 @@ export async function claimCommand(opts) {
71
71
  console.log('');
72
72
  }
73
73
 
74
- export async function releaseCommand() {
74
+ export async function releaseCommand(opts) {
75
75
  const result = loadConfig();
76
76
  if (!result) { console.log(chalk.red(' No agentxchain.json found.')); process.exit(1); }
77
77
 
@@ -84,7 +84,18 @@ export async function releaseCommand() {
84
84
  return;
85
85
  }
86
86
 
87
+ if (lock.holder !== 'human' && !opts.force) {
88
+ const name = config.agents[lock.holder]?.name || lock.holder;
89
+ console.log('');
90
+ console.log(chalk.red(` Lock is held by ${chalk.bold(lock.holder)} (${name}), not human.`));
91
+ console.log(chalk.dim(' Refusing to release another holder without explicit override.'));
92
+ console.log(chalk.dim(' Use `agentxchain release --force` if you really want to break this lock.'));
93
+ console.log('');
94
+ process.exit(1);
95
+ }
96
+
87
97
  const who = lock.holder;
98
+ const priorLastReleasedBy = lock.last_released_by;
88
99
  const lockPath = join(root, LOCK_FILE);
89
100
  const newLock = {
90
101
  holder: null,
@@ -112,8 +123,12 @@ export async function releaseCommand() {
112
123
  }
113
124
 
114
125
  if (session?.ide === 'cursor' && apiKey) {
115
- const agentIds = Object.keys(config.agents);
116
- const next = agentIds[0];
126
+ const next = pickNextAgent(priorLastReleasedBy, config);
127
+ if (!next) {
128
+ console.log(chalk.dim(' No agents configured to wake.'));
129
+ console.log('');
130
+ return;
131
+ }
117
132
  const cloudAgent = session.launched.find(a => a.id === next);
118
133
 
119
134
  if (cloudAgent) {
@@ -133,3 +148,12 @@ export async function releaseCommand() {
133
148
 
134
149
  console.log('');
135
150
  }
151
+
152
+ function pickNextAgent(lastReleasedBy, config) {
153
+ const agentIds = Object.keys(config.agents || {});
154
+ if (agentIds.length === 0) return null;
155
+ if (!lastReleasedBy || !agentIds.includes(lastReleasedBy)) return agentIds[0];
156
+
157
+ const idx = agentIds.indexOf(lastReleasedBy);
158
+ return agentIds[(idx + 1) % agentIds.length];
159
+ }
@@ -102,6 +102,11 @@ function removeAgent(config, configPath, id) {
102
102
  }
103
103
 
104
104
  const name = config.agents[id].name;
105
+ if (Object.keys(config.agents).length <= 1) {
106
+ console.log(chalk.red(' Cannot remove the last agent.'));
107
+ console.log(chalk.dim(' Add another agent first, then remove this one if needed.'));
108
+ process.exit(1);
109
+ }
105
110
  delete config.agents[id];
106
111
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
107
112
 
@@ -11,9 +11,22 @@ export async function startCommand(opts) {
11
11
  }
12
12
 
13
13
  const { root, config } = result;
14
- const agentCount = Object.keys(config.agents).length;
14
+ const agentIds = Object.keys(config.agents || {});
15
+ const agentCount = agentIds.length;
15
16
  const ide = opts.ide;
16
17
 
18
+ if (agentCount === 0) {
19
+ console.log(chalk.red(' No agents configured in agentxchain.json.'));
20
+ console.log(chalk.dim(' Add an agent with: agentxchain config --add-agent'));
21
+ process.exit(1);
22
+ }
23
+
24
+ if (opts.agent && !config.agents?.[opts.agent]) {
25
+ console.log(chalk.red(` Agent "${opts.agent}" not found in agentxchain.json.`));
26
+ console.log(chalk.dim(` Available: ${agentIds.join(', ')}`));
27
+ process.exit(1);
28
+ }
29
+
17
30
  console.log('');
18
31
  console.log(chalk.bold(` Launching ${agentCount} agents via ${ide}`));
19
32
  console.log(chalk.dim(` Project: ${config.project}`));
@@ -23,6 +23,7 @@ export async function stopCommand() {
23
23
  console.log('');
24
24
  console.log(chalk.bold(` Stopping ${session.launched.length} agents (${session.ide})`));
25
25
  console.log('');
26
+ let allStopped = true;
26
27
 
27
28
  if (session.ide === 'cursor') {
28
29
  const apiKey = getCursorApiKey(root);
@@ -40,9 +41,11 @@ export async function stopCommand() {
40
41
  console.log(chalk.green(` ✓ Deleted ${chalk.bold(agent.id)} (${agent.cloudId})`));
41
42
  } else {
42
43
  console.log(chalk.yellow(` ⚠ Could not delete ${agent.id} — may already be gone`));
44
+ allStopped = false;
43
45
  }
44
46
  } catch (err) {
45
47
  console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
48
+ allStopped = false;
46
49
  }
47
50
  }
48
51
  } else if (session.ide === 'claude-code') {
@@ -56,18 +59,22 @@ export async function stopCommand() {
56
59
  console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
57
60
  } else {
58
61
  console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
62
+ allStopped = false;
59
63
  }
60
64
  }
61
65
  }
62
66
  }
63
67
  }
64
68
 
65
- // Remove session file
66
- const sessionPath = join(root, SESSION_FILE);
67
- if (existsSync(sessionPath)) unlinkSync(sessionPath);
68
-
69
69
  console.log('');
70
- console.log(chalk.dim(' Session file removed.'));
71
- console.log(chalk.green(' All agents stopped.'));
70
+ const sessionPath = join(root, SESSION_FILE);
71
+ if (allStopped) {
72
+ if (existsSync(sessionPath)) unlinkSync(sessionPath);
73
+ console.log(chalk.dim(' Session file removed.'));
74
+ console.log(chalk.green(' All agents stopped.'));
75
+ } else {
76
+ console.log(chalk.yellow(' Some agents could not be stopped.'));
77
+ console.log(chalk.dim(' Session file was kept so you can retry `agentxchain stop`.'));
78
+ }
72
79
  console.log('');
73
80
  }
@@ -1,5 +1,7 @@
1
1
  import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'fs';
2
- import { join } from 'path';
2
+ import { join, dirname } from 'path';
3
+ import { spawn } from 'child_process';
4
+ import { fileURLToPath } from 'url';
3
5
  import chalk from 'chalk';
4
6
  import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
5
7
  import { notifyHuman as sendNotification } from '../lib/notify.js';
@@ -7,6 +9,11 @@ import { sendFollowup, getAgentStatus, stopAgent, loadSession } from '../adapter
7
9
  import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
8
10
 
9
11
  export async function watchCommand(opts) {
12
+ if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
13
+ startWatchDaemon();
14
+ return;
15
+ }
16
+
10
17
  const result = loadConfig();
11
18
  if (!result) {
12
19
  console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
@@ -17,6 +24,11 @@ export async function watchCommand(opts) {
17
24
  const interval = config.rules?.watch_interval_ms || 5000;
18
25
  const ttlMinutes = config.rules?.ttl_minutes || 10;
19
26
  const agentIds = Object.keys(config.agents);
27
+ if (agentIds.length === 0) {
28
+ console.log(chalk.red(' No agents configured in agentxchain.json.'));
29
+ console.log(chalk.dim(' Add an agent with: agentxchain config --add-agent'));
30
+ process.exit(1);
31
+ }
20
32
  const apiKey = getCursorApiKey(root);
21
33
  const session = loadSession(root);
22
34
  const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
@@ -108,12 +120,19 @@ export async function watchCommand(opts) {
108
120
  if (stateKey !== lastState) {
109
121
  const next = pickNextAgent(lock, config);
110
122
  log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Waking ${chalk.bold(next)}.`);
111
- lastState = stateKey;
123
+ let wakeSucceeded = false;
112
124
 
113
125
  if (hasCursorSession && apiKey) {
114
- await wakeCursorAgent(apiKey, session, next, lock, config);
126
+ wakeSucceeded = await wakeCursorAgent(apiKey, session, next, lock, config, root);
115
127
  } else {
116
128
  writeTrigger(root, next, lock, config);
129
+ wakeSucceeded = true;
130
+ }
131
+
132
+ if (wakeSucceeded) {
133
+ lastState = stateKey;
134
+ } else {
135
+ log('warn', `Wake for ${next} failed. Will retry next poll.`);
117
136
  }
118
137
  }
119
138
  } catch (err) {
@@ -132,11 +151,12 @@ export async function watchCommand(opts) {
132
151
  });
133
152
  }
134
153
 
135
- async function wakeCursorAgent(apiKey, session, agentId, lock, config) {
154
+ async function wakeCursorAgent(apiKey, session, agentId, lock, config, root) {
136
155
  const cloudAgent = session.launched.find(a => a.id === agentId);
137
156
  if (!cloudAgent) {
138
157
  log('warn', `No Cursor cloud agent found for "${agentId}". Using trigger file.`);
139
- return;
158
+ writeTrigger(root, agentId, lock, config);
159
+ return true;
140
160
  }
141
161
 
142
162
  const name = config.agents[agentId]?.name || agentId;
@@ -158,13 +178,16 @@ This must be the last thing you write. The watch process will wake the next agen
158
178
  try {
159
179
  await sendFollowup(apiKey, cloudAgent.cloudId, wakeMessage);
160
180
  log('wake', `Sent followup to ${chalk.bold(agentId)} (${cloudAgent.cloudId})`);
181
+ return true;
161
182
  } catch (err) {
162
183
  log('error', `Failed to wake ${agentId}: ${err.message}`);
184
+ return false;
163
185
  }
164
186
  }
165
187
 
166
188
  function pickNextAgent(lock, config) {
167
189
  const agentIds = Object.keys(config.agents);
190
+ if (agentIds.length === 0) return null;
168
191
  const lastAgent = lock.last_released_by;
169
192
 
170
193
  if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
@@ -191,6 +214,7 @@ function forceRelease(root, lock, staleAgent, config) {
191
214
  }
192
215
 
193
216
  function writeTrigger(root, agentId, lock, config) {
217
+ if (!agentId) return;
194
218
  const triggerPath = join(root, '.agentxchain-trigger.json');
195
219
  writeFileSync(triggerPath, JSON.stringify({
196
220
  agent: agentId,
@@ -214,3 +238,20 @@ function log(type, msg) {
214
238
  };
215
239
  console.log(` ${chalk.dim(time)} ${tags[type] || chalk.dim(type)} ${msg}`);
216
240
  }
241
+
242
+ function startWatchDaemon() {
243
+ const currentDir = dirname(fileURLToPath(import.meta.url));
244
+ const cliBin = join(currentDir, '../../bin/agentxchain.js');
245
+ const child = spawn(process.execPath, [cliBin, 'watch'], {
246
+ cwd: process.cwd(),
247
+ detached: true,
248
+ stdio: 'ignore',
249
+ env: { ...process.env, AGENTXCHAIN_WATCH_DAEMON: '1' }
250
+ });
251
+ child.unref();
252
+
253
+ console.log('');
254
+ console.log(chalk.green(` ✓ Watch started in daemon mode (PID: ${child.pid})`));
255
+ console.log(chalk.dim(' Use `agentxchain stop` or kill the PID to stop it.'));
256
+ console.log('');
257
+ }
package/src/lib/repo.js CHANGED
@@ -2,7 +2,11 @@ import { execSync } from 'child_process';
2
2
 
3
3
  export async function getRepoUrl(root) {
4
4
  try {
5
- const raw = execSync('git remote get-url origin', { cwd: root, encoding: 'utf8' }).trim();
5
+ const raw = execSync('git remote get-url origin', {
6
+ cwd: root,
7
+ encoding: 'utf8',
8
+ stdio: ['ignore', 'pipe', 'ignore']
9
+ }).trim();
6
10
 
7
11
  // Convert SSH to HTTPS if needed
8
12
  // git@github.com:user/repo.git -> https://github.com/user/repo
@@ -11,18 +15,17 @@ export async function getRepoUrl(root) {
11
15
  return `https://github.com/${path}`;
12
16
  }
13
17
 
14
- // Already HTTPS strip .git suffix
15
- if (raw.includes('github.com')) {
16
- return raw.replace(/\.git$/, '');
17
- }
18
+ // Strip embedded credentials/tokens from HTTPS URLs.
19
+ // https://x-access-token:TOKEN@github.com/org/repo.git -> https://github.com/org/repo
20
+ // https://user:pass@github.com/org/repo.git -> https://github.com/org/repo
21
+ const credentialStripped = raw.replace(/^https?:\/\/[^/@]+@github\.com\//, 'https://github.com/');
18
22
 
19
- // Strip tokens from URL (x-access-token:TOKEN@github.com)
20
- if (raw.includes('x-access-token')) {
21
- const cleaned = raw.replace(/https:\/\/x-access-token:[^@]+@/, 'https://');
22
- return cleaned.replace(/\.git$/, '');
23
+ // Already HTTPS strip .git suffix
24
+ if (credentialStripped.includes('github.com')) {
25
+ return credentialStripped.replace(/\.git$/, '');
23
26
  }
24
27
 
25
- return raw;
28
+ return credentialStripped;
26
29
  } catch {
27
30
  return null;
28
31
  }
@@ -30,12 +33,20 @@ export async function getRepoUrl(root) {
30
33
 
31
34
  export function getCurrentBranch(root) {
32
35
  try {
33
- const current = execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, encoding: 'utf8' }).trim();
36
+ const current = execSync('git rev-parse --abbrev-ref HEAD', {
37
+ cwd: root,
38
+ encoding: 'utf8',
39
+ stdio: ['ignore', 'pipe', 'ignore']
40
+ }).trim();
34
41
  if (current && current !== 'HEAD') return current;
35
42
  } catch {}
36
43
 
37
44
  try {
38
- const remoteHead = execSync('git symbolic-ref --short refs/remotes/origin/HEAD', { cwd: root, encoding: 'utf8' }).trim();
45
+ const remoteHead = execSync('git symbolic-ref --short refs/remotes/origin/HEAD', {
46
+ cwd: root,
47
+ encoding: 'utf8',
48
+ stdio: ['ignore', 'pipe', 'ignore']
49
+ }).trim();
39
50
  if (remoteHead.includes('/')) return remoteHead.split('/').pop();
40
51
  } catch {
41
52
  return 'main';