@dmsdc-ai/aigentry-deliberation 0.0.21 → 0.0.23
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/README.md +74 -2
- package/browser-control-port.js +0 -9
- package/index.js +59 -14
- package/observer.js +11 -3
- package/package.json +1 -1
- package/session-monitor-win.js +1 -1
package/README.md
CHANGED
|
@@ -17,6 +17,12 @@ MCP Deliberation Server — Multi-session AI deliberation with smart speaker ord
|
|
|
17
17
|
- **Cross-platform**: macOS (tmux + Terminal.app), Windows (Windows Terminal), Linux
|
|
18
18
|
- **Obsidian archiving**: Auto-archive deliberation results to Obsidian vault
|
|
19
19
|
- **Session monitoring**: Real-time tmux/terminal monitoring
|
|
20
|
+
- **Vote enforcement**: Automatic [AGREE]/[DISAGREE]/[CONDITIONAL] vote marker requirement
|
|
21
|
+
- **Dynamic CLI timeout**: Smart cold-start handling (180s first turn, 120s subsequent)
|
|
22
|
+
- **Runtime logging**: Session lifecycle event logging for observability
|
|
23
|
+
- **Resilient browser automation**: 5-stage degradation state machine with 60s SLO
|
|
24
|
+
- **Model routing**: Dynamic per-provider model selection based on prompt analysis
|
|
25
|
+
- **Role drift detection**: Structural heading markers + keyword analysis for accurate role inference
|
|
20
26
|
|
|
21
27
|
## Installation
|
|
22
28
|
|
|
@@ -31,6 +37,8 @@ npx @dmsdc-ai/aigentry-deliberation install
|
|
|
31
37
|
2. npm 의존성 설치
|
|
32
38
|
3. `~/.claude/.mcp.json`에 MCP 서버 자동 등록
|
|
33
39
|
4. Claude Code 재시작하면 바로 사용 가능
|
|
40
|
+
5. Gemini CLI MCP 서버 자동 등록 (`~/.gemini/settings.json`)
|
|
41
|
+
6. deliberation-gate 스킬 자동 설치 (`~/.claude/skills/deliberation-gate/`)
|
|
34
42
|
|
|
35
43
|
### 기타 설치 방법
|
|
36
44
|
|
|
@@ -53,6 +61,8 @@ cd aigentry-deliberation && npm install && node install.js
|
|
|
53
61
|
npx @dmsdc-ai/aigentry-deliberation uninstall
|
|
54
62
|
```
|
|
55
63
|
|
|
64
|
+
MCP 서버 등록 해제 + 설치 파일 삭제 + 스킬 파일 정리까지 자동 처리됩니다.
|
|
65
|
+
|
|
56
66
|
## Forum Demo
|
|
57
67
|
|
|
58
68
|
Deliberation이 완료되면 결과를 시각화하는 Forum View를 생성합니다.
|
|
@@ -68,6 +78,16 @@ Deliberation이 완료되면 결과를 시각화하는 Forum View를 생성합
|
|
|
68
78
|
open demo/forum/index.html
|
|
69
79
|
```
|
|
70
80
|
|
|
81
|
+
## Diagnostics
|
|
82
|
+
|
|
83
|
+
MCP 연결 문제 자동 진단:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @dmsdc-ai/aigentry-deliberation doctor
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Claude Code, Codex CLI, Gemini CLI의 MCP 설정을 자동 점검하고 문제를 진단합니다.
|
|
90
|
+
|
|
71
91
|
## MCP Tools
|
|
72
92
|
|
|
73
93
|
| Tool | Description |
|
|
@@ -107,9 +127,37 @@ open demo/forum/index.html
|
|
|
107
127
|
| `researcher` | Data, benchmarks, references |
|
|
108
128
|
| `free` | No role constraint (default) |
|
|
109
129
|
|
|
130
|
+
### Supported CLI Speakers
|
|
131
|
+
|
|
132
|
+
| CLI | Command | Status |
|
|
133
|
+
|-----|---------|--------|
|
|
134
|
+
| Claude Code | `claude` | ✅ Tested |
|
|
135
|
+
| Codex CLI | `codex` | ✅ Tested |
|
|
136
|
+
| Gemini CLI | `gemini` | ✅ Tested |
|
|
137
|
+
| Aider | `aider` | 🔧 Supported |
|
|
138
|
+
| Cursor Agent | `cursor` | 🔧 Supported |
|
|
139
|
+
| OpenCode | `opencode` | 🔧 Supported |
|
|
140
|
+
| Continue | `continue` | 🔧 Supported |
|
|
141
|
+
|
|
142
|
+
### Supported Browser LLMs
|
|
143
|
+
|
|
144
|
+
| Provider | Transport | Status |
|
|
145
|
+
|----------|-----------|--------|
|
|
146
|
+
| ChatGPT | CDP / Clipboard | ✅ Tested |
|
|
147
|
+
| Claude Web | CDP / Clipboard | ✅ Tested |
|
|
148
|
+
| Gemini Web | CDP / Clipboard | ✅ Tested |
|
|
149
|
+
| DeepSeek | CDP / Clipboard | ✅ Tested |
|
|
150
|
+
| Qwen | CDP / Clipboard | ✅ Tested |
|
|
151
|
+
| Poe | CDP / Clipboard | ✅ Tested |
|
|
152
|
+
| Copilot | CDP / Clipboard | 🔧 Supported |
|
|
153
|
+
| Perplexity | CDP / Clipboard | 🔧 Supported |
|
|
154
|
+
| Mistral | CDP / Clipboard | 🔧 Supported |
|
|
155
|
+
| Grok | CDP / Clipboard | 🔧 Supported |
|
|
156
|
+
| HuggingChat | CDP / Clipboard | 🔧 Supported |
|
|
157
|
+
|
|
110
158
|
### deliberation-gate (Superpowers Integration)
|
|
111
159
|
|
|
112
|
-
Inserts multi-AI verification gates at key [superpowers](https://github.com/
|
|
160
|
+
Inserts multi-AI verification gates at key [superpowers](https://github.com/obra/superpowers) workflow decision points.
|
|
113
161
|
|
|
114
162
|
**Scenarios:**
|
|
115
163
|
- **brainstorming** → multi-AI design validation before writing plans
|
|
@@ -118,11 +166,35 @@ Inserts multi-AI verification gates at key [superpowers](https://github.com/anth
|
|
|
118
166
|
|
|
119
167
|
**Trigger:** Semi-automatic — skill recommends deliberation, user approves.
|
|
120
168
|
|
|
121
|
-
**
|
|
169
|
+
**Fallback:** MCP 미설치 시 self-criticism 기반 자가 검증으로 대체 (Silver 등급). MCP 설치 시 멀티-AI 토론 (Gold 등급).
|
|
170
|
+
|
|
171
|
+
**Install:** `npx @dmsdc-ai/aigentry-deliberation install` 실행 시 자동 설치됩니다.
|
|
172
|
+
|
|
173
|
+
수동 설치:
|
|
122
174
|
```bash
|
|
123
175
|
cp skills/deliberation-gate/SKILL.md ~/.claude/skills/deliberation-gate/SKILL.md
|
|
124
176
|
```
|
|
125
177
|
|
|
178
|
+
**RFC:** [Prerequisites header for tool-dependent skills](https://github.com/obra/superpowers/issues/589)
|
|
179
|
+
|
|
180
|
+
## What's New
|
|
181
|
+
|
|
182
|
+
### v0.0.23
|
|
183
|
+
- **Vote enforcement**: Turn prompts now require [AGREE]/[DISAGREE]/[CONDITIONAL] markers for reliable consensus measurement
|
|
184
|
+
- **Dynamic CLI timeout**: First CLI invocation gets 180s (cold-start buffer), subsequent turns use default 120s
|
|
185
|
+
- **Runtime logging**: INFO-level lifecycle logging (SESSION_CREATED, TURN, CLI_TURN, SYNTHESIZED) to `runtime.log`
|
|
186
|
+
- **Role inference improvement**: Structural heading markers (e.g., `## 조사 결과` → researcher) with +5 weight prevent false role drift detection
|
|
187
|
+
|
|
188
|
+
### v0.0.22
|
|
189
|
+
- **Security**: CDP `--remote-allow-origins` restricted to `127.0.0.1:9222` (was `*`)
|
|
190
|
+
- **Security**: Observer CORS restricted to localhost allowlist, server bound to `127.0.0.1`
|
|
191
|
+
- **Performance**: Async sleep for Chrome CDP initialization (was blocking event loop)
|
|
192
|
+
- **Bug fix**: Fabrication guard uses `detectCallerSpeaker()` instead of hardcoded `"claude"`
|
|
193
|
+
- **Bug fix**: CLI reviewer uses per-CLI invocation flags via `CLI_INVOCATION_HINTS`
|
|
194
|
+
- **Bug fix**: Windows monitor state directory path corrected
|
|
195
|
+
- **Memory**: SSE client Map cleanup on disconnect prevents memory leak
|
|
196
|
+
- **Code quality**: Removed unreachable dead code in browser-control-port
|
|
197
|
+
|
|
126
198
|
## aigentry Ecosystem
|
|
127
199
|
|
|
128
200
|
aigentry-deliberation is one component of the unified aigentry platform. All packages work together to make AI decisions transparent and auditable.
|
package/browser-control-port.js
CHANGED
|
@@ -509,15 +509,6 @@ class DevToolsMcpAdapter extends BrowserControlPort {
|
|
|
509
509
|
}
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
const sendResult = { ok: true };
|
|
513
|
-
|
|
514
|
-
if (!sendResult.ok) {
|
|
515
|
-
return makeResult(false, null, {
|
|
516
|
-
code: "SEND_FAILED",
|
|
517
|
-
message: `Send button not found: ${binding.selectors.sendButton}`,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
512
|
sent.add(turnId);
|
|
522
513
|
return makeResult(true, { turnId, sent: true });
|
|
523
514
|
} catch (err) {
|
package/index.js
CHANGED
|
@@ -218,12 +218,25 @@ const ROLE_KEYWORDS = {
|
|
|
218
218
|
researcher: /사례|데이터|연구|벤치마크|비교|논문|참고/,
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
+
const ROLE_HEADING_MARKERS = {
|
|
222
|
+
critic: /^##?\s*(Critic|비판|약점|심각도|위험\s*분석)/m,
|
|
223
|
+
implementer: /^##?\s*(코드\s*스케치|구현|Implementation|제안\s*코드)/m,
|
|
224
|
+
mediator: /^##?\s*(합의|종합|중재|Consensus|Mediation)/m,
|
|
225
|
+
researcher: /^##?\s*(조사\s*결과|비교\s*분석|Research|사례\s*연구|근거)/m,
|
|
226
|
+
};
|
|
227
|
+
|
|
221
228
|
function inferSuggestedRole(text) {
|
|
222
229
|
const scores = {};
|
|
223
230
|
for (const [role, pattern] of Object.entries(ROLE_KEYWORDS)) {
|
|
224
231
|
const matches = (text.match(new RegExp(pattern, "g")) || []).length;
|
|
225
232
|
if (matches > 0) scores[role] = matches;
|
|
226
233
|
}
|
|
234
|
+
// Structural heading markers get extra weight (equivalent to 5 keyword matches)
|
|
235
|
+
for (const [role, pattern] of Object.entries(ROLE_HEADING_MARKERS)) {
|
|
236
|
+
if (pattern.test(text)) {
|
|
237
|
+
scores[role] = (scores[role] || 0) + 5;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
227
240
|
if (Object.keys(scores).length === 0) return "free";
|
|
228
241
|
return Object.entries(scores).sort((a, b) => b[1] - a[1])[0][0];
|
|
229
242
|
}
|
|
@@ -948,7 +961,7 @@ async function ensureCdpAvailable() {
|
|
|
948
961
|
|
|
949
962
|
const launchArgs = [
|
|
950
963
|
"--remote-debugging-port=9222",
|
|
951
|
-
"--remote-allow-origins
|
|
964
|
+
"--remote-allow-origins=http://127.0.0.1:9222",
|
|
952
965
|
`--user-data-dir=${cdpDataDir}`,
|
|
953
966
|
`--profile-directory=${profileDir}`,
|
|
954
967
|
"--no-first-run",
|
|
@@ -965,7 +978,7 @@ async function ensureCdpAvailable() {
|
|
|
965
978
|
}
|
|
966
979
|
|
|
967
980
|
// Wait for Chrome to initialize CDP
|
|
968
|
-
|
|
981
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
969
982
|
|
|
970
983
|
// Retry CDP connection after launch
|
|
971
984
|
for (const endpoint of endpoints) {
|
|
@@ -2237,6 +2250,7 @@ ${recent}
|
|
|
2237
2250
|
[response_rule]
|
|
2238
2251
|
- 위 토론 맥락을 반영해 ${speaker}의 이번 턴 응답만 작성
|
|
2239
2252
|
- 마크다운 본문만 출력 (불필요한 머리말/꼬리말 금지)${speakerRole !== "free" ? `\n- 배정된 역할(${speakerRole})의 관점에서 분석하고 응답` : ""}
|
|
2253
|
+
- 응답 마지막에 반드시 [AGREE], [DISAGREE], 또는 [CONDITIONAL: 사유] 중 하나를 포함
|
|
2240
2254
|
[/response_rule]
|
|
2241
2255
|
[/deliberation_turn_request]
|
|
2242
2256
|
`;
|
|
@@ -2305,6 +2319,7 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
2305
2319
|
suggested_next_role: suggestedRole !== "free" ? suggestedRole : undefined,
|
|
2306
2320
|
role_drift: roleDrift || undefined,
|
|
2307
2321
|
});
|
|
2322
|
+
appendRuntimeLog("INFO", `TURN: ${state.id} | R${state.current_round} | speaker: ${normalizedSpeaker} | votes: ${votes.length > 0 ? votes.map(v => v.vote).join(",") : "none"} | channel: ${channel_used || "respond"}`);
|
|
2308
2323
|
|
|
2309
2324
|
state.current_speaker = selectNextSpeaker(state);
|
|
2310
2325
|
|
|
@@ -2611,6 +2626,7 @@ server.tool(
|
|
|
2611
2626
|
return ` - \`${p.speaker}\`: ${transport} (${p.type})`;
|
|
2612
2627
|
}).join("\n");
|
|
2613
2628
|
|
|
2629
|
+
appendRuntimeLog("INFO", `SESSION_CREATED: ${sessionId} | topic: ${topic.slice(0, 60)} | speakers: ${speakerOrder.join(",")} | rounds: ${rounds}`);
|
|
2614
2630
|
return {
|
|
2615
2631
|
content: [{
|
|
2616
2632
|
type: "text",
|
|
@@ -2989,6 +3005,10 @@ server.tool(
|
|
|
2989
3005
|
}] };
|
|
2990
3006
|
}
|
|
2991
3007
|
|
|
3008
|
+
// Dynamic timeout: first turn gets extra time for cold-start
|
|
3009
|
+
const speakerPriorTurns = state.log.filter(e => e.speaker === speaker).length;
|
|
3010
|
+
const effectiveTimeout = speakerPriorTurns === 0 ? Math.max(timeout_sec, 180) : timeout_sec;
|
|
3011
|
+
|
|
2992
3012
|
const hint = CLI_INVOCATION_HINTS[speaker];
|
|
2993
3013
|
if (!hint) {
|
|
2994
3014
|
return { content: [{ type: "text", text: `speaker "${speaker}"에 대한 CLI 호출 정보가 없습니다. CLI_INVOCATION_HINTS에 등록되지 않은 speaker입니다.` }] };
|
|
@@ -3039,8 +3059,8 @@ server.tool(
|
|
|
3039
3059
|
|
|
3040
3060
|
const timer = setTimeout(() => {
|
|
3041
3061
|
child.kill("SIGTERM");
|
|
3042
|
-
reject(new Error(`CLI 타임아웃 (${
|
|
3043
|
-
},
|
|
3062
|
+
reject(new Error(`CLI 타임아웃 (${effectiveTimeout}초)`));
|
|
3063
|
+
}, effectiveTimeout * 1000);
|
|
3044
3064
|
|
|
3045
3065
|
child.stdout.on("data", (data) => { stdout += data.toString(); });
|
|
3046
3066
|
child.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
@@ -3072,6 +3092,7 @@ server.tool(
|
|
|
3072
3092
|
});
|
|
3073
3093
|
|
|
3074
3094
|
const elapsedMs = Date.now() - startTime;
|
|
3095
|
+
appendRuntimeLog("INFO", `CLI_TURN: ${resolved} | speaker: ${speaker} | cli: ${hint.cmd} | elapsed: ${elapsedMs}ms | response_len: ${response.length}`);
|
|
3075
3096
|
|
|
3076
3097
|
if (!response) {
|
|
3077
3098
|
return { content: [{ type: "text", text: `⚠️ CLI "${speaker}"가 빈 응답을 반환했습니다.` }] };
|
|
@@ -3124,7 +3145,8 @@ server.tool(
|
|
|
3124
3145
|
const { transport } = resolveTransportForSpeaker(state, speaker);
|
|
3125
3146
|
if (transport === "cli_respond" || transport === "browser_auto") {
|
|
3126
3147
|
// Check if caller is the same speaker (legitimate self-response) or an impersonator
|
|
3127
|
-
const
|
|
3148
|
+
const callerSpeaker = detectCallerSpeaker();
|
|
3149
|
+
const callerIsSpeaker = callerSpeaker && (speaker === callerSpeaker);
|
|
3128
3150
|
if (!callerIsSpeaker) {
|
|
3129
3151
|
return {
|
|
3130
3152
|
content: [{
|
|
@@ -3232,6 +3254,8 @@ server.tool(
|
|
|
3232
3254
|
return lockedResult;
|
|
3233
3255
|
}
|
|
3234
3256
|
|
|
3257
|
+
appendRuntimeLog("INFO", `SYNTHESIZED: ${resolved} | turns: ${state.log.length} | rounds: ${state.max_rounds}`);
|
|
3258
|
+
|
|
3235
3259
|
// 토론 종료 즉시 모니터 터미널(물리 Terminal 포함) 강제 종료
|
|
3236
3260
|
closeMonitorTerminal(state.id, getSessionWindowIds(state));
|
|
3237
3261
|
|
|
@@ -3448,15 +3472,36 @@ server.tool(
|
|
|
3448
3472
|
// ── Request Review (auto-review) ───────────────────────────────
|
|
3449
3473
|
|
|
3450
3474
|
function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
3451
|
-
const
|
|
3475
|
+
const hint = CLI_INVOCATION_HINTS[command];
|
|
3476
|
+
let args;
|
|
3477
|
+
let opts = { encoding: "utf-8", timeout: timeoutMs, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 5 * 1024 * 1024, windowsHide: true };
|
|
3478
|
+
const env = { ...process.env };
|
|
3479
|
+
|
|
3480
|
+
switch (command) {
|
|
3481
|
+
case "claude":
|
|
3482
|
+
if (hint?.envPrefix?.includes("CLAUDECODE=")) delete env.CLAUDECODE;
|
|
3483
|
+
args = ["-p", "--output-format", "text", "--no-input"];
|
|
3484
|
+
opts.input = prompt;
|
|
3485
|
+
opts.stdio = ["pipe", "pipe", "pipe"];
|
|
3486
|
+
break;
|
|
3487
|
+
case "codex":
|
|
3488
|
+
args = ["exec", prompt];
|
|
3489
|
+
opts.stdio = ["ignore", "pipe", "pipe"];
|
|
3490
|
+
break;
|
|
3491
|
+
case "gemini":
|
|
3492
|
+
args = ["-p", prompt];
|
|
3493
|
+
opts.stdio = ["ignore", "pipe", "pipe"];
|
|
3494
|
+
break;
|
|
3495
|
+
default: {
|
|
3496
|
+
const flags = hint?.flags ? hint.flags.split(/\s+/).filter(Boolean) : ["-p"];
|
|
3497
|
+
args = [...flags, prompt];
|
|
3498
|
+
opts.stdio = ["ignore", "pipe", "pipe"];
|
|
3499
|
+
break;
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3452
3503
|
try {
|
|
3453
|
-
const result = execFileSync(command, args, {
|
|
3454
|
-
encoding: "utf-8",
|
|
3455
|
-
timeout: timeoutMs,
|
|
3456
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3457
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
3458
|
-
windowsHide: true,
|
|
3459
|
-
});
|
|
3504
|
+
const result = execFileSync(command, args, { ...opts, env });
|
|
3460
3505
|
return { ok: true, response: result.trim() };
|
|
3461
3506
|
} catch (error) {
|
|
3462
3507
|
if (error && error.killed) {
|
|
@@ -3736,4 +3781,4 @@ if (__entryFile && path.resolve(__currentFile) === __entryFile) {
|
|
|
3736
3781
|
}
|
|
3737
3782
|
|
|
3738
3783
|
// ── Test exports (used by vitest) ──
|
|
3739
|
-
export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS };
|
|
3784
|
+
export { selectNextSpeaker, loadRolePrompt, inferSuggestedRole, parseVotes, ROLE_KEYWORDS, ROLE_HEADING_MARKERS, loadRolePresets, applyRolePreset, detectDegradationLevels, formatDegradationReport, DEGRADATION_TIERS };
|
package/observer.js
CHANGED
|
@@ -261,7 +261,9 @@ function createServer(port) {
|
|
|
261
261
|
const pathname = url.pathname;
|
|
262
262
|
|
|
263
263
|
// CORS
|
|
264
|
-
|
|
264
|
+
const origin = req.headers.origin || "";
|
|
265
|
+
const allowedOrigins = [`http://127.0.0.1:${port}`, `http://localhost:${port}`];
|
|
266
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigins.includes(origin) ? origin : allowedOrigins[0]);
|
|
265
267
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
266
268
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
267
269
|
|
|
@@ -377,7 +379,13 @@ function createServer(port) {
|
|
|
377
379
|
|
|
378
380
|
req.on("close", () => {
|
|
379
381
|
const clients = sseClients.get(sessionId) || [];
|
|
380
|
-
|
|
382
|
+
const remaining = clients.filter(c => c !== res);
|
|
383
|
+
if (remaining.length === 0) {
|
|
384
|
+
sseClients.delete(sessionId);
|
|
385
|
+
sessionSnapshots.delete(sessionId);
|
|
386
|
+
} else {
|
|
387
|
+
sseClients.set(sessionId, remaining);
|
|
388
|
+
}
|
|
381
389
|
});
|
|
382
390
|
return;
|
|
383
391
|
}
|
|
@@ -468,7 +476,7 @@ const server = createServer(port);
|
|
|
468
476
|
// Poll every 1 second
|
|
469
477
|
const pollInterval = setInterval(pollSessions, 1000);
|
|
470
478
|
|
|
471
|
-
server.listen(port, () => {
|
|
479
|
+
server.listen(port, "127.0.0.1", () => {
|
|
472
480
|
console.log(`Deliberation Observer running at http://localhost:${port}`);
|
|
473
481
|
console.log(` Dashboard: http://localhost:${port}/`);
|
|
474
482
|
console.log(` API: http://localhost:${port}/api/sessions`);
|
package/package.json
CHANGED
package/session-monitor-win.js
CHANGED
|
@@ -14,7 +14,7 @@ if (!sessionId) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const HOME = process.env.HOME || process.env.USERPROFILE || "";
|
|
17
|
-
const stateDir = path.join(HOME, ".local", "
|
|
17
|
+
const stateDir = path.join(HOME, ".local", "lib", "mcp-deliberation", "state", project);
|
|
18
18
|
const stateFile = path.join(stateDir, `${sessionId}.json`);
|
|
19
19
|
|
|
20
20
|
const BOLD = "\x1b[1m";
|