apexbot 1.0.1 → 1.0.4

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,144 @@
1
+ "use strict";
2
+ /**
3
+ * Tool Executor - Handles tool calling from AI models
4
+ *
5
+ * This module integrates the tool system with AI responses,
6
+ * parsing tool calls and executing them.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.parseToolCalls = parseToolCalls;
10
+ exports.executeTool = executeTool;
11
+ exports.executeToolCalls = executeToolCalls;
12
+ exports.formatToolResults = formatToolResults;
13
+ exports.getToolsSystemPrompt = getToolsSystemPrompt;
14
+ exports.buildToolContext = buildToolContext;
15
+ const tools_1 = require("../tools");
16
+ /**
17
+ * Parse tool calls from AI response
18
+ * Supports multiple formats:
19
+ * 1. JSON block: ```json\n{"tool": "name", "args": {...}}```
20
+ * 2. XML-style: <tool name="name">{"args": {...}}</tool>
21
+ * 3. Function call style: tool_name({"arg": "value"})
22
+ */
23
+ function parseToolCalls(text) {
24
+ const calls = [];
25
+ // Pattern 1: JSON blocks with tool calls
26
+ const jsonBlockRegex = /```(?:json)?\s*\n?\s*\{[\s\S]*?"(?:tool|function|name)"[\s\S]*?\}\s*```/gi;
27
+ const jsonBlocks = text.match(jsonBlockRegex) || [];
28
+ for (const block of jsonBlocks) {
29
+ try {
30
+ const jsonStr = block.replace(/```(?:json)?\s*\n?/gi, '').replace(/\s*```/g, '');
31
+ const parsed = JSON.parse(jsonStr);
32
+ if (parsed.tool || parsed.function || parsed.name) {
33
+ calls.push({
34
+ name: parsed.tool || parsed.function || parsed.name,
35
+ arguments: parsed.args || parsed.arguments || parsed.parameters || {},
36
+ });
37
+ }
38
+ }
39
+ catch (e) {
40
+ // Invalid JSON, skip
41
+ }
42
+ }
43
+ // Pattern 2: XML-style tool calls
44
+ const xmlRegex = /<tool\s+name=["']([^"']+)["']>\s*([\s\S]*?)\s*<\/tool>/gi;
45
+ let xmlMatch;
46
+ while ((xmlMatch = xmlRegex.exec(text)) !== null) {
47
+ try {
48
+ const name = xmlMatch[1];
49
+ const argsStr = xmlMatch[2].trim();
50
+ const args = argsStr ? JSON.parse(argsStr) : {};
51
+ calls.push({ name, arguments: args });
52
+ }
53
+ catch (e) {
54
+ // Invalid args, skip
55
+ }
56
+ }
57
+ // Pattern 3: Function call style: tool_name({"arg": "value"})
58
+ const funcRegex = /\b([a-z_][a-z0-9_]*)\s*\(\s*(\{[\s\S]*?\})\s*\)/gi;
59
+ let funcMatch;
60
+ while ((funcMatch = funcRegex.exec(text)) !== null) {
61
+ const name = funcMatch[1];
62
+ // Check if it's a registered tool
63
+ if (tools_1.toolRegistry.has(name)) {
64
+ try {
65
+ const args = JSON.parse(funcMatch[2]);
66
+ calls.push({ name, arguments: args });
67
+ }
68
+ catch (e) {
69
+ // Invalid args, skip
70
+ }
71
+ }
72
+ }
73
+ return calls;
74
+ }
75
+ /**
76
+ * Execute a single tool call
77
+ */
78
+ async function executeTool(call, context) {
79
+ console.log(`[ToolExecutor] Executing: ${call.name}`, call.arguments);
80
+ return tools_1.toolRegistry.execute(call.name, call.arguments, context);
81
+ }
82
+ /**
83
+ * Execute multiple tool calls in sequence
84
+ */
85
+ async function executeToolCalls(calls, context) {
86
+ const results = [];
87
+ for (const call of calls) {
88
+ const result = await executeTool(call, context);
89
+ results.push({ call, result });
90
+ }
91
+ return results;
92
+ }
93
+ /**
94
+ * Format tool results for inclusion in AI response
95
+ */
96
+ function formatToolResults(results) {
97
+ if (results.length === 0)
98
+ return '';
99
+ return results.map(({ call, result }) => {
100
+ const status = result.success ? 'SUCCESS' : 'ERROR';
101
+ const data = result.success
102
+ ? JSON.stringify(result.data, null, 2)
103
+ : result.error;
104
+ return `[Tool: ${call.name}] ${status}\n${data}`;
105
+ }).join('\n\n');
106
+ }
107
+ /**
108
+ * Get the system prompt addition for tool usage
109
+ */
110
+ function getToolsSystemPrompt() {
111
+ const tools = tools_1.toolRegistry.list();
112
+ if (tools.length === 0) {
113
+ return '';
114
+ }
115
+ const toolDescriptions = tools.map(t => {
116
+ const params = t.parameters.map(p => ` - ${p.name} (${p.type}${p.required ? ', required' : ''}): ${p.description}`).join('\n');
117
+ return `**${t.name}**: ${t.description}\nParameters:\n${params}`;
118
+ }).join('\n\n');
119
+ return `
120
+ You have access to the following tools. To use a tool, include a JSON block in your response:
121
+
122
+ \`\`\`json
123
+ {"tool": "tool_name", "args": {"param1": "value1"}}
124
+ \`\`\`
125
+
126
+ Available tools:
127
+
128
+ ${toolDescriptions}
129
+
130
+ After I execute the tool, I'll show you the result so you can incorporate it into your response.
131
+ `;
132
+ }
133
+ /**
134
+ * Build context for tool execution
135
+ */
136
+ function buildToolContext(sessionId, userId, channel, config) {
137
+ return {
138
+ sessionId,
139
+ userId,
140
+ channel,
141
+ workspaceDir: config?.workspaceDir || process.cwd(),
142
+ config,
143
+ };
144
+ }
@@ -47,10 +47,12 @@ class ChannelManager {
47
47
  if (!channel) {
48
48
  // For webchat, broadcast via gateway
49
49
  if (channelName === 'webchat') {
50
+ console.log(`[Channels] Broadcasting to webchat: ${text.slice(0, 50)}...`);
50
51
  this.gateway.broadcast({ type: 'response', text, to });
51
52
  return;
52
53
  }
53
- throw new Error(`Channel not found: ${channelName}`);
54
+ console.error(`[Channels] Channel not found: ${channelName}`);
55
+ return;
54
56
  }
55
57
  await channel.send(to, text, replyTo);
56
58
  }
