@mandujs/mcp 0.9.19 → 0.9.21

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.
Files changed (122) hide show
  1. package/README.md +320 -0
  2. package/package.json +1 -1
  3. package/src/activity-monitor.ts +847 -231
  4. package/src/resources/handlers.ts +244 -0
  5. package/src/resources/skills/guides.ts +1136 -0
  6. package/src/resources/skills/index.ts +12 -0
  7. package/src/resources/skills/loader.ts +218 -0
  8. package/src/resources/skills/mandu-composition/SKILL.md +91 -0
  9. package/src/resources/skills/mandu-composition/metadata.json +13 -0
  10. package/src/resources/skills/mandu-composition/rules/_sections.md +26 -0
  11. package/src/resources/skills/mandu-composition/rules/_template.md +77 -0
  12. package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -0
  13. package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -0
  14. package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -0
  15. package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -0
  16. package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -0
  17. package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -0
  18. package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -0
  19. package/src/resources/skills/mandu-deployment/SKILL.md +92 -0
  20. package/src/resources/skills/mandu-deployment/_sections.md +41 -0
  21. package/src/resources/skills/mandu-deployment/_template.md +38 -0
  22. package/src/resources/skills/mandu-deployment/metadata.json +13 -0
  23. package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -0
  24. package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -0
  25. package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -0
  26. package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -0
  27. package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -0
  28. package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -0
  29. package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -0
  30. package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -0
  31. package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -0
  32. package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -0
  33. package/src/resources/skills/mandu-fs-routes/metadata.json +12 -0
  34. package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -0
  35. package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -0
  36. package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -0
  37. package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -0
  38. package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -0
  39. package/src/resources/skills/mandu-guard/SKILL.md +129 -0
  40. package/src/resources/skills/mandu-guard/metadata.json +12 -0
  41. package/src/resources/skills/mandu-guard/rules/_sections.md +36 -0
  42. package/src/resources/skills/mandu-guard/rules/_template.md +82 -0
  43. package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -0
  44. package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -0
  45. package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -0
  46. package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -0
  47. package/src/resources/skills/mandu-hydration/SKILL.md +91 -0
  48. package/src/resources/skills/mandu-hydration/metadata.json +12 -0
  49. package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -0
  50. package/src/resources/skills/mandu-hydration/rules/_template.md +72 -0
  51. package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -0
  52. package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -0
  53. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -0
  54. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -0
  55. package/src/resources/skills/mandu-performance/SKILL.md +85 -0
  56. package/src/resources/skills/mandu-performance/metadata.json +14 -0
  57. package/src/resources/skills/mandu-performance/rules/_sections.md +31 -0
  58. package/src/resources/skills/mandu-performance/rules/_template.md +64 -0
  59. package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -0
  60. package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -0
  61. package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -0
  62. package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -0
  63. package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -0
  64. package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -0
  65. package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -0
  66. package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -0
  67. package/src/resources/skills/mandu-security/SKILL.md +87 -0
  68. package/src/resources/skills/mandu-security/metadata.json +13 -0
  69. package/src/resources/skills/mandu-security/rules/_sections.md +31 -0
  70. package/src/resources/skills/mandu-security/rules/_template.md +74 -0
  71. package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -0
  72. package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -0
  73. package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -0
  74. package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -0
  75. package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -0
  76. package/src/resources/skills/mandu-slot/SKILL.md +85 -0
  77. package/src/resources/skills/mandu-slot/metadata.json +12 -0
  78. package/src/resources/skills/mandu-slot/rules/_sections.md +36 -0
  79. package/src/resources/skills/mandu-slot/rules/_template.md +63 -0
  80. package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -0
  81. package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -0
  82. package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -0
  83. package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -0
  84. package/src/resources/skills/mandu-styling/SKILL.md +118 -0
  85. package/src/resources/skills/mandu-styling/_sections.md +36 -0
  86. package/src/resources/skills/mandu-styling/_template.md +32 -0
  87. package/src/resources/skills/mandu-styling/metadata.json +13 -0
  88. package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -0
  89. package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -0
  90. package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -0
  91. package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -0
  92. package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -0
  93. package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -0
  94. package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -0
  95. package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -0
  96. package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -0
  97. package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -0
  98. package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +161 -0
  99. package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -0
  100. package/src/resources/skills/mandu-testing/SKILL.md +99 -0
  101. package/src/resources/skills/mandu-testing/metadata.json +13 -0
  102. package/src/resources/skills/mandu-testing/rules/_sections.md +26 -0
  103. package/src/resources/skills/mandu-testing/rules/_template.md +65 -0
  104. package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -0
  105. package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -0
  106. package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -0
  107. package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -0
  108. package/src/resources/skills/mandu-ui/SKILL.md +117 -0
  109. package/src/resources/skills/mandu-ui/_sections.md +23 -0
  110. package/src/resources/skills/mandu-ui/_template.md +32 -0
  111. package/src/resources/skills/mandu-ui/metadata.json +13 -0
  112. package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -0
  113. package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -0
  114. package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -0
  115. package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -0
  116. package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -0
  117. package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -0
  118. package/src/resources/skills/recipes.ts +932 -0
  119. package/src/server.ts +3 -0
  120. package/src/tools/hydration.ts +8 -8
  121. package/src/tools/index.ts +1 -0
  122. package/src/tools/seo.ts +417 -0
