natureco-cli 5.7.13 → 5.7.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.7.13",
3
+ "version": "5.7.14",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -25,6 +25,8 @@ const tui = require('../utils/tui');
25
25
  const { loadToolDefinitions, toOpenAIFormat, executeTool } = require('../utils/tools');
26
26
  const { accumulateToolCallDeltas, finalizeToolCalls } = require('../utils/streaming-tools');
27
27
  const { createPasteSafeInput, createOutputFilter, enableBracketedPaste, disableBracketedPaste, restoreNewlines, clearPasteContext } = require('../utils/paste-safe-input');
28
+ const { getMemoryStore } = require('../utils/memory-store');
29
+ const { buildSkillIndex } = require('../utils/skill-index');
28
30
 
29
31
  // v5.4.6: Model adi sizintisini engelle — global'e ata, callback'lerden erisebilir olsun
30
32
  const MODEL_NAMES_TO_HIDE = ['MiniMax-M2.5', 'MiniMaxM2.5', 'minimaxm25', 'Claude-3', 'GPT-4', 'ChatGPT'];
@@ -502,6 +504,12 @@ async function startRepl(args) {
502
504
  const { buildSoulContext, summarizeSoul } = require("../tools/soul");
503
505
  const soulSummary = buildSoulContext(); // 3 dosya birlesik ozet
504
506
 
507
+ // v5.7.14: Hermes-style memory store (MEMORY.md / USER.md) + skill index
508
+ const memoryStore = getMemoryStore();
509
+ memoryStore.load();
510
+ const memorySnapshotBlock = memoryStore.getSystemPromptBlock();
511
+ const skillsIndexBlock = buildSkillIndex();
512
+
505
513
  // Resume?
506
514
  let messages = [];
507
515
  if (resumeId) {
@@ -573,6 +581,12 @@ async function startRepl(args) {
573
581
  // v5.4.11: Cross-session context (Sasuke Brain)
574
582
  crossSessionContext ? `GECMISTE KONUSULAN KONULAR: Bu konulari biliyorsun, tekrar sorma:\n${crossSessionContext}` : '',
575
583
 
584
+ // === v5.7.14: Hermes-style memory store (MEMORY.md / USER.md) ===
585
+ memorySnapshotBlock,
586
+
587
+ // === v5.7.14: Skill index (progressive disclosure) ===
588
+ skillsIndexBlock,
589
+
576
590
 
577
591
  ].filter(Boolean).join(' ');
578
592
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * memory — Unified memory tool (Hermes-style)
3
+ *
4
+ * Single tool with action=add|remove|replace|list, target=memory|user
5
+ *
6
+ * Coexists with existing memory_write / memory_search tools.
7
+ * New system prompt uses memory-store snapshot; this tool mutates live state.
8
+ */
9
+
10
+ const { getMemoryStore } = require('../utils/memory-store');
11
+
12
+ const name = 'memory';
13
+ const description = 'Persistent memory across sessions. Use action=add to save facts, action=list to see everything, action=remove to delete by substring match. target=memory for environment facts, target=user for user preferences and traits.';
14
+ const parameters = {
15
+ type: 'object',
16
+ properties: {
17
+ action: {
18
+ type: 'string',
19
+ enum: ['add', 'remove', 'replace', 'list'],
20
+ description: 'Operation: add (append entry), remove (by substring match), replace (find by substring, replace), list (show all)',
21
+ },
22
+ target: {
23
+ type: 'string',
24
+ enum: ['memory', 'user'],
25
+ description: 'Which store: memory (agent notes) or user (user preferences/habits)',
26
+ },
27
+ content: {
28
+ type: 'string',
29
+ description: 'Content to add/remove/replace. For replace, this is the new content.',
30
+ },
31
+ oldContent: {
32
+ type: 'string',
33
+ description: 'For replace: substring to match existing entry.',
34
+ },
35
+ },
36
+ required: ['action', 'target'],
37
+ };
38
+
39
+ async function execute(args) {
40
+ const store = getMemoryStore();
41
+ const { action, target = 'memory', content, oldContent } = args;
42
+
43
+ switch (action) {
44
+ case 'add':
45
+ if (!content) return JSON.stringify({ success: false, error: 'content required for add' });
46
+ return store.add(target, content);
47
+ case 'remove':
48
+ if (!content) return JSON.stringify({ success: false, error: 'content required for remove' });
49
+ return store.remove(target, content);
50
+ case 'replace':
51
+ if (!content || !oldContent) return JSON.stringify({ success: false, error: 'content and oldContent required for replace' });
52
+ return store.replace(target, oldContent, content);
53
+ case 'list':
54
+ return store.list(target);
55
+ default:
56
+ return JSON.stringify({ success: false, error: `Unknown action: ${action}` });
57
+ }
58
+ }
59
+
60
+ module.exports = { name, description, parameters, execute };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * skill_view — Load full skill content (progressive disclosure tier 2-3)
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { skillView, skillLookup } = require('../utils/skill-index');
8
+
9
+ const name = 'skill_view';
10
+ const description = 'Load full skill content by name. Use skills_list to discover available skills. First call returns SKILL.md body plus linked_files. Call again with filePath to access references/templates/scripts.';
11
+ const parameters = {
12
+ type: 'object',
13
+ properties: {
14
+ name: { type: 'string', description: 'The skill name (use skills_list to see available skills)' },
15
+ filePath: { type: 'string', description: 'Optional: path to a linked file within the skill (e.g. references/api.md)' },
16
+ },
17
+ required: ['name'],
18
+ };
19
+
20
+ async function execute(args) {
21
+ const skill = skillLookup(args.name);
22
+ if (!skill) {
23
+ return JSON.stringify({ success: false, error: `Skill not found: ${args.name}` });
24
+ }
25
+ if (args.filePath) {
26
+ const skillDir = path.dirname(skill.path);
27
+ const filePath = path.join(skillDir, args.filePath);
28
+ if (!fs.existsSync(filePath)) {
29
+ return JSON.stringify({ success: false, error: `File not found: ${args.filePath}` });
30
+ }
31
+ const content = fs.readFileSync(filePath, 'utf8');
32
+ return JSON.stringify({ success: true, name: skill.name, filePath: args.filePath, content });
33
+ }
34
+ return skillView(args.name);
35
+ }
36
+
37
+ module.exports = { name, description, parameters, execute };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * skills_list — Discover available skills (progressive disclosure tier 1)
3
+ */
4
+
5
+ const { skillsList } = require('../utils/skill-index');
6
+
7
+ const name = 'skills_list';
8
+ const description = 'List available skills with name and description. Use skill_view(name) to load full content.';
9
+ const parameters = {
10
+ type: 'object',
11
+ properties: {
12
+ category: { type: 'string', description: 'Optional category filter' },
13
+ },
14
+ required: [],
15
+ };
16
+
17
+ async function execute(args) {
18
+ return skillsList(args.category || null);
19
+ }
20
+
21
+ module.exports = { name, description, parameters, execute };
@@ -1,185 +1,154 @@
1
1
  /**
2
- * memory_write - Memory'ye fact/kayit yaz (v5.1.1)
2
+ * memory-store — Hermes-style flat-file memory (MEMORY.md / USER.md)
3
3
  *
4
- * REPL'in extractMemoryFromMessage ozelligini tool olarak expose eder.
5
- * Parton'un vizyonu: "Benim asistanim, her seyimi hatirlayacak"
4
+ * Two stores:
5
+ * - MEMORY.md: agent's notes (environment facts, project conventions, tool quirks)
6
+ * - USER.md: what the agent knows about the user (preferences, habits, workflow)
7
+ *
8
+ * Entry delimiter: § (section sign). Multiline entries allowed.
9
+ *
10
+ * Frozen snapshot pattern:
11
+ * - System prompt gets a snapshot captured at session start (never changes mid-session)
12
+ * - Tool responses reflect live state (mutations write to disk immediately)
13
+ * - Prefix cache stays stable for the entire session
14
+ * - Snapshot refreshes on next session start
6
15
  */
7
16
 
8
- const fs = require("fs");
9
- const path = require("path");
10
- const os = require("os");
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
11
20
 
12
- const MEMORY_DIR = path.join(os.homedir(), ".natureco", "memory");
21
+ const MEMORY_DIR = path.join(os.homedir(), '.natureco', 'memories');
22
+ const ENTRY_DELIMITER = '\n§\n';
13
23
 
14
- function getMemoryFile(username) {
15
- const name = (username || "default").toLowerCase();
16
- return path.join(MEMORY_DIR, `${name}.json`);
17
- }
24
+ class MemoryStore {
25
+ constructor(charLimit = 3000) {
26
+ this._memoryEntries = [];
27
+ this._userEntries = [];
28
+ this._charLimit = charLimit;
29
+ this._snapshot = { memory: '', user: '' };
30
+ }
18
31
 
19
- 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: [] };
32
+ _ensureDir() {
33
+ if (!fs.existsSync(MEMORY_DIR)) {
34
+ fs.mkdirSync(MEMORY_DIR, { recursive: true });
24
35
  }
25
- return JSON.parse(fs.readFileSync(file, "utf8"));
26
- } catch {
27
- return { name: username || "User", nickname: null, botName: null, facts: [], preferences: [] };
28
36
  }
29
- }
30
37
 
31
- function saveMemory(username, memory) {
32
- if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
33
- memory.lastUpdated = new Date().toISOString();
34
- fs.writeFileSync(getMemoryFile(username), JSON.stringify(memory, null, 2), "utf8");
35
- return memory;
36
- }
38
+ _pathFor(target) {
39
+ return target === 'user'
40
+ ? path.join(MEMORY_DIR, 'USER.md')
41
+ : path.join(MEMORY_DIR, 'MEMORY.md');
42
+ }
37
43
 
38
- /**
39
- * Score azalt (eski fact'ler zamanla unutuluyor)
40
- */
41
- function decayFacts(memory) {
42
- if (!memory.facts) return memory;
43
- const now = Date.now();
44
- memory.facts = memory.facts.map(f => {
45
- if (!f.score) f.score = 5;
46
- // 1 haftadan eski -1, 1 aydan eski -3
47
- const ageMs = now - new Date(f.updatedAt || f.createdAt || now).getTime();
48
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
49
- if (ageDays > 30) f.score = Math.max(0, f.score - 3);
50
- else if (ageDays > 7) f.score = Math.max(0, f.score - 1);
51
- return f;
52
- });
53
- // score 0 olanlari sil
54
- 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);
58
- return memory;
59
- }
44
+ _readEntries(filePath) {
45
+ if (!fs.existsSync(filePath)) return [];
46
+ const content = fs.readFileSync(filePath, 'utf8').trim();
47
+ if (!content) return [];
48
+ return content.split(ENTRY_DELIMITER).map(e => e.trim()).filter(Boolean);
49
+ }
60
50
 
