hivemind-context-governance 1.3.0

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 (139) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +21 -0
  3. package/README.md +218 -0
  4. package/dist/cli/init.d.ts +23 -0
  5. package/dist/cli/init.d.ts.map +1 -0
  6. package/dist/cli/init.js +175 -0
  7. package/dist/cli/init.js.map +1 -0
  8. package/dist/cli.d.ts +14 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +162 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/dashboard/i18n/en.d.ts +43 -0
  13. package/dist/dashboard/i18n/en.d.ts.map +1 -0
  14. package/dist/dashboard/i18n/en.js +43 -0
  15. package/dist/dashboard/i18n/en.js.map +1 -0
  16. package/dist/dashboard/i18n/index.d.ts +10 -0
  17. package/dist/dashboard/i18n/index.d.ts.map +1 -0
  18. package/dist/dashboard/i18n/index.js +14 -0
  19. package/dist/dashboard/i18n/index.js.map +1 -0
  20. package/dist/dashboard/i18n/vi.d.ts +43 -0
  21. package/dist/dashboard/i18n/vi.d.ts.map +1 -0
  22. package/dist/dashboard/i18n/vi.js +43 -0
  23. package/dist/dashboard/i18n/vi.js.map +1 -0
  24. package/dist/dashboard/server.d.ts +23 -0
  25. package/dist/dashboard/server.d.ts.map +1 -0
  26. package/dist/dashboard/server.js +282 -0
  27. package/dist/dashboard/server.js.map +1 -0
  28. package/dist/hooks/compaction.d.ts +21 -0
  29. package/dist/hooks/compaction.d.ts.map +1 -0
  30. package/dist/hooks/compaction.js +79 -0
  31. package/dist/hooks/compaction.js.map +1 -0
  32. package/dist/hooks/index.d.ts +8 -0
  33. package/dist/hooks/index.d.ts.map +1 -0
  34. package/dist/hooks/index.js +7 -0
  35. package/dist/hooks/index.js.map +1 -0
  36. package/dist/hooks/session-lifecycle.d.ts +28 -0
  37. package/dist/hooks/session-lifecycle.d.ts.map +1 -0
  38. package/dist/hooks/session-lifecycle.js +94 -0
  39. package/dist/hooks/session-lifecycle.js.map +1 -0
  40. package/dist/hooks/tool-gate.d.ts +28 -0
  41. package/dist/hooks/tool-gate.d.ts.map +1 -0
  42. package/dist/hooks/tool-gate.js +152 -0
  43. package/dist/hooks/tool-gate.js.map +1 -0
  44. package/dist/index.d.ts +19 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +159 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/lib/complexity.d.ts +41 -0
  49. package/dist/lib/complexity.d.ts.map +1 -0
  50. package/dist/lib/complexity.js +67 -0
  51. package/dist/lib/complexity.js.map +1 -0
  52. package/dist/lib/context-refresh.d.ts +46 -0
  53. package/dist/lib/context-refresh.d.ts.map +1 -0
  54. package/dist/lib/context-refresh.js +77 -0
  55. package/dist/lib/context-refresh.js.map +1 -0
  56. package/dist/lib/index.d.ts +10 -0
  57. package/dist/lib/index.d.ts.map +1 -0
  58. package/dist/lib/index.js +10 -0
  59. package/dist/lib/index.js.map +1 -0
  60. package/dist/lib/logging.d.ts +13 -0
  61. package/dist/lib/logging.d.ts.map +1 -0
  62. package/dist/lib/logging.js +40 -0
  63. package/dist/lib/logging.js.map +1 -0
  64. package/dist/lib/persistence.d.ts +15 -0
  65. package/dist/lib/persistence.d.ts.map +1 -0
  66. package/dist/lib/persistence.js +62 -0
  67. package/dist/lib/persistence.js.map +1 -0
  68. package/dist/lib/planning-fs.d.ts +34 -0
  69. package/dist/lib/planning-fs.d.ts.map +1 -0
  70. package/dist/lib/planning-fs.js +134 -0
  71. package/dist/lib/planning-fs.js.map +1 -0
  72. package/dist/lib/sentiment.d.ts +31 -0
  73. package/dist/lib/sentiment.d.ts.map +1 -0
  74. package/dist/lib/sentiment.js +158 -0
  75. package/dist/lib/sentiment.js.map +1 -0
  76. package/dist/schemas/brain-state.d.ts +60 -0
  77. package/dist/schemas/brain-state.d.ts.map +1 -0
  78. package/dist/schemas/brain-state.js +176 -0
  79. package/dist/schemas/brain-state.js.map +1 -0
  80. package/dist/schemas/config.d.ts +51 -0
  81. package/dist/schemas/config.d.ts.map +1 -0
  82. package/dist/schemas/config.js +128 -0
  83. package/dist/schemas/config.js.map +1 -0
  84. package/dist/schemas/hierarchy.d.ts +22 -0
  85. package/dist/schemas/hierarchy.d.ts.map +1 -0
  86. package/dist/schemas/hierarchy.js +27 -0
  87. package/dist/schemas/hierarchy.js.map +1 -0
  88. package/dist/schemas/index.d.ts +7 -0
  89. package/dist/schemas/index.d.ts.map +1 -0
  90. package/dist/schemas/index.js +7 -0
  91. package/dist/schemas/index.js.map +1 -0
  92. package/dist/tools/compact-session.d.ts +15 -0
  93. package/dist/tools/compact-session.d.ts.map +1 -0
  94. package/dist/tools/compact-session.js +76 -0
  95. package/dist/tools/compact-session.js.map +1 -0
  96. package/dist/tools/declare-intent.d.ts +15 -0
  97. package/dist/tools/declare-intent.d.ts.map +1 -0
  98. package/dist/tools/declare-intent.js +83 -0
  99. package/dist/tools/declare-intent.js.map +1 -0
  100. package/dist/tools/index.d.ts +14 -0
  101. package/dist/tools/index.d.ts.map +1 -0
  102. package/dist/tools/index.js +14 -0
  103. package/dist/tools/index.js.map +1 -0
  104. package/dist/tools/map-context.d.ts +15 -0
  105. package/dist/tools/map-context.d.ts.map +1 -0
  106. package/dist/tools/map-context.js +85 -0
  107. package/dist/tools/map-context.js.map +1 -0
  108. package/dist/tools/self-rate.d.ts +15 -0
  109. package/dist/tools/self-rate.d.ts.map +1 -0
  110. package/dist/tools/self-rate.js +76 -0
  111. package/dist/tools/self-rate.js.map +1 -0
  112. package/package.json +71 -0
  113. package/src/cli/init.ts +214 -0
  114. package/src/cli.ts +178 -0
  115. package/src/dashboard/i18n/en.ts +43 -0
  116. package/src/dashboard/i18n/index.ts +20 -0
  117. package/src/dashboard/i18n/vi.ts +43 -0
  118. package/src/dashboard/server.ts +372 -0
  119. package/src/hooks/compaction.ts +104 -0
  120. package/src/hooks/index.ts +8 -0
  121. package/src/hooks/session-lifecycle.ts +133 -0
  122. package/src/hooks/tool-gate.ts +205 -0
  123. package/src/index.ts +206 -0
  124. package/src/lib/complexity.ts +96 -0
  125. package/src/lib/context-refresh.ts +107 -0
  126. package/src/lib/index.ts +10 -0
  127. package/src/lib/logging.ts +53 -0
  128. package/src/lib/persistence.ts +83 -0
  129. package/src/lib/planning-fs.ts +187 -0
  130. package/src/lib/sentiment.ts +183 -0
  131. package/src/schemas/brain-state.ts +257 -0
  132. package/src/schemas/config.ts +184 -0
  133. package/src/schemas/hierarchy.ts +58 -0
  134. package/src/schemas/index.ts +7 -0
  135. package/src/tools/compact-session.ts +97 -0
  136. package/src/tools/declare-intent.ts +104 -0
  137. package/src/tools/index.ts +14 -0
  138. package/src/tools/map-context.ts +105 -0
  139. package/src/tools/self-rate.ts +86 -0
