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.
- package/CHANGELOG.md +95 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/cli/init.d.ts +23 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +175 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +162 -0
- package/dist/cli.js.map +1 -0
- package/dist/dashboard/i18n/en.d.ts +43 -0
- package/dist/dashboard/i18n/en.d.ts.map +1 -0
- package/dist/dashboard/i18n/en.js +43 -0
- package/dist/dashboard/i18n/en.js.map +1 -0
- package/dist/dashboard/i18n/index.d.ts +10 -0
- package/dist/dashboard/i18n/index.d.ts.map +1 -0
- package/dist/dashboard/i18n/index.js +14 -0
- package/dist/dashboard/i18n/index.js.map +1 -0
- package/dist/dashboard/i18n/vi.d.ts +43 -0
- package/dist/dashboard/i18n/vi.d.ts.map +1 -0
- package/dist/dashboard/i18n/vi.js +43 -0
- package/dist/dashboard/i18n/vi.js.map +1 -0
- package/dist/dashboard/server.d.ts +23 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +282 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/hooks/compaction.d.ts +21 -0
- package/dist/hooks/compaction.d.ts.map +1 -0
- package/dist/hooks/compaction.js +79 -0
- package/dist/hooks/compaction.js.map +1 -0
- package/dist/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/session-lifecycle.d.ts +28 -0
- package/dist/hooks/session-lifecycle.d.ts.map +1 -0
- package/dist/hooks/session-lifecycle.js +94 -0
- package/dist/hooks/session-lifecycle.js.map +1 -0
- package/dist/hooks/tool-gate.d.ts +28 -0
- package/dist/hooks/tool-gate.d.ts.map +1 -0
- package/dist/hooks/tool-gate.js +152 -0
- package/dist/hooks/tool-gate.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +159 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/complexity.d.ts +41 -0
- package/dist/lib/complexity.d.ts.map +1 -0
- package/dist/lib/complexity.js +67 -0
- package/dist/lib/complexity.js.map +1 -0
- package/dist/lib/context-refresh.d.ts +46 -0
- package/dist/lib/context-refresh.d.ts.map +1 -0
- package/dist/lib/context-refresh.js +77 -0
- package/dist/lib/context-refresh.js.map +1 -0
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +10 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logging.d.ts +13 -0
- package/dist/lib/logging.d.ts.map +1 -0
- package/dist/lib/logging.js +40 -0
- package/dist/lib/logging.js.map +1 -0
- package/dist/lib/persistence.d.ts +15 -0
- package/dist/lib/persistence.d.ts.map +1 -0
- package/dist/lib/persistence.js +62 -0
- package/dist/lib/persistence.js.map +1 -0
- package/dist/lib/planning-fs.d.ts +34 -0
- package/dist/lib/planning-fs.d.ts.map +1 -0
- package/dist/lib/planning-fs.js +134 -0
- package/dist/lib/planning-fs.js.map +1 -0
- package/dist/lib/sentiment.d.ts +31 -0
- package/dist/lib/sentiment.d.ts.map +1 -0
- package/dist/lib/sentiment.js +158 -0
- package/dist/lib/sentiment.js.map +1 -0
- package/dist/schemas/brain-state.d.ts +60 -0
- package/dist/schemas/brain-state.d.ts.map +1 -0
- package/dist/schemas/brain-state.js +176 -0
- package/dist/schemas/brain-state.js.map +1 -0
- package/dist/schemas/config.d.ts +51 -0
- package/dist/schemas/config.d.ts.map +1 -0
- package/dist/schemas/config.js +128 -0
- package/dist/schemas/config.js.map +1 -0
- package/dist/schemas/hierarchy.d.ts +22 -0
- package/dist/schemas/hierarchy.d.ts.map +1 -0
- package/dist/schemas/hierarchy.js +27 -0
- package/dist/schemas/hierarchy.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/tools/compact-session.d.ts +15 -0
- package/dist/tools/compact-session.d.ts.map +1 -0
- package/dist/tools/compact-session.js +76 -0
- package/dist/tools/compact-session.js.map +1 -0
- package/dist/tools/declare-intent.d.ts +15 -0
- package/dist/tools/declare-intent.d.ts.map +1 -0
- package/dist/tools/declare-intent.js +83 -0
- package/dist/tools/declare-intent.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/map-context.d.ts +15 -0
- package/dist/tools/map-context.d.ts.map +1 -0
- package/dist/tools/map-context.js +85 -0
- package/dist/tools/map-context.js.map +1 -0
- package/dist/tools/self-rate.d.ts +15 -0
- package/dist/tools/self-rate.d.ts.map +1 -0
- package/dist/tools/self-rate.js +76 -0
- package/dist/tools/self-rate.js.map +1 -0
- package/package.json +71 -0
- package/src/cli/init.ts +214 -0
- package/src/cli.ts +178 -0
- package/src/dashboard/i18n/en.ts +43 -0
- package/src/dashboard/i18n/index.ts +20 -0
- package/src/dashboard/i18n/vi.ts +43 -0
- package/src/dashboard/server.ts +372 -0
- package/src/hooks/compaction.ts +104 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/session-lifecycle.ts +133 -0
- package/src/hooks/tool-gate.ts +205 -0
- package/src/index.ts +206 -0
- package/src/lib/complexity.ts +96 -0
- package/src/lib/context-refresh.ts +107 -0
- package/src/lib/index.ts +10 -0
- package/src/lib/logging.ts +53 -0
- package/src/lib/persistence.ts +83 -0
- package/src/lib/planning-fs.ts +187 -0
- package/src/lib/sentiment.ts +183 -0
- package/src/schemas/brain-state.ts +257 -0
- package/src/schemas/config.ts +184 -0
- package/src/schemas/hierarchy.ts +58 -0
- package/src/schemas/index.ts +7 -0
- package/src/tools/compact-session.ts +97 -0
- package/src/tools/declare-intent.ts +104 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/map-context.ts +105 -0
- 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
|
+
}
|