agentxchain 0.4.0 → 0.4.2
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 +37 -2
- package/bin/agentxchain.js +10 -1
- package/package.json +3 -2
- package/src/adapters/cursor.js +26 -34
- package/src/commands/branch.js +98 -0
- package/src/commands/claim.js +48 -6
- package/src/commands/config.js +5 -0
- package/src/commands/init.js +21 -1
- package/src/commands/start.js +20 -2
- package/src/commands/status.js +5 -1
- package/src/commands/stop.js +31 -21
- package/src/commands/watch.js +53 -6
- package/src/lib/cursor-api-key.js +61 -0
- package/src/lib/repo.js +30 -11
package/README.md
CHANGED
|
@@ -19,35 +19,70 @@ npx agentxchain init
|
|
|
19
19
|
```bash
|
|
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
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
|
+
|
|
55
|
+
### Additional command flags
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
agentxchain watch --daemon # run watch in background
|
|
59
|
+
agentxchain release --force # force-release non-human holder lock
|
|
60
|
+
```
|
|
61
|
+
|
|
41
62
|
## Key features
|
|
42
63
|
|
|
43
64
|
- **Claim-based coordination** — no fixed turn order; agents self-organize
|
|
44
65
|
- **User-defined teams** — any number of agents, any roles
|
|
45
66
|
- **Cursor Cloud Agents** — launch and manage agents via API
|
|
67
|
+
- **Branch-safe launching** — defaults to active git branch; optional `branch` override
|
|
68
|
+
- **Project `.env` loading** — CLI auto-reads `CURSOR_API_KEY` from project root `.env`
|
|
46
69
|
- **Lock TTL** — stale locks auto-released after timeout
|
|
47
70
|
- **Verify command** — agents must pass tests before releasing
|
|
48
71
|
- **Human-in-the-loop** — claim/release to intervene anytime
|
|
49
72
|
- **Team templates** — SaaS MVP, Landing Page, Bug Squad, API Builder, Refactor Team
|
|
50
73
|
|
|
74
|
+
## Publish updates (maintainers)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cd cli
|
|
78
|
+
bash scripts/publish-npm.sh # patch bump + publish
|
|
79
|
+
bash scripts/publish-npm.sh minor # minor bump + publish
|
|
80
|
+
bash scripts/publish-npm.sh 0.5.0 # explicit version + publish
|
|
81
|
+
bash scripts/publish-npm.sh patch --dry-run
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If `NPM_TOKEN` exists in `cli/.env`, the script uses it automatically.
|
|
85
|
+
|
|
51
86
|
## Links
|
|
52
87
|
|
|
53
88
|
- [agentxchain.dev](https://agentxchain.dev)
|
package/bin/agentxchain.js
CHANGED
|
@@ -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.
|
|
19
|
+
.version('0.4.1');
|
|
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)')
|
|
@@ -66,6 +74,7 @@ program
|
|
|
66
74
|
program
|
|
67
75
|
.command('release')
|
|
68
76
|
.description('Release the lock (hand back to agents)')
|
|
77
|
+
.option('--force', 'Force release even if a non-human holder has the lock')
|
|
69
78
|
.action(releaseCommand);
|
|
70
79
|
|
|
71
80
|
program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
package/src/adapters/cursor.js
CHANGED
|
@@ -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 =
|
|
20
|
+
const apiKey = getCursorApiKey(root);
|
|
20
21
|
|
|
21
22
|
if (!apiKey) {
|
|
22
|
-
|
|
23
|
-
return
|
|
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
|
-
|
|
164
|
-
|
|
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
|
+
}
|
package/src/commands/claim.js
CHANGED
|
@@ -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 =
|
|
16
|
+
const apiKey = getCursorApiKey(root);
|
|
16
17
|
const session = loadSession(root);
|
|
17
|
-
const
|
|
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('');
|
|
@@ -62,7 +71,7 @@ export async function claimCommand(opts) {
|
|
|
62
71
|
console.log('');
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
export async function releaseCommand() {
|
|
74
|
+
export async function releaseCommand(opts) {
|
|
66
75
|
const result = loadConfig();
|
|
67
76
|
if (!result) { console.log(chalk.red(' No agentxchain.json found.')); process.exit(1); }
|
|
68
77
|
|
|
@@ -75,7 +84,18 @@ export async function releaseCommand() {
|
|
|
75
84
|
return;
|
|
76
85
|
}
|
|
77
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
|
+
|
|
78
97
|
const who = lock.holder;
|
|
98
|
+
const priorLastReleasedBy = lock.last_released_by;
|
|
79
99
|
const lockPath = join(root, LOCK_FILE);
|
|
80
100
|
const newLock = {
|
|
81
101
|
holder: null,
|
|
@@ -90,12 +110,25 @@ export async function releaseCommand() {
|
|
|
90
110
|
|
|
91
111
|
// If releasing from human and Cursor session exists, wake the next agent
|
|
92
112
|
if (who === 'human') {
|
|
93
|
-
const apiKey =
|
|
113
|
+
const apiKey = getCursorApiKey(root);
|
|
94
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
|
+
}
|
|
95
124
|
|
|
96
125
|
if (session?.ide === 'cursor' && apiKey) {
|
|
97
|
-
const
|
|
98
|
-
|
|
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
|
+
}
|
|
99
132
|
const cloudAgent = session.launched.find(a => a.id === next);
|
|
100
133
|
|
|
101
134
|
if (cloudAgent) {
|
|
@@ -115,3 +148,12 @@ export async function releaseCommand() {
|
|
|
115
148
|
|
|
116
149
|
console.log('');
|
|
117
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
|
+
}
|
package/src/commands/config.js
CHANGED
|
@@ -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
|
|
package/src/commands/init.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/start.js
CHANGED
|
@@ -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();
|
|
@@ -11,9 +11,22 @@ export async function startCommand(opts) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
const { root, config } = result;
|
|
14
|
-
const
|
|
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}`));
|
|
@@ -33,6 +46,11 @@ export async function startCommand(opts) {
|
|
|
33
46
|
|
|
34
47
|
switch (ide) {
|
|
35
48
|
case 'cursor': {
|
|
49
|
+
const apiKey = getCursorApiKey(root);
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
printCursorApiKeyRequired('`agentxchain start --ide cursor`');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
36
54
|
const { launchCursorAgents } = await import('../adapters/cursor.js');
|
|
37
55
|
await launchCursorAgents(config, root, opts);
|
|
38
56
|
break;
|
package/src/commands/status.js
CHANGED
|
@@ -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 =
|
|
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
|
|
package/src/commands/stop.js
CHANGED
|
@@ -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,
|
|
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
|
|
|
@@ -22,24 +23,29 @@ export async function stopCommand() {
|
|
|
22
23
|
console.log('');
|
|
23
24
|
console.log(chalk.bold(` Stopping ${session.launched.length} agents (${session.ide})`));
|
|
24
25
|
console.log('');
|
|
26
|
+
let allStopped = true;
|
|
25
27
|
|
|
26
28
|
if (session.ide === 'cursor') {
|
|
27
|
-
const apiKey =
|
|
29
|
+
const apiKey = getCursorApiKey(root);
|
|
28
30
|
if (!apiKey) {
|
|
29
|
-
|
|
30
|
-
console.log(chalk.dim('
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
console.log(chalk.
|
|
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;
|
|
42
45
|
}
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
48
|
+
allStopped = false;
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
} else if (session.ide === 'claude-code') {
|
|
@@ -53,18 +59,22 @@ export async function stopCommand() {
|
|
|
53
59
|
console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
|
|
54
60
|
} else {
|
|
55
61
|
console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
|
|
62
|
+
allStopped = false;
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
}
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
// Remove session file
|
|
63
|
-
const sessionPath = join(root, SESSION_FILE);
|
|
64
|
-
if (existsSync(sessionPath)) unlinkSync(sessionPath);
|
|
65
|
-
|
|
66
69
|
console.log('');
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
79
|
console.log('');
|
|
70
80
|
}
|
package/src/commands/watch.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
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';
|
|
6
8
|
import { sendFollowup, getAgentStatus, stopAgent, loadSession } from '../adapters/cursor.js';
|
|
9
|
+
import { getCursorApiKey, printCursorApiKeyRequired } from '../lib/cursor-api-key.js';
|
|
7
10
|
|
|
8
11
|
export async function watchCommand(opts) {
|
|
12
|
+
if (opts.daemon && process.env.AGENTXCHAIN_WATCH_DAEMON !== '1') {
|
|
13
|
+
startWatchDaemon();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
const result = loadConfig();
|
|
10
18
|
if (!result) {
|
|
11
19
|
console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
|
|
@@ -16,10 +24,20 @@ export async function watchCommand(opts) {
|
|
|
16
24
|
const interval = config.rules?.watch_interval_ms || 5000;
|
|
17
25
|
const ttlMinutes = config.rules?.ttl_minutes || 10;
|
|
18
26
|
const agentIds = Object.keys(config.agents);
|
|
19
|
-
|
|
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
|
+
}
|
|
32
|
+
const apiKey = getCursorApiKey(root);
|
|
20
33
|
const session = loadSession(root);
|
|
21
34
|
const hasCursorSession = session?.ide === 'cursor' && session?.launched?.length > 0;
|
|
22
35
|
|
|
36
|
+
if (hasCursorSession && !apiKey) {
|
|
37
|
+
printCursorApiKeyRequired('`agentxchain watch` with a Cursor session');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
console.log('');
|
|
24
42
|
console.log(chalk.bold(' AgentXchain Watch'));
|
|
25
43
|
console.log(chalk.dim(` Project: ${config.project}`));
|
|
@@ -102,12 +120,19 @@ export async function watchCommand(opts) {
|
|
|
102
120
|
if (stateKey !== lastState) {
|
|
103
121
|
const next = pickNextAgent(lock, config);
|
|
104
122
|
log('free', `Lock free (released by ${lock.last_released_by || 'none'}). Waking ${chalk.bold(next)}.`);
|
|
105
|
-
|
|
123
|
+
let wakeSucceeded = false;
|
|
106
124
|
|
|
107
125
|
if (hasCursorSession && apiKey) {
|
|
108
|
-
await wakeCursorAgent(apiKey, session, next, lock, config);
|
|
126
|
+
wakeSucceeded = await wakeCursorAgent(apiKey, session, next, lock, config, root);
|
|
109
127
|
} else {
|
|
110
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.`);
|
|
111
136
|
}
|
|
112
137
|
}
|
|
113
138
|
} catch (err) {
|
|
@@ -126,11 +151,12 @@ export async function watchCommand(opts) {
|
|
|
126
151
|
});
|
|
127
152
|
}
|
|
128
153
|
|
|
129
|
-
async function wakeCursorAgent(apiKey, session, agentId, lock, config) {
|
|
154
|
+
async function wakeCursorAgent(apiKey, session, agentId, lock, config, root) {
|
|
130
155
|
const cloudAgent = session.launched.find(a => a.id === agentId);
|
|
131
156
|
if (!cloudAgent) {
|
|
132
157
|
log('warn', `No Cursor cloud agent found for "${agentId}". Using trigger file.`);
|
|
133
|
-
|
|
158
|
+
writeTrigger(root, agentId, lock, config);
|
|
159
|
+
return true;
|
|
134
160
|
}
|
|
135
161
|
|
|
136
162
|
const name = config.agents[agentId]?.name || agentId;
|
|
@@ -152,13 +178,16 @@ This must be the last thing you write. The watch process will wake the next agen
|
|
|
152
178
|
try {
|
|
153
179
|
await sendFollowup(apiKey, cloudAgent.cloudId, wakeMessage);
|
|
154
180
|
log('wake', `Sent followup to ${chalk.bold(agentId)} (${cloudAgent.cloudId})`);
|
|
181
|
+
return true;
|
|
155
182
|
} catch (err) {
|
|
156
183
|
log('error', `Failed to wake ${agentId}: ${err.message}`);
|
|
184
|
+
return false;
|
|
157
185
|
}
|
|
158
186
|
}
|
|
159
187
|
|
|
160
188
|
function pickNextAgent(lock, config) {
|
|
161
189
|
const agentIds = Object.keys(config.agents);
|
|
190
|
+
if (agentIds.length === 0) return null;
|
|
162
191
|
const lastAgent = lock.last_released_by;
|
|
163
192
|
|
|
164
193
|
if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
|
|
@@ -185,6 +214,7 @@ function forceRelease(root, lock, staleAgent, config) {
|
|
|
185
214
|
}
|
|
186
215
|
|
|
187
216
|
function writeTrigger(root, agentId, lock, config) {
|
|
217
|
+
if (!agentId) return;
|
|
188
218
|
const triggerPath = join(root, '.agentxchain-trigger.json');
|
|
189
219
|
writeFileSync(triggerPath, JSON.stringify({
|
|
190
220
|
agent: agentId,
|
|
@@ -208,3 +238,20 @@ function log(type, msg) {
|
|
|
208
238
|
};
|
|
209
239
|
console.log(` ${chalk.dim(time)} ${tags[type] || chalk.dim(type)} ${msg}`);
|
|
210
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
|
+
}
|
|
@@ -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
|
@@ -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', {
|
|
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
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
//
|
|
20
|
-
if (
|
|
21
|
-
|
|
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
|
|
28
|
+
return credentialStripped;
|
|
26
29
|
} catch {
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
@@ -30,8 +33,24 @@ export async function getRepoUrl(root) {
|
|
|
30
33
|
|
|
31
34
|
export function getCurrentBranch(root) {
|
|
32
35
|
try {
|
|
33
|
-
|
|
36
|
+
const current = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
37
|
+
cwd: root,
|
|
38
|
+
encoding: 'utf8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
40
|
+
}).trim();
|
|
41
|
+
if (current && current !== 'HEAD') return current;
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
44
|
+
try {
|
|
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();
|
|
50
|
+
if (remoteHead.includes('/')) return remoteHead.split('/').pop();
|
|
34
51
|
} catch {
|
|
35
52
|
return 'main';
|
|
36
53
|
}
|
|
54
|
+
|
|
55
|
+
return 'main';
|
|
37
56
|
}
|