natureco-cli 2.23.30 → 2.23.32

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.
Files changed (69) hide show
  1. package/bin/natureco.js +178 -167
  2. package/package.json +1 -1
  3. package/src/commands/acp.js +39 -0
  4. package/src/commands/admin-rpc.js +83 -0
  5. package/src/commands/agent.js +214 -23
  6. package/src/commands/agents.js +114 -30
  7. package/src/commands/approvals.js +172 -11
  8. package/src/commands/ask.js +1 -1
  9. package/src/commands/browser.js +815 -0
  10. package/src/commands/capability.js +195 -22
  11. package/src/commands/channels.js +422 -267
  12. package/src/commands/chat.js +5 -8
  13. package/src/commands/clawbot.js +19 -0
  14. package/src/commands/code.js +3 -2
  15. package/src/commands/commitments.js +125 -9
  16. package/src/commands/completion.js +40 -32
  17. package/src/commands/config.js +228 -30
  18. package/src/commands/configure.js +84 -67
  19. package/src/commands/cron.js +239 -19
  20. package/src/commands/daemon.js +34 -4
  21. package/src/commands/dashboard.js +47 -374
  22. package/src/commands/devices.js +53 -26
  23. package/src/commands/directory.js +146 -14
  24. package/src/commands/dns.js +148 -10
  25. package/src/commands/docs.js +119 -26
  26. package/src/commands/doctor.js +143 -492
  27. package/src/commands/exec-policy.js +57 -48
  28. package/src/commands/gateway.js +492 -249
  29. package/src/commands/health.js +141 -11
  30. package/src/commands/help.js +24 -25
  31. package/src/commands/hooks.js +141 -87
  32. package/src/commands/infer.js +1442 -41
  33. package/src/commands/logs.js +122 -99
  34. package/src/commands/mcp.js +121 -309
  35. package/src/commands/memory.js +128 -0
  36. package/src/commands/message.js +720 -140
  37. package/src/commands/models.js +39 -1
  38. package/src/commands/node.js +77 -77
  39. package/src/commands/nodes.js +278 -22
  40. package/src/commands/onboard.js +115 -56
  41. package/src/commands/pairing.js +108 -107
  42. package/src/commands/path.js +206 -0
  43. package/src/commands/plugins.js +35 -1
  44. package/src/commands/proxy.js +159 -8
  45. package/src/commands/qr.js +55 -13
  46. package/src/commands/reset.js +101 -94
  47. package/src/commands/secrets.js +104 -21
  48. package/src/commands/sessions.js +110 -51
  49. package/src/commands/setup.js +229 -649
  50. package/src/commands/skills.js +67 -1
  51. package/src/commands/status.js +101 -127
  52. package/src/commands/tasks.js +208 -100
  53. package/src/commands/terminal.js +130 -12
  54. package/src/commands/transcripts.js +24 -1
  55. package/src/commands/tui.js +41 -0
  56. package/src/commands/uninstall.js +73 -92
  57. package/src/commands/update.js +146 -91
  58. package/src/commands/web-fetch.js +34 -0
  59. package/src/commands/webhooks.js +58 -66
  60. package/src/commands/wiki.js +783 -0
  61. package/src/utils/agents-md.js +85 -0
  62. package/src/utils/api.js +40 -41
  63. package/src/utils/format.js +144 -0
  64. package/src/utils/headless.js +2 -1
  65. package/src/utils/parallel-tools.js +106 -0
  66. package/src/utils/sub-agent.js +148 -0
  67. package/src/utils/token-budget.js +304 -0
  68. package/src/utils/tool-runner.js +7 -5
  69. package/src/utils/web-fetch.js +107 -0
