agentxchain 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,6 +58,33 @@ For Cursor Cloud Agents, set `CURSOR_API_KEY` in your environment. Without it, t
58
58
 
59
59
  Stop all running agent sessions. Reads `.agentxchain-session.json` to find active agents.
60
60
 
61
+ ### `agentxchain config`
62
+
63
+ View or edit project configuration.
64
+
65
+ - `--add-agent` — interactively add a new agent
66
+ - `--remove-agent <id>` — remove an agent by ID
67
+ - `--set "<key> <value>"` — update a setting (e.g. `--set "rules.max_consecutive_claims 3"`)
68
+ - `-j, --json` — output config as JSON
69
+
70
+ Examples:
71
+
72
+ ```bash
73
+ agentxchain config # show current config
74
+ agentxchain config --add-agent # add a new agent
75
+ agentxchain config --remove-agent ux # remove the ux agent
76
+ agentxchain config --set "project My New Name" # change project name
77
+ agentxchain config --set "rules.compress_after_words 8000"
78
+ ```
79
+
80
+ ### `agentxchain update`
81
+
82
+ Update the CLI to the latest version from npm.
83
+
84
+ ```bash
85
+ agentxchain update
86
+ ```
87
+
61
88
  ## How it works
62
89
 
63
90
  AgentXchain uses a **claim-based protocol**:
@@ -1,28 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
- import chalk from 'chalk';
5
4
  import { initCommand } from '../src/commands/init.js';
6
5
  import { statusCommand } from '../src/commands/status.js';
7
6
  import { startCommand } from '../src/commands/start.js';
8
7
  import { stopCommand } from '../src/commands/stop.js';
8
+ import { configCommand } from '../src/commands/config.js';
9
+ import { updateCommand } from '../src/commands/update.js';
10
+ import { watchCommand } from '../src/commands/watch.js';
11
+ import { claimCommand, releaseCommand } from '../src/commands/claim.js';
9
12
 
10
13
  const program = new Command();
11
14
 
12
15
  program
13
16
  .name('agentxchain')
14
17
  .description('Multi-agent coordination in your IDE')
15
- .version('0.1.0');
18
+ .version('0.1.1');
16
19
 
17
20
  program
18
21
  .command('init')
19
- .description('Initialize a new AgentXchain project')
22
+ .description('Create a new AgentXchain project folder')
20
23
  .option('-y, --yes', 'Skip prompts, use defaults')
21
24
  .action(initCommand);
22
25
 
23
26
  program
24
27
  .command('status')
25
- .description('Show current lock status, phase, and agents')
28
+ .description('Show lock status, phase, and agents')
26
29
  .option('-j, --json', 'Output as JSON')
27
30
  .action(statusCommand);
28
31
 
@@ -39,4 +42,35 @@ program
39
42
  .description('Stop all running agent sessions')
40
43
  .action(stopCommand);
41
44
 
45
+ program
46
+ .command('config')
47
+ .description('View or edit project configuration')
48
+ .option('--add-agent', 'Add a new agent interactively')
49
+ .option('--remove-agent <id>', 'Remove an agent by ID')
50
+ .option('--set <key_value>', 'Set a config value (e.g. --set "rules.max_consecutive_claims 3")')
51
+ .option('-j, --json', 'Output config as JSON')
52
+ .action(configCommand);
53
+
54
+ program
55
+ .command('watch')
56
+ .description('Watch lock.json and coordinate agent turns (the referee)')
57
+ .option('--daemon', 'Run in background mode')
58
+ .action(watchCommand);
59
+
60
+ program
61
+ .command('claim')
62
+ .description('Claim the lock as a human (take control)')
63
+ .option('--force', 'Force-claim even if an agent holds the lock')
64
+ .action(claimCommand);
65
+
66
+ program
67
+ .command('release')
68
+ .description('Release the lock (hand back to agents)')
69
+ .action(releaseCommand);
70
+
71
+ program
72
+ .command('update')
73
+ .description('Update agentxchain CLI to the latest version')
74
+ .action(updateCommand);
75
+
42
76
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "CLI for AgentXchain — multi-agent coordination in your IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,26 +1,38 @@
1
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
1
3
  import chalk from 'chalk';