package/dist/cli/index.js CHANGED
@@ -181,7 +181,7 @@ you log in on this machine, or a bot account like Telegram/Discord).
181
181
  ${chalk.yellow("If you're new to this, start with Ollama and least privilege.")} It helps limit what
182
182
  an agent can do if it's tricked or makes a mistake.
183
183
 
184
- ${chalk.cyan('Good news:')} ApexBot uses ${chalk.green('Ollama (local AI)')} by default — your data never leaves
184
+ ${chalk.cyan('Good news:')} ApexBot uses ${chalk.green('Ollama (local AI)')} by default — your data never leaves
185
185
  your computer. No cloud APIs, no tracking, 100% private and FREE.`);
186
186
  console.log('');
187
187
  const { continueSetup } = await inquirer.prompt([{
@@ -339,9 +339,9 @@ Configure at least one channel to start chatting.`);
339
339
  name: 'channels',
340
340
  message: 'Select channels to configure (space to toggle, enter to confirm):',
341
341
  choices: [
342
- { name: '📱 Telegram', value: 'telegram' },
343
- { name: '🎮 Discord', value: 'discord' },
344
- { name: '🌐 WebChat (built-in)', value: 'webchat' },
342
+ { name: 'Telegram', value: 'telegram' },
343
+ { name: 'Discord', value: 'discord' },
344
+ { name: 'WebChat (built-in)', value: 'webchat' },
345
345
  ],