@@ -0,0 +1,85 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const cache = new Map();
5
+
6
+ const AGENTS_MD_FILENAMES = [
7
+ 'AGENTS.md',
8
+ '.natureco/AGENTS.md',
9
+ '.natureco/INSTRUCTIONS.md',
10
+ ];
11
+
12
+ function isRoot(dir) {
13
+ const parsed = path.parse(dir);
14
+ return parsed.root === dir;
15
+ }
16
+
17
+ function hasGit(dir) {
18
+ return fs.existsSync(path.join(dir, '.git'));
19
+ }
20
+
21
+ function findAgentsMd(cwd) {
22
+ let current = path.resolve(cwd || process.cwd());
23
+
24
+ while (true) {
25
+ for (const relPath of AGENTS_MD_FILENAMES) {
26
+ const candidate = path.join(current, relPath);
27
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
28
+ return candidate;
29
+ }
30
+ }
31
+
32
+ if (isRoot(current) || hasGit(current)) {
33
+ break;
34
+ }
35
+
36
+ const parent = path.dirname(current);
37
+ if (parent === current) break;
38
+ current = parent;
39
+ }
40
+
41
+ return null;
42
+ }
43
+
44
+ function loadInstructions(cwd) {
45
+ const resolvedCwd = path.resolve(cwd || process.cwd());
46
+
47
+ if (cache.has(resolvedCwd)) {
48
+ return cache.get(resolvedCwd);
49
+ }
50
+
51
+ const filePath = findAgentsMd(resolvedCwd);
52
+ if (!filePath) {
53
+ cache.set(resolvedCwd, null);
54
+ return null;
55
+ }
56
+
57
+ try {
58
+ const content = fs.readFileSync(filePath, 'utf8');
59
+ cache.set(resolvedCwd, content);
60
+ return content;
61
+ } catch {
62
+ cache.set(resolvedCwd, null);
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function injectIntoPrompt(systemPrompt, cwd) {
68
+ const instructions = loadInstructions(cwd);
69
+ if (!instructions) {
70
+ return systemPrompt;
71
+ }
72
+
73
+ const header = '\n\n## Project Instructions (from AGENTS.md)\n\n';
74
+ return systemPrompt + header + instructions.trim();
75
+ }
76
+
77
+ function clearCache() {
78
+ cache.clear();
79
+ }
80
+
81
+ module.exports = {
82
+ loadInstructions,
83
+ injectIntoPrompt,
84
+ clearCache,
85
+ };
package/src/utils/api.js CHANGED
@@ -8,6 +8,7 @@ const chalk = require('chalk');
8
8
  const { getConfig } = require('./config');
9
9
  const { getToolDefinitions, executeToolCalls } = require('./tool-runner');
10
10
  const { MCPClient } = require('./mcp-client');
11
+ const TB = require('./token-budget');
11
12
 
12
13
  // Persistent conversation directory
13
14
  const CONV_DIR = path.join(os.homedir(), '.natureco', 'conversations');
@@ -60,7 +61,7 @@ function saveConversation(convId, messages) {
60
61
  try {
61
62
  fs.mkdirSync(CONV_DIR, { recursive: true });
62
63
  // Keep only last 10 messages
63
- fs.writeFileSync(file, JSON.stringify(messages.slice(-10), null, 2));
64
+ fs.writeFileSync(file, JSON.stringify(messages.slice(-(TB.load().conversationOnDisk)), null, 2));
64
65
  } catch (e) {
65
66
  // Silently fail
66
67
  }
@@ -188,7 +189,7 @@ function normalizeMcpToolSchema(tool) {
188
189
  function minimizeMcpTool(tool) {
189
190
  return {
190
191
  name: tool.name,
191
- description: (tool.description || '').slice(0, 100),
192
+ description: TB.capMcpDesc(tool.description),
192
193
  inputSchema: {
193
194
  type: tool.inputSchema?.type || 'object',
194
195
  properties: Object.fromEntries(
@@ -265,9 +266,10 @@ async function executeMcpTool(toolName, toolArgs) {
265
266
  if (textContents.length > 0) {
266
267
  let output = textContents.join('\n');
267
268
 
268
- // Truncate MCP result to max 1500 characters
269
- if (output.length > 1500) {
270
- output = output.slice(0, 1500) + '... (truncated)';
269
+ // Truncate MCP result
270
+ const maxChars = TB.load().toolMaxChars;
271
+ if (output.length > maxChars) {
272
+ output = output.slice(0, maxChars) + '... (truncated)';
271
273
  }
272
274
 
273
275
  return {
@@ -281,8 +283,9 @@ async function executeMcpTool(toolName, toolArgs) {
281
283
  let fallbackOutput = JSON.stringify(result, null, 2);
282
284
 
283
285
  // Truncate fallback output too
284
- if (fallbackOutput.length > 1500) {
285
- fallbackOutput = fallbackOutput.slice(0, 1500) + '... (truncated)';
286
+ const maxChars = TB.load().toolMaxChars;
287
+ if (fallbackOutput.length > maxChars) {
288
+ fallbackOutput = fallbackOutput.slice(0, maxChars) + '... (truncated)';
286
289
  }
287
290
 
288
291
  return {
@@ -467,6 +470,7 @@ async function sendMessageOpenAICompatible(providerConfig, messages, tools) {
467
470
  role: 'assistant',
468
471
  content,
469
472
  tool_calls: data.choices?.[0]?.message?.tool_calls || undefined,
473
+ usage: data.usage || undefined,
470
474
  };
471
475
  }
472
476
 
@@ -519,7 +523,8 @@ async function sendMessageAnthropic(providerConfig, messages, tools) {
519
523
  return {
520
524
  role: 'assistant',
521
525
  content: content,
522
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined
526
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
527
+ usage: data.usage || undefined,
523
528
  };
524
529
  }
525
530
 
@@ -548,18 +553,22 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
548
553
  const convId = conversationId || generateDefaultConvId();
549
554
  const history = loadConversation(convId);
550
555
 
556
+ // Augment system prompt with project AGENTS.md instructions
557
+ const agentsMd = require('./agents-md');
558
+ const augmentedPrompt = agentsMd.injectIntoPrompt(systemPrompt || '', options?.cwd || process.cwd());
559
+
551
560
  // Build messages
552
- const messages = [];
553
- if (systemPrompt) {
554
- messages.push({ role: 'system', content: systemPrompt });
561
+ let messages = [];
562
+ if (augmentedPrompt) {
563
+ messages.push({ role: 'system', content: augmentedPrompt });
555
564
  }
556
565
  messages.push(...history);
557
566
  messages.push({ role: 'user', content: message });
558
567
 
559
- // Get tool definitions (local + MCP)
560
- const tools = providerConfig.isAnthropic
561
- ? formatToolsForAnthropic()
562
- : formatToolsForOpenAI();
568
+ // Get tool definitions (local + MCP) — skip if noTools flag set (chat mode)
569
+ const tools = options.noTools
570
+ ? []
571
+ : (providerConfig.isAnthropic ? formatToolsForAnthropic() : formatToolsForOpenAI());
563
572
 
564
573
  debugLog('\n[Provider] Sending request...');
565
574
  debugLog('[Provider] URL:', providerConfig.url);
@@ -572,7 +581,7 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
572
581
  let iteration = 0;
573
582
  const maxIterations = 10;
574
583
  let finalResponse = null;
575
- const stream = options.stream !== false &&
584
+ const stream = (options.stream ?? options.noStream === undefined) !== false &&
576
585
  !providerConfig.url.includes('api.natureco.me');
577
586
 
578
587
  while (iteration < maxIterations) {
@@ -604,6 +613,14 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
604
613
  };
605
614
  }
606
615
 
616
+ // Track token usage if available
617
+ if (assistantMessage.usage) {
618
+ TB.trackUsage(convId, {
619
+ input: assistantMessage.usage.prompt_tokens || assistantMessage.usage.input_tokens || 0,
620
+ output: assistantMessage.usage.completion_tokens || assistantMessage.usage.output_tokens || 0
621
+ });
622
+ }
623
+
607
624
  debugLog('[Provider] Response type:', assistantMessage?.tool_calls ? 'tool_calls' : 'text');
608
625
 
609
626
  // Add assistant message to history
@@ -681,6 +698,9 @@ async function sendMessageToProvider(apiKey, message, conversationId = null, sys
681
698
  finalResponse = finalResponse || 'Max tool execution iterations reached.';
682
699
  }
683
700
 
701
+ // Apply token budget trimming
702
+ messages = TB.trimMessages(messages);
703
+
684
704
  // Save to conversation history (only user and final assistant message)
685
705
  history.push({ role: 'user', content: message });
686
706
  history.push({ role: 'assistant', content: finalResponse });
@@ -755,35 +775,14 @@ async function sendMessage(apiKey, botId, message, conversationId = null, chatSy
755
775
  return sendMessageToProvider(apiKey, message, conversationId, prompt, options);
756
776
  }
757
777
 
758
- // Base system prompt — kısa (~3000 token max)
778
+ // Minimal base prompt (~200 token)
759
779
  const toolDefs = getToolDefinitions();
760
780
  const toolsDesc = toolDefs.map(t => t.name).join(', ');
761
- let baseSystemPrompt = `Terminal assistant. Use tools when needed.
762
- Home: ${homeDir}
763
- Tools: ${toolsDesc}`;
781
+ let systemPrompt = `Assistant. Tools: ${toolsDesc}. Home: ${homeDir}.`;
764
782
 
765
- // MCP sayı parametresi uyarısı sadece MCP aktifse
766
- if (config.mcpEnabled !== false) {
767
- baseSystemPrompt += `\nMCP: send number params as numbers, not strings.`;
768
- }
769
-
770
- // Prepend botName
771
- if (mem.botName) {
772
- baseSystemPrompt = `Adın ${mem.botName}. Sen ${mem.botName}'sun. Adın sorulduğunda her zaman "${mem.botName}" de.\n\n` + baseSystemPrompt;
773
- }
774
-
775
- let systemPrompt = baseSystemPrompt;
776
-
777
- // Append chat.js system prompt (memory + skills + agents) — max 3000 chars
783
+ // Skill prompts only, max 500 chars
778
784
  if (chatSystemPrompt) {
779
- systemPrompt += '\n\n' + chatSystemPrompt.slice(0, 3000);
780
- }
781
-
782
- // Add MCP server names if enabled and loaded
783
- const mcpTools = getMcpTools();
784
- if (config.mcpEnabled !== false && mcpTools.length > 0) {
785
- const mcpServerNames = Object.keys(mcpClients);
786
- systemPrompt += `\n\nMCP SERVERS: ${mcpServerNames.join(', ')}`;
785
+ systemPrompt += '\n' + chatSystemPrompt.slice(0, TB.load().systemPromptMaxChars);
787
786
  }
788
787
 
789
788
  return sendMessageToProvider(apiKey, message, conversationId, systemPrompt, options);
@@ -0,0 +1,144 @@
1
+ const chalk = require('chalk');
2
+
3
+ const W = () => Math.min(process.stdout.columns || 100, 100);
4
+
5
+ function header(text) {
6
+ const w = W();
7
+ const line = chalk.dim('┌' + '─'.repeat(w - 2) + '┐');
8
+ const padding = Math.max(0, w - text.length - 4);
9
+ const leftPad = Math.floor(padding / 2);
10
+ const rightPad = padding - leftPad;
11
+ console.log('');
12
+ console.log(line);
13
+ console.log(chalk.dim('│') + ' '.repeat(leftPad) + chalk.bold.cyan(text) + ' '.repeat(rightPad) + chalk.dim('│'));
14
+ console.log(chalk.dim('└' + '─'.repeat(w - 2) + '┘'));
15
+ }
16
+
17
+ function section(text) {
18
+ const w = W();
19
+ console.log('');
20
+ console.log(chalk.dim('▔').repeat(Math.min(w - 4, 48)));
21
+ console.log(chalk.bold.cyan(' ' + text));
22
+ }
23
+
24
+ function divider() {
25
+ const w = W();
26
+ console.log(chalk.dim('─').repeat(Math.min(w - 4, 48)));
27
+ }
28
+
29
+ function label(key, value, options = {}) {
30
+ const { indent = 2, keyWidth = 14, valueColor = 'white' } = options;
31
+ const pad = ' '.repeat(indent);
32
+ const coloredKey = chalk.dim(key.padEnd(keyWidth));
33
+ const coloredValue = chalk[valueColor] ? chalk[valueColor](value) : chalk.white(value);
34
+ console.log(pad + coloredKey + coloredValue);
35
+ }
36
+
37
+ function kv(key, value) {
38
+ if (value === undefined || value === null) value = chalk.dim('—');
39
+ const keyStr = chalk.dim(key.padEnd(14));
40
+ console.log(' ' + keyStr + chalk.white(value));
41
+ }
42
+
43
+ function badge(text, color = 'cyan') {
44
+ const c = chalk[color] || chalk.cyan;
45
+ return c.bold(' ' + text + ' ');
46
+ }
47
+
48
+ function cmd(text) {
49
+ return chalk.cyan(text);
50
+ }
51
+
52
+ function flag(text) {
53
+ return chalk.yellow(text);
54
+ }
55
+
56
+ function success(text) {
57
+ console.log(chalk.green(' ✓ ' + text));
58
+ }
59
+
60
+ function error(text) {
61
+ console.log(chalk.red(' ✗ ' + text));
62
+ }
63
+
64
+ function warning(text) {
65
+ console.log(chalk.yellow(' ⚠ ' + text));
66
+ }
67
+
68
+ function info(text) {
69
+ console.log(chalk.blue(' ℹ ' + text));
70
+ }
71
+
72
+ function list(items, options = {}) {
73
+ const { indent = 2, bullet = '•' } = options;
74
+ const pad = ' '.repeat(indent);
75
+ for (const item of items) {
76
+ if (typeof item === 'string') {
77
+ console.log(pad + chalk.dim(bullet + ' ') + chalk.white(item));
78
+ } else if (item.label && item.value) {
79
+ console.log(pad + chalk.dim(bullet + ' ') + chalk.white(item.label + ': ') + chalk.dim(item.value));
80
+ } else if (item.label) {
81
+ console.log(pad + chalk.dim(bullet + ' ') + chalk.white(item.label));
82
+ if (item.desc) console.log(pad + ' ' + chalk.dim(item.desc));
83
+ }
84
+ }
85
+ }
86
+
87
+ function table(headers, rows, options = {}) {
88
+ const { indent = 2, headerColor = 'bold.cyan' } = options;
89
+ const pad = ' '.repeat(indent);
90
+
91
+ if (rows.length === 0) {
92
+ console.log(pad + chalk.dim('(empty)'));
93
+ return;
94
+ }
95
+
96
+ const colCount = headers.length;
97
+ const colWidths = headers.map((h, i) => {
98
+ const maxData = rows.reduce((max, row) => Math.max(max, String(row[i] || '').length), 0);
99
+ return Math.max(h.length, maxData) + 2;
100
+ });
101
+
102
+ const totalWidth = colWidths.reduce((a, b) => a + b, 0) + colCount - 1;
103
+ if (totalWidth > W() - indent) {
104
+ const ratio = (W() - indent - colCount + 1) / (totalWidth - colCount + 1);
105
+ for (let i = 0; i < colWidths.length; i++) {
106
+ colWidths[i] = Math.max(3, Math.floor((colWidths[i] - 2) * ratio) + 2);
107
+ }
108
+ }
109
+
110
+ const hdr = headers.map((h, i) => {
111
+ const w = colWidths[i];
112
+ const text = h.length > w - 1 ? h.slice(0, w - 2) + '…' : h.padEnd(w);
113
+ return chalk.bold.cyan(text);
114
+ }).join(' ');
115
+ console.log(pad + chalk.dim('┌' + '─'.repeat(totalWidth) + '┐'));
116
+ console.log(pad + chalk.dim('│') + hdr + chalk.dim('│'));
117
+ console.log(pad + chalk.dim('├' + '─'.repeat(totalWidth) + '┤'));
118
+
119
+ for (const row of rows) {
120
+ const cells = row.map((cell, i) => {
121
+ const w = colWidths[i];
122
+ const text = String(cell || '');
123
+ return (text.length > w - 1 ? text.slice(0, w - 2) + '…' : text.padEnd(w));
124
+ }).join(' ');
125
+ console.log(pad + chalk.dim('│') + cells + chalk.dim('│'));
126
+ }
127
+
128
+ console.log(pad + chalk.dim('└' + '─'.repeat(totalWidth) + '┘'));
129
+ }
130
+
131
+ function dot(enabled, label) {
132
+ const d = enabled ? chalk.green('●') : chalk.dim('○');
133
+ console.log(' ' + d + ' ' + chalk.white(label));
134
+ }
135
+
136
+ function meta(text) {
137
+ console.log(chalk.dim(' ' + text));
138
+ }
139
+
140
+ function json(obj) {
141
+ console.log(chalk.dim(' ') + chalk.white(JSON.stringify(obj, null, 2).replace(/\n/g, '\n ')));
142
+ }
143
+
144
+ module.exports = { header, section, divider, label, kv, badge, cmd, flag, success, error, warning, info, list, table, dot, meta, json };
@@ -6,6 +6,7 @@
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { getProviderConfig } = require('./config');
9
+ const TB = require('./token-budget');
9
10
  const { getToolDefinitions, executeTool } = require('./tool-runner');
10
11
 
11
12
  // ── Proje indexing (code.js'den paylaşılan) ───────────────────────────────────
@@ -169,7 +170,7 @@ Dosyalar: ${projectIndex.files.slice(0, 25).join(', ')}`;
169
170
  role: 'tool',
170
171
  tool_call_id: assistantMsg.tool_calls?.find(tc => tc.function.name === toolCall.name)?.id || toolCall.id,
171
172
  name: toolCall.name,
172
- content: resultStr.slice(0, 3000),
173
+ content: resultStr.slice(0, TB.load().toolMaxChars),
173
174
  });
174
175
  }
175
176
  }
@@ -0,0 +1,106 @@
1
+ // Parallel Tool Runner — run multiple independent tool/MCP calls in parallel
2
+
3
+ async function executeSingle(tool, options = {}) {
4
+ const { type, name, params } = tool;
5
+ const executeTool = options.executeTool || defaultExecuteTool;
6
+
7
+ if (type === 'mcp') {
8
+ return executeMcpCall(tool, options);
9
+ }
10
+ if (type === 'function') {
11
+ return executeTool(name, params);
12
+ }
13
+ throw new Error(`Unknown tool type: ${type}`);
14
+ }
15
+
16
+ async function defaultExecuteTool(toolName, params) {
17
+ const { executeTool } = require('./tool-runner');
18
+ return executeTool(toolName, params);
19
+ }
20
+
21
+ async function executeMcpCall(tool, options = {}) {
22
+ const { name, params } = tool;
23
+ const getClient = options.getMcpClient;
24
+
25
+ if (!getClient) {
26
+ return { success: false, error: 'No MCP client lookup provided (options.getMcpClient)' };
27
+ }
28
+
29
+ const client = getClient(name);
30
+ if (!client) {
31
+ return { success: false, error: `MCP client not found for tool: ${name}` };
32
+ }
33
+
34
+ try {
35
+ const result = await client.callTool(name, params);
36
+ if (result.content && result.content.length > 0) {
37
+ const textContents = result.content
38
+ .filter(c => c.type === 'text')
39
+ .map(c => c.text);
40
+ if (textContents.length > 0) {
41
+ return { success: true, output: textContents.join('\n') };
42
+ }
43
+ }
44
+ return { success: true, output: JSON.stringify(result, null, 2) };
45
+ } catch (err) {
46
+ return { success: false, error: err.message };
47
+ }
48
+ }
49
+
50
+ // Run multiple independent tool calls in parallel
51
+ // tools: [{ name, params, type: 'mcp' | 'function' }]
52
+ async function runParallel(tools, options = {}) {
53
+ if (!tools || tools.length === 0) return [];
54
+
55
+ const results = await Promise.allSettled(
56
+ tools.map(t => executeSingle(t, options))
57
+ );
58
+
59
+ return results.map((r, i) => ({
60
+ tool: tools[i].name,
61
+ status: r.status === 'fulfilled' ? 'success' : 'error',
62
+ result: r.status === 'fulfilled' ? r.value : r.reason.message,
63
+ }));
64
+ }
65
+
66
+ // Group tool calls by independence
67
+ // dependencyMap: { inputKeys?: string[], outputKeys?: string[] }
68
+ // Tools that don't share input/output keys are independent
69
+ function groupIndependent(tools, dependencyMap = {}) {
70
+ if (!tools || tools.length === 0) return [];
71
+
72
+ const maps = tools.map(t => ({
73
+ tool: t,
74
+ inputs: dependencyMap[t.name]?.inputKeys || Object.keys(t.params || {}),
75
+ outputs: dependencyMap[t.name]?.outputKeys || [],
76
+ }));
77
+
78
+ const groups = [];
79
+
80
+ for (const item of maps) {
81
+ const allKeys = [...item.inputs, ...item.outputs];
82
+ let placed = false;
83
+
84
+ for (const group of groups) {
85
+ const overlap = allKeys.some(k => group.keys.includes(k));
86
+ if (!overlap) {
87
+ group.items.push(item.tool);
88
+ group.keys.push(...allKeys);
89
+ placed = true;
90
+ break;
91
+ }
92
+ }
93
+
94
+ if (!placed) {
95
+ groups.push({ items: [item.tool], keys: allKeys });
96
+ }
97
+ }
98
+
99
+ return groups.map(g => g.items);
100
+ }
101
+
102
+ module.exports = {
103
+ executeSingle,
104
+ runParallel,
105
+ groupIndependent,
106
+ };
@@ -0,0 +1,148 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { getProviderConfig } = require('./api');
5
+
6
+ const SUB_AGENTS_FILE = path.join(os.homedir(), '.natureco', 'sub-agents.json');
7
+
8
+ const SYSTEM_PROMPTS = {
9
+ explore: 'You are a research agent. Find information, explore codebases, search files. Be concise.',
10
+ general: 'You are a general-purpose implementation agent. Write code, fix bugs, refactor.',
11
+ review: 'You are a code review agent. Analyze code for bugs, security, performance, style.',
12
+ };
13
+
14
+ function ensureDir() {
15
+ const dir = path.dirname(SUB_AGENTS_FILE);
16
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ function loadAgents() {
20
+ ensureDir();
21
+ if (!fs.existsSync(SUB_AGENTS_FILE)) return [];
22
+ try {
23
+ return JSON.parse(fs.readFileSync(SUB_AGENTS_FILE, 'utf8'));
24
+ } catch {
25
+ return [];
26
+ }
27
+ }
28
+
29
+ function saveAgents(agents) {
30
+ ensureDir();
31
+ fs.writeFileSync(SUB_AGENTS_FILE, JSON.stringify(agents, null, 2), 'utf8');
32
+ }
33
+
34
+ function generateId() {
35
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
36
+ }
37
+
38
+ async function spawnSubAgent(type, task, options = {}) {
39
+ const validTypes = Object.keys(SYSTEM_PROMPTS);
40
+ if (!validTypes.includes(type)) {
41
+ throw new Error(`Invalid sub-agent type: ${type}. Valid: ${validTypes.join(', ')}`);
42
+ }
43
+
44
+ console.log(` [Sub-agent ${type}] Spawned for: ${task.slice(0, 80)}`);
45
+
46
+ const entry = {
47
+ id: generateId(),
48
+ type,
49
+ task,
50
+ status: 'running',
51
+ result: null,
52
+ startedAt: new Date().toISOString(),
53
+ completedAt: null,
54
+ };
55
+
56
+ const agents = loadAgents();
57
+ agents.push(entry);
58
+ saveAgents(agents);
59
+
60
+ try {
61
+ const providerConfig = getProviderConfig();
62
+ if (!providerConfig) {
63
+ throw new Error('Provider not configured. Run: natureco configure');
64
+ }
65
+
66
+ const systemPrompt = options.systemPrompt || SYSTEM_PROMPTS[type];
67
+
68
+ const baseUrl = providerConfig.url.replace(/\/+$/, '');
69
+ const endpoint = `${baseUrl}/chat/completions`;
70
+
71
+ const maxTokens = options.maxTokens || 512;
72
+
73
+ const response = await fetch(endpoint, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Authorization': `Bearer ${providerConfig.apiKey}`,
77
+ 'Content-Type': 'application/json',
78
+ },
79
+ body: JSON.stringify({
80
+ model: providerConfig.model,
81
+ messages: [
82
+ { role: 'system', content: systemPrompt },
83
+ { role: 'user', content: task },
84
+ ],
85
+ temperature: 0.3,
86
+ max_tokens: maxTokens,
87
+ }),
88
+ });
89
+
90
+ if (!response.ok) {
91
+ const errorText = await response.text();
92
+ throw new Error(`Provider API error: ${response.status} - ${errorText}`);
93
+ }
94
+
95
+ const data = await response.json();
96
+ const content = data.choices?.[0]?.message?.content || data.choices?.[0]?.text || '';
97
+
98
+ entry.status = 'completed';
99
+ entry.result = content;
100
+ entry.completedAt = new Date().toISOString();
101
+ saveAgents(loadAgents().map(a => a.id === entry.id ? entry : a));
102
+
103
+ const usage = data.usage || {};
104
+ const duration = new Date(entry.completedAt) - new Date(entry.startedAt);
105
+
106
+ return { result: content, usage, duration };
107
+ } catch (err) {
108
+ entry.status = 'failed';
109
+ entry.result = err.message;
110
+ entry.completedAt = new Date().toISOString();
111
+ saveAgents(loadAgents().map(a => a.id === entry.id ? entry : a));
112
+
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ async function spawnParallel(agents) {
118
+ const promises = agents.map(a =>
119
+ spawnSubAgent(a.type, a.task, a.options || {}).then(
120
+ result => ({ status: 'fulfilled', result }),
121
+ error => ({ status: 'rejected', reason: error.message })
122
+ )
123
+ );
124
+
125
+ const results = await Promise.allSettled(promises);
126
+
127
+ const failed = results.filter(r => r.status === 'rejected');
128
+ return { results, failed };
129
+ }
130
+
131
+ function getStatus() {
132
+ const agents = loadAgents();
133
+ const last20 = agents.slice(-20).reverse();
134
+ return {
135
+ total: agents.length,
136
+ running: agents.filter(a => a.status === 'running').length,
137
+ completed: agents.filter(a => a.status === 'completed').length,
138
+ failed: agents.filter(a => a.status === 'failed').length,
139
+ agents: last20,
140
+ };
141
+ }
142
+
143
+ module.exports = {
144
+ spawnSubAgent,
145
+ spawnParallel,
146
+ getStatus,
147
+ SYSTEM_PROMPTS,
148
+ };