2
4
  import { generateSeedPrompt } from '../lib/seed-prompt.js';
5
+ import { getRepoUrl } from '../lib/repo.js';
3
6
 
4
7
  const API_BASE = 'https://api.cursor.com/v0';
5
8
 
9
+ function authHeaders(apiKey) {
10
+ return {
11
+ 'Authorization': `Basic ${Buffer.from(apiKey + ':').toString('base64')}`,
12
+ 'Content-Type': 'application/json'
13
+ };
14
+ }
15
+
16
+ // --- Public API ---
17
+
6
18
  export async function launchCursorAgents(config, root, opts) {
7
19
  const apiKey = process.env.CURSOR_API_KEY;
8
20
 
9
21
  if (!apiKey) {
10
- console.log('');
11
- console.log(chalk.yellow(' Cursor Cloud Agents API key not found.'));
12
- console.log('');
13
- console.log(' To launch agents via Cursor Cloud API:');
14
- console.log(` 1. Go to ${chalk.cyan('cursor.com/dashboard')} → Cloud Agents`);
15
- console.log(' 2. Create an API key');
16
- console.log(` 3. Set: ${chalk.bold('export CURSOR_API_KEY=your_key')}`);
17
- console.log(` 4. Run: ${chalk.bold('agentxchain start --ide cursor')}`);
18
- console.log('');
19
- console.log(chalk.dim(' Falling back to seed prompt output...'));
20
- console.log('');
22
+ printApiKeyHelp();
21
23
  return fallbackPromptOutput(config, opts);
22
24
  }
23
25
 
26
+ const repoUrl = await getRepoUrl(root);
27
+ if (!repoUrl) {
28
+ console.log(chalk.red(' Could not detect GitHub repo URL.'));
29
+ console.log(chalk.dim(' Make sure this project is a git repo with a GitHub remote.'));
30
+ console.log(chalk.dim(' Or set source.repository manually in agentxchain.json.'));
31
+ return [];
32
+ }
33
+
34
+ const model = config.cursor?.model || 'default';
35
+ const ref = config.cursor?.ref || 'main';
24
36
  const agents = filterAgents(config, opts.agent);
25
37
  const launched = [];
26
38
 
@@ -28,51 +40,130 @@ export async function launchCursorAgents(config, root, opts) {
28
40
  const prompt = generateSeedPrompt(id, agent, config);
29
41
 
30
42
  try {
43
+ const body = {
44
+ prompt: { text: prompt },
45
+ source: { repository: repoUrl, ref },
46
+ target: { autoCreatePr: false }
47
+ };
48
+ if (model !== 'default') body.model = model;
49
+
31
50
  const res = await fetch(`${API_BASE}/agents`, {
32
51
  method: 'POST',
33
- headers: {
34
- 'Authorization': `Basic ${btoa(apiKey + ':')}`,
35
- 'Content-Type': 'application/json'
36
- },
37
- body: JSON.stringify({
38
- prompt,
39
- repository: root,
40
- name: `agentxchain-${id}`
41
- })
52
+ headers: authHeaders(apiKey),
53
+ body: JSON.stringify(body)
42
54
  });
43
55
 
44
56
  if (!res.ok) {
45
- const body = await res.text();
46
- console.log(chalk.red(` Failed to launch ${id}: ${res.status} ${body}`));
57
+ const errBody = await res.text();
58
+ console.log(chalk.red(` ${id}: ${res.status} ${errBody}`));
47
59
  continue;
48
60
  }
49
61
 
50
62
  const data = await res.json();
51
- launched.push({ id, name: agent.name, cloudId: data.id || 'unknown' });
52
- console.log(chalk.green(` ✓ Launched ${chalk.bold(id)} (${agent.name}) — cloud ID: ${data.id || '?'}`));
63
+ launched.push({
64
+ id,
65
+ name: agent.name,
66
+ cloudId: data.id,
67
+ status: data.status || 'CREATING',
68
+ url: data.target?.url || null
69
+ });
70
+
71
+ const urlStr = data.target?.url ? chalk.dim(` → ${data.target.url}`) : '';
72
+ console.log(chalk.green(` ✓ ${chalk.bold(id)} (${agent.name}) — ${data.id}${urlStr}`));
53
73
  } catch (err) {
54
- console.log(chalk.red(` Failed to launch ${id}: ${err.message}`));
74
+ console.log(chalk.red(` ${id}: ${err.message}`));
55
75
  }
56
76
  }
57
77
 
58
78
  if (launched.length > 0) {
59
- const sessionFile = JSON.stringify({ launched, started_at: new Date().toISOString(), ide: 'cursor' }, null, 2);
60
- const { writeFileSync } = await import('fs');
61
- const { join } = await import('path');
62
- writeFileSync(join(root, '.agentxchain-session.json'), sessionFile + '\n');
63
- console.log('');
64
- console.log(chalk.dim(` Session saved to .agentxchain-session.json`));
79
+ saveSession(root, launched, repoUrl);
65
80
  }
66
81
 
67
82
  return launched;
68
83
  }
