n2-soul 4.1.0
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/LICENSE +121 -0
- package/README.ko.md +197 -0
- package/README.md +197 -0
- package/index.js +30 -0
- package/lib/agent-registry.js +60 -0
- package/lib/config.default.js +68 -0
- package/lib/config.example.js +28 -0
- package/lib/config.js +28 -0
- package/lib/context.js +34 -0
- package/lib/intercom-log.js +187 -0
- package/lib/kv-cache/agent-adapter.js +192 -0
- package/lib/kv-cache/backup.js +357 -0
- package/lib/kv-cache/compressor.js +130 -0
- package/lib/kv-cache/embedding.js +205 -0
- package/lib/kv-cache/index.js +446 -0
- package/lib/kv-cache/schema.js +108 -0
- package/lib/kv-cache/snapshot.js +213 -0
- package/lib/kv-cache/sqlite-store.js +402 -0
- package/lib/kv-cache/tier-manager.js +239 -0
- package/lib/kv-cache/token-saver.js +153 -0
- package/lib/paths.js +20 -0
- package/lib/soul-engine.js +189 -0
- package/lib/utils.js +97 -0
- package/package.json +31 -0
- package/sequences/boot.js +81 -0
- package/sequences/end.js +132 -0
- package/sequences/work.js +257 -0
- package/tools/brain.js +45 -0
- package/tools/kv-cache.js +246 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n2-soul",
|
|
3
|
+
"version": "4.1.0",
|
|
4
|
+
"description": "Multi-agent session orchestrator with KV-Cache for MCP (Model Context Protocol)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"mcp", "multi-agent", "kv-cache", "session", "orchestrator",
|
|
11
|
+
"ai-agent", "model-context-protocol", "soul", "n2", "persistent-memory"
|
|
12
|
+
],
|
|
13
|
+
"license": "Apache-2.0",
|
|
14
|
+
"homepage": "https://nton2.com",
|
|
15
|
+
"author": "N2 <lagi0730@gmail.com>",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/choihyunsus/soul.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/choihyunsus/soul/issues"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "~1.6.1",
|
|
28
|
+
"zod": "~3.24.1",
|
|
29
|
+
"sql.js": "~1.11.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Soul MCP v4.1 — Boot sequence. Handoff + KV-Cache restore.
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { readJson, today, nowISO, logError } = require('../lib/utils');
|
|
5
|
+
const { detectAgentsDir, listAgents } = require('../lib/agent-registry');
|
|
6
|
+
const { SoulEngine } = require('../lib/soul-engine');
|
|
7
|
+
const { setAgentName, setKvChainParent } = require('../lib/context');
|
|
8
|
+
|
|
9
|
+
function registerBootSequence(server, z, config) {
|
|
10
|
+
const engine = new SoulEngine(config.DATA_DIR);
|
|
11
|
+
|
|
12
|
+
server.registerTool(
|
|
13
|
+
'n2_boot',
|
|
14
|
+
{
|
|
15
|
+
title: 'Soul Boot',
|
|
16
|
+
description: 'Boot sequence — loads soul-board handoff, agent list, and KV-Cache context.',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
agent: z.string().describe('Agent name'),
|
|
19
|
+
project: z.string().optional().describe('Project name to load context for'),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
async ({ agent, project }) => {
|
|
23
|
+
const lines = [];
|
|
24
|
+
|
|
25
|
+
// -- Agent resolution --
|
|
26
|
+
const agentsDir = config.AGENTS_DIR || detectAgentsDir();
|
|
27
|
+
const agents = listAgents(agentsDir);
|
|
28
|
+
const agentName = agent || process.env.N2_AGENT_NAME || 'default';
|
|
29
|
+
setAgentName(agentName);
|
|
30
|
+
|
|
31
|
+
lines.push(`--- Soul Boot | ${agentName} | ${today()} ---`);
|
|
32
|
+
if (agents.length > 0) {
|
|
33
|
+
lines.push(`Agents: ${agents.map(a => `${a.name}[${a.model}]`).join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// -- Soul Board: handoff + TODO --
|
|
37
|
+
if (project) {
|
|
38
|
+
const board = engine.readBoard(project);
|
|
39
|
+
lines.push(`\n--- ${project} | v${board.state.version || '?'} | ${board.state.health || '?'} ---`);
|
|
40
|
+
|
|
41
|
+
if (board.handoff && board.handoff.summary) {
|
|
42
|
+
lines.push(`Handoff(${board.handoff.from}): ${board.handoff.summary}`);
|
|
43
|
+
if (board.handoff.todo && board.handoff.todo.length > 0) {
|
|
44
|
+
lines.push(`TODO: ${board.handoff.todo.join(' | ')}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const activeEntries = Object.entries(board.activeWork).filter(([_, v]) => v);
|
|
49
|
+
if (activeEntries.length > 0) {
|
|
50
|
+
lines.push(`Active: ${activeEntries.map(([n, i]) => `${n}:${i.task}`).join(', ')}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (board.decisions && board.decisions.length > 0) {
|
|
54
|
+
const recent = board.decisions.slice(-3);
|
|
55
|
+
lines.push(`Decisions: ${recent.map(d => `[${d.date}] ${d.what}`).join(' | ')}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// -- KV-Cache auto-load --
|
|
60
|
+
if (project && config.KV_CACHE?.enabled && config.KV_CACHE?.autoLoadOnBoot) {
|
|
61
|
+
try {
|
|
62
|
+
const { SoulKVCache } = require('../lib/kv-cache');
|
|
63
|
+
const kvCache = new SoulKVCache(config.DATA_DIR, config.KV_CACHE);
|
|
64
|
+
const snap = kvCache.load(project);
|
|
65
|
+
if (snap) {
|
|
66
|
+
setKvChainParent(project, snap.id);
|
|
67
|
+
const level = snap._level || 'auto';
|
|
68
|
+
const tokens = snap._promptTokens || '?';
|
|
69
|
+
lines.push(`\nKV-Cache: ${level} | ~${tokens}t | ${snap.id.slice(0, 8)}`);
|
|
70
|
+
if (snap._resumePrompt) lines.push(snap._resumePrompt);
|
|
71
|
+
}
|
|
72
|
+
} catch (e) { logError('boot:kv-cache', e); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lines.push(`\n--- Soul Boot complete ---`);
|
|
76
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { registerBootSequence };
|
package/sequences/end.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Soul MCP v4.1 — End sequence. Ledger + board handoff + KV-Cache snapshot.
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { nowISO, logError } = require('../lib/utils');
|
|
4
|
+
const { SoulEngine } = require('../lib/soul-engine');
|
|
5
|
+
const { popKvChainParent } = require('../lib/context');
|
|
6
|
+
const { activeSessions } = require('./work');
|
|
7
|
+
|
|
8
|
+
function registerEndSequence(server, z, config) {
|
|
9
|
+
const engine = new SoulEngine(config.DATA_DIR);
|
|
10
|
+
|
|
11
|
+
server.registerTool(
|
|
12
|
+
'n2_work_end',
|
|
13
|
+
{
|
|
14
|
+
title: 'Soul Work End',
|
|
15
|
+
description: 'End work sequence. Writes immutable ledger entry, updates soul-board handoff, releases file ownership.',
|
|
16
|
+
inputSchema: {
|
|
17
|
+
agent: z.string().describe('Agent name'),
|
|
18
|
+
project: z.string().describe('Project name'),
|
|
19
|
+
title: z.string().describe('Work title'),
|
|
20
|
+
summary: z.string().describe('Work summary'),
|
|
21
|
+
todo: z.array(z.string()).optional().describe('Next TODO items'),
|
|
22
|
+
decisions: z.array(z.string()).optional().describe('Key decisions made'),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
async ({ agent, project, title, summary, todo, decisions }) => {
|
|
26
|
+
const session = activeSessions[project] || {};
|
|
27
|
+
const allDecisions = [...(session.decisions || []), ...(decisions || [])];
|
|
28
|
+
|
|
29
|
+
// 1. Write immutable ledger entry
|
|
30
|
+
let ledgerResult;
|
|
31
|
+
try {
|
|
32
|
+
ledgerResult = engine.writeLedger(project, agent, {
|
|
33
|
+
startedAt: session.startedAt,
|
|
34
|
+
title,
|
|
35
|
+
summary,
|
|
36
|
+
filesCreated: session.filesCreated || [],
|
|
37
|
+
filesModified: session.filesModified || [],
|
|
38
|
+
filesDeleted: session.filesDeleted || [],
|
|
39
|
+
decisions: allDecisions,
|
|
40
|
+
});
|
|
41
|
+
} catch (e) {
|
|
42
|
+
logError('end:ledger', e);
|
|
43
|
+
return { content: [{ type: 'text', text: `❌ Ledger write failed: ${e.message}` }] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Update soul-board handoff
|
|
47
|
+
try {
|
|
48
|
+
const board = engine.readBoard(project);
|
|
49
|
+
board.handoff = {
|
|
50
|
+
from: agent,
|
|
51
|
+
summary,
|
|
52
|
+
todo: todo || [],
|
|
53
|
+
blockers: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const dateStr = nowISO().split('T')[0].slice(5);
|
|
57
|
+
for (const d of allDecisions) {
|
|
58
|
+
board.decisions.push({ date: dateStr, by: agent, what: d, why: '' });
|
|
59
|
+
}
|
|
60
|
+
if (board.decisions.length > 20) {
|
|
61
|
+
board.decisions = board.decisions.slice(-20);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
board.updatedBy = agent;
|
|
65
|
+
engine.writeBoard(project, board);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
logError('end:board', e);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Release file ownership
|
|
71
|
+
engine.releaseFiles(project, agent);
|
|
72
|
+
|
|
73
|
+
// 4. Clear active work
|
|
74
|
+
engine.clearActiveWork(project, agent);
|
|
75
|
+
|
|
76
|
+
// 5. Auto-update file-index tree
|
|
77
|
+
try {
|
|
78
|
+
const projectRoot = path.resolve(config.SOUL_ROOT, '..');
|
|
79
|
+
const tree = engine.scanDirectory(projectRoot, {
|
|
80
|
+
maxDepth: config.SEARCH?.maxDepth || 4,
|
|
81
|
+
});
|
|
82
|
+
engine.writeFileIndex(project, { updatedAt: nowISO(), tree });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
logError('end:file-index', e);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 6. Clear in-memory session
|
|
88
|
+
delete activeSessions[project];
|
|
89
|
+
|
|
90
|
+
// 7. Auto-save KV-Cache snapshot (with session chaining)
|
|
91
|
+
if (config.KV_CACHE?.enabled && config.KV_CACHE?.autoSaveOnWorkEnd) {
|
|
92
|
+
try {
|
|
93
|
+
const { SoulKVCache } = require('../lib/kv-cache');
|
|
94
|
+
const kvCache = new SoulKVCache(config.DATA_DIR, config.KV_CACHE);
|
|
95
|
+
const parentId = popKvChainParent(project);
|
|
96
|
+
kvCache.save(agent, project, {
|
|
97
|
+
summary,
|
|
98
|
+
decisions: allDecisions,
|
|
99
|
+
todo: todo || [],
|
|
100
|
+
filesCreated: session.filesCreated || [],
|
|
101
|
+
filesModified: session.filesModified || [],
|
|
102
|
+
filesDeleted: session.filesDeleted || [],
|
|
103
|
+
startedAt: session.startedAt,
|
|
104
|
+
parentSessionId: parentId,
|
|
105
|
+
});
|
|
106
|
+
} catch (e) {
|
|
107
|
+
logError('end:kv-cache', e);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const totalFiles = (session.filesCreated || []).length +
|
|
112
|
+
(session.filesModified || []).length +
|
|
113
|
+
(session.filesDeleted || []).length;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{
|
|
117
|
+
type: 'text',
|
|
118
|
+
text: [
|
|
119
|
+
`Work ${ledgerResult.id} completed: ${title}`,
|
|
120
|
+
`Agent: ${agent}`,
|
|
121
|
+
`Files: ${totalFiles} changes`,
|
|
122
|
+
`Decisions: ${allDecisions.length}`,
|
|
123
|
+
`Ledger: ${ledgerResult.path}`,
|
|
124
|
+
`Handoff TODO: ${(todo || []).join(' | ') || 'none'}`,
|
|
125
|
+
].join('\n'),
|
|
126
|
+
}],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { registerEndSequence };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Soul MCP v4.1 — Work sequence. Real-time change tracking, file ownership, context search.
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { nowISO, readJson, readFile, validateFirstLineComment, logError } = require('../lib/utils');
|
|
5
|
+
const { SoulEngine } = require('../lib/soul-engine');
|
|
6
|
+
|
|
7
|
+
// In-memory work session state per project
|
|
8
|
+
const activeSessions = {};
|
|
9
|
+
|
|
10
|
+
// ── Helper: recursively walk files in a directory ──
|
|
11
|
+
function walkFiles(dir, callback, maxDepth, depth = 0) {
|
|
12
|
+
if (depth > maxDepth) return;
|
|
13
|
+
try {
|
|
14
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
15
|
+
const fullPath = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
walkFiles(fullPath, callback, maxDepth, depth + 1);
|
|
18
|
+
} else {
|
|
19
|
+
callback(fullPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
logError("walkFiles", `${dir}: ${e.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function registerWorkSequence(server, z, config) {
|
|
28
|
+
const engine = new SoulEngine(config.DATA_DIR);
|
|
29
|
+
|
|
30
|
+
// Start a work sequence
|
|
31
|
+
server.registerTool(
|
|
32
|
+
'n2_work_start',
|
|
33
|
+
{
|
|
34
|
+
title: 'N2 Work Start',
|
|
35
|
+
description: 'Start a work sequence. Registers agent in activeWork on soul-board.',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
agent: z.string().describe('Agent name'),
|
|
38
|
+
project: z.string().describe('Project name'),
|
|
39
|
+
task: z.string().describe('Task description'),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
async ({ agent, project, task }) => {
|
|
43
|
+
engine.setActiveWork(project, agent, task, []);
|
|
44
|
+
activeSessions[project] = {
|
|
45
|
+
agent,
|
|
46
|
+
task,
|
|
47
|
+
startedAt: nowISO(),
|
|
48
|
+
filesCreated: [],
|
|
49
|
+
filesModified: [],
|
|
50
|
+
filesDeleted: [],
|
|
51
|
+
decisions: [],
|
|
52
|
+
};
|
|
53
|
+
return { content: [{ type: 'text', text: `Work started: ${agent} on ${project} — ${task}` }] };
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Claim file ownership before editing
|
|
58
|
+
server.registerTool(
|
|
59
|
+
'n2_work_claim',
|
|
60
|
+
{
|
|
61
|
+
title: 'N2 Work Claim',
|
|
62
|
+
description: 'Claim file ownership before modifying. Prevents collision with other agents.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
project: z.string().describe('Project name'),
|
|
65
|
+
agent: z.string().describe('Agent name'),
|
|
66
|
+
filePath: z.string().describe('File path relative to project root'),
|
|
67
|
+
intent: z.string().describe('Why you are modifying this file'),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
async ({ project, agent, filePath, intent }) => {
|
|
71
|
+
const result = engine.claimFile(project, filePath, agent, intent);
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
return { content: [{ type: 'text', text: `COLLISION: ${filePath} is owned by ${result.owner} (${result.intent}). Choose a different file.` }] };
|
|
74
|
+
}
|
|
75
|
+
return { content: [{ type: 'text', text: `Claimed: ${filePath} -> ${agent} (${intent})` }] };
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Log file changes during work
|
|
80
|
+
server.registerTool(
|
|
81
|
+
'n2_work_log',
|
|
82
|
+
{
|
|
83
|
+
title: 'N2 Work Log',
|
|
84
|
+
description: 'Log file changes during work. Reports created/modified/deleted files with descriptions.',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
project: z.string().describe('Project name'),
|
|
87
|
+
filesCreated: z.array(z.object({
|
|
88
|
+
path: z.string(),
|
|
89
|
+
desc: z.string(),
|
|
90
|
+
})).optional().describe('Files created'),
|
|
91
|
+
filesModified: z.array(z.object({
|
|
92
|
+
path: z.string(),
|
|
93
|
+
desc: z.string(),
|
|
94
|
+
})).optional().describe('Files modified'),
|
|
95
|
+
filesDeleted: z.array(z.object({
|
|
96
|
+
path: z.string(),
|
|
97
|
+
desc: z.string(),
|
|
98
|
+
})).optional().describe('Files deleted'),
|
|
99
|
+
decisions: z.array(z.string()).optional().describe('Decisions made'),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
async ({ project, filesCreated, filesModified, filesDeleted, decisions }) => {
|
|
103
|
+
const session = activeSessions[project];
|
|
104
|
+
if (!session) {
|
|
105
|
+
return { content: [{ type: 'text', text: 'ERROR: No active work session. Call n2_work_start first.' }] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (filesCreated) session.filesCreated.push(...filesCreated);
|
|
109
|
+
if (filesModified) session.filesModified.push(...filesModified);
|
|
110
|
+
if (filesDeleted) session.filesDeleted.push(...filesDeleted);
|
|
111
|
+
if (decisions) session.decisions.push(...decisions);
|
|
112
|
+
|
|
113
|
+
// Validate first-line comments on created files
|
|
114
|
+
const warnings = [];
|
|
115
|
+
for (const f of (filesCreated || [])) {
|
|
116
|
+
try {
|
|
117
|
+
const fullPath = path.resolve(f.path);
|
|
118
|
+
if (!validateFirstLineComment(fullPath)) {
|
|
119
|
+
warnings.push(`MISSING first-line comment: ${f.path}`);
|
|
120
|
+
}
|
|
121
|
+
} catch (e) { logError("work:validate", e); }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const total = (session.filesCreated.length + session.filesModified.length + session.filesDeleted.length);
|
|
125
|
+
let msg = `Logged: ${total} file changes, ${session.decisions.length} decisions.`;
|
|
126
|
+
if (warnings.length > 0) {
|
|
127
|
+
msg += `\nWARNINGS:\n ${warnings.join('\n ')}`;
|
|
128
|
+
}
|
|
129
|
+
if ((filesCreated && filesCreated.length > 0) || (filesDeleted && filesDeleted.length > 0)) {
|
|
130
|
+
msg += `\nFile tree will auto-update at n2_work_end. Use n2_project_scan for immediate refresh.`;
|
|
131
|
+
}
|
|
132
|
+
msg += `\nTODO RULE: All TODO files go in _data/ ONLY. Always mark completed items as [x]. Never use brain memory for TODOs.`;
|
|
133
|
+
return { content: [{ type: 'text', text: msg }] };
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ── n2_context_search: Search across Brain memory and Ledger entries ──
|
|
138
|
+
server.registerTool(
|
|
139
|
+
'n2_context_search',
|
|
140
|
+
{
|
|
141
|
+
title: 'N2 Context Search',
|
|
142
|
+
description: 'Search across Brain memory and Ledger entries for relevant past context. Uses keyword matching with recency weighting. Great for finding related past work or decisions.',
|
|
143
|
+
inputSchema: {
|
|
144
|
+
query: z.string().describe('Search query (keywords, space-separated)'),
|
|
145
|
+
sources: z.array(z.string()).optional().describe('Sources to search: "brain", "ledger". Default: all.'),
|
|
146
|
+
maxResults: z.number().optional().describe('Max results (default: 10)'),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
async ({ query, sources, maxResults }) => {
|
|
150
|
+
try {
|
|
151
|
+
const dataDir = config.DATA_DIR;
|
|
152
|
+
const searchCfg = config.SEARCH || {};
|
|
153
|
+
const minKwLen = searchCfg.minKeywordLength || 2;
|
|
154
|
+
const previewLen = searchCfg.previewLength || 200;
|
|
155
|
+
const recencyBonus = searchCfg.recencyBonus || 10;
|
|
156
|
+
const maxDepth = searchCfg.maxDepth || 6;
|
|
157
|
+
const keywords = query.toLowerCase().split(/\s+/).filter(k => k.length >= minKwLen);
|
|
158
|
+
const max = maxResults || searchCfg.defaultMaxResults || 10;
|
|
159
|
+
const searchSources = sources || ['brain', 'ledger'];
|
|
160
|
+
const results = [];
|
|
161
|
+
|
|
162
|
+
function scoreText(text, filePath, source, meta = {}) {
|
|
163
|
+
if (!text) return;
|
|
164
|
+
const lower = text.toLowerCase();
|
|
165
|
+
let score = 0;
|
|
166
|
+
const matchedKeywords = [];
|
|
167
|
+
for (const kw of keywords) {
|
|
168
|
+
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
169
|
+
const count = (lower.match(new RegExp(escaped, 'g')) || []).length;
|
|
170
|
+
if (count > 0) { score += count; matchedKeywords.push(kw); }
|
|
171
|
+
}
|
|
172
|
+
if (score > 0) {
|
|
173
|
+
if (meta.timestamp) {
|
|
174
|
+
const age = (Date.now() - new Date(meta.timestamp).getTime()) / (1000 * 60 * 60 * 24);
|
|
175
|
+
score += Math.max(0, recencyBonus - age);
|
|
176
|
+
}
|
|
177
|
+
results.push({
|
|
178
|
+
source, path: filePath,
|
|
179
|
+
score: Math.round(score * 100) / 100,
|
|
180
|
+
matchedKeywords,
|
|
181
|
+
preview: text.slice(0, previewLen).replace(/\n/g, ' '),
|
|
182
|
+
...meta,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Search Brain memory
|
|
188
|
+
if (searchSources.includes('brain')) {
|
|
189
|
+
const memoryDir = path.join(dataDir, 'memory');
|
|
190
|
+
if (fs.existsSync(memoryDir)) {
|
|
191
|
+
walkFiles(memoryDir, (fp) => {
|
|
192
|
+
const content = readFile(fp);
|
|
193
|
+
if (content) {
|
|
194
|
+
const relPath = path.relative(memoryDir, fp);
|
|
195
|
+
scoreText(content, `memory/${relPath}`, 'brain', {
|
|
196
|
+
timestamp: fs.statSync(fp).mtime.toISOString(),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}, maxDepth);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Search Ledger entries
|
|
204
|
+
if (searchSources.includes('ledger')) {
|
|
205
|
+
const projectsDir = path.join(dataDir, 'projects');
|
|
206
|
+
if (fs.existsSync(projectsDir)) {
|
|
207
|
+
for (const proj of fs.readdirSync(projectsDir)) {
|
|
208
|
+
const ledgerBase = path.join(projectsDir, proj, 'ledger');
|
|
209
|
+
if (!fs.existsSync(ledgerBase)) continue;
|
|
210
|
+
walkFiles(ledgerBase, (fp) => {
|
|
211
|
+
if (!fp.endsWith('.json')) return;
|
|
212
|
+
const data = readJson(fp);
|
|
213
|
+
if (!data) return;
|
|
214
|
+
const text = [data.title, data.summary, ...(data.decisions || [])].filter(Boolean).join(' ');
|
|
215
|
+
const relPath = path.relative(projectsDir, fp);
|
|
216
|
+
scoreText(text, `projects/${relPath}`, 'ledger', {
|
|
217
|
+
timestamp: data.completedAt || data.startedAt,
|
|
218
|
+
agent: data.agent,
|
|
219
|
+
title: data.title,
|
|
220
|
+
});
|
|
221
|
+
}, maxDepth);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
results.sort((a, b) => b.score - a.score);
|
|
227
|
+
const top = results.slice(0, max);
|
|
228
|
+
|
|
229
|
+
if (top.length === 0) {
|
|
230
|
+
return { content: [{ type: 'text', text: `🔍 No results for "${query}".` }] };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const lines = top.map((r, i) => {
|
|
234
|
+
const icon = r.source === 'brain' ? '🧠' : '📖';
|
|
235
|
+
const meta = [
|
|
236
|
+
r.title ? `"${r.title}"` : '',
|
|
237
|
+
r.agent ? `by ${r.agent}` : '',
|
|
238
|
+
`score: ${r.score}`,
|
|
239
|
+
].filter(Boolean).join(' | ');
|
|
240
|
+
return `${i + 1}. ${icon} ${r.path}\n ${meta}\n Keywords: [${r.matchedKeywords.join(', ')}]\n ${r.preview}`;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
content: [{
|
|
245
|
+
type: 'text', text:
|
|
246
|
+
`🔍 Context search: "${query}" (${top.length} results)\n\n${lines.join('\n\n')}`
|
|
247
|
+
}],
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
return { content: [{ type: 'text', text: `❌ Search error: ${err.message}` }] };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Export activeSessions for end.js to access
|
|
257
|
+
module.exports = { registerWorkSequence, activeSessions };
|
package/tools/brain.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Soul MCP v4.0 — Brain tools. Shared memory read/write with path traversal protection.
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { readFile, writeFile, safePath } = require('../lib/utils');
|
|
4
|
+
|
|
5
|
+
function registerBrainTools(server, z, config) {
|
|
6
|
+
const memoryDir = path.join(config.DATA_DIR, 'memory');
|
|
7
|
+
|
|
8
|
+
server.registerTool(
|
|
9
|
+
'n2_brain_read',
|
|
10
|
+
{
|
|
11
|
+
title: 'N2 Brain Read',
|
|
12
|
+
description: 'Read a file from shared memory (data/memory/). Agents share information here.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
filename: z.string().describe('File path relative to memory directory'),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
async ({ filename }) => {
|
|
18
|
+
const filePath = safePath(filename, memoryDir);
|
|
19
|
+
if (!filePath) return { content: [{ type: 'text', text: `BLOCKED: Path traversal denied — "${filename}"` }] };
|
|
20
|
+
const content = readFile(filePath);
|
|
21
|
+
if (!content) return { content: [{ type: 'text', text: `NOT FOUND: ${filePath}` }] };
|
|
22
|
+
return { content: [{ type: 'text', text: content }] };
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
server.registerTool(
|
|
27
|
+
'n2_brain_write',
|
|
28
|
+
{
|
|
29
|
+
title: 'N2 Brain Write',
|
|
30
|
+
description: 'Write a file to shared memory (data/memory/). Share information between agents.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
filename: z.string().describe('File path relative to memory directory'),
|
|
33
|
+
content: z.string().describe('File content'),
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
async ({ filename, content }) => {
|
|
37
|
+
const filePath = safePath(filename, memoryDir);
|
|
38
|
+
if (!filePath) return { content: [{ type: 'text', text: `BLOCKED: Path traversal denied — "${filename}"` }] };
|
|
39
|
+
writeFile(filePath, content);
|
|
40
|
+
return { content: [{ type: 'text', text: `Saved: memory/${filename} (${content.length} chars)` }] };
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { registerBrainTools };
|