agentxchain 0.8.6 → 0.8.8

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.
@@ -1,7 +1,10 @@
1
- import { writeFileSync, existsSync, readFileSync } from 'fs';
1
+ import { existsSync, readFileSync } 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 { safeWriteJson } from '../lib/safe-write.js';
6
+ import { resolveExpectedClaimer } from '../lib/next-owner.js';
7
+ import { runConfiguredVerify } from '../lib/verify-command.js';
5
8
 
6
9
  export async function claimCommand(opts) {
7
10
  const result = loadConfig();
@@ -39,7 +42,12 @@ export async function claimCommand(opts) {
39
42
  turn_number: lock.turn_number,
40
43
  claimed_at: new Date().toISOString()
41
44
  };
42
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
45
+ safeWriteJson(lockPath, newLock);
46
+ const verify = loadLock(root);
47
+ if (verify?.holder !== 'human') {
48
+ console.log(chalk.red(` Claim race: expected holder=human, got ${verify?.holder}. Another process won.`));
49
+ process.exit(1);
50
+ }
43
51
  clearBlockedState(root);
44
52
 
45
53
  console.log('');
@@ -75,6 +83,11 @@ export async function releaseCommand(opts) {
75
83
  }
76
84
 
77
85
  const who = lock.holder;
86
+ const verifyResult = runConfiguredVerify(config, root);
87
+ if (!verifyResult.ok) {
88
+ console.log(chalk.red(` Verification failed: ${verifyResult.command}`));
89
+ process.exit(1);
90
+ }
78
91
  const lockPath = join(root, LOCK_FILE);
79
92
  const newLock = {
80
93
  holder: null,
@@ -82,14 +95,19 @@ export async function releaseCommand(opts) {
82
95
  turn_number: who === 'human' ? lock.turn_number : lock.turn_number + 1,
83
96
  claimed_at: null
84
97
  };
85
- writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
98
+ safeWriteJson(lockPath, newLock);
99
+ const verify = loadLock(root);
100
+ if (verify?.holder !== null || verify?.last_released_by !== who || verify?.turn_number !== newLock.turn_number) {
101
+ console.log(chalk.red(' Release race: lock.json changed unexpectedly after release attempt.'));
102
+ process.exit(1);
103
+ }
86
104
  if (who === 'human') {
87
105
  clearBlockedState(root);
88
106
  }
89
107
 
90
108
  console.log('');
91
109
  console.log(chalk.green(` ✓ Lock released by ${chalk.bold(who)} (turn ${newLock.turn_number})`));
92
- console.log(chalk.dim(' The Stop hook will coordinate the next agent turn in VS Code.'));
110
+ console.log(chalk.dim(' Next turn will be coordinated by the VS Code Stop hook or watch/supervise.'));
93
111
  console.log('');
94
112
  }
95
113
 
@@ -106,12 +124,26 @@ function claimAsAgent({ opts, root, config, lock }) {
106
124
  process.exit(1);
107
125
  }
108
126
 
109
- const expected = pickNextAgent(lock, config);
127
+ const expected = pickNextAgent(root, lock, config);
128
+ if (!opts.force && config.rules?.strict_next_owner && (expected === null || expected === undefined)) {
129
+ console.log(chalk.red(' No next owner resolved. Add a valid `Next owner: <agent_id>` line to TALK.md, or set rules.strict_next_owner to false.'));
130
+ process.exit(1);
131
+ }
110
132
  if (!opts.force && expected && expected !== agentId) {
111
133
  console.log(chalk.red(` Out-of-turn claim blocked. Expected: ${expected}, got: ${agentId}.`));
112
134
  process.exit(1);
113
135
  }
114
136
 
137
+ const maxClaims = Number(config.rules?.max_consecutive_claims || 0);
138
+ if (!opts.force && maxClaims > 0 && lock.last_released_by === agentId) {
139
+ const consecutiveTurns = countRecentTurnsByAgent(root, config, agentId);
140
+ if (consecutiveTurns >= maxClaims) {
141
+ console.log(chalk.red(` Consecutive-claim limit reached for "${agentId}" (${consecutiveTurns}/${maxClaims}).`));
142
+ console.log(chalk.dim(' Hand off to another agent or use --force for recovery only.'));
143
+ process.exit(1);
144
+ }
145
+ }
146
+
115
147
  const lockPath = join(root, LOCK_FILE);
116
148
  const next = {
117
149
  holder: agentId,
@@ -119,7 +151,14 @@ function claimAsAgent({ opts, root, config, lock }) {
119
151
  turn_number: lock.turn_number,
120
152
  claimed_at: new Date().toISOString()
121
153
  };
122
- writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
154
+ safeWriteJson(lockPath, next);
155
+
156
+ const verify = loadLock(root);
157
+ if (verify?.holder !== agentId) {
158
+ console.log(chalk.red(` Claim race: expected holder=${agentId}, got ${verify?.holder}. Another process won.`));
159
+ process.exit(1);
160
+ }
161
+
123
162
  console.log(chalk.green(` ✓ Lock claimed by ${agentId} (turn ${next.turn_number})`));
124
163
  }
125
164
 
@@ -134,6 +173,12 @@ function releaseAsAgent({ opts, root, config, lock }) {
134
173
  process.exit(1);
135
174
  }
136
175
 
176
+ const verifyResult = runConfiguredVerify(config, root);
177
+ if (!verifyResult.ok) {
178
+ console.log(chalk.red(` Verification failed: ${verifyResult.command}`));
179
+ process.exit(1);
180
+ }
181
+
137
182
  const lockPath = join(root, LOCK_FILE);
138
183
  const next = {
139
184
  holder: null,
@@ -141,17 +186,21 @@ function releaseAsAgent({ opts, root, config, lock }) {
141
186
  turn_number: lock.turn_number + 1,
142
187
  claimed_at: null
143
188
  };
144
- writeFileSync(lockPath, JSON.stringify(next, null, 2) + '\n');
189
+ safeWriteJson(lockPath, next);
190
+ const verifyRelease = loadLock(root);
191
+ if (
192
+ verifyRelease?.holder !== null ||
193
+ verifyRelease?.last_released_by !== agentId ||
194
+ verifyRelease?.turn_number !== next.turn_number
195
+ ) {
196
+ console.log(chalk.red(' Release race: lock.json changed unexpectedly after release attempt.'));
197
+ process.exit(1);
198
+ }
145
199
  console.log(chalk.green(` ✓ Lock released by ${agentId} (turn ${next.turn_number})`));
146
200
  }
147
201
 
148
- function pickNextAgent(lock, config) {
149
- const agentIds = Object.keys(config.agents || {});
150
- if (agentIds.length === 0) return null;
151
- const lastAgent = lock.last_released_by;
152
- if (!lastAgent || !agentIds.includes(lastAgent)) return agentIds[0];
153
- const lastIndex = agentIds.indexOf(lastAgent);
154
- return agentIds[(lastIndex + 1) % agentIds.length];
202
+ function pickNextAgent(root, lock, config) {
203
+ return resolveExpectedClaimer(root, config, lock).next;
155
204
  }
156
205
 
157
206
  function clearBlockedState(root) {
@@ -161,7 +210,29 @@ function clearBlockedState(root) {
161
210
  const state = JSON.parse(readFileSync(statePath, 'utf8'));
162
211
  if (state.blocked || state.blocked_on) {
163
212
  const next = { ...state, blocked: false, blocked_on: null };
164
- writeFileSync(statePath, JSON.stringify(next, null, 2) + '\n');
213
+ safeWriteJson(statePath, next);
165
214
  }
166
215
  } catch {}
167
216
  }
217
+
218
+ function countRecentTurnsByAgent(root, config, agentId) {
219
+ const historyPath = join(root, config.history_file || 'history.jsonl');
220
+ if (!existsSync(historyPath)) return 0;
221
+
222
+ try {
223
+ const lines = readFileSync(historyPath, 'utf8')
224
+ .split(/\r?\n/)
225
+ .map(line => line.trim())
226
+ .filter(Boolean);
227
+
228
+ let count = 0;
229
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
230
+ const entry = JSON.parse(lines[i]);
231
+ if (entry?.agent !== agentId) break;
232
+ count += 1;
233
+ }
234
+ return count;
235
+ } catch {
236
+ return 0;
237
+ }
238
+ }
@@ -3,6 +3,7 @@ import { join } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import inquirer from 'inquirer';
5
5
  import { loadConfig, CONFIG_FILE } from '../lib/config.js';
6
+ import { validateConfigSchema } from '../lib/schema.js';
6
7
 
7
8
  export async function configCommand(opts) {
8
9
  const result = loadConfig();
@@ -127,6 +128,12 @@ function setSetting(config, configPath, keyValPair) {
127
128
  const key = parts[0];
128
129
  const rawVal = parts.slice(1).join(' ');
129
130
  const segments = key.split('.');
131
+ const forbiddenKeys = new Set(['__proto__', 'prototype', 'constructor']);
132
+
133
+ if (segments.some(segment => forbiddenKeys.has(segment))) {
134
+ console.log(chalk.red(' Refusing to write reserved object path.'));
135
+ process.exit(1);
136
+ }
130
137
 
131
138
  let target = config;
132
139
  for (let i = 0; i < segments.length - 1; i++) {
@@ -145,6 +152,15 @@ function setSetting(config, configPath, keyValPair) {
145
152
  else if (!isNaN(rawVal) && rawVal !== '') val = Number(rawVal);
146
153
 
147
154
  target[lastKey] = val;
155
+ const validation = validateConfigSchema(config);
156
+ if (!validation.ok) {
157
+ target[lastKey] = oldVal;
158
+ if (oldVal === undefined) {
159
+ delete target[lastKey];
160
+ }
161
+ console.log(chalk.red(` Refusing to save invalid config: ${validation.errors.join(', ')}`));
162
+ process.exit(1);
163
+ }
148
164
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
149
165
 
150
166
  console.log('');
@@ -4,6 +4,7 @@ import { join } from 'path';
4
4
  import chalk from 'chalk';
5
5
  import { loadConfig, loadLock } from '../lib/config.js';
6
6
  import { validateProject } from '../lib/validation.js';
7
+ import { getWatchPid } from './watch.js';
7
8
 
8
9
  export async function doctorCommand() {
9
10
  const result = loadConfig();
@@ -86,9 +87,16 @@ function checkPm(config) {
86
87
  }
87
88
 
88
89
  function checkWatchProcess() {
90
+ const result = loadConfig();
91
+ if (result) {
92
+ const pid = getWatchPid(result.root);
93
+ if (pid) {
94
+ return { name: 'watch process', level: 'pass', detail: `watch running (PID: ${pid})` };
95
+ }
96
+ }
89
97
  try {
90
98
  execSync('pgrep -f "agentxchain.*watch" >/dev/null', { stdio: 'ignore' });
91
- return { name: 'watch process', level: 'pass', detail: 'watch appears to be running' };
99
+ return { name: 'watch process', level: 'pass', detail: 'watch appears to be running (no PID file)' };
92
100
  } catch {
93
101
  return { name: 'watch process', level: 'warn', detail: 'watch not running (start with `agentxchain watch` or `agentxchain supervise --autonudge`)' };
94
102
  }
@@ -52,7 +52,12 @@ export async function initCommand(opts) {
52
52
  project = 'My AgentXchain project';
53
53
  agents = DEFAULT_AGENTS;
54
54
  folderName = slugify(project);
55
- rules = { max_consecutive_claims: 2, require_message: true, compress_after_words: 5000 };
55
+ rules = {
56
+ max_consecutive_claims: 2,
57
+ require_message: true,
58
+ compress_after_words: 5000,
59
+ strict_next_owner: false
60
+ };
56
61
  } else {
57
62
  const templates = loadTemplates();
58
63
 
@@ -86,7 +91,12 @@ export async function initCommand(opts) {
86
91
  project = projectName;
87
92
  } else if (template === 'default') {
88
93
  agents = DEFAULT_AGENTS;
89
- rules = { max_consecutive_claims: 2, require_message: true, compress_after_words: 5000 };
94
+ rules = {
95
+ max_consecutive_claims: 2,
96
+ require_message: true,
97
+ compress_after_words: 5000,
98
+ strict_next_owner: false
99
+ };
90
100
  const { projectName } = await inquirer.prompt([{
91
101
  type: 'input',
92
102
  name: 'projectName',
@@ -103,7 +113,12 @@ export async function initCommand(opts) {
103
113
  }]);
104
114
  project = projectName;
105
115
  agents = {};
106
- rules = { max_consecutive_claims: 2, require_message: true, compress_after_words: 5000 };
116
+ rules = {
117
+ max_consecutive_claims: 2,
118
+ require_message: true,
119
+ compress_after_words: 5000,
120
+ strict_next_owner: false
121
+ };
107
122
 
108
123
  const { count } = await inquirer.prompt([{
109
124
  type: 'number',
@@ -192,6 +207,7 @@ export async function initCommand(opts) {
192
207
  compress_after_words: rules.compress_after_words || 5000,
193
208
  ttl_minutes: rules.ttl_minutes || 10,
194
209
  watch_interval_ms: rules.watch_interval_ms || 5000,
210
+ strict_next_owner: rules.strict_next_owner === true,
195
211
  ...(rules.verify_command ? { verify_command: rules.verify_command } : {})
196
212
  }
197
213
  };
@@ -209,7 +225,7 @@ export async function initCommand(opts) {
209
225
  writeFileSync(join(dir, 'TALK.md'), `# ${project} — Team Talk File\n\nCanonical human-readable handoff log for all agents.\n\n## How to write entries\n\nUse this exact structure:\n\n## Turn N — <agent_id> (<role>)\n- Status:\n- Decision:\n- Action:\n- Risks/Questions:\n- Next owner:\n\n---\n\n`);
210
226
  writeFileSync(join(dir, 'HUMAN_TASKS.md'), '# Human Tasks\n\n(Agents append tasks here when they need human action.)\n');
211
227
  const gitignorePath = join(dir, '.gitignore');
212
- const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/'];
228
+ const requiredIgnores = ['.env', '.agentxchain-trigger.json', '.agentxchain-prompts/', '.agentxchain-workspaces/', '.agentxchain-watch.pid', '.agentxchain-autonudge.state'];
213
229
  if (!existsSync(gitignorePath)) {
214
230
  writeFileSync(gitignorePath, requiredIgnores.join('\n') + '\n');
215
231
  } else {
@@ -230,10 +246,13 @@ export async function initCommand(opts) {
230
246
 
231
247
  writeFileSync(join(dir, '.planning', 'REQUIREMENTS.md'), `# Requirements — ${project}\n\n## v1 (MVP)\n\n(PM fills this: numbered list of requirements. Each requirement has one-sentence acceptance criteria.)\n\n| # | Requirement | Acceptance criteria | Phase | Status |\n|---|-------------|-------------------|-------|--------|\n| 1 | | | | Pending |\n\n## v2 (Future)\n\n(Out of scope for MVP. Captured here so they don't creep in.)\n\n## Out of scope\n\n(Explicitly not building.)\n`);
232
248
 
233
- writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${project}\n\n## Phases\n\n| Phase | Description | Status | Requirements |\n|-------|-------------|--------|-------------|\n| 1 | Discovery + setup | In progress | — |\n\n(PM updates this as phases are planned and completed.)\n`);
249
+ writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${project}\n\n## Waves\n\n| Wave | Goal | Status |\n|------|------|--------|\n| Wave 1 | Discovery, planning, and phase setup | In progress |\n\n## Phases\n\n| Phase | Description | Status | Requirements |\n|-------|-------------|--------|-------------|\n| 1 | Discovery + setup | In progress | — |\n\n(PM updates this as phases are planned and completed.)\n`);
234
250
  writeFileSync(join(dir, '.planning', 'PM_SIGNOFF.md'), `# PM Signoff — ${project}\n\nApproved: NO\n\n## Discovery Checklist\n- [ ] Target user defined\n- [ ] Core pain point defined\n- [ ] Core workflow defined\n- [ ] MVP scope defined\n- [ ] Out-of-scope list defined\n- [ ] Success metric defined\n\n## Notes for team\n(PM and human add final kickoff notes here.)\n`);
235
251
 
236
252
  // QA structure
253
+ mkdirSync(join(dir, '.planning', 'phases', 'phase-1'), { recursive: true });
254
+ writeFileSync(join(dir, '.planning', 'phases', 'phase-1', 'PLAN.md'), `# Phase 1 Plan — ${project}\n\n## Goal\n\nAlign scope, requirements, and initial implementation plan.\n\n## Deliverables\n\n- PM signoff\n- Initial requirements and roadmap\n- First implementation slice\n`);
255
+ writeFileSync(join(dir, '.planning', 'phases', 'phase-1', 'TESTS.md'), `# Phase 1 Tests — ${project}\n\n## Planned checks\n\n- Kickoff validation passes\n- Requirements have acceptance criteria\n- First implementation slice has an executable verification path\n`);
237
256
  writeFileSync(join(dir, '.planning', 'qa', 'TEST-COVERAGE.md'), `# Test Coverage — ${project}\n\n## Coverage Map\n\n| Feature / Area | Unit tests | Integration tests | E2E tests | Manual QA | UX audit | Status |\n|---------------|-----------|------------------|----------|----------|---------|--------|\n| (QA fills this as testing progresses) | | | | | | |\n\n## Coverage gaps\n\n(Areas with no tests or insufficient coverage.)\n`);
238
257
 
239
258
  writeFileSync(join(dir, '.planning', 'qa', 'REGRESSION-LOG.md'), `# Regression Log — ${project}\n\nBugs that were found and fixed. Each entry has a regression test to prevent recurrence.\n\n| Bug ID | Description | Found turn | Fixed turn | Regression test | Status |\n|--------|-------------|-----------|-----------|----------------|--------|\n| (QA adds entries as bugs are found and fixed) | | | | | |\n`);
@@ -0,0 +1,77 @@
1
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
5
+ import { loadConfig } from '../lib/config.js';
6
+ import { generatePollingPrompt } from '../lib/seed-prompt-polling.js';
7
+
8
+ export async function rebindCommand(opts) {
9
+ const result = loadConfig();
10
+ if (!result) {
11
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
12
+ process.exit(1);
13
+ }
14
+
15
+ const { root, config } = result;
16
+ const agentEntries = Object.entries(config.agents || {});
17
+ if (agentEntries.length === 0) {
18
+ console.log(chalk.red('No agents configured in agentxchain.json.'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const selected = opts.agent
23
+ ? agentEntries.filter(([id]) => id === opts.agent)
24
+ : agentEntries;
25
+
26
+ if (opts.agent && selected.length === 0) {
27
+ console.log(chalk.red(`Agent "${opts.agent}" not found in agentxchain.json.`));
28
+ process.exit(1);
29
+ }
30
+
31
+ const promptDir = join(root, '.agentxchain-prompts');
32
+ const workspacesDir = join(root, '.agentxchain-workspaces');
33
+ mkdirSync(promptDir, { recursive: true });
34
+ mkdirSync(workspacesDir, { recursive: true });
35
+
36
+ for (const [id, def] of selected) {
37
+ const prompt = generatePollingPrompt(id, def, config, root);
38
+ writeFileSync(join(promptDir, `${id}.prompt.md`), prompt);
39
+
40
+ const wsPath = join(workspacesDir, `${id}.code-workspace`);
41
+ const workspaceJson = {
42
+ folders: [{ path: root }],
43
+ settings: { 'agentxchain.agentId': id }
44
+ };
45
+ writeFileSync(wsPath, JSON.stringify(workspaceJson, null, 2) + '\n');
46
+
47
+ if (opts.open) {
48
+ openCursorWindow(wsPath);
49
+ }
50
+ }
51
+
52
+ const statePath = join(root, '.agentxchain-autonudge.state');
53
+ if (existsSync(statePath)) {
54
+ rmSync(statePath, { force: true });
55
+ }
56
+
57
+ console.log('');
58
+ console.log(chalk.green(` ✓ Rebound ${selected.length} agent session(s).`));
59
+ console.log(chalk.dim(` Prompts: ${join('.agentxchain-prompts', opts.agent ? `${opts.agent}.prompt.md` : '')}`));
60
+ console.log(chalk.dim(` Workspaces: ${join('.agentxchain-workspaces', opts.agent ? `${opts.agent}.code-workspace` : '')}`));
61
+ console.log(chalk.dim(' Auto-nudge dispatch state reset.'));
62
+ if (!opts.open) {
63
+ console.log(chalk.dim(' Use `agentxchain rebind --open` to reopen agent windows now.'));
64
+ }
65
+ console.log('');
66
+ }
67
+
68
+ function openCursorWindow(targetPath) {
69
+ try {
70
+ if (process.platform === 'darwin') {
71
+ execSync(`open -na "Cursor" --args "${targetPath}"`, { stdio: 'ignore' });
72
+ return;
73
+ }
74
+ execSync(`cursor --new-window "${targetPath}"`, { stdio: 'ignore' });
75
+ } catch {}
76
+ }
77
+
@@ -2,8 +2,10 @@ 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 { getWatchPid } from './watch.js';
5
6
 
6
7
  const SESSION_FILE = '.agentxchain-session.json';
8
+ const WATCH_PID_FILE = '.agentxchain-watch.pid';
7
9
 
8
10
  export async function stopCommand() {
9
11
  const result = loadConfig();
@@ -11,47 +13,77 @@ export async function stopCommand() {
11
13
 
12
14
  const { root } = result;
13
15
  const sessionPath = join(root, SESSION_FILE);
16
+ const watchPidPath = join(root, WATCH_PID_FILE);
17
+ const watchPid = getWatchPid(root);
18
+ let didStopAnything = false;
14
19
 
15
- if (!existsSync(sessionPath)) {
16
- console.log(chalk.yellow(' No active session found.'));
17
- console.log(chalk.dim(' If agents are running in VS Code / Cursor, close their chat sessions manually.'));
18
- return;
20
+ if (watchPid) {
21
+ try {
22
+ process.kill(watchPid, 'SIGTERM');
23
+ didStopAnything = true;
24
+ console.log('');
25
+ console.log(chalk.green(` ✓ Stopped watch process (PID: ${watchPid})`));
26
+ console.log('');
27
+ } catch (err) {
28
+ if (err.code === 'ESRCH') {
29
+ if (existsSync(watchPidPath)) {
30
+ try { unlinkSync(watchPidPath); } catch {}
31
+ }
32
+ } else {
33
+ console.log(chalk.red(` ✗ Could not stop watch process (PID: ${watchPid}): ${err.message}`));
34
+ }
35
+ }
36
+ } else if (existsSync(watchPidPath)) {
37
+ // Stale PID file from an unexpected shutdown.
38
+ try {
39
+ unlinkSync(watchPidPath);
40
+ console.log(chalk.dim(' Removed stale watch PID file.'));
41
+ } catch {}
19
42
  }
20
43
 
21
- let session;
22
- try {
23
- session = JSON.parse(readFileSync(sessionPath, 'utf8'));
24
- } catch {
25
- console.log(chalk.yellow(' Could not read session file.'));
26
- return;
27
- }
44
+ if (existsSync(sessionPath)) {
45
+ let session;
46
+ try {
47
+ session = JSON.parse(readFileSync(sessionPath, 'utf8'));
48
+ } catch {
49
+ console.log(chalk.yellow(' Could not read session file.'));
50
+ return;
51
+ }
52
+
53
+ console.log('');
54
+ console.log(chalk.bold(` Stopping ${session.launched?.length || 0} agents (${session.ide || 'unknown'})`));
55
+ console.log('');
28
56
 
29
- console.log('');
30
- console.log(chalk.bold(` Stopping ${session.launched?.length || 0} agents (${session.ide || 'unknown'})`));
31
- console.log('');
32
-
33
- if (session.ide === 'claude-code') {
34
- for (const agent of (session.launched || [])) {
35
- if (agent.pid) {
36
- try {
37
- process.kill(agent.pid, 'SIGTERM');
38
- console.log(chalk.green(` ✓ Sent SIGTERM to ${agent.id} (PID: ${agent.pid})`));
39
- } catch (err) {
40
- if (err.code === 'ESRCH') {
41
- console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
42
- } else {
43
- console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
57
+ if (session.ide === 'claude-code') {
58
+ for (const agent of (session.launched || [])) {
59
+ if (agent.pid) {
60
+ try {
61
+ process.kill(agent.pid, 'SIGTERM');
62
+ didStopAnything = true;
63
+ console.log(chalk.green(` ✓ Sent SIGTERM to ${agent.id} (PID: ${agent.pid})`));
64
+ } catch (err) {
65
+ if (err.code === 'ESRCH') {
66
+ console.log(chalk.dim(` ${agent.id} (PID: ${agent.pid}) — already stopped`));
67
+ } else {
68
+ console.log(chalk.red(` ✗ ${agent.id}: ${err.message}`));
69
+ }
44
70
  }
45
71
  }
46
72
  }
73
+ } else {
74
+ console.log(chalk.dim(' For VS Code / Cursor agents, close the chat sessions manually.'));
47
75
  }
48
- } else {
49
- console.log(chalk.dim(' For VS Code / Cursor agents, close the chat sessions manually.'));
76
+
77
+ unlinkSync(sessionPath);
78
+ console.log('');
79
+ console.log(chalk.dim(' Session file removed.'));
80
+ console.log(chalk.green(' Done.'));
81
+ console.log('');
82
+ return;
50
83
  }
51
84
 
52
- unlinkSync(sessionPath);
53
- console.log('');
54
- console.log(chalk.dim(' Session file removed.'));
55
- console.log(chalk.green(' Done.'));
56
- console.log('');
85
+ if (!didStopAnything) {
86
+ console.log(chalk.yellow(' No active session found.'));
87
+ console.log(chalk.dim(' If agents are running in VS Code / Cursor, close their chat sessions manually.'));
88
+ }
57
89
  }
@@ -4,6 +4,19 @@ import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
5
  import chalk from 'chalk';
6
6
 
7
+ function printGlobalInstallFallbacks() {
8
+ console.log(chalk.dim(' If global install failed with permission errors (EACCES):'));
9
+ console.log(` ${chalk.bold('sudo npm install -g agentxchain@latest')} ${chalk.dim('(macOS/Linux)')}`);
10
+ console.log(` ${chalk.bold('npm config get prefix')} ${chalk.dim('— fix ownership of that directory, or use a user prefix:')}`);
11
+ console.log(` ${chalk.bold('mkdir -p ~/.npm-global && npm config set prefix ~/.npm-global')}`);
12
+ console.log(` ${chalk.dim('(add ~/.npm-global/bin to PATH)')}`);
13
+ console.log(chalk.dim(' Or run without global install:'));
14
+ console.log(` ${chalk.bold('npx agentxchain@latest <command>')}`);
15
+ console.log('');
16
+ console.log(chalk.dim(' Node: use 18.17+ or 20.5+ to avoid engine warnings from dependencies.'));
17
+ console.log('');
18
+ }
19
+
7
20
  export async function updateCommand() {
8
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
22
  const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
@@ -24,18 +37,26 @@ export async function updateCommand() {
24
37
 
25
38
  console.log(` ${chalk.dim('Latest version:')} ${chalk.cyan(latest)}`);
26
39
  console.log('');
27
- console.log(` Updating...`);
40
+ console.log(' Updating...');
28
41
 
29
- execSync('npm install -g agentxchain@latest', { stdio: 'inherit' });
42
+ try {
43
+ execSync('npm install -g agentxchain@latest', { stdio: 'inherit' });
44
+ } catch {
45
+ console.log('');
46
+ console.log(chalk.yellow(' Global install failed. Common fixes:'));
47
+ printGlobalInstallFallbacks();
48
+ return;
49
+ }
30
50
 
31
51
  console.log('');
32
52
  console.log(chalk.green(` ✓ Updated to ${latest}`));
33
53
  console.log('');
34
54
  } catch (err) {
35
55
  console.log('');
36
- console.log(chalk.yellow(' Could not auto-update. Run manually:'));
56
+ console.log(chalk.yellow(' Could not check or install the latest version.'));
37
57
  console.log(` ${chalk.bold('npm install -g agentxchain@latest')}`);
38
58
  console.log('');
59
+ printGlobalInstallFallbacks();
39
60
  console.log(chalk.dim(` Error: ${err.message}`));
40
61
  console.log('');
41
62
  }