346
346
  validate: (input) => {
347
347
  if (input.length === 0) {
@@ -413,15 +413,15 @@ WebChat UI will be available at http://localhost:<port>/chat`);
413
413
  console.log('');
414
414
  console.log(chalk.gray('─'.repeat(60)));
415
415
  console.log('');
416
- showBox(`${chalk.green('Configuration saved!')}
416
+ showBox(`${chalk.green('Configuration saved!')}
417
417
 
418
- Config file: ${chalk.cyan(CONFIG_FILE)}`, '🎉 Setup Complete!');
418
+ Config file: ${chalk.cyan(CONFIG_FILE)}`, 'Setup Complete');
419
419
  console.log('');
420
420
  // Ask to start gateway immediately
421
421
  const { startNow } = await inquirer.prompt([{
422
422
  type: 'confirm',
423
423
  name: 'startNow',
424
- message: chalk.yellow('🚀 Start ApexBot gateway now?'),
424
+ message: chalk.yellow('Start ApexBot gateway now?'),
425
425
  default: true,
426
426
  }]);
427
427
  if (startNow) {
@@ -452,9 +452,12 @@ async function startGatewayServer(config, options = {}) {
452
452
  host: '127.0.0.1',
453
453
  token: config.gateway?.token,
454
454
  verbose: options.verbose,
455
+ configDir: CONFIG_DIR,
455
456
  });
456
457
  // Configure agent
457
458
  gateway.agents.configure(config.agent);
459
+ // Initialize tools and skills system
460
+ await gateway.initializeTools();
458
461
  // Register channels
459
462
  if (config.channels?.telegram?.botToken) {
460
463
  const telegram = new telegram_1.TelegramChannel({
@@ -466,19 +469,25 @@ async function startGatewayServer(config, options = {}) {
466
469
  // Start gateway
467
470
  await gateway.start();
468
471
  await gateway.channels.connectAll();
472
+ // Show tool info
473
+ const tools = gateway.getToolRegistry().list();
474
+ const skills = gateway.getSkillManager()?.list() || [];
469
475
  console.log('');
470
- console.log(chalk.green('ApexBot is running!'));
476
+ console.log(chalk.green('ApexBot is running!'));
471
477
  console.log('');
472
478
  console.log(` ${chalk.cyan('Dashboard:')} http://127.0.0.1:${port}/chat`);
473
479
  console.log(` ${chalk.cyan('API:')} http://127.0.0.1:${port}`);
474
480
  console.log(` ${chalk.cyan('WebSocket:')} ws://127.0.0.1:${port}`);
475
481
  console.log('');
482
+ console.log(` ${chalk.cyan('Tools:')} ${tools.length} available`);
483
+ console.log(` ${chalk.cyan('Skills:')} ${skills.length} registered`);
484
+ console.log('');
476
485
  console.log(chalk.gray('Press Ctrl+C to stop.'));
477
486
  console.log('');
478
487
  // Handle shutdown
479
488
  process.on('SIGINT', async () => {
480
489
  console.log('');
481
- console.log(chalk.yellow('🛑 Shutting down...'));
490
+ console.log(chalk.yellow('Shutting down...'));
482
491
  await gateway.stop();
483
492
  process.exit(0);
484
493
  });
@@ -497,7 +506,7 @@ program
497
506
  const config = loadConfig();
498
507
  if (!config.agent?.provider) {
499
508
  console.log('');
500
- console.log(chalk.yellow('⚠️ ApexBot is not configured yet.'));
509
+ console.log(chalk.yellow('ApexBot is not configured yet.'));
501
510
  console.log('');
502
511
  console.log(`Run ${chalk.green('apexbot onboard')} to set up your bot.`);
503
512
  console.log('');
@@ -520,12 +529,12 @@ program
520
529
  // Check config
521
530
  const hasConfig = fs.existsSync(CONFIG_FILE);
522
531
  // Check Ollama
523
- let ollamaStatus = 'Not checked';
532
+ let ollamaStatus = 'Not checked';
524
533
  if (config.agent?.provider === 'ollama') {
525
534
  const ollama = await checkOllama(config.agent.apiUrl || 'http://localhost:11434');
526
535
  ollamaStatus = ollama.running
527
- ? chalk.green(`✅ Running (${ollama.models.length} models)`)
528
- : chalk.red('Not running');
536
+ ? chalk.green(`Running (${ollama.models.length} models)`)
537
+ : chalk.red('Not running');
529
538
  }
530
539
  // Check gateway
531
540
  let gatewayStatus = chalk.gray('Not running');
@@ -534,7 +543,7 @@ program
534
543
  const res = await fetch(`http://127.0.0.1:${port}/health`);
535
544
  if (res.ok) {
536
545
  const data = await res.json();
537
- gatewayStatus = chalk.green(`✅ Running (${data.sessions} sessions)`);
546
+ gatewayStatus = chalk.green(`Running (${data.sessions} sessions)`);
538
547
  }
539
548
  }
540
549
  catch (e) {
@@ -543,7 +552,7 @@ program
543
552
  spinner.stop();
544
553
  console.log(chalk.cyan('Status:'));
545
554
  console.log('');
546
- console.log(` Config: ${hasConfig ? chalk.green('✅ ' + CONFIG_FILE) : chalk.yellow('⚠️ Not configured')}`);
555
+ console.log(` Config: ${hasConfig ? chalk.green(CONFIG_FILE) : chalk.yellow('Not configured')}`);
547
556
  console.log(` Provider: ${config.agent?.provider || chalk.gray('None')}`);
548
557
  console.log(` Model: ${config.agent?.model || chalk.gray('None')}`);
549
558
  console.log(` Ollama: ${ollamaStatus}`);
@@ -575,7 +584,7 @@ program
575
584
  if (fs.existsSync(CONFIG_FILE)) {
576
585
  fs.unlinkSync(CONFIG_FILE);
577
586
  }
578
- console.log(chalk.green('Configuration reset.'));
587
+ console.log(chalk.green('Configuration reset.'));
579
588
  console.log(`Run ${chalk.cyan('apexbot onboard')} to set up again.`);
580
589
  }
581
590
  return;
@@ -625,7 +634,7 @@ program
625
634
  console.log(stdout);
626
635
  if (stderr)
627
636
  console.log(stderr);
628
- console.log(chalk.green('Model pulled successfully!'));
637
+ console.log(chalk.green('Model pulled successfully!'));
629
638
  });
630
639
  return;
631
640
  }
@@ -634,7 +643,7 @@ program
634
643
  spinner.stop();
635
644
  if (!ollama.running) {
636
645
  console.log('');
637
- console.log(chalk.red('Ollama is not running.'));
646
+ console.log(chalk.red('Ollama is not running.'));
638
647
  console.log(chalk.gray('Start it with: ollama serve'));
639
648
  console.log('');
640
649
  return;
@@ -677,7 +686,7 @@ program
677
686
  try {
678
687
  process.kill(parseInt(oldPid), 0);
679
688
  console.log('');
680
- console.log(chalk.yellow(`⚠️ Daemon already running (PID: ${oldPid})`));
689
+ console.log(chalk.yellow(`Daemon already running (PID: ${oldPid})`));
681
690
  console.log(chalk.gray(`Use 'apexbot daemon restart' to restart.`));
682
691
  console.log('');
683
692
  return;
@@ -688,23 +697,24 @@ program
688
697
  }
689
698
  }
690
699
  console.log('');
691
- console.log(chalk.cyan('🚀 Starting ApexBot daemon...'));
700
+ console.log(chalk.cyan('Starting ApexBot daemon...'));
692
701
  // Spawn detached process (cross-platform)
693
702
  // Use 'apexbot gateway' command directly - works with global npm install
694
703
  const { spawn } = require('child_process');
695
704
  const out = fs.openSync(logFile, 'a');
696
705
  const err = fs.openSync(logFile, 'a');
697
706
  const isWindows = process.platform === 'win32';
698
- const child = spawn(isWindows ? 'cmd.exe' : '/bin/sh', isWindows
707
+ const child = spawn(isWindows ? process.env.ComSpec || 'cmd.exe' : '/bin/sh', isWindows
699
708
  ? ['/c', 'apexbot', 'gateway']
700
709
  : ['-c', 'apexbot gateway'], {
701
710
  detached: true,
702
711
  stdio: ['ignore', out, err],
703
712
  shell: false,
713
+ windowsHide: true, // Hide console window on Windows
704
714
  });
705
715
  fs.writeFileSync(pidFile, String(child.pid));
706
716
  child.unref();
707
- console.log(chalk.green(`✅ Daemon started (PID: ${child.pid})`));
717
+ console.log(chalk.green('Daemon started (PID: ' + child.pid + ')'));
708
718
  console.log('');
709
719
  console.log(` ${chalk.cyan('Dashboard:')} http://127.0.0.1:${config.gateway?.port || 18789}/chat`);
710
720
  console.log(` ${chalk.cyan('Logs:')} ${logFile}`);
@@ -717,25 +727,25 @@ program
717
727
  case 'stop': {
718
728
  if (!fs.existsSync(pidFile)) {
719
729
  console.log('');
720
- console.log(chalk.yellow('⚠️ Daemon is not running.'));
730
+ console.log(chalk.yellow('Daemon is not running.'));
721
731
  console.log('');
722
732
  return;
723
733
  }
724
734
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
725
735
  console.log('');
726
- console.log(chalk.cyan(`🛑 Stopping daemon (PID: ${pid})...`));
736
+ console.log(chalk.cyan(`Stopping daemon (PID: ${pid})...`));
727
737
  try {
728
738
  process.kill(pid, 'SIGTERM');
729
739
  fs.unlinkSync(pidFile);
730
- console.log(chalk.green('Daemon stopped.'));
740
+ console.log(chalk.green('Daemon stopped.'));
731
741
  }
732
742
  catch (e) {
733
743
  if (e.code === 'ESRCH') {
734
744
  fs.unlinkSync(pidFile);
735
- console.log(chalk.yellow('⚠️ Daemon was not running (stale PID file removed).'));
745
+ console.log(chalk.yellow('Daemon was not running (stale PID file removed).'));
736
746
  }
737
747
  else {
738
- console.log(chalk.red(`❌ Failed to stop daemon: ${e.message}`));
748
+ console.log(chalk.red(`Failed to stop daemon: ${e.message}`));
739
749
  }
740
750
  }
741
751
  console.log('');
@@ -743,7 +753,7 @@ program
743
753
  }
744
754
  case 'restart': {
745
755
  console.log('');
746
- console.log(chalk.cyan('🔄 Restarting daemon...'));
756
+ console.log(chalk.cyan('Restarting daemon...'));
747
757
  // Stop first
748
758
  if (fs.existsSync(pidFile)) {
749
759
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim());
@@ -761,16 +771,17 @@ program
761
771
  const out = fs.openSync(logFile, 'a');
762
772
  const err = fs.openSync(logFile, 'a');
763
773
  const isWindows = process.platform === 'win32';
764
- const child = spawn(isWindows ? 'cmd.exe' : '/bin/sh', isWindows
774
+ const child = spawn(isWindows ? process.env.ComSpec || 'cmd.exe' : '/bin/sh', isWindows
765
775
  ? ['/c', 'apexbot', 'gateway']
766
776
  : ['-c', 'apexbot gateway'], {
767
777
  detached: true,
768
778
  stdio: ['ignore', out, err],
769
779
  shell: false,
780
+ windowsHide: true,
770
781
  });
771
782
  fs.writeFileSync(pidFile, String(child.pid));
772
783
  child.unref();
773
- console.log(chalk.green(`✅ Daemon restarted (PID: ${child.pid})`));
784
+ console.log(chalk.green('Daemon restarted (PID: ' + child.pid + ')'));
774
785
  console.log('');
775
786
  break;
776
787
  }
@@ -779,7 +790,7 @@ program
779
790
  console.log(chalk.cyan(MINI_LOGO));
780
791
  console.log('');
781
792
  if (!fs.existsSync(pidFile)) {
782
- console.log(chalk.yellow('⚠️ Daemon is not running.'));
793
+ console.log(chalk.yellow('Daemon is not running.'));
783
794
  console.log('');
784
795
  console.log(chalk.gray('Start with: apexbot daemon start'));
785
796
  console.log('');
@@ -795,7 +806,7 @@ program
795
806
  isRunning = false;
796
807
  }
797
808
  if (isRunning) {
798
- console.log(chalk.green(`✅ Daemon is running (PID: ${pid})`));
809
+ console.log(chalk.green('Daemon is running (PID: ' + pid + ')'));
799
810
  console.log('');
800
811
  // Try to get status from gateway
801
812
  try {
@@ -811,7 +822,7 @@ program
811
822
  }
812
823
  }
813
824
  else {
814
- console.log(chalk.yellow('⚠️ Daemon process died (cleaning up PID file).'));
825
+ console.log(chalk.yellow('Daemon process died (cleaning up PID file).'));
815
826
  fs.unlinkSync(pidFile);
816
827
  }
817
828
  console.log('');
@@ -826,6 +837,111 @@ program
826
837
  }
827
838
  });
828
839
  // ─────────────────────────────────────────────────────────────────
840
+ // SKILLS Command - Manage skills
841
+ // ─────────────────────────────────────────────────────────────────
842
+ program
843
+ .command('skills [action]')
844
+ .alias('skill')
845
+ .description('Manage ApexBot skills (integrations)')
846
+ .option('-n, --name <name>', 'Skill name')
847
+ .action(async (action = 'list', options) => {
848
+ const config = loadConfig();
849
+ const port = config.gateway?.port || 18789;
850
+ switch (action) {
851
+ case 'list':
852
+ console.log('');
853
+ console.log(chalk.cyan('Available Skills:'));
854
+ console.log('');
855
+ try {
856
+ const res = await fetch(`http://127.0.0.1:${port}/api/skills`);
857
+ const data = await res.json();
858
+ for (const skill of data.skills) {
859
+ const status = skill.enabled ? chalk.green('[ENABLED]') : chalk.gray('[disabled]');
860
+ console.log(` ${status} ${chalk.white(skill.name)} v${skill.version}`);
861
+ console.log(` ${chalk.gray(skill.description)}`);
862
+ console.log(` Tools: ${skill.tools.join(', ')}`);
863
+ console.log('');
864
+ }
865
+ }
866
+ catch (e) {
867
+ console.log(chalk.yellow('Gateway not running. Available skills:'));
868
+ console.log('');
869
+ console.log(' - obsidian Obsidian note-taking integration');
870
+ console.log(' - weather Weather forecasts');
871
+ console.log(' - reminder Reminders and timers');
872
+ console.log(' - system System info and process management');
873
+ console.log('');
874
+ }
875
+ break;
876
+ case 'enable':
877
+ if (!options.name) {
878
+ console.log(chalk.red('Please specify skill name: --name <skill>'));
879
+ return;
880
+ }
881
+ console.log(chalk.yellow(`Enabling skill: ${options.name}...`));
882
+ console.log(chalk.gray('(Skill configuration will be saved to ~/.apexbot/skills.json)'));
883
+ break;
884
+ case 'disable':
885
+ if (!options.name) {
886
+ console.log(chalk.red('Please specify skill name: --name <skill>'));
887
+ return;
888
+ }
889
+ console.log(chalk.yellow(`Disabling skill: ${options.name}...`));
890
+ break;
891
+ default:
892
+ console.log('Usage: apexbot skills [list|enable|disable] --name <skill>');
893
+ }
894
+ });
895
+ // ─────────────────────────────────────────────────────────────────
896
+ // TOOLS Command - List available tools
897
+ // ─────────────────────────────────────────────────────────────────
898
+ program
899
+ .command('tools')
900
+ .alias('tool')
901
+ .description('List available tools')
902
+ .action(async () => {
903
+ const config = loadConfig();
904
+ const port = config.gateway?.port || 18789;
905
+ console.log('');
906
+ console.log(chalk.cyan('Available Tools:'));
907
+ console.log('');
908
+ try {
909
+ const res = await fetch(`http://127.0.0.1:${port}/api/tools`);
910
+ const data = await res.json();
911
+ // Group by category
912
+ const byCategory = {};
913
+ for (const tool of data.tools) {
914
+ const cat = tool.category || 'other';
915
+ if (!byCategory[cat])
916
+ byCategory[cat] = [];
917
+ byCategory[cat].push(tool);
918
+ }
919
+ for (const [category, tools] of Object.entries(byCategory)) {
920
+ console.log(chalk.yellow(` ${category.toUpperCase()}`));
921
+ for (const tool of tools) {
922
+ console.log(` ${chalk.white(tool.name)} - ${chalk.gray(tool.description)}`);
923
+ }
924
+ console.log('');
925
+ }
926
+ }
927
+ catch (e) {
928
+ console.log(chalk.yellow('Gateway not running. Built-in tools:'));
929
+ console.log('');
930
+ console.log(' CORE');
931
+ console.log(' shell Execute shell commands');
932
+ console.log(' read_file Read file contents');
933
+ console.log(' write_file Write to files');
934
+ console.log(' list_dir List directory contents');
935
+ console.log(' edit_file Edit files (search/replace)');
936
+ console.log(' fetch_url Fetch web pages');
937
+ console.log(' web_search Search the web');
938
+ console.log(' datetime Date/time operations');
939
+ console.log(' math Mathematical calculations');
940
+ console.log(' convert Unit conversions');
941
+ console.log('');
942
+ }
943
+ });
944
+ // ─────────────────────────────────────────────────────────────────
829
945
  // Default action - show help with banner
830
946
  // ─────────────────────────────────────────────────────────────────
831
947
  program