n2-soul 6.1.3 โ 6.1.5
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/.gitattributes +13 -0
- package/.github/FUNDING.yml +3 -0
- package/README.ko.md +61 -1
- package/README.md +59 -1
- package/index.js +6 -4
- package/lib/ark/index.js +3 -3
- package/lib/ark/parser.js +45 -15
- package/lib/config.default.js +7 -0
- package/lib/intercom-log.js +1 -1
- package/lib/paths.js +10 -3
- package/lib/utils.js +120 -109
- package/package.json +1 -1
- package/sequences/boot.js +4 -3
- package/sequences/end.js +2 -2
- package/sequences/work.js +271 -271
- package/tests/test-simulation.js +153 -0
package/.gitattributes
ADDED
package/README.ko.md
CHANGED
|
@@ -39,16 +39,76 @@ npm install
|
|
|
39
39
|
|
|
40
40
|
### 2. MCP ์ค์ ์ Soul ์ถ๊ฐ
|
|
41
41
|
|
|
42
|
+
Soul์ ํ์ค MCP ์๋ฒ(stdio)์
๋๋ค. ์ฌ์ฉ ์ค์ธ ํธ์คํธ์ ์ค์ ์ ์ถ๊ฐํ์ธ์:
|
|
43
|
+
|
|
44
|
+
<details>
|
|
45
|
+
<summary><strong>Cursor / VS Code Copilot / Claude Desktop</strong></summary>
|
|
46
|
+
|
|
47
|
+
`mcp.json`, `settings.json`, ๋๋ `claude_desktop_config.json`์ ์ถ๊ฐ:
|
|
42
48
|
```json
|
|
43
49
|
{
|
|
44
50
|
"mcpServers": {
|
|
45
51
|
"soul": {
|
|
46
52
|
"command": "node",
|
|
47
|
-
"args": ["/path/to/soul/index.js"]
|
|
53
|
+
"args": ["/path/to/node_modules/n2-soul/index.js"]
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
```
|
|
58
|
+
</details>
|
|
59
|
+
|
|
60
|
+
<details>
|
|
61
|
+
<summary><strong>๐ฆ Ollama + Open WebUI</strong></summary>
|
|
62
|
+
|
|
63
|
+
Open WebUI๋ MCP ๋๊ตฌ๋ฅผ ๋ค์ดํฐ๋ธ๋ก ์ง์ํฉ๋๋ค.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# 1. Ollama ์คํ ํ์ธ
|
|
67
|
+
ollama serve
|
|
68
|
+
|
|
69
|
+
# 2. Soul ์ค์น
|
|
70
|
+
npm install n2-soul
|
|
71
|
+
|
|
72
|
+
# 3. Soul ๊ฒฝ๋ก ํ์ธ
|
|
73
|
+
# Windows:
|
|
74
|
+
echo %cd%\node_modules\n2-soul\index.js
|
|
75
|
+
# Mac/Linux:
|
|
76
|
+
echo $(pwd)/node_modules/n2-soul/index.js
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Open WebUI**์์: **โ๏ธ ์ค์ โ Tools โ MCP Servers** โ ์ ์๋ฒ ์ถ๊ฐ:
|
|
80
|
+
```
|
|
81
|
+
Name: soul
|
|
82
|
+
Command: node
|
|
83
|
+
Args: /your/path/to/node_modules/n2-soul/index.js
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
์ด์ Open WebUI์์ ์ฑํ
ํ๋ ๋ชจ๋ ๋ชจ๋ธ์ด Soul์ 20๊ฐ ์ด์์ ๋ฉ๋ชจ๋ฆฌ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
<details>
|
|
90
|
+
<summary><strong>๐ฅ๏ธ LM Studio</strong></summary>
|
|
91
|
+
|
|
92
|
+
LM Studio๋ MCP๋ฅผ ๋ค์ดํฐ๋ธ๋ก ์ง์ํฉ๋๋ค. `~/.lmstudio/mcp.json`์ ์ถ๊ฐ:
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"soul": {
|
|
97
|
+
"command": "node",
|
|
98
|
+
"args": ["/path/to/node_modules/n2-soul/index.js"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
</details>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary><strong>๐ง ๊ธฐํ MCP ํธํ ํธ์คํธ</strong></summary>
|
|
107
|
+
|
|
108
|
+
Soul์ **stdio** ์์ ํ์ค MCP ํ๋กํ ์ฝ์ ์ฌ์ฉํฉ๋๋ค. MCP๋ฅผ ์ง์ํ๋ ๋๊ตฌ๋ผ๋ฉด Soul์ด ์๋ํฉ๋๋ค. command๋ฅผ `node`๋ก, args๋ฅผ `n2-soul/index.js` ๊ฒฝ๋ก๋ก ์ง์ ํ๋ฉด ๋ฉ๋๋ค.
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
> **๐ก ํ:** npm์ผ๋ก ์ค์นํ ๊ฒฝ์ฐ ๊ฒฝ๋ก๋ `node_modules/n2-soul/index.js`์
๋๋ค. ์์ค์์ ์ค์นํ ๊ฒฝ์ฐ ํด๋ก ํ ๋๋ ํ ๋ฆฌ์ ์ ๋ ๊ฒฝ๋ก๋ฅผ ์ฌ์ฉํ์ธ์.
|
|
52
112
|
|
|
53
113
|
### 3. ์์ด์ ํธ์๊ฒ Soul ์ฌ์ฉ๋ฒ ์๋ ค์ฃผ๊ธฐ
|
|
54
114
|
|
package/README.md
CHANGED
|
@@ -66,16 +66,74 @@ npm install
|
|
|
66
66
|
|
|
67
67
|
### 2. Add Soul to your MCP config
|
|
68
68
|
|
|
69
|
+
Soul is a standard MCP server (stdio). Add it to your host's config:
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary><strong>Cursor / VS Code Copilot / Claude Desktop</strong></summary>
|
|
73
|
+
|
|
74
|
+
Add to `mcp.json`, `settings.json`, or `claude_desktop_config.json`:
|
|
69
75
|
```json
|
|
70
76
|
{
|
|
71
77
|
"mcpServers": {
|
|
72
78
|
"soul": {
|
|
73
79
|
"command": "node",
|
|
74
|
-
"args": ["/path/to/soul/index.js"]
|
|
80
|
+
"args": ["/path/to/node_modules/n2-soul/index.js"]
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
```
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
<details>
|
|
88
|
+
<summary><strong>๐ฆ Ollama + Open WebUI</strong></summary>
|
|
89
|
+
|
|
90
|
+
Open WebUI supports MCP tools natively.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 1. Make sure Ollama is running
|
|
94
|
+
ollama serve
|
|
95
|
+
|
|
96
|
+
# 2. Install Soul
|
|
97
|
+
npm install n2-soul
|
|
98
|
+
|
|
99
|
+
# 3. Find your Soul path
|
|
100
|
+
# Windows:
|
|
101
|
+
echo %cd%\node_modules\n2-soul\index.js
|
|
102
|
+
# Mac/Linux:
|
|
103
|
+
echo $(pwd)/node_modules/n2-soul/index.js
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
In **Open WebUI**: Go to **โ๏ธ Settings โ Tools โ MCP Servers** โ Add new server:
|
|
107
|
+
```
|
|
108
|
+
Name: soul
|
|
109
|
+
Command: node
|
|
110
|
+
Args: /your/path/to/node_modules/n2-soul/index.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Now any model you chat with in Open WebUI can use Soul's 20+ memory tools.
|
|
114
|
+
</details>
|
|
115
|
+
|
|
116
|
+
<details>
|
|
117
|
+
<summary><strong>๐ฅ๏ธ LM Studio</strong></summary>
|
|
118
|
+
|
|
119
|
+
LM Studio supports MCP natively. Add to `~/.lmstudio/mcp.json`:
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"soul": {
|
|
124
|
+
"command": "node",
|
|
125
|
+
"args": ["/path/to/node_modules/n2-soul/index.js"]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
</details>
|
|
131
|
+
|
|
132
|
+
<details>
|
|
133
|
+
<summary><strong>๐ง Any other MCP-compatible host</strong></summary>
|
|
134
|
+
|
|
135
|
+
Soul speaks standard MCP protocol over **stdio**. If your tool supports MCP, Soul works. Just point the command to `node` and the args to `n2-soul/index.js`.
|
|
136
|
+
</details>
|
|
79
137
|
|
|
80
138
|
> **๐ก Tip:** If you installed via npm, the path is `node_modules/n2-soul/index.js`. If from source, use the absolute path to your cloned directory.
|
|
81
139
|
|
package/index.js
CHANGED
|
@@ -15,9 +15,10 @@ const { registerEndSequence } = require('./sequences/end');
|
|
|
15
15
|
const { registerBrainTools } = require('./tools/brain');
|
|
16
16
|
const { registerKVCacheTools } = require('./tools/kv-cache');
|
|
17
17
|
|
|
18
|
+
const pkg = require('./package.json');
|
|
18
19
|
const server = new McpServer({
|
|
19
20
|
name: 'n2-soul',
|
|
20
|
-
version:
|
|
21
|
+
version: pkg.version,
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
@@ -29,9 +30,10 @@ const server = new McpServer({
|
|
|
29
30
|
// The RULES decide what's blocked โ not the code.
|
|
30
31
|
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
31
32
|
const ark = createArk({
|
|
32
|
-
rulesDir: config.ARK?.rulesDir
|
|
33
|
-
auditDir: config.ARK?.auditDir
|
|
34
|
-
strictMode: config.ARK?.strictMode
|
|
33
|
+
rulesDir: config.ARK?.rulesDir ?? path.join(__dirname, 'rules'),
|
|
34
|
+
auditDir: config.ARK?.auditDir ?? path.join(config.DATA_DIR, 'ark-audit'),
|
|
35
|
+
strictMode: config.ARK?.strictMode ?? false,
|
|
36
|
+
auditMaxAgeDays: config.ARK?.auditMaxAgeDays ?? 7,
|
|
35
37
|
auditEnabled: true,
|
|
36
38
|
});
|
|
37
39
|
|
package/lib/ark/index.js
CHANGED
|
@@ -23,16 +23,16 @@ function createArk(options = {}) {
|
|
|
23
23
|
// Load and parse all .n2 rule files
|
|
24
24
|
const rules = loadRules(rulesDir);
|
|
25
25
|
|
|
26
|
-
// Create audit logger
|
|
27
26
|
const audit = new AuditLogger({
|
|
28
27
|
dir: auditDir,
|
|
29
28
|
enabled: options.auditEnabled !== false,
|
|
30
|
-
logPasses: options.auditPasses
|
|
29
|
+
logPasses: options.auditPasses ?? false,
|
|
30
|
+
maxAgeDays: options.auditMaxAgeDays ?? 7,
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// Create safety gate with audit hooks
|
|
34
34
|
const gate = new SafetyGate(rules, {
|
|
35
|
-
strictMode: options.strictMode
|
|
35
|
+
strictMode: options.strictMode ?? false,
|
|
36
36
|
onBlock: (result) => audit.log(result, { type: result.type }),
|
|
37
37
|
onPass: (result) => audit.log(result, { type: result.type }),
|
|
38
38
|
});
|
package/lib/ark/parser.js
CHANGED
|
@@ -193,26 +193,56 @@ function extractPatterns(body) {
|
|
|
193
193
|
*/
|
|
194
194
|
function parseGates(source) {
|
|
195
195
|
const gates = {};
|
|
196
|
-
const
|
|
197
|
-
let match;
|
|
196
|
+
const lines = source.split('\n');
|
|
198
197
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
let inGate = false;
|
|
199
|
+
let gateName = '';
|
|
200
|
+
let braceDepth = 0;
|
|
201
|
+
let bodyLines = [];
|
|
202
202
|
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
? actionsMatch[1].split(',').map(a => a.trim().replace(/["']/g, ''))
|
|
206
|
-
: [];
|
|
203
|
+
for (const line of lines) {
|
|
204
|
+
const trimmed = line.trim();
|
|
207
205
|
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
if (!inGate) {
|
|
207
|
+
const m = trimmed.match(/^@gate\s+(\w+)\s*\{?/);
|
|
208
|
+
if (m) {
|
|
209
|
+
inGate = true;
|
|
210
|
+
gateName = m[1];
|
|
211
|
+
braceDepth = (trimmed.includes('{')) ? 1 : 0;
|
|
212
|
+
bodyLines = [];
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
210
216
|
|
|
211
|
-
|
|
212
|
-
|
|
217
|
+
if (inGate) {
|
|
218
|
+
bodyLines.push(line);
|
|
219
|
+
for (const ch of line) {
|
|
220
|
+
if (ch === '{') braceDepth++;
|
|
221
|
+
if (ch === '}') braceDepth--;
|
|
222
|
+
}
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
if (braceDepth <= 0) {
|
|
225
|
+
const body = bodyLines.join('\n');
|
|
226
|
+
|
|
227
|
+
const actionsMatch = body.match(/actions\s*:\s*\[([^\]]+)\]/);
|
|
228
|
+
const actions = actionsMatch
|
|
229
|
+
? actionsMatch[1].split(',').map(a => a.trim().replace(/["']/g, ''))
|
|
230
|
+
: [];
|
|
231
|
+
|
|
232
|
+
const requiresMatch = body.match(/requires\s*:\s*(\w+)/);
|
|
233
|
+
const requires = requiresMatch ? requiresMatch[1] : 'human_approval';
|
|
234
|
+
|
|
235
|
+
const minMatch = body.match(/min_approval_level\s*:\s*(\d+)/);
|
|
236
|
+
const minApproval = minMatch ? parseInt(minMatch[1], 10) : 1;
|
|
237
|
+
|
|
238
|
+
if (actions.length > 0) {
|
|
239
|
+
gates[gateName] = { actions, requires, minApproval };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
inGate = false;
|
|
243
|
+
gateName = '';
|
|
244
|
+
bodyLines = [];
|
|
245
|
+
}
|
|
216
246
|
}
|
|
217
247
|
}
|
|
218
248
|
|
package/lib/config.default.js
CHANGED
|
@@ -37,6 +37,12 @@ module.exports = {
|
|
|
37
37
|
childLimit: 20,
|
|
38
38
|
},
|
|
39
39
|
|
|
40
|
+
// Work sequence settings
|
|
41
|
+
WORK: {
|
|
42
|
+
sessionTtlHours: 24, // Auto-expire stale work sessions
|
|
43
|
+
maxDecisions: 20, // Max decisions kept on soul-board
|
|
44
|
+
},
|
|
45
|
+
|
|
40
46
|
// KV-Cache settings
|
|
41
47
|
KV_CACHE: {
|
|
42
48
|
enabled: true,
|
|
@@ -77,5 +83,6 @@ module.exports = {
|
|
|
77
83
|
rulesDir: null, // null = soul/rules/
|
|
78
84
|
auditDir: null, // null = soul/data/ark-audit/
|
|
79
85
|
strictMode: false, // true = block unknown actions too
|
|
86
|
+
auditMaxAgeDays: 7, // Auto-cleanup audit logs after N days
|
|
80
87
|
},
|
|
81
88
|
};
|
package/lib/intercom-log.js
CHANGED
|
@@ -51,7 +51,7 @@ function normalizeName(name) {
|
|
|
51
51
|
// โโ Path helpers โโ
|
|
52
52
|
|
|
53
53
|
function getConversationsDir(config) {
|
|
54
|
-
const dataDir = config?.dataDir || path.join(__dirname, '..', 'data');
|
|
54
|
+
const dataDir = config?.dataDir || config?.DATA_DIR || path.join(__dirname, '..', 'data');
|
|
55
55
|
return path.join(dataDir, 'conversations');
|
|
56
56
|
}
|
|
57
57
|
|
package/lib/paths.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
// Soul MCP v6.0 โ Central path manager. Cross-platform compatible.
|
|
2
|
-
// NOTE: DATA_ROOT here is a fallback for agent-registry only.
|
|
3
|
-
// For Cloud Storage, use config.DATA_DIR (set in config.local.js).
|
|
4
2
|
const path = require('path');
|
|
5
3
|
const fs = require('fs');
|
|
6
4
|
|
|
7
5
|
// soul/lib/paths.js โ 2 levels up = project root
|
|
8
6
|
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
7
|
+
// Local fallback only โ always use config.DATA_DIR when available
|
|
9
8
|
const DATA_ROOT = path.join(path.resolve(__dirname, '..'), 'data');
|
|
10
9
|
|
|
11
10
|
/** Agents directory path (auto-created if needed) */
|
|
12
11
|
function getAgentsDir() {
|
|
13
|
-
|
|
12
|
+
// Prefer config.DATA_DIR (supports Cloud Storage) over local fallback
|
|
13
|
+
let dataDir;
|
|
14
|
+
try {
|
|
15
|
+
const config = require('./config');
|
|
16
|
+
dataDir = config.DATA_DIR || DATA_ROOT;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
dataDir = DATA_ROOT;
|
|
19
|
+
}
|
|
20
|
+
const dir = path.join(dataDir, 'agents');
|
|
14
21
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
15
22
|
return dir;
|
|
16
23
|
}
|
package/lib/utils.js
CHANGED
|
@@ -1,109 +1,120 @@
|
|
|
1
|
-
// Soul MCP v6.0 โ Shared utility functions (file I/O, time, security, logging)
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
// Timezone:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
return
|
|
31
|
-
} catch (e) {
|
|
32
|
-
logError('
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
1
|
+
// Soul MCP v6.0 โ Shared utility functions (file I/O, time, security, logging)
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Timezone: prioritizes env > config.TIMEZONE > fallback 'Asia/Seoul'
|
|
6
|
+
let _tz = null;
|
|
7
|
+
function _getTimezone() {
|
|
8
|
+
if (!_tz) {
|
|
9
|
+
try {
|
|
10
|
+
const config = require('./config');
|
|
11
|
+
_tz = process.env.N2_TIMEZONE || config.TIMEZONE || 'Asia/Seoul';
|
|
12
|
+
} catch (e) {
|
|
13
|
+
_tz = process.env.N2_TIMEZONE || 'Asia/Seoul';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return _tz;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// -- Logging --
|
|
20
|
+
|
|
21
|
+
function logError(context, err) {
|
|
22
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23
|
+
console.error(`[soul:${context}]`, msg);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// -- File I/O --
|
|
27
|
+
|
|
28
|
+
function readFile(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
} catch (e) {
|
|
32
|
+
logError('readFile', e);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJson(filePath) {
|
|
38
|
+
const content = readFile(filePath);
|
|
39
|
+
if (!content) return null;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(content);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
logError('readJson', e);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeJson(filePath, data) {
|
|
49
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
50
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeFile(filePath, content) {
|
|
54
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
55
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// -- Time --
|
|
59
|
+
|
|
60
|
+
function today() {
|
|
61
|
+
return new Date().toLocaleDateString('sv-SE', { timeZone: _getTimezone() });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function nowISO() {
|
|
65
|
+
const formatter = new Intl.DateTimeFormat('sv-SE', {
|
|
66
|
+
timeZone: _getTimezone(),
|
|
67
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
68
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
69
|
+
hour12: false,
|
|
70
|
+
});
|
|
71
|
+
const parts = formatter.formatToParts(new Date());
|
|
72
|
+
const get = (type) => parts.find(p => p.type === type)?.value || '00';
|
|
73
|
+
// Compute UTC offset dynamically for the configured timezone
|
|
74
|
+
const now = new Date();
|
|
75
|
+
const utcStr = now.toLocaleString('en-US', { timeZone: 'UTC' });
|
|
76
|
+
const tzStr = now.toLocaleString('en-US', { timeZone: _getTimezone() });
|
|
77
|
+
const diffMs = new Date(tzStr) - new Date(utcStr);
|
|
78
|
+
const diffH = Math.floor(Math.abs(diffMs) / 3600000);
|
|
79
|
+
const diffM = Math.floor((Math.abs(diffMs) % 3600000) / 60000);
|
|
80
|
+
const sign = diffMs >= 0 ? '+' : '-';
|
|
81
|
+
const offset = `${sign}${String(diffH).padStart(2, '0')}:${String(diffM).padStart(2, '0')}`;
|
|
82
|
+
return `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}:${get('second')}${offset}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// -- Security --
|
|
86
|
+
|
|
87
|
+
function safePath(filePath, baseDir) {
|
|
88
|
+
const resolved = path.resolve(baseDir, filePath);
|
|
89
|
+
const normalizedBase = path.resolve(baseDir);
|
|
90
|
+
if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
|
|
91
|
+
logError('safePath', `Path traversal blocked: ${filePath}`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// -- First-line comment validation --
|
|
98
|
+
|
|
99
|
+
function validateFirstLineComment(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
102
|
+
const firstLine = content.split('\n')[0].trim();
|
|
103
|
+
const patterns = [
|
|
104
|
+
/^\/\/\s*.+/, // JS/TS
|
|
105
|
+
/^#\s*.+/, // Python/Shell/YAML
|
|
106
|
+
/^<!--\s*.+/, // HTML/MD
|
|
107
|
+
/^\/\*\s*.+/, // CSS/Java
|
|
108
|
+
/^\{.*"_desc"/, // JSON with _desc field
|
|
109
|
+
];
|
|
110
|
+
return patterns.some(p => p.test(firstLine));
|
|
111
|
+
} catch (e) {
|
|
112
|
+
logError('validateFirstLineComment', e);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
logError, readFile, readJson, writeJson, writeFile,
|
|
119
|
+
today, nowISO, safePath, validateFirstLineComment,
|
|
120
|
+
};
|
package/package.json
CHANGED
package/sequences/boot.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Soul MCP
|
|
1
|
+
// Soul MCP โ Boot sequence. Handoff + Entity/Core Memory injection + KV-Cache restore.
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const pkg = require('../package.json');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const { readJson, today, nowISO, logError } = require('../lib/utils');
|
|
5
6
|
const { detectAgentsDir, listAgents } = require('../lib/agent-registry');
|
|
@@ -32,7 +33,7 @@ function registerBootSequence(server, z, config) {
|
|
|
32
33
|
const agentName = agent || process.env.N2_AGENT_NAME || 'default';
|
|
33
34
|
setAgentName(agentName);
|
|
34
35
|
|
|
35
|
-
lines.push(`--- Soul Boot
|
|
36
|
+
lines.push(`--- Soul Boot v${pkg.version} | ${agentName} | ${today()} ---`);
|
|
36
37
|
if (agents.length > 0) {
|
|
37
38
|
lines.push(`Agents: ${agents.map(a => `${a.name}[${a.model}]`).join(', ')}`);
|
|
38
39
|
}
|
|
@@ -114,7 +115,7 @@ function registerBootSequence(server, z, config) {
|
|
|
114
115
|
lines.push(`โ ๏ธ Core Memory: ${e.message}`);
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
lines.push(`\n--- Soul Boot
|
|
118
|
+
lines.push(`\n--- Soul Boot v${pkg.version} complete ---`);
|
|
118
119
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
119
120
|
}
|
|
120
121
|
);
|
package/sequences/end.js
CHANGED
|
@@ -66,8 +66,8 @@ function registerEndSequence(server, z, config) {
|
|
|
66
66
|
for (const d of allDecisions) {
|
|
67
67
|
board.decisions.push({ date: dateStr, by: agent, what: d, why: '' });
|
|
68
68
|
}
|
|
69
|
-
if (board.decisions.length > 20) {
|
|
70
|
-
board.decisions = board.decisions.slice(-20);
|
|
69
|
+
if (board.decisions.length > (config.WORK?.maxDecisions ?? 20)) {
|
|
70
|
+
board.decisions = board.decisions.slice(-(config.WORK?.maxDecisions ?? 20));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
board.updatedBy = agent;
|