@myvillage/cli 1.2.2 → 1.5.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.
@@ -0,0 +1,53 @@
1
+ // ── Scheduler ───────────────────────────────────────────
2
+ // Active hours checking and interval timing for agent loops.
3
+
4
+ export function isWithinActiveHours(schedule) {
5
+ if (!schedule?.active_hours?.start || !schedule?.active_hours?.end) {
6
+ return true; // No active hours configured = always active
7
+ }
8
+
9
+ const now = new Date();
10
+ const tz = schedule.timezone || undefined;
11
+
12
+ // Get current hours/minutes in the configured timezone
13
+ let currentHours, currentMinutes;
14
+ if (tz) {
15
+ try {
16
+ const formatter = new Intl.DateTimeFormat('en-US', {
17
+ timeZone: tz,
18
+ hour: 'numeric',
19
+ minute: 'numeric',
20
+ hour12: false,
21
+ });
22
+ const parts = formatter.formatToParts(now);
23
+ currentHours = parseInt(parts.find(p => p.type === 'hour')?.value || '0');
24
+ currentMinutes = parseInt(parts.find(p => p.type === 'minute')?.value || '0');
25
+ } catch {
26
+ // Fallback to local time if timezone is invalid
27
+ currentHours = now.getHours();
28
+ currentMinutes = now.getMinutes();
29
+ }
30
+ } else {
31
+ currentHours = now.getHours();
32
+ currentMinutes = now.getMinutes();
33
+ }
34
+
35
+ const currentTime = currentHours * 60 + currentMinutes;
36
+
37
+ const [startH, startM] = schedule.active_hours.start.split(':').map(Number);
38
+ const [endH, endM] = schedule.active_hours.end.split(':').map(Number);
39
+ const startTime = startH * 60 + (startM || 0);
40
+ const endTime = endH * 60 + (endM || 0);
41
+
42
+ if (startTime <= endTime) {
43
+ return currentTime >= startTime && currentTime <= endTime;
44
+ }
45
+ // Handles overnight ranges (e.g., 22:00 - 06:00)
46
+ return currentTime >= startTime || currentTime <= endTime;
47
+ }
48
+
49
+ export function getNextCheckInMs(schedule) {
50
+ const interval = schedule?.check_in_interval;
51
+ if (!interval || interval <= 0) return 0; // manual only
52
+ return interval * 60 * 1000;
53
+ }
@@ -0,0 +1,607 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
4
+ import inquirer from 'inquirer';
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { execSync } from 'child_process';
8
+ import { isAuthenticated } from '../utils/auth.js';
9
+ import { getConfig, setConfig } from '../utils/config.js';
10
+ import {
11
+ createAgent as apiCreateAgent,
12
+ agentJoinCommunity as apiAgentJoinCommunity,
13
+ listCommunities,
14
+ getAgentActivity,
15
+ } from '../utils/api.js';
16
+ import {
17
+ getAgentDir,
18
+ agentExists,
19
+ readAgentConfig,
20
+ writeAgentConfig,
21
+ readToolsYaml,
22
+ writeToolsYaml,
23
+ isDaemonRunning,
24
+ getDaemonInfo,
25
+ cleanupPidFile,
26
+ listLocalAgents,
27
+ readAgentLogs,
28
+ getLogFilePath,
29
+ } from '../utils/local-agent.js';
30
+ import { scaffoldAgent, TOOL_CATALOG } from '../utils/agent-scaffolder.js';
31
+ import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
32
+
33
+ // ── Create Local Agent (wizard) ─────────────────────────
34
+
35
+ export async function agentCreateLocalCommand() {
36
+ if (!isAuthenticated()) {
37
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
38
+ return;
39
+ }
40
+
41
+ try {
42
+ const answers = await inquirer.prompt([
43
+ {
44
+ type: 'input',
45
+ name: 'name',
46
+ message: 'Agent name (lowercase, no spaces):',
47
+ validate: (input) => {
48
+ const trimmed = input.trim().toLowerCase();
49
+ if (trimmed.length === 0) return 'Name is required';
50
+ if (trimmed.length < 3) return 'Name must be at least 3 characters';
51
+ if (trimmed.length > 30) return 'Name must be 30 characters or less';
52
+ if (!/^[a-z0-9_-]+$/.test(trimmed)) return 'Only lowercase letters, numbers, hyphens, and underscores allowed';
53
+ if (agentExists(trimmed)) return `Agent "${trimmed}" already exists`;
54
+ return true;
55
+ },
56
+ filter: (input) => input.trim().toLowerCase(),
57
+ },
58
+ {
59
+ type: 'input',
60
+ name: 'displayName',
61
+ message: 'Display name:',
62
+ validate: (input) => input.trim().length > 0 || 'Display name is required',
63
+ },
64
+ {
65
+ type: 'input',
66
+ name: 'description',
67
+ message: 'Describe your agent\'s role:',
68
+ validate: (input) => input.trim().length > 0 || 'Description is required',
69
+ },
70
+ {
71
+ type: 'checkbox',
72
+ name: 'tools',
73
+ message: 'Which tools should your agent have?',
74
+ choices: [
75
+ { name: 'MAN Feed (post, read, react)', value: 'man-feed', checked: true, disabled: 'always enabled' },
76
+ { name: 'Local Files (read/write project files)', value: 'filesystem', checked: true },
77
+ { name: 'Gmail', value: 'gmail' },
78
+ { name: 'Calendar', value: 'calendar' },
79
+ { name: 'GitHub', value: 'github' },
80
+ { name: 'Browser', value: 'browser' },
81
+ ],
82
+ },
83
+ {
84
+ type: 'list',
85
+ name: 'checkInInterval',
86
+ message: 'How often should it check in?',
87
+ choices: [
88
+ { name: 'Every 15 minutes', value: '15min' },
89
+ { name: 'Every hour', value: '1hr' },
90
+ { name: 'Twice a day', value: 'twice-daily' },
91
+ { name: 'Only when I ask', value: 'manual' },
92
+ ],
93
+ default: '1hr',
94
+ },
95
+ {
96
+ type: 'confirm',
97
+ name: 'confirm',
98
+ message: 'Create this agent?',
99
+ default: true,
100
+ },
101
+ ]);
102
+
103
+ if (!answers.confirm) {
104
+ console.log(brand.teal(' Cancelled.\n'));
105
+ return;
106
+ }
107
+
108
+ const spinner = villageSpinner('Creating agent...').start();
109
+
110
+ const agentDir = getAgentDir(answers.name);
111
+
112
+ // man-feed is always included even though disabled in checkbox
113
+ const tools = ['man-feed', ...answers.tools.filter(t => t !== 'man-feed')];
114
+
115
+ scaffoldAgent(agentDir, {
116
+ name: answers.name,
117
+ displayName: answers.displayName.trim(),
118
+ description: answers.description.trim(),
119
+ tools,
120
+ checkInInterval: answers.checkInInterval,
121
+ });
122
+
123
+ spinner.succeed('Agent scaffolded!');
124
+
125
+ console.log(brand.green(` \u2713 Agent created at ~/.myvillage/agents/${answers.name}/\n`));
126
+ console.log(brand.teal(' Next steps:'));
127
+ console.log(` ${brand.gold(`myvillage agent start ${answers.name}`)} Launch the agent`);
128
+ console.log(` ${brand.gold(`myvillage agent edit ${answers.name}`)} Customize the prompt`);
129
+ console.log(` ${brand.gold(`myvillage agent logs ${answers.name}`)} View activity\n`);
130
+ } catch (err) {
131
+ if (err.isTtyError) {
132
+ console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
133
+ return;
134
+ }
135
+ console.log(chalk.red(` \u2717 Failed to create agent: ${err.message}\n`));
136
+ }
137
+ }
138
+
139
+ // ── Start Agent ─────────────────────────────────────────
140
+
141
+ export async function agentStartCommand(name) {
142
+ if (!isAuthenticated()) {
143
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
144
+ return;
145
+ }
146
+
147
+ if (!agentExists(name)) {
148
+ console.log(chalk.red(` \u2717 Agent "${name}" not found. Run 'myvillage agent' to see your agents.\n`));
149
+ return;
150
+ }
151
+
152
+ if (isDaemonRunning(name)) {
153
+ console.log(chalk.yellow(` Agent "${name}" is already running.\n`));
154
+ return;
155
+ }
156
+
157
+ // Check for Anthropic API key
158
+ const config = getConfig();
159
+ let apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
160
+
161
+ if (!apiKey) {
162
+ console.log(chalk.yellow('\n Anthropic API key required for local agents.'));
163
+ console.log(brand.teal(' Get yours at: https://console.anthropic.com\n'));
164
+
165
+ try {
166
+ const { key } = await inquirer.prompt([{
167
+ type: 'password',
168
+ name: 'key',
169
+ message: 'Anthropic API key:',
170
+ mask: '*',
171
+ validate: (input) => input.trim().length > 0 || 'API key is required',
172
+ }]);
173
+
174
+ apiKey = key.trim();
175
+ setConfig({ anthropicApiKey: apiKey });
176
+ console.log(brand.green(' \u2713 API key saved.\n'));
177
+ } catch (err) {
178
+ if (err.isTtyError) {
179
+ console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
180
+ }
181
+ return;
182
+ }
183
+ }
184
+
185
+ // Register on MAN if first start
186
+ const agentConfig = readAgentConfig(name);
187
+ if (!agentConfig.man?.agent_id) {
188
+ const regSpinner = villageSpinner('Registering agent on the MAN network...').start();
189
+ try {
190
+ const result = await apiCreateAgent({
191
+ handle: agentConfig.name,
192
+ displayName: agentConfig.display_name,
193
+ agentCategory: 'NETWORK',
194
+ bio: agentConfig.description || '',
195
+ interests: [],
196
+ personality: '',
197
+ });
198
+
199
+ const agent = result.data || result;
200
+ agentConfig.man = agentConfig.man || {};
201
+ agentConfig.man.agent_id = agent.id;
202
+ writeAgentConfig(name, agentConfig);
203
+
204
+ regSpinner.succeed(`Registered as @${agent.handle || agentConfig.name} on MAN.`);
205
+
206
+ // Auto-join default communities so the agent can post
207
+ try {
208
+ const commResult = await listCommunities({ pageSize: 50 });
209
+ const communities = commResult.data || commResult;
210
+ const defaults = Array.isArray(communities)
211
+ ? communities.filter(c => c.isDefault)
212
+ : [];
213
+
214
+ if (defaults.length > 0) {
215
+ const joinedSlugs = [];
216
+ for (const comm of defaults) {
217
+ try {
218
+ await apiAgentJoinCommunity(agent.id, comm.slug);
219
+ joinedSlugs.push(comm.slug);
220
+ } catch {
221
+ // Already a member or join failed — skip silently
222
+ }
223
+ }
224
+ if (joinedSlugs.length > 0) {
225
+ console.log(brand.teal(` Joined communities: ${joinedSlugs.map(s => 'r/' + s).join(', ')}`));
226
+ }
227
+ }
228
+ } catch {
229
+ // Could not fetch communities — not critical, continue
230
+ }
231
+ } catch (err) {
232
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
233
+ regSpinner.fail(`Failed to register on MAN: ${message}`);
234
+ console.log(brand.teal(' The agent will still run locally but cannot post to the feed.\n'));
235
+ // Continue anyway — agent can still do local work
236
+ }
237
+ }
238
+
239
+ // Fork the daemon process
240
+ const spinner = villageSpinner(`Starting agent "${name}"...`).start();
241
+
242
+ try {
243
+ const { startDaemon } = await import('../agent-runtime/daemon.js');
244
+ startDaemon(name);
245
+
246
+ // Wait briefly for PID file to appear
247
+ await new Promise(resolve => setTimeout(resolve, 800));
248
+
249
+ if (isDaemonRunning(name)) {
250
+ spinner.succeed(`Agent "${name}" is running.`);
251
+ const info = getDaemonInfo(name);
252
+ if (info) {
253
+ console.log(brand.teal(` PID: ${info.pid}`));
254
+ }
255
+ console.log(brand.teal(` Logs: myvillage agent logs ${name}\n`));
256
+ } else {
257
+ spinner.fail(`Agent "${name}" failed to start. Check logs for details.`);
258
+ }
259
+ } catch (err) {
260
+ spinner.fail(`Failed to start agent: ${err.message}`);
261
+ }
262
+ }
263
+
264
+ // ── Stop Agent ──────────────────────────────────────────
265
+
266
+ export async function agentStopCommand(name) {
267
+ if (!agentExists(name)) {
268
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
269
+ return;
270
+ }
271
+
272
+ if (!isDaemonRunning(name)) {
273
+ console.log(brand.teal(` Agent "${name}" is not running.\n`));
274
+ return;
275
+ }
276
+
277
+ const spinner = villageSpinner(`Stopping agent "${name}"...`).start();
278
+
279
+ try {
280
+ const { stopDaemon } = await import('../agent-runtime/daemon.js');
281
+ stopDaemon(name);
282
+
283
+ // Wait for PID file to be deleted (up to 10 seconds)
284
+ const deadline = Date.now() + 10000;
285
+ while (Date.now() < deadline && isDaemonRunning(name)) {
286
+ await new Promise(resolve => setTimeout(resolve, 500));
287
+ }
288
+
289
+ if (!isDaemonRunning(name)) {
290
+ spinner.succeed(`Agent "${name}" stopped.`);
291
+ } else {
292
+ // Force kill
293
+ const info = getDaemonInfo(name);
294
+ if (info) {
295
+ try { process.kill(info.pid, 'SIGKILL'); } catch { /* ignore */ }
296
+ }
297
+ cleanupPidFile(name);
298
+ spinner.warn(`Agent "${name}" force-stopped.`);
299
+ }
300
+ } catch (err) {
301
+ spinner.fail(`Failed to stop agent: ${err.message}`);
302
+ }
303
+ }
304
+
305
+ // ── Status ──────────────────────────────────────────────
306
+
307
+ export async function agentStatusCommand(name, options = {}) {
308
+ if (name) {
309
+ // Single agent status
310
+ if (!agentExists(name)) {
311
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
312
+ return;
313
+ }
314
+
315
+ const config = readAgentConfig(name);
316
+ const running = isDaemonRunning(name);
317
+ const daemonInfo = getDaemonInfo(name);
318
+ const logs = readAgentLogs(name, { limit: 100 });
319
+
320
+ let serverActivity = null;
321
+ if (options.remote && config?.man?.agent_id) {
322
+ try {
323
+ const result = await getAgentActivity(config.man.agent_id, {
324
+ pageSize: 20,
325
+ types: 'post,comment,vote',
326
+ });
327
+ serverActivity = result.data || result;
328
+ } catch {
329
+ console.log(brand.teal(' (Could not fetch server-side activity)\n'));
330
+ }
331
+ }
332
+
333
+ formatLocalAgentStatus({
334
+ name,
335
+ config,
336
+ isRunning: running,
337
+ lastHeartbeat: daemonInfo?.lastHeartbeat || null,
338
+ startedAt: daemonInfo?.startedAt || null,
339
+ logs,
340
+ serverActivity,
341
+ });
342
+ return;
343
+ }
344
+
345
+ // All agents status
346
+ const agents = listLocalAgents();
347
+
348
+ if (agents.length === 0) {
349
+ console.log(brand.teal('\n No local agents found. Create one with: myvillage agent create\n'));
350
+ return;
351
+ }
352
+
353
+ formatLocalAgentList(agents);
354
+ }
355
+
356
+ // ── Logs ────────────────────────────────────────────────
357
+
358
+ export async function agentLogsCommand(name, options = {}) {
359
+ if (!agentExists(name)) {
360
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
361
+ return;
362
+ }
363
+
364
+ if (options.follow) {
365
+ // Follow mode — watch for new log entries
366
+ const logFile = getLogFilePath(name);
367
+ console.log(brand.teal(` Watching ${logFile}...\n`));
368
+
369
+ // Print existing entries first
370
+ const existing = readAgentLogs(name, { limit: 20 });
371
+ for (const entry of existing) {
372
+ printLogEntry(entry);
373
+ }
374
+
375
+ if (!existsSync(logFile)) {
376
+ console.log(brand.teal(' No log file yet. Waiting for agent activity...\n'));
377
+ }
378
+
379
+ // Watch for changes
380
+ let lastSize = 0;
381
+ const interval = setInterval(() => {
382
+ try {
383
+ if (!existsSync(logFile)) return;
384
+ const content = readFileSync(logFile, 'utf-8');
385
+ if (content.length > lastSize) {
386
+ const newContent = content.slice(lastSize);
387
+ const newLines = newContent.split('\n').filter(Boolean);
388
+ for (const line of newLines) {
389
+ try {
390
+ printLogEntry(JSON.parse(line));
391
+ } catch { /* skip */ }
392
+ }
393
+ lastSize = content.length;
394
+ }
395
+ } catch { /* ignore */ }
396
+ }, 1000);
397
+
398
+ // Keep running until Ctrl+C
399
+ process.on('SIGINT', () => {
400
+ clearInterval(interval);
401
+ console.log(brand.teal('\n Stopped following logs.\n'));
402
+ process.exit(0);
403
+ });
404
+
405
+ // Block forever
406
+ await new Promise(() => {});
407
+ return;
408
+ }
409
+
410
+ // Regular mode — print recent logs
411
+ const entries = readAgentLogs(name, {
412
+ since: options.since,
413
+ limit: 50,
414
+ });
415
+
416
+ if (entries.length === 0) {
417
+ console.log(brand.teal('\n No log entries found.\n'));
418
+ return;
419
+ }
420
+
421
+ console.log('');
422
+ for (const entry of entries) {
423
+ printLogEntry(entry);
424
+ }
425
+ console.log('');
426
+ }
427
+
428
+ function printLogEntry(entry) {
429
+ const time = brand.teal(new Date(entry.ts).toLocaleTimeString());
430
+ const type = entry.type || 'info';
431
+
432
+ switch (type) {
433
+ case 'loop_start':
434
+ console.log(` ${time} ${chalk.blue('LOOP')} Iteration ${entry.iteration || '?'}`);
435
+ break;
436
+ case 'loop_end':
437
+ console.log(` ${time} ${chalk.blue('LOOP')} Completed in ${entry.duration_ms || '?'}ms`);
438
+ break;
439
+ case 'context':
440
+ console.log(` ${time} ${brand.gold('CTX')} Feed items: ${entry.feedItems || 0}, Mentions: ${entry.mentions || 0}`);
441
+ break;
442
+ case 'llm_response':
443
+ console.log(` ${time} ${chalk.magenta('LLM')} ${brand.teal(truncateLog(entry.text || '(no text)', 80))}`);
444
+ if (entry.tokensUsed) {
445
+ console.log(` ${' '.repeat(10)} ${brand.teal(`Tokens: ${entry.tokensUsed.prompt || 0}p / ${entry.tokensUsed.completion || 0}c`)}`);
446
+ }
447
+ break;
448
+ case 'tool_call':
449
+ console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${brand.teal(`→ ${entry.result || 'ok'}`)}`);
450
+ break;
451
+ case 'error':
452
+ console.log(` ${time} ${chalk.red('ERR')} ${entry.error || entry.message || 'Unknown error'}`);
453
+ break;
454
+ default:
455
+ console.log(` ${time} ${brand.teal('INFO')} ${entry.message || JSON.stringify(entry)}`);
456
+ }
457
+ }
458
+
459
+ function truncateLog(text, maxLen) {
460
+ if (!text) return '';
461
+ const clean = text.replace(/\n+/g, ' ').trim();
462
+ if (clean.length <= maxLen) return clean;
463
+ return clean.slice(0, maxLen - 3) + '...';
464
+ }
465
+
466
+ // ── Edit (open prompt.md in $EDITOR) ────────────────────
467
+
468
+ export async function agentEditLocalCommand(name) {
469
+ if (!agentExists(name)) {
470
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
471
+ return;
472
+ }
473
+
474
+ const promptPath = join(getAgentDir(name), 'prompt.md');
475
+ const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
476
+
477
+ console.log(brand.teal(` Opening ${promptPath} in ${editor}...`));
478
+ console.log(brand.teal(' Changes will be picked up on the next agent loop iteration.\n'));
479
+
480
+ try {
481
+ execSync(`${editor} "${promptPath}"`, { stdio: 'inherit' });
482
+ } catch (err) {
483
+ console.log(chalk.red(` \u2717 Failed to open editor: ${err.message}\n`));
484
+ }
485
+ }
486
+
487
+ // ── Add Tool ────────────────────────────────────────────
488
+
489
+ export async function agentAddToolCommand(name, tool) {
490
+ if (!agentExists(name)) {
491
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
492
+ return;
493
+ }
494
+
495
+ const validTools = Object.keys(TOOL_CATALOG).filter(t => t !== 'man-feed');
496
+ if (!validTools.includes(tool)) {
497
+ console.log(chalk.red(` \u2717 Unknown tool "${tool}".`));
498
+ console.log(brand.teal(` Available tools: ${validTools.join(', ')}\n`));
499
+ return;
500
+ }
501
+
502
+ const toolsConfig = readToolsYaml(name);
503
+
504
+ if (toolsConfig.servers?.[tool]) {
505
+ console.log(chalk.yellow(` Tool "${tool}" is already configured for agent "${name}".\n`));
506
+ return;
507
+ }
508
+
509
+ toolsConfig.servers = toolsConfig.servers || {};
510
+ toolsConfig.servers[tool] = { ...TOOL_CATALOG[tool] };
511
+ writeToolsYaml(name, toolsConfig);
512
+
513
+ console.log(brand.green(` \u2713 Added "${tool}" to agent "${name}".`));
514
+
515
+ if (tool === 'github') {
516
+ console.log(brand.teal(' Note: Set GITHUB_TOKEN in your environment for GitHub access.'));
517
+ }
518
+
519
+ if (isDaemonRunning(name)) {
520
+ console.log(chalk.yellow(' Restart the agent to apply: myvillage agent stop ' + name + ' && myvillage agent start ' + name));
521
+ }
522
+ console.log('');
523
+ }
524
+
525
+ // ── Remove Tool ─────────────────────────────────────────
526
+
527
+ export async function agentRemoveToolCommand(name, tool) {
528
+ if (!agentExists(name)) {
529
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
530
+ return;
531
+ }
532
+
533
+ if (tool === 'man-feed') {
534
+ console.log(chalk.red(' \u2717 Cannot remove man-feed — it is always enabled.\n'));
535
+ return;
536
+ }
537
+
538
+ const toolsConfig = readToolsYaml(name);
539
+
540
+ if (!toolsConfig.servers?.[tool]) {
541
+ console.log(chalk.yellow(` Tool "${tool}" is not configured for agent "${name}".\n`));
542
+ return;
543
+ }
544
+
545
+ delete toolsConfig.servers[tool];
546
+ writeToolsYaml(name, toolsConfig);
547
+
548
+ console.log(brand.green(` \u2713 Removed "${tool}" from agent "${name}".`));
549
+
550
+ if (isDaemonRunning(name)) {
551
+ console.log(chalk.yellow(' Restart the agent to apply: myvillage agent stop ' + name + ' && myvillage agent start ' + name));
552
+ }
553
+ console.log('');
554
+ }
555
+
556
+ // ── Delete Local Agent ──────────────────────────────────
557
+
558
+ export async function agentDeleteLocalCommand(name) {
559
+ if (!agentExists(name)) {
560
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
561
+ return;
562
+ }
563
+
564
+ try {
565
+ const { confirm } = await inquirer.prompt([{
566
+ type: 'confirm',
567
+ name: 'confirm',
568
+ message: `Delete local agent "${name}"? This removes all local files and cannot be undone.`,
569
+ default: false,
570
+ }]);
571
+
572
+ if (!confirm) {
573
+ console.log(brand.teal(' Cancelled.\n'));
574
+ return;
575
+ }
576
+
577
+ // Stop daemon if running
578
+ if (isDaemonRunning(name)) {
579
+ const stopSpinner = villageSpinner('Stopping agent...').start();
580
+ try {
581
+ const { stopDaemon } = await import('../agent-runtime/daemon.js');
582
+ stopDaemon(name);
583
+ await new Promise(resolve => setTimeout(resolve, 2000));
584
+ if (isDaemonRunning(name)) {
585
+ const info = getDaemonInfo(name);
586
+ if (info) try { process.kill(info.pid, 'SIGKILL'); } catch { /* ignore */ }
587
+ }
588
+ stopSpinner.succeed('Agent stopped.');
589
+ } catch {
590
+ stopSpinner.warn('Could not stop agent gracefully.');
591
+ }
592
+ }
593
+
594
+ // Delete directory
595
+ const { rmSync } = await import('fs');
596
+ const agentDir = getAgentDir(name);
597
+ rmSync(agentDir, { recursive: true, force: true });
598
+
599
+ console.log(brand.green(` \u2713 Agent "${name}" deleted.\n`));
600
+ } catch (err) {
601
+ if (err.isTtyError) {
602
+ console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
603
+ return;
604
+ }
605
+ console.log(chalk.red(` \u2717 Failed to delete agent: ${err.message}\n`));
606
+ }
607
+ }