natureco-cli 5.6.48 → 5.7.1

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 (172) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/DEPLOY_v2.0.0.md +0 -0
  3. package/LICENSE +21 -0
  4. package/README.md +2 -2
  5. package/README.md.bak +0 -0
  6. package/bin/natureco.js +0 -0
  7. package/package.json +14 -6
  8. package/skills/code-review/SKILL.md +0 -0
  9. package/skills/summarize/SKILL.md +0 -0
  10. package/skills/translate/SKILL.md +0 -0
  11. package/src/commands/acp.js +0 -0
  12. package/src/commands/admin-rpc.js +0 -0
  13. package/src/commands/agent.js +0 -0
  14. package/src/commands/agents.js +0 -0
  15. package/src/commands/approvals.js +0 -0
  16. package/src/commands/ask.js +0 -0
  17. package/src/commands/backup.js +0 -0
  18. package/src/commands/bonjour.js +0 -0
  19. package/src/commands/bots.js +0 -0
  20. package/src/commands/browser.js +0 -0
  21. package/src/commands/capability.js +0 -0
  22. package/src/commands/channels.js +0 -0
  23. package/src/commands/chat.js +5 -0
  24. package/src/commands/clawbot.js +0 -0
  25. package/src/commands/clickclack.js +0 -0
  26. package/src/commands/code.js +0 -0
  27. package/src/commands/commands.js +0 -0
  28. package/src/commands/commitments.js +0 -0
  29. package/src/commands/completion.js +0 -0
  30. package/src/commands/config.js +4 -0
  31. package/src/commands/configure.js +0 -0
  32. package/src/commands/crestodian.js +0 -0
  33. package/src/commands/cron.js +0 -0
  34. package/src/commands/daemon.js +0 -0
  35. package/src/commands/dashboard.js +0 -0
  36. package/src/commands/device-pair.js +0 -0
  37. package/src/commands/devices.js +0 -0
  38. package/src/commands/directory.js +0 -0
  39. package/src/commands/discord.js +0 -0
  40. package/src/commands/dns.js +0 -0
  41. package/src/commands/docs.js +0 -0
  42. package/src/commands/doctor.js +0 -0
  43. package/src/commands/exec-policy.js +0 -0
  44. package/src/commands/gateway-server.js +38 -3
  45. package/src/commands/gateway.js +2 -0
  46. package/src/commands/git.js +0 -0
  47. package/src/commands/health.js +0 -0
  48. package/src/commands/help.js +0 -0
  49. package/src/commands/hooks.js +0 -0
  50. package/src/commands/imessage.js +0 -0
  51. package/src/commands/infer.js +0 -0
  52. package/src/commands/init.js +0 -0
  53. package/src/commands/irc.js +0 -0
  54. package/src/commands/login.js +0 -0
  55. package/src/commands/logout.js +0 -0
  56. package/src/commands/logs.js +0 -0
  57. package/src/commands/mattermost.js +0 -0
  58. package/src/commands/mcp.js +0 -0
  59. package/src/commands/memory-cmd.js +0 -0
  60. package/src/commands/memory.js +0 -0
  61. package/src/commands/message.js +0 -0
  62. package/src/commands/migrate.js +0 -0
  63. package/src/commands/models.js +0 -0
  64. package/src/commands/node.js +0 -0
  65. package/src/commands/nodes.js +4 -0
  66. package/src/commands/oc-path.js +0 -0
  67. package/src/commands/onboard.js +0 -0
  68. package/src/commands/open-prose.js +0 -0
  69. package/src/commands/pairing.js +0 -0
  70. package/src/commands/path.js +0 -0
  71. package/src/commands/plugins.js +0 -0
  72. package/src/commands/policy.js +0 -0
  73. package/src/commands/proxy.js +0 -0
  74. package/src/commands/qr.js +0 -0
  75. package/src/commands/repl.js +13 -18
  76. package/src/commands/reset.js +0 -0
  77. package/src/commands/run.js +0 -0
  78. package/src/commands/sandbox.js +0 -0
  79. package/src/commands/secrets.js +0 -0
  80. package/src/commands/security.js +0 -0
  81. package/src/commands/sessions.js +0 -0
  82. package/src/commands/setup.js +7 -6
  83. package/src/commands/signal.js +0 -0
  84. package/src/commands/skills.js +0 -0
  85. package/src/commands/slack.js +0 -0
  86. package/src/commands/sms.js +0 -0
  87. package/src/commands/status.js +0 -0
  88. package/src/commands/system.js +0 -0
  89. package/src/commands/tasks.js +0 -0
  90. package/src/commands/telegram.js +0 -0
  91. package/src/commands/terminal.js +0 -0
  92. package/src/commands/thread-ownership.js +0 -0
  93. package/src/commands/transcripts.js +0 -0
  94. package/src/commands/tui.js +0 -0
  95. package/src/commands/ultrareview.js +0 -0
  96. package/src/commands/uninstall.js +0 -0
  97. package/src/commands/update.js +0 -0
  98. package/src/commands/voice.js +0 -0
  99. package/src/commands/vydra.js +0 -0
  100. package/src/commands/web-fetch.js +0 -0
  101. package/src/commands/webhooks.js +0 -0
  102. package/src/commands/whatsapp.js +0 -0
  103. package/src/commands/wiki.js +0 -0
  104. package/src/commands/workboard.js +0 -0
  105. package/src/tools/audio_understanding.js +0 -0
  106. package/src/tools/bash.js +0 -0
  107. package/src/tools/browser.js +0 -0
  108. package/src/tools/canvas.js +0 -0
  109. package/src/tools/dashboard.js +2 -1
  110. package/src/tools/document_extract.js +0 -0
  111. package/src/tools/duckduckgo.js +0 -0
  112. package/src/tools/exa_search.js +0 -0
  113. package/src/tools/filesystem.js +0 -0
  114. package/src/tools/firecrawl.js +0 -0
  115. package/src/tools/git.js +0 -0
  116. package/src/tools/http.js +0 -0
  117. package/src/tools/image_generation.js +0 -0
  118. package/src/tools/list_dir.js +0 -0
  119. package/src/tools/llm_task.js +0 -0
  120. package/src/tools/media_understanding.js +0 -0
  121. package/src/tools/memory_write.js +70 -16
  122. package/src/tools/music_generation.js +0 -0
  123. package/src/tools/parallel_search.js +0 -0
  124. package/src/tools/phone_control.js +0 -0
  125. package/src/tools/phone_control_enhanced.js +0 -0
  126. package/src/tools/read_file.js +0 -0
  127. package/src/tools/searxng.js +0 -0
  128. package/src/tools/speech_to_text.js +0 -0
  129. package/src/tools/text_to_speech.js +0 -0
  130. package/src/tools/thread_ownership.js +0 -0
  131. package/src/tools/video_generation.js +0 -0
  132. package/src/tools/web_readability.js +0 -0
  133. package/src/tools/web_search.js +0 -0
  134. package/src/tools/write_file.js +0 -0
  135. package/src/utils/agents-md.js +0 -0
  136. package/src/utils/agents.js +0 -0
  137. package/src/utils/api.js +47 -40
  138. package/src/utils/approvals.js +27 -12
  139. package/src/utils/atomic-file.js +91 -0
  140. package/src/utils/background.js +0 -0
  141. package/src/utils/baileys.js +0 -0
  142. package/src/utils/commands.js +0 -0
  143. package/src/utils/config.js +0 -0
  144. package/src/utils/cron.js +0 -0
  145. package/src/utils/dashboard-server.js +3 -2
  146. package/src/utils/error.js +4 -0
  147. package/src/utils/errors.js +0 -0
  148. package/src/utils/format.js +0 -0
  149. package/src/utils/gateway-ws.js +0 -0
  150. package/src/utils/headless.js +0 -0
  151. package/src/utils/history.js +6 -14
  152. package/src/utils/hooks.js +0 -0
  153. package/src/utils/inquirer-wrapper.js +0 -0
  154. package/src/utils/mcp-client.js +0 -0
  155. package/src/utils/mcp.js +0 -0
  156. package/src/utils/memory.js +0 -0
  157. package/src/utils/parallel-tools.js +0 -0
  158. package/src/utils/path-utils.js +0 -0
  159. package/src/utils/plugin-registry.js +0 -0
  160. package/src/utils/ports.js +44 -0
  161. package/src/utils/process-errors.js +115 -0
  162. package/src/utils/provider-detect.js +78 -0
  163. package/src/utils/secrets.js +0 -0
  164. package/src/utils/sessions.js +6 -13
  165. package/src/utils/skills.js +0 -0
  166. package/src/utils/streaming-tools.js +97 -0
  167. package/src/utils/sub-agent.js +0 -0
  168. package/src/utils/token-budget.js +0 -0
  169. package/src/utils/tool-adapter.js +0 -0
  170. package/src/utils/tool-runner.js +0 -0
  171. package/src/utils/tools.js +4 -2
  172. package/src/utils/web-fetch.js +0 -0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/tools/bash.js CHANGED
