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 ADDED
@@ -0,0 +1,13 @@
1
+ # Normalize line endings to LF on commit
2
+ * text=auto eol=lf
3
+
4
+ # Ensure these are always LF
5
+ *.js text eol=lf
6
+ *.json text eol=lf
7
+ *.md text eol=lf
8
+ *.n2 text eol=lf
9
+
10
+ # Binary files
11
+ *.png binary
12
+ *.jpg binary
13
+ *.ico binary
@@ -0,0 +1,3 @@
1
+ # Soul MCP โ€” Funding configuration
2
+ # This enables the ๐Ÿ’– Sponsor button on the GitHub repository
3
+ github: [choihyunsus]
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: '6.1.1',
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 || path.join(__dirname, 'rules'),
33
- auditDir: config.ARK?.auditDir || path.join(config.DATA_DIR, 'ark-audit'),
34
- strictMode: config.ARK?.strictMode || false,
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 || false,
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 || false,
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 gateRegex = /@gate\s+(\w+)\s*\{([^}]*)\}/g;
197
- let match;
196
+ const lines = source.split('\n');
198
197
 
199
- while ((match = gateRegex.exec(source)) !== null) {
200
- const name = match[1];
201
- const body = match[2];
198
+ let inGate = false;
199
+ let gateName = '';
200
+ let braceDepth = 0;
201
+ let bodyLines = [];
202
202
 
203
- const actionsMatch = body.match(/actions\s*:\s*\[([^\]]+)\]/);
204
- const actions = actionsMatch
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
- const requiresMatch = body.match(/requires\s*:\s*(\w+)/);
209
- const requires = requiresMatch ? requiresMatch[1] : 'human_approval';
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
- const minMatch = body.match(/min_approval_level\s*:\s*(\d+)/);
212
- const minApproval = minMatch ? parseInt(minMatch[1], 10) : 1;
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
- if (actions.length > 0) {
215
- gates[name] = { actions, requires, minApproval };
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
 
@@ -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
  };
@@ -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
- const dir = path.join(DATA_ROOT, 'agents');
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: configurable via N2_TIMEZONE env or config.TIMEZONE
6
- const _tz = process.env.N2_TIMEZONE || 'Asia/Seoul';
7
-
8
- // -- Logging --
9
-
10
- function logError(context, err) {
11
- const msg = err instanceof Error ? err.message : String(err);
12
- console.error(`[soul:${context}]`, msg);
13
- }
14
-
15
- // -- File I/O --
16
-
17
- function readFile(filePath) {
18
- try {
19
- return fs.readFileSync(filePath, 'utf8');
20
- } catch (e) {
21
- logError('readFile', e);
22
- return null;
23
- }
24
- }
25
-
26
- function readJson(filePath) {
27
- const content = readFile(filePath);
28
- if (!content) return null;
29
- try {
30
- return JSON.parse(content);
31
- } catch (e) {
32
- logError('readJson', e);
33
- return null;
34
- }
35
- }
36
-
37
- function writeJson(filePath, data) {
38
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
39
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
40
- }
41
-
42
- function writeFile(filePath, content) {
43
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
44
- fs.writeFileSync(filePath, content, 'utf8');
45
- }
46
-
47
- // -- Time --
48
-
49
- function today() {
50
- return new Date().toLocaleDateString('sv-SE', { timeZone: _tz });
51
- }
52
-
53
- function nowISO() {
54
- const formatter = new Intl.DateTimeFormat('sv-SE', {
55
- timeZone: _tz,
56
- year: 'numeric', month: '2-digit', day: '2-digit',
57
- hour: '2-digit', minute: '2-digit', second: '2-digit',
58
- hour12: false,
59
- });
60
- const parts = formatter.formatToParts(new Date());
61
- const get = (type) => parts.find(p => p.type === type)?.value || '00';
62
- // Compute UTC offset dynamically for the configured timezone
63
- const now = new Date();
64
- const utcStr = now.toLocaleString('en-US', { timeZone: 'UTC' });
65
- const tzStr = now.toLocaleString('en-US', { timeZone: _tz });
66
- const diffMs = new Date(tzStr) - new Date(utcStr);
67
- const diffH = Math.floor(Math.abs(diffMs) / 3600000);
68
- const diffM = Math.floor((Math.abs(diffMs) % 3600000) / 60000);
69
- const sign = diffMs >= 0 ? '+' : '-';
70
- const offset = `${sign}${String(diffH).padStart(2, '0')}:${String(diffM).padStart(2, '0')}`;
71
- return `${get('year')}-${get('month')}-${get('day')}T${get('hour')}:${get('minute')}:${get('second')}${offset}`;
72
- }
73
-
74
- // -- Security --
75
-
76
- function safePath(filePath, baseDir) {
77
- const resolved = path.resolve(baseDir, filePath);
78
- const normalizedBase = path.resolve(baseDir);
79
- if (!resolved.startsWith(normalizedBase + path.sep) && resolved !== normalizedBase) {
80
- logError('safePath', `Path traversal blocked: ${filePath}`);
81
- return null;
82
- }
83
- return resolved;
84
- }
85
-
86
- // -- First-line comment validation --
87
-
88
- function validateFirstLineComment(filePath) {
89
- try {
90
- const content = fs.readFileSync(filePath, 'utf8');
91
- const firstLine = content.split('\n')[0].trim();
92
- const patterns = [
93
- /^\/\/\s*.+/, // JS/TS
94
- /^#\s*.+/, // Python/Shell/YAML
95
- /^<!--\s*.+/, // HTML/MD
96
- /^\/\*\s*.+/, // CSS/Java
97
- /^\{.*"_desc"/, // JSON with _desc field
98
- ];
99
- return patterns.some(p => p.test(firstLine));
100
- } catch (e) {
101
- logError('validateFirstLineComment', e);
102
- return false;
103
- }
104
- }
105
-
106
- module.exports = {
107
- logError, readFile, readJson, writeJson, writeFile,
108
- today, nowISO, safePath, validateFirstLineComment,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2-soul",
3
- "version": "6.1.3",
3
+ "version": "6.1.5",
4
4
  "description": "Multi-agent session orchestrator with KV-Cache and Ark for MCP (Model Context Protocol)",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/sequences/boot.js CHANGED
@@ -1,5 +1,6 @@
1
- // Soul MCP v6.0 โ€” Boot sequence. Handoff + Entity/Core Memory injection + KV-Cache restore.
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 v6.0 | ${agentName} | ${today()} ---`);
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 v6.0 complete ---`);
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;