@@ -1,231 +1,847 @@
1
- /**
2
- * Mandu Activity Monitor
3
- *
4
- * Real-time terminal dashboard for MCP server activity.
5
- * Opens automatically when the MCP server starts.
6
- * Shows all tool calls, watch events, errors, and agent behavior.
7
- */
8
-
9
- import fs from "fs";
10
- import path from "path";
11
- import { spawn, type ChildProcess } from "child_process";
12
-
13
- const TOOL_ICONS: Record<string, string> = {
14
- // Spec
15
- mandu_list_routes: "SPEC",
16
- mandu_get_route: "SPEC",
17
- mandu_add_route: "SPEC+",
18
- mandu_update_route: "SPEC~",
19
- mandu_delete_route: "SPEC-",
20
- mandu_validate_spec: "SPEC?",
21
- // Generate
22
- mandu_generate: "GEN",
23
- mandu_generate_status: "GEN?",
24
- // Guard
25
- mandu_guard_check: "GUARD",
26
- // Slot
27
- mandu_read_slot: "SLOT",
28
- mandu_write_slot: "SLOT~",
29
- mandu_validate_slot: "SLOT?",
30
- // Contract
31
- mandu_list_contracts: "CONTRACT",
32
- mandu_get_contract: "CONTRACT",
33
- mandu_create_contract: "CONTRACT+",
34
- mandu_validate_contracts: "CONTRACT?",
35
- mandu_sync_contract_slot: "SYNC",
36
- mandu_generate_openapi: "OPENAPI",
37
- mandu_update_route_contract: "CONTRACT~",
38
- // Transaction
39
- mandu_begin: "TX-BEGIN",
40
- mandu_commit: "TX-COMMIT",
41
- mandu_rollback: "TX-ROLLBACK",
42
- mandu_tx_status: "TX?",
43
- // History
44
- mandu_list_history: "HISTORY",
45
- mandu_get_snapshot: "SNAPSHOT",
46
- mandu_prune_history: "PRUNE",
47
- // Brain
48
- mandu_doctor: "DOCTOR",
49
- mandu_watch_start: "WATCH+",
50
- mandu_watch_status: "WATCH?",
51
- mandu_watch_stop: "WATCH-",
52
- mandu_check_location: "ARCH?",
53
- mandu_check_import: "IMPORT?",
54
- mandu_get_architecture: "ARCH",
55
- // Build
56
- mandu_build: "BUILD",
57
- mandu_build_status: "BUILD?",
58
- mandu_list_islands: "ISLAND",
59
- mandu_set_hydration: "HYDRA~",
60
- mandu_add_client_slot: "CLIENT+",
61
- // Error
62
- mandu_analyze_error: "ERROR",
63
- };
64
-
65
- function getTime(): string {
66
- return new Date().toLocaleTimeString("ko-KR", { hour12: false });
67
- }
68
-
69
- function summarizeArgs(args: Record<string, unknown> | null | undefined): string {
70
- if (!args || Object.keys(args).length === 0) return "";
71
- const entries = Object.entries(args)
72
- .filter(([_, v]) => v !== undefined && v !== null)
73
- .map(([k, v]) => {
74
- const val = typeof v === "string"
75
- ? (v.length > 40 ? v.slice(0, 40) + "..." : v)
76
- : JSON.stringify(v);
77
- return `${k}=${val}`;
78
- });
79
- return entries.length > 0 ? ` (${entries.join(", ")})` : "";
80
- }
81
-
82
- function summarizeResult(result: unknown): string {
83
- if (!result || typeof result !== "object") return "";
84
- const obj = result as Record<string, unknown>;
85
-
86
- // Common patterns
87
- if (obj.error) return ` >> ERROR: ${obj.error}`;
88
- if (obj.success === true) return obj.message ? ` >> ${obj.message}` : " >> OK";
89
- if (obj.success === false) return ` >> FAILED: ${obj.message || "unknown"}`;
90
- if (Array.isArray(obj.routes)) return ` >> ${obj.routes.length} routes`;
91
- if (obj.passed === true) return obj.message ? ` >> ${obj.message}` : " >> PASSED";
92
- if (obj.passed === false) return ` >> FAILED (${(obj.violations as unknown[])?.length || 0} violations)`;
93
- if (obj.valid === true) return obj.message ? ` >> ${obj.message}` : " >> VALID";
94
- if (obj.valid === false) return ` >> INVALID (${(obj.violations as unknown[])?.length || 0} violations)`;
95
- if (obj.generated) return " >> Generated";
96
- if (obj.status) return ` >> ${JSON.stringify(obj.status).slice(0, 60)}`;
97
-
98
- return "";
99
- }
100
-
101
- export class ActivityMonitor {
102
- private logFile: string;
103
- private logStream: fs.WriteStream | null = null;
104
- private tailProcess: ChildProcess | null = null;
105
- private projectRoot: string;
106
- private callCount = 0;
107
-
108
- constructor(projectRoot: string) {
109
- this.projectRoot = projectRoot;
110
- const manduDir = path.join(projectRoot, ".mandu");
111
- if (!fs.existsSync(manduDir)) {
112
- fs.mkdirSync(manduDir, { recursive: true });
113
- }
114
- this.logFile = path.join(manduDir, "activity.log");
115
- }
116
-
117
- start(): void {
118
- // Create/overwrite log file
119
- this.logStream = fs.createWriteStream(this.logFile, { flags: "w" });
120
-
121
- const time = getTime();
122
- const header =
123
- `\n` +
124
- ` ╔══════════════════════════════════════════════╗\n` +
125
- ` ║ MANDU MCP Activity Monitor ║\n` +
126
- ` ║ ║\n` +
127
- ` ║ ${time} ║\n` +
128
- ` ║ ${this.projectRoot.slice(-40).padEnd(40)} ║\n` +
129
- ` ╚══════════════════════════════════════════════╝\n\n`;
130
-
131
- this.logStream.write(header);
132
-
133
- // Auto-open terminal
134
- this.openTerminal();
135
- }
136
-
137
- stop(): void {
138
- if (this.tailProcess) {
139
- this.tailProcess.kill();
140
- this.tailProcess = null;
141
- }
142
- if (this.logStream) {
143
- this.logStream.end();
144
- this.logStream = null;
145
- }
146
- }
147
-
148
- /**
149
- * Log a tool call (invocation)
150
- */
151
- logTool(
152
- name: string,
153
- args?: Record<string, unknown> | null,
154
- _result?: unknown,
155
- error?: string,
156
- ): void {
157
- this.callCount++;
158
- const time = getTime();
159
- const tag = TOOL_ICONS[name] || name.replace("mandu_", "").toUpperCase();
160
- const argsStr = summarizeArgs(args);
161
-
162
- let line: string;
163
- if (error) {
164
- line = `${time} ✗ [${tag}]${argsStr}\n ERROR: ${error}\n`;
165
- } else {
166
- line = `${time} → [${tag}]${argsStr}\n`;
167
- }
168
-
169
- this.write(line);
170
- }
171
-
172
- /**
173
- * Log a tool result
174
- */
175
- logResult(name: string, result: unknown): void {
176
- const time = getTime();
177
- const tag = TOOL_ICONS[name] || name.replace("mandu_", "").toUpperCase();
178
- const summary = summarizeResult(result);
179
-
180
- if (summary) {
181
- this.write(`${time} ✓ [${tag}]${summary}\n`);
182
- }
183
- }
184
-
185
- /**
186
- * Log a watch event (called from watcher)
187
- */
188
- logWatch(level: string, ruleId: string, file: string, message: string): void {
189
- const time = getTime();
190
- const icon = level === "info" ? "ℹ" : "⚠";
191
- this.write(`${time} ${icon} [WATCH:${ruleId}] ${file}\n ${message}\n`);
192
- }
193
-
194
- /**
195
- * Log a custom event
196
- */
197
- logEvent(category: string, message: string): void {
198
- const time = getTime();
199
- this.write(`${time} [${category}] ${message}\n`);
200
- }
201
-
202
- private write(text: string): void {
203
- if (this.logStream) {
204
- this.logStream.write(text);
205
- }
206
- }
207
-
208
- private openTerminal(): void {
209
- try {
210
- if (process.platform === "win32") {
211
- this.tailProcess = spawn("cmd", [
212
- "/c", "start",
213
- "Mandu Activity Monitor",
214
- "powershell", "-NoExit", "-Command",
215
- `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; chcp 65001 | Out-Null; Get-Content '${this.logFile}' -Wait -Encoding UTF8`,
216
- ], { cwd: this.projectRoot, detached: true, stdio: "ignore" });
217
- } else if (process.platform === "darwin") {
218
- this.tailProcess = spawn("osascript", [
219
- "-e", `tell application "Terminal" to do script "tail -f '${this.logFile}'"`,
220
- ], { detached: true, stdio: "ignore" });
221
- } else {
222
- this.tailProcess = spawn("x-terminal-emulator", [
223
- "-e", `tail -f '${this.logFile}'`,
224
- ], { cwd: this.projectRoot, detached: true, stdio: "ignore" });
225
- }
226
- this.tailProcess?.unref();
227
- } catch {
228
- // Terminal auto-open failed silently
229
- }
230
- }
231
- }
1
+ /**
2
+ * Mandu Activity Monitor
3
+ *
4
+ * CLI-first real-time monitor for MCP server activity.
5
+ * Supports pretty (human) and JSON (agent) outputs with dedupe + batching.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { spawn, type ChildProcess } from "child_process";
11
+
12
+ const TOOL_ICONS: Record<string, string> = {
13
+ // Spec
14
+ mandu_list_routes: "SPEC",
15
+ mandu_get_route: "SPEC",
16
+ mandu_add_route: "SPEC+",
17
+ mandu_update_route: "SPEC~",
18
+ mandu_delete_route: "SPEC-",
19
+ mandu_validate_spec: "SPEC?",
20
+ // Generate
21
+ mandu_generate: "GEN",
22
+ mandu_generate_status: "GEN?",
23
+ // Guard
24
+ mandu_guard_check: "GUARD",
25
+ // Slot
26
+ mandu_read_slot: "SLOT",
27
+ mandu_write_slot: "SLOT~",
28
+ mandu_validate_slot: "SLOT?",
29
+ // Contract
30
+ mandu_list_contracts: "CONTRACT",
31
+ mandu_get_contract: "CONTRACT",
32
+ mandu_create_contract: "CONTRACT+",
33
+ mandu_validate_contracts: "CONTRACT?",
34
+ mandu_sync_contract_slot: "SYNC",
35
+ mandu_generate_openapi: "OPENAPI",
36
+ mandu_update_route_contract: "CONTRACT~",
37
+ // Transaction
38
+ mandu_begin: "TX-BEGIN",
39
+ mandu_commit: "TX-COMMIT",
40
+ mandu_rollback: "TX-ROLLBACK",
41
+ mandu_tx_status: "TX?",
42
+ // History
43
+ mandu_list_history: "HISTORY",
44
+ mandu_get_snapshot: "SNAPSHOT",
45
+ mandu_prune_history: "PRUNE",
46
+ // Brain
47
+ mandu_doctor: "DOCTOR",
48
+ mandu_watch_start: "WATCH+",
49
+ mandu_watch_status: "WATCH?",
50
+ mandu_watch_stop: "WATCH-",
51
+ mandu_check_location: "ARCH?",
52
+ mandu_check_import: "IMPORT?",
53
+ mandu_get_architecture: "ARCH",
54
+ // Build
55
+ mandu_build: "BUILD",
56
+ mandu_build_status: "BUILD?",
57
+ mandu_list_islands: "ISLAND",
58
+ mandu_set_hydration: "HYDRA~",
59
+ mandu_add_client_slot: "CLIENT+",
60
+ // Error
61
+ mandu_analyze_error: "ERROR",
62
+ };
63
+
64
+ type MonitorSeverity = "info" | "warn" | "error";
65
+ type MonitorOutputFormat = "pretty" | "json";
66
+ type MonitorOutputPreference = MonitorOutputFormat | "auto" | "console" | "agent";
67
+ const SCHEMA_VERSION = "1.0";
68
+
69
+ interface MonitorStoreConfig {
70
+ enabled?: boolean;
71
+ retentionDays?: number;
72
+ maxArchived?: number;
73
+ }
74
+
75
+ interface MonitorConfig {
76
+ output?: MonitorOutputPreference;
77
+ openTerminal?: boolean;
78
+ dedupeWindowMs?: number;
79
+ flushIntervalMs?: number;
80
+ summaryIntervalMs?: number;
81
+ store?: MonitorStoreConfig;
82
+ }
83
+
84
+ interface MonitorEvent {
85
+ ts: string;
86
+ type: string;
87
+ severity: MonitorSeverity;
88
+ source: string;
89
+ message?: string;
90
+ data?: Record<string, unknown>;
91
+ actionRequired?: boolean;
92
+ fingerprint?: string;
93
+ count?: number;
94
+ schemaVersion?: string;
95
+ }
96
+
97
+ interface DedupeEntry {
98
+ event: MonitorEvent;
99
+ count: number;
100
+ windowStart: number;
101
+ lastTs: number;
102
+ }
103
+
104
+ const DEFAULT_CONFIG: Required<MonitorConfig> = {
105
+ output: "auto",
106
+ openTerminal: true,
107
+ dedupeWindowMs: 1500,
108
+ flushIntervalMs: 500,
109
+ summaryIntervalMs: 30000,
110
+ store: {
111
+ enabled: true,
112
+ retentionDays: 7,
113
+ maxArchived: 20,
114
+ },
115
+ };
116
+
117
+ function normalizeOutput(
118
+ value: MonitorOutputPreference | undefined
119
+ ): MonitorOutputFormat | undefined {
120
+ if (!value) return undefined;
121
+ if (value === "json" || value === "agent") return "json";
122
+ if (value === "pretty" || value === "console") return "pretty";
123
+ return undefined;
124
+ }
125
+
126
+ function resolveOutputFormat(
127
+ preference: MonitorOutputPreference | undefined
128
+ ): MonitorOutputFormat {
129
+ const env = process.env;
130
+ const direct =
131
+ normalizeOutput(preference) ??
132
+ normalizeOutput(env.MANDU_MONITOR_FORMAT as MonitorOutputPreference) ??
133
+ normalizeOutput(env.MANDU_OUTPUT as MonitorOutputPreference);
134
+
135
+ if (direct) return direct;
136
+
137
+ const agentSignals = [
138
+ "MANDU_AGENT",
139
+ "CODEX_AGENT",
140
+ "CODEX",
141
+ "CLAUDE_CODE",
142
+ "ANTHROPIC_CLAUDE_CODE",
143
+ ];
144
+
145
+ for (const key of agentSignals) {
146
+ const value = env[key];
147
+ if (value === "1" || value === "true") {
148
+ return "json";
149
+ }
150
+ }
151
+
152
+ if (env.CI === "true") {
153
+ return "json";
154
+ }
155
+
156
+ if (process.stdout && !process.stdout.isTTY) {
157
+ return "json";
158
+ }
159
+
160
+ return "pretty";
161
+ }
162
+
163
+ function formatTime(ts: string): string {
164
+ return new Date(ts).toLocaleTimeString("ko-KR", { hour12: false });
165
+ }
166
+
167
+ function getTime(): string {
168
+ return formatTime(new Date().toISOString());
169
+ }
170
+
171
+ function summarizeArgs(args: Record<string, unknown> | null | undefined): string {
172
+ if (!args || Object.keys(args).length === 0) return "";
173
+ const entries = Object.entries(args)
174
+ .filter(([_, v]) => v !== undefined && v !== null)
175
+ .map(([k, v]) => {
176
+ const val =
177
+ typeof v === "string"
178
+ ? v.length > 40
179
+ ? v.slice(0, 40) + "..."
180
+ : v
181
+ : JSON.stringify(v);
182
+ return `${k}=${val}`;
183
+ });
184
+ return entries.length > 0 ? ` (${entries.join(", ")})` : "";
185
+ }
186
+
187
+ function summarizeResult(result: unknown): string {
188
+ if (!result || typeof result !== "object") return "";
189
+ const obj = result as Record<string, unknown>;
190
+
191
+ // Common patterns
192
+ if (obj.error) return ` >> ERROR: ${obj.error}`;
193
+ if (obj.success === true) return obj.message ? ` >> ${obj.message}` : " >> OK";
194
+ if (obj.success === false) return ` >> FAILED: ${obj.message || "unknown"}`;
195
+ if (Array.isArray(obj.routes)) return ` >> ${obj.routes.length} routes`;
196
+ if (obj.passed === true) return obj.message ? ` >> ${obj.message}` : " >> PASSED";
197
+ if (obj.passed === false) {
198
+ return ` >> FAILED (${(obj.violations as unknown[])?.length || 0} violations)`;
199
+ }
200
+ if (obj.valid === true) return obj.message ? ` >> ${obj.message}` : " >> VALID";
201
+ if (obj.valid === false) {
202
+ return ` >> INVALID (${(obj.violations as unknown[])?.length || 0} violations)`;
203
+ }
204
+ if (obj.generated) return " >> Generated";
205
+ if (obj.status) return ` >> ${JSON.stringify(obj.status).slice(0, 60)}`;
206
+
207
+ return "";
208
+ }
209
+
210
+ function mergeConfig(
211
+ base: Required<MonitorConfig>,
212
+ override: MonitorConfig
213
+ ): Required<MonitorConfig> {
214
+ return {
215
+ ...base,
216
+ ...override,
217
+ store: {
218
+ ...base.store,
219
+ ...override.store,
220
+ },
221
+ };
222
+ }
223
+
224
+ function loadMonitorConfig(projectRoot: string): MonitorConfig {
225
+ try {
226
+ const configPath = path.join(projectRoot, ".mandu", "monitor.config.json");
227
+ if (!fs.existsSync(configPath)) return {};
228
+ const raw = fs.readFileSync(configPath, "utf-8");
229
+ const parsed = JSON.parse(raw);
230
+ return typeof parsed === "object" && parsed ? parsed : {};
231
+ } catch {
232
+ return {};
233
+ }
234
+ }
235
+
236
+ function ensureManduDir(projectRoot: string): string {
237
+ const manduDir = path.join(projectRoot, ".mandu");
238
+ if (!fs.existsSync(manduDir)) {
239
+ fs.mkdirSync(manduDir, { recursive: true });
240
+ }
241
+ return manduDir;
242
+ }
243
+
244
+ function writeDefaultConfig(projectRoot: string, config: Required<MonitorConfig>): void {
245
+ try {
246
+ const configPath = path.join(projectRoot, ".mandu", "monitor.config.json");
247
+ if (fs.existsSync(configPath)) return;
248
+ const template = {
249
+ output: config.output,
250
+ openTerminal: config.openTerminal,
251
+ dedupeWindowMs: config.dedupeWindowMs,
252
+ flushIntervalMs: config.flushIntervalMs,
253
+ summaryIntervalMs: config.summaryIntervalMs,
254
+ store: {
255
+ enabled: config.store.enabled,
256
+ retentionDays: config.store.retentionDays,
257
+ maxArchived: config.store.maxArchived,
258
+ },
259
+ };
260
+ fs.writeFileSync(configPath, JSON.stringify(template, null, 2));
261
+ } catch {
262
+ // ignore config write errors
263
+ }
264
+ }
265
+
266
+ export class ActivityMonitor {
267
+ private logFile = "";
268
+ private logStream: fs.WriteStream | null = null;
269
+ private tailProcess: ChildProcess | null = null;
270
+ private projectRoot: string;
271
+ private config: Required<MonitorConfig>;
272
+ private outputFormat: MonitorOutputFormat;
273
+ private pending: MonitorEvent[] = [];
274
+ private dedupeMap = new Map<string, DedupeEntry>();
275
+ private flushTimer: NodeJS.Timeout | null = null;
276
+ private summaryTimer: NodeJS.Timeout | null = null;
277
+ private summaryCounts = { total: 0, info: 0, warn: 0, error: 0 };
278
+ private lastToolArgs = new Map<string, Record<string, unknown> | null>();
279
+
280
+ constructor(projectRoot: string) {
281
+ this.projectRoot = projectRoot;
282
+ const userConfig = loadMonitorConfig(projectRoot);
283
+ this.config = mergeConfig(DEFAULT_CONFIG, userConfig);
284
+ this.outputFormat = resolveOutputFormat(this.config.output);
285
+ }
286
+
287
+ start(): void {
288
+ const manduDir = ensureManduDir(this.projectRoot);
289
+ writeDefaultConfig(this.projectRoot, this.config);
290
+ const extension = this.outputFormat === "json" ? "jsonl" : "log";
291
+ this.logFile = path.join(manduDir, `activity.${extension}`);
292
+
293
+ if (this.config.store.enabled) {
294
+ this.archiveExistingLog();
295
+ this.pruneArchivedLogs();
296
+ }
297
+
298
+ this.logStream = fs.createWriteStream(this.logFile, { flags: "w" });
299
+
300
+ if (this.outputFormat === "pretty") {
301
+ const time = getTime();
302
+ const header =
303
+ `\n` +
304
+ ` ╔══════════════════════════════════════════════╗\n` +
305
+ ` ║ MANDU MCP Activity Monitor ║\n` +
306
+ ` ║ ║\n` +
307
+ ` ║ ${time} ║\n` +
308
+ ` ║ ${this.projectRoot.slice(-40).padEnd(40)} ║\n` +
309
+ ` ╚══════════════════════════════════════════════╝\n\n`;
310
+ this.logStream.write(header);
311
+ }
312
+
313
+ if (this.config.flushIntervalMs > 0) {
314
+ this.flushTimer = setInterval(
315
+ () => this.flush(false),
316
+ this.config.flushIntervalMs
317
+ );
318
+ }
319
+
320
+ if (
321
+ this.outputFormat === "pretty" &&
322
+ this.config.summaryIntervalMs > 0
323
+ ) {
324
+ this.summaryTimer = setInterval(
325
+ () => this.emitSummary(),
326
+ this.config.summaryIntervalMs
327
+ );
328
+ }
329
+
330
+ const openTerminal =
331
+ this.config.openTerminal && this.outputFormat === "pretty";
332
+ if (openTerminal) {
333
+ this.openTerminal();
334
+ }
335
+ }
336
+
337
+ stop(): void {
338
+ this.flush(true);
339
+ if (this.tailProcess) {
340
+ this.tailProcess.kill();
341
+ this.tailProcess = null;
342
+ }
343
+ if (this.flushTimer) {
344
+ clearInterval(this.flushTimer);
345
+ this.flushTimer = null;
346
+ }
347
+ if (this.summaryTimer) {
348
+ clearInterval(this.summaryTimer);
349
+ this.summaryTimer = null;
350
+ }
351
+ if (this.logStream) {
352
+ this.logStream.end();
353
+ this.logStream = null;
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Log a tool call (invocation)
359
+ */
360
+ logTool(
361
+ name: string,
362
+ args?: Record<string, unknown> | null,
363
+ _result?: unknown,
364
+ error?: string,
365
+ ): void {
366
+ this.lastToolArgs.set(name, args ?? null);
367
+ const argsStr = summarizeArgs(args);
368
+ const tag = TOOL_ICONS[name] || name.replace("mandu_", "").toUpperCase();
369
+
370
+ if (error) {
371
+ this.enqueue({
372
+ ts: new Date().toISOString(),
373
+ type: "tool.error",
374
+ severity: "error",
375
+ source: "tool",
376
+ message: `ERROR: ${error}`,
377
+ actionRequired: true,
378
+ fingerprint: `tool:error:${name}:${argsStr}`,
379
+ data: { tool: name, tag, args, argsSummary: argsStr, error },
380
+ });
381
+ return;
382
+ }
383
+
384
+ this.enqueue({
385
+ ts: new Date().toISOString(),
386
+ type: "tool.call",
387
+ severity: "info",
388
+ source: "tool",
389
+ data: { tool: name, tag, args, argsSummary: argsStr },
390
+ });
391
+ }
392
+
393
+ /**
394
+ * Log a tool result
395
+ */
396
+ logResult(name: string, result: unknown): void {
397
+ const lastArgs = this.lastToolArgs.get(name) ?? null;
398
+ if (this.lastToolArgs.has(name)) {
399
+ this.lastToolArgs.delete(name);
400
+ }
401
+ const summary = summarizeResult(result);
402
+ const tag = TOOL_ICONS[name] || name.replace("mandu_", "").toUpperCase();
403
+
404
+ if (summary) {
405
+ this.enqueue({
406
+ ts: new Date().toISOString(),
407
+ type: "tool.result",
408
+ severity: "info",
409
+ source: "tool",
410
+ data: { tool: name, tag, summary },
411
+ });
412
+ }
413
+
414
+ this.logStructuredResult(name, result, lastArgs);
415
+ }
416
+
417
+ /**
418
+ * Log a watch event (called from watcher)
419
+ */
420
+ logWatch(level: string, ruleId: string, file: string, message: string): void {
421
+ const severity: MonitorSeverity =
422
+ level === "error" ? "error" : level === "info" ? "info" : "warn";
423
+
424
+ this.enqueue({
425
+ ts: new Date().toISOString(),
426
+ type: "watch.warning",
427
+ severity,
428
+ source: "watch",
429
+ message,
430
+ actionRequired: severity !== "info",
431
+ fingerprint: `watch:${ruleId}:${file}:${message}`,
432
+ data: { ruleId, file, level, message },
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Log a custom event
438
+ */
439
+ logEvent(category: string, message: string): void {
440
+ this.enqueue({
441
+ ts: new Date().toISOString(),
442
+ type: "system.event",
443
+ severity: "info",
444
+ source: "system",
445
+ message,
446
+ data: { category },
447
+ });
448
+ }
449
+
450
+ private logStructuredResult(
451
+ name: string,
452
+ result: unknown,
453
+ lastArgs: Record<string, unknown> | null
454
+ ): void {
455
+ if (!result || typeof result !== "object") return;
456
+ const obj = result as Record<string, unknown>;
457
+
458
+ if (name === "mandu_guard_check") {
459
+ const violations = Array.isArray(obj.violations) ? obj.violations : [];
460
+ const passed = obj.passed === true;
461
+ const count = violations.length;
462
+
463
+ if (count > 0) {
464
+ this.enqueue({
465
+ ts: new Date().toISOString(),
466
+ type: "guard.summary",
467
+ severity: passed ? "info" : "error",
468
+ source: "guard",
469
+ actionRequired: !passed,
470
+ fingerprint: `guard:summary:${count}`,
471
+ data: { passed, count },
472
+ });
473
+
474
+ for (const violation of violations) {
475
+ const v = violation as Record<string, unknown>;
476
+ const ruleId = (v.ruleId as string | undefined) ?? "UNKNOWN_RULE";
477
+ const file = v.file as string | undefined;
478
+ const line = typeof v.line === "number" ? v.line : undefined;
479
+ const column = typeof v.column === "number" ? v.column : undefined;
480
+ const message =
481
+ (v.message as string | undefined) ??
482
+ (v.reason as string | undefined) ??
483
+ "Guard violation";
484
+ const suggestion =
485
+ (v.suggestion as string | undefined) ??
486
+ (v.tip as string | undefined);
487
+
488
+ this.enqueue({
489
+ ts: new Date().toISOString(),
490
+ type: "guard.violation",
491
+ severity: "error",
492
+ source: "guard",
493
+ message,
494
+ actionRequired: true,
495
+ fingerprint: `guard:${ruleId}:${file ?? ""}:${line ?? ""}:${column ?? ""}`,
496
+ data: {
497
+ ruleId,
498
+ file,
499
+ line,
500
+ column,
501
+ message,
502
+ suggestion,
503
+ },
504
+ });
505
+ }
506
+ }
507
+
508
+ return;
509
+ }
510
+
511
+ if (name === "mandu_check_location") {
512
+ const allowed = obj.allowed === true;
513
+ if (allowed) return;
514
+
515
+ const violations = Array.isArray(obj.violations) ? obj.violations : [];
516
+ const argPath = typeof lastArgs?.path === "string" ? lastArgs.path : undefined;
517
+ for (const violation of violations) {
518
+ const v = violation as Record<string, unknown>;
519
+ const ruleId = (v.rule as string | undefined) ?? "LOCATION_RULE";
520
+ const message = (v.message as string | undefined) ?? "Location violation";
521
+ const severity = (v.severity as MonitorSeverity | undefined) ?? "warn";
522
+
523
+ this.enqueue({
524
+ ts: new Date().toISOString(),
525
+ type: "guard.violation",
526
+ severity,
527
+ source: "architecture",
528
+ message,
529
+ actionRequired: severity !== "info",
530
+ fingerprint: `guard:location:${ruleId}:${argPath ?? ""}`,
531
+ data: {
532
+ ruleId,
533
+ file: argPath,
534
+ message,
535
+ suggestion: obj.suggestion,
536
+ recommendedPath: obj.recommendedPath,
537
+ },
538
+ });
539
+ }
540
+ return;
541
+ }
542
+
543
+ if (name === "mandu_check_import") {
544
+ const allowed = obj.allowed === true;
545
+ if (allowed) return;
546
+
547
+ const violations = Array.isArray(obj.violations) ? obj.violations : [];
548
+ const sourceFile = typeof lastArgs?.sourceFile === "string" ? lastArgs.sourceFile : undefined;
549
+ for (const violation of violations) {
550
+ const v = violation as Record<string, unknown>;
551
+ const message = (v.reason as string | undefined) ?? "Import violation";
552
+ const suggestion = v.suggestion as string | undefined;
553
+ const importTarget = typeof v.import === "string" ? v.import : undefined;
554
+
555
+ this.enqueue({
556
+ ts: new Date().toISOString(),
557
+ type: "guard.violation",
558
+ severity: "error",
559
+ source: "architecture",
560
+ message,
561
+ actionRequired: true,
562
+ fingerprint: `guard:import:${sourceFile ?? ""}:${importTarget ?? ""}`,
563
+ data: {
564
+ ruleId: "IMPORT_RULE",
565
+ file: sourceFile,
566
+ import: importTarget,
567
+ message,
568
+ suggestion,
569
+ },
570
+ });
571
+ }
572
+ return;
573
+ }
574
+
575
+ if (
576
+ name === "mandu_add_route" ||
577
+ name === "mandu_update_route" ||
578
+ name === "mandu_delete_route"
579
+ ) {
580
+ if (obj.success !== true) return;
581
+ const action =
582
+ name === "mandu_add_route"
583
+ ? "add"
584
+ : name === "mandu_update_route"
585
+ ? "update"
586
+ : "delete";
587
+ const route =
588
+ (obj.route as Record<string, unknown> | undefined) ??
589
+ (obj.deletedRoute as Record<string, unknown> | undefined);
590
+
591
+ this.enqueue({
592
+ ts: new Date().toISOString(),
593
+ type: "routes.change",
594
+ severity: "info",
595
+ source: "routes",
596
+ actionRequired: false,
597
+ fingerprint: `routes:${action}:${route?.id ?? ""}`,
598
+ data: {
599
+ action,
600
+ routeId: route?.id,
601
+ pattern: route?.pattern,
602
+ kind: route?.kind,
603
+ },
604
+ });
605
+ }
606
+ }
607
+
608
+ private enqueue(event: MonitorEvent): void {
609
+ if (!this.logStream) return;
610
+ const now = Date.now();
611
+
612
+ if (!event.fingerprint || this.config.dedupeWindowMs <= 0) {
613
+ this.pending.push(event);
614
+ return;
615
+ }
616
+
617
+ const existing = this.dedupeMap.get(event.fingerprint);
618
+ if (!existing) {
619
+ this.dedupeMap.set(event.fingerprint, {
620
+ event,
621
+ count: 1,
622
+ windowStart: now,
623
+ lastTs: now,
624
+ });
625
+ return;
626
+ }
627
+
628
+ if (now - existing.windowStart >= this.config.dedupeWindowMs) {
629
+ this.pending.push(this.withCount(existing));
630
+ this.dedupeMap.set(event.fingerprint, {
631
+ event,
632
+ count: 1,
633
+ windowStart: now,
634
+ lastTs: now,
635
+ });
636
+ return;
637
+ }
638
+
639
+ existing.count += 1;
640
+ existing.lastTs = now;
641
+ existing.event = event;
642
+ }
643
+
644
+ private flush(force: boolean): void {
645
+ if (!this.logStream) return;
646
+ const now = Date.now();
647
+ const windowMs = this.config.dedupeWindowMs;
648
+
649
+ for (const [key, entry] of this.dedupeMap) {
650
+ const idleMs = now - entry.lastTs;
651
+ if (force || idleMs >= windowMs) {
652
+ this.pending.push(this.withCount(entry));
653
+ this.dedupeMap.delete(key);
654
+ }
655
+ }
656
+
657
+ if (this.pending.length === 0) return;
658
+
659
+ for (const event of this.pending) {
660
+ const line = this.formatEvent(event);
661
+ if (!line) continue;
662
+ this.write(line);
663
+ this.updateSummary(event);
664
+ }
665
+
666
+ this.pending = [];
667
+ }
668
+
669
+ private withCount(entry: DedupeEntry): MonitorEvent {
670
+ return {
671
+ ...entry.event,
672
+ count: entry.count,
673
+ };
674
+ }
675
+
676
+ private formatEvent(event: MonitorEvent): string {
677
+ if (this.outputFormat === "json") {
678
+ const payload = event.schemaVersion
679
+ ? event
680
+ : { schemaVersion: SCHEMA_VERSION, ...event };
681
+ return `${JSON.stringify(payload)}\n`;
682
+ }
683
+ return this.formatEventPretty(event);
684
+ }
685
+
686
+ private formatEventPretty(event: MonitorEvent): string {
687
+ const time = formatTime(event.ts);
688
+ const countSuffix = event.count && event.count > 1 ? ` x${event.count}` : "";
689
+
690
+ switch (event.type) {
691
+ case "tool.call": {
692
+ const tag = event.data?.tag as string | undefined;
693
+ const argsSummary = event.data?.argsSummary as string | undefined;
694
+ return `${time} → [${tag ?? "TOOL"}]${argsSummary ?? ""}${countSuffix}\n`;
695
+ }
696
+ case "tool.error": {
697
+ const tag = event.data?.tag as string | undefined;
698
+ const argsSummary = event.data?.argsSummary as string | undefined;
699
+ const message = event.message ?? "ERROR";
700
+ return `${time} ✗ [${tag ?? "TOOL"}]${argsSummary ?? ""}${countSuffix}\n ${message}\n`;
701
+ }
702
+ case "tool.result": {
703
+ const tag = event.data?.tag as string | undefined;
704
+ const summary = event.data?.summary as string | undefined;
705
+ if (!summary) return "";
706
+ return `${time} ✓ [${tag ?? "TOOL"}]${summary}${countSuffix}\n`;
707
+ }
708
+ case "watch.warning": {
709
+ const ruleId = event.data?.ruleId as string | undefined;
710
+ const file = event.data?.file as string | undefined;
711
+ const message = event.message ?? "";
712
+ const icon = event.severity === "info" ? "ℹ" : "⚠";
713
+ return `${time} ${icon} [WATCH:${ruleId ?? "UNKNOWN"}] ${file ?? ""}${countSuffix}\n ${message}\n`;
714
+ }
715
+ case "system.event": {
716
+ const category = event.data?.category as string | undefined;
717
+ const message = event.message ?? "";
718
+ return `${time} [${category ?? "SYSTEM"}] ${message}${countSuffix}\n`;
719
+ }
720
+ case "monitor.summary": {
721
+ return `${time} · SUMMARY ${event.message ?? ""}\n`;
722
+ }
723
+ default:
724
+ return `${time} [${event.type}] ${event.message ?? ""}${countSuffix}\n`;
725
+ }
726
+ }
727
+
728
+ private updateSummary(event: MonitorEvent): void {
729
+ const count = event.count ?? 1;
730
+ this.summaryCounts.total += count;
731
+ this.summaryCounts[event.severity] += count;
732
+ }
733
+
734
+ private emitSummary(): void {
735
+ if (this.summaryCounts.total === 0) return;
736
+ const seconds = Math.round(this.config.summaryIntervalMs / 1000);
737
+ const message = `last ${seconds}s · total=${this.summaryCounts.total} · error=${this.summaryCounts.error} · warn=${this.summaryCounts.warn} · info=${this.summaryCounts.info}`;
738
+ this.summaryCounts = { total: 0, info: 0, warn: 0, error: 0 };
739
+ const summaryEvent: MonitorEvent = {
740
+ ts: new Date().toISOString(),
741
+ type: "monitor.summary",
742
+ severity: "info",
743
+ source: "monitor",
744
+ message,
745
+ };
746
+ const line = this.formatEvent(summaryEvent);
747
+ if (line) {
748
+ this.write(line);
749
+ }
750
+ }
751
+
752
+ private write(text: string): void {
753
+ if (this.logStream) {
754
+ this.logStream.write(text);
755
+ }
756
+ }
757
+
758
+ private archiveExistingLog(): void {
759
+ try {
760
+ if (!fs.existsSync(this.logFile)) return;
761
+ const dir = path.dirname(this.logFile);
762
+ const base = path.basename(this.logFile);
763
+ const stamp = new Date()
764
+ .toISOString()
765
+ .replace(/[:.]/g, "")
766
+ .slice(0, 15);
767
+ const archived = path.join(dir, `${base}.${stamp}.bak`);
768
+ fs.renameSync(this.logFile, archived);
769
+ } catch {
770
+ // ignore archive errors
771
+ }
772
+ }
773
+
774
+ private pruneArchivedLogs(): void {
775
+ try {
776
+ const dir = path.dirname(this.logFile);
777
+ const base = path.basename(this.logFile);
778
+ const files = fs
779
+ .readdirSync(dir)
780
+ .filter((file) => file.startsWith(`${base}.`) && file.endsWith(".bak"));
781
+
782
+ const entries = files
783
+ .map((file) => {
784
+ const fullPath = path.join(dir, file);
785
+ const stat = fs.statSync(fullPath);
786
+ return { file, fullPath, mtime: stat.mtime.getTime() };
787
+ })
788
+ .sort((a, b) => b.mtime - a.mtime);
789
+
790
+ const retentionMs =
791
+ (this.config.store.retentionDays || 7) * 24 * 60 * 60 * 1000;
792
+ const now = Date.now();
793
+
794
+ for (const entry of entries) {
795
+ if (now - entry.mtime > retentionMs) {
796
+ fs.unlinkSync(entry.fullPath);
797
+ }
798
+ }
799
+
800
+ const maxArchived = this.config.store.maxArchived || 20;
801
+ const remaining = entries.filter((entry) => fs.existsSync(entry.fullPath));
802
+ if (remaining.length > maxArchived) {
803
+ const toRemove = remaining.slice(maxArchived);
804
+ for (const entry of toRemove) {
805
+ fs.unlinkSync(entry.fullPath);
806
+ }
807
+ }
808
+ } catch {
809
+ // ignore prune errors
810
+ }
811
+ }
812
+
813
+ private openTerminal(): void {
814
+ try {
815
+ if (process.platform === "win32") {
816
+ this.tailProcess = spawn(
817
+ "cmd",
818
+ [
819
+ "/c",
820
+ "start",
821
+ "Mandu Activity Monitor",
822
+ "powershell",
823
+ "-NoExit",
824
+ "-Command",
825
+ `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; chcp 65001 | Out-Null; Get-Content '${this.logFile}' -Wait -Encoding UTF8`,
826
+ ],
827
+ { cwd: this.projectRoot, detached: true, stdio: "ignore" }
828
+ );
829
+ } else if (process.platform === "darwin") {
830
+ this.tailProcess = spawn(
831
+ "osascript",
832
+ ["-e", `tell application "Terminal" to do script "tail -f '${this.logFile}'"`],
833
+ { detached: true, stdio: "ignore" }
834
+ );
835
+ } else {
836
+ this.tailProcess = spawn(
837
+ "x-terminal-emulator",
838
+ ["-e", `tail -f '${this.logFile}'`],
839
+ { cwd: this.projectRoot, detached: true, stdio: "ignore" }
840
+ );
841
+ }
842
+ this.tailProcess?.unref();
843
+ } catch {
844
+ // Terminal auto-open failed silently
845
+ }
846
+ }
847
+ }