natureco-cli 5.19.0 → 5.20.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 (196) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/package.json +1 -1
  3. package/skills/a-b-testing/SKILL.md +34 -0
  4. package/skills/accessibility-audit/SKILL.md +34 -0
  5. package/skills/agent-memory/SKILL.md +34 -0
  6. package/skills/agent-orchestration/SKILL.md +34 -0
  7. package/skills/analytics-setup/SKILL.md +34 -0
  8. package/skills/api-design/SKILL.md +34 -0
  9. package/skills/api-versioning/SKILL.md +34 -0
  10. package/skills/audio-transcription/SKILL.md +34 -0
  11. package/skills/authentication-patterns/SKILL.md +34 -0
  12. package/skills/authorization-rbac/SKILL.md +34 -0
  13. package/skills/backup-strategy/SKILL.md +34 -0
  14. package/skills/bash-scripting/SKILL.md +34 -0
  15. package/skills/batch-api-calls/SKILL.md +34 -0
  16. package/skills/batch-processing/SKILL.md +34 -0
  17. package/skills/bulk-file-operations/SKILL.md +34 -0
  18. package/skills/cache-responses/SKILL.md +34 -0
  19. package/skills/caching-strategy/SKILL.md +34 -0
  20. package/skills/calendar-optimization/SKILL.md +34 -0
  21. package/skills/changelog-generation/SKILL.md +34 -0
  22. package/skills/chat-bot-design/SKILL.md +34 -0
  23. package/skills/chunking-strategy/SKILL.md +34 -0
  24. package/skills/ci-cd-pipeline/SKILL.md +34 -0
  25. package/skills/classification-pipeline/SKILL.md +34 -0
  26. package/skills/clipboard-master/SKILL.md +34 -0
  27. package/skills/code-explanation/SKILL.md +34 -0
  28. package/skills/code-generation/SKILL.md +34 -0
  29. package/skills/code-migration/SKILL.md +34 -0
  30. package/skills/code-review-checklist/SKILL.md +34 -0
  31. package/skills/contact-management/SKILL.md +34 -0
  32. package/skills/content-generation/SKILL.md +34 -0
  33. package/skills/context-pruning/SKILL.md +34 -0
  34. package/skills/context-window-management/SKILL.md +34 -0
  35. package/skills/continuous-improvement/SKILL.md +34 -0
  36. package/skills/cron-scheduling/SKILL.md +34 -0
  37. package/skills/cross-platform-compat/SKILL.md +34 -0
  38. package/skills/csv-processing/SKILL.md +34 -0
  39. package/skills/daily-journal/SKILL.md +34 -0
  40. package/skills/data-analysis/SKILL.md +34 -0
  41. package/skills/data-backup/SKILL.md +34 -0
  42. package/skills/data-cleaning/SKILL.md +34 -0
  43. package/skills/data-extraction/SKILL.md +34 -0
  44. package/skills/data-validation/SKILL.md +34 -0
  45. package/skills/database-migrations/SKILL.md +34 -0
  46. package/skills/database-schema-design/SKILL.md +34 -0
  47. package/skills/decision-matrix/SKILL.md +34 -0
  48. package/skills/dependency-management/SKILL.md +34 -0
  49. package/skills/dev-environment-setup/SKILL.md +34 -0
  50. package/skills/distraction-blocker/SKILL.md +34 -0
  51. package/skills/docker-optimization/SKILL.md +34 -0
  52. package/skills/document-template/SKILL.md +34 -0
  53. package/skills/documentation-gen/SKILL.md +34 -0
  54. package/skills/efficient-formatting/SKILL.md +34 -0
  55. package/skills/email-management/SKILL.md +34 -0
  56. package/skills/email-service/SKILL.md +34 -0
  57. package/skills/embedding-generation/SKILL.md +34 -0
  58. package/skills/entity-extraction/SKILL.md +34 -0
  59. package/skills/environment-config/SKILL.md +34 -0
  60. package/skills/error-handling/SKILL.md +34 -0
  61. package/skills/estimation-techniques/SKILL.md +34 -0
  62. package/skills/event-driven-architecture/SKILL.md +34 -0
  63. package/skills/excel-automation/SKILL.md +34 -0
  64. package/skills/execution-monitoring/SKILL.md +34 -0
  65. package/skills/expense-tracking/SKILL.md +34 -0
  66. package/skills/feature-engineering/SKILL.md +34 -0
  67. package/skills/feature-flags/SKILL.md +34 -0
  68. package/skills/file-conversion/SKILL.md +34 -0
  69. package/skills/file-upload-handling/SKILL.md +34 -0
  70. package/skills/fine-tuning-prep/SKILL.md +34 -0
  71. package/skills/focus-mode/SKILL.md +34 -0
  72. package/skills/form-validation/SKILL.md +34 -0
  73. package/skills/function-calling/SKILL.md +34 -0
  74. package/skills/git-workflow/SKILL.md +34 -0
  75. package/skills/goal-setting/SKILL.md +34 -0
  76. package/skills/graphql-design/SKILL.md +34 -0
  77. package/skills/habit-tracker/SKILL.md +34 -0
  78. package/skills/hallucination-detection/SKILL.md +34 -0
  79. package/skills/image-processing/SKILL.md +34 -0
  80. package/skills/json-transformation/SKILL.md +34 -0
  81. package/skills/keyboard-shortcuts/SKILL.md +34 -0
  82. package/skills/knowledge-base/SKILL.md +34 -0
  83. package/skills/knowledge-graph/SKILL.md +34 -0
  84. package/skills/kubernetes-deployment/SKILL.md +34 -0
  85. package/skills/language-translation/SKILL.md +34 -0
  86. package/skills/llm-chaining/SKILL.md +34 -0
  87. package/skills/localization-i18n/SKILL.md +34 -0
  88. package/skills/logging-best-practices/SKILL.md +34 -0
  89. package/skills/machine-learning-pipeline/SKILL.md +34 -0
  90. package/skills/meeting-notes/SKILL.md +34 -0
  91. package/skills/meeting-scheduler/SKILL.md +34 -0
  92. package/skills/message-queues/SKILL.md +34 -0
  93. package/skills/message-summarization/SKILL.md +34 -0
  94. package/skills/microservices-architecture/SKILL.md +34 -0
  95. package/skills/milestone-tracking/SKILL.md +34 -0
  96. package/skills/model-deployment/SKILL.md +34 -0
  97. package/skills/model-evaluation/SKILL.md +34 -0
  98. package/skills/monitoring-setup/SKILL.md +34 -0
  99. package/skills/monorepo-setup/SKILL.md +34 -0
  100. package/skills/multi-agent-systems/SKILL.md +34 -0
  101. package/skills/network-troubleshooting/SKILL.md +34 -0
  102. package/skills/note-organization/SKILL.md +34 -0
  103. package/skills/package-publishing/SKILL.md +34 -0
  104. package/skills/password-generator/SKILL.md +34 -0
  105. package/skills/pdf-processing/SKILL.md +34 -0
  106. package/skills/performance-optimization/SKILL.md +34 -0
  107. package/skills/pomodoro-timer/SKILL.md +34 -0
  108. package/skills/powershell-automation/SKILL.md +34 -0
  109. package/skills/priority-filtering/SKILL.md +34 -0
  110. package/skills/project-tracking/SKILL.md +34 -0
  111. package/skills/prompt-compression/SKILL.md +34 -0
  112. package/skills/prompt-engineering/SKILL.md +34 -0
  113. package/skills/push-notifications/SKILL.md +34 -0
  114. package/skills/rag-implementation/SKILL.md +34 -0
  115. package/skills/rate-limiting/SKILL.md +34 -0
  116. package/skills/real-time-communication/SKILL.md +34 -0
  117. package/skills/refactoring-strategy/SKILL.md +34 -0
  118. package/skills/regex-mastery/SKILL.md +34 -0
  119. package/skills/report-generation/SKILL.md +34 -0
  120. package/skills/research-compiler/SKILL.md +34 -0
  121. package/skills/responsive-design/SKILL.md +34 -0
  122. package/skills/rest-api-patterns/SKILL.md +34 -0
  123. package/skills/retrospectives/SKILL.md +34 -0
  124. package/skills/risk-assessment/SKILL.md +34 -0
  125. package/skills/roadmap-creation/SKILL.md +34 -0
  126. package/skills/search-implementation/SKILL.md +34 -0
  127. package/skills/secrets-management/SKILL.md +34 -0
  128. package/skills/security-audit/SKILL.md +34 -0
  129. package/skills/selective-memory/SKILL.md +34 -0
  130. package/skills/semantic-search/SKILL.md +34 -0
  131. package/skills/semantic-versioning/SKILL.md +34 -0
  132. package/skills/sentiment-analysis/SKILL.md +34 -0
  133. package/skills/seo-optimization/SKILL.md +34 -0
  134. package/skills/snippet-manager/SKILL.md +34 -0
  135. package/skills/sprint-planning/SKILL.md +34 -0
  136. package/skills/sql-query-optimization/SKILL.md +34 -0
  137. package/skills/stakeholder-communication/SKILL.md +34 -0
  138. package/skills/state-management/SKILL.md +34 -0
  139. package/skills/statistical-analysis/SKILL.md +34 -0
  140. package/skills/streaming-responses/SKILL.md +34 -0
  141. package/skills/structured-output/SKILL.md +34 -0
  142. package/skills/summarization-techniques/SKILL.md +34 -0
  143. package/skills/system-diagnostics/SKILL.md +34 -0
  144. package/skills/task-decomposition/SKILL.md +34 -0
  145. package/skills/task-prioritization/SKILL.md +34 -0
  146. package/skills/template-library/SKILL.md +34 -0
  147. package/skills/test-generation/SKILL.md +34 -0
  148. package/skills/testing-strategy/SKILL.md +34 -0
  149. package/skills/text-expansion/SKILL.md +34 -0
  150. package/skills/time-blocking/SKILL.md +34 -0
  151. package/skills/token-budgeting/SKILL.md +34 -0
  152. package/skills/token-optimization/SKILL.md +34 -0
  153. package/skills/tool-calling-patterns/SKILL.md +34 -0
  154. package/skills/vector-database-setup/SKILL.md +34 -0
  155. package/skills/video-editing/SKILL.md +34 -0
  156. package/skills/visualization-creation/SKILL.md +34 -0
  157. package/skills/web-scraping/SKILL.md +34 -0
  158. package/skills/webhook-handling/SKILL.md +34 -0
  159. package/skills/weekly-review/SKILL.md +34 -0
  160. package/skills/workflow-automation/SKILL.md +34 -0
  161. package/src/commands/code_v5.js +377 -73
  162. package/src/commands/help.js +3 -1
  163. package/src/commands/naturehub.js +105 -55
  164. package/src/commands/repl.js +506 -87
  165. package/src/providers/model/anthropic.js +84 -0
  166. package/src/providers/model/gemini.js +49 -0
  167. package/src/providers/model/minimax.js +48 -0
  168. package/src/providers/model/ollama.js +42 -0
  169. package/src/providers/model/openai.js +49 -0
  170. package/src/providers/search/duckduckgo.js +45 -0
  171. package/src/providers/search/exa.js +52 -0
  172. package/src/providers/search/searxng.js +51 -0
  173. package/src/providers/search/tavily.js +47 -0
  174. package/src/tools/model_provider.js +71 -0
  175. package/src/tools/parallel_search.js +40 -49
  176. package/src/tools/search_provider.js +79 -0
  177. package/src/tools/web_search.js +42 -62
  178. package/src/tools/workflow.js +27 -10
  179. package/src/utils/cron.js +82 -126
  180. package/src/utils/effort-levels.js +42 -0
  181. package/src/utils/fallback-chain.js +72 -0
  182. package/src/utils/file-history.js +78 -0
  183. package/src/utils/model-provider.js +113 -0
  184. package/src/utils/permissions.js +147 -0
  185. package/src/utils/plan-mode.js +164 -0
  186. package/src/utils/provider-detect.js +7 -32
  187. package/src/utils/sandbox.js +60 -0
  188. package/src/utils/search-provider.js +62 -0
  189. package/src/utils/session-search.js +66 -0
  190. package/src/utils/structured-output.js +20 -0
  191. package/src/utils/system-prompt.js +28 -2
  192. package/src/utils/tasks.js +132 -0
  193. package/src/utils/tool-hooks.js +154 -0
  194. package/src/utils/tools.js +16 -0
  195. package/src/utils/ultra-review.js +59 -0
  196. package/src/utils/worktree.js +192 -0