69
84
 
85
+ export async function sendFollowup(apiKey, cloudId, message) {
86
+ const res = await fetch(`${API_BASE}/agents/${cloudId}/followup`, {
87
+ method: 'POST',
88
+ headers: authHeaders(apiKey),
89
+ body: JSON.stringify({ prompt: { text: message } })
90
+ });
91
+ if (!res.ok) {
92
+ const body = await res.text();
93
+ throw new Error(`Followup failed (${res.status}): ${body}`);
94
+ }
95
+ return await res.json();
96
+ }
97
+
98
+ export async function getAgentStatus(apiKey, cloudId) {
99
+ const res = await fetch(`${API_BASE}/agents/${cloudId}`, {
100
+ method: 'GET',
101
+ headers: authHeaders(apiKey)
102
+ });
103
+ if (!res.ok) return null;
104
+ return await res.json();
105
+ }
106
+
107
+ export async function getAgentConversation(apiKey, cloudId) {
108
+ const res = await fetch(`${API_BASE}/agents/${cloudId}/conversation`, {
109
+ method: 'GET',
110
+ headers: authHeaders(apiKey)
111
+ });
112
+ if (!res.ok) return null;
113
+ return await res.json();
114
+ }
115
+
116
+ export async function stopAgent(apiKey, cloudId) {
117
+ const res = await fetch(`${API_BASE}/agents/${cloudId}/stop`, {
118
+ method: 'POST',
119
+ headers: authHeaders(apiKey)
120
+ });
121
+ return res.ok;
122
+ }
123
+
124
+ export async function deleteAgent(apiKey, cloudId) {
125
+ const res = await fetch(`${API_BASE}/agents/${cloudId}`, {
126
+ method: 'DELETE',
127
+ headers: authHeaders(apiKey)
128
+ });
129
+ return res.ok;
130
+ }
131
+
132
+ export function loadSession(root) {
133
+ const sessionPath = join(root, '.agentxchain-session.json');
134
+ if (!existsSync(sessionPath)) return null;
135
+ return JSON.parse(readFileSync(sessionPath, 'utf8'));
136
+ }
137
+
138
+ // --- Internal ---
139
+
140
+ function saveSession(root, launched, repoUrl) {
141
+ const session = {
142
+ launched,
143
+ started_at: new Date().toISOString(),
144
+ ide: 'cursor',
145
+ repo: repoUrl
146
+ };
147
+ const sessionPath = join(root, '.agentxchain-session.json');
148
+ writeFileSync(sessionPath, JSON.stringify(session, null, 2) + '\n');
149
+ console.log(chalk.dim(` Session saved to .agentxchain-session.json`));
150
+ }
151
+
152
+ function filterAgents(config, specificId) {
153
+ if (specificId) {
154
+ if (!config.agents[specificId]) {
155
+ console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
156
+ process.exit(1);
157
+ }
158
+ return { [specificId]: config.agents[specificId] };
159
+ }
160
+ return config.agents;
161
+ }
162
+
70
163
  function fallbackPromptOutput(config, opts) {
71
164
  const agents = filterAgents(config, opts.agent);
72
-
73
- console.log(chalk.bold(' Copy-paste these prompts into separate Cursor sessions:'));
165
+ console.log(chalk.bold(' No API key. Printing seed prompts for manual use:'));
74
166
  console.log('');
75
-
76
167
  for (const [id, agent] of Object.entries(agents)) {
77
168
  const prompt = generateSeedPrompt(id, agent, config);
78
169
  console.log(chalk.dim(' ' + '─'.repeat(50)));
@@ -82,17 +173,16 @@ function fallbackPromptOutput(config, opts) {
82
173
  console.log(prompt);
83
174
  console.log('');
84
175
  }
85
-
86
176
  return [];
87
177
  }
88
178
 
89
- function filterAgents(config, specificId) {
90
- if (specificId) {
91
- if (!config.agents[specificId]) {
92
- console.log(chalk.red(` Agent "${specificId}" not found in agentxchain.json`));
93
- process.exit(1);
94
- }
95
- return { [specificId]: config.agents[specificId] };
96
- }
97
- return config.agents;
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('');
98
188
  }
@@ -0,0 +1,117 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import { loadConfig, loadLock, LOCK_FILE } from '../lib/config.js';
5
+ import { stopAgent, sendFollowup, loadSession } from '../adapters/cursor.js';
6
+
7
+ export async function claimCommand(opts) {
8
+ const result = loadConfig();
9
+ if (!result) { console.log(chalk.red(' No agentxchain.json found.')); process.exit(1); }
10
+
11
+ const { root, config } = result;
12
+ const lock = loadLock(root);
13
+ if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
14
+
15
+ const apiKey = process.env.CURSOR_API_KEY;
16
+ const session = loadSession(root);
17
+ const hasCursor = session?.ide === 'cursor' && apiKey;
18
+
19
+ if (lock.holder === 'human') {
20
+ console.log('');
21
+ console.log(chalk.yellow(' You already hold the lock.'));
22
+ console.log(` ${chalk.dim('Release with:')} ${chalk.bold('agentxchain release')}`);
23
+ console.log('');
24
+ return;
25
+ }
26
+
27
+ if (lock.holder && !opts.force) {
28
+ const name = config.agents[lock.holder]?.name || lock.holder;
29
+ console.log('');
30
+ console.log(chalk.yellow(` Lock held by ${chalk.bold(lock.holder)} (${name}).`));
31
+ console.log(chalk.dim(' Use --force to override.'));
32
+ console.log('');
33
+ return;
34
+ }
35
+
36
+ // Pause all Cursor agents when human claims
37
+ if (hasCursor && session.launched.length > 0) {
38
+ console.log(chalk.dim(' Pausing Cursor agents...'));
39
+ for (const agent of session.launched) {
40
+ try {
41
+ await stopAgent(apiKey, agent.cloudId);
42
+ console.log(chalk.dim(` Paused ${agent.id}`));
43
+ } catch {
44
+ console.log(chalk.dim(` Could not pause ${agent.id}`));
45
+ }
46
+ }
47
+ }
48
+
49
+ const lockPath = join(root, LOCK_FILE);
50
+ const newLock = {
51
+ holder: 'human',
52
+ last_released_by: lock.last_released_by,
53
+ turn_number: lock.turn_number,
54
+ claimed_at: new Date().toISOString()
55
+ };
56
+ writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
57
+
58
+ console.log('');
59
+ console.log(chalk.green(` ✓ Lock claimed by ${chalk.bold('human')} (turn ${lock.turn_number})`));
60
+ if (hasCursor) console.log(chalk.dim(' All Cursor agents paused.'));
61
+ console.log(` ${chalk.dim('Do your work, then:')} ${chalk.bold('agentxchain release')}`);
62
+ console.log('');
63
+ }
64
+
65
+ export async function releaseCommand() {
66
+ const result = loadConfig();
67
+ if (!result) { console.log(chalk.red(' No agentxchain.json found.')); process.exit(1); }
68
+
69
+ const { root, config } = result;
70
+ const lock = loadLock(root);
71
+ if (!lock) { console.log(chalk.red(' lock.json not found.')); process.exit(1); }
72
+
73
+ if (!lock.holder) {
74
+ console.log(chalk.yellow(' Lock is already free.'));
75
+ return;
76
+ }
77
+
78
+ const who = lock.holder;
79
+ const lockPath = join(root, LOCK_FILE);
80
+ const newLock = {
81
+ holder: null,
82
+ last_released_by: who,
83
+ turn_number: who === 'human' ? lock.turn_number : lock.turn_number + 1,
84
+ claimed_at: null
85
+ };
86
+ writeFileSync(lockPath, JSON.stringify(newLock, null, 2) + '\n');
87
+
88
+ console.log('');
89
+ console.log(chalk.green(` ✓ Lock released by ${chalk.bold(who)} (turn ${newLock.turn_number})`));
90
+
91
+ // If releasing from human and Cursor session exists, wake the next agent
92
+ if (who === 'human') {
93
+ const apiKey = process.env.CURSOR_API_KEY;
94
+ const session = loadSession(root);
95
+
96
+ if (session?.ide === 'cursor' && apiKey) {
97
+ const agentIds = Object.keys(config.agents);
98
+ const next = agentIds[0];
99
+ const cloudAgent = session.launched.find(a => a.id === next);
100
+
101
+ if (cloudAgent) {
102
+ try {
103
+ const name = config.agents[next]?.name || next;
104
+ await sendFollowup(apiKey, cloudAgent.cloudId,
105
+ `Human released the lock. It's your turn. Read lock.json, claim it, and do your work as ${name}.`
106
+ );
107
+ console.log(chalk.cyan(` Woke ${chalk.bold(next)} via Cursor followup.`));
108
+ } catch (err) {
109
+ console.log(chalk.dim(` Could not wake ${next}: ${err.message}`));
110
+ }
111
+ }
112
+ console.log(chalk.dim(' The watch process will coordinate from here.'));
113
+ }
114
+ }
115
+
116
+ console.log('');
117
+ }
@@ -0,0 +1,149 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+ import { loadConfig, CONFIG_FILE } from '../lib/config.js';
6
+
7
+ export async function configCommand(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
+
17
+ if (opts.addAgent) {
18
+ await addAgent(config, configPath);
19
+ return;
20
+ }
21
+
22
+ if (opts.removeAgent) {
23
+ removeAgent(config, configPath, opts.removeAgent);
24
+ return;
25
+ }
26
+
27
+ if (opts.set) {
28
+ setSetting(config, configPath, opts.set);
29
+ return;
30
+ }
31
+
32
+ if (opts.json) {
33
+ console.log(JSON.stringify(config, null, 2));
34
+ return;
35
+ }
36
+
37
+ printConfig(config);
38
+ }
39
+
40
+ function printConfig(config) {
41
+ console.log('');
42
+ console.log(chalk.bold(' AgentXchain Config'));
43
+ console.log(chalk.dim(' ' + '─'.repeat(40)));
44
+ console.log('');
45
+ console.log(` ${chalk.dim('Project:')} ${config.project}`);
46
+ console.log(` ${chalk.dim('Version:')} ${config.version}`);
47
+ console.log(` ${chalk.dim('Log:')} ${config.log}`);
48
+ console.log('');
49
+
50
+ console.log(` ${chalk.dim('Rules:')}`);
51
+ for (const [key, val] of Object.entries(config.rules || {})) {
52
+ console.log(` ${chalk.dim(key + ':')} ${val}`);
53
+ }
54
+ console.log('');
55
+
56
+ console.log(` ${chalk.dim('Agents:')} ${Object.keys(config.agents).length}`);
57
+ for (const [id, agent] of Object.entries(config.agents)) {
58
+ console.log(` ${chalk.cyan(id)} — ${agent.name}`);
59
+ console.log(` ${chalk.dim(agent.mandate.slice(0, 80))}${agent.mandate.length > 80 ? '...' : ''}`);
60
+ console.log('');
61
+ }
62
+
63
+ console.log(chalk.dim(' Commands:'));
64
+ console.log(` ${chalk.bold('agentxchain config --add-agent')} Add a new agent`);
65
+ console.log(` ${chalk.bold('agentxchain config --remove-agent <id>')} Remove an agent`);
66
+ console.log(` ${chalk.bold('agentxchain config --set <key> <val>')} Update a setting`);
67
+ console.log(` ${chalk.bold('agentxchain config --json')} Output as JSON`);
68
+ console.log('');
69
+ }
70
+
71
+ async function addAgent(config, configPath) {
72
+ const answers = await inquirer.prompt([
73
+ {
74
+ type: 'input',
75
+ name: 'id',
76
+ message: 'Agent ID (lowercase, no spaces):',
77
+ validate: (val) => {
78
+ if (!val.match(/^[a-z0-9-]+$/)) return 'Use lowercase letters, numbers, and hyphens only.';
79
+ if (val === 'human' || val === 'system') return `"${val}" is a reserved ID.`;
80
+ if (config.agents[val]) return `Agent "${val}" already exists.`;
81
+ return true;
82
+ }
83
+ },
84
+ { type: 'input', name: 'name', message: 'Display name:' },
85
+ { type: 'input', name: 'mandate', message: 'Mandate (what this agent does):' }
86
+ ]);
87
+
88
+ config.agents[answers.id] = { name: answers.name, mandate: answers.mandate };
89
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
90
+
91
+ console.log('');
92
+ console.log(chalk.green(` ✓ Added agent ${chalk.bold(answers.id)} (${answers.name})`));
93
+ console.log(` ${chalk.dim('Agents now:')} ${Object.keys(config.agents).join(', ')}`);
94
+ console.log('');
95
+ }
96
+
97
+ function removeAgent(config, configPath, id) {
98
+ if (!config.agents[id]) {
99
+ console.log(chalk.red(` Agent "${id}" not found.`));
100
+ console.log(` ${chalk.dim('Available:')} ${Object.keys(config.agents).join(', ')}`);
101
+ process.exit(1);
102
+ }
103
+
104
+ const name = config.agents[id].name;
105
+ delete config.agents[id];
106
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
107
+
108
+ console.log('');
109
+ console.log(chalk.green(` ✓ Removed agent ${chalk.bold(id)} (${name})`));
110
+ console.log(` ${chalk.dim('Agents now:')} ${Object.keys(config.agents).join(', ')}`);
111
+ console.log('');
112
+ }
113
+
114
+ function setSetting(config, configPath, keyValPair) {
115
+ const parts = keyValPair.split(/\s+/);
116
+ if (parts.length < 2) {
117
+ console.log(chalk.red(' Usage: agentxchain config --set <key> <value>'));
118
+ console.log(chalk.dim(' Example: agentxchain config --set rules.max_consecutive_claims 3'));
119
+ process.exit(1);
120
+ }
121
+
122
+ const key = parts[0];
123
+ const rawVal = parts.slice(1).join(' ');
124
+ const segments = key.split('.');
125
+
126
+ let target = config;
127
+ for (let i = 0; i < segments.length - 1; i++) {
128
+ if (target[segments[i]] === undefined) {
129
+ target[segments[i]] = {};
130
+ }
131
+ target = target[segments[i]];
132
+ }
133
+
134
+ const lastKey = segments[segments.length - 1];
135
+ const oldVal = target[lastKey];
136
+
137
+ let val = rawVal;
138
+ if (rawVal === 'true') val = true;
139
+ else if (rawVal === 'false') val = false;
140
+ else if (!isNaN(rawVal) && rawVal !== '') val = Number(rawVal);
141
+
142
+ target[lastKey] = val;
143
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
144
+
145
+ console.log('');
146
+ console.log(chalk.green(` ✓ Set ${chalk.bold(key)} = ${val}`));
147
+ if (oldVal !== undefined) console.log(chalk.dim(` (was: ${oldVal})`));
148
+ console.log('');
149
+ }