51
+ _writeEntries(filePath, entries) {
52
+ this._ensureDir();
53
+ const content = entries.join(ENTRY_DELIMITER);
54
+ fs.writeFileSync(filePath, content + '\n', 'utf8');
55
+ }
61
56
 
62
- /**
63
- * v5.4.9: Memory yazma sonrasi verification — geri oku ve gercekten yazildigini dogrula
64
- * Self-validation mekanizmasi: tool cagirip "success" demesine ragmen dosya bos olabilir
65
- */
66
- function verifyMemoryWrite(username, expectedFact, expectedBotName) {
67
- try {
68
- const memFile = getMemoryFile(username);
69
- if (!fs.existsSync(memFile)) {
70
- return { success: false, error: "Memory dosyasi olusturulamadi: " + memFile };
71
- }
72
- const mem = JSON.parse(fs.readFileSync(memFile, "utf8"));
73
-
74
- // Fact verification
75
- if (expectedFact) {
76
- const found = (mem.facts || []).some(f => f.value === expectedFact);
77
- if (!found) {
78
- return { success: false, error: "Fact memory'de bulunamadi: " + expectedFact };
79
- }
80
- }
57
+ _renderBlock(label, entries) {
58
+ if (!entries || entries.length === 0) return '';
59
+ return `=== ${label} ===\n${entries.join('\n')}\n=== ${label} sonu ===`;
60
+ }
81
61
 
82
- // BotName verification
83
- if (expectedBotName && mem.botName !== expectedBotName) {
84
- return { success: false, error: "BotName guncellenmedi: " + mem.botName };
85
- }
62
+ load() {
63
+ this._ensureDir();
64
+ this._memoryEntries = this._readEntries(this._pathFor('memory'));
65
+ this._userEntries = this._readEntries(this._pathFor('user'));
66
+ this._memoryEntries = [...new Map(this._memoryEntries.map(e => [e, e])).values()];
67
+ this._userEntries = [...new Map(this._userEntries.map(e => [e, e])).values()];
68
+ this._snapshot = {
69
+ memory: this._renderBlock('memory', this._memoryEntries),
70
+ user: this._renderBlock('user', this._userEntries),
71
+ };
72
+ }
86
73
 
87
- return { success: true, message: "Memory dogrulandi" };
88
- } catch (e) {
89
- return { success: false, error: e.message };
74
+ getSnapshot() {
75
+ return this._snapshot;
90
76
  }
91
- }
92
77
 
78
+ getSystemPromptBlock() {
79
+ const parts = [];
80
+ if (this._snapshot.memory) parts.push(this._snapshot.memory);
81
+ if (this._snapshot.user) parts.push(this._snapshot.user);
82
+ return parts.join('\n\n');
83
+ }
93
84
 
94
- function addMemory({ username, fact, score = 5, category = "general", botName, nickname, name }) {
95
- // Username yoksa ve 'name' parametresi varsa, onu username olarak kullan
96
- // (Parton'un "patron" diye hitap etmesi durumu icin)
97
- const effectiveUsername = username || (name && name.toLowerCase()) || 'default';
98
- if (!effectiveUsername || effectiveUsername === 'default') {
99
- // Hicbir username yok, default.json'a yaz
85
+ list(target) {
86
+ const entries = target === 'user' ? this._userEntries : this._memoryEntries;
87
+ return JSON.stringify({ success: true, entries, count: entries.length });
100
88
  }
101
89
 
102
- let memory = loadMemory(effectiveUsername);
103
- memory = decayFacts(memory);
104
-
105
- // identity updates (botName, nickname, name) — name sadece memory.name, username degil
106
- if (botName) memory.botName = botName;
107
- if (nickname !== undefined) memory.nickname = nickname;
108
- if (name) memory.name = name; // Bu memory.name (kullanici gercek adi), username degil
109
-
110
- if (fact) {
111
- // duplicate kontrol
112
- const existing = memory.facts.find(f => (f.value || f).toLowerCase() === fact.toLowerCase());
113
- if (existing) {
114
- existing.score = Math.min(10, (existing.score || 5) + 2);
115
- existing.updatedAt = new Date().toISOString();
116
- } else {
117
- memory.facts.push({
118
- value: fact,
119
- score,
120
- category,
121
- updatedAt: new Date().toISOString(),
122
- createdAt: new Date().toISOString(),
123
- });
90
+ add(target, content) {
91
+ if (!content || !content.trim()) {
92
+ return JSON.stringify({ success: false, error: 'Content cannot be empty.' });
124
93
  }
94
+ content = content.trim();
95
+ const entries = target === 'user' ? this._userEntries : this._memoryEntries;
96
+ if (entries.includes(content)) {
97
+ return JSON.stringify({ success: false, error: 'Duplicate entry.' });
98
+ }
99
+ const currentTotal = entries.reduce((sum, e) => sum + e.length + 3, 0);
100
+ if (currentTotal + content.length > this._charLimit) {
101
+ return JSON.stringify({ success: false, error: `Memory ${target} is full (limit ~${this._charLimit} chars). Remove some entries first.` });
102
+ }
103
+ entries.push(content);
104
+ this._writeEntries(this._pathFor(target), entries);
105
+ return JSON.stringify({ success: true, message: 'Memory entry added.', count: entries.length });
125
106
  }
126
107
 
127
- if (!memory.preferences) memory.preferences = [];
128
- memory = saveMemory(effectiveUsername, memory);
129
-
130
- // v5.4.9: Verification - geri oku ve dogrula
131
- const verifyResult = verifyMemoryWrite(effectiveUsername, fact, botName);
132
- if (!verifyResult.success) {
133
- return {
134
- success: false,
135
- error: "Memory yazildi ama dogrulanamadi: " + verifyResult.error,
136
- username: effectiveUsername,
137
- };
108
+ remove(target, content) {
109
+ if (!content || !content.trim()) {
110
+ return JSON.stringify({ success: false, error: 'Content cannot be empty.' });
111
+ }
112
+ content = content.trim();
113
+ const entries = target === 'user' ? this._userEntries : this._memoryEntries;
114
+ const idx = entries.findIndex(e => e.includes(content));
115
+ if (idx === -1) {
116
+ return JSON.stringify({ success: false, error: 'No matching entry found.' });
117
+ }
118
+ const removed = entries.splice(idx, 1);
119
+ this._writeEntries(this._pathFor(target), entries);
120
+ return JSON.stringify({ success: true, message: 'Memory entry removed.', removed: removed[0] });
138
121
  }
139
122
 
140
- return {
141
- success: true,
142
- message: "Memory guncellendi ve dogrulandi",
143
- username: effectiveUsername,
144
- verified: true,
145
- totalFacts: memory.facts.length,
146
- facts: memory.facts.map(f => ({ value: f.value, score: f.score, category: f.category })),
147
- botName: memory.botName,
148
- nickname: memory.nickname,
149
- name: memory.name,
150
- };
123
+ replace(target, oldContent, newContent) {
124
+ if (!oldContent || !newContent) {
125
+ return JSON.stringify({ success: false, error: 'Both old and new content required.' });
126
+ }
127
+ oldContent = oldContent.trim();
128
+ newContent = newContent.trim();
129
+ const entries = target === 'user' ? this._userEntries : this._memoryEntries;
130
+ const idx = entries.findIndex(e => e.includes(oldContent));
131
+ if (idx === -1) {
132
+ return JSON.stringify({ success: false, error: 'No matching entry found.' });
133
+ }
134
+ entries[idx] = newContent;
135
+ this._writeEntries(this._pathFor(target), entries);
136
+ return JSON.stringify({ success: true, message: 'Memory entry updated.' });
137
+ }
151
138
  }
152
139
 
153
- function clearMemory({ username }) {
154
- if (!username) return { success: false, error: "username gerekli" };
155
- const file = getMemoryFile(username);
156
- if (fs.existsSync(file)) fs.unlinkSync(file);
157
- return { success: true, message: `Memory temizlendi: ${username}` };
158
- }
140
+ let _defaultStore = null;
159
141
 
160
- function showMemory({ username }) {
161
- if (!username) return { success: false, error: "username gerekli" };
162
- const memory = loadMemory(username);
163
- return {
164
- success: true,
165
- username,
166
- name: memory.name,
167
- nickname: memory.nickname,
168
- botName: memory.botName,
169
- totalFacts: (memory.facts || []).length,
170
- facts: memory.facts || [],
171
- preferences: memory.preferences || [],
172
- };
142
+ function getMemoryStore() {
143
+ if (!_defaultStore) {
144
+ _defaultStore = new MemoryStore();
145
+ _defaultStore.load();
146
+ }
147
+ return _defaultStore;
173
148
  }
174
149
 
150
+ function resetMemoryStore() {
151
+ _defaultStore = null;
152
+ }
175
153
 
176
- module.exports = {
177
- getMemoryFile,
178
- loadMemory,
179
- saveMemory,
180
- decayFacts,
181
- verifyMemoryWrite,
182
- addMemory,
183
- clearMemory,
184
- showMemory,
185
- };
154
+ module.exports = { MemoryStore, getMemoryStore, resetMemoryStore, ENTRY_DELIMITER };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * registry — Hermes-style tool registry
3
+ *
4
+ * Each tool registers itself with:
5
+ * - name: unique tool name
6
+ * - toolset: group name (e.g. "file", "web", "terminal", "skills", "memory")
7
+ * - schema: OpenAI function-calling schema
8
+ * - handler: async function(args) => string (JSON)
9
+ * - checkFn: optional function() => bool (is tool available?)
10
+ * - requiresEnv: optional [envVarNames]
11
+ * - emoji: optional display emoji
12
+ */
13
+
14
+ class ToolRegistry {
15
+ constructor() {
16
+ this._tools = new Map();
17
+ }
18
+
19
+ register({ name, toolset, schema, handler, checkFn, requiresEnv, emoji }) {
20
+ if (this._tools.has(name)) {
21
+ return;
22
+ }
23
+ this._tools.set(name, { name, toolset, schema, handler, checkFn: checkFn || null, requiresEnv: requiresEnv || [], emoji: emoji || '' });
24
+ }
25
+
26
+ get(name) {
27
+ return this._tools.get(name) || null;
28
+ }
29
+
30
+ getAll() {
31
+ return Array.from(this._tools.values());
32
+ }
33
+
34
+ getByToolset(toolset) {
35
+ return this.getAll().filter(t => t.toolset === toolset);
36
+ }
37
+
38
+ getToolsets() {
39
+ const sets = new Set();
40
+ for (const t of this._tools.values()) {
41
+ sets.add(t.toolset);
42
+ }
43
+ return Array.from(sets).sort();
44
+ }
45
+
46
+ getDefinitions({ filterUnavailable } = {}) {
47
+ const result = [];
48
+ for (const entry of this._tools.values()) {
49
+ if (filterUnavailable && entry.checkFn) {
50
+ try {
51
+ if (!entry.checkFn()) continue;
52
+ } catch {
53
+ continue;
54
+ }
55
+ }
56
+ result.push({
57
+ type: 'function',
58
+ function: {
59
+ name: entry.name,
60
+ description: entry.schema.description || entry.schema.name || '',
61
+ parameters: entry.schema.parameters || { type: 'object', properties: {} },
62
+ },
63
+ });
64
+ }
65
+ return result;
66
+ }
67
+
68
+ async dispatch(name, args) {
69
+ const entry = this._tools.get(name);
70
+ if (!entry) {
71
+ return JSON.stringify({ error: `Unknown tool: ${name}` });
72
+ }
73
+ try {
74
+ return await entry.handler(args);
75
+ } catch (e) {
76
+ return JSON.stringify({ error: `${e.message || e}` });
77
+ }
78
+ }
79
+
80
+ getSchema(name) {
81
+ const entry = this._tools.get(name);
82
+ return entry ? entry.schema : null;
83
+ }
84
+
85
+ getEmoji(name, fallback = '') {
86
+ const entry = this._tools.get(name);
87
+ return entry ? (entry.emoji || fallback) : fallback;
88
+ }
89
+ }
90
+
91
+ const globalRegistry = new ToolRegistry();
92
+
93
+ module.exports = { ToolRegistry, globalRegistry };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * skill-index — Hermes-style skill index builder (progressive disclosure)
3
+ *
4
+ * Scans skills/ and ~/.natureco/skills/ for SKILL.md files with YAML frontmatter.
5
+ * Builds a compact index for system prompt injection.
6
+ * Skills are loaded on-demand via skill_view(name) tool.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const BUILTIN_SKILLS_DIR = path.join(__dirname, '..', '..', 'skills');
14
+ const USER_SKILLS_DIR = path.join(os.homedir(), '.natureco', 'skills');
15
+ const PROJECT_SKILLS_DIR = path.join(process.cwd(), '.natureco', 'skills');
16
+
17
+ function _parseFrontmatter(content) {
18
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
19
+ if (!match) return { frontmatter: {}, body: content };
20
+ const raw = match[1];
21
+ const body = content.slice(match[0].length).trim();
22
+ const fm = {};
23
+ for (const line of raw.split('\n')) {
24
+ const sep = line.indexOf(':');
25
+ if (sep === -1) continue;
26
+ const key = line.slice(0, sep).trim();
27
+ const val = line.slice(sep + 1).trim();
28
+ fm[key] = val;
29
+ }
30
+ return { frontmatter: fm, body };
31
+ }
32
+
33
+ function _getSkillDirs() {
34
+ const dirs = [];
35
+ for (const d of [BUILTIN_SKILLS_DIR, USER_SKILLS_DIR, PROJECT_SKILLS_DIR]) {
36
+ if (fs.existsSync(d)) dirs.push(d);
37
+ }
38
+ return dirs;
39
+ }
40
+
41
+ function _discoverSkills() {
42
+ const skills = [];
43
+ for (const skillsDir of _getSkillDirs()) {
44
+ const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory()) continue;
47
+ const skillPath = path.join(skillsDir, entry.name, 'SKILL.md');
48
+ if (!fs.existsSync(skillPath)) continue;
49
+ const raw = fs.readFileSync(skillPath, 'utf8');
50
+ const { frontmatter, body } = _parseFrontmatter(raw);
51
+ const name = frontmatter.name || entry.name;
52
+ const description = frontmatter.description || '';
53
+ const category = path.basename(skillsDir) === 'skills' ? 'general' : path.basename(skillsDir);
54
+ const tags = frontmatter.metadata ? (() => { try { const m = JSON.parse(frontmatter.metadata); return m.natureco?.tags || m.hermes?.tags || []; } catch { return []; } })() : [];
55
+ skills.push({ name, description, fullName: entry.name, path: skillPath, category, tags, raw, body, frontmatter });
56
+ }
57
+ }
58
+ return skills;
59
+ }
60
+
61
+ function buildSkillIndex() {
62
+ const skills = _discoverSkills();
63
+ if (skills.length === 0) return '';
64
+ const byCategory = {};
65
+ for (const s of skills) {
66
+ if (!byCategory[s.category]) byCategory[s.category] = [];
67
+ byCategory[s.category].push(s);
68
+ }
69
+ const lines = ['## Skills'];
70
+ lines.push('Before replying, scan the skills below. If a skill matches your task, load it with skill_view(name) and follow its instructions.');
71
+ lines.push('<available_skills>');
72
+ for (const [cat, catSkills] of Object.entries(byCategory)) {
73
+ lines.push(` ${cat}:`);
74
+ for (const s of catSkills) {
75
+ lines.push(` - ${s.name}: ${s.description}`);
76
+ }
77
+ }
78
+ lines.push('</available_skills>');
79
+ lines.push('Only proceed without loading a skill if genuinely none are relevant.');
80
+ return lines.join('\n');
81
+ }
82
+
83
+ function skillLookup(name) {
84
+ const skills = _discoverSkills();
85
+ const exact = skills.find(s => s.name === name || s.fullName === name);
86
+ if (exact) return exact;
87
+ return skills.find(s => s.name.toLowerCase() === name.toLowerCase() || s.fullName.toLowerCase() === name.toLowerCase()) || null;
88
+ }
89
+
90
+ function skillView(name) {
91
+ const skill = skillLookup(name);
92
+ if (!skill) {
93
+ return JSON.stringify({ success: false, error: `Skill not found: ${name}` });
94
+ }
95
+ const linkedFiles = [];
96
+ const skillDir = path.dirname(skill.path);
97
+ if (fs.existsSync(skillDir)) {
98
+ for (const sub of ['references', 'templates', 'scripts', 'assets']) {
99
+ const subDir = path.join(skillDir, sub);
100
+ if (fs.existsSync(subDir)) {
101
+ for (const f of fs.readdirSync(subDir)) {
102
+ linkedFiles.push(`${sub}/${f}`);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ return JSON.stringify({
108
+ success: true,
109
+ name: skill.name,
110
+ description: skill.description,
111
+ category: skill.category,
112
+ tags: skill.tags,
113
+ content: skill.body,
114
+ linkedFiles: linkedFiles.length > 0 ? linkedFiles : undefined,
115
+ });
116
+ }
117
+
118
+ function skillsList(category) {
119
+ const skills = _discoverSkills();
120
+ const filtered = category ? skills.filter(s => s.category === category) : skills;
121
+ return JSON.stringify({
122
+ success: true,
123
+ skills: filtered.map(s => ({ name: s.name, description: s.description, category: s.category, tags: s.tags })),
124
+ });
125
+ }
126
+
127
+ module.exports = { buildSkillIndex, skillLookup, skillView, skillsList };