n2-soul 6.1.3 → 6.1.4

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
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
@@ -2,8 +2,19 @@
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
 
5
- // Timezone: configurable via N2_TIMEZONE env or config.TIMEZONE
6
- const _tz = process.env.N2_TIMEZONE || 'Asia/Seoul';
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
+ }
7
18
 
8
19
  // -- Logging --
9
20
 
@@ -47,12 +58,12 @@ function writeFile(filePath, content) {
47
58
  // -- Time --
48
59
 
49
60
  function today() {
50
- return new Date().toLocaleDateString('sv-SE', { timeZone: _tz });
61
+ return new Date().toLocaleDateString('sv-SE', { timeZone: _getTimezone() });
51
62
  }
52
63
 
53
64
  function nowISO() {
54
65
  const formatter = new Intl.DateTimeFormat('sv-SE', {
55
- timeZone: _tz,
66
+ timeZone: _getTimezone(),
56
67
  year: 'numeric', month: '2-digit', day: '2-digit',
57
68
  hour: '2-digit', minute: '2-digit', second: '2-digit',
58
69
  hour12: false,
@@ -62,7 +73,7 @@ function nowISO() {
62
73
  // Compute UTC offset dynamically for the configured timezone
63
74
  const now = new Date();
64
75
  const utcStr = now.toLocaleString('en-US', { timeZone: 'UTC' });
65
- const tzStr = now.toLocaleString('en-US', { timeZone: _tz });
76
+ const tzStr = now.toLocaleString('en-US', { timeZone: _getTimezone() });
66
77
  const diffMs = new Date(tzStr) - new Date(utcStr);
67
78
  const diffH = Math.floor(Math.abs(diffMs) / 3600000);
68
79
  const diffM = Math.floor((Math.abs(diffMs) % 3600000) / 60000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n2-soul",
3
- "version": "6.1.3",
3
+ "version": "6.1.4",
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;
package/sequences/work.js CHANGED
@@ -7,8 +7,8 @@ const { SoulEngine } = require('../lib/soul-engine');
7
7
  // In-memory work session state per project
8
8
  const activeSessions = {};
9
9
 
10
- // TTL: auto-expire stale sessions (24 hours, checked every hour)
11
- const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
10
+ // TTL: auto-expire stale sessions (checked every hour)
11
+ const SESSION_TTL_MS = (require('../lib/config').WORK?.sessionTtlHours ?? 24) * 60 * 60 * 1000;
12
12
  const _sessionGcTimer = setInterval(() => {
13
13
  const now = Date.now();
14
14
  for (const [project, session] of Object.entries(activeSessions)) {
@@ -0,0 +1,153 @@
1
+ // Soul 통합 시뮬레이션 — 수정된 모듈들이 실제로 정상 동작하는지 확인
2
+ const path = require('path');
3
+ const assert = (cond, msg) => {
4
+ if (cond) { console.log(` ✅ ${msg}`); pass++; }
5
+ else { console.error(` ❌ ${msg}`); fail++; }
6
+ };
7
+ let pass = 0, fail = 0;
8
+
9
+ // ═══════════════════════════════════════
10
+ // 1. 버전 동적 로드 확인 (#1, #2)
11
+ // ═══════════════════════════════════════
12
+ console.log('\n[1] 버전 동적 로드');
13
+ const pkg = require('../package.json');
14
+ assert(pkg.version === '6.1.3', `package.json version: ${pkg.version}`);
15
+ // boot.js에서 어떻게 사용하는지 검증
16
+ const bootPkg = require('../package.json');
17
+ assert(bootPkg.version === pkg.version, `boot.js도 같은 버전 참조: ${bootPkg.version}`);
18
+
19
+ // ═══════════════════════════════════════
20
+ // 2. Config 로드 + deepMerge (#3, #8, #13)
21
+ // ═══════════════════════════════════════
22
+ console.log('\n[2] Config 로드 & 새 설정 확인');
23
+ const config = require('../lib/config');
24
+ assert(config.TIMEZONE === 'Asia/Seoul', `TIMEZONE: ${config.TIMEZONE}`);
25
+ assert(config.WORK !== undefined, 'WORK 섹션 존재');
26
+ assert(config.WORK.sessionTtlHours === 24, `sessionTtlHours: ${config.WORK.sessionTtlHours}`);
27
+ assert(config.WORK.maxDecisions === 20, `maxDecisions: ${config.WORK.maxDecisions}`);
28
+ assert(config.ARK.auditMaxAgeDays === 7, `auditMaxAgeDays: ${config.ARK.auditMaxAgeDays}`);
29
+
30
+ // ═══════════════════════════════════════
31
+ // 3. Timezone — config.TIMEZONE 반영 확인 (#3)
32
+ // ═══════════════════════════════════════
33
+ console.log('\n[3] Timezone 함수');
34
+ const { today, nowISO } = require('../lib/utils');
35
+ const todayStr = today();
36
+ assert(/^\d{4}-\d{2}-\d{2}$/.test(todayStr), `today(): ${todayStr}`);
37
+ const nowStr = nowISO();
38
+ assert(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}$/.test(nowStr), `nowISO(): ${nowStr}`);
39
+ assert(nowStr.endsWith('+09:00'), `Timezone 반영 (Asia/Seoul +09:00): ${nowStr.slice(-6)}`);
40
+
41
+ // ═══════════════════════════════════════
42
+ // 4. Paths — DATA_DIR 우선 참조 확인 (#4)
43
+ // ═══════════════════════════════════════
44
+ console.log('\n[4] Paths 모듈');
45
+ const { PROJECT_ROOT, getAgentsDir } = require('../lib/paths');
46
+ const agentsDir = getAgentsDir();
47
+ assert(agentsDir.includes('data'), `getAgentsDir(): ${agentsDir}`);
48
+ assert(PROJECT_ROOT === path.resolve(__dirname, '..', '..'), `PROJECT_ROOT: ${PROJECT_ROOT}`);
49
+
50
+ // ═══════════════════════════════════════
51
+ // 5. Ark — createArk + ?? + auditMaxAgeDays (#7, #8)
52
+ // ═══════════════════════════════════════
53
+ console.log('\n[5] Ark 로드 & strictMode ?? 검증');
54
+ const { createArk } = require('../lib/ark');
55
+ const ark = createArk({
56
+ rulesDir: path.join(__dirname, '..', 'rules'),
57
+ auditDir: path.join(config.DATA_DIR, 'ark-audit'),
58
+ strictMode: false, // 명시적 false — || 였으면 fallback 됐을 것
59
+ auditMaxAgeDays: 14, // #8: config에서 전달 가능
60
+ auditEnabled: false, // 테스트이므로 감사 비활성
61
+ });
62
+ const summary = ark.summary();
63
+ assert(summary.blacklists > 0, `Blacklist 규칙: ${summary.blacklists} (${summary.patterns} patterns)`);
64
+ assert(summary.gates > 0, `Gate 규칙: ${summary.gates}`);
65
+
66
+ // strictMode=false일 때 unknown 도구 허용 확인
67
+ const unknownResult = ark.check('some_unknown_tool', '{}', 'tool_call');
68
+ assert(unknownResult.allowed === true, 'strictMode=false: unknown tool 허용');
69
+
70
+ // 차단 확인
71
+ const blockResult = ark.check('run_command', 'rm -rf /', 'tool_call');
72
+ assert(blockResult.allowed === false, `rm -rf / 차단: ${blockResult.rule}`);
73
+
74
+ // 2차 실행 공격 차단
75
+ const scriptResult = ark.check('run_command', 'bash exploit.sh', 'tool_call');
76
+ assert(scriptResult.allowed === false, `bash *.sh 차단: ${scriptResult.rule}`);
77
+
78
+ // ═══════════════════════════════════════
79
+ // 6. Parser — @gate brace counting 검증 (#9)
80
+ // ═══════════════════════════════════════
81
+ console.log('\n[6] @gate 파서 brace counting');
82
+ const { parse } = require('../lib/ark/parser');
83
+ const testRule = `
84
+ @gate complex_gate {
85
+ actions: [deploy, publish]
86
+ requires: human_approval
87
+ min_approval_level: 2
88
+ }
89
+ `;
90
+ const parsed = parse(testRule);
91
+ assert(parsed.gates.complex_gate !== undefined, 'complex_gate 파싱 성공');
92
+ assert(parsed.gates.complex_gate.actions.length === 2, `actions: ${parsed.gates.complex_gate.actions}`);
93
+ assert(parsed.gates.complex_gate.minApproval === 2, `minApproval: ${parsed.gates.complex_gate.minApproval}`);
94
+
95
+ // ═══════════════════════════════════════
96
+ // 7. SoulEngine — Board/Ledger 기본 동작
97
+ // ═══════════════════════════════════════
98
+ console.log('\n[7] SoulEngine 기본 동작');
99
+ const { SoulEngine } = require('../lib/soul-engine');
100
+ const engine = new SoulEngine(config.DATA_DIR);
101
+ const board = engine.readBoard('__test_sim__');
102
+ assert(board.project === '__test_sim__', `readBoard: project=${board.project}`);
103
+
104
+ // ═══════════════════════════════════════
105
+ // 8. CoreMemory + EntityMemory
106
+ // ═══════════════════════════════════════
107
+ console.log('\n[8] Memory 모듈');
108
+ const { CoreMemory } = require('../lib/core-memory');
109
+ const { EntityMemory } = require('../lib/entity-memory');
110
+ const coreMem = new CoreMemory(config.DATA_DIR);
111
+ const coreData = coreMem.read('__test_agent__');
112
+ assert(coreData.agent === '__test_agent__', 'CoreMemory read OK');
113
+
114
+ const entityMem = new EntityMemory(config.DATA_DIR);
115
+ const entityResult = entityMem.search('test');
116
+ assert(Array.isArray(entityResult), 'EntityMemory search OK');
117
+
118
+ // ═══════════════════════════════════════
119
+ // 9. KV-Cache 로드 시뮬레이션
120
+ // ═══════════════════════════════════════
121
+ console.log('\n[9] KV-Cache 초기화');
122
+ const { SoulKVCache } = require('../lib/kv-cache');
123
+ const kvCache = new SoulKVCache(config.DATA_DIR, config.KV_CACHE);
124
+ const loadResult = kvCache.load('__test_sim__');
125
+ assert(loadResult === null || typeof loadResult === 'object', 'KV-Cache load: OK (null or object)');
126
+ const snapList = kvCache.listSnapshots('__test_sim__');
127
+ assert(Array.isArray(snapList), `KV-Cache list: ${snapList.length} snapshots`);
128
+
129
+ // ═══════════════════════════════════════
130
+ // 10. intercom-log — config.DATA_DIR 참조 (#5)
131
+ // ═══════════════════════════════════════
132
+ console.log('\n[10] intercom-log config 호환성');
133
+ const { normalizeName, getValidAgentNames } = require('../lib/intercom-log');
134
+ const agents = getValidAgentNames();
135
+ assert(agents.has('master'), 'Default agent "master" 존재');
136
+ const normalized = normalizeName('UNKNOWN_PERSON');
137
+ assert(normalized === 'master', `Unknown name → master: "${normalized}"`);
138
+
139
+ // ═══════════════════════════════════════
140
+ // 결과
141
+ // ═══════════════════════════════════════
142
+ console.log(`\n${'='.repeat(40)}`);
143
+ console.log(`=== SIMULATION: ${pass}/${pass + fail} passed ===`);
144
+ if (fail > 0) {
145
+ console.log(`⚠️ ${fail} FAILED`);
146
+ process.exit(1);
147
+ } else {
148
+ console.log('🎉 All checks passed!');
149
+ }
150
+
151
+ // Cleanup: close ark audit to prevent hang
152
+ ark.close();
153
+ kvCache.stopAutoBackup();