package/src/cli.ts ADDED
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HiveMind CLI — One-command initialization and management.
4
+ *
5
+ * Usage:
6
+ * npx hivemind-context-governance — Initialize (default)
7
+ * npx hivemind-context-governance init — Same as above
8
+ * npx hivemind-context-governance status — Show current brain state
9
+ *
10
+ * CRITICAL: NO console.log in library code. CLI is the ONLY
11
+ * place where console output is allowed (it IS the user interface).
12
+ */
13
+
14
+ import { argv } from "node:process"
15
+ import { initProject } from "./cli/init.js"
16
+ import { createStateManager, loadConfig } from "./lib/persistence.js"
17
+ import { listArchives } from "./lib/planning-fs.js"
18
+
19
+ const COMMANDS = ["init", "status", "compact", "dashboard", "help"] as const
20
+ type Command = (typeof COMMANDS)[number]
21
+
22
+ function printHelp(): void {
23
+ const help = `
24
+ HiveMind Context Governance — CLI
25
+
26
+ Usage:
27
+ npx hivemind-context-governance [command] [options]
28
+
29
+ Commands:
30
+ (default) Initialize HiveMind in current project
31
+ init Same as default — initialize project
32
+ status Show current session and governance state
33
+ compact Archive current session and reset
34
+ dashboard Launch dashboard server
35
+ help Show this help message
36
+
37
+ Options:
38
+ --lang <en|vi> Language (default: en)
39
+ --mode <permissive|assisted|strict> Governance mode (default: assisted)
40
+ --expert <beginner|intermediate|advanced|expert> Expert level (default: intermediate)
41
+ --style <explanatory|outline|skeptical|architecture|minimal> Output style (default: explanatory)
42
+ --code-review Require code review before accepting
43
+ --tdd Enforce test-driven development
44
+
45
+ Examples:
46
+ npx hivemind-context-governance
47
+ npx hivemind-context-governance --mode strict --lang vi
48
+ npx hivemind-context-governance status
49
+ `
50
+ // CLI is the user interface — console output is allowed here
51
+ // eslint-disable-next-line no-console
52
+ console.log(help)
53
+ }
54
+
55
+ async function showStatus(directory: string): Promise<void> {
56
+ const stateManager = createStateManager(directory)
57
+ const config = await loadConfig(directory)
58
+ const state = await stateManager.load()
59
+
60
+ if (!state) {
61
+ // eslint-disable-next-line no-console
62
+ console.log("No active session. Run 'npx hivemind-context-governance' first.")
63
+ return
64
+ }
65
+
66
+ const archives = await listArchives(directory)
67
+
68
+ const status = `
69
+ ┌─ HiveMind Status ────────────────────────┐
70
+ │ Session: ${state.session.id.padEnd(30)}│
71
+ │ Status: ${state.session.governance_status.padEnd(30)}│
72
+ │ Mode: ${state.session.mode.padEnd(30)}│
73
+ │ Govern: ${config.governance_mode.padEnd(30)}│
74
+ ├─ Hierarchy ──────────────────────────────┤
75
+ │ Trajectory: ${(state.hierarchy.trajectory || "(none)").padEnd(27)}│
76
+ │ Tactic: ${(state.hierarchy.tactic || "(none)").padEnd(27)}│
77
+ │ Action: ${(state.hierarchy.action || "(none)").padEnd(27)}│
78
+ ├─ Metrics ────────────────────────────────┤
79
+ │ Turns: ${String(state.metrics.turn_count).padEnd(30)}│
80
+ │ Drift: ${(state.metrics.drift_score + "/100").padEnd(30)}│
81
+ │ Files: ${String(state.metrics.files_touched.length).padEnd(30)}│
82
+ │ Updates: ${String(state.metrics.context_updates).padEnd(30)}│
83
+ │ Archive: ${(archives.length + " sessions").padEnd(30)}│
84
+ └──────────────────────────────────────────┘
85
+ `
86
+ // eslint-disable-next-line no-console
87
+ console.log(status)
88
+ }
89
+
90
+ async function main(): Promise<void> {
91
+ const args = argv.slice(2)
92
+
93
+ // Parse flags from all args
94
+ const flags: Record<string, string> = {}
95
+ const positionalArgs: string[] = []
96
+ for (let i = 0; i < args.length; i++) {
97
+ if (args[i].startsWith("--") && args[i + 1] && !args[i + 1].startsWith("--")) {
98
+ flags[args[i].slice(2)] = args[i + 1]
99
+ i++
100
+ } else if (args[i].startsWith("--")) {
101
+ flags[args[i].slice(2)] = ""
102
+ } else {
103
+ positionalArgs.push(args[i])
104
+ }
105
+ }
106
+
107
+ // Default to "init" when no command given (npx hivemind-context-governance)
108
+ const command = (positionalArgs[0] ?? "init") as Command
109
+ const directory = process.cwd()
110
+
111
+ switch (command) {
112
+ case "init":
113
+ await initProject(directory, {
114
+ language: (flags["lang"] as "en" | "vi") ?? undefined,
115
+ governanceMode:
116
+ (flags["mode"] as "permissive" | "assisted" | "strict") ?? undefined,
117
+ expertLevel: (flags["expert"] as "beginner" | "intermediate" | "advanced" | "expert") ?? undefined,
118
+ outputStyle: (flags["style"] as "explanatory" | "outline" | "skeptical" | "architecture" | "minimal") ?? undefined,
119
+ requireCodeReview: "code-review" in flags,
120
+ enforceTdd: "tdd" in flags,
121
+ })
122
+ break
123
+
124
+ case "status":
125
+ await showStatus(directory)
126
+ break
127
+
128
+ case "compact":
129
+ // eslint-disable-next-line no-console
130
+ console.log("Manual compaction: use compact_session tool within OpenCode.")
131
+ break
132
+
133
+ case "dashboard": {
134
+ // Lazy-load dashboard to avoid pulling heavy deps on init/status
135
+ const { createDashboardServer } = await import("./dashboard/server.js")
136
+ const port = flags["port"] ? parseInt(flags["port"], 10) : undefined
137
+ const server = createDashboardServer(directory, { port })
138
+ try {
139
+ await server.start()
140
+ // eslint-disable-next-line no-console
141
+ console.log(`HiveMind Dashboard running at ${server.url}`)
142
+ // eslint-disable-next-line no-console
143
+ console.log(`\nPress Ctrl+C to stop`)
144
+
145
+ // Keep running until interrupted
146
+ await new Promise(() => {
147
+ process.on("SIGINT", async () => {
148
+ // eslint-disable-next-line no-console
149
+ console.log("\nShutting down...")
150
+ await server.stop()
151
+ process.exit(0)
152
+ })
153
+ })
154
+ } catch (err) {
155
+ // eslint-disable-next-line no-console
156
+ console.error("Failed to start dashboard:", err)
157
+ process.exit(1)
158
+ }
159
+ break
160
+ }
161
+
162
+ case "help":
163
+ printHelp()
164
+ break
165
+
166
+ default:
167
+ // eslint-disable-next-line no-console
168
+ console.log(`Unknown command: ${command}`)
169
+ printHelp()
170
+ break
171
+ }
172
+ }
173
+
174
+ main().catch((err) => {
175
+ // eslint-disable-next-line no-console
176
+ console.error("Error:", err)
177
+ process.exit(1)
178
+ })
@@ -0,0 +1,43 @@
1
+ /**
2
+ * English i18n strings — HiveMind Dashboard
3
+ */
4
+
5
+ export const en = {
6
+ dashboard: {
7
+ title: "HiveMind Context Governance",
8
+ hierarchy: {
9
+ trajectory: "TRAJECTORY",
10
+ tactic: "TACTIC",
11
+ action: "ACTION",
12
+ status: {
13
+ pending: "PENDING",
14
+ active: "ACTIVE",
15
+ complete: "COMPLETE",
16
+ blocked: "BLOCKED",
17
+ },
18
+ },
19
+ agentLog: {
20
+ title: "AGENT ACTIONS",
21
+ empty: "No actions yet...",
22
+ timestamp: "Time",
23
+ },
24
+ metrics: {
25
+ title: "METRICS",
26
+ turns: "Turns",
27
+ drift: "Drift",
28
+ files: "Files",
29
+ status: "Status",
30
+ },
31
+ planning: {
32
+ title: "PLANNING FILES",
33
+ index: "index.md",
34
+ active: "active.md",
35
+ archive: "Archive",
36
+ },
37
+ governance: {
38
+ locked: "LOCKED",
39
+ open: "OPEN",
40
+ warning: "Declare intent to unlock",
41
+ },
42
+ },
43
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * i18n barrel — translation loader
3
+ */
4
+
5
+ import { en } from "./en.js"
6
+ import { vi } from "./vi.js"
7
+
8
+ export type I18nStrings = typeof en
9
+ export type SupportedLanguage = "en" | "vi"
10
+
11
+ const translations: Record<SupportedLanguage, I18nStrings> = {
12
+ en,
13
+ vi,
14
+ }
15
+
16
+ export function getTranslations(lang: SupportedLanguage = "en"): I18nStrings {
17
+ return translations[lang] ?? translations.en
18
+ }
19
+
20
+ export { en, vi }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Vietnamese i18n strings — HiveMind Dashboard
3
+ */
4
+
5
+ export const vi = {
6
+ dashboard: {
7
+ title: "HiveMind Quản Trị Ngữ Cảnh",
8
+ hierarchy: {
9
+ trajectory: "MỤC TIÊU",
10
+ tactic: "CHIẾN THUẬT",
11
+ action: "HÀNH ĐỘNG",
12
+ status: {
13
+ pending: "CHỜ",
14
+ active: "ĐANG LÀM",
15
+ complete: "XONG",
16
+ blocked: "BỊ CHẶN",
17
+ },
18
+ },
19
+ agentLog: {
20
+ title: "HÀNH ĐỘNG AGENT",
21
+ empty: "Chưa có hành động...",
22
+ timestamp: "Thời gian",
23
+ },
24
+ metrics: {
25
+ title: "SỐ LIỆU",
26
+ turns: "Lượt",
27
+ drift: "Độ lệch",
28
+ files: "File",
29
+ status: "Trạng thái",
30
+ },
31
+ planning: {
32
+ title: "FILE KẾ HOẠCH",
33
+ index: "index.md",
34
+ active: "active.md",
35
+ archive: "Lưu trữ",
36
+ },
37
+ governance: {
38
+ locked: "KHÓA",
39
+ open: "MỞ",
40
+ warning: "Khai báo ý định để mở khóa",
41
+ },
42
+ },
43
+ }
@@ -0,0 +1,372 @@
1
+ /**
2
+ * HiveMind Dashboard Backend
3
+ *
4
+ * Provides HTTP API for dashboard data:
5
+ * - GET /api/state — Current brain state
6
+ * - GET /api/active — Active session content
7
+ * - GET /api/index — Project trajectory
8
+ * - GET /api/archives — List of archived sessions
9
+ * - GET /api/archives/:id — Specific archive content
10
+ * - GET /api/metrics — Session metrics
11
+ *
12
+ * Serves static dashboard files in production.
13
+ */
14
+
15
+ import { createServer, IncomingMessage, ServerResponse } from "http"
16
+ import { readFile } from "fs/promises"
17
+ import { existsSync } from "fs"
18
+ import { join, basename } from "path"
19
+ import { createStateManager } from "../lib/persistence.js"
20
+ import { readActiveMd, listArchives, getPlanningPaths } from "../lib/planning-fs.js"
21
+ import { createLogger } from "../lib/logging.js"
22
+
23
+ // ─── Types ───────────────────────────────────────────────────────────
24
+
25
+ interface DashboardConfig {
26
+ projectRoot: string
27
+ port: number
28
+ host: string
29
+ }
30
+
31
+ interface ApiResponse {
32
+ success: boolean
33
+ data?: unknown
34
+ error?: string
35
+ }
36
+
37
+ // ─── CORS Headers ────────────────────────────────────────────────────
38
+
39
+ const CORS_HEADERS = {
40
+ "Access-Control-Allow-Origin": "*",
41
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
42
+ "Access-Control-Allow-Headers": "Content-Type",
43
+ "Content-Type": "application/json",
44
+ }
45
+
46
+ // ─── Response Helpers ────────────────────────────────────────────────
47
+
48
+ function sendJson(res: ServerResponse, status: number, data: ApiResponse): void {
49
+ res.writeHead(status, CORS_HEADERS)
50
+ res.end(JSON.stringify(data))
51
+ }
52
+
53
+ function sendError(res: ServerResponse, status: number, message: string): void {
54
+ sendJson(res, status, { success: false, error: message })
55
+ }
56
+
57
+ // ─── Route Handlers ──────────────────────────────────────────────────
58
+
59
+ async function handleGetState(
60
+ config: DashboardConfig,
61
+ res: ServerResponse
62
+ ): Promise<void> {
63
+ try {
64
+ const stateManager = createStateManager(config.projectRoot)
65
+ const state = await stateManager.load()
66
+
67
+ if (!state) {
68
+ sendJson(res, 200, { success: true, data: null })
69
+ return
70
+ }
71
+
72
+ sendJson(res, 200, { success: true, data: state })
73
+ } catch (err) {
74
+ sendError(res, 500, `Failed to load state: ${err}`)
75
+ }
76
+ }
77
+
78
+ async function handleGetActive(
79
+ config: DashboardConfig,
80
+ res: ServerResponse
81
+ ): Promise<void> {
82
+ try {
83
+ const activeMd = await readActiveMd(config.projectRoot)
84
+ sendJson(res, 200, { success: true, data: activeMd })
85
+ } catch (err) {
86
+ sendError(res, 500, `Failed to load active.md: ${err}`)
87
+ }
88
+ }
89
+
90
+ async function handleGetIndex(
91
+ config: DashboardConfig,
92
+ res: ServerResponse
93
+ ): Promise<void> {
94
+ try {
95
+ const paths = getPlanningPaths(config.projectRoot)
96
+ const content = await readFile(paths.indexPath, "utf-8")
97
+ sendJson(res, 200, { success: true, data: { content } })
98
+ } catch (err) {
99
+ sendError(res, 500, `Failed to load index.md: ${err}`)
100
+ }
101
+ }
102
+
103
+ async function handleGetArchives(
104
+ config: DashboardConfig,
105
+ res: ServerResponse
106
+ ): Promise<void> {
107
+ try {
108
+ const archives = await listArchives(config.projectRoot)
109
+ sendJson(res, 200, {
110
+ success: true,
111
+ data: archives.map((path) => ({
112
+ id: basename(path, ".md"),
113
+ path,
114
+ })),
115
+ })
116
+ } catch (err) {
117
+ sendError(res, 500, `Failed to list archives: ${err}`)
118
+ }
119
+ }
120
+
121
+ async function handleGetArchive(
122
+ config: DashboardConfig,
123
+ archiveId: string,
124
+ res: ServerResponse
125
+ ): Promise<void> {
126
+ try {
127
+ const paths = getPlanningPaths(config.projectRoot)
128
+ const archivePath = join(paths.archiveDir, `${archiveId}.md`)
129
+
130
+ // Security: ensure path is within archive directory
131
+ if (!archivePath.startsWith(paths.archiveDir)) {
132
+ sendError(res, 403, "Invalid archive ID")
133
+ return
134
+ }
135
+
136
+ if (!existsSync(archivePath)) {
137
+ sendError(res, 404, "Archive not found")
138
+ return
139
+ }
140
+
141
+ const content = await readFile(archivePath, "utf-8")
142
+ sendJson(res, 200, { success: true, data: { id: archiveId, content } })
143
+ } catch (err) {
144
+ sendError(res, 500, `Failed to load archive: ${err}`)
145
+ }
146
+ }
147
+
148
+ async function handleGetMetrics(
149
+ config: DashboardConfig,
150
+ res: ServerResponse
151
+ ): Promise<void> {
152
+ try {
153
+ const stateManager = createStateManager(config.projectRoot)
154
+ const state = await stateManager.load()
155
+ const archives = await listArchives(config.projectRoot)
156
+
157
+ if (!state) {
158
+ sendJson(res, 200, {
159
+ success: true,
160
+ data: {
161
+ hasSession: false,
162
+ totalArchives: archives.length,
163
+ },
164
+ })
165
+ return
166
+ }
167
+
168
+ sendJson(res, 200, {
169
+ success: true,
170
+ data: {
171
+ hasSession: true,
172
+ session: {
173
+ id: state.session.id,
174
+ mode: state.session.mode,
175
+ status: state.session.governance_status,
176
+ startTime: state.session.start_time,
177
+ },
178
+ hierarchy: state.hierarchy,
179
+ metrics: state.metrics,
180
+ totalArchives: archives.length,
181
+ },
182
+ })
183
+ } catch (err) {
184
+ sendError(res, 500, `Failed to load metrics: ${err}`)
185
+ }
186
+ }
187
+
188
+ // ─── Router ──────────────────────────────────────────────────────────
189
+
190
+ function route(
191
+ config: DashboardConfig,
192
+ req: IncomingMessage,
193
+ res: ServerResponse
194
+ ): void {
195
+ const url = req.url ?? "/"
196
+ const method = req.method ?? "GET"
197
+
198
+ // CORS preflight
199
+ if (method === "OPTIONS") {
200
+ res.writeHead(204, CORS_HEADERS)
201
+ res.end()
202
+ return
203
+ }
204
+
205
+ // Only accept GET requests for API
206
+ if (method !== "GET") {
207
+ sendError(res, 405, "Method not allowed")
208
+ return
209
+ }
210
+
211
+ // API routes
212
+ if (url === "/api/state") {
213
+ handleGetState(config, res)
214
+ return
215
+ }
216
+
217
+ if (url === "/api/active") {
218
+ handleGetActive(config, res)
219
+ return
220
+ }
221
+
222
+ if (url === "/api/index") {
223
+ handleGetIndex(config, res)
224
+ return
225
+ }
226
+
227
+ if (url === "/api/archives") {
228
+ handleGetArchives(config, res)
229
+ return
230
+ }
231
+
232
+ if (url.startsWith("/api/archives/")) {
233
+ const archiveId = url.slice("/api/archives/".length)
234
+ handleGetArchive(config, archiveId, res)
235
+ return
236
+ }
237
+
238
+ if (url === "/api/metrics") {
239
+ handleGetMetrics(config, res)
240
+ return
241
+ }
242
+
243
+ if (url === "/api/health") {
244
+ sendJson(res, 200, { success: true, data: { status: "ok" } })
245
+ return
246
+ }
247
+
248
+ // 404 for unknown routes
249
+ sendError(res, 404, "Not found")
250
+ }
251
+
252
+ // ─── Server Factory ──────────────────────────────────────────────────
253
+
254
+ export interface DashboardServer {
255
+ start(): Promise<void>
256
+ stop(): Promise<void>
257
+ url: string
258
+ }
259
+
260
+ export function createDashboardServer(
261
+ projectRoot: string,
262
+ options: { port?: number; host?: string } = {}
263
+ ): DashboardServer {
264
+ const config: DashboardConfig = {
265
+ projectRoot,
266
+ port: options.port ?? 0, // 0 = random available port
267
+ host: options.host ?? "127.0.0.1",
268
+ }
269
+
270
+ let server: ReturnType<typeof createServer> | null = null
271
+ let serverUrl = ""
272
+
273
+ return {
274
+ get url() {
275
+ return serverUrl
276
+ },
277
+
278
+ async start(): Promise<void> {
279
+ // Ensure planning directory exists
280
+ const paths = getPlanningPaths(projectRoot)
281
+ if (!existsSync(paths.planningDir)) {
282
+ throw new Error(
283
+ `HiveMind not initialized. Run 'hivemind init' first.`
284
+ )
285
+ }
286
+
287
+ // Setup logging
288
+ const log = await createLogger(
289
+ join(paths.planningDir, "logs"),
290
+ "dashboard"
291
+ )
292
+
293
+ return new Promise((resolve, reject) => {
294
+ server = createServer((req, res) => {
295
+ route(config, req, res)
296
+ })
297
+
298
+ server.listen(config.port, config.host, () => {
299
+ const address = server?.address()
300
+ if (address && typeof address === "object") {
301
+ serverUrl = `http://${config.host}:${address.port}`
302
+ log.info(`Dashboard server started at ${serverUrl}`)
303
+ resolve()
304
+ } else {
305
+ reject(new Error("Failed to get server address"))
306
+ }
307
+ })
308
+
309
+ server.on("error", (err) => {
310
+ reject(err)
311
+ })
312
+ })
313
+ },
314
+
315
+ async stop(): Promise<void> {
316
+ return new Promise((resolve) => {
317
+ if (server) {
318
+ server.close(() => resolve())
319
+ server = null
320
+ } else {
321
+ resolve()
322
+ }
323
+ })
324
+ },
325
+ }
326
+ }
327
+
328
+ // ─── CLI Entry Point ─────────────────────────────────────────────────
329
+
330
+ async function main(): Promise<void> {
331
+ const projectRoot = process.cwd()
332
+ const port = process.env.HIVEMIND_PORT
333
+ ? parseInt(process.env.HIVEMIND_PORT, 10)
334
+ : undefined
335
+
336
+ const server = createDashboardServer(projectRoot, { port })
337
+
338
+ try {
339
+ await server.start()
340
+ // eslint-disable-next-line no-console
341
+ console.log(`HiveMind Dashboard running at ${server.url}`)
342
+ // eslint-disable-next-line no-console
343
+ console.log(`API endpoints:`)
344
+ // eslint-disable-next-line no-console
345
+ console.log(` ${server.url}/api/state`)
346
+ // eslint-disable-next-line no-console
347
+ console.log(` ${server.url}/api/metrics`)
348
+ // eslint-disable-next-line no-console
349
+ console.log(` ${server.url}/api/active`)
350
+ // eslint-disable-next-line no-console
351
+ console.log(` ${server.url}/api/archives`)
352
+ // eslint-disable-next-line no-console
353
+ console.log(`\nPress Ctrl+C to stop`)
354
+ } catch (err) {
355
+ // eslint-disable-next-line no-console
356
+ console.error("Failed to start dashboard:", err)
357
+ process.exit(1)
358
+ }
359
+
360
+ // Graceful shutdown
361
+ process.on("SIGINT", async () => {
362
+ // eslint-disable-next-line no-console
363
+ console.log("\nShutting down...")
364
+ await server.stop()
365
+ process.exit(0)
366
+ })
367
+ }
368
+
369
+ // Run if called directly
370
+ if (import.meta.url === `file://${process.argv[1]}`) {
371
+ main()
372
+ }