File without changes
File without changes
File without changes
@@ -15,8 +15,9 @@ const http = require("http");
15
15
  const fs = require("fs");
16
16
  const path = require("path");
17
17
  const os = require("os");
18
+ const { getDashboardPort } = require("../utils/ports");
18
19
 
19
- const PORT = 7421;
20
+ const PORT = getDashboardPort();
20
21
  const DASHBOARD_HTML = `
21
22
  <!DOCTYPE html>
22
23
  <html lang="tr">
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/tools/git.js CHANGED
File without changes
package/src/tools/http.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -8,53 +8,93 @@
8
8
  const fs = require("fs");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
+ const { writeJsonAtomicSync, readJsonSafeSync } = require("../utils/atomic-file");
11
12
 
12
13
  const MEMORY_DIR = path.join(os.homedir(), ".natureco", "memory");
13
14
 
15
+ // Soft cap on per-user fact count. Decay already prunes old/low-importance
16
+ // facts; this just bounds the worst-case file size and prompt-injection
17
+ // surface. Configurable via NATURECO_MAX_FACTS (default 50, was a hard 15
18
+ // that was silently truncating new writes when full).
19
+ const MAX_FACTS_PER_USER = (() => {
20
+ const raw = parseInt(process.env.NATURECO_MAX_FACTS || "", 10);
21
+ return Number.isFinite(raw) && raw > 0 ? raw : 50;
22
+ })();
23
+
14
24
  function getMemoryFile(username) {
15
25
  const name = (username || "default").toLowerCase();
16
26
  return path.join(MEMORY_DIR, `${name}.json`);
17
27
  }
18
28
 
29
+ function _emptyMemory(username) {
30
+ return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
31
+ }
32
+
19
33
  function loadMemory(username) {
20
- const file = getMemoryFile(username);
21
- try {
22
- if (!fs.existsSync(file)) {
23
- return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
24
- }
25
- return JSON.parse(fs.readFileSync(file, "utf8"));
26
- } catch {
27
- return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
28
- }
34
+ return readJsonSafeSync(getMemoryFile(username), _emptyMemory(username));
29
35
  }
30
36
 
31
37
  function saveMemory(username, memory) {
32
38
  if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
33
39
  memory.lastUpdated = new Date().toISOString();
34
- fs.writeFileSync(getMemoryFile(username), JSON.stringify(memory, null, 2), "utf8");
40
+ writeJsonAtomicSync(getMemoryFile(username), memory);
35
41
  return memory;
36
42
  }
37
43
 
38
44
  /**
39
- * Score azalt (eski fact'ler zamanla unutuluyor)
45
+ * Score azalt (eski fact'ler zamanla unutulur).
46
+ * Sıralama veya soft-cap UYGULAMAZ — onu enforceFactLimit yapar
47
+ * (push'tan sonra çağrılır, böylece yeni fact silinmez).
40
48
  */
41
49
  function decayFacts(memory) {
42
50
  if (!memory.facts) return memory;
43
51
  const now = Date.now();
44
52
  memory.facts = memory.facts.map(f => {
45
53
  if (!f.score) f.score = 5;
46
- // 1 haftadan eski -1, 1 aydan eski -3
47
54
  const ageMs = now - new Date(f.updatedAt || f.createdAt || now).getTime();
48
55
  const ageDays = ageMs / (1000 * 60 * 60 * 24);
49
56
  if (ageDays > 30) f.score = Math.max(0, f.score - 3);
50
57
  else if (ageDays > 7) f.score = Math.max(0, f.score - 1);
51
58
  return f;
52
59
  });
53
- // score 0 olanlari sil
54
60
  memory.facts = memory.facts.filter(f => (f.score || 0) > 0);
55
- // max 15 fact tut
56
- memory.facts.sort((a, b) => (b.score || 0) - (a.score || 0));
57
- memory.facts = memory.facts.slice(0, 15);
61
+ return memory;
62
+ }
63
+
64
+ /**
65
+ * Soft cap: yeni fact eklenmesinden SONRA uygulanır, böylece az önce
66
+ * yazılan fact eviction kurbanı olmaz. En düşük score + en eski updatedAt
67
+ * önce gider. Cap aşılırsa stderr'e tek satır warn yazar (eski "silent
68
+ * truncate to 15" davranışını gözlemlenebilir hale getirir).
69
+ *
70
+ * @param {{facts: Array<object>}} memory
71
+ * @param {{recentValue?: string}} [opts] Korunması zorunlu fact (yeni eklenen)
72
+ * @returns the same memory
73
+ */
74
+ function enforceFactLimit(memory, opts = {}) {
75
+ if (!memory.facts || memory.facts.length <= MAX_FACTS_PER_USER) return memory;
76
+ const before = memory.facts.length;
77
+ const recent = opts.recentValue ? opts.recentValue.toLowerCase() : null;
78
+ // Sort by score desc, then updatedAt desc (newest+highest first).
79
+ // The just-pushed fact is pinned at the top regardless of its score.
80
+ memory.facts.sort((a, b) => {
81
+ if (recent) {
82
+ if ((a.value || "").toLowerCase() === recent) return -1;
83
+ if ((b.value || "").toLowerCase() === recent) return 1;
84
+ }
85
+ const sa = a.score || 0, sb = b.score || 0;
86
+ if (sa !== sb) return sb - sa;
87
+ return String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""));
88
+ });
89
+ memory.facts = memory.facts.slice(0, MAX_FACTS_PER_USER);
90
+ const dropped = before - memory.facts.length;
91
+ if (dropped > 0 && !process.env.NATURECO_QUIET_MEMORY) {
92
+ // eslint-disable-next-line no-console
93
+ console.warn(
94
+ `[memory] cap ${MAX_FACTS_PER_USER} aşıldı, en düşük skorlu ${dropped} fact düşürüldü ` +
95
+ `(NATURECO_MAX_FACTS ile değiştir, NATURECO_QUIET_MEMORY=1 ile sustur)`
96
+ );
97
+ }
58
98
  return memory;
59
99
  }
60
100
 
@@ -122,6 +162,8 @@ function addMemory({ username, fact, score = 5, category = "general", botName, n
122
162
  createdAt: new Date().toISOString(),
123
163
  });
124
164
  }
165
+ // Limit'i ZIM push'tan sonra uygula → yeni eklenen fact düşürülmez.
166
+ memory = enforceFactLimit(memory, { recentValue: fact });
125
167
  }
126
168
 
127
169
  if (!memory.preferences) memory.preferences = [];
@@ -173,6 +215,18 @@ function showMemory({ username }) {
173
215
  }
174
216
 
175
217
  module.exports = {
218
+ // Exposed for tests / advanced consumers — not part of the tool schema.
219
+ _internals: {
220
+ MAX_FACTS_PER_USER,
221
+ enforceFactLimit,
222
+ decayFacts,
223
+ loadMemory,
224
+ saveMemory,
225
+ addMemory,
226
+ clearMemory,
227
+ showMemory,
228
+ getMemoryFile,
229
+ },
176
230
  name: "memory_write",
177
231
  description: "Memory'ye yeni fact/bilgi kaydet veya bot ismini/nickname'i degistir. Kalici, REPL'in extractMemoryFromMessage ozelligi.",
178
232
  inputSchema: {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/utils/api.js CHANGED
@@ -9,26 +9,17 @@ const { getConfig } = require('./config');
9
9
  const { getToolDefinitions, executeToolCalls } = require('./tool-runner');
10
10
  const { MCPClient } = require('./mcp-client');
11
11
  const TB = require('./token-budget');
12
+ const { accumulateToolCallDeltas } = require('./streaming-tools');
12
13
 
13
14
  /**
14
15
  * v5.5.0: Provider-specific format detection
15
16
  * Groq, OpenAI, Anthropic, Mistral, DeepSeek, OpenRouter, Ollama, MiniMax
17
+ *
18
+ * Canonical implementation lives in src/utils/provider-detect.js;
19
+ * re-exported here so the historical `detectProvider` reference inside
20
+ * api.js continues to work without touching every call site.
16
21
  */
17
- function detectProvider(providerUrl, model) {
18
- const url = (providerUrl || '').toLowerCase();
19
- const m = (model || '').toLowerCase();
20
- if (url.includes('anthropic.com') || m.includes('claude')) return 'anthropic';
21
- if (url.includes('groq.com') || m.includes('groq') || m.includes('llama-3') || m.includes('mixtral')) return 'groq';
22
- if (url.includes('openrouter.ai')) return 'openrouter';
23
- if (url.includes('api.deepseek.com') || m.includes('deepseek')) return 'deepseek';
24
- if (url.includes('mistral.ai') || m.includes('mistral') || m.includes('codestral')) return 'mistral';
25
- if (url.includes('together.xyz') || m.includes('together')) return 'together';
26
- if (url.includes('fireworks.ai') || m.includes('fireworks')) return 'fireworks';
27
- if (url.includes('perplexity.ai') || m.includes('pplx') || m.includes('sonar')) return 'perplexity';
28
- if (url.includes('localhost') || url.includes('127.0.0.1') || url.includes('ollama')) return 'ollama';
29
- if (url.includes('minimax.io') || url.includes('minimax')) return 'minimax';
30
- return 'openai'; // default
31
- }
22
+ const { detectProvider, isMiniMax } = require('./provider-detect');
32
23
 
33
24
  /**
34
25
  * v5.5.0: Tool definitions'ı provider'a göre normalize et
@@ -81,6 +72,29 @@ function parseToolCallsFromResponse(message, provider) {
81
72
  return message.tool_calls || [];
82
73
  }
83
74
 
75
+ /**
76
+ * Anthropic Messages API requires `system` to be either a non-empty string
77
+ * or omitted entirely. Sending `undefined` (which JSON.stringify drops to
78
+ * silent absence) leaves the model unanchored; sending `''` returns 400
79
+ * "system: cannot be empty" on recent API revisions. Always pass a
80
+ * meaningful default when no system message is present.
81
+ */
82
+ const DEFAULT_ANTHROPIC_SYSTEM =
83
+ 'You are a helpful AI assistant running inside the natureco CLI.';
84
+
85
+ function extractSystemForAnthropic(messages) {
86
+ const systemMsg = messages.find(m => m.role === 'system');
87
+ if (!systemMsg) return DEFAULT_ANTHROPIC_SYSTEM;
88
+ // content may be a string or an array of content blocks; both round-trip.
89
+ if (typeof systemMsg.content === 'string') {
90
+ return systemMsg.content.trim() || DEFAULT_ANTHROPIC_SYSTEM;
91
+ }
92
+ if (Array.isArray(systemMsg.content) && systemMsg.content.length > 0) {
93
+ return systemMsg.content;
94
+ }
95
+ return DEFAULT_ANTHROPIC_SYSTEM;
96
+ }
97
+
84
98
  /**
85
99
  * v5.5.0: System mesajı provider'a göre ayarla
86
100
  * - OpenAI: messages[].role=system
@@ -88,8 +102,6 @@ function parseToolCallsFromResponse(message, provider) {
88
102
  */
89
103
  function buildRequestBody(messages, model, options, provider) {
90
104
  if (provider === 'anthropic') {
91
- // System mesajını ayır
92
- const systemMsg = messages.find(m => m.role === 'system');
93
105
  const userMsgs = messages.filter(m => m.role !== 'system');
94
106
  return {
95
107
  model,
@@ -97,7 +109,7 @@ function buildRequestBody(messages, model, options, provider) {
97
109
  role: m.role,
98
110
  content: m.content
99
111
  })),
100
- system: systemMsg ? systemMsg.content : undefined,
112
+ system: extractSystemForAnthropic(messages),
101
113
  max_tokens: options.max_tokens || 4096,
102
114
  temperature: options.temperature || 0.7,
103
115
  ...(options.tools && options.tools.length > 0 ? { tools: options.tools } : {})
@@ -534,9 +546,8 @@ function formatToolsForAnthropic() {
534
546
  */
535
547
  async function sendMessageOpenAICompatible(providerConfig, messages, tools) {
536
548
  const baseUrl = providerConfig.url.replace(/\/+$/, '');
537
- // MiniMax özel endpoint tespiti
538
- const isMiniMax = baseUrl.includes('minimax.io') || baseUrl.includes('minimaxi.com') || baseUrl.includes('minimax.cn');
539
- const endpoint = isMiniMax
549
+ // MiniMax özel endpoint tespiti — provider-detect.js'deki kanonik tanım.
550
+ const endpoint = isMiniMax(baseUrl)
540
551
  ? `${baseUrl}/v1/text/chatcompletion_v2`
541
552
  : `${baseUrl}/chat/completions`;
542
553
  const requestBody = {
@@ -587,11 +598,10 @@ async function sendMessageOpenAICompatible(providerConfig, messages, tools) {
587
598
  */
588
599
  async function sendMessageAnthropic(providerConfig, messages, tools) {
589
600
  const endpoint = `${providerConfig.url}/v1/messages`;
590
-
591
- // Anthropic requires system message separate
592
- const systemMessage = messages.find(m => m.role === 'system');
601
+
602
+ // Anthropic requires system message separate; never send empty string.
593
603
  const userMessages = messages.filter(m => m.role !== 'system');
594
-
604
+
595
605
  const response = await fetch(endpoint, {
596
606
  method: 'POST',
597
607
  headers: {
@@ -602,7 +612,7 @@ async function sendMessageAnthropic(providerConfig, messages, tools) {
602
612
  body: JSON.stringify({
603
613
  model: providerConfig.model,
604
614
  max_tokens: 2000,
605
- system: systemMessage?.content || '',
615
+ system: extractSystemForAnthropic(messages),
606
616
  messages: userMessages,
607
617
  tools: tools,
608
618
  }),
@@ -992,9 +1002,8 @@ async function streamProviderCompletion(providerConfig, messages, tools) {
992
1002
 
993
1003
  async function streamOpenAICompletion(providerConfig, messages, tools) {
994
1004
  const baseUrl = providerConfig.url.replace(/\/+$/, '');
995
- // MiniMax özel endpoint tespiti (streaming için de aynı)
996
- const isMiniMax = baseUrl.includes('minimax.io') || baseUrl.includes('minimaxi.com') || baseUrl.includes('minimax.cn');
997
- const endpoint = isMiniMax
1005
+ // MiniMax özel endpoint tespiti (streaming için de aynı) — provider-detect.js.
1006
+ const endpoint = isMiniMax(baseUrl)
998
1007
  ? `${baseUrl}/v1/text/chatcompletion_v2`
999
1008
  : `${baseUrl}/chat/completions`;
1000
1009
 
@@ -1046,15 +1055,7 @@ async function streamOpenAICompletion(providerConfig, messages, tools) {
1046
1055
 
1047
1056
  if (delta.tool_calls) {
1048
1057
  hasToolCalls = true;
1049
- for (const tc of delta.tool_calls) {
1050
- const idx = tc.index;
1051
- if (!toolCalls[idx]) {
1052
- toolCalls[idx] = { id: tc.id || '', type: 'function', function: { name: '', arguments: '' } };
1053
- }
1054
- if (tc.id) toolCalls[idx].id = tc.id;
1055
- if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
1056
- if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
1057
- }
1058
+ accumulateToolCallDeltas(toolCalls, delta.tool_calls);
1058
1059
  }
1059
1060
 
1060
1061
  const token = delta.content || '';
@@ -1089,7 +1090,6 @@ async function streamOpenAICompletion(providerConfig, messages, tools) {
1089
1090
  async function streamAnthropicCompletion(providerConfig, messages) {
1090
1091
  const endpoint = `${providerConfig.url}/v1/messages`;
1091
1092
 
1092
- const systemMessage = messages.find(m => m.role === 'system');
1093
1093
  const userMessages = messages.filter(m => m.role !== 'system');
1094
1094
 
1095
1095
  const response = await fetch(endpoint, {
@@ -1102,7 +1102,7 @@ async function streamAnthropicCompletion(providerConfig, messages) {
1102
1102
  body: JSON.stringify({
1103
1103
  model: providerConfig.model,
1104
1104
  max_tokens: 2000,
1105
- system: systemMessage?.content || '',
1105
+ system: extractSystemForAnthropic(messages),
1106
1106
  messages: userMessages,
1107
1107
  stream: true,
1108
1108
  }),
@@ -1154,5 +1154,12 @@ module.exports = {
1154
1154
  streamProviderCompletion,
1155
1155
  streamOpenAICompletion,
1156
1156
  streamAnthropicCompletion,
1157
+ // Exposed for tests + advanced consumers (does not appear in the
1158
+ // public API surface of natureco's user-facing docs).
1159
+ _internals: {
1160
+ extractSystemForAnthropic,
1161
+ DEFAULT_ANTHROPIC_SYSTEM,
1162
+ buildRequestBody,
1163
+ },
1157
1164
  _sendMessage: sendMessage,
1158
1165
  };
@@ -1,15 +1,25 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const net = require('net');
5
4
  const chalk = require('chalk');
6
5
  const inquirer = require('./inquirer-wrapper');
7
6
  const { NatureCoError } = require('./errors');
7
+ const { writeJsonAtomicSync, readJsonSafeSync } = require('./atomic-file');
8
8
 
9
9
  const APPROVALS_FILE = path.join(os.homedir(), '.natureco', 'exec-approvals.json');
10
- const APPROVALS_SOCKET_PATH = path.join(os.homedir(), '.natureco', 'exec-approvals.sock');
11
10
  const DEFAULT_TIMEOUT_MS = 1800000; // 30 min
12
11
 
12
+ // 0o600 = owner read/write only. The allowlist controls which commands
13
+ // auto-execute without prompting; on a shared machine, world-read leaks
14
+ // the user's automation surface and world-write lets another local
15
+ // account inject auto-approved commands. Treat it like ssh keys.
16
+ const APPROVALS_FILE_MODE = 0o600;
17
+ const APPROVALS_DIR_MODE = 0o700;
18
+
19
+ function _emptyApprovals() {
20
+ return { version: 1, defaults: { security: 'full', ask: 'off' }, agents: {} };
21
+ }
22
+
13
23
  class ExecApprovalError extends NatureCoError {
14
24
  constructor(message, options = {}) {
15
25
  super(message, options);
@@ -33,20 +43,22 @@ function getApprovalsPath() {
33
43
  }
34
44
 
35
45
  function loadApprovals() {
36
- if (!fs.existsSync(APPROVALS_FILE)) {
37
- return { version: 1, defaults: { security: 'full', ask: 'off' }, agents: {} };
38
- }
39
- try {
40
- return JSON.parse(fs.readFileSync(APPROVALS_FILE, 'utf8'));
41
- } catch {
42
- return { version: 1, defaults: { security: 'full', ask: 'off' }, agents: {} };
43
- }
46
+ return readJsonSafeSync(APPROVALS_FILE, _emptyApprovals());
44
47
  }
45
48
 
46
49
  function saveApprovals(data) {
47
50
  const dir = path.dirname(APPROVALS_FILE);
48
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
49
- fs.writeFileSync(APPROVALS_FILE, JSON.stringify(data, null, 2), 'utf8');
51
+ if (!fs.existsSync(dir)) {
52
+ fs.mkdirSync(dir, { recursive: true, mode: APPROVALS_DIR_MODE });
53
+ } else {
54
+ // Tighten the dir even if it pre-existed with looser bits.
55
+ try { fs.chmodSync(dir, APPROVALS_DIR_MODE); } catch { /* best-effort */ }
56
+ }
57
+ writeJsonAtomicSync(APPROVALS_FILE, data, { mode: APPROVALS_FILE_MODE });
58
+ // Tighten the file even if it pre-existed with looser bits (the rename
59
+ // in writeJsonAtomicSync preserves the temp's mode, but only when we
60
+ // pass it through — defensively chmod again in case of older installs).
61
+ try { fs.chmodSync(APPROVALS_FILE, APPROVALS_FILE_MODE); } catch { /* best-effort */ }
50
62
  }
51
63
 
52
64
  function resolveEffectivePolicy(agentId) {
@@ -294,4 +306,7 @@ module.exports = {
294
306
  getApprovalsPath,
295
307
  DANGEROUS_PATTERNS,
296
308
  SAFE_COMMANDS,
309
+ APPROVALS_FILE,
310
+ APPROVALS_FILE_MODE,
311
+ APPROVALS_DIR_MODE,
297
312
  };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Crash-safe file write + safe read.
3
+ *
4
+ * Why this exists: `fs.writeFileSync(path, data)` is NOT atomic. If the
5
+ * process is killed mid-write (SIGTERM, OOM, power loss) the file is
6
+ * left truncated, which becomes a "corrupted session" on next load
7
+ * (JSON.parse throws and session history is lost).
8
+ *
9
+ * `writeFileAtomicSync(path, data)` writes to a temp sibling first, then
10
+ * `rename(2)`s it into place — atomic on POSIX when both paths are on
11
+ * the same filesystem (always true here, since the temp is in the same
12
+ * directory).
13
+ *
14
+ * `readFileSafeSync(path, fallback)` returns the parsed JSON or falls
15
+ * back gracefully on missing/corrupted files instead of throwing.
16
+ */
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const crypto = require('crypto');
20
+
21
+ function _tempName(target) {
22
+ // Same directory so rename(2) is atomic. Include pid+rand to avoid
23
+ // collisions if two processes write the same file.
24
+ const dir = path.dirname(target);
25
+ const base = path.basename(target);
26
+ const suffix = `${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
27
+ return path.join(dir, `.${base}.${suffix}`);
28
+ }
29
+
30
+ /**
31
+ * Atomically write a string/Buffer to `filePath`.
32
+ * On error the temp file is cleaned up and the original is untouched.
33
+ *
34
+ * @param {string} filePath
35
+ * @param {string|Buffer} data
36
+ * @param {{encoding?: BufferEncoding, mode?: number}} [opts]
37
+ */
38
+ function writeFileAtomicSync(filePath, data, opts = {}) {
39
+ const encoding = opts.encoding ?? 'utf-8';
40
+ const mode = opts.mode; // undefined → fs default
41
+ const tmp = _tempName(filePath);
42
+ try {
43
+ if (mode !== undefined) {
44
+ fs.writeFileSync(tmp, data, { encoding, mode });
45
+ } else {
46
+ fs.writeFileSync(tmp, data, encoding);
47
+ }
48
+ fs.renameSync(tmp, filePath);
49
+ } catch (err) {
50
+ // Best-effort cleanup; if the temp already vanished, ignore.
51
+ try { fs.unlinkSync(tmp); } catch { /* ignore */ }
52
+ throw err;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Read `filePath` as JSON; return `fallback` on missing or corrupt.
58
+ *
59
+ * @template T
60
+ * @param {string} filePath
61
+ * @param {T} fallback
62
+ * @returns {T | object | Array<any>}
63
+ */
64
+ function readJsonSafeSync(filePath, fallback) {
65
+ if (!fs.existsSync(filePath)) return fallback;
66
+ try {
67
+ const raw = fs.readFileSync(filePath, 'utf-8');
68
+ if (!raw.trim()) return fallback;
69
+ return JSON.parse(raw);
70
+ } catch {
71
+ return fallback;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Convenience: stringify + atomic write. Pretty-printed for human-edit
77
+ * friendliness (matches the existing JSON.stringify(..., null, 2) calls).
78
+ *
79
+ * @param {string} filePath
80
+ * @param {any} value
81
+ * @param {{mode?: number}} [opts] e.g. {mode: 0o600} for secrets/PII
82
+ */
83
+ function writeJsonAtomicSync(filePath, value, opts = {}) {
84
+ writeFileAtomicSync(filePath, JSON.stringify(value, null, 2), opts);
85
+ }
86
+
87
+ module.exports = {
88
+ writeFileAtomicSync,
89
+ writeJsonAtomicSync,
90
+ readJsonSafeSync,
91
+ };
File without changes
File without changes
File without changes
File without changes
package/src/utils/cron.js CHANGED
File without changes
@@ -18,9 +18,10 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const os = require('os');
20
20
  const url = require('url');
21
+ const { getDashboardPort, getDashboardHost } = require('./ports');
21
22
 
22
- const PORT = 7421;
23
- const HOST = '127.0.0.1';
23
+ const PORT = getDashboardPort();
24
+ const HOST = getDashboardHost();
24
25
 
25
26
  const NATURECO_DIR = path.join(os.homedir(), '.natureco');
26
27
 
@@ -9,6 +9,10 @@
9
9
  * - Error codes
10
10
  */
11
11
 
12
+ // checkMacPermission below uses os.platform() — was relying on a global
13
+ // `os` from another module's load order.
14
+ const os = require('os');
15
+
12
16
  class ToolError extends Error {
13
17
  constructor(message, code = "TOOL_ERROR", details = {}) {
14
18
  super(message);
File without changes
File without changes
File without changes
File without changes