agentxchain 0.6.0 → 0.7.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 +25 -56
- package/bin/agentxchain.js +8 -10
- package/package.json +1 -1
- package/src/commands/claim.js +2 -79
- package/src/commands/init.js +4 -12
- package/src/commands/start.js +19 -35
- package/src/commands/status.js +1 -47
- package/src/commands/stop.js +19 -42
- package/src/commands/watch.js +7 -90
- package/src/adapters/cursor.js +0 -182
- package/src/lib/cursor-api-key.js +0 -61
package/README.md
CHANGED
|
@@ -14,60 +14,32 @@ Or run without installing:
|
|
|
14
14
|
npx agentxchain init
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
## Quick start
|
|
18
|
-
|
|
19
|
-
No API keys or cloud connection needed. Uses native VS Code custom agents.
|
|
17
|
+
## Quick start
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
agentxchain init # create project with agents + hooks
|
|
23
21
|
cd my-project/ && code . # open in VS Code / Cursor
|
|
24
|
-
# Select an agent from Chat dropdown (auto-discovered from .github/agents/)
|
|
22
|
+
# Select an agent from the Chat dropdown (auto-discovered from .github/agents/)
|
|
25
23
|
agentxchain release # release human lock to begin turns
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
The `Stop` hook acts as referee: when an agent finishes, it
|
|
29
|
-
|
|
30
|
-
## Quick start — Cursor Cloud Agents
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
agentxchain init
|
|
34
|
-
cd my-project/
|
|
35
|
-
echo "CURSOR_API_KEY=your_key" >> .env # cursor.com/settings -> Cloud Agents
|
|
36
|
-
# Connect GitHub in Cursor Settings -> GitHub integration
|
|
37
|
-
agentxchain start --ide cursor # launch cloud agents
|
|
38
|
-
agentxchain watch # coordinate turns
|
|
39
|
-
agentxchain release # begin turns (initial lock is human)
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
> `CURSOR_API_KEY` is required for Cloud commands. Your Cursor account needs GitHub access to the target repository.
|
|
26
|
+
The `Stop` hook acts as referee: when an agent finishes, it determines the next agent and hands off automatically. No polling process needed.
|
|
43
27
|
|
|
44
28
|
## Commands
|
|
45
29
|
|
|
46
30
|
| Command | What it does |
|
|
47
31
|
|---------|-------------|
|
|
48
|
-
| `init` | Create project folder with agents, protocol files,
|
|
32
|
+
| `init` | Create project folder with agents, hooks, protocol files, and templates |
|
|
49
33
|
| `generate` | Regenerate VS Code agent files (`.agent.md`, hooks) from `agentxchain.json` |
|
|
50
|
-
| `start` |
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `claim` | Human takes control (pauses Cursor agents) |
|
|
34
|
+
| `start` | Show agent setup instructions for your IDE |
|
|
35
|
+
| `status` | Show lock, phase, agents |
|
|
36
|
+
| `claim` | Human takes control |
|
|
54
37
|
| `release` | Hand lock back to agents |
|
|
55
|
-
| `stop` | Terminate
|
|
56
|
-
| `
|
|
38
|
+
| `stop` | Terminate running Claude Code agent sessions |
|
|
39
|
+
| `watch` | Fallback referee for non-IDE environments |
|
|
57
40
|
| `config` | View/edit config, add/remove agents, change rules |
|
|
58
41
|
| `update` | Self-update CLI from npm |
|
|
59
42
|
|
|
60
|
-
### Branch selection
|
|
61
|
-
|
|
62
|
-
By default, Cursor launches use your current local git branch.
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
agentxchain branch # show current/effective branch
|
|
66
|
-
agentxchain branch develop # pin to a specific branch
|
|
67
|
-
agentxchain branch --use-current # pin to whatever branch you're on now
|
|
68
|
-
agentxchain branch --unset # remove pin; follow active git branch
|
|
69
|
-
```
|
|
70
|
-
|
|
71
43
|
### Additional flags
|
|
72
44
|
|
|
73
45
|
```bash
|
|
@@ -75,45 +47,42 @@ agentxchain watch --daemon # run watch in background
|
|
|
75
47
|
agentxchain release --force # force-release non-human holder lock
|
|
76
48
|
```
|
|
77
49
|
|
|
78
|
-
##
|
|
50
|
+
## How it works
|
|
79
51
|
|
|
80
52
|
`agentxchain init` generates native VS Code agent files:
|
|
81
53
|
|
|
82
|
-
- `.github/agents/*.agent.md` — custom agents (auto-discovered by VS Code Chat)
|
|
54
|
+
- `.github/agents/*.agent.md` — custom agents (auto-discovered by VS Code / Cursor Chat)
|
|
83
55
|
- `.github/hooks/agentxchain.json` — lifecycle hooks (Stop = referee, SessionStart = context injection)
|
|
84
56
|
- `scripts/agentxchain-*.sh` — hook shell scripts
|
|
85
57
|
|
|
86
|
-
|
|
87
|
-
- Status bar: lock holder, turn, phase (live-updated via file watcher)
|
|
88
|
-
- Sidebar: agent dashboard with quick actions
|
|
89
|
-
- Commands: Claim, Release, Status, Generate
|
|
90
|
-
|
|
91
|
-
Install the extension:
|
|
92
|
-
```bash
|
|
93
|
-
code --install-extension cli/vscode-extension/agentxchain-0.1.0.vsix
|
|
94
|
-
```
|
|
58
|
+
When an agent finishes its response, the Stop hook reads `lock.json`, determines the next agent, and hands off automatically.
|
|
95
59
|
|
|
96
60
|
## Key features
|
|
97
61
|
|
|
98
62
|
- **Native VS Code agents** — `.agent.md` files, lifecycle hooks, handoffs
|
|
99
|
-
- **
|
|
100
|
-
- **Stop hook referee** — deterministic turn coordination via
|
|
63
|
+
- **Works in any VS Code fork** — Cursor, VS Code, Windsurf, etc.
|
|
64
|
+
- **Stop hook referee** — deterministic turn coordination via lifecycle hooks
|
|
101
65
|
- **User-defined teams** — any number of agents, any roles
|
|
102
|
-
- **
|
|
103
|
-
- **Branch-safe launching** — defaults to active git branch
|
|
104
|
-
- **Lock TTL** — stale locks auto-released after timeout
|
|
105
|
-
- **Verify command** — agents must pass tests before releasing
|
|
66
|
+
- **No API keys or cloud required** — everything runs locally
|
|
106
67
|
- **Human-in-the-loop** — claim/release to intervene anytime
|
|
107
68
|
- **Team templates** — SaaS MVP, Landing Page, Bug Squad, API Builder, Refactor Team
|
|
108
69
|
|
|
70
|
+
## VS Code extension (optional)
|
|
71
|
+
|
|
72
|
+
For a richer UI experience, install the extension:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
code --install-extension cli/vscode-extension/agentxchain-0.1.0.vsix
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Adds: status bar (lock holder, turn, phase), sidebar dashboard, command palette integration.
|
|
79
|
+
|
|
109
80
|
## Publish updates (maintainers)
|
|
110
81
|
|
|
111
82
|
```bash
|
|
112
83
|
cd cli
|
|
113
84
|
bash scripts/publish-npm.sh # patch bump + publish
|
|
114
85
|
bash scripts/publish-npm.sh minor # minor bump + publish
|
|
115
|
-
bash scripts/publish-npm.sh 0.5.0 # explicit version + publish
|
|
116
|
-
bash scripts/publish-npm.sh patch --dry-run
|
|
117
86
|
```
|
|
118
87
|
|
|
119
88
|
If `NPM_TOKEN` exists in `agentXchain.dev/.env` (project root), the script uses it automatically.
|
package/bin/agentxchain.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
3
6
|
import { Command } from 'commander';
|
|
4
7
|
import { initCommand } from '../src/commands/init.js';
|
|
5
8
|
import { statusCommand } from '../src/commands/status.js';
|
|
@@ -9,15 +12,17 @@ import { configCommand } from '../src/commands/config.js';
|
|
|
9
12
|
import { updateCommand } from '../src/commands/update.js';
|
|
10
13
|
import { watchCommand } from '../src/commands/watch.js';
|
|
11
14
|
import { claimCommand, releaseCommand } from '../src/commands/claim.js';
|
|
12
|
-
import { branchCommand } from '../src/commands/branch.js';
|
|
13
15
|
import { generateCommand } from '../src/commands/generate.js';
|
|
14
16
|
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
19
|
+
|
|
15
20
|
const program = new Command();
|
|
16
21
|
|
|
17
22
|
program
|
|
18
23
|
.name('agentxchain')
|
|
19
24
|
.description('Multi-agent coordination in your IDE')
|
|
20
|
-
.version(
|
|
25
|
+
.version(pkg.version);
|
|
21
26
|
|
|
22
27
|
program
|
|
23
28
|
.command('init')
|
|
@@ -34,7 +39,7 @@ program
|
|
|
34
39
|
program
|
|
35
40
|
.command('start')
|
|
36
41
|
.description('Launch agents in your IDE')
|
|
37
|
-
.option('--ide <ide>', 'Target IDE:
|
|
42
|
+
.option('--ide <ide>', 'Target IDE: vscode, claude-code', 'vscode')
|
|
38
43
|
.option('--agent <id>', 'Launch a specific agent only')
|
|
39
44
|
.option('--dry-run', 'Print what would be launched without doing it')
|
|
40
45
|
.action(startCommand);
|
|
@@ -53,13 +58,6 @@ program
|
|
|
53
58
|
.option('-j, --json', 'Output config as JSON')
|
|
54
59
|
.action(configCommand);
|
|
55
60
|
|
|
56
|
-
program
|
|
57
|
-
.command('branch [name]')
|
|
58
|
-
.description('Show or set the Cursor branch used for agent launches')
|
|
59
|
-
.option('--use-current', 'Set override to the current local git branch')
|
|
60
|
-
.option('--unset', 'Remove override and follow active git branch automatically')
|
|
61
|
-
.action(branchCommand);
|
|
62
|
-
|
|
63
61
|
program
|
|
64
62
|
.command('generate')
|
|
65
63
|
.description('Regenerate VS Code agent files (.agent.md, hooks) from agentxchain.json')
|
package/package.json
CHANGED
package/src/commands/claim.js
CHANGED
|
@@ -2,8 +2,6 @@ import { writeFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
|
|
5
|
-
import { stopAgent, sendFollowup, loadSession } from '../adapters/cursor.js';
|
|
6
|
-
import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
|
|
7
5
|
|
|
8
6
|
export async function claimCommand(opts) {
|
|
9
7
|
const result = loadConfig();
|
|
@@ -13,18 +11,6 @@ export async function claimCommand(opts) {
|
|
|
13
11
|
const lock = loadLock(root);
|
|
14
12
|
if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
|
|
15
13
|
|
|
16
|
-
const apiKey = getCursorApiKey(root);
|
|
17
|
-
const session = loadSession(root);
|
|
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
|
-
}
|
|
27
|
-
|
|
28
14
|
if (lock.holder === 'human') {
|
|
29
15
|
console.log('');
|
|
30
16
|
console.log(chalk.yellow(' You already hold the lock.'));
|
|
@@ -42,19 +28,6 @@ export async function claimCommand(opts) {
|
|
|
42
28
|
return;
|
|
43
29
|
}
|
|
44
30
|
|
|
45
|
-
// Pause all Cursor agents when human claims
|
|
46
|
-
if (hasCursor && session.launched.length > 0) {
|
|
47
|
-
console.log(chalk.dim(' Pausing Cursor agents...'));
|
|
48
|
-
for (const agent of session.launched) {
|
|
49
|
-
try {
|
|
50
|
-
await stopAgent(apiKey, agent.cloudId);
|
|
51
|
-
console.log(chalk.dim(` Paused ${agent.id}`));
|
|
52
|
-
} catch {
|
|
53
|
-
console.log(chalk.dim(` Could not pause ${agent.id}`));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
31
|
const lockPath = join(root, LOCK_FILE);
|
|
59
32
|
const newLock = {
|
|
60
33
|
holder: 'human',
|
|
@@ -66,7 +39,6 @@ export async function claimCommand(opts) {
|
|
|
66
39
|
|
|
67
40
|
console.log('');
|
|
68
41
|
console.log(chalk.green(` ✓ Lock claimed by ${chalk.bold('human')} (turn ${lock.turn_number})`));
|
|
69
|
-
if (hasCursor) console.log(chalk.dim(' All Cursor agents paused.'));
|
|
70
42
|
console.log(` ${chalk.dim('Do your work, then:')} ${chalk.bold('agentxchain release')}`);
|
|
71
43
|
console.log('');
|
|
72
44
|
}
|
|
@@ -88,14 +60,12 @@ export async function releaseCommand(opts) {
|
|
|
88
60
|
const name = config.agents[lock.holder]?.name || lock.holder;
|
|
89
61
|
console.log('');
|
|
90
62
|
console.log(chalk.red(` Lock is held by ${chalk.bold(lock.holder)} (${name}), not human.`));
|
|
91
|
-
console.log(chalk.dim('
|
|
92
|
-
console.log(chalk.dim(' Use `agentxchain release --force` if you really want to break this lock.'));
|
|
63
|
+
console.log(chalk.dim(' Use `agentxchain release --force` to break this lock.'));
|
|
93
64
|
console.log('');
|
|
94
65
|
process.exit(1);
|
|
95
66
|
}
|
|
96
67
|
|
|
97
68
|
const who = lock.holder;
|
|
98
|
-
const priorLastReleasedBy = lock.last_released_by;
|
|
99
69
|
const lockPath = join(root, LOCK_FILE);
|
|
100
70
|
const newLock = {
|
|
101
71
|
holder: null,
|
|
@@ -107,53 +77,6 @@ export async function releaseCommand(opts) {
|
|
|
107
77
|
|
|
108
78
|
console.log('');
|
|
109
79
|
console.log(chalk.green(` ✓ Lock released by ${chalk.bold(who)} (turn ${newLock.turn_number})`));
|
|
110
|
-
|
|
111
|
-
// If releasing from human and Cursor session exists, wake the next agent
|
|
112
|
-
if (who === 'human') {
|
|
113
|
-
const apiKey = getCursorApiKey(root);
|
|
114
|
-
const session = loadSession(root);
|
|
115
|
-
const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
|
|
116
|
-
|
|
117
|
-
if (hasCursorSession && !apiKey) {
|
|
118
|
-
printCursorApiKeyRequired('`agentxchain release` with Cursor agents');
|
|
119
|
-
console.log(chalk.dim(' Lock released, but wake-up followup was skipped due to missing key.'));
|
|
120
|
-
console.log(chalk.dim(' Start `agentxchain watch` after setting the key.'));
|
|
121
|
-
console.log('');
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (session?.ide === 'cursor' && apiKey) {
|
|
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
|
-
}
|
|
132
|
-
const cloudAgent = session.launched.find(a => a.id === next);
|
|
133
|
-
|
|
134
|
-
if (cloudAgent) {
|
|
135
|
-
try {
|
|
136
|
-
const name = config.agents[next]?.name || next;
|
|
137
|
-
await sendFollowup(apiKey, cloudAgent.cloudId,
|
|
138
|
-
`Human released the lock. It's your turn. Read lock.json, claim it, and do your work as ${name}.`
|
|
139
|
-
);
|
|
140
|
-
console.log(chalk.cyan(` Woke ${chalk.bold(next)} via Cursor followup.`));
|
|
141
|
-
} catch (err) {
|
|
142
|
-
console.log(chalk.dim(` Could not wake ${next}: ${err.message}`));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
console.log(chalk.dim(' The watch process will coordinate from here.'));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
80
|
+
console.log(chalk.dim(' The Stop hook will coordinate the next agent turn in VS Code.'));
|
|
149
81
|
console.log('');
|
|
150
82
|
}
|
|
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
|
-
}
|
package/src/commands/init.js
CHANGED
|
@@ -206,15 +206,8 @@ export async function initCommand(opts) {
|
|
|
206
206
|
writeFileSync(join(dir, 'history.jsonl'), '');
|
|
207
207
|
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`);
|
|
208
208
|
writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
|
|
209
|
-
writeFileSync(join(dir, '.env.example'), 'CURSOR_API_KEY=\n');
|
|
210
|
-
if (!existsSync(join(dir, '.env'))) {
|
|
211
|
-
writeFileSync(
|
|
212
|
-
join(dir, '.env'),
|
|
213
|
-
'# Required for Cursor commands: start/watch/stop/claim/release\nCURSOR_API_KEY=\n'
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
209
|
const gitignorePath = join(dir, '.gitignore');
|
|
217
|
-
const requiredIgnores = ['.env', '.agentxchain-
|
|
210
|
+
const requiredIgnores = ['.env', '.agentxchain-trigger.json'];
|
|
218
211
|
if (!existsSync(gitignorePath)) {
|
|
219
212
|
writeFileSync(gitignorePath, requiredIgnores.join('\n') + '\n');
|
|
220
213
|
} else {
|
|
@@ -271,9 +264,8 @@ export async function initCommand(opts) {
|
|
|
271
264
|
console.log('');
|
|
272
265
|
console.log(` ${chalk.cyan('Next:')}`);
|
|
273
266
|
console.log(` ${chalk.bold(`cd ${folderName}`)}`);
|
|
274
|
-
console.log(` ${chalk.bold('
|
|
275
|
-
console.log(` ${chalk.
|
|
276
|
-
console.log(` ${chalk.bold('agentxchain
|
|
277
|
-
console.log(` ${chalk.bold('agentxchain release')} ${chalk.dim('# begin automation (initial lock is human)')}`);
|
|
267
|
+
console.log(` ${chalk.bold('code .')} ${chalk.dim('# open in VS Code / Cursor')}`);
|
|
268
|
+
console.log(` ${chalk.dim('Select an agent from the Chat dropdown (auto-discovered from .github/agents/)')}`);
|
|
269
|
+
console.log(` ${chalk.bold('agentxchain release')} ${chalk.dim('# release human lock to begin turns')}`);
|
|
278
270
|
console.log('');
|
|
279
271
|
}
|
package/src/commands/start.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig } from '../lib/config.js';
|
|
3
3
|
import { generateSeedPrompt } from '../lib/seed-prompt.js';
|
|
4
|
-
import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
|
|
5
4
|
|
|
6
5
|
export async function startCommand(opts) {
|
|
7
6
|
const result = loadConfig();
|
|
@@ -28,12 +27,11 @@ export async function startCommand(opts) {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
console.log('');
|
|
31
|
-
console.log(chalk.bold(`
|
|
32
|
-
console.log(chalk.dim(` Project: ${config.project}`));
|
|
30
|
+
console.log(chalk.bold(` ${agentCount} agents configured for ${config.project}`));
|
|
33
31
|
console.log('');
|
|
34
32
|
|
|
35
33
|
if (opts.dryRun) {
|
|
36
|
-
console.log(chalk.yellow(' DRY RUN — showing
|
|
34
|
+
console.log(chalk.yellow(' DRY RUN — showing agents:'));
|
|
37
35
|
console.log('');
|
|
38
36
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
39
37
|
if (opts.agent && opts.agent !== id) continue;
|
|
@@ -45,14 +43,23 @@ export async function startCommand(opts) {
|
|
|
45
43
|
}
|
|
46
44
|
|
|
47
45
|
switch (ide) {
|
|
48
|
-
case '
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
case 'vscode': {
|
|
47
|
+
console.log(chalk.green(' Agents are set up as VS Code custom agents.'));
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.dim(' Your agents in .github/agents/:'));
|
|
50
|
+
for (const [id, agent] of Object.entries(config.agents)) {
|
|
51
|
+
console.log(` ${chalk.cyan(id)}.agent.md — ${agent.name}`);
|
|
53
52
|
}
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log(` ${chalk.bold('How to use:')}`);
|
|
55
|
+
console.log(` 1. Open this project in VS Code / Cursor`);
|
|
56
|
+
console.log(` 2. Open Chat (${chalk.bold('Cmd+L')})`);
|
|
57
|
+
console.log(` 3. Select an agent from the Chat dropdown`);
|
|
58
|
+
console.log(` 4. Run ${chalk.bold('agentxchain release')} to release the human lock`);
|
|
59
|
+
console.log(` 5. Agents coordinate via hooks — Stop hook hands off automatically`);
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log(chalk.dim(' If agents don\'t appear, run: agentxchain generate'));
|
|
62
|
+
console.log('');
|
|
56
63
|
break;
|
|
57
64
|
}
|
|
58
65
|
case 'claude-code': {
|
|
@@ -60,31 +67,8 @@ export async function startCommand(opts) {
|
|
|
60
67
|
await launchClaudeCodeAgents(config, root, opts);
|
|
61
68
|
break;
|
|
62
69
|
}
|
|
63
|
-
case 'vscode': {
|
|
64
|
-
console.log(chalk.yellow(' VS Code adapter coming soon.'));
|
|
65
|
-
console.log(chalk.dim(' For now, use the seed prompts below in VS Code chat panels.'));
|
|
66
|
-
console.log('');
|
|
67
|
-
printPrompts(config, opts);
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
70
|
default:
|
|
71
|
-
console.log(chalk.red(` Unknown IDE: ${ide}. Supported:
|
|
71
|
+
console.log(chalk.red(` Unknown IDE: ${ide}. Supported: vscode, claude-code`));
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
function printPrompts(config, opts) {
|
|
77
|
-
const agents = opts.agent
|
|
78
|
-
? { [opts.agent]: config.agents[opts.agent] }
|
|
79
|
-
: config.agents;
|
|
80
|
-
|
|
81
|
-
for (const [id, agent] of Object.entries(agents)) {
|
|
82
|
-
const prompt = generateSeedPrompt(id, agent, config);
|
|
83
|
-
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
84
|
-
console.log(chalk.cyan(` Agent: ${chalk.bold(id)} (${agent.name})`));
|
|
85
|
-
console.log(chalk.dim(' ' + '─'.repeat(50)));
|
|
86
|
-
console.log('');
|
|
87
|
-
console.log(prompt);
|
|
88
|
-
console.log('');
|
|
89
|
-
}
|
|
90
|
-
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadConfig, loadLock, loadState } from '../lib/config.js';
|
|
3
|
-
import { getAgentStatus, loadSession } from '../adapters/cursor.js';
|
|
4
|
-
import { getCursorApiKey } from '../lib/cursor-api-key.js';
|
|
5
3
|
|
|
6
4
|
export async function statusCommand(opts) {
|
|
7
5
|
const result = loadConfig();
|
|
@@ -31,7 +29,6 @@ export async function statusCommand(opts) {
|
|
|
31
29
|
}
|
|
32
30
|
console.log('');
|
|
33
31
|
|
|
34
|
-
// Lock
|
|
35
32
|
if (lock) {
|
|
36
33
|
if (lock.holder === 'human') {
|
|
37
34
|
console.log(` ${chalk.dim('Lock:')} ${chalk.magenta('HUMAN')} — you hold the lock`);
|
|
@@ -51,45 +48,13 @@ export async function statusCommand(opts) {
|
|
|
51
48
|
}
|
|
52
49
|
console.log('');
|
|
53
50
|
|
|
54
|
-
// Cursor session info
|
|
55
|
-
const session = loadSession(root);
|
|
56
|
-
const apiKey = getCursorApiKey(root);
|
|
57
|
-
const hasCursor = session?.ide === 'cursor' && session?.launched?.length > 0;
|
|
58
|
-
|
|
59
|
-
if (hasCursor) {
|
|
60
|
-
console.log(` ${chalk.dim('Cursor:')} ${chalk.cyan('Active session')} (${session.launched.length} agents)`);
|
|
61
|
-
console.log(` ${chalk.dim('Started:')} ${session.started_at}`);
|
|
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
|
-
}
|
|
66
|
-
console.log('');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Agents
|
|
70
51
|
console.log(` ${chalk.dim('Agents:')} ${Object.keys(config.agents).length}`);
|
|
71
52
|
|
|
72
53
|
for (const [id, agent] of Object.entries(config.agents)) {
|
|
73
54
|
const isHolder = lock?.holder === id;
|
|
74
55
|
const marker = isHolder ? chalk.yellow('●') : chalk.dim('○');
|
|
75
56
|
const label = isHolder ? chalk.bold(id) : id;
|
|
76
|
-
|
|
77
|
-
let cursorStatus = '';
|
|
78
|
-
if (hasCursor && apiKey) {
|
|
79
|
-
const cloudAgent = session.launched.find(a => a.id === id);
|
|
80
|
-
if (cloudAgent) {
|
|
81
|
-
try {
|
|
82
|
-
const statusData = await getAgentStatus(apiKey, cloudAgent.cloudId);
|
|
83
|
-
if (statusData?.status) {
|
|
84
|
-
cursorStatus = ` ${formatCursorStatus(statusData.status)}`;
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
cursorStatus = chalk.dim(' [API error]');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
console.log(` ${marker} ${label} — ${agent.name}${cursorStatus}`);
|
|
57
|
+
console.log(` ${marker} ${label} — ${agent.name}`);
|
|
93
58
|
}
|
|
94
59
|
|
|
95
60
|
if (lock?.holder === 'human') {
|
|
@@ -104,17 +69,6 @@ function formatPhase(phase) {
|
|
|
104
69
|
return (colors[phase] || chalk.white)(phase);
|
|
105
70
|
}
|
|
106
71
|
|
|
107
|
-
function formatCursorStatus(status) {
|
|
108
|
-
const map = {
|
|
109
|
-
CREATING: chalk.dim('[creating]'),
|
|
110
|
-
RUNNING: chalk.cyan('[running]'),
|
|
111
|
-
FINISHED: chalk.green('[finished]'),
|
|
112
|
-
STOPPED: chalk.yellow('[stopped]'),
|
|
113
|
-
ERRORED: chalk.red('[errored]'),
|
|
114
|
-
};
|
|
115
|
-
return map[status] || chalk.dim(`[${status}]`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
72
|
function timeSince(iso) {
|
|
119
73
|
const ms = Date.now() - new Date(iso).getTime();
|
|
120
74
|
const sec = Math.floor(ms / 1000);
|
package/src/commands/stop.js
CHANGED
|
@@ -2,8 +2,6 @@ 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, loadSession } from '../adapters/cursor.js';
|
|
6
|
-
import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
|
|
7
5
|
|
|
8
6
|
const SESSION_FILE = '.agentxchain-session.json';
|
|
9
7
|
|
|
@@ -12,44 +10,28 @@ export async function stopCommand() {
|
|
|
12
10
|
if (!result) { console.log(chalk.red(' No agentxchain.json found.')); process.exit(1); }
|
|
13
11
|
|
|
14
12
|
const { root } = result;
|
|
15
|
-
const
|
|
13
|
+
const sessionPath = join(root, SESSION_FILE);
|
|
16
14
|
|
|
17
|
-
if (!
|
|
15
|
+
if (!existsSync(sessionPath)) {
|
|
18
16
|
console.log(chalk.yellow(' No active session found.'));
|
|
19
|
-
console.log(chalk.dim(' If agents are running,
|
|
17
|
+
console.log(chalk.dim(' If agents are running in VS Code / Cursor, close their chat sessions manually.'));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let session;
|
|
22
|
+
try {
|
|
23
|
+
session = JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
console.log(chalk.yellow(' Could not read session file.'));
|
|
20
26
|
return;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
console.log('');
|
|
24
|
-
console.log(chalk.bold(` Stopping ${session.launched
|
|
30
|
+
console.log(chalk.bold(` Stopping ${session.launched?.length || 0} agents (${session.ide || 'unknown'})`));
|
|
25
31
|
console.log('');
|
|
26
|
-
let allStopped = true;
|
|
27
32
|
|
|
28
|
-
if (session.ide === '
|
|
29
|
-
const
|
|
30
|
-
if (!apiKey) {
|
|
31
|
-
printCursorApiKeyRequired('`agentxchain stop` for Cursor agents');
|
|
32
|
-
console.log(chalk.dim(' Session file was kept so you can retry after setting the key.'));
|
|
33
|
-
console.log('');
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const agent of session.launched) {
|
|
38
|
-
try {
|
|
39
|
-
const deleted = await deleteAgent(apiKey, agent.cloudId);
|
|
40
|
-
if (deleted) {
|
|
41
|
-
console.log(chalk.green(` ✓ Deleted ${chalk.bold(agent.id)} (${agent.cloudId})`));
|
|
42
|
-
} else {
|
|
43
|
-
console.log(chalk.yellow(` ⚠ Could not delete ${agent.id} — may already be gone`));
|
|
44
|
-
allStopped = false;
|
|
45
|
-
}
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
48
|
-
allStopped = false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
} else if (session.ide === 'claude-code') {
|
|
52
|
-
for (const agent of session.launched) {
|
|
33
|
+
if (session.ide === 'claude-code') {
|
|
34
|
+
for (const agent of (session.launched || [])) {
|
|
53
35
|
if (agent.pid) {
|
|
54
36
|
try {
|
|
55
37
|
process.kill(agent.pid, 'SIGTERM');
|
|
@@ -59,22 +41,17 @@ export async function stopCommand() {
|
|
|
59
41
|
console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
|
|
60
42
|
} else {
|
|
61
43
|
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
62
|
-
allStopped = false;
|
|
63
44
|
}
|
|
64
45
|
}
|
|
65
46
|
}
|
|
66
47
|
}
|
|
48
|
+
} else {
|
|
49
|
+
console.log(chalk.dim(' For VS Code / Cursor agents, close the chat sessions manually.'));
|
|
67
50
|
}
|
|
68
51
|
|
|
52
|
+
unlinkSync(sessionPath);
|
|
69
53
|
console.log('');
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
}
|
|
54
|
+
console.log(chalk.dim(' Session file removed.'));
|
|
55
|
+
console.log(chalk.green(' Done.'));
|
|
79
56
|
console.log('');
|
|
80
57
|
}
|
package/src/commands/watch.js
CHANGED
|
@@ -5,8 +5,6 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
|
|
7
7
|
import { notifyHuman as sendNotification } from '../lib/notify.js';
|
|
8
|
-
import { sendFollowup, getAgentStatus, stopAgent, loadSession } from '../adapters/cursor.js';
|
|
9
|
-
import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
|
|
10
8
|
|
|
11
9
|
export async function watchCommand(opts) {
|
|
12
10
|
if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
|
|
@@ -24,17 +22,9 @@ export async function watchCommand(opts) {
|
|
|
24
22
|
const interval = config.rules?.watch_interval_ms || 5000;
|
|
25
23
|
const ttlMinutes = config.rules?.ttl_minutes || 10;
|
|
26
24
|
const agentIds = Object.keys(config.agents);
|
|
25
|
+
|
|
27
26
|
if (agentIds.length === 0) {
|
|
28
27
|
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
|
-
}
|
|
32
|
-
const apiKey = getCursorApiKey(root);
|
|
33
|
-
const session = loadSession(root);
|
|
34
|
-
const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
|
|
35
|
-
|
|
36
|
-
if (hasCursorSession && !apiKey) {
|
|
37
|
-
printCursorApiKeyRequired('`agentxchain watch` with a Cursor session');
|
|
38
28
|
process.exit(1);
|
|
39
29
|
}
|
|
40
30
|
|
|
@@ -43,13 +33,11 @@ export async function watchCommand(opts) {
|
|
|
43
33
|
console.log(chalk.dim(` Project: ${config.project}`));
|
|
44
34
|
console.log(chalk.dim(` Agents: ${agentIds.join(', ')}`));
|
|
45
35
|
console.log(chalk.dim(` Poll: ${interval}ms | TTL: ${ttlMinutes}min`));
|
|
46
|
-
|
|
47
|
-
console.log(chalk.cyan(` Mode: Cursor Cloud Agents (${session.launched.length} agents)`));
|
|
48
|
-
} else {
|
|
49
|
-
console.log(chalk.dim(` Mode: Local trigger file (no Cursor session found)`));
|
|
50
|
-
}
|
|
36
|
+
console.log(chalk.dim(` Mode: Local file watcher + trigger file`));
|
|
51
37
|
console.log('');
|
|
52
38
|
console.log(chalk.cyan(' Watching lock.json... (Ctrl+C to stop)'));
|
|
39
|
+
console.log(chalk.dim(' Note: In VS Code/Cursor, the Stop hook coordinates turns automatically.'));
|
|
40
|
+
console.log(chalk.dim(' This watch process is a fallback for non-IDE environments.'));
|
|
53
41
|
console.log('');
|
|
54
42
|
|
|
55
43
|
let lastState = null;
|
|
@@ -61,7 +49,6 @@ export async function watchCommand(opts) {
|
|
|
61
49
|
|
|
62
50
|
const stateKey = `${lock.holder}:${lock.turn_number}`;
|
|
63
51
|
|
|
64
|
-
// TTL check — stale lock
|
|
65
52
|
if (lock.holder && lock.holder !== 'human' && lock.claimed_at) {
|
|
66
53
|
const elapsed = Date.now() - new Date(lock.claimed_at).getTime();
|
|
67
54
|
const ttlMs = ttlMinutes * 60 * 1000;
|
|
@@ -70,22 +57,12 @@ export async function watchCommand(opts) {
|
|
|
70
57
|
const staleAgent = lock.holder;
|
|
71
58
|
const minutes = Math.round(elapsed / 60000);
|
|
72
59
|
log('ttl', `Lock held by ${staleAgent} for ${minutes}min. Force-releasing.`);
|
|
73
|
-
|
|
74
|
-
if (hasCursorSession && apiKey) {
|
|
75
|
-
const cloudAgent = session.launched.find(a => a.id === staleAgent);
|
|
76
|
-
if (cloudAgent) {
|
|
77
|
-
await stopAgent(apiKey, cloudAgent.cloudId);
|
|
78
|
-
log('ttl', `Stopped Cursor agent ${staleAgent}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
60
|
forceRelease(root, lock, staleAgent, config);
|
|
83
61
|
lastState = null;
|
|
84
62
|
return;
|
|
85
63
|
}
|
|
86
64
|
}
|
|
87
65
|
|
|
88
|
-
// Human holder
|
|
89
66
|
if (lock.holder === 'human') {
|
|
90
67
|
if (stateKey !== lastState) {
|
|
91
68
|
log('human', 'Lock held by HUMAN. Run `agentxchain release` when done.');
|
|
@@ -95,45 +72,20 @@ export async function watchCommand(opts) {
|
|
|
95
72
|
return;
|
|
96
73
|
}
|
|
97
74
|
|
|
98
|
-
// Agent is working
|
|
99
75
|
if (lock.holder) {
|
|
100
76
|
if (stateKey !== lastState) {
|
|
101
77
|
const name = config.agents[lock.holder]?.name || lock.holder;
|
|
102
78
|
log('claimed', `${lock.holder} (${name}) working... (turn ${lock.turn_number})`);
|
|
103
79
|
lastState = stateKey;
|
|
104
|
-
|
|
105
|
-
// Check Cursor agent status if available
|
|
106
|
-
if (hasCursorSession && apiKey) {
|
|
107
|
-
const cloudAgent = session.launched.find(a => a.id === lock.holder);
|
|
108
|
-
if (cloudAgent) {
|
|
109
|
-
const status = await getAgentStatus(apiKey, cloudAgent.cloudId);
|
|
110
|
-
if (status?.status === 'FINISHED') {
|
|
111
|
-
log('warn', `${lock.holder} Cursor agent is FINISHED but lock not released. May need TTL.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
80
|
}
|
|
116
81
|
return;
|
|
117
82
|
}
|
|
118
83
|
|
|
119
|
-
// Lock is FREE — wake the next agent
|
|
120
84
|
if (stateKey !== lastState) {
|
|
121
85
|
const next = pickNextAgent(lock, config);
|
|
122
|
-
log('free', `Lock free (released by ${lock.last_released_by || 'none'}).
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (hasCursorSession && apiKey) {
|
|
126
|
-
wakeSucceeded = await wakeCursorAgent(apiKey, session, next, lock, config, root);
|
|
127
|
-
} else {
|
|
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.`);
|
|
136
|
-
}
|
|
86
|
+
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Next: ${chalk.bold(next)}.`);
|
|
87
|
+
writeTrigger(root, next, lock, config);
|
|
88
|
+
lastState = stateKey;
|
|
137
89
|
}
|
|
138
90
|
} catch (err) {
|
|
139
91
|
log('error', err.message);
|
|
@@ -151,40 +103,6 @@ export async function watchCommand(opts) {
|
|
|
151
103
|
});
|
|
152
104
|
}
|
|
153
105
|
|
|
154
|
-
async function wakeCursorAgent(apiKey, session, agentId, lock, config, root) {
|
|
155
|
-
const cloudAgent = session.launched.find(a => a.id === agentId);
|
|
156
|
-
if (!cloudAgent) {
|
|
157
|
-
log('warn', `No Cursor cloud agent found for "${agentId}". Using trigger file.`);
|
|
158
|
-
writeTrigger(root, agentId, lock, config);
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const name = config.agents[agentId]?.name || agentId;
|
|
163
|
-
const wakeMessage = `The lock is free. It's your turn.
|
|
164
|
-
|
|
165
|
-
READ lock.json — if holder is null, CLAIM it by writing holder="${agentId}" and claimed_at=now.
|
|
166
|
-
Then do your work per your mandate:
|
|
167
|
-
- Name: ${name}
|
|
168
|
-
- Mandate: ${config.agents[agentId]?.mandate || '(see agentxchain.json)'}
|
|
169
|
-
|
|
170
|
-
When done:
|
|
171
|
-
1. Update state.md with current project state
|
|
172
|
-
2. Append one line to history.jsonl
|
|
173
|
-
${config.rules?.verify_command ? `3. Run verify: ${config.rules.verify_command} — fix if it fails` : ''}
|
|
174
|
-
${config.rules?.verify_command ? '4' : '3'}. RELEASE lock.json: holder=null, last_released_by="${agentId}", turn_number=${lock.turn_number + 1}, claimed_at=null
|
|
175
|
-
|
|
176
|
-
This must be the last thing you write. The watch process will wake the next agent.`;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
await sendFollowup(apiKey, cloudAgent.cloudId, wakeMessage);
|
|
180
|
-
log('wake', `Sent followup to ${chalk.bold(agentId)} (${cloudAgent.cloudId})`);
|
|
181
|
-
return true;
|
|
182
|
-
} catch (err) {
|
|
183
|
-
log('error', `Failed to wake ${agentId}: ${err.message}`);
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
106
|
function pickNextAgent(lock, config) {
|
|
189
107
|
const agentIds = Object.keys(config.agents);
|
|
190
108
|
if (agentIds.length === 0) return null;
|
|
@@ -229,7 +147,6 @@ function log(type, msg) {
|
|
|
229
147
|
const tags = {
|
|
230
148
|
free: chalk.green('FREE '),
|
|
231
149
|
claimed: chalk.yellow('WORK '),
|
|
232
|
-
wake: chalk.cyan('WAKE '),
|
|
233
150
|
ttl: chalk.red(' TTL '),
|
|
234
151
|
human: chalk.magenta('HUMAN'),
|
|
235
152
|
warn: chalk.yellow('WARN '),
|
package/src/adapters/cursor.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { generateSeedPrompt } from '../lib/seed-prompt.js';
|
|
5
|
-
import { getRepoUrl, getCurrentBranch } from '../lib/repo.js';
|
|
6
|
-
import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
|
|
7
|
-
|
|
8
|
-
const API_BASE = 'https://api.cursor.com/v0';
|
|
9
|
-
|
|
10
|
-
function authHeaders(apiKey) {
|
|
11
|
-
return {
|
|
12
|
-
'Authorization': `Basic ${Buffer.from(apiKey + ':').toString('base64')}`,
|
|
13
|
-
'Content-Type': 'application/json'
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// --- Public API ---
|
|
18
|
-
|
|
19
|
-
export async function launchCursorAgents(config, root, opts) {
|
|
20
|
-
const apiKey = getCursorApiKey(root);
|
|
21
|
-
|
|
22
|
-
if (!apiKey) {
|
|
23
|
-
printCursorApiKeyRequired('`agentxchain start --ide cursor`');
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const repoUrl = await getRepoUrl(root);
|
|
28
|
-
if (!repoUrl) {
|
|
29
|
-
console.log(chalk.red(' Could not detect GitHub repo URL.'));
|
|
30
|
-
console.log(chalk.dim(' Make sure this project is a git repo with a GitHub remote.'));
|
|
31
|
-
console.log(chalk.dim(' Or set source.repository manually in agentxchain.json.'));
|
|
32
|
-
return [];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const model = config.cursor?.model || 'default';
|
|
36
|
-
const ref = config.cursor?.ref || getCurrentBranch(root) || 'main';
|
|
37
|
-
console.log(chalk.dim(` Cursor source: ${repoUrl} @ ${ref}`));
|
|
38
|
-
const agents = filterAgents(config, opts.agent);
|
|
39
|
-
const launched = [];
|
|
40
|
-
let branchErrorCount = 0;
|
|
41
|
-
|
|
42
|
-
for (const [id, agent] of Object.entries(agents)) {
|
|
43
|
-
const prompt = generateSeedPrompt(id, agent, config);
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const body = {
|
|
47
|
-
prompt: { text: prompt },
|
|
48
|
-
source: { repository: repoUrl, ref },
|
|
49
|
-
target: { autoCreatePr: false }
|
|
50
|
-
};
|
|
51
|
-
if (model !== 'default') body.model = model;
|
|
52
|
-
|
|
53
|
-
const res = await fetch(`${API_BASE}/agents`, {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers: authHeaders(apiKey),
|
|
56
|
-
body: JSON.stringify(body)
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (!res.ok) {
|
|
60
|
-
const errBody = await res.text();
|
|
61
|
-
console.log(chalk.red(` ✗ ${id}: ${res.status} ${errBody}`));
|
|
62
|
-
if (errBody.includes('Failed to verify existence of branch')) {
|
|
63
|
-
branchErrorCount += 1;
|
|
64
|
-
}
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const data = await res.json();
|
|
69
|
-
launched.push({
|
|
70
|
-
id,
|
|
71
|
-
name: agent.name,
|
|
72
|
-
cloudId: data.id,
|
|
73
|
-
status: data.status || 'CREATING',
|
|
74
|
-
url: data.target?.url || null
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const urlStr = data.target?.url ? chalk.dim(` → ${data.target.url}`) : '';
|
|
78
|
-
console.log(chalk.green(` ✓ ${chalk.bold(id)} (${agent.name}) — ${data.id}${urlStr}`));
|
|
79
|
-
} catch (err) {
|
|
80
|
-
console.log(chalk.red(` ✗ ${id}: ${err.message}`));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (launched.length > 0) {
|
|
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(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).'));
|
|
96
|
-
console.log('');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return launched;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export async function sendFollowup(apiKey, cloudId, message) {
|
|
103
|
-
const res = await fetch(`${API_BASE}/agents/${cloudId}/followup`, {
|
|
104
|
-
method: 'POST',
|
|
105
|
-
headers: authHeaders(apiKey),
|
|
106
|
-
body: JSON.stringify({ prompt: { text: message } })
|
|
107
|
-
});
|
|
108
|
-
if (!res.ok) {
|
|
109
|
-
const body = await res.text();
|
|
110
|
-
throw new Error(`Followup failed (${res.status}): ${body}`);
|
|
111
|
-
}
|
|
112
|
-
return await res.json();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function getAgentStatus(apiKey, cloudId) {
|
|
116
|
-
const res = await fetch(`${API_BASE}/agents/${cloudId}`, {
|
|
117
|
-
method: 'GET',
|
|
118
|
-
headers: authHeaders(apiKey)
|
|
119
|
-
});
|
|
120
|
-
if (!res.ok) return null;
|
|
121
|
-
return await res.json();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function getAgentConversation(apiKey, cloudId) {
|
|
125
|
-
const res = await fetch(`${API_BASE}/agents/${cloudId}/conversation`, {
|
|
126
|
-
method: 'GET',
|
|
127
|
-
headers: authHeaders(apiKey)
|
|
128
|
-
});
|
|
129
|
-
if (!res.ok) return null;
|
|
130
|
-
return await res.json();
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export async function stopAgent(apiKey, cloudId) {
|
|
134
|
-
const res = await fetch(`${API_BASE}/agents/${cloudId}/stop`, {
|
|
135
|
-
method: 'POST',
|
|
136
|
-
headers: authHeaders(apiKey)
|
|
137
|
-
});
|
|
138
|
-
return res.ok;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function deleteAgent(apiKey, cloudId) {
|
|
142
|
-
const res = await fetch(`${API_BASE}/agents/${cloudId}`, {
|
|
143
|
-
method: 'DELETE',
|
|
144
|
-
headers: authHeaders(apiKey)
|
|
145
|
-
});
|
|
146
|
-
return res.ok;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export function loadSession(root) {
|
|
150
|
-
const sessionPath = join(root, '.agentxchain-session.json');
|
|
151
|
-
if (!existsSync(sessionPath)) return null;
|
|
152
|
-
return JSON.parse(readFileSync(sessionPath, 'utf8'));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// --- Internal ---
|
|
156
|
-
|
|
157
|
-
function saveSession(root, launched, repoUrl, ref) {
|
|
158
|
-
const session = {
|
|
159
|
-
launched,
|
|
160
|
-
started_at: new Date().toISOString(),
|
|
161
|
-
ide: 'cursor',
|
|
162
|
-
repo: repoUrl,
|
|
163
|
-
ref
|
|
164
|
-
};
|
|
165
|
-
const sessionPath = join(root, '.agentxchain-session.json');
|
|
166
|
-
writeFileSync(sessionPath, JSON.stringify(session, null, 2) + '\n');
|
|
167
|
-
console.log(chalk.dim(` Session saved to .agentxchain-session.json`));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function filterAgents(config, specificId) {
|
|
171
|
-
if (specificId) {
|
|
172
|
-
if (!config.agents[specificId]) {
|
|
173
|
-
console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
|
|
174
|
-
process.exit(1);
|
|
175
|
-
}
|
|
176
|
-
return { [specificId]: config.agents[specificId] };
|
|
177
|
-
}
|
|
178
|
-
return config.agents;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// No prompt fallback here by design.
|
|
182
|
-
// Cursor mode is strict: an API key is required.
|
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
}
|