@@ -0,0 +1,78 @@
1
+ /**
2
+ * file-history — File change history with undo support
3
+ *
4
+ * Snapshots files before edits and stores them in .natureco/history/
5
+ * Supports undo via `file-restore` CLI command.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const HISTORY_DIR = path.join(process.cwd(), '.natureco', 'history');
13
+ const MAX_SNAPSHOTS_PER_FILE = 20;
14
+
15
+ function ensureDir() {
16
+ fs.mkdirSync(HISTORY_DIR, { recursive: true });
17
+ }
18
+
19
+ function snapshotKey(filePath) {
20
+ const rel = path.relative(process.cwd(), path.resolve(filePath));
21
+ return rel.replace(/\//g, '__');
22
+ }
23
+
24
+ function getHistory(filePath) {
25
+ const key = snapshotKey(filePath);
26
+ const dir = path.join(HISTORY_DIR, key);
27
+ if (!fs.existsSync(dir)) return [];
28
+ return fs.readdirSync(dir)
29
+ .filter(f => f.endsWith('.snap'))
30
+ .map(f => {
31
+ const ts = parseInt(f.replace('.snap', ''), 10);
32
+ const snapPath = path.join(dir, f);
33
+ return { timestamp: ts, size: fs.statSync(snapPath).size, file: f };
34
+ })
35
+ .sort((a, b) => b.timestamp - a.timestamp);
36
+ }
37
+
38
+ function snapshot(filePath, content) {
39
+ ensureDir();
40
+ const key = snapshotKey(filePath);
41
+ const dir = path.join(HISTORY_DIR, key);
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ fs.writeFileSync(path.join(dir, `${Date.now()}.snap`), content || '');
44
+
45
+ // Prune old snapshots
46
+ const snaps = fs.readdirSync(dir).filter(f => f.endsWith('.snap')).sort();
47
+ while (snaps.length > MAX_SNAPSHOTS_PER_FILE) {
48
+ const old = snaps.shift();
49
+ try { fs.unlinkSync(path.join(dir, old)); } catch {}
50
+ }
51
+ }
52
+
53
+ function restore(filePath, timestamp) {
54
+ const key = snapshotKey(filePath);
55
+ const snapPath = path.join(HISTORY_DIR, key, `${timestamp}.snap`);
56
+ if (!fs.existsSync(snapPath)) return { error: 'Snapshot not found' };
57
+ const content = fs.readFileSync(snapPath, 'utf8');
58
+ fs.writeFileSync(path.resolve(filePath), content);
59
+ return { restored: true, filePath, timestamp, size: content.length };
60
+ }
61
+
62
+ function listAll() {
63
+ if (!fs.existsSync(HISTORY_DIR)) return [];
64
+ const results = [];
65
+ for (const entry of fs.readdirSync(HISTORY_DIR)) {
66
+ const dir = path.join(HISTORY_DIR, entry);
67
+ if (fs.statSync(dir).isDirectory()) {
68
+ const snaps = fs.readdirSync(dir).filter(f => f.endsWith('.snap'));
69
+ if (snaps.length > 0) {
70
+ const filePath = entry.replace(/__/g, '/');
71
+ results.push({ file: filePath, snapshots: snaps.length, latest: Math.max(...snaps.map(s => parseInt(s, 10))) });
72
+ }
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+
78
+ module.exports = { snapshot, restore, getHistory, listAll };
@@ -0,0 +1,113 @@
1
+ const _registry = new Map();
2
+ const _instances = new Map();
3
+
4
+ const PROVIDER_CONFIG_KEY = 'modelProvider';
5
+ const DEFAULT_PROVIDER = 'auto';
6
+
7
+ function registerProvider(name, providerClass) {
8
+ _registry.set(name, providerClass);
9
+ }
10
+
11
+ function getProviderNames() {
12
+ return Array.from(_registry.keys());
13
+ }
14
+
15
+ function getProvider(name) {
16
+ return _registry.get(name) || null;
17
+ }
18
+
19
+ function detectFamily(providerUrl, model) {
20
+ const url = (providerUrl || '').toLowerCase();
21
+ const m = (model || '').toLowerCase();
22
+ if (url.includes('anthropic.com') || m.includes('claude')) return 'anthropic';
23
+ if (url.includes('groq.com') || m.includes('groq') || m.includes('llama-3') || m.includes('mixtral')) return 'groq';
24
+ if (url.includes('openrouter.ai')) return 'openrouter';
25
+ if (url.includes('api.deepseek.com') || m.includes('deepseek')) return 'deepseek';
26
+ if (url.includes('mistral.ai') || m.includes('mistral') || m.includes('codestral')) return 'mistral';
27
+ if (url.includes('together.xyz') || m.includes('together')) return 'together';
28
+ if (url.includes('fireworks.ai') || m.includes('fireworks')) return 'fireworks';
29
+ if (url.includes('perplexity.ai') || m.includes('pplx') || m.includes('sonar')) return 'perplexity';
30
+ if (url.includes('localhost') || url.includes('127.0.0.1') || url.includes('ollama')) return 'ollama';
31
+ if (url.includes('minimax.io') || url.includes('minimax')) return 'minimax';
32
+ if (url.includes('generativelanguage.googleapis.com') || url.includes('gemini')) return 'gemini';
33
+ return 'openai';
34
+ }
35
+
36
+ function getProviderFamily(name) {
37
+ const openaiCompat = ['openai', 'groq', 'openrouter', 'deepseek', 'mistral', 'together', 'fireworks', 'perplexity'];
38
+ if (openaiCompat.includes(name)) return 'openai';
39
+ return name;
40
+ }
41
+
42
+ function resolveProviderConfig(cfg) {
43
+ const env = process.env.NATURECO_MODEL_PROVIDER || '';
44
+ if (env && _registry.has(env)) return env;
45
+ if (cfg && cfg[PROVIDER_CONFIG_KEY] && _registry.has(cfg[PROVIDER_CONFIG_KEY])) {
46
+ return cfg[PROVIDER_CONFIG_KEY];
47
+ }
48
+ const family = detectFamily(cfg?.providerUrl, cfg?.providerModel);
49
+ const resolved = getProviderFamily(family);
50
+ if (_registry.has(resolved)) return resolved;
51
+ return DEFAULT_PROVIDER;
52
+ }
53
+
54
+ function getActiveProvider(cfg) {
55
+ const name = resolveProviderConfig(cfg);
56
+ if (name === 'auto') {
57
+ const family = detectFamily(cfg?.providerUrl, cfg?.providerModel);
58
+ const familyGroup = getProviderFamily(family);
59
+ const Cls = _registry.get(familyGroup);
60
+ if (Cls) return Cls;
61
+ }
62
+ const Cls = _registry.get(name);
63
+ if (!Cls) return getProvider('openai') || null;
64
+ return Cls;
65
+ }
66
+
67
+ const DEFAULT_ANTHROPIC_SYSTEM = 'You are a helpful AI assistant running inside the natureco CLI.';
68
+
69
+ class ModelProvider {
70
+ constructor(config = {}) {
71
+ this.config = config;
72
+ this.name = 'base';
73
+ }
74
+
75
+ getEndpoint() {
76
+ throw new Error('Not implemented: getEndpoint');
77
+ }
78
+
79
+ buildRequest(messages, options = {}) {
80
+ throw new Error('Not implemented: buildRequest');
81
+ }
82
+
83
+ parseResponse(data) {
84
+ throw new Error('Not implemented: parseResponse');
85
+ }
86
+
87
+ normalizeTools(tools) {
88
+ return tools || [];
89
+ }
90
+
91
+ parseToolCalls(message) {
92
+ return message?.tool_calls || [];
93
+ }
94
+
95
+ extractSystem(messages) {
96
+ const systemMsg = messages.find(m => m.role === 'system');
97
+ return systemMsg?.content || '';
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ ModelProvider,
103
+ registerProvider,
104
+ getProviderNames,
105
+ getProvider,
106
+ detectFamily,
107
+ getProviderFamily,
108
+ resolveProviderConfig,
109
+ getActiveProvider,
110
+ PROVIDER_CONFIG_KEY,
111
+ DEFAULT_PROVIDER,
112
+ DEFAULT_ANTHROPIC_SYSTEM,
113
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * permissions — Granular tool-level allow/deny rules (Claude Code style)
3
+ *
4
+ * Config format (~/.natureco/config.json or .natureco/config.json):
5
+ * "permissions": {
6
+ * "Read(~/.ssh/**)": { "action": "deny", "reason": "SSH keys are sensitive" },
7
+ * "Read(~/.aws/**)": "deny",
8
+ * "Edit(.env*)": { "action": "ask", "reason": "Review env changes" },
9
+ * "Bash(npm *)": "ask",
10
+ * "Bash(rm *rf *)": "deny"
11
+ * }
12
+ *
13
+ * Resolution order (first match wins):
14
+ * 1. Config permissions (deny > ask > allow)
15
+ * 2. Built-in high-risk patterns (assessRisk in code_v5)
16
+ * 3. Default: allow
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+
23
+ const { matchGlob, flattenArgs } = require('./tool-hooks');
24
+
25
+ function loadPermissionRules() {
26
+ const rules = [];
27
+ const sources = [
28
+ path.join(os.homedir(), '.natureco', 'config.json'),
29
+ path.join(process.cwd(), '.natureco', 'config.json'),
30
+ ];
31
+ for (const cfgPath of sources) {
32
+ try {
33
+ if (fs.existsSync(cfgPath)) {
34
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
35
+ if (cfg.permissions) {
36
+ for (const [pattern, value] of Object.entries(cfg.permissions)) {
37
+ const parsed = parsePermissionRule(pattern, value);
38
+ if (parsed) rules.push(parsed);
39
+ }
40
+ }
41
+ }
42
+ } catch {}
43
+ }
44
+ return rules;
45
+ }
46
+
47
+ function parsePermissionRule(pattern, value) {
48
+ const match = pattern.match(/^(\*|[a-zA-Z_]+)\((.+)\)$/);
49
+ if (!match && pattern !== '*') return null;
50
+ const toolName = match ? match[1] : '*';
51
+ const glob = match ? match[2] : '*';
52
+
53
+ let action, reason;
54
+ if (typeof value === 'string') {
55
+ action = value;
56
+ } else if (value && typeof value === 'object') {
57
+ action = value.action || 'ask';
58
+ reason = value.reason;
59
+ }
60
+ if (!['allow', 'deny', 'ask'].includes(action)) action = 'ask';
61
+
62
+ return { toolName, glob, action, reason, raw: pattern };
63
+ }
64
+
65
+ /**
66
+ * Check permissions for a tool call.
67
+ * Returns { action: 'allow'|'deny'|'ask', reason, rule }
68
+ */
69
+ function checkPermission(toolName, args) {
70
+ const rules = loadPermissionRules();
71
+ const flat = flattenArgs(args);
72
+
73
+ for (const rule of rules) {
74
+ if (rule.toolName !== '*' && rule.toolName !== toolName) continue;
75
+ const matchesGlob = rule.glob === '*' || matchGlob(flat, rule.glob) || matchGlob(toolName, rule.glob);
76
+ if (!matchesGlob) continue;
77
+ if (rule.action === 'deny') {
78
+ return { action: 'deny', reason: rule.reason || `Engellendi: ${rule.raw}`, rule };
79
+ }
80
+ if (rule.action === 'ask') {
81
+ return { action: 'ask', reason: rule.reason || `Onay gerekli: ${rule.raw}`, rule };
82
+ }
83
+ if (rule.action === 'allow') {
84
+ return { action: 'allow', reason: '', rule };
85
+ }
86
+ }
87
+
88
+ return { action: 'allow', reason: '', rule: null };
89
+ }
90
+
91
+ /**
92
+ * Persistent permission approvals (session + disk cache).
93
+ */
94
+ const _permSessionCache = new Map();
95
+
96
+ function loadPersistentApprovals() {
97
+ try {
98
+ const f = path.join(os.homedir(), '.natureco', 'perm-approvals.json');
99
+ if (fs.existsSync(f)) return JSON.parse(fs.readFileSync(f, 'utf8'));
100
+ } catch {}
101
+ return {};
102
+ }
103
+
104
+ function savePersistentApproval(key) {
105
+ try {
106
+ const f = path.join(os.homedir(), '.natureco', 'perm-approvals.json');
107
+ const data = loadPersistentApprovals();
108
+ data[key] = { approved: true, at: Date.now() };
109
+ const dir = path.dirname(f);
110
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
111
+ fs.writeFileSync(f, JSON.stringify(data, null, 2), { mode: 0o600 });
112
+ } catch {}
113
+ }
114
+
115
+ function isApproved(key) {
116
+ if (_permSessionCache.get(key) === true) return true;
117
+ const persistent = loadPersistentApprovals();
118
+ if (persistent[key]?.approved) {
119
+ _permSessionCache.set(key, true);
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+
125
+ function markApproved(key, persistent = false) {
126
+ _permSessionCache.set(key, true);
127
+ if (persistent) savePersistentApproval(key);
128
+ }
129
+
130
+ /**
131
+ * Format a tool call for permission display.
132
+ */
133
+ function formatPermissionPrompt(toolName, args, reason) {
134
+ const parts = Object.entries(args || {}).map(([k, v]) => {
135
+ const s = typeof v === 'string' ? v : JSON.stringify(v);
136
+ return s.length > 80 ? s.slice(0, 77) + '...' : s;
137
+ });
138
+ return `${toolName}(${parts.join(', ')})`;
139
+ }
140
+
141
+ module.exports = {
142
+ checkPermission,
143
+ loadPermissionRules,
144
+ isApproved,
145
+ markApproved,
146
+ formatPermissionPrompt,
147
+ };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * plan-mode — Plan Mode (Claude Code EnterPlanMode/ExitPlanMode)
3
+ *
4
+ * Plan mode lets the model research, explore, and create a plan
5
+ * without making any changes. All write/destructive operations are blocked.
6
+ *
7
+ * Tools:
8
+ * EnterPlanMode — switch to plan-only mode
9
+ * ExitPlanMode — exit plan mode, present plan to user
10
+ *
11
+ * State transitions:
12
+ * NORMAL → (EnterPlanMode) → PLANNING → (ExitPlanMode) → REVIEW → (approve) → NORMAL
13
+ * → (reject) → PLANNING
14
+ */
15
+
16
+ const STATE = {
17
+ NORMAL: 'normal',
18
+ PLANNING: 'planning',
19
+ REVIEW: 'review',
20
+ };
21
+
22
+ class PlanMode {
23
+ constructor() {
24
+ this.state = STATE.NORMAL;
25
+ this.plan = null;
26
+ this.steps = [];
27
+ this.filesRead = [];
28
+ this.commandsRun = [];
29
+ this.planHistory = [];
30
+ }
31
+
32
+ enter() {
33
+ if (this.state !== STATE.NORMAL) return false;
34
+ this.state = STATE.PLANNING;
35
+ this.plan = null;
36
+ this.steps = [];
37
+ this.filesRead = [];
38
+ this.commandsRun = [];
39
+ return true;
40
+ }
41
+
42
+ exit(planContent) {
43
+ if (this.state !== STATE.PLANNING) return false;
44
+ this.plan = planContent;
45
+ this.state = STATE.REVIEW;
46
+ this.planHistory.push({
47
+ plan: planContent,
48
+ steps: [...this.steps],
49
+ at: Date.now(),
50
+ });
51
+ return true;
52
+ }
53
+
54
+ approve() {
55
+ if (this.state !== STATE.REVIEW) return false;
56
+ this.state = STATE.NORMAL;
57
+ return true;
58
+ }
59
+
60
+ reject() {
61
+ if (this.state !== STATE.REVIEW) return false;
62
+ this.state = STATE.PLANNING;
63
+ return true;
64
+ }
65
+
66
+ isPlanning() {
67
+ return this.state === STATE.PLANNING;
68
+ }
69
+
70
+ inReview() {
71
+ return this.state === STATE.REVIEW;
72
+ }
73
+
74
+ isNormal() {
75
+ return this.state === STATE.NORMAL;
76
+ }
77
+
78
+ getSystemPrompt() {
79
+ if (this.state === STATE.PLANNING) {
80
+ return [
81
+ 'You are in PLAN MODE. You MUST NOT make any changes to files or execute destructive commands.',
82
+ 'You can: read files, search code, browse the web, explore the codebase.',
83
+ 'You MUST NOT: write/edit files, run git commands that change history, install packages, or execute destructive bash commands.',
84
+ 'Create a detailed step-by-step plan. When ready, use ExitPlanMode to present your plan.',
85
+ '',
86
+ 'Plan format (markdown):',
87
+ '## Plan: <title>',
88
+ '### Step 1: <action>',
89
+ ' - Files: <paths>',
90
+ ' - Changes: <description>',
91
+ '### Step 2: ...',
92
+ ].join('\n');
93
+ }
94
+ return '';
95
+ }
96
+
97
+ /**
98
+ * Check if a tool is allowed in current mode.
99
+ * Returns { allowed, reason }
100
+ */
101
+ checkTool(toolName, args) {
102
+ if (this.state !== STATE.PLANNING) return { allowed: true };
103
+
104
+ const writeTools = new Set([
105
+ 'write_file', 'edit_file', 'edit', 'write',
106
+ 'file_write', 'file_edit', 'rename_file', 'delete_file',
107
+ 'move_file', 'copy_file', 'create_file',
108
+ ]);
109
+ const destructiveBash = [
110
+ 'rm', 'mv', 'cp', 'dd', 'mkfs', 'format', '>', '>>', '|',
111
+ 'chmod', 'chown', 'ln', 'git push', 'git commit', 'git merge',
112
+ 'git rebase', 'git reset', 'npm publish', 'npm install',
113
+ 'pip install', 'brew install', 'apt', 'yum', 'dnf',
114
+ ];
115
+
116
+ if (writeTools.has(toolName)) {
117
+ return { allowed: false, reason: 'Plan modunda dosya değişikliği yasaktır. Önce ExitPlanMode ile çıkıp onay alın.' };
118
+ }
119
+
120
+ if (toolName === 'bash' || toolName === 'execute_command') {
121
+ const cmd = (args.command || args.cmd || '').trim();
122
+ for (const pattern of destructiveBash) {
123
+ if (cmd.includes(pattern)) {
124
+ return { allowed: false, reason: `Plan modunda yasaklı komut: ${pattern}` };
125
+ }
126
+ }
127
+ }
128
+
129
+ if (toolName === 'browser') {
130
+ return { allowed: false, reason: 'Plan modunda browser yasaktır.' };
131
+ }
132
+
133
+ return { allowed: true };
134
+ }
135
+
136
+ /**
137
+ * Record a read/search tool call for the plan summary.
138
+ */
139
+ recordTool(toolName, args) {
140
+ if (this.state === STATE.PLANNING) {
141
+ if (toolName === 'read_file' || toolName === 'file_search' || toolName === 'grep_search') {
142
+ const target = args.filePath || args.pattern || args.path || '';
143
+ if (target && !this.filesRead.includes(target)) {
144
+ this.filesRead.push(target);
145
+ }
146
+ }
147
+ if (toolName === 'bash' || toolName === 'execute_command') {
148
+ const cmd = (args.command || args.cmd || '').trim();
149
+ if (cmd && !this.commandsRun.includes(cmd)) {
150
+ this.commandsRun.push(cmd);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ // Singleton
158
+ let _instance = null;
159
+ function getPlanMode() {
160
+ if (!_instance) _instance = new PlanMode();
161
+ return _instance;
162
+ }
163
+
164
+ module.exports = { PlanMode, getPlanMode, STATE };
@@ -2,46 +2,21 @@
2
2
  * Provider detection — single source of truth for "given a base URL
3
3
  * (and optionally a model name) which provider family is this?"
4
4
  *
5
- * Previously this logic lived inline in three places:
6
- * - src/utils/api.js (the canonical detectProvider, exported only
7
- * informally via its module surface)
8
- * - src/commands/setup.js (string contains checks against minimax /
9
- * anthropic / groq, scattered through the OAuth + test flows)
10
- * - src/utils/api.js again, with `const isMiniMax = baseUrl.includes(
11
- * 'minimax.io') || baseUrl.includes('minimaxi.com') || baseUrl.includes(
12
- * 'minimax.cn')` repeated twice (lines 560 + 1017)
13
- *
14
- * Refactoring opportunity from the v5.7 audit. This module preserves
15
- * EXACTLY the same detection rules as the original detectProvider so
16
- * the migration is a behavioral no-op verifiable by tests.
5
+ * v5.22.0: Delegates real detection to model-provider.js's detectFamily.
6
+ * Keeps convenience predicates (isAnthropic, isMiniMax, etc.) and
7
+ * buildChatEndpoint for backward compatibility.
17
8
  */
18
9
 
19
- /**
20
- * Family of an inference provider.
21
- *
22
- * @typedef {'openai'|'anthropic'|'groq'|'openrouter'|'deepseek'|'mistral'
23
- * |'together'|'fireworks'|'perplexity'|'ollama'|'minimax'} ProviderName
24
- */
10
+ const { detectFamily } = require('./model-provider');
25
11
 
26
12
  /**
13
+ * Canonical detection — delegates to model-provider.
27
14
  * @param {string} providerUrl
28
15
  * @param {string} [model]
29
- * @returns {ProviderName}
16
+ * @returns {string}
30
17
  */
31
18
  function detectProvider(providerUrl, model) {
32
- const url = (providerUrl || '').toLowerCase();
33
- const m = (model || '').toLowerCase();
34
- if (url.includes('anthropic.com') || m.includes('claude')) return 'anthropic';
35
- if (url.includes('groq.com') || m.includes('groq') || m.includes('llama-3') || m.includes('mixtral')) return 'groq';
36
- if (url.includes('openrouter.ai')) return 'openrouter';
37
- if (url.includes('api.deepseek.com') || m.includes('deepseek')) return 'deepseek';
38
- if (url.includes('mistral.ai') || m.includes('mistral') || m.includes('codestral')) return 'mistral';
39
- if (url.includes('together.xyz') || m.includes('together')) return 'together';
40
- if (url.includes('fireworks.ai') || m.includes('fireworks')) return 'fireworks';
41
- if (url.includes('perplexity.ai') || m.includes('pplx') || m.includes('sonar')) return 'perplexity';
42
- if (url.includes('localhost') || url.includes('127.0.0.1') || url.includes('ollama')) return 'ollama';
43
- if (url.includes('minimax.io') || url.includes('minimax')) return 'minimax';
44
- return 'openai';
19
+ return detectFamily(providerUrl, model);
45
20
  }
46
21
 
47
22
  /** Convenience predicates — each one short-circuits on hostname only. */
@@ -0,0 +1,60 @@
1
+ /**
2
+ * sandbox — Lightweight command execution sandbox
3
+ *
4
+ * Levels: none | basic | strict
5
+ *
6
+ * - none: no restrictions
7
+ * - basic: timeout + cwd restriction
8
+ * - strict: timeout + cwd + no-network + env cleanup
9
+ */
10
+
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const SANDBOX_LEVELS = { none: 0, basic: 1, strict: 2 };
15
+
16
+ function getLevel(cfg = {}) {
17
+ const level = (cfg.sandbox || cfg.sandboxLevel || 'none').toLowerCase();
18
+ return SANDBOX_LEVELS[level] !== undefined ? level : 'none';
19
+ }
20
+
21
+ function isPathAllowed(targetPath, cwd) {
22
+ if (!targetPath) return true;
23
+ const abs = path.resolve(targetPath);
24
+ // Always allow /tmp and project directory
25
+ if (abs.startsWith('/tmp/') || abs.startsWith('/private/tmp/')) return true;
26
+ if (cwd && abs.startsWith(path.resolve(cwd))) return true;
27
+ return false;
28
+ }
29
+
30
+ function isNetworkCommand(cmd) {
31
+ const networkPatterns = [
32
+ 'curl ', 'wget ', 'nc ', 'ncat ', 'ssh ', 'scp ', 'rsync',
33
+ 'pip install', 'npm install', 'yarn add', 'pnpm add',
34
+ 'apt-get', 'apt ', 'yum ', 'dnf ', 'brew install',
35
+ 'git clone', 'git fetch', 'git pull',
36
+ 'docker pull', 'docker run',
37
+ ];
38
+ const lower = cmd.toLowerCase();
39
+ return networkPatterns.some(p => lower.includes(p));
40
+ }
41
+
42
+ function getSandboxEnv(level) {
43
+ if (level === 'strict') {
44
+ return {
45
+ PATH: process.env.PATH || '',
46
+ HOME: os.homedir(),
47
+ TMPDIR: os.tmpdir(),
48
+ NODE_ENV: 'development',
49
+ };
50
+ }
51
+ return undefined; // Use parent env
52
+ }
53
+
54
+ function getTimeout(level, defaultTimeout = 30000) {
55
+ if (level === 'strict') return 10000;
56
+ if (level === 'basic') return defaultTimeout;
57
+ return 0; // no timeout
58
+ }
59
+
60
+ module.exports = { getLevel, isPathAllowed, isNetworkCommand, getSandboxEnv, getTimeout, SANDBOX_LEVELS };
@@ -0,0 +1,62 @@
1
+ const PROVIDER_CONFIG_KEY = 'searchProvider';
2
+ const DEFAULT_PROVIDER = 'tavily';
3
+
4
+ const _registry = new Map();
5
+
6
+ function registerProvider(name, providerClass) {
7
+ _registry.set(name, providerClass);
8
+ }
9
+
10
+ function getProviderNames() {
11
+ return Array.from(_registry.keys());
12
+ }
13
+
14
+ function getProvider(name) {
15
+ return _registry.get(name) || null;
16
+ }
17
+
18
+ function resolveProviderConfig(cfg) {
19
+ const env = process.env.NATURECO_SEARCH_PROVIDER || '';
20
+ if (env && _registry.has(env)) return env;
21
+ if (cfg && cfg[PROVIDER_CONFIG_KEY] && _registry.has(cfg[PROVIDER_CONFIG_KEY])) {
22
+ return cfg[PROVIDER_CONFIG_KEY];
23
+ }
24
+ return DEFAULT_PROVIDER;
25
+ }
26
+
27
+ function getActiveProvider(cfg) {
28
+ const name = resolveProviderConfig(cfg);
29
+ const Cls = getProvider(name);
30
+ if (!Cls) return getProvider(DEFAULT_PROVIDER);
31
+ return Cls;
32
+ }
33
+
34
+ class SearchProvider {
35
+ constructor(config = {}) {
36
+ this.config = config;
37
+ this.name = 'base';
38
+ }
39
+
40
+ async search(query, options = {}) {
41
+ throw new Error('Not implemented: search');
42
+ }
43
+
44
+ normalizeResults(rawResults) {
45
+ return (rawResults || []).map(r => ({
46
+ title: r.title || '',
47
+ snippet: r.snippet || r.content || r.text || '',
48
+ url: r.url || '',
49
+ }));
50
+ }
51
+ }
52
+
53
+ module.exports = {
54
+ SearchProvider,
55
+ registerProvider,
56
+ getProviderNames,
57
+ getProvider,
58
+ resolveProviderConfig,
59
+ getActiveProvider,
60
+ PROVIDER_CONFIG_KEY,
61
+ DEFAULT_PROVIDER,
62
+ };