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 +1 -1
- package/src/commands/repl.js +14 -0
- package/src/tools/memory.js +60 -0
- package/src/tools/skill_view.js +37 -0
- package/src/tools/skills_list.js +21 -0
- package/src/utils/memory-store.js +123 -154
- package/src/utils/registry.js +93 -0
- package/src/utils/skill-index.js +127 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "natureco-cli",
|
|
3
|
-
"version": "5.7.
|
|
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"
|
package/src/commands/repl.js
CHANGED
|
@@ -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
|
-
*
|
|
2
|
+
* memory-store — Hermes-style flat-file memory (MEMORY.md / USER.md)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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(
|
|
9
|
-
const path = require(
|
|
10
|
-
const os = require(
|
|
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(),
|
|
21
|
+
const MEMORY_DIR = path.join(os.homedir(), '.natureco', 'memories');
|
|
22
|
+
const ENTRY_DELIMITER = '\n§\n';
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
161
|
-
if (!
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 };
|