@monoes/monomindcli 1.11.11 → 1.11.12
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/.claude/commands/mastermind/idea.md +1 -1
- package/.claude/commands/mastermind/master.md +1 -1
- package/.claude/skills/mastermind/_protocol.md +4 -4
- package/.claude/skills/mastermind/autodev.md +1 -1
- package/.claude/skills/mastermind/build.md +3 -3
- package/.claude/skills/mastermind/content.md +3 -3
- package/.claude/skills/mastermind/createorg.md +2 -2
- package/.claude/skills/mastermind/finance.md +3 -3
- package/.claude/skills/mastermind/marketing.md +3 -3
- package/.claude/skills/mastermind/ops.md +3 -3
- package/.claude/skills/mastermind/release.md +3 -3
- package/.claude/skills/mastermind/research.md +3 -3
- package/.claude/skills/mastermind/review.md +3 -3
- package/.claude/skills/mastermind/sales.md +3 -3
- package/dist/src/init/statusline-generator.js +3 -3
- package/dist/src/observability/replay-reader.d.ts +1 -1
- package/dist/src/observability/replay-reader.d.ts.map +1 -1
- package/dist/src/update/checker.d.ts.map +1 -1
- package/dist/src/update/checker.js +24 -7
- package/dist/src/update/checker.js.map +1 -1
- package/dist/src/update/index.d.ts.map +1 -1
- package/dist/src/update/index.js +3 -6
- package/dist/src/update/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/dist/src/agents/halt-signal.d.ts +0 -25
- package/dist/src/agents/halt-signal.d.ts.map +0 -1
- package/dist/src/agents/halt-signal.js +0 -76
- package/dist/src/agents/halt-signal.js.map +0 -1
- package/dist/src/agents/index.d.ts +0 -18
- package/dist/src/agents/index.d.ts.map +0 -1
- package/dist/src/agents/index.js +0 -13
- package/dist/src/agents/index.js.map +0 -1
- package/dist/src/agents/managed-agent.d.ts +0 -41
- package/dist/src/agents/managed-agent.d.ts.map +0 -1
- package/dist/src/agents/managed-agent.js +0 -69
- package/dist/src/agents/managed-agent.js.map +0 -1
- package/dist/src/agents/prompt-experiment.d.ts +0 -23
- package/dist/src/agents/prompt-experiment.d.ts.map +0 -1
- package/dist/src/agents/prompt-experiment.js +0 -49
- package/dist/src/agents/prompt-experiment.js.map +0 -1
- package/dist/src/agents/prompt-version-manager.d.ts +0 -22
- package/dist/src/agents/prompt-version-manager.d.ts.map +0 -1
- package/dist/src/agents/prompt-version-manager.js +0 -80
- package/dist/src/agents/prompt-version-manager.js.map +0 -1
- package/dist/src/agents/registry-query.d.ts +0 -71
- package/dist/src/agents/registry-query.d.ts.map +0 -1
- package/dist/src/agents/registry-query.js +0 -125
- package/dist/src/agents/registry-query.js.map +0 -1
- package/dist/src/agents/score-decay.d.ts +0 -19
- package/dist/src/agents/score-decay.d.ts.map +0 -1
- package/dist/src/agents/score-decay.js +0 -22
- package/dist/src/agents/score-decay.js.map +0 -1
- package/dist/src/agents/shared-instructions-loader.d.ts +0 -13
- package/dist/src/agents/shared-instructions-loader.d.ts.map +0 -1
- package/dist/src/agents/shared-instructions-loader.js +0 -40
- package/dist/src/agents/shared-instructions-loader.js.map +0 -1
- package/dist/src/agents/specialization-scorer.d.ts +0 -54
- package/dist/src/agents/specialization-scorer.d.ts.map +0 -1
- package/dist/src/agents/specialization-scorer.js +0 -212
- package/dist/src/agents/specialization-scorer.js.map +0 -1
- package/dist/src/agents/termination-watcher.d.ts +0 -30
- package/dist/src/agents/termination-watcher.d.ts.map +0 -1
- package/dist/src/agents/termination-watcher.js +0 -84
- package/dist/src/agents/termination-watcher.js.map +0 -1
- package/dist/src/agents/trigger-index.d.ts +0 -20
- package/dist/src/agents/trigger-index.d.ts.map +0 -1
- package/dist/src/agents/trigger-index.js +0 -38
- package/dist/src/agents/trigger-index.js.map +0 -1
- package/dist/src/agents/trigger-scanner.d.ts +0 -64
- package/dist/src/agents/trigger-scanner.d.ts.map +0 -1
- package/dist/src/agents/trigger-scanner.js +0 -308
- package/dist/src/agents/trigger-scanner.js.map +0 -1
- package/dist/src/agents/version-diff.d.ts +0 -18
- package/dist/src/agents/version-diff.d.ts.map +0 -1
- package/dist/src/agents/version-diff.js +0 -64
- package/dist/src/agents/version-diff.js.map +0 -1
- package/dist/src/agents/version-store.d.ts +0 -60
- package/dist/src/agents/version-store.d.ts.map +0 -1
- package/dist/src/agents/version-store.js +0 -235
- package/dist/src/agents/version-store.js.map +0 -1
- package/dist/src/benchmarks/pretrain/index.d.ts +0 -45
- package/dist/src/benchmarks/pretrain/index.d.ts.map +0 -1
- package/dist/src/benchmarks/pretrain/index.js +0 -404
- package/dist/src/benchmarks/pretrain/index.js.map +0 -1
- package/dist/src/commands/agent-wasm.d.ts +0 -14
- package/dist/src/commands/agent-wasm.d.ts.map +0 -1
- package/dist/src/commands/agent-wasm.js +0 -333
- package/dist/src/commands/agent-wasm.js.map +0 -1
- package/dist/src/commands/embeddings.d.ts.map +0 -1
- package/dist/src/commands/embeddings.js.map +0 -1
- package/dist/src/commands/ui.js +0 -68
- package/dist/src/consensus/index.d.ts +0 -7
- package/dist/src/consensus/index.d.ts.map +0 -1
- package/dist/src/consensus/index.js +0 -6
- package/dist/src/consensus/index.js.map +0 -1
- package/dist/src/context/context-provider.d.ts +0 -44
- package/dist/src/context/context-provider.d.ts.map +0 -1
- package/dist/src/context/context-provider.js +0 -25
- package/dist/src/context/context-provider.js.map +0 -1
- package/dist/src/context/git-state-provider.d.ts +0 -12
- package/dist/src/context/git-state-provider.d.ts.map +0 -1
- package/dist/src/context/git-state-provider.js +0 -34
- package/dist/src/context/git-state-provider.js.map +0 -1
- package/dist/src/context/index.d.ts +0 -12
- package/dist/src/context/index.d.ts.map +0 -1
- package/dist/src/context/index.js +0 -12
- package/dist/src/context/index.js.map +0 -1
- package/dist/src/context/project-conventions-provider.d.ts +0 -15
- package/dist/src/context/project-conventions-provider.d.ts.map +0 -1
- package/dist/src/context/project-conventions-provider.js +0 -19
- package/dist/src/context/project-conventions-provider.js.map +0 -1
- package/dist/src/context/prompt-assembler.d.ts +0 -26
- package/dist/src/context/prompt-assembler.d.ts.map +0 -1
- package/dist/src/context/prompt-assembler.js +0 -93
- package/dist/src/context/prompt-assembler.js.map +0 -1
- package/dist/src/context/task-history-provider.d.ts +0 -24
- package/dist/src/context/task-history-provider.d.ts.map +0 -1
- package/dist/src/context/task-history-provider.js +0 -32
- package/dist/src/context/task-history-provider.js.map +0 -1
- package/dist/src/context/user-preferences-provider.d.ts +0 -14
- package/dist/src/context/user-preferences-provider.d.ts.map +0 -1
- package/dist/src/context/user-preferences-provider.js +0 -27
- package/dist/src/context/user-preferences-provider.js.map +0 -1
- package/dist/src/dlq/dlq-reader.d.ts +0 -31
- package/dist/src/dlq/dlq-reader.d.ts.map +0 -1
- package/dist/src/dlq/dlq-reader.js +0 -81
- package/dist/src/dlq/dlq-reader.js.map +0 -1
- package/dist/src/dlq/dlq-writer.d.ts +0 -24
- package/dist/src/dlq/dlq-writer.d.ts.map +0 -1
- package/dist/src/dlq/dlq-writer.js +0 -65
- package/dist/src/dlq/dlq-writer.js.map +0 -1
- package/dist/src/dlq/index.d.ts +0 -10
- package/dist/src/dlq/index.d.ts.map +0 -1
- package/dist/src/dlq/index.js +0 -7
- package/dist/src/dlq/index.js.map +0 -1
- package/dist/src/eval/dataset-manager.d.ts +0 -33
- package/dist/src/eval/dataset-manager.d.ts.map +0 -1
- package/dist/src/eval/dataset-manager.js +0 -107
- package/dist/src/eval/dataset-manager.js.map +0 -1
- package/dist/src/eval/dataset-runner.d.ts +0 -23
- package/dist/src/eval/dataset-runner.d.ts.map +0 -1
- package/dist/src/eval/dataset-runner.js +0 -59
- package/dist/src/eval/dataset-runner.js.map +0 -1
- package/dist/src/eval/index.d.ts +0 -10
- package/dist/src/eval/index.d.ts.map +0 -1
- package/dist/src/eval/index.js +0 -7
- package/dist/src/eval/index.js.map +0 -1
- package/dist/src/eval/trace-collector.d.ts +0 -40
- package/dist/src/eval/trace-collector.d.ts.map +0 -1
- package/dist/src/eval/trace-collector.js +0 -102
- package/dist/src/eval/trace-collector.js.map +0 -1
- package/dist/src/graph/enrich.mjs +0 -362
- package/dist/src/infrastructure/in-memory-repositories.d.ts +0 -68
- package/dist/src/infrastructure/in-memory-repositories.d.ts.map +0 -1
- package/dist/src/infrastructure/in-memory-repositories.js +0 -264
- package/dist/src/infrastructure/in-memory-repositories.js.map +0 -1
- package/dist/src/interactive/interrupt.d.ts +0 -22
- package/dist/src/interactive/interrupt.d.ts.map +0 -1
- package/dist/src/interactive/interrupt.js +0 -71
- package/dist/src/interactive/interrupt.js.map +0 -1
- package/dist/src/mcp/deprecation-injector.d.ts +0 -25
- package/dist/src/mcp/deprecation-injector.d.ts.map +0 -1
- package/dist/src/mcp/deprecation-injector.js +0 -48
- package/dist/src/mcp/deprecation-injector.js.map +0 -1
- package/dist/src/mcp/tool-registry.d.ts +0 -61
- package/dist/src/mcp/tool-registry.d.ts.map +0 -1
- package/dist/src/mcp/tool-registry.js +0 -246
- package/dist/src/mcp/tool-registry.js.map +0 -1
- package/dist/src/mcp-tools/wasm-agent-tools.d.ts +0 -9
- package/dist/src/mcp-tools/wasm-agent-tools.d.ts.map +0 -1
- package/dist/src/mcp-tools/wasm-agent-tools.js +0 -230
- package/dist/src/mcp-tools/wasm-agent-tools.js.map +0 -1
- package/dist/src/model/complexity-scorer.d.ts +0 -21
- package/dist/src/model/complexity-scorer.d.ts.map +0 -1
- package/dist/src/model/complexity-scorer.js +0 -106
- package/dist/src/model/complexity-scorer.js.map +0 -1
- package/dist/src/model/index.d.ts +0 -4
- package/dist/src/model/index.d.ts.map +0 -1
- package/dist/src/model/index.js +0 -4
- package/dist/src/model/index.js.map +0 -1
- package/dist/src/model/model-settings.d.ts +0 -22
- package/dist/src/model/model-settings.d.ts.map +0 -1
- package/dist/src/model/model-settings.js +0 -33
- package/dist/src/model/model-settings.js.map +0 -1
- package/dist/src/model/model-tier-resolver.d.ts +0 -24
- package/dist/src/model/model-tier-resolver.d.ts.map +0 -1
- package/dist/src/model/model-tier-resolver.js +0 -65
- package/dist/src/model/model-tier-resolver.js.map +0 -1
- package/dist/src/monovector/capabilities.d.ts +0 -34
- package/dist/src/monovector/capabilities.d.ts.map +0 -1
- package/dist/src/monovector/capabilities.js +0 -37
- package/dist/src/monovector/capabilities.js.map +0 -1
- package/dist/src/orchestration/index.d.ts +0 -7
- package/dist/src/orchestration/index.d.ts.map +0 -1
- package/dist/src/orchestration/index.js +0 -6
- package/dist/src/orchestration/index.js.map +0 -1
- package/dist/src/orchestration/mode-dispatcher.d.ts +0 -11
- package/dist/src/orchestration/mode-dispatcher.d.ts.map +0 -1
- package/dist/src/orchestration/mode-dispatcher.js +0 -31
- package/dist/src/orchestration/mode-dispatcher.js.map +0 -1
- package/dist/src/orchestration/routing-modes.d.ts +0 -68
- package/dist/src/orchestration/routing-modes.d.ts.map +0 -1
- package/dist/src/orchestration/routing-modes.js +0 -180
- package/dist/src/orchestration/routing-modes.js.map +0 -1
- package/dist/src/plugins/tests/demo-plugin-store.d.ts +0 -7
- package/dist/src/plugins/tests/demo-plugin-store.d.ts.map +0 -1
- package/dist/src/plugins/tests/demo-plugin-store.js +0 -126
- package/dist/src/plugins/tests/demo-plugin-store.js.map +0 -1
- package/dist/src/plugins/tests/standalone-test.d.ts +0 -12
- package/dist/src/plugins/tests/standalone-test.d.ts.map +0 -1
- package/dist/src/plugins/tests/standalone-test.js +0 -188
- package/dist/src/plugins/tests/standalone-test.js.map +0 -1
- package/dist/src/plugins/tests/test-plugin-store.d.ts +0 -7
- package/dist/src/plugins/tests/test-plugin-store.d.ts.map +0 -1
- package/dist/src/plugins/tests/test-plugin-store.js +0 -206
- package/dist/src/plugins/tests/test-plugin-store.js.map +0 -1
- package/dist/src/runtime/headless.d.ts +0 -60
- package/dist/src/runtime/headless.d.ts.map +0 -1
- package/dist/src/runtime/headless.js +0 -284
- package/dist/src/runtime/headless.js.map +0 -1
- package/dist/src/services/agentic-flow-bridge.d.ts +0 -50
- package/dist/src/services/agentic-flow-bridge.d.ts.map +0 -1
- package/dist/src/services/agentic-flow-bridge.js +0 -95
- package/dist/src/services/agentic-flow-bridge.js.map +0 -1
- package/dist/src/services/container-worker-pool.d.ts +0 -197
- package/dist/src/services/container-worker-pool.d.ts.map +0 -1
- package/dist/src/services/container-worker-pool.js +0 -623
- package/dist/src/services/container-worker-pool.js.map +0 -1
- package/dist/src/services/index.d.ts +0 -13
- package/dist/src/services/index.d.ts.map +0 -1
- package/dist/src/services/index.js +0 -11
- package/dist/src/services/index.js.map +0 -1
- package/dist/src/services/worker-queue.d.ts +0 -201
- package/dist/src/services/worker-queue.d.ts.map +0 -1
- package/dist/src/services/worker-queue.js +0 -594
- package/dist/src/services/worker-queue.js.map +0 -1
- package/dist/src/swarm/communication-graph.d.ts +0 -25
- package/dist/src/swarm/communication-graph.d.ts.map +0 -1
- package/dist/src/swarm/communication-graph.js +0 -77
- package/dist/src/swarm/communication-graph.js.map +0 -1
- package/dist/src/swarm/flow-enforcer.d.ts +0 -31
- package/dist/src/swarm/flow-enforcer.d.ts.map +0 -1
- package/dist/src/swarm/flow-enforcer.js +0 -61
- package/dist/src/swarm/flow-enforcer.js.map +0 -1
- package/dist/src/swarm/flow-visualizer.d.ts +0 -19
- package/dist/src/swarm/flow-visualizer.d.ts.map +0 -1
- package/dist/src/swarm/flow-visualizer.js +0 -68
- package/dist/src/swarm/flow-visualizer.js.map +0 -1
- package/dist/src/transfer/deploy-seraphine.d.ts +0 -13
- package/dist/src/transfer/deploy-seraphine.d.ts.map +0 -1
- package/dist/src/transfer/deploy-seraphine.js +0 -205
- package/dist/src/transfer/deploy-seraphine.js.map +0 -1
- package/dist/src/transfer/store/tests/standalone-test.d.ts +0 -12
- package/dist/src/transfer/store/tests/standalone-test.d.ts.map +0 -1
- package/dist/src/transfer/store/tests/standalone-test.js +0 -190
- package/dist/src/transfer/store/tests/standalone-test.js.map +0 -1
- package/dist/src/transfer/test-seraphine.d.ts +0 -6
- package/dist/src/transfer/test-seraphine.d.ts.map +0 -1
- package/dist/src/transfer/test-seraphine.js +0 -105
- package/dist/src/transfer/test-seraphine.js.map +0 -1
- package/dist/src/transfer/tests/test-store.d.ts +0 -7
- package/dist/src/transfer/tests/test-store.d.ts.map +0 -1
- package/dist/src/transfer/tests/test-store.js +0 -214
- package/dist/src/transfer/tests/test-store.js.map +0 -1
- package/dist/src/ui/.monomind/data/pending-insights.jsonl +0 -0
- package/dist/src/ui/.monomind/data/ranked-context.json +0 -5
- package/dist/src/ui/.monomind/loops/mastermind-review-1778664132789.json +0 -16
- package/dist/src/ui/.monomind/sessions/current.json +0 -13
- package/dist/src/ui/.monomind/sessions/session-1776778451399.json +0 -15
- package/dist/src/ui/collector.mjs +0 -646
- package/dist/src/ui/dashboard.html +0 -9694
- package/dist/src/ui/data/mastermind-events.jsonl +0 -59
- package/dist/src/ui/data/mastermind-sessions.json +0 -1
- package/dist/src/ui/orgs.html +0 -1360
- package/dist/src/ui/server.mjs +0 -4334
- package/dist/src/workflow/condition-evaluator.d.ts +0 -10
- package/dist/src/workflow/condition-evaluator.d.ts.map +0 -1
- package/dist/src/workflow/condition-evaluator.js +0 -82
- package/dist/src/workflow/condition-evaluator.js.map +0 -1
- package/dist/src/workflow/context-resolver.d.ts +0 -12
- package/dist/src/workflow/context-resolver.d.ts.map +0 -1
- package/dist/src/workflow/context-resolver.js +0 -23
- package/dist/src/workflow/context-resolver.js.map +0 -1
- package/dist/src/workflow/dag-builder.d.ts +0 -17
- package/dist/src/workflow/dag-builder.d.ts.map +0 -1
- package/dist/src/workflow/dag-builder.js +0 -129
- package/dist/src/workflow/dag-builder.js.map +0 -1
- package/dist/src/workflow/dag-executor.d.ts +0 -9
- package/dist/src/workflow/dag-executor.d.ts.map +0 -1
- package/dist/src/workflow/dag-executor.js +0 -116
- package/dist/src/workflow/dag-executor.js.map +0 -1
- package/dist/src/workflow/dag-types.d.ts +0 -41
- package/dist/src/workflow/dag-types.d.ts.map +0 -1
- package/dist/src/workflow/dag-types.js +0 -8
- package/dist/src/workflow/dag-types.js.map +0 -1
- package/dist/src/workflow/dsl-parser.d.ts +0 -12
- package/dist/src/workflow/dsl-parser.d.ts.map +0 -1
- package/dist/src/workflow/dsl-parser.js +0 -20
- package/dist/src/workflow/dsl-parser.js.map +0 -1
- package/dist/src/workflow/dsl-schema.d.ts +0 -165
- package/dist/src/workflow/dsl-schema.d.ts.map +0 -1
- package/dist/src/workflow/dsl-schema.js +0 -82
- package/dist/src/workflow/dsl-schema.js.map +0 -1
- package/dist/src/workflow/index.d.ts +0 -13
- package/dist/src/workflow/index.d.ts.map +0 -1
- package/dist/src/workflow/index.js +0 -11
- package/dist/src/workflow/index.js.map +0 -1
- package/dist/src/workflow/template-engine.d.ts +0 -11
- package/dist/src/workflow/template-engine.d.ts.map +0 -1
- package/dist/src/workflow/template-engine.js +0 -40
- package/dist/src/workflow/template-engine.js.map +0 -1
- package/dist/src/workflow/workflow-executor.d.ts +0 -29
- package/dist/src/workflow/workflow-executor.d.ts.map +0 -1
- package/dist/src/workflow/workflow-executor.js +0 -227
- package/dist/src/workflow/workflow-executor.js.map +0 -1
package/dist/src/ui/server.mjs
DELETED
|
@@ -1,4334 +0,0 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { createRequire } from 'module';
|
|
7
|
-
import { collectAll, getWatchPaths, collectProject, collectSessions, collectSwarm, collectSwarmHistory, appendSwarmHistory, collectSwarmEvents, getSwarmDataSize, cleanSwarmData, collectAgents, collectTokens, collectHooks, collectKnowledge, collectMetrics, collectMemory, collectMemoryFiles, collectSystem } from './collector.mjs';
|
|
8
|
-
|
|
9
|
-
const JSONL_SIZE_CAP = 10 * 1024 * 1024; // 10 MB — skip files larger than this in /api/graph
|
|
10
|
-
const buildDocsState = new Map();
|
|
11
|
-
|
|
12
|
-
// Pricing per token (mirrors token-tracker.cjs FALLBACK_PRICING)
|
|
13
|
-
const _SJ_PRICING = {
|
|
14
|
-
'claude-opus-4-6': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
15
|
-
'claude-opus-4-5': { in: 5e-6, out: 25e-6, cw: 6.25e-6, cr: 0.5e-6 },
|
|
16
|
-
'claude-opus-4': { in: 15e-6, out: 75e-6, cw: 18.75e-6, cr: 1.5e-6 },
|
|
17
|
-
'claude-sonnet-4-6': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
18
|
-
'claude-sonnet-4-5': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
19
|
-
'claude-sonnet-4': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
20
|
-
'claude-3-7-sonnet': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
21
|
-
'claude-3-5-sonnet': { in: 3e-6, out: 15e-6, cw: 3.75e-6, cr: 0.3e-6 },
|
|
22
|
-
'claude-haiku-4-5': { in: 1e-6, out: 5e-6, cw: 1.25e-6, cr: 0.1e-6 },
|
|
23
|
-
'claude-3-5-haiku': { in: 0.8e-6, out: 4e-6, cw: 1e-6, cr: 0.08e-6 },
|
|
24
|
-
'gpt-4o': { in: 2.5e-6, out: 10e-6, cw: 2.5e-6, cr: 1.25e-6 },
|
|
25
|
-
'gpt-4o-mini': { in: 0.15e-6, out: 0.6e-6, cw: 0.15e-6, cr: 0.075e-6 },
|
|
26
|
-
'gemini-2.5-pro': { in: 1.25e-6, out: 10e-6, cw: 1.25e-6, cr: 0.315e-6 },
|
|
27
|
-
};
|
|
28
|
-
function _sjGetPricing(model) {
|
|
29
|
-
const canonical = (model || '').replace(/@.*$/, '').replace(/-\d{8}$/, '');
|
|
30
|
-
if (_SJ_PRICING[canonical]) return _SJ_PRICING[canonical];
|
|
31
|
-
for (const k of Object.keys(_SJ_PRICING)) { if (canonical.startsWith(k) || canonical.includes(k)) return _SJ_PRICING[k]; }
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
function _sjCalcCost(model, usage) {
|
|
35
|
-
const p = _sjGetPricing(model);
|
|
36
|
-
if (!p || !usage) return 0;
|
|
37
|
-
const webSearch = ((usage.server_tool_use || {}).web_search_requests || 0) * 0.01;
|
|
38
|
-
return (usage.input_tokens || 0) * p.in
|
|
39
|
-
+ (usage.output_tokens || 0) * p.out
|
|
40
|
-
+ (usage.cache_creation_input_tokens || 0) * p.cw
|
|
41
|
-
+ (usage.cache_read_input_tokens || 0) * p.cr
|
|
42
|
-
+ webSearch;
|
|
43
|
-
} // key: resolved dir → { status, sections, files, error, startedAt, completedAt }
|
|
44
|
-
|
|
45
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
-
const MASTERMIND_DIAGRAM_HTML = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>MASTERMIND — Live Dashboard</title>\n<style>\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body {\n width: 100%; height: 100%; overflow: hidden;\n background: #07071a;\n font-family: 'Azeret Mono', 'Space Mono', 'Courier New', monospace;\n color: #e0e0ff;\n user-select: none;\n}\n\n/* ── Main layout ── */\n#app { display: flex; height: 100vh; }\n#sidebar {\n width: 220px; flex-shrink: 0;\n background: oklch(9% 0.012 186);\n border-right: 1px solid oklch(62% 0.2 186 / 0.18);\n display: flex; flex-direction: column;\n overflow: hidden; z-index: 10;\n}\n#stage-wrap { flex: 1; position: relative; overflow: hidden; }\n#detail-panel {\n width: 0; flex-shrink: 0; overflow: hidden;\n background: oklch(9% 0.012 186);\n border-left: 1px solid oklch(62% 0.2 186 / 0.18);\n transition: width 0.3s ease;\n display: flex; flex-direction: column;\n z-index: 10;\n}\n#detail-panel.open { width: 280px; }\n#stage { position: absolute; inset: 0; width: 100%; height: 100%; }\n\n/* ── Sidebar ── */\n#sb-header {\n padding: 14px 14px 10px;\n border-bottom: 1px solid oklch(62% 0.2 186 / 0.18);\n flex-shrink: 0;\n}\n#sb-title {\n font-size: 8px; letter-spacing: 4px; color: oklch(52% 0.1 186); margin-bottom: 4px;\n}\n.live-row { display: flex; align-items: center; gap: 6px; }\n.l-dot {\n width: 6px; height: 6px; border-radius: 50%;\n background: #252560; flex-shrink: 0;\n transition: background 0.5s;\n}\n.l-dot.on { background: #28c068; }\n@media (prefers-reduced-motion: no-preference) { .l-dot.on { animation: ldp 2s ease-in-out infinite; } }\n@keyframes ldp { 0%,100%{opacity:1} 50%{opacity:0.4} }\n#l-status { font-size: 9px; letter-spacing: 2px; color: oklch(44% 0.08 186); }\n#l-agents { font-size: 8px; color: oklch(40% 0.07 186); margin-left: auto; }\n#sb-sessions {\n flex: 1; overflow-y: auto; padding: 8px 0;\n scrollbar-width: thin; scrollbar-color: oklch(62% 0.2 186 / 0.3) transparent;\n}\n#sb-sessions::-webkit-scrollbar { width: 4px; }\n#sb-sessions::-webkit-scrollbar-thumb { background: oklch(62% 0.2 186 / 0.3); border-radius: 2px; }\n.sess-item {\n padding: 8px 14px; cursor: pointer;\n border-left: 2px solid transparent;\n transition: background 0.15s, border-color 0.15s;\n}\n.sess-item:hover { background: oklch(62% 0.2 186 / 0.09); }\n.sess-item.active { border-left-color: transparent; background: oklch(62% 0.2 186 / 0.14); box-shadow: inset 0 0 0 1px oklch(62% 0.2 186 / 0.32); }\n.sess-item.running { border-left-color: #28c068; }\n.sess-ts { font-size: 10px; color: oklch(42% 0.05 186); margin-bottom: 3px; }\n.sess-prompt {\n font-size: 12px; color: oklch(70% 0.05 186); line-height: 1.4;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 188px;\n}\n.sess-badges { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }\n.sess-project { font-size: 7px; color: oklch(40% 0.1 186); letter-spacing: 1px; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.sess-badge {\n font-size: 8px; padding: 2px 6px; border-radius: 3px;\n border: 1px solid oklch(62% 0.2 186 / 0.25); color: oklch(62% 0.09 186);\n background: oklch(62% 0.2 186 / 0.08);\n}\n.sess-badge.running-badge { border-color: rgba(40,192,104,0.4); color: #28c068; background: rgba(40,192,104,0.08); }\n#git-user-row {\n display: flex; align-items: center; gap: 5px;\n margin-top: 7px; padding-top: 6px;\n border-top: 1px solid oklch(62% 0.2 186 / 0.12);\n}\n#git-user-icon { font-size: 9px; color: #3a3a70; }\n#git-user-name {\n font-size: 9px; letter-spacing: 0.5px; color: #4a4a90;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n#git-cwd-row {\n display: flex; align-items: center; gap: 5px; margin-top: 4px;\n}\n#git-cwd-icon { font-size: 9px; color: #2a2a58; }\n#git-cwd-name {\n font-size: 9px; letter-spacing: 0.3px; color: #38386a;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n direction: rtl; text-align: left;\n}\n.sess-trace-link {\n font-size: 7px; color: #3a3a70; text-decoration: none; letter-spacing: 0.5px;\n padding: 1px 5px; border: 1px solid oklch(62% 0.2 186 / 0.2); border-radius: 3px;\n margin-left: auto; flex-shrink: 0;\n}\n.sess-trace-link:hover { color: oklch(66% 0.11 186); border-color: oklch(62% 0.2 186 / 0.5); }\n.dp-export-btn {\n font-size: 9px; font-family: inherit; color: oklch(58% 0.09 186); text-decoration: none;\n padding: 4px 8px; border: 1px solid oklch(62% 0.2 186 / 0.25); border-radius: 4px;\n background: oklch(62% 0.2 186 / 0.07); cursor: pointer; letter-spacing: 0.3px;\n}\n.dp-export-btn:hover { color: oklch(72% 0.12 186); border-color: oklch(62% 0.2 186 / 0.5); background: oklch(62% 0.2 186 / 0.15); }\n#sb-no-sessions {\n padding: 20px 14px; font-size: 9px; color: oklch(42% 0.06 186); line-height: 1.7;\n text-align: center;\n}\n#sb-movie-btn {\n margin: 10px 14px;\n background: oklch(62% 0.2 186 / 0.12);\n border: 1px solid oklch(62% 0.2 186 / 0.35);\n color: oklch(56% 0.16 186); font-size: 9px; letter-spacing: 2px;\n border-radius: 6px; padding: 7px; cursor: pointer; width: calc(100% - 28px);\n transition: background 0.15s, color 0.15s;\n font-family: 'Azeret Mono', 'Space Mono', 'Courier New', monospace;\n}\n#sb-movie-btn:hover { background: oklch(62% 0.2 186 / 0.25); color: #d0b0ff; }\n#sb-movie-btn.active { background: oklch(62% 0.2 186 / 0.25); color: #d0b0ff; border-color: oklch(62% 0.2 186 / 0.6); }\n\n/* ── SVG title overlay ── */\n#title-wrap {\n position: absolute; top: 16px; left: 50%; transform: translateX(-50%);\n text-align: center; pointer-events: none; z-index: 5;\n}\n#title-h1 {\n font-size: 22px; font-weight: 900; letter-spacing: 0.38em;\n color: oklch(84% 0.14 186);\n}\n#title-sub { font-size: 9px; color: oklch(38% 0.06 186); letter-spacing: 3px; margin-top: 6px; }\n\n/* ── Prompt box ── */\n#prompt-box {\n position: absolute; bottom: 76px; left: 50%; transform: translateX(-50%);\n min-width: 340px; max-width: 500px;\n background: rgba(6,4,22,0.96);\n border: 1px solid rgba(130,80,255,0.5);\n border-radius: 12px; padding: 10px 18px;\n z-index: 50; opacity: 0;\n box-shadow: 0 4px 28px rgba(100,50,255,0.16);\n backdrop-filter: blur(18px);\n}\n#p-tag { font-size: 8px; letter-spacing: 3px; color: #48489a; margin-bottom: 4px; }\n#p-line { font-size: 12.5px; color: #90c8ff; display: flex; align-items: center; gap: 2px; min-height: 19px; }\n#p-cursor {\n display: inline-block; width: 2px; height: 14px;\n background: #90c8ff; flex-shrink: 0;\n animation: blink 0.8s step-end infinite;\n}\n@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }\n\n/* ── Activity log ── */\n#activity-log {\n position: absolute; left: 10px; bottom: 76px;\n width: 240px;\n background: rgba(5,3,18,0.93);\n border: 1px solid rgba(70,45,165,0.35);\n border-radius: 10px; padding: 9px 12px;\n z-index: 50; opacity: 0;\n}\n#log-title { font-size: 7.5px; letter-spacing: 3px; color: #282870; margin-bottom: 6px;\n padding-bottom: 5px; border-bottom: 1px solid rgba(70,45,165,0.18); }\n#log-entries { font-size: 9px; line-height: 1.95; max-height: 160px; overflow: hidden; }\n.log-row { display: flex; gap: 5px; opacity: 0; }\n.log-tag { font-weight: bold; min-width: 58px; flex-shrink: 0; }\n.log-msg { color: #525298; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 150px; }\n\n/* ── Mode banner ── */\n#mode-banner {\n position: absolute; top: 14px; right: 10px;\n font-size: 8px; letter-spacing: 3px; color: #303070;\n z-index: 5; pointer-events: none;\n}\n#mode-banner.live-mode { color: #28c068; }\n\n/* ── Control bar ── */\n#ctrl {\n position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);\n display: flex; align-items: center; gap: 7px;\n background: rgba(8,6,26,0.95);\n border: 1px solid rgba(100,60,220,0.35);\n border-radius: 26px; padding: 6px 16px;\n z-index: 100; backdrop-filter: blur(18px);\n opacity: 0;\n}\n.c-btn {\n background: none; border: 1px solid rgba(100,60,220,0.4);\n color: #7858d0; width: 26px; height: 26px; border-radius: 50%;\n cursor: pointer; font-size: 10px;\n display: flex; align-items: center; justify-content: center;\n transition: background 0.12s, color 0.12s; flex-shrink: 0; line-height: 1;\n}\n.c-btn:hover { background: rgba(100,60,220,0.2); color: #d0b0ff; }\n.c-btn.disabled { opacity: 0.3; pointer-events: none; }\n#scrubber {\n width: 180px; height: 3px; cursor: pointer;\n -webkit-appearance: none; appearance: none;\n background: rgba(100,60,220,0.2); border-radius: 2px; outline: none;\n}\n#scrubber::-webkit-slider-thumb {\n -webkit-appearance: none; width: 11px; height: 11px;\n border-radius: 50%; background: #7858d0; cursor: pointer; border: none;\n}\n#t-disp { font-size: 9px; color: #484888; min-width: 36px; text-align: right; font-variant-numeric: tabular-nums; }\n#spd {\n background: rgba(8,6,26,0.85); border: 1px solid rgba(100,60,220,0.3);\n color: oklch(55% 0.12 186); font-size: 9px; font-family: 'Azeret Mono', 'Space Mono', monospace;\n border-radius: 4px; padding: 2px 4px; cursor: pointer; outline: none;\n}\n#spd option { background: #0d0a20; }\n\n/* ── Detail panel ── */\n#dp-header {\n padding: 14px 16px 10px;\n border-bottom: 1px solid oklch(62% 0.2 186 / 0.18); flex-shrink: 0;\n}\n#dp-close {\n float: right; background: none; border: none; color: #404070;\n cursor: pointer; font-size: 13px; padding: 0; line-height: 1;\n}\n#dp-close:hover { color: #a090e0; }\n#dp-title { font-size: 9px; letter-spacing: 3px; color: #5050a0; margin-top: 2px; }\n#dp-emoji { font-size: 22px; display: block; margin-bottom: 4px; }\n#dp-body { flex: 1; overflow-y: auto; padding: 12px 16px; scrollbar-width: thin; scrollbar-color: oklch(62% 0.2 186 / 0.3) transparent; }\n#dp-body::-webkit-scrollbar { width: 4px; }\n#dp-body::-webkit-scrollbar-thumb { background: oklch(62% 0.2 186 / 0.3); border-radius: 2px; }\n.dp-section { margin-bottom: 14px; }\n.dp-section-title { font-size: 7.5px; letter-spacing: 3px; color: oklch(38% 0.07 186); margin-bottom: 6px; padding-bottom: 4px; border-bottom: 1px solid oklch(62% 0.2 186 / 0.15); }\n.dp-event { font-size: 9px; line-height: 1.6; color: #5060a0; margin-bottom: 4px; }\n.dp-event .ev-ts { color: #282855; }\n.dp-event .ev-type { color: inherit; font-weight: bold; }\n.dp-artifact { font-size: 9px; color: #6070a0; padding: 3px 6px; background: oklch(62% 0.2 186 / 0.08); border-radius: 3px; margin-bottom: 3px; }\n.dp-agent { display: inline-block; font-size: 8px; padding: 2px 7px; border-radius: 10px; margin: 2px 3px 2px 0; border: 1px solid oklch(62% 0.2 186 / 0.3); color: oklch(55% 0.09 186); }\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n animation-iteration-count: 1 !important;\n transition-duration: 0.01ms !important;\n }\n}\n</style>\n</head>\n<body>\n<div id=\"app\">\n <!-- ── Left sidebar: session history ── -->\n <div id=\"sidebar\">\n <div id=\"sb-header\">\n <div id=\"sb-title\">SESSIONS</div>\n <select id=\"proj-filter\" onchange=\"applyProjectFilter(this.value)\" style=\"width:100%;margin-top:6px;background:oklch(12% 0.015 186);color:oklch(52% 0.1 186);border:1px solid oklch(62% 0.2 186 / 0.18);border-radius:3px;font-size:8px;letter-spacing:1px;padding:3px 4px;cursor:pointer\"><option value=\"\">ALL PROJECTS</option></select>\n <div class=\"live-row\">\n <div class=\"l-dot\" id=\"l-dot\"></div>\n <span id=\"l-status\">OFFLINE</span>\n <span id=\"l-agents\">0 agents</span>\n </div>\n <div id=\"git-user-row\">\n <span id=\"git-user-icon\">⬡</span>\n <span id=\"git-user-name\">—</span>\n </div>\n <div id=\"git-cwd-row\">\n <span id=\"git-cwd-icon\">◎</span>\n <span id=\"git-cwd-name\">—</span>\n </div>\n </div>\n <div id=\"sb-sessions\">\n <div id=\"sb-no-sessions\">No sessions yet.<br><br>Describe a goal and<br>Mastermind routes it<br>across specialist agents.<br><br><span style=\"color:oklch(56% 0.16 186);letter-spacing:1px\">/mastermind</span></div>\n </div>\n <button id=\"sb-movie-btn\" onclick=\"toggleMovieMode(currentSessionObj)\">▶ MOVIE MODE</button>\n </div>\n\n <!-- ── Stage ── -->\n <div id=\"stage-wrap\">\n <!-- SVG -->\n <svg id=\"stage\" viewBox=\"0 0 960 720\" preserveAspectRatio=\"xMidYMid meet\">\n <defs>\n <filter id=\"glow\" x=\"-55%\" y=\"-55%\" width=\"210%\" height=\"210%\">\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"5\" result=\"b\"/>\n <feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge>\n </filter>\n <filter id=\"bloom\" x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\">\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"15\" result=\"b\"/>\n <feMerge><feMergeNode in=\"b\"/><feMergeNode in=\"SourceGraphic\"/></feMerge>\n </filter>\n </defs>\n <rect width=\"960\" height=\"720\" fill=\"#07071a\"/>\n <g id=\"stars\"></g>\n <g id=\"net-edges\"></g>\n <g id=\"net-particles\"></g>\n <g id=\"net-nodes\"></g>\n </svg>\n\n <!-- Overlays -->\n <div id=\"title-wrap\">\n <div id=\"title-h1\">MASTERMIND</div>\n <div id=\"title-sub\">AUTONOMOUS EXECUTION · 12 DOMAINS · PERSISTENT ORGS</div>\n </div>\n\n <div id=\"mode-banner\">LIVE</div>\n\n <div id=\"prompt-box\">\n <div id=\"p-tag\">USER PROMPT</div>\n <div id=\"p-line\"><span id=\"p-text\"></span><span id=\"p-cursor\"></span></div>\n </div>\n\n <div id=\"activity-log\">\n <div id=\"log-title\">ACTIVITY LOG</div>\n <div id=\"log-entries\"></div>\n </div>\n\n <div id=\"ctrl\">\n <button class=\"c-btn disabled\" id=\"btn-restart\" title=\"Restart\">↺</button>\n <button class=\"c-btn disabled\" id=\"btn-play\" title=\"Play\">▶</button>\n <button class=\"c-btn disabled\" id=\"btn-pause\" title=\"Pause\">⏸</button>\n <input type=\"range\" id=\"scrubber\" min=\"0\" max=\"100\" value=\"0\" step=\"0.1\" disabled/>\n <span id=\"t-disp\">—</span>\n <select id=\"spd\">\n <option value=\"0.5\">0.5×</option>\n <option value=\"1\" selected>1×</option>\n <option value=\"2\">2×</option>\n <option value=\"3\">3×</option>\n </select>\n </div>\n </div>\n\n <!-- ── Right panel: session/domain detail ── -->\n <div id=\"detail-panel\">\n <div id=\"dp-header\">\n <button id=\"dp-close\" onclick=\"closeDetail()\">✕</button>\n <span id=\"dp-emoji\"></span>\n <div id=\"dp-title\">SELECT A DOMAIN OR SESSION</div>\n </div>\n <div id=\"dp-body\"></div>\n </div>\n</div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js\"></script>\n<script>\n'use strict';\n\n// ── Graph constants ──────────────────────────────────────────────────────────\nconst CX = 480, CY = 360;\nconst DOMAIN_COLORS = {\n build:'#60a5fa', idea:'#fbbf24', marketing:'#f472b6', review:'#34d399',\n research:'#a78bfa', content:'#fb923c', release:'#22d3ee', sales:'#f87171',\n ops:'#4ade80', finance:'#fde68a', orgs:'#c084fc', default:'#00E5C8'\n};\nconst DOMAIN_EMOJIS = {\n build:'⚙️', idea:'💡', marketing:'📣', review:'🔍', research:'🔬',\n content:'✍️', release:'🚀', sales:'💼', ops:'⚡', finance:'💰', orgs:'🏛'\n};\nconst AGENT_EMOJIS = {\n 'coder':'⚙', 'architect':'🏗', 'tester':'🧪', 'reviewer':'🔍',\n 'researcher':'🔬', 'frontend-dev':'🎨', 'backend-dev':'⚡',\n 'coordinator':'🎯', 'planner':'📋', 'general-purpose':'🤖',\n 'frontend':'🎨', 'backend':'⚡', 'ml-developer':'🧠',\n 'security-architect':'🔒', 'sparc-coder':'💻', 'default':'◈'\n};\n\n// ── Node/edge model ───────────────────────────────────────────────────────────\nconst nodes = new Map();\nconst edges = [];\nlet rootId = null;\nlet simActive = false;\n\n// ── SVG helpers ───────────────────────────────────────────────────────────────\nconst NS = 'http://www.w3.org/2000/svg';\nconst mkN = (tag, a) => {\n const el = document.createElementNS(NS, tag);\n if (a) for (const [k,v] of Object.entries(a)) el.setAttribute(k, v);\n return el;\n};\nconst starsG = document.getElementById('stars');\nconst edgesG = document.getElementById('net-edges');\nconst particlesG= document.getElementById('net-particles');\nconst nodesG = document.getElementById('net-nodes');\n\n// ── Star field ────────────────────────────────────────────────────────────────\n(function buildStars() {\n for (let i = 0; i < 170; i++) {\n starsG.appendChild(mkN('circle', {\n cx: (Math.random()*960).toFixed(1),\n cy: (Math.random()*720).toFixed(1),\n r: (Math.random()<0.1 ? Math.random()*1.5+0.8 : Math.random()*0.8+0.15).toFixed(2),\n fill: `rgba(160,150,255,${(Math.random()*0.35+0.08).toFixed(2)})`\n }));\n }\n if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {\n gsap.to([...starsG.children], {\n opacity: 'random(0.06,0.6)', duration: 'random(2,5)',\n stagger:{ amount:16, from:'random', repeat:-1, yoyo:true, ease:'sine.inOut' }, delay:1\n });\n }\n})();\n\n// ── Hex helper ────────────────────────────────────────────────────────────────\nfunction hexPts(r) {\n return Array.from({length:6},(_,i)=>{\n const a=i*Math.PI/3-Math.PI/6;\n return `${(r*Math.cos(a)).toFixed(1)},${(r*Math.sin(a)).toFixed(1)}`;\n }).join(' ');\n}\n\n// ── Force simulation (Verlet) ─────────────────────────────────────────────────\nconst SPRING_K = 0.030;\nconst REPULSION = 6000;\nconst DAMPING = 0.78;\nconst REST_DIST = { root:0, domain:185, agent:68, org:160 };\n\nfunction forceStep() {\n const arr = [...nodes.values()];\n for (const n of arr) { n.ax=0; n.ay=0; }\n for (let i=0; i<arr.length; i++) {\n for (let j=i+1; j<arr.length; j++) {\n const a=arr[i], b=arr[j];\n const dx=b.x-a.x, dy=b.y-a.y;\n const d2=dx*dx+dy*dy+1, d=Math.sqrt(d2);\n const f=REPULSION/(d2*d);\n if (!a.fixed){a.ax-=dx*f; a.ay-=dy*f;}\n if (!b.fixed){b.ax+=dx*f; b.ay+=dy*f;}\n }\n }\n for (const e of edges) {\n const a=nodes.get(e.fromId), b=nodes.get(e.toId);\n if (!a||!b) continue;\n const dx=b.x-a.x, dy=b.y-a.y;\n const d=Math.sqrt(dx*dx+dy*dy)+0.001;\n const rest=REST_DIST[b.type]??110;\n const f=(d-rest)*SPRING_K;\n if (!a.fixed){a.ax+=dx/d*f; a.ay+=dy/d*f;}\n if (!b.fixed){b.ax-=dx/d*f; b.ay-=dy/d*f;}\n }\n for (const n of arr) {\n if (n.fixed) continue;\n n.vx=(n.vx+n.ax)*DAMPING;\n n.vy=(n.vy+n.ay)*DAMPING;\n n.x=Math.max(60,Math.min(900,n.x+n.vx));\n n.y=Math.max(60,Math.min(660,n.y+n.vy));\n }\n}\n\n// ── Node renderer ─────────────────────────────────────────────────────────────\nfunction buildNodeEl(n) {\n const g=mkN('g',{transform:`translate(${n.x.toFixed(1)},${n.y.toFixed(1)})`});\n g.style.opacity='0'; g.style.cursor='pointer';\n if (n.type==='root') {\n g.appendChild(mkN('circle',{r:'58',fill:'none',stroke:n.color,'stroke-width':'0.5',opacity:'0.15'}));\n g.appendChild(mkN('circle',{r:'38',fill:'#070620',stroke:n.color,'stroke-width':'2.8',filter:'url(#glow)'}));\n g.appendChild(mkN('circle',{r:'30',fill:'none',stroke:n.color,'stroke-width':'0.8',opacity:'0.35'}));\n const hex=mkN('polygon',{points:hexPts(16),fill:'none',stroke:n.color,'stroke-width':'1.8',opacity:'0.75'});\n g.appendChild(hex);\n gsap.to(hex,{rotate:360,transformOrigin:'0 0',duration:24,repeat:-1,ease:'none'});\n const lbl=mkN('text',{x:'0',y:'58','text-anchor':'middle','font-size':'6.5',fill:n.color,'letter-spacing':'2',\n 'font-family':\"'Azeret Mono','Space Mono',monospace\"});\n lbl.textContent='MASTERMIND'; g.appendChild(lbl);\n } else if (n.type==='domain') {\n g.appendChild(mkN('circle',{r:'44',fill:'none',stroke:n.color,'stroke-width':'0.5',opacity:'0.2'}));\n g.appendChild(mkN('circle',{r:'30',fill:'#09071e',stroke:n.color,'stroke-width':'2.5',filter:'url(#glow)'}));\n const emj=mkN('text',{x:'0',y:'9','text-anchor':'middle','font-size':'17'});\n emj.textContent=n.emoji||'◈'; g.appendChild(emj);\n const lbl=mkN('text',{x:'0',y:'45','text-anchor':'middle','font-size':'7',fill:n.color,'letter-spacing':'1.5',\n 'font-family':\"'Azeret Mono','Space Mono',monospace\"});\n lbl.textContent=n.label; g.appendChild(lbl);\n const ring=mkN('circle',{r:'34',fill:'none',stroke:'#fbbf24','stroke-width':'2',opacity:'0',\n transform:'rotate(-90)','stroke-dasharray':'213.6','stroke-dashoffset':'213.6','stroke-linecap':'round'});\n ring.dataset.cring=n.id;\n g.appendChild(ring);\n } else if (n.type==='agent') {\n g.appendChild(mkN('circle',{r:'20',fill:'#08061a',stroke:n.color,'stroke-width':'1.8',filter:'url(#glow)'}));\n const emj=mkN('text',{x:'0',y:'5','text-anchor':'middle','font-size':'12'});\n emj.textContent=n.emoji||'◈'; g.appendChild(emj);\n const sl=n.label.length>11?n.label.slice(0,10)+'…':n.label;\n const lbl=mkN('text',{x:'0',y:'31','text-anchor':'middle','font-size':'6',fill:n.color,'letter-spacing':'0.6',\n 'font-family':\"'Azeret Mono','Space Mono',monospace\"});\n lbl.textContent=sl; g.appendChild(lbl);\n } else if (n.type==='org') {\n g.appendChild(mkN('polygon',{points:'0,-38 32,0 0,38 -32,0',fill:'#09071e',stroke:n.color,'stroke-width':'2.5',filter:'url(#glow)'}));\n const emj=mkN('text',{x:'0',y:'7','text-anchor':'middle','font-size':'16'});\n emj.textContent='🏛'; g.appendChild(emj);\n const lbl=mkN('text',{x:'0',y:'53','text-anchor':'middle','font-size':'6.5',fill:n.color,'letter-spacing':'1.5',\n 'font-family':\"'Azeret Mono','Space Mono',monospace\"});\n lbl.textContent=n.label; g.appendChild(lbl);\n }\n g.appendChild(mkN('circle',{r:n.type==='agent'?'22':'50',fill:'transparent'}));\n nodesG.appendChild(g);\n n.el=g;\n gsap.to(g,{opacity:1,duration:0.4,ease:'power2.out'});\n gsap.from(g,{scale:0.15,transformOrigin:'0 0',duration:0.55,ease:'back.out(1.7)'});\n}\n\n// ── Edge renderer ─────────────────────────────────────────────────────────────\nfunction buildEdgeEl(e) {\n const a=nodes.get(e.fromId), b=nodes.get(e.toId);\n if (!a||!b) return;\n const g=mkN('g'); g.style.opacity='0';\n const isIC=e.type==='intercom';\n const path=mkN('path',{fill:'none',stroke:a.color,opacity:isIC?'0.75':'0.4',\n 'stroke-width':isIC?'1.5':'0.9','stroke-dasharray':isIC?'5 3':'none'});\n g.appendChild(path);\n if (isIC&&e.msg) {\n const txt=mkN('text',{'font-size':'6.5',fill:a.color,\n 'font-family':\"'Azeret Mono','Space Mono',monospace\",'letter-spacing':'0.4',opacity:'0.8'});\n txt.textContent=e.msg.length>24?e.msg.slice(0,23)+'…':e.msg;\n g.appendChild(txt); e.msgEl=txt;\n }\n edgesG.insertBefore(g,edgesG.firstChild);\n e.el=g; e.pathEl=path;\n gsap.to(g,{opacity:1,duration:0.6,delay:0.12});\n updateEdge(e);\n}\n\nfunction updateEdge(e) {\n const a=nodes.get(e.fromId), b=nodes.get(e.toId);\n if (!a||!b||!e.pathEl) return;\n if (e.type==='intercom') {\n const mx=(a.x+b.x)/2, my=(a.y+b.y)/2-65;\n e.pathEl.setAttribute('d',`M${a.x.toFixed(1)},${a.y.toFixed(1)} Q${mx.toFixed(1)},${my.toFixed(1)} ${b.x.toFixed(1)},${b.y.toFixed(1)}`);\n if (e.msgEl){e.msgEl.setAttribute('x',(mx-22).toFixed(1));e.msgEl.setAttribute('y',(my-9).toFixed(1));}\n } else {\n e.pathEl.setAttribute('d',`M${a.x.toFixed(1)},${a.y.toFixed(1)} L${b.x.toFixed(1)},${b.y.toFixed(1)}`);\n }\n}\n\n// ── Particle system ───────────────────────────────────────────────────────────\nconst PPC = 7;\nfunction spawnParticles(e) {\n const col=(nodes.get(e.fromId)||{color:'#00E5C8'}).color;\n e.ptcls=Array.from({length:PPC},(_,i)=>{\n const dot=mkN('circle',{r:'2',fill:col,opacity:'0'});\n particlesG.appendChild(dot);\n return {el:dot, t:i/PPC};\n });\n}\nfunction tickParticles() {\n for (const e of edges) {\n if (!e.ptcls) continue;\n const a=nodes.get(e.fromId), b=nodes.get(e.toId);\n if (!a||!b) continue;\n for (const p of e.ptcls) {\n p.t=(p.t+0.0045)%1;\n const t=p.t;\n let px,py;\n if (e.type==='intercom') {\n const mx=(a.x+b.x)/2, my=(a.y+b.y)/2-65;\n px=(1-t)*(1-t)*a.x+2*(1-t)*t*mx+t*t*b.x;\n py=(1-t)*(1-t)*a.y+2*(1-t)*t*my+t*t*b.y;\n } else {\n px=a.x+(b.x-a.x)*t; py=a.y+(b.y-a.y)*t;\n }\n p.el.setAttribute('cx',px.toFixed(1));\n p.el.setAttribute('cy',py.toFixed(1));\n p.el.setAttribute('opacity',(Math.sin(t*Math.PI)*0.85).toFixed(2));\n }\n }\n}\n\n// ── RAF render loop ───────────────────────────────────────────────────────────\nlet rafLast=0;\nfunction rafLoop(ts) {\n if (ts-rafLast>=16) {\n if (simActive) forceStep();\n for (const n of nodes.values()) {\n if (n.el) n.el.setAttribute('transform',`translate(${n.x.toFixed(1)},${n.y.toFixed(1)})`);\n }\n for (const e of edges) updateEdge(e);\n tickParticles();\n rafLast=ts;\n }\n requestAnimationFrame(rafLoop);\n}\nrequestAnimationFrame(rafLoop);\n\n// ── Graph mutation API ────────────────────────────────────────────────────────\nfunction gAddNode({id,type,domain,agentSlug,label,emoji,color,parentId,cmd}) {\n if (nodes.has(id)) return nodes.get(id);\n const par=parentId?nodes.get(parentId):null;\n const px=par?par.x:CX, py=par?par.y:CY;\n const ang=Math.random()*Math.PI*2;\n const dist={root:0,domain:175,agent:75,org:155}[type]??120;\n const n={\n id,type,domain,agentSlug,\n label:label||id,\n emoji:emoji||(agentSlug?(AGENT_EMOJIS[agentSlug]||AGENT_EMOJIS.default):'◈'),\n color:color||(domain?(DOMAIN_COLORS[domain]||DOMAIN_COLORS.default):DOMAIN_COLORS.default),\n x:type==='root'?CX:px+Math.cos(ang)*dist+(Math.random()-.5)*28,\n y:type==='root'?CY:py+Math.sin(ang)*dist+(Math.random()-.5)*28,\n vx:0,vy:0,ax:0,ay:0,\n fixed:type==='root',\n parentId:parentId||null,\n cmd:cmd||null,\n done:false,\n state:'active',ts:Date.now()\n };\n nodes.set(id,n);\n simActive=nodes.size>1;\n buildNodeEl(n);\n return n;\n}\nfunction gAddEdge({id,fromId,toId,type,msg}) {\n const eid=id||`${fromId}→${toId}`;\n if (edges.find(e=>e.id===eid)) return;\n const e={id:eid,fromId,toId,type:type||'activation',msg};\n edges.push(e);\n buildEdgeEl(e);\n spawnParticles(e);\n}\nfunction gComplete(id) {\n const cn = nodes.get(id); if (cn) cn.done = true;\n const n=nodes.get(id);\n if (!n||!n.el) return;\n n.state='complete';\n const circ=n.el.querySelector('circle[r=\"30\"]')||n.el.querySelector('circle');\n if (circ) gsap.to(circ,{attr:{stroke:'#fbbf24'},duration:0.3,yoyo:true,repeat:2,\n onComplete:()=>gsap.to(n.el,{opacity:0.65,duration:1.5})});\n const ring=n.el.querySelector('[data-cring]');\n if (ring) gsap.to(ring,{opacity:1,'stroke-dashoffset':0,duration:1.6,ease:'power1.inOut'});\n}\nfunction gClear() {\n nodes.clear(); edges.length=0; rootId=null; simActive=false;\n nodesG.innerHTML=''; edgesG.innerHTML=''; particlesG.innerHTML='';\n}\nfunction pulseRoot() {\n const n=nodes.get(rootId);\n if (!n||!n.el) return;\n const c=n.el.querySelector('circle[r=\"38\"]');\n if (c) gsap.to(c,{attr:{'stroke-width':6},duration:0.25,yoyo:true,repeat:1});\n}\n\n// ── Activity log ──────────────────────────────────────────────────────────────\nfunction addLog(tag,msg,color) {\n const wrap=document.getElementById('log-entries');\n const row=document.createElement('div');\n row.className='log-row';\n row.innerHTML=`<span class=\"log-tag\" style=\"color:${color}\">${tag}</span><span class=\"log-msg\">${msg}</span>`;\n wrap.appendChild(row);\n gsap.fromTo(row,{opacity:0},{opacity:1,duration:0.3});\n const rows=wrap.querySelectorAll('.log-row');\n if (rows.length>10) gsap.to(rows[0],{opacity:0,height:0,duration:0.22,onComplete:()=>rows[0].remove()});\n}\n\n// ── Movie mode ────────────────────────────────────────────────────────────────\nlet isMovieMode=false;\nlet movieTl=null;\n\nfunction buildMovieTl(sessionData) {\n gClear();\n document.getElementById('log-entries').innerHTML='';\n document.getElementById('p-text').textContent='';\n const evts=[...(sessionData&&sessionData.events?sessionData.events:[])].sort((a,b)=>(a.ts||0)-(b.ts||0));\n const tl=gsap.timeline({paused:true,defaults:{ease:'power2.out'}});\n if (!evts.length) {\n tl.add(()=>addLog('[DEMO]','Select a session from the sidebar','#00E5C8'),0.2);\n return tl;\n }\n evts.forEach((ev,i)=>{\n const ev2=Object.assign({},ev);\n tl.add(()=>handleGraphEvent(ev2), 0.3+i*0.75);\n });\n tl.duration(0.3+evts.length*0.75+1.5);\n return tl;\n}\n\nfunction toggleMovieMode(sessionData) {\n isMovieMode=!isMovieMode;\n const btn=document.getElementById('sb-movie-btn');\n const banner=document.getElementById('mode-banner');\n const scrubEl=document.getElementById('scrubber');\n const tDisp=document.getElementById('t-disp');\n if (isMovieMode) {\n btn.classList.add('active'); btn.textContent='■ EXIT MOVIE';\n banner.textContent='MOVIE'; banner.classList.remove('live-mode');\n ['btn-restart','btn-play','btn-pause'].forEach(id=>document.getElementById(id).classList.remove('disabled'));\n scrubEl.disabled=false;\n if (movieTl) movieTl.kill();\n movieTl=buildMovieTl(sessionData);\n document.getElementById('btn-play').onclick=()=>movieTl.resume();\n document.getElementById('btn-pause').onclick=()=>movieTl.pause();\n document.getElementById('btn-restart').onclick=()=>{\n gClear(); document.getElementById('log-entries').innerHTML='';\n movieTl=buildMovieTl(sessionData); movieTl.play();\n };\n document.getElementById('spd').onchange=e=>movieTl&&movieTl.timeScale(Number(e.target.value));\n let scrubbing=false;\n scrubEl.addEventListener('mousedown',()=>{scrubbing=true;movieTl&&movieTl.pause();});\n scrubEl.addEventListener('mouseup',()=>{scrubbing=false;});\n scrubEl.addEventListener('input',()=>{if(movieTl)movieTl.progress(Number(scrubEl.value)/100);tDisp.textContent=(movieTl?movieTl.time():0).toFixed(1)+'s';});\n gsap.ticker.add(()=>{\n if (!scrubbing&&movieTl&&movieTl.totalDuration()>0) {\n scrubEl.value=movieTl.progress()*100;\n tDisp.textContent=movieTl.time().toFixed(1)+'s';\n }\n });\n gsap.to('#ctrl',{opacity:1,duration:0.35});\n movieTl.play();\n } else {\n btn.classList.remove('active'); btn.textContent='▶ MOVIE MODE';\n banner.textContent='LIVE'; banner.classList.add('live-mode');\n ['btn-restart','btn-play','btn-pause'].forEach(id=>document.getElementById(id).classList.add('disabled'));\n scrubEl.disabled=true; tDisp.textContent='—';\n if (movieTl){movieTl.kill();movieTl=null;}\n gsap.to('#ctrl',{opacity:0,duration:0.25});\n }\n}\n\n// ── Core event dispatcher ─────────────────────────────────────────────────────\nfunction handleGraphEvent(ev) {\n const {type,session,domain,agent,from,to,msg,cmd,prompt,status} = ev;\n if (type==='session:start') {\n gClear(); rootId=session;\n gAddNode({id:session,type:'root',label:'MASTERMIND',color:DOMAIN_COLORS.default});\n if (prompt) {\n document.getElementById('p-tag').textContent='RUNNING';\n document.getElementById('p-text').textContent=prompt;\n gsap.to('#prompt-box',{opacity:1,duration:0.4});\n }\n gsap.to('#activity-log',{opacity:1,duration:0.4,delay:0.2});\n gsap.to('#ctrl',{opacity:1,duration:0.4,delay:0.4});\n addLog('[SESSION]',(prompt||session).slice(0,32),'#00E5C8');\n refreshSessions();\n } else if (type==='domain:dispatch') {\n if (!domain||!rootId) return;\n const domId=`${session}:${domain}`;\n gAddNode({id:domId,type:'domain',domain,parentId:rootId,cmd:cmd||null,\n label:domain.toUpperCase(),emoji:DOMAIN_EMOJIS[domain]||'◈'});\n gAddEdge({fromId:rootId,toId:domId,type:'activation'});\n pulseRoot();\n addLog(`[${domain.toUpperCase()}]`,cmd||domain,DOMAIN_COLORS[domain]||'#00E5C8');\n } else if (type==='agent:spawn') {\n if (!domain||!rootId) return;\n const domId=`${session}:${domain}`;\n const agId=`${session}:${domain}:${agent||'agent'}:${ev._replayIdx!==undefined?ev._replayIdx:Date.now()}`;\n gAddNode({id:agId,type:'agent',domain,agentSlug:agent,parentId:domId,\n label:agent||'agent',emoji:AGENT_EMOJIS[agent]||AGENT_EMOJIS.default});\n gAddEdge({fromId:domId,toId:agId,type:'spawn'});\n addLog(`[${(agent||'agent').slice(0,9)}]`,ev.task||agent||'',DOMAIN_COLORS[domain]||'#00E5C8');\n } else if (type==='intercom') {\n if (!from||!to||!rootId) return;\n gAddEdge({id:`ic-${from}-${to}-${Date.now()}`,fromId:`${session}:${from}`,\n toId:`${session}:${to}`,type:'intercom',msg});\n addLog('[IC]',`${from}→${to}: ${msg||''}`,'#c084fc');\n } else if (type==='domain:complete') {\n gComplete(`${session}:${domain}`);\n pulseRoot();\n addLog(`[${(domain||'').toUpperCase()}]`,`done · ${status||'✓'}`,'#34d399');\n refreshSessions();\n } else if (type==='session:complete') {\n for (const n of nodes.values()) {\n if (n.el) gsap.to(n.el,{opacity:1,duration:0.3,yoyo:true,repeat:2,ease:'power1.inOut'});\n }\n addLog('[✓]',`complete — ${(ev.domains||[]).length||'all'} domains`,'#34d399');\n setTimeout(()=>gsap.to('#prompt-box',{opacity:0,duration:1}),4000);\n refreshSessions();\n }\n}\n\n// ── Live event handler ────────────────────────────────────────────────────────\nfunction handleLiveEvent(ev) {\n if (isMovieMode) return;\n handleGraphEvent(ev);\n}\n\n\n// ── Session graph replay ───────────────────────────────────────────────\nfunction replaySessionGraph(events) {\n if (!events || !events.length) return;\n gClear();\n let skipRefresh = true;\n const origRefresh = window.refreshSessions;\n window.refreshSessions = () => {}; // suppress during replay\n events.forEach((ev, idx) => handleGraphEvent({...ev, _replayIdx: idx}));\n window.refreshSessions = origRefresh;\n gsap.to('#activity-log', {opacity:1, duration:0.4});\n gsap.to('#ctrl', {opacity:1, duration:0.4, delay:0.2});\n}\n\n// ── SSE event stream ───────────────────────────────────────────────────────────\nlet evtSource = null;\nfunction connectSSE() {\n if (evtSource) evtSource.close();\n evtSource = new EventSource('/api/mastermind-stream');\n evtSource.onmessage = (e) => {\n try {\n const ev = JSON.parse(e.data);\n handleLiveEvent(ev);\n } catch (_) {}\n };\n evtSource.onerror = () => {\n const dot = document.getElementById('l-dot');\n if (dot) dot.classList.remove('on');\n const st = document.getElementById('l-status');\n if (st) st.textContent = 'RECONNECTING';\n showStatusBanner('SSE disconnected — reconnecting in 4s');\n setTimeout(connectSSE, 4000);\n };\n}\n\n// ── Session sidebar ────────────────────────────────────────────────────────────\nlet currentSessionId = null;\n let currentSessionObj = null;\n\nlet activeProjectFilter = '';\n\nfunction applyProjectFilter(val) {\n activeProjectFilter = val;\n refreshSessions();\n}\n\nasync function refreshSessions() {\n try {\n const url = activeProjectFilter\n ? `/api/mastermind/sessions?project=${encodeURIComponent(activeProjectFilter)}`\n : '/api/mastermind/sessions';\n const res = await fetch(url);\n const sessions = await res.json();\n // Populate project filter options\n const sel = document.getElementById('proj-filter');\n if (sel) {\n const projects = [...new Set(sessions.map(s => s.project).filter(Boolean))];\n const current = sel.value;\n sel.innerHTML = '<option value=\"\">ALL PROJECTS</option>' +\n projects.map(p => {\n const name = p.split('/').pop();\n return `<option value=\"${p}\" ${p===current?'selected':''}>${name}</option>`;\n }).join('');\n }\n renderSessions(sessions);\n } catch (_) {}\n}\n\nfunction renderSessions(sessions) {\n const wrap = document.getElementById('sb-sessions');\n const noSess = document.getElementById('sb-no-sessions');\n if (!sessions || !sessions.length) {\n if (noSess) noSess.style.display = 'block';\n const items = wrap.querySelectorAll('.sess-item');\n items.forEach(i => i.remove());\n return;\n }\n if (noSess) noSess.style.display = 'none';\n // Remove old items\n wrap.querySelectorAll('.sess-item').forEach(el => el.remove());\n sessions.forEach(s => {\n const item = document.createElement('div');\n item.className = 'sess-item' + (s.status === 'running' ? ' running' : '') + (s.id === currentSessionId ? ' active' : '');\n const ts = new Date(s.ts).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});\n const date = new Date(s.ts).toLocaleDateString([], {month:'short',day:'numeric'});\n const elapsed = s.endTs ? ((s.endTs - s.ts)/1000).toFixed(0)+'s' : (s.status==='running'?'RUNNING…':'?');\n const projName = s.project ? s.project.split('/').pop() : '';\n item.innerHTML = `\n ${projName ? `<div class=\"sess-project\">◈ ${projName}</div>` : ''}\n <div class=\"sess-ts\">${date} ${ts} · ${elapsed}</div>\n <div class=\"sess-prompt\">${s.prompt||'(no prompt)'}</div>\n <div class=\"sess-badges\">\n <span class=\"sess-badge ${s.status==='running'?'running-badge':''}\">${s.status||'?'}</span>\n ${(s.domains||[]).slice(0,4).map(d=>`<span class=\"sess-badge\">${d}</span>`).join('')}\n ${(s.domains||[]).length>4?`<span class=\"sess-badge\">+${s.domains.length-4}</span>`:''}\n <a class=\"sess-trace-link\" href=\"/api/mastermind/session/${s.id}/trace\" target=\"_blank\" title=\"View raw trace\" onclick=\"event.stopPropagation()\">trace↗</a>\n </div>`;\n item.addEventListener('click', () => {\n wrap.querySelectorAll('.sess-item').forEach(x=>x.classList.remove('active'));\n item.classList.add('active');\n currentSessionId = s.id;\n currentSessionObj = s;\n openSessionDetail(s);\n replaySessionGraph(s.events||[]);\n });\n wrap.appendChild(item);\n });\n}\n\n// ── Detail panel ───────────────────────────────────────────────────────────────\nfunction openDomainDetail(d) {\n const panel = document.getElementById('detail-panel');\n document.getElementById('dp-emoji').textContent = d.emoji || '◈';\n document.getElementById('dp-title').textContent = `DOMAIN · ${d.label}`;\n const body = document.getElementById('dp-body');\n // Gather events from current session for this domain\n const sessEvts = (currentSessionObj && currentSessionObj.events) ? currentSessionObj.events : [];\n const domEvts = sessEvts.filter(e => e.domain === d.domain || e.domain === (d.label||'').toLowerCase());\n const spawnEvts = domEvts.filter(e => e.type === 'agent:spawn');\n const artifacts = domEvts.flatMap(e => e.artifacts || []);\n // Also collect child agent nodes\n const agentNodes = [];\n nodes.forEach(n => { if (n.parentId === d.id) agentNodes.push(n); });\n body.innerHTML = `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">DOMAIN INFO</div>\n <div class=\"dp-event\"><span class=\"ev-type\" style=\"color:${d.color}\">${d.emoji||'◈'} ${d.label}</span></div>\n ${d.cmd ? `<div class=\"dp-event\">Command: <span style=\"color:#7080c0\">${d.cmd}</span></div>` : ''}\n <div class=\"dp-event\">Status: <span style=\"color:${d.done?'#40e880':'#28c068'}\">${d.done?'COMPLETE':'RUNNING'}</span></div>\n <div class=\"dp-event\">Agents spawned: <span style=\"color:${d.color}\">${agentNodes.length}</span></div>\n </div>\n ${agentNodes.length > 0 ? `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">AGENTS</div>\n ${agentNodes.map(a => `<div class=\"dp-event\"><span class=\"ev-type\" style=\"color:${a.color||d.color}\">${a.emoji||'◈'} ${a.label}</span></div>`).join('')}\n </div>` : ''}\n ${spawnEvts.length > 0 ? `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">TASKS</div>\n ${spawnEvts.map(e => `<div class=\"dp-event\" style=\"color:#506080;font-size:8px;white-space:normal;word-break:break-word;line-height:1.5\">${e.agent ? '<b>'+e.agent+'</b>: ' : ''}${(e.task||'').slice(0,120)}</div>`).join('')}\n </div>` : ''}\n ${artifacts.length > 0 ? `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">ARTIFACTS</div>\n ${artifacts.map(a => `<div class=\"dp-artifact\">📄 ${a}</div>`).join('')}\n </div>` : ''}\n ${domEvts.length > 0 ? `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">RECENT EVENTS</div>\n ${domEvts.slice(-8).map(e => {\n const ts = new Date(e.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});\n return `<div class=\"dp-event\"><span class=\"ev-ts\">${ts}</span> <span class=\"ev-type\" style=\"color:${d.color}\">${e.type}</span>${e.cmd?' '+e.cmd:e.agent?' '+e.agent:''}</div>`;\n }).join('')}\n </div>` : ''}\n `;\n panel.classList.add('open');\n}\n\nasync function openSessionDetail(s) {\n const panel = document.getElementById('detail-panel');\n document.getElementById('dp-emoji').textContent = '📋';\n document.getElementById('dp-title').textContent = 'SESSION DETAIL';\n const body = document.getElementById('dp-body');\n body.innerHTML = '<div style=\"color:#303060;font-size:9px;padding:8px\">Loading…</div>';\n panel.classList.add('open');\n try {\n const res = await fetch(`/api/mastermind/session/${s.id}`);\n const full = await res.json();\n if (!full) { body.innerHTML = '<div style=\"color:#303060;font-size:9px\">Session not found.</div>'; return; }\n const ts = new Date(full.ts).toLocaleString();\n const elapsed = full.endTs ? ((full.endTs - full.ts)/1000).toFixed(1)+'s' : 'running';\n const evts = full.events || [];\n const domainSet = full.domains || [];\n body.innerHTML = `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">OVERVIEW</div>\n <div class=\"dp-event\">Started: <span style=\"color:#6060a0\">${ts}</span></div>\n <div class=\"dp-event\">Duration: <span style=\"color:#6060a0\">${elapsed}</span></div>\n <div class=\"dp-event\">Status: <span style=\"color:${full.status==='complete'?'#40e880':full.status==='running'?'#28c068':'#f87171'}\">${full.status||'?'}</span></div>\n <div class=\"dp-event\">Domains: <span style=\"color:#8080c0\">${domainSet.join(', ')||'—'}</span></div>\n </div>\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">PROMPT</div>\n <div class=\"dp-event\" style=\"color:oklch(58% 0.09 186);word-break:break-all;white-space:normal;line-height:1.6\">${full.prompt||'—'}</div>\n </div>\n ${domainSet.length ? `\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">ACTIVE DOMAINS</div>\n ${domainSet.map(did => {\n const color = DOMAIN_COLORS[did] || '#8080c0';\n const emoji = DOMAIN_EMOJIS[did] || '◈';\n const label = (did||'').toUpperCase();\n return `<div class=\"dp-event\"><span style=\"color:${color}\">${emoji} ${label}</span></div>`;\n }).join('')}\n </div>` : ''}\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">EVENT TIMELINE (${evts.length})</div>\n ${evts.map(e => {\n const et = new Date(e.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});\n const color = e.domain ? (DOMAIN_COLORS[e.domain] || '#6060a0') : '#6060a0';\n let detail = '';\n if (e.type === 'session:start') detail = `<span style=\"color:#5050a0;font-size:8px;word-break:break-all\">${e.prompt||''}</span>`;\n else if (e.type === 'domain:dispatch') detail = `<span style=\"color:#5060a0;font-size:8px\">${e.cmd||''}</span>`;\n else if (e.type === 'agent:spawn') detail = `<span style=\"color:#507090;font-size:8px\">agent: <b>${e.agent||''}</b> — ${(e.task||'').slice(0,50)}</span>`;\n else if (e.type === 'intercom') detail = `<span style=\"color:#506070;font-size:8px\">${e.from||'?'} → ${e.to||'?'}: ${e.msg||''}</span>`;\n else if (e.type === 'domain:complete') {\n const arts = (e.artifacts||[]).map(a=>`<span style=\"color:#407050;font-size:7px\">📄 ${a}</span>`).join(' ');\n detail = `<span style=\"color:#406050;font-size:8px\">status: ${e.status||'?'}</span>${arts?' '+arts:''}`;\n }\n else if (e.type === 'session:complete') detail = `<span style=\"color:#405080;font-size:8px\">domains: ${(e.domains||[]).join(', ')}</span>`;\n return `<div class=\"dp-event\" style=\"flex-direction:column;align-items:flex-start;gap:1px\"><div><span class=\"ev-ts\">${et}</span> <span class=\"ev-type\" style=\"color:${color}\">${e.type}</span>${e.domain?' <span style=\"color:#404060;font-size:8px\">['+e.domain+']</span>':''}</div>${detail?'<div style=\"padding-left:4px\">'+detail+'</div>':''}</div>`;\n }).join('')}\n </div>\n <div class=\"dp-section\">\n <div class=\"dp-section-title\">EXPORT</div>\n <div style=\"display:flex;gap:6px;flex-wrap:wrap\">\n <a class=\"dp-export-btn\" href=\"/api/mastermind/session/${full.id}/trace\" target=\"_blank\">📄 View Trace</a>\n <button class=\"dp-export-btn\" onclick=\"downloadSession('${full.id}')\">⬇ Download JSON</button>\n </div>\n </div>\n `;\n } catch(err) {\n body.innerHTML = `<div style=\"color:#a03030;font-size:9px\">${err.message}</div>`;\n }\n}\n\nfunction closeDetail() {\n document.getElementById('detail-panel').classList.remove('open');\n currentSessionId = null;\n document.querySelectorAll('.sess-item').forEach(x=>x.classList.remove('active'));\n}\n\nasync function downloadSession(id) {\n const res = await fetch(`/api/mastermind/session/${id}`);\n const data = await res.json();\n const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});\n const a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = `${id}.json`;\n a.click();\n URL.revokeObjectURL(a.href);\n}\n\n// ── Live data polling for status bar ──────────────────────────────────────────\nasync function pollStatus() {\n try {\n const res = await fetch('/api/data');\n if (!res.ok) return;\n const data = await res.json();\n const active = !!data?.swarm?.activity?.swarm?.active;\n const dot = document.getElementById('l-dot');\n dot.classList.toggle('on', active);\n document.getElementById('l-status').textContent = active ? 'LIVE' : 'IDLE';\n const n = data?.swarm?.state?.agentPlan?.length || 0;\n document.getElementById('l-agents').textContent = n + ' agent' + (n!==1?'s':'');\n // Highlight last routed domain\n const route = data?.hooks?.lastRoute || '';\n if (route && !isMovieMode) {\n const hit = DOMAINS.find(d => route.toLowerCase().includes(d.id));\n if (hit) {\n gsap.to(`#gr-${hit.id}`, { opacity:0.85, attr:{r:52}, duration:0.35 });\n gsap.to(`#gr-${hit.id}`, { opacity:0.2, attr:{r:44}, duration:1.8, delay:0.35 });\n }\n }\n } catch (_) {}\n}\n\n\nfunction showStatusBanner(msg) {\n let b = document.getElementById('status-banner');\n if (!b) {\n b = document.createElement('div'); b.id = 'status-banner';\n b.style.cssText = 'position:fixed;top:0;left:0;right:0;padding:5px 14px;background:oklch(24% 0.05 186);border-bottom:1px solid oklch(68% 0.18 186 / 0.35);color:oklch(70% 0.05 186);font-size:9px;letter-spacing:1.5px;text-align:center;z-index:9999;transition:opacity 0.5s;pointer-events:none;';\n document.body.appendChild(b);\n }\n b.textContent = msg; b.style.opacity = '1';\n clearTimeout(b._t); b._t = setTimeout(() => { b.style.opacity = '0'; }, 5000);\n}\n\n// ── Bootstrap ──────────────────────────────────────────────────────────────────\nconnectSSE();\nrefreshSessions();\npollStatus();\nfetch('/api/git-user').then(r=>r.json()).then(u=>{\n if (u.name) document.getElementById('git-user-name').textContent = u.name;\n if (u.cwd) {\n const parts = u.cwd.replace(/\\\\/g, '/').split('/');\n document.getElementById('git-cwd-name').textContent = parts.slice(-2).join('/');\n document.getElementById('git-cwd-name').title = u.cwd;\n }\n}).catch(()=>{});\nsetInterval(pollStatus, 4000);\nsetInterval(refreshSessions, 8000);\n\n// Set initial live mode banner\ndocument.getElementById('mode-banner').classList.add('live-mode');\n</script>\n</body>\n</html>\n";
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// ─── Session JSONL parser ────────────────────────────────────────────────────
|
|
50
|
-
function categorizeTool(name) {
|
|
51
|
-
if (['Read','Write','Edit','MultiEdit','Glob','Grep','LS'].includes(name)) return 'file';
|
|
52
|
-
if (name === 'Bash') return 'bash';
|
|
53
|
-
if (['Agent','Task'].includes(name)) return 'agent';
|
|
54
|
-
if (name.startsWith('mcp__monobrain__memory') || name.startsWith('mcp__monobrain__agentdb')) return 'memory';
|
|
55
|
-
if (['WebFetch','WebSearch'].includes(name)) return 'web';
|
|
56
|
-
if (name === 'TodoWrite' || name === 'TodoRead') return 'task';
|
|
57
|
-
if (name === 'Skill') return 'skill';
|
|
58
|
-
if (name === 'ToolSearch') return 'search';
|
|
59
|
-
if (name.startsWith('mcp__')) return 'mcp';
|
|
60
|
-
return 'other';
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function parseSessionLines(lines) {
|
|
64
|
-
const events = [];
|
|
65
|
-
let agentDepth = 0;
|
|
66
|
-
const toolMap = new Map(); // id → tool event index
|
|
67
|
-
|
|
68
|
-
for (const line of lines) {
|
|
69
|
-
let entry;
|
|
70
|
-
try { entry = JSON.parse(line); } catch { continue; }
|
|
71
|
-
const type = entry.type;
|
|
72
|
-
const ts = entry.timestamp || null;
|
|
73
|
-
const uuid = entry.uuid || null;
|
|
74
|
-
|
|
75
|
-
if (type === 'user') {
|
|
76
|
-
const content = entry.message?.content;
|
|
77
|
-
let text = '';
|
|
78
|
-
if (typeof content === 'string') text = content;
|
|
79
|
-
else if (Array.isArray(content)) {
|
|
80
|
-
text = content.filter(b => b && b.type === 'text').map(b => b.text).join('');
|
|
81
|
-
}
|
|
82
|
-
if (text && text.length > 0) {
|
|
83
|
-
events.push({ kind: 'user', text: text.slice(0, 500), uuid, ts });
|
|
84
|
-
}
|
|
85
|
-
} else if (type === 'assistant') {
|
|
86
|
-
const content = entry.message?.content || [];
|
|
87
|
-
for (const block of (Array.isArray(content) ? content : [])) {
|
|
88
|
-
if (!block || typeof block !== 'object') continue;
|
|
89
|
-
if (block.type === 'thinking') {
|
|
90
|
-
events.push({ kind: 'thinking', text: (block.thinking || '').slice(0, 200), uuid, ts });
|
|
91
|
-
} else if (block.type === 'text') {
|
|
92
|
-
const t = (block.text || '').trim();
|
|
93
|
-
if (t) events.push({ kind: 'text', text: t.slice(0, 600), uuid, ts });
|
|
94
|
-
} else if (block.type === 'tool_use') {
|
|
95
|
-
const cat = categorizeTool(block.name);
|
|
96
|
-
const label = buildToolLabel(block.name, block.input || {});
|
|
97
|
-
const idx = events.length;
|
|
98
|
-
const ev = { kind: 'tool', name: block.name, cat, label, id: block.id, uuid, ts };
|
|
99
|
-
if (cat === 'agent') {
|
|
100
|
-
ev.subagent = block.input?.subagent_type || block.input?.description || '?';
|
|
101
|
-
ev.background = !!block.input?.run_in_background;
|
|
102
|
-
}
|
|
103
|
-
events.push(ev);
|
|
104
|
-
if (block.id) toolMap.set(block.id, idx);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} else if (type === 'tool') {
|
|
108
|
-
const content = entry.message?.content || [];
|
|
109
|
-
for (const block of (Array.isArray(content) ? content : [])) {
|
|
110
|
-
if (!block || block.type !== 'tool_result') continue;
|
|
111
|
-
const resultText = Array.isArray(block.content)
|
|
112
|
-
? block.content.filter(b => b && b.type === 'text').map(b => b.text).join('').slice(0, 400)
|
|
113
|
-
: String(block.content || '').slice(0, 400);
|
|
114
|
-
const isError = !!block.is_error;
|
|
115
|
-
const toolIdx = toolMap.get(block.tool_use_id);
|
|
116
|
-
events.push({ kind: 'tool_result', tool_use_id: block.tool_use_id, text: resultText, isError, toolIdx, uuid, ts });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return events;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function buildToolLabel(name, input) {
|
|
124
|
-
if (name === 'Read') return input.file_path ? `Read ${path.basename(input.file_path)}` : 'Read';
|
|
125
|
-
if (name === 'Write') return input.file_path ? `Write ${path.basename(input.file_path)}` : 'Write';
|
|
126
|
-
if (name === 'Edit') return input.file_path ? `Edit ${path.basename(input.file_path)}` : 'Edit';
|
|
127
|
-
if (name === 'Bash') return (input.description || input.command || 'Bash').slice(0, 60);
|
|
128
|
-
if (name === 'Grep') return `Grep ${(input.pattern || '').slice(0, 30)}`;
|
|
129
|
-
if (name === 'Glob') return `Glob ${(input.pattern || '').slice(0, 30)}`;
|
|
130
|
-
if (name === 'Agent' || name === 'Task') return `→ ${input.subagent_type || input.description || 'agent'}`;
|
|
131
|
-
if (name === 'WebFetch') return `Fetch ${(input.url || '').slice(0, 50)}`;
|
|
132
|
-
if (name === 'WebSearch') return `Search ${(input.query || '').slice(0, 40)}`;
|
|
133
|
-
if (name === 'Skill') return `Skill: ${input.skill || '?'}`;
|
|
134
|
-
if (name.startsWith('mcp__monobrain__memory')) return name.replace('mcp__monobrain__memory_', 'mem:');
|
|
135
|
-
if (name.startsWith('mcp__')) return name.replace('mcp__monobrain__', '⬡ ').replace('mcp__', '⬡ ').slice(0, 40);
|
|
136
|
-
return name.slice(0, 40);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ─── Section collectors (for /api/section lazy load) ────────────────────────
|
|
140
|
-
function buildSectionData(name, dir) {
|
|
141
|
-
const d = path.resolve(dir);
|
|
142
|
-
switch (name) {
|
|
143
|
-
case 'sessions': return { sessions: collectSessions(d) };
|
|
144
|
-
case 'swarm': return { swarm: collectSwarm(d), swarmHistory: collectSwarmHistory(d), agents: collectAgents(d) };
|
|
145
|
-
case 'agents': return { agents: collectAgents(d) };
|
|
146
|
-
case 'tokens': return { tokens: collectTokens(d) };
|
|
147
|
-
case 'hooks': return { hooks: collectHooks(d) };
|
|
148
|
-
case 'knowledge':return { knowledge: collectKnowledge(d) };
|
|
149
|
-
case 'metrics': return { metrics: collectMetrics(d) };
|
|
150
|
-
case 'system': return { system: collectSystem() };
|
|
151
|
-
case 'memory': {
|
|
152
|
-
const s = collectSessions(d);
|
|
153
|
-
return { sessions: { palace: s.palace }, memory: collectMemory(d) };
|
|
154
|
-
}
|
|
155
|
-
case 'overview': return { project: collectProject(d), system: collectSystem() };
|
|
156
|
-
default: return {};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Map file path fragment → affected section names
|
|
161
|
-
function pathToSections(filename) {
|
|
162
|
-
if (!filename) return null;
|
|
163
|
-
const f = filename.toLowerCase();
|
|
164
|
-
if (f.includes('swarm')) return ['swarm'];
|
|
165
|
-
if (f.includes('token')) return ['tokens'];
|
|
166
|
-
if (f.includes('registry') || f.includes('registrations')) return ['agents'];
|
|
167
|
-
if (f.includes('route') || f.includes('worker-dispatch')) return ['hooks'];
|
|
168
|
-
if (f.includes('chunk') || f.includes('skills')) return ['knowledge'];
|
|
169
|
-
if (f.includes('memory.db') || f.includes('memory.graph') || f.includes('hnsw.index') ||
|
|
170
|
-
f.includes('monovector.db') || f.includes('ranked-context') ||
|
|
171
|
-
(f.includes('/memory/') && f.endsWith('.md'))) return ['memory', 'sessions'];
|
|
172
|
-
if (f.includes('palace') || f.includes('drawers') || f.includes('identity')) return ['memory', 'sessions'];
|
|
173
|
-
if (f.includes('ddd') || f.includes('learning') || f.includes('audit')) return ['metrics'];
|
|
174
|
-
if (f.endsWith('.jsonl') || f.includes('sessions')) return ['sessions'];
|
|
175
|
-
return ['sessions', 'swarm', 'agents', 'tokens', 'hooks'];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// SSE client registry
|
|
179
|
-
const sseClients = new Set();
|
|
180
|
-
// Mastermind real-time event stream clients
|
|
181
|
-
const mmSseClients = new Set();
|
|
182
|
-
|
|
183
|
-
// Server state
|
|
184
|
-
let running = false;
|
|
185
|
-
let currentPort = null;
|
|
186
|
-
let currentUrl = null;
|
|
187
|
-
let activeServer = null;
|
|
188
|
-
const activeWatchers = [];
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Broadcasts a data payload to all connected SSE clients.
|
|
192
|
-
* Silently removes clients that have disconnected.
|
|
193
|
-
*/
|
|
194
|
-
function broadcast(data) {
|
|
195
|
-
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
196
|
-
for (const client of sseClients) {
|
|
197
|
-
try {
|
|
198
|
-
client.write(msg);
|
|
199
|
-
} catch {
|
|
200
|
-
sseClients.delete(client);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Opens a URL in the default browser, cross-platform.
|
|
207
|
-
*/
|
|
208
|
-
async function openUrl(url) {
|
|
209
|
-
const { exec } = await import('child_process');
|
|
210
|
-
const cmd =
|
|
211
|
-
process.platform === 'darwin'
|
|
212
|
-
? `open "${url}"`
|
|
213
|
-
: process.platform === 'win32'
|
|
214
|
-
? `start "${url}"`
|
|
215
|
-
: `xdg-open "${url}"`;
|
|
216
|
-
exec(cmd);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Attempts to bind the HTTP server to a port, trying up to 10 increments
|
|
221
|
-
* if the initial port is already in use.
|
|
222
|
-
*/
|
|
223
|
-
function bindServer(server, port) {
|
|
224
|
-
return new Promise((resolve, reject) => {
|
|
225
|
-
const maxTries = 10;
|
|
226
|
-
let attempt = 0;
|
|
227
|
-
|
|
228
|
-
function tryPort(p) {
|
|
229
|
-
server.listen(p, () => resolve(p));
|
|
230
|
-
server.once('error', (err) => {
|
|
231
|
-
if (err.code === 'EADDRINUSE' && attempt < maxTries) {
|
|
232
|
-
attempt += 1;
|
|
233
|
-
server.removeAllListeners('error');
|
|
234
|
-
tryPort(p + 1);
|
|
235
|
-
} else {
|
|
236
|
-
reject(err);
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
tryPort(port);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Starts the monomind live dashboard HTTP server.
|
|
247
|
-
*
|
|
248
|
-
* @param {object} [options]
|
|
249
|
-
* @param {number} [options.port=4242] - Preferred port. Tries up to port+10 on collision.
|
|
250
|
-
* @param {string} [options.projectDir] - Root of the project to collect data from.
|
|
251
|
-
* @param {boolean} [options.openBrowser=true] - Whether to open the dashboard in the default browser.
|
|
252
|
-
* @returns {Promise<{port: number, url: string, server: http.Server}>}
|
|
253
|
-
*/
|
|
254
|
-
/**
|
|
255
|
-
* Resolve a Claude project slug back to the real filesystem path.
|
|
256
|
-
* Slugs are created by replacing all '/' with '-', so paths containing
|
|
257
|
-
* hyphens (like agent-f/agf-accounting) are ambiguous. This function
|
|
258
|
-
* uses a greedy BFS over the real filesystem to find the correct path.
|
|
259
|
-
* Falls back to cwd in session files, then to direct slug replacement.
|
|
260
|
-
*/
|
|
261
|
-
function resolveSlugToPath(slug, projDir) {
|
|
262
|
-
// 1. Try filesystem BFS (most reliable)
|
|
263
|
-
const parts = slug.replace(/^-/, '').split('-');
|
|
264
|
-
function tryPaths(idx, current) {
|
|
265
|
-
if (idx === parts.length) return fs.existsSync(current) ? current : null;
|
|
266
|
-
// Option A: next part is a new path component
|
|
267
|
-
const asDir = path.join(current, parts[idx]);
|
|
268
|
-
const r1 = tryPaths(idx + 1, asDir);
|
|
269
|
-
if (r1) return r1;
|
|
270
|
-
// Option B: combine with hyphen into current basename
|
|
271
|
-
if (current !== '/') {
|
|
272
|
-
const combined = path.join(path.dirname(current), path.basename(current) + '-' + parts[idx]);
|
|
273
|
-
const r2 = tryPaths(idx + 1, combined);
|
|
274
|
-
if (r2) return r2;
|
|
275
|
-
}
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
const fsResolved = parts.length ? tryPaths(1, '/' + parts[0]) : null;
|
|
279
|
-
if (fsResolved) return fsResolved;
|
|
280
|
-
|
|
281
|
-
// 2. Try reading cwd from a session file
|
|
282
|
-
try {
|
|
283
|
-
const sfiles = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
|
|
284
|
-
for (const sf of sfiles) {
|
|
285
|
-
try {
|
|
286
|
-
const line = fs.readFileSync(path.join(projDir, sf), 'utf-8').split('\n').find(l => l.includes('"cwd"'));
|
|
287
|
-
if (line) { const m = line.match(/"cwd"\s*:\s*"([^"]+)"/); if (m?.[1]) return m[1]; }
|
|
288
|
-
} catch {}
|
|
289
|
-
}
|
|
290
|
-
} catch {}
|
|
291
|
-
|
|
292
|
-
// 3. Dumb fallback (known-broken for hyphenated dirs, but last resort)
|
|
293
|
-
return slug.replace(/-/g, '/');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export async function startServer({ port = 4242, projectDir, openBrowser = true } = {}) {
|
|
297
|
-
// Parse a .claude/agents/*.md definition into { name, description, capability{}, document }.
|
|
298
|
-
// Tolerant line-based parse of the YAML frontmatter (expertise / task_types as lists).
|
|
299
|
-
function parseAgentDef(raw) {
|
|
300
|
-
const out = { name: '', description: '', capability: {}, document: '' };
|
|
301
|
-
const fm = String(raw).match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
302
|
-
let front = '', body = String(raw);
|
|
303
|
-
if (fm) { front = fm[1]; body = fm[2]; }
|
|
304
|
-
out.document = body.trim();
|
|
305
|
-
const strip = (s) => s.trim().replace(/^["']|["']$/g, '');
|
|
306
|
-
let inCap = false, listKey = null;
|
|
307
|
-
for (const line of front.split('\n')) {
|
|
308
|
-
const top = line.match(/^([a-z_]+):\s*(.*)$/i);
|
|
309
|
-
if (top && !/^\s/.test(line)) {
|
|
310
|
-
inCap = (top[1] === 'capability'); listKey = null;
|
|
311
|
-
if (top[1] === 'name') out.name = strip(top[2]);
|
|
312
|
-
else if (top[1] === 'description') out.description = strip(top[2]);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
if (!inCap) continue;
|
|
316
|
-
const li = line.match(/^\s+-\s+(.+)$/);
|
|
317
|
-
if (li && listKey) { (out.capability[listKey] = out.capability[listKey] || []).push(strip(li[1])); continue; }
|
|
318
|
-
const kv = line.match(/^\s+([a-z_]+):\s*(.*)$/i);
|
|
319
|
-
if (kv) {
|
|
320
|
-
if (kv[2].trim() === '') { listKey = kv[1]; out.capability[kv[1]] = out.capability[kv[1]] || []; }
|
|
321
|
-
else { listKey = null; out.capability[kv[1]] = strip(kv[2]); }
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return out;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const server = http.createServer(async (req, res) => {
|
|
328
|
-
const url = req.url.split('?')[0];
|
|
329
|
-
|
|
330
|
-
// ------------------------------------------------------------------ GET /
|
|
331
|
-
if (req.method === 'GET' && url === '/') {
|
|
332
|
-
const htmlPath = path.join(__dirname, 'dashboard.html');
|
|
333
|
-
try {
|
|
334
|
-
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
335
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
336
|
-
res.end(html);
|
|
337
|
-
} catch (err) {
|
|
338
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
339
|
-
res.end(`Failed to load dashboard.html: ${err.message}`);
|
|
340
|
-
}
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// ------------------------------------------------- GET /data/avatars/*.svg (agent avatars)
|
|
345
|
-
if (req.method === 'GET' && /^\/data\/avatars\/[A-Za-z0-9._-]+\.svg$/.test(url)) {
|
|
346
|
-
try {
|
|
347
|
-
const name = path.basename(decodeURIComponent(url));
|
|
348
|
-
if (!/^[A-Za-z0-9._-]+\.svg$/.test(name) || name.includes('..')) { res.writeHead(400); res.end(); return; }
|
|
349
|
-
const avatarsDir = path.join(__dirname, 'data', 'avatars');
|
|
350
|
-
const filePath = path.join(avatarsDir, name);
|
|
351
|
-
if (!filePath.startsWith(avatarsDir + path.sep) || !fs.existsSync(filePath)) { res.writeHead(404); res.end(); return; }
|
|
352
|
-
const svg = fs.readFileSync(filePath);
|
|
353
|
-
res.writeHead(200, { 'Content-Type': 'image/svg+xml; charset=utf-8', 'Cache-Control': 'public, max-age=86400' });
|
|
354
|
-
res.end(svg);
|
|
355
|
-
} catch (_) { res.writeHead(404); res.end(); }
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ----------------------------------------------------------------- GET /v2 (alias → /)
|
|
360
|
-
if (req.method === 'GET' && url === '/v2') {
|
|
361
|
-
res.writeHead(301, { 'Location': '/' });
|
|
362
|
-
res.end();
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// --------------------------------------------------------- GET /api/git-user
|
|
367
|
-
if (req.method === 'GET' && url === '/api/git-user') {
|
|
368
|
-
try {
|
|
369
|
-
const { execSync: gitExec } = await import('child_process');
|
|
370
|
-
const cwd = projectDir || process.cwd();
|
|
371
|
-
const name = gitExec('git config user.name', { cwd, encoding: 'utf8' }).trim();
|
|
372
|
-
const email = gitExec('git config user.email', { cwd, encoding: 'utf8' }).trim();
|
|
373
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
374
|
-
res.end(JSON.stringify({ name, email, cwd }));
|
|
375
|
-
} catch (_) {
|
|
376
|
-
const cwd2 = projectDir || process.cwd();
|
|
377
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
378
|
-
res.end(JSON.stringify({ name: '', email: '', cwd: cwd2 }));
|
|
379
|
-
}
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// --------------------------------------------------------- GET /api/data
|
|
384
|
-
if (req.method === 'GET' && url === '/api/data') {
|
|
385
|
-
try {
|
|
386
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
387
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
388
|
-
const snapshot = await collectAll(dir);
|
|
389
|
-
res.writeHead(200, {
|
|
390
|
-
'Content-Type': 'application/json',
|
|
391
|
-
'Access-Control-Allow-Origin': '*',
|
|
392
|
-
'Cache-Control': 'no-cache',
|
|
393
|
-
});
|
|
394
|
-
res.end(JSON.stringify(snapshot));
|
|
395
|
-
} catch (err) {
|
|
396
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
397
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
398
|
-
}
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// ------------------------------------------------------ GET /api/session
|
|
403
|
-
if (req.method === 'GET' && url === '/api/session') {
|
|
404
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
405
|
-
const file = qs.get('file');
|
|
406
|
-
const limit = Math.min(parseInt(qs.get('limit') || '600', 10), 3000);
|
|
407
|
-
if (!file) {
|
|
408
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
409
|
-
res.end(JSON.stringify({ error: 'missing file param' }));
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
try {
|
|
413
|
-
const raw = fs.readFileSync(file, 'utf8');
|
|
414
|
-
const allLines = raw.split('\n').filter(Boolean);
|
|
415
|
-
const lines = allLines.slice(-limit);
|
|
416
|
-
const events = parseSessionLines(lines);
|
|
417
|
-
res.writeHead(200, {
|
|
418
|
-
'Content-Type': 'application/json',
|
|
419
|
-
'Access-Control-Allow-Origin': '*',
|
|
420
|
-
'Cache-Control': 'no-cache',
|
|
421
|
-
});
|
|
422
|
-
res.end(JSON.stringify({ events, total: allLines.length, shown: lines.length }));
|
|
423
|
-
} catch (err) {
|
|
424
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
425
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
426
|
-
}
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// ------------------------------------------------------- GET /api/session-journal
|
|
431
|
-
if (req.method === 'GET' && url === '/api/session-journal') {
|
|
432
|
-
try {
|
|
433
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
434
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
435
|
-
const d = path.resolve(dir || process.cwd());
|
|
436
|
-
const slug = d.replace(/\//g, '-');
|
|
437
|
-
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
438
|
-
|
|
439
|
-
let sessionFiles = [];
|
|
440
|
-
try {
|
|
441
|
-
sessionFiles = fs.readdirSync(projectClaudeDir)
|
|
442
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
443
|
-
.map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
|
|
444
|
-
.filter(Boolean)
|
|
445
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
446
|
-
.slice(0, 15);
|
|
447
|
-
} catch {}
|
|
448
|
-
|
|
449
|
-
const sessions = [];
|
|
450
|
-
for (const { f, mtime } of sessionFiles) {
|
|
451
|
-
const fp = path.join(projectClaudeDir, f);
|
|
452
|
-
const id = f.replace('.jsonl', '');
|
|
453
|
-
let lastPrompt = '', summaries = [], totalDurationMs = 0, totalMessages = 0, firstTs = null, lastTs = null, totalCost = 0, toolCalls = 0, userMessages = 0, cacheReadTokens = 0, totalInputTokens = 0;
|
|
454
|
-
const modelBreakdown = {};
|
|
455
|
-
const filesTouchedSet = new Set();
|
|
456
|
-
try {
|
|
457
|
-
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
|
|
458
|
-
let pendingCompact = false;
|
|
459
|
-
for (const line of lines) {
|
|
460
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
461
|
-
if (e.timestamp) { if (!firstTs) firstTs = e.timestamp; lastTs = e.timestamp; }
|
|
462
|
-
if (e.type === 'last-prompt' && e.lastPrompt) lastPrompt = e.lastPrompt;
|
|
463
|
-
if (e.type === 'user') userMessages++;
|
|
464
|
-
if (e.type === 'system' && e.subtype === 'compact_boundary') pendingCompact = true;
|
|
465
|
-
if (pendingCompact && e.type === 'user') {
|
|
466
|
-
const msg = e.message || {};
|
|
467
|
-
const ct = msg.content || [];
|
|
468
|
-
let text = '';
|
|
469
|
-
if (Array.isArray(ct)) { for (const b of ct) { if (b && b.type === 'text') { text = b.text; break; } } }
|
|
470
|
-
else if (typeof ct === 'string') text = ct;
|
|
471
|
-
const m = text.match(/Summary:\s*([\s\S]+)/);
|
|
472
|
-
if (m) summaries.push({ ts: e.timestamp, text: m[1].trim() });
|
|
473
|
-
pendingCompact = false;
|
|
474
|
-
}
|
|
475
|
-
if (e.type === 'assistant') {
|
|
476
|
-
const msg = e.message || {};
|
|
477
|
-
for (const block of (msg.content || [])) {
|
|
478
|
-
if (block && block.type === 'tool_use') {
|
|
479
|
-
toolCalls++;
|
|
480
|
-
if (['Write','Edit','Read','MultiEdit'].includes(block.name) && block.input?.file_path) {
|
|
481
|
-
filesTouchedSet.add(path.basename(block.input.file_path));
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (msg.usage && msg.model) {
|
|
486
|
-
const c = _sjCalcCost(msg.model, msg.usage);
|
|
487
|
-
totalCost += c;
|
|
488
|
-
const mk = msg.model.replace(/@.*$/, '').replace(/-\d{8}$/, '');
|
|
489
|
-
if (!modelBreakdown[mk]) modelBreakdown[mk] = { calls: 0, cost: 0 };
|
|
490
|
-
modelBreakdown[mk].calls++;
|
|
491
|
-
modelBreakdown[mk].cost += c;
|
|
492
|
-
cacheReadTokens += (msg.usage.cache_read_input_tokens || 0);
|
|
493
|
-
totalInputTokens += (msg.usage.input_tokens || 0)
|
|
494
|
-
+ (msg.usage.cache_creation_input_tokens || 0)
|
|
495
|
-
+ (msg.usage.cache_read_input_tokens || 0);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (e.type === 'system' && e.subtype === 'turn_duration') {
|
|
499
|
-
totalDurationMs += e.durationMs || 0;
|
|
500
|
-
if ((e.messageCount || 0) > totalMessages) totalMessages = e.messageCount;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
} catch {}
|
|
504
|
-
const filesTouched = [...filesTouchedSet].slice(0, 20);
|
|
505
|
-
sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, totalDurationMs, totalMessages, totalCost, toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, filesTouched, file: fp });
|
|
506
|
-
}
|
|
507
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
508
|
-
res.end(JSON.stringify({ sessions }));
|
|
509
|
-
} catch (err) {
|
|
510
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
511
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
512
|
-
}
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// ------------------------------------------------------- GET /api/search-sessions
|
|
517
|
-
if (req.method === 'GET' && url === '/api/search-sessions') {
|
|
518
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
519
|
-
const dir = qs.get('dir') || '';
|
|
520
|
-
const q = (qs.get('q') || '').toLowerCase().trim();
|
|
521
|
-
if (!q || q.length < 2) {
|
|
522
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
523
|
-
res.end(JSON.stringify({ results: [] }));
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
try {
|
|
527
|
-
const d = path.resolve(dir || process.cwd());
|
|
528
|
-
const slug = d.replace(/\//g, '-');
|
|
529
|
-
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
530
|
-
let sessionFiles = [];
|
|
531
|
-
try {
|
|
532
|
-
sessionFiles = fs.readdirSync(projectClaudeDir)
|
|
533
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
534
|
-
.map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
|
|
535
|
-
.filter(Boolean)
|
|
536
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
537
|
-
.slice(0, 20);
|
|
538
|
-
} catch {}
|
|
539
|
-
const results = [];
|
|
540
|
-
for (const { f, mtime } of sessionFiles) {
|
|
541
|
-
const fp = path.join(projectClaudeDir, f);
|
|
542
|
-
const id = f.replace('.jsonl', '');
|
|
543
|
-
let lastPrompt = '';
|
|
544
|
-
const matches = [];
|
|
545
|
-
try {
|
|
546
|
-
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
|
|
547
|
-
for (const line of lines) {
|
|
548
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
549
|
-
if (e.type === 'last-prompt' && e.lastPrompt) lastPrompt = e.lastPrompt;
|
|
550
|
-
if (e.type === 'user') {
|
|
551
|
-
const msg = e.message || {};
|
|
552
|
-
const ct = msg.content || [];
|
|
553
|
-
let text = '';
|
|
554
|
-
if (Array.isArray(ct)) { for (const b of ct) { if (b && b.type === 'text') { text = b.text; break; } } }
|
|
555
|
-
else if (typeof ct === 'string') text = ct;
|
|
556
|
-
if (text.toLowerCase().includes(q)) matches.push({ text: text.slice(0, 150), ts: e.timestamp });
|
|
557
|
-
}
|
|
558
|
-
if (matches.length >= 3) break;
|
|
559
|
-
}
|
|
560
|
-
} catch {}
|
|
561
|
-
if (matches.length) results.push({ id, file: fp, lastPrompt, mtime, matches });
|
|
562
|
-
}
|
|
563
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
564
|
-
res.end(JSON.stringify({ results, q }));
|
|
565
|
-
} catch (err) {
|
|
566
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
567
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
568
|
-
}
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// ------------------------------------------------------- GET /api/tool-errors
|
|
573
|
-
if (req.method === 'GET' && url === '/api/tool-errors') {
|
|
574
|
-
try {
|
|
575
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
576
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
577
|
-
const d = path.resolve(dir || process.cwd());
|
|
578
|
-
const slug = d.replace(/\//g, '-');
|
|
579
|
-
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
580
|
-
let sessionFiles = [];
|
|
581
|
-
try {
|
|
582
|
-
sessionFiles = fs.readdirSync(projectClaudeDir)
|
|
583
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
584
|
-
.map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
|
|
585
|
-
.filter(Boolean).sort((a,b) => b.mtime - a.mtime).slice(0, 10);
|
|
586
|
-
} catch {}
|
|
587
|
-
// tool_use id → name map, then count is_error:true tool_result per tool name
|
|
588
|
-
const errorCounts = {}, totalCounts = {};
|
|
589
|
-
for (const { f } of sessionFiles) {
|
|
590
|
-
const fp = path.join(projectClaudeDir, f);
|
|
591
|
-
try {
|
|
592
|
-
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
|
|
593
|
-
const toolIdMap = {};
|
|
594
|
-
for (const line of lines) {
|
|
595
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
596
|
-
if (e.type === 'assistant') {
|
|
597
|
-
for (const b of (e.message?.content || [])) {
|
|
598
|
-
if (b && b.type === 'tool_use') { toolIdMap[b.id] = b.name; totalCounts[b.name] = (totalCounts[b.name] || 0) + 1; }
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
if (e.type === 'user') {
|
|
602
|
-
for (const b of (e.message?.content || [])) {
|
|
603
|
-
if (b && b.type === 'tool_result' && b.is_error) {
|
|
604
|
-
const name = toolIdMap[b.tool_use_id] || '?';
|
|
605
|
-
errorCounts[name] = (errorCounts[name] || 0) + 1;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
} catch {}
|
|
611
|
-
}
|
|
612
|
-
const errors = Object.entries(errorCounts)
|
|
613
|
-
.map(([tool, count]) => ({ tool, count, total: totalCounts[tool] || count }))
|
|
614
|
-
.sort((a,b) => b.count - a.count);
|
|
615
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
616
|
-
res.end(JSON.stringify({ errors }));
|
|
617
|
-
} catch (err) {
|
|
618
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
619
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
620
|
-
}
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// ------------------------------------------------------- GET /api/tool-ranking
|
|
625
|
-
if (req.method === 'GET' && url === '/api/tool-ranking') {
|
|
626
|
-
try {
|
|
627
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
628
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
629
|
-
const d = path.resolve(dir || process.cwd());
|
|
630
|
-
const slug = d.replace(/\//g, '-');
|
|
631
|
-
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
632
|
-
let sessionFiles = [];
|
|
633
|
-
try {
|
|
634
|
-
sessionFiles = fs.readdirSync(projectClaudeDir)
|
|
635
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
636
|
-
.map(f => { try { return { f, mtime: fs.statSync(path.join(projectClaudeDir, f)).mtimeMs }; } catch { return null; } })
|
|
637
|
-
.filter(Boolean).sort((a,b) => b.mtime - a.mtime).slice(0, 30);
|
|
638
|
-
} catch {}
|
|
639
|
-
const toolCounts = {}, errorCounts = {};
|
|
640
|
-
for (const { f } of sessionFiles) {
|
|
641
|
-
const fp = path.join(projectClaudeDir, f);
|
|
642
|
-
try {
|
|
643
|
-
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
|
|
644
|
-
const toolIdMap = {};
|
|
645
|
-
for (const line of lines) {
|
|
646
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
647
|
-
if (e.type === 'assistant') {
|
|
648
|
-
for (const b of (e.message?.content || [])) {
|
|
649
|
-
if (b && b.type === 'tool_use') { toolIdMap[b.id] = b.name; toolCounts[b.name] = (toolCounts[b.name] || 0) + 1; }
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
if (e.type === 'user') {
|
|
653
|
-
for (const b of (e.message?.content || [])) {
|
|
654
|
-
if (b && b.type === 'tool_result' && b.is_error) {
|
|
655
|
-
const name = toolIdMap[b.tool_use_id] || '?';
|
|
656
|
-
errorCounts[name] = (errorCounts[name] || 0) + 1;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
} catch {}
|
|
662
|
-
}
|
|
663
|
-
const tools = Object.entries(toolCounts)
|
|
664
|
-
.map(([tool, count]) => ({ tool, count, errors: errorCounts[tool] || 0 }))
|
|
665
|
-
.sort((a,b) => b.count - a.count);
|
|
666
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
667
|
-
res.end(JSON.stringify({ tools }));
|
|
668
|
-
} catch (err) {
|
|
669
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
670
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
671
|
-
}
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// ------------------------------------------------------- GET /api/project-costs
|
|
676
|
-
if (req.method === 'GET' && url === '/api/project-costs') {
|
|
677
|
-
try {
|
|
678
|
-
const projectsBase = path.join(os.homedir(), '.claude', 'projects');
|
|
679
|
-
let slugDirs = [];
|
|
680
|
-
try { slugDirs = fs.readdirSync(projectsBase, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); } catch {}
|
|
681
|
-
const projectCosts = [];
|
|
682
|
-
for (const slug of slugDirs) {
|
|
683
|
-
const projDir = path.join(projectsBase, slug);
|
|
684
|
-
const projPath = resolveSlugToPath(slug, projDir);
|
|
685
|
-
let sessionFiles = [];
|
|
686
|
-
try { sessionFiles = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl')).map(f => path.join(projDir, f)); } catch {}
|
|
687
|
-
if (!sessionFiles.length) continue;
|
|
688
|
-
let totalCost = 0;
|
|
689
|
-
for (const fp of sessionFiles) {
|
|
690
|
-
try {
|
|
691
|
-
const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
|
|
692
|
-
for (const line of lines) {
|
|
693
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
694
|
-
if (e.type === 'assistant' && e.message?.usage) { totalCost += _sjCalcCost(e.message.model || '', e.message.usage); }
|
|
695
|
-
}
|
|
696
|
-
} catch {}
|
|
697
|
-
}
|
|
698
|
-
if (totalCost > 0) projectCosts.push({ path: projPath, cost: totalCost, sessions: sessionFiles.length });
|
|
699
|
-
}
|
|
700
|
-
projectCosts.sort((a, b) => b.cost - a.cost);
|
|
701
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
702
|
-
res.end(JSON.stringify({ projects: projectCosts }));
|
|
703
|
-
} catch (err) {
|
|
704
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
705
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
706
|
-
}
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// ------------------------------------------------------- GET /api/projects
|
|
711
|
-
if (req.method === 'GET' && url === '/api/projects') {
|
|
712
|
-
try {
|
|
713
|
-
const projectsBase = path.join(os.homedir(), '.claude', 'projects');
|
|
714
|
-
let slugDirs = [];
|
|
715
|
-
try { slugDirs = fs.readdirSync(projectsBase, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); } catch {}
|
|
716
|
-
const projects = slugDirs.map(slug => {
|
|
717
|
-
const projDir = path.join(projectsBase, slug);
|
|
718
|
-
const projPath = resolveSlugToPath(slug, projDir);
|
|
719
|
-
const name = projPath.split('/').filter(Boolean).pop() || slug.split('-').filter(Boolean).pop() || slug;
|
|
720
|
-
let sessionCount = 0; let lastActivity = 0; let memoryCount = 0;
|
|
721
|
-
try {
|
|
722
|
-
const files = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
|
|
723
|
-
sessionCount = files.length;
|
|
724
|
-
for (const f of files) {
|
|
725
|
-
try { const st = fs.statSync(path.join(projDir, f)); if (st.mtimeMs > lastActivity) lastActivity = st.mtimeMs; } catch {}
|
|
726
|
-
}
|
|
727
|
-
} catch {}
|
|
728
|
-
try {
|
|
729
|
-
const memDir = path.join(projDir, 'memory');
|
|
730
|
-
memoryCount = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md').length;
|
|
731
|
-
} catch {}
|
|
732
|
-
return { slug, path: projPath, name, sessionCount, memoryCount, lastActivity: lastActivity || null };
|
|
733
|
-
}).filter(p => p.sessionCount > 0).sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0));
|
|
734
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
735
|
-
res.end(JSON.stringify({ projects }));
|
|
736
|
-
} catch (err) {
|
|
737
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
738
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
739
|
-
}
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// ------------------------------------------------------- GET /api/palace
|
|
744
|
-
if (req.method === 'GET' && url === '/api/palace') {
|
|
745
|
-
try {
|
|
746
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
747
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
748
|
-
const d = path.resolve(dir || process.cwd());
|
|
749
|
-
const palaceDir = path.join(d, '.monomind', 'palace');
|
|
750
|
-
|
|
751
|
-
let drawers = [];
|
|
752
|
-
try {
|
|
753
|
-
const raw = fs.readFileSync(path.join(palaceDir, 'drawers.jsonl'), 'utf8');
|
|
754
|
-
drawers = raw.split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
755
|
-
} catch {}
|
|
756
|
-
|
|
757
|
-
let identity = null;
|
|
758
|
-
try { identity = fs.readFileSync(path.join(palaceDir, 'identity.md'), 'utf8'); } catch {}
|
|
759
|
-
|
|
760
|
-
let kg = [];
|
|
761
|
-
try { const raw = fs.readFileSync(path.join(palaceDir, 'kg.json'), 'utf8'); kg = JSON.parse(raw); } catch {}
|
|
762
|
-
|
|
763
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
764
|
-
res.end(JSON.stringify({ drawers, identity, kg }));
|
|
765
|
-
} catch (err) {
|
|
766
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
767
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
768
|
-
}
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// ------------------------------------------------------- GET /api/adrs
|
|
773
|
-
if (req.method === 'GET' && url.startsWith('/api/adrs')) {
|
|
774
|
-
try {
|
|
775
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
776
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
777
|
-
const d = path.resolve(dir || process.cwd());
|
|
778
|
-
|
|
779
|
-
const adrDirs = [
|
|
780
|
-
{ path: path.join(d, 'docs', 'adrs'), group: 'all' },
|
|
781
|
-
];
|
|
782
|
-
|
|
783
|
-
const adrs = [];
|
|
784
|
-
for (const { path: adrDir, group } of adrDirs) {
|
|
785
|
-
if (!fs.existsSync(adrDir)) continue;
|
|
786
|
-
const files = fs.readdirSync(adrDir).filter(f => f.endsWith('.md') && f !== 'README.md' && f !== 'v3-adrs.md' && f !== 'SECURITY-REVIEW-SUMMARY.md');
|
|
787
|
-
for (const fname of files.sort()) {
|
|
788
|
-
const resolvedGroup = /^ADR-G/i.test(fname) ? 'guidance' : 'implementation';
|
|
789
|
-
try {
|
|
790
|
-
const raw = fs.readFileSync(path.join(adrDir, fname), 'utf8');
|
|
791
|
-
const titleMatch = raw.match(/^#\s+(.+)$/m);
|
|
792
|
-
const header = raw.split('\n').slice(0, 20).join('\n');
|
|
793
|
-
const statusTableMatch = header.match(/^\|\s*\*{0,2}Status\*{0,2}\s*\|\s*\*{0,2}([^|*\n]{2,40}?)\*{0,2}\s*\|/im);
|
|
794
|
-
const statusInlineMatch = header.match(/\*\*Status[:\s]+\*?\*?\s*(Accepted|Implemented|Proposed|Superseded|Deprecated|Draft|Rejected|Complete|Active|Retired)[^*]*/i);
|
|
795
|
-
const statusMatch = statusTableMatch || statusInlineMatch;
|
|
796
|
-
const dateInlineMatch = header.match(/\*\*Date[:\s]+\*?\*?\s*([0-9]{4}-[0-9]{2}-[0-9]{2})/i);
|
|
797
|
-
const dateMatch = raw.match(/\|\s*\*{0,2}Date\*{0,2}\s*\|\s*\*{0,2}([^|*\n]+?)\*{0,2}\s*\|/i) || dateInlineMatch || raw.match(/Date[:\s]+([0-9]{4}-[0-9]{2}-[0-9]{2})/);
|
|
798
|
-
const numMatch = fname.match(/ADR-([A-Z]*[0-9]+)/i);
|
|
799
|
-
const summaryMatch = raw.match(/##\s+(?:Context|Summary|Problem Statement)[^\n]*\n+([\s\S]{20,300})/i);
|
|
800
|
-
adrs.push({
|
|
801
|
-
number: numMatch ? 'ADR-' + numMatch[1] : fname.replace('.md', ''),
|
|
802
|
-
title: titleMatch ? titleMatch[1].replace(/^ADR-[A-Z0-9-]+[:\s]+/i, '').trim() : fname.replace('.md', ''),
|
|
803
|
-
status: statusMatch ? statusMatch[1].trim() : 'Unknown',
|
|
804
|
-
date: dateMatch ? dateMatch[1].trim() : null,
|
|
805
|
-
summary: summaryMatch ? summaryMatch[1].replace(/\n/g, ' ').replace(/\s+/g, ' ').trim() : null,
|
|
806
|
-
group: resolvedGroup,
|
|
807
|
-
file: fname,
|
|
808
|
-
});
|
|
809
|
-
} catch { /* skip unreadable */ }
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
814
|
-
res.end(JSON.stringify({ adrs }));
|
|
815
|
-
} catch (err) {
|
|
816
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
817
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
818
|
-
}
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// ------------------------------------------------------- GET /api/memory-files
|
|
823
|
-
if (req.method === 'GET' && url === '/api/memory-files') {
|
|
824
|
-
try {
|
|
825
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
826
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
827
|
-
const d = path.resolve(dir || process.cwd());
|
|
828
|
-
const homeDir = os.homedir();
|
|
829
|
-
const slug = d.replace(/\//g, '-');
|
|
830
|
-
const memDir = path.join(homeDir, '.claude', 'projects', slug, 'memory');
|
|
831
|
-
|
|
832
|
-
let files = [];
|
|
833
|
-
try { files = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md'); } catch {}
|
|
834
|
-
|
|
835
|
-
const memories = files.map(fname => {
|
|
836
|
-
const fp = path.join(memDir, fname);
|
|
837
|
-
let stat = null; try { stat = fs.statSync(fp); } catch {}
|
|
838
|
-
let raw = ''; try { raw = fs.readFileSync(fp, 'utf8').replace(/\r\n/g, '\n'); } catch {}
|
|
839
|
-
// Parse frontmatter — escHtml ordering: bold replace runs on already-escaped content (safe)
|
|
840
|
-
let name = fname.replace('.md', ''), description = '', type = 'project', body = raw;
|
|
841
|
-
const fm = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
842
|
-
if (fm) {
|
|
843
|
-
body = fm[2].trim();
|
|
844
|
-
for (const line of fm[1].split('\n')) {
|
|
845
|
-
const m = line.match(/^(\w+):\s*(.+)$/);
|
|
846
|
-
if (m) {
|
|
847
|
-
if (m[1] === 'name') name = m[2].trim();
|
|
848
|
-
if (m[1] === 'description') description = m[2].trim();
|
|
849
|
-
if (m[1] === 'type') type = m[2].trim();
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return { filename: fname, name, description, type, body, source: 'file', readonly: false, mtime: stat ? stat.mtimeMs : null };
|
|
854
|
-
}).sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
|
|
855
|
-
|
|
856
|
-
// Merge backend store (AgentDB / auto-memory bridge). These live in the
|
|
857
|
-
// SQLite-backed store, not as .md files, so the file-only listing above
|
|
858
|
-
// misses them. Surface them read-only with a source badge so the dashboard
|
|
859
|
-
// reflects ALL memory, not just whatever has been flushed to disk.
|
|
860
|
-
let backend = [];
|
|
861
|
-
try {
|
|
862
|
-
const storePath = path.join(d, '.monomind', 'data', 'auto-memory-store.json');
|
|
863
|
-
if (fs.existsSync(storePath)) {
|
|
864
|
-
const raw = JSON.parse(fs.readFileSync(storePath, 'utf8'));
|
|
865
|
-
const rows = Array.isArray(raw) ? raw : (raw.entries || []);
|
|
866
|
-
backend = rows
|
|
867
|
-
.filter(e => e && (e.content != null) && e.status !== 'deleted')
|
|
868
|
-
.map(e => ({
|
|
869
|
-
filename: 'backend:' + (e.key || e.id),
|
|
870
|
-
name: e.key || e.id || 'entry',
|
|
871
|
-
description: e.namespace ? ('namespace: ' + e.namespace) : '',
|
|
872
|
-
type: e.type || 'semantic',
|
|
873
|
-
body: String(e.content),
|
|
874
|
-
source: 'backend',
|
|
875
|
-
readonly: true,
|
|
876
|
-
mtime: e.updatedAt || e.createdAt || null,
|
|
877
|
-
}))
|
|
878
|
-
.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
|
|
879
|
-
}
|
|
880
|
-
} catch {}
|
|
881
|
-
|
|
882
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
883
|
-
res.end(JSON.stringify({ memories: memories.concat(backend), memDir }));
|
|
884
|
-
} catch (err) {
|
|
885
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
886
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
887
|
-
}
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
// ------------------------------------------------------- PUT /api/memory-file
|
|
892
|
-
if (req.method === 'PUT' && url === '/api/memory-file') {
|
|
893
|
-
let body = '';
|
|
894
|
-
req.on('data', chunk => { body += chunk; });
|
|
895
|
-
req.on('end', () => {
|
|
896
|
-
try {
|
|
897
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
898
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
899
|
-
const slug = d.replace(/\//g, '-');
|
|
900
|
-
const memDir = path.join(os.homedir(), '.claude', 'projects', slug, 'memory');
|
|
901
|
-
const { filename, content } = JSON.parse(body);
|
|
902
|
-
if (!filename || filename.includes('..') || !filename.endsWith('.md') || filename.includes('/')) {
|
|
903
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
904
|
-
res.end(JSON.stringify({ error: 'Invalid filename' }));
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
const fp = path.join(memDir, filename);
|
|
908
|
-
if (!fp.startsWith(memDir + path.sep) && fp !== memDir) {
|
|
909
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
910
|
-
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
fs.mkdirSync(memDir, { recursive: true });
|
|
914
|
-
fs.writeFileSync(fp, content || '', 'utf8');
|
|
915
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
916
|
-
res.end(JSON.stringify({ ok: true }));
|
|
917
|
-
} catch (err) {
|
|
918
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
919
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
920
|
-
}
|
|
921
|
-
});
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// ------------------------------------------------------- DELETE /api/memory-file
|
|
926
|
-
if (req.method === 'DELETE' && url === '/api/memory-file') {
|
|
927
|
-
let body = '';
|
|
928
|
-
req.on('data', chunk => { body += chunk; });
|
|
929
|
-
req.on('end', () => {
|
|
930
|
-
try {
|
|
931
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
932
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
933
|
-
const slug = d.replace(/\//g, '-');
|
|
934
|
-
const memDir = path.join(os.homedir(), '.claude', 'projects', slug, 'memory');
|
|
935
|
-
const { filename } = JSON.parse(body);
|
|
936
|
-
if (!filename || filename.includes('..') || !filename.endsWith('.md') || filename.includes('/')) {
|
|
937
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
938
|
-
res.end(JSON.stringify({ error: 'Invalid filename' }));
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
const fp = path.join(memDir, filename);
|
|
942
|
-
if (!fp.startsWith(memDir + path.sep) && fp !== memDir) {
|
|
943
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
944
|
-
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
fs.unlinkSync(fp);
|
|
948
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
949
|
-
res.end(JSON.stringify({ ok: true }));
|
|
950
|
-
} catch (err) {
|
|
951
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
952
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
953
|
-
}
|
|
954
|
-
});
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// ------------------------------------------------- GET /api/routing-feedback
|
|
959
|
-
if (req.method === 'GET' && url === '/api/routing-feedback') {
|
|
960
|
-
try {
|
|
961
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
962
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
963
|
-
const feedbackPath = path.join(d, '.monomind', 'routing-feedback.jsonl');
|
|
964
|
-
let rows = [];
|
|
965
|
-
if (fs.existsSync(feedbackPath)) {
|
|
966
|
-
const raw = fs.readFileSync(feedbackPath, 'utf-8');
|
|
967
|
-
rows = raw.split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
968
|
-
}
|
|
969
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
970
|
-
res.end(JSON.stringify(rows));
|
|
971
|
-
} catch (err) {
|
|
972
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
973
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
974
|
-
}
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// ---------------------------------------------------- GET /api/memory/stats
|
|
979
|
-
if (req.method === 'GET' && url === '/api/memory/stats') {
|
|
980
|
-
try {
|
|
981
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
982
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
983
|
-
const slug = d.replace(/\//g, '-');
|
|
984
|
-
const memDir = path.join(os.homedir(), '.claude', 'projects', slug, 'memory');
|
|
985
|
-
|
|
986
|
-
let total = 0, namespaces = 0, size = 0, lastWrite = null;
|
|
987
|
-
const byType = {};
|
|
988
|
-
if (fs.existsSync(memDir)) {
|
|
989
|
-
const files = fs.readdirSync(memDir).filter(f => f.endsWith('.md'));
|
|
990
|
-
total = files.length;
|
|
991
|
-
namespaces = files.length; // each .md file is a memory namespace
|
|
992
|
-
files.forEach(f => {
|
|
993
|
-
const fp = path.join(memDir, f);
|
|
994
|
-
try {
|
|
995
|
-
const st = fs.statSync(fp);
|
|
996
|
-
size += st.size;
|
|
997
|
-
if (!lastWrite || st.mtimeMs > lastWrite) lastWrite = st.mtimeMs;
|
|
998
|
-
} catch {}
|
|
999
|
-
const type = f.replace('.md', '');
|
|
1000
|
-
byType[type] = (byType[type] || 0) + 1;
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
// Check for AgentDB / HNSW / RVF backends
|
|
1005
|
-
const dbPath = path.join(d, '.monomind', 'agentdb.db');
|
|
1006
|
-
const hnswPath = path.join(d, '.monomind', 'hnsw.index');
|
|
1007
|
-
const rvfPath = path.join(d, '.monomind', 'memory.rvf');
|
|
1008
|
-
|
|
1009
|
-
const stats = {
|
|
1010
|
-
total,
|
|
1011
|
-
count: total,
|
|
1012
|
-
namespaces,
|
|
1013
|
-
ns: Object.keys(byType).length,
|
|
1014
|
-
size,
|
|
1015
|
-
byType,
|
|
1016
|
-
hnsw: fs.existsSync(hnswPath),
|
|
1017
|
-
agentdb: fs.existsSync(dbPath),
|
|
1018
|
-
rvf: fs.existsSync(rvfPath),
|
|
1019
|
-
lastWrite,
|
|
1020
|
-
memDir,
|
|
1021
|
-
};
|
|
1022
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1023
|
-
res.end(JSON.stringify({ stats }));
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1026
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1027
|
-
}
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
// ---------------------------------------------------------- GET /api/loops
|
|
1032
|
-
if (req.method === 'GET' && url === '/api/loops') {
|
|
1033
|
-
try {
|
|
1034
|
-
const cwd = projectDir || process.cwd();
|
|
1035
|
-
const loopsDir = path.join(cwd, '.monomind', 'loops');
|
|
1036
|
-
let loops = [];
|
|
1037
|
-
let stopFiles = new Set();
|
|
1038
|
-
try {
|
|
1039
|
-
const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('.json'));
|
|
1040
|
-
stopFiles = new Set(fs.readdirSync(loopsDir).filter(f => f.endsWith('.stop')).map(f => f.replace('.stop', '')));
|
|
1041
|
-
for (const file of files) {
|
|
1042
|
-
try {
|
|
1043
|
-
const data = JSON.parse(fs.readFileSync(path.join(loopsDir, file), 'utf-8'));
|
|
1044
|
-
data.stopRequested = stopFiles.has(data.id);
|
|
1045
|
-
loops.push(data);
|
|
1046
|
-
} catch {}
|
|
1047
|
-
}
|
|
1048
|
-
} catch (e) { if (e.code !== 'ENOENT') throw e; }
|
|
1049
|
-
|
|
1050
|
-
// Also read .claude/scheduled_tasks.lock — active Claude Code /loop sessions
|
|
1051
|
-
// that haven't had their ScheduleWakeup hook fire yet (or running on older version)
|
|
1052
|
-
try {
|
|
1053
|
-
const lockPath = path.join(cwd, '.claude', 'scheduled_tasks.lock');
|
|
1054
|
-
if (fs.existsSync(lockPath)) {
|
|
1055
|
-
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
1056
|
-
const sessionId = lock.sessionId;
|
|
1057
|
-
const pid = lock.pid;
|
|
1058
|
-
// Verify PID is alive
|
|
1059
|
-
let alive = false;
|
|
1060
|
-
try { process.kill(pid, 0); alive = true; } catch {}
|
|
1061
|
-
const alreadyTracked = loops.some(l => l.id === sessionId || l.sessionId === sessionId);
|
|
1062
|
-
if (alive && sessionId && !alreadyTracked && !stopFiles.has(sessionId)) {
|
|
1063
|
-
// Try to extract ScheduleWakeup context from session JSONL
|
|
1064
|
-
let loopEntry = null;
|
|
1065
|
-
try {
|
|
1066
|
-
const escaped = cwd.replace(/\//g, '-');
|
|
1067
|
-
const sessionFile = path.join(os.homedir(), '.claude', 'projects', escaped, `${sessionId}.jsonl`);
|
|
1068
|
-
if (fs.existsSync(sessionFile)) {
|
|
1069
|
-
const stat = fs.statSync(sessionFile);
|
|
1070
|
-
const readStart = Math.max(0, stat.size - 100000);
|
|
1071
|
-
const buf = Buffer.alloc(stat.size - readStart);
|
|
1072
|
-
const fd = fs.openSync(sessionFile, 'r');
|
|
1073
|
-
fs.readSync(fd, buf, 0, buf.length, readStart);
|
|
1074
|
-
fs.closeSync(fd);
|
|
1075
|
-
const lines = buf.toString('utf-8').split('\n').filter(Boolean);
|
|
1076
|
-
let lastWakeup = null;
|
|
1077
|
-
for (const line of lines) {
|
|
1078
|
-
try {
|
|
1079
|
-
const entry = JSON.parse(line);
|
|
1080
|
-
const content = entry?.message?.content;
|
|
1081
|
-
if (Array.isArray(content)) {
|
|
1082
|
-
for (const block of content) {
|
|
1083
|
-
if (block?.type === 'tool_use' && block?.name === 'ScheduleWakeup') {
|
|
1084
|
-
lastWakeup = block.input;
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
} catch {}
|
|
1089
|
-
}
|
|
1090
|
-
if (lastWakeup) {
|
|
1091
|
-
const prompt = lastWakeup.prompt || '';
|
|
1092
|
-
const reason = lastWakeup.reason || '';
|
|
1093
|
-
const delaySeconds = lastWakeup.delaySeconds || 60;
|
|
1094
|
-
// Parse rep info from reason e.g. "repeat run 2/10"
|
|
1095
|
-
const repM = (reason || prompt).match(/(\d+)\s*\/\s*(\d+)/);
|
|
1096
|
-
const currentRep = repM ? parseInt(repM[1]) : 1;
|
|
1097
|
-
const maxReps = repM ? parseInt(repM[2]) : 0;
|
|
1098
|
-
const repFlag = (prompt).match(/--rep\s+(\d+)/);
|
|
1099
|
-
const timesFlag = (prompt).match(/--times\s+(\d+)/);
|
|
1100
|
-
const finalRep = repFlag ? parseInt(repFlag[1]) : currentRep;
|
|
1101
|
-
const finalMax = timesFlag ? parseInt(timesFlag[1]) : maxReps;
|
|
1102
|
-
const type = (finalMax > 0 || /repeat|loop/i.test(prompt)) ? 'repeat' : 'do';
|
|
1103
|
-
loopEntry = {
|
|
1104
|
-
id: sessionId,
|
|
1105
|
-
sessionId,
|
|
1106
|
-
type,
|
|
1107
|
-
status: 'waiting',
|
|
1108
|
-
prompt: prompt.slice(0, 300),
|
|
1109
|
-
reason,
|
|
1110
|
-
startedAt: lock.acquiredAt || Date.now(),
|
|
1111
|
-
lastRunAt: Date.now(),
|
|
1112
|
-
nextRunAt: Date.now() + delaySeconds * 1000,
|
|
1113
|
-
currentRep: finalRep,
|
|
1114
|
-
maxReps: finalMax,
|
|
1115
|
-
interval: Math.round(delaySeconds / 60),
|
|
1116
|
-
source: 'scheduled_tasks_lock',
|
|
1117
|
-
};
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
} catch {}
|
|
1121
|
-
// Fallback: minimal entry from lock file alone
|
|
1122
|
-
if (!loopEntry) {
|
|
1123
|
-
loopEntry = {
|
|
1124
|
-
id: sessionId,
|
|
1125
|
-
sessionId,
|
|
1126
|
-
type: 'do',
|
|
1127
|
-
status: 'running',
|
|
1128
|
-
prompt: '(active session)',
|
|
1129
|
-
reason: '',
|
|
1130
|
-
startedAt: lock.acquiredAt || Date.now(),
|
|
1131
|
-
lastRunAt: lock.acquiredAt || Date.now(),
|
|
1132
|
-
nextRunAt: null,
|
|
1133
|
-
source: 'scheduled_tasks_lock',
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
loops.push(loopEntry);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
} catch {}
|
|
1140
|
-
|
|
1141
|
-
loops.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
1142
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
1143
|
-
res.end(JSON.stringify({ loops }));
|
|
1144
|
-
} catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// ---------------------------------------------------------- POST /api/loops/stop
|
|
1149
|
-
if (req.method === 'POST' && url === '/api/loops/stop') {
|
|
1150
|
-
let body = '';
|
|
1151
|
-
req.on('data', chunk => { body += chunk; });
|
|
1152
|
-
req.on('end', () => {
|
|
1153
|
-
try {
|
|
1154
|
-
const { id } = JSON.parse(body);
|
|
1155
|
-
if (!id) { res.writeHead(400); res.end(JSON.stringify({ error: 'id required' })); return; }
|
|
1156
|
-
const loopsDir = path.join(projectDir || process.cwd(), '.monomind', 'loops');
|
|
1157
|
-
fs.mkdirSync(loopsDir, { recursive: true });
|
|
1158
|
-
fs.writeFileSync(path.join(loopsDir, `${id}.stop`), `stop-requested-${Date.now()}`);
|
|
1159
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1160
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1161
|
-
} catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
|
|
1162
|
-
});
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
// ---------------------------------------------------------- POST /api/loops/create
|
|
1167
|
-
if (req.method === 'POST' && url === '/api/loops/create') {
|
|
1168
|
-
let body = '';
|
|
1169
|
-
req.on('data', chunk => { body += chunk; });
|
|
1170
|
-
req.on('end', () => {
|
|
1171
|
-
try {
|
|
1172
|
-
const { name, prompt, interval, maxReps } = JSON.parse(body);
|
|
1173
|
-
if (!prompt) { res.writeHead(400); res.end(JSON.stringify({ error: 'prompt required' })); return; }
|
|
1174
|
-
const loopsDir = path.join(projectDir || process.cwd(), '.monomind', 'loops');
|
|
1175
|
-
fs.mkdirSync(loopsDir, { recursive: true });
|
|
1176
|
-
const id = `loop-${Date.now()}-${Math.random().toString(36).slice(2,7)}`;
|
|
1177
|
-
const loop = { id, name: name || prompt.slice(0, 40), prompt, interval: interval || '1h', maxReps: maxReps || null, status: 'active', currentRep: 0, startedAt: new Date().toISOString(), lastRunAt: null };
|
|
1178
|
-
fs.writeFileSync(path.join(loopsDir, `${id}.json`), JSON.stringify(loop, null, 2));
|
|
1179
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1180
|
-
res.end(JSON.stringify({ ok: true, id }));
|
|
1181
|
-
} catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
|
|
1182
|
-
});
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
// ---------------------------------------------------------- GET /api/session-errors
|
|
1187
|
-
if (req.method === 'GET' && url === '/api/session-errors') {
|
|
1188
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1189
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
1190
|
-
const sessionId = qs.get('id') || '';
|
|
1191
|
-
const slug = d.replace(/\//g, '-');
|
|
1192
|
-
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
1193
|
-
try {
|
|
1194
|
-
const files = fs.readdirSync(projectClaudeDir).filter(f => f.endsWith('.jsonl'));
|
|
1195
|
-
let fp = null;
|
|
1196
|
-
// Find the file matching sessionId
|
|
1197
|
-
for (const f of files) {
|
|
1198
|
-
if (f.includes(sessionId) || sessionId === f.replace('.jsonl', '')) { fp = path.join(projectClaudeDir, f); break; }
|
|
1199
|
-
}
|
|
1200
|
-
if (!fp) {
|
|
1201
|
-
// fallback: find by scanning
|
|
1202
|
-
for (const f of files) {
|
|
1203
|
-
const raw = fs.readFileSync(path.join(projectClaudeDir, f), 'utf8');
|
|
1204
|
-
const lines = raw.trim().split('\n').filter(Boolean);
|
|
1205
|
-
if (lines.length > 0) {
|
|
1206
|
-
try { const first = JSON.parse(lines[0]); if (first.sessionId === sessionId) { fp = path.join(projectClaudeDir, f); break; } } catch {}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
if (!fp) { res.writeHead(404); res.end(JSON.stringify({ errors: [] })); return; }
|
|
1211
|
-
const raw = fs.readFileSync(fp, 'utf8');
|
|
1212
|
-
const lines = raw.trim().split('\n').filter(Boolean);
|
|
1213
|
-
const errors = [];
|
|
1214
|
-
for (const line of lines) {
|
|
1215
|
-
try {
|
|
1216
|
-
const obj = JSON.parse(line);
|
|
1217
|
-
const content = obj.message?.content;
|
|
1218
|
-
if (!Array.isArray(content)) continue;
|
|
1219
|
-
for (const block of content) {
|
|
1220
|
-
if (block.type === 'tool_result' && block.is_error) {
|
|
1221
|
-
const errText = Array.isArray(block.content) ? block.content.map(c => c.text || '').join('') : String(block.content || '');
|
|
1222
|
-
if (errText) errors.push({ toolUseId: block.tool_use_id || '', text: errText.slice(0, 500) });
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
} catch {}
|
|
1226
|
-
}
|
|
1227
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1228
|
-
res.end(JSON.stringify({ errors: errors.slice(0, 50) }));
|
|
1229
|
-
} catch (err) { res.writeHead(500); res.end(JSON.stringify({ errors: [], error: err.message })); }
|
|
1230
|
-
return;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// ---------------------------------------------------------- GET /api/events-stream (SSE)
|
|
1234
|
-
if (req.method === 'GET' && url.startsWith('/api/events-stream')) {
|
|
1235
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1236
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
1237
|
-
const slug = d.replace(/\//g, '-');
|
|
1238
|
-
const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
|
|
1239
|
-
res.writeHead(200, {
|
|
1240
|
-
'Content-Type': 'text/event-stream',
|
|
1241
|
-
'Cache-Control': 'no-cache',
|
|
1242
|
-
'Connection': 'keep-alive',
|
|
1243
|
-
'Access-Control-Allow-Origin': '*',
|
|
1244
|
-
});
|
|
1245
|
-
const send = (ev, data) => { try { res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`); } catch {} };
|
|
1246
|
-
send('connected', { ts: Date.now() });
|
|
1247
|
-
let watcher = null;
|
|
1248
|
-
try {
|
|
1249
|
-
watcher = fs.watch(projectClaudeDir, { persistent: false }, (evtype) => {
|
|
1250
|
-
if (evtype === 'change' || evtype === 'rename') send('update', { ts: Date.now() });
|
|
1251
|
-
});
|
|
1252
|
-
} catch {}
|
|
1253
|
-
const pingInterval = setInterval(() => { try { res.write(': ping\n\n'); } catch {} }, 20000);
|
|
1254
|
-
req.on('close', () => { clearInterval(pingInterval); try { watcher?.close(); } catch {} });
|
|
1255
|
-
return;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// ------------------------------------------------------- DELETE /api/knowledge-chunk
|
|
1259
|
-
if (req.method === 'DELETE' && url === '/api/knowledge-chunk') {
|
|
1260
|
-
let body = '';
|
|
1261
|
-
req.on('data', chunk => { body += chunk; });
|
|
1262
|
-
req.on('end', () => {
|
|
1263
|
-
try {
|
|
1264
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1265
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
1266
|
-
const chunksFile = path.join(d, '.monomind', 'knowledge', 'chunks.jsonl');
|
|
1267
|
-
const { chunkId } = JSON.parse(body);
|
|
1268
|
-
if (!chunkId || typeof chunkId !== 'string') {
|
|
1269
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1270
|
-
res.end(JSON.stringify({ error: 'Invalid chunkId' }));
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
if (!fs.existsSync(chunksFile)) {
|
|
1274
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1275
|
-
res.end(JSON.stringify({ error: 'chunks.jsonl not found' }));
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
const entries = fs.readFileSync(chunksFile, 'utf8').split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
1279
|
-
const before = entries.length;
|
|
1280
|
-
const filtered = entries.filter(e => e.chunkId !== chunkId);
|
|
1281
|
-
if (filtered.length === before) {
|
|
1282
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1283
|
-
res.end(JSON.stringify({ error: 'Chunk not found' }));
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
fs.writeFileSync(chunksFile, filtered.map(e => JSON.stringify(e)).join('\n') + (filtered.length ? '\n' : ''), 'utf8');
|
|
1287
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1288
|
-
res.end(JSON.stringify({ ok: true, removed: before - filtered.length }));
|
|
1289
|
-
} catch (err) {
|
|
1290
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1291
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1292
|
-
}
|
|
1293
|
-
});
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// ------------------------------------------------------- PUT /api/knowledge-chunk
|
|
1298
|
-
if (req.method === 'PUT' && url === '/api/knowledge-chunk') {
|
|
1299
|
-
let body = '';
|
|
1300
|
-
req.on('data', chunk => { body += chunk; });
|
|
1301
|
-
req.on('end', () => {
|
|
1302
|
-
try {
|
|
1303
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1304
|
-
const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
1305
|
-
const chunksFile = path.join(d, '.monomind', 'knowledge', 'chunks.jsonl');
|
|
1306
|
-
const { chunkId, text } = JSON.parse(body);
|
|
1307
|
-
if (!chunkId || typeof chunkId !== 'string' || typeof text !== 'string') {
|
|
1308
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1309
|
-
res.end(JSON.stringify({ error: 'Invalid chunkId or text' }));
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
if (!fs.existsSync(chunksFile)) {
|
|
1313
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1314
|
-
res.end(JSON.stringify({ error: 'chunks.jsonl not found' }));
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
const entries = fs.readFileSync(chunksFile, 'utf8').split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
1318
|
-
const idx = entries.findIndex(e => e.chunkId === chunkId);
|
|
1319
|
-
if (idx === -1) {
|
|
1320
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
1321
|
-
res.end(JSON.stringify({ error: 'Chunk not found' }));
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
entries[idx] = { ...entries[idx], text };
|
|
1325
|
-
fs.writeFileSync(chunksFile, entries.map(e => JSON.stringify(e)).join('\n') + '\n', 'utf8');
|
|
1326
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1327
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1328
|
-
} catch (err) {
|
|
1329
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1330
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1331
|
-
}
|
|
1332
|
-
});
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// ------------------------------------------------------- GET /api/monograph-html
|
|
1337
|
-
if (req.method === 'GET' && url === '/api/monograph-html') {
|
|
1338
|
-
try {
|
|
1339
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1340
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1341
|
-
const d = path.resolve(dir || process.cwd());
|
|
1342
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1343
|
-
|
|
1344
|
-
// Generate HTML on-the-fly from SQLite DB using the improved toHtml export
|
|
1345
|
-
if (fs.existsSync(dbPath)) {
|
|
1346
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1347
|
-
const { toHtml } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/export/html.js');
|
|
1348
|
-
const db = openDb(dbPath);
|
|
1349
|
-
let html;
|
|
1350
|
-
try {
|
|
1351
|
-
const rawNodes = db.prepare('SELECT * FROM nodes LIMIT 5000').all();
|
|
1352
|
-
const rawEdges = db.prepare('SELECT * FROM edges').all();
|
|
1353
|
-
// Remap snake_case DB columns to camelCase MonographNode/MonographEdge interfaces
|
|
1354
|
-
const parsedNodes = rawNodes.map(n => ({
|
|
1355
|
-
id: n.id,
|
|
1356
|
-
label: n.label,
|
|
1357
|
-
name: n.name,
|
|
1358
|
-
normLabel: n.norm_label,
|
|
1359
|
-
filePath: n.file_path,
|
|
1360
|
-
startLine: n.start_line,
|
|
1361
|
-
endLine: n.end_line,
|
|
1362
|
-
communityId: n.community_id,
|
|
1363
|
-
isExported: !!n.is_exported,
|
|
1364
|
-
language: n.language,
|
|
1365
|
-
properties: n.properties ? JSON.parse(n.properties) : {},
|
|
1366
|
-
}));
|
|
1367
|
-
const parsedEdges = rawEdges.map(e => ({
|
|
1368
|
-
id: e.id,
|
|
1369
|
-
sourceId: e.source_id,
|
|
1370
|
-
targetId: e.target_id,
|
|
1371
|
-
relation: e.relation,
|
|
1372
|
-
confidence: e.confidence,
|
|
1373
|
-
confidenceScore: e.confidence_score,
|
|
1374
|
-
weight: e.weight,
|
|
1375
|
-
}));
|
|
1376
|
-
html = toHtml(parsedNodes, parsedEdges);
|
|
1377
|
-
} finally {
|
|
1378
|
-
closeDb(db);
|
|
1379
|
-
}
|
|
1380
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
1381
|
-
res.end(html);
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// Fallback: try legacy graph.html on disk
|
|
1386
|
-
const htmlPath = path.join(d, '.monomind', 'graph', 'graph.html');
|
|
1387
|
-
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
1388
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
1389
|
-
res.end(html);
|
|
1390
|
-
} catch (err) {
|
|
1391
|
-
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
1392
|
-
res.end('<html><body style="background:#0f0f1a;color:#888;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;"><h3 style="color:#4E79A7;">No Graph Built Yet</h3><p>Run <code style="color:#00E5C8;">mcp__monomind__monograph_build</code> or click BUILD in the sidebar.</p></div></body></html>');
|
|
1393
|
-
}
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// ------------------------------------------------------- GET /api/monograph-report
|
|
1398
|
-
if (req.method === 'GET' && url === '/api/monograph-report') {
|
|
1399
|
-
try {
|
|
1400
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1401
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1402
|
-
const d = path.resolve(dir || process.cwd());
|
|
1403
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1404
|
-
let report = null, exists = false, stats = null;
|
|
1405
|
-
if (fs.existsSync(dbPath)) {
|
|
1406
|
-
exists = true;
|
|
1407
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1408
|
-
const db = openDb(dbPath);
|
|
1409
|
-
try {
|
|
1410
|
-
const nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
|
|
1411
|
-
const edgeCount = db.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
|
|
1412
|
-
const topNodes = db.prepare(`SELECT n.id, n.name, n.label, (SELECT COUNT(*) FROM edges e WHERE e.source_id=n.id OR e.target_id=n.id) AS deg FROM nodes n ORDER BY deg DESC LIMIT 20`).all();
|
|
1413
|
-
const labelDist = db.prepare('SELECT label, COUNT(*) AS cnt FROM nodes GROUP BY label ORDER BY cnt DESC LIMIT 10').all();
|
|
1414
|
-
const dbStat = fs.statSync(dbPath);
|
|
1415
|
-
stats = { nodes: nodeCount, edges: edgeCount, size: dbStat.size, mtime: dbStat.mtimeMs };
|
|
1416
|
-
report = [
|
|
1417
|
-
'# Monograph Knowledge Graph',
|
|
1418
|
-
'',
|
|
1419
|
-
`## Overview`,
|
|
1420
|
-
`- **Nodes**: ${nodeCount.toLocaleString()}`,
|
|
1421
|
-
`- **Edges**: ${edgeCount.toLocaleString()}`,
|
|
1422
|
-
`- **Last built**: ${new Date(dbStat.mtimeMs).toLocaleString()}`,
|
|
1423
|
-
'',
|
|
1424
|
-
'## Top 20 Nodes by Degree',
|
|
1425
|
-
...topNodes.map((n, i) => `${String(i+1).padStart(3,' ')}. **${n.name || n.id}** \`${n.label}\` — ${n.deg} connections`),
|
|
1426
|
-
'',
|
|
1427
|
-
'## Node Type Distribution',
|
|
1428
|
-
...labelDist.map(r => `- **${r.label}**: ${r.cnt}`),
|
|
1429
|
-
].join('\n');
|
|
1430
|
-
} finally { closeDb(db); }
|
|
1431
|
-
}
|
|
1432
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
1433
|
-
res.end(JSON.stringify({ exists, report, stats }));
|
|
1434
|
-
} catch (err) {
|
|
1435
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1436
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1437
|
-
}
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// -------------------------------------------------- GET /api/monograph-graph
|
|
1442
|
-
if (req.method === 'GET' && url === '/api/monograph-graph') {
|
|
1443
|
-
try {
|
|
1444
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1445
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1446
|
-
const d = path.resolve(dir || process.cwd());
|
|
1447
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1448
|
-
let nodes = [], edges = [];
|
|
1449
|
-
if (fs.existsSync(dbPath)) {
|
|
1450
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1451
|
-
const db = openDb(dbPath);
|
|
1452
|
-
try {
|
|
1453
|
-
const nodeLimit = Math.min(parseInt(qs.get('limit') || '500', 10), 5000);
|
|
1454
|
-
// ?labels=Section,Concept → fetch only those label types (no degree cutoff)
|
|
1455
|
-
const labelFilter = qs.get('labels') ? new Set(qs.get('labels').split(',').map(s => s.trim())) : null;
|
|
1456
|
-
const rawNodes = labelFilter
|
|
1457
|
-
? db.prepare(`SELECT id, name, label, file_path, community_id FROM nodes WHERE label IN (${[...labelFilter].map(() => '?').join(',')}) LIMIT 5000`).all(...labelFilter)
|
|
1458
|
-
: db.prepare('SELECT id, name, label, file_path, community_id FROM nodes LIMIT 5000').all();
|
|
1459
|
-
const rawEdges = db.prepare('SELECT source_id, target_id, relation FROM edges').all();
|
|
1460
|
-
// Compute degree
|
|
1461
|
-
const degree = new Map();
|
|
1462
|
-
for (const n of rawNodes) degree.set(n.id, 0);
|
|
1463
|
-
for (const e of rawEdges) {
|
|
1464
|
-
if (degree.has(e.source_id)) degree.set(e.source_id, (degree.get(e.source_id) || 0) + 1);
|
|
1465
|
-
if (degree.has(e.target_id)) degree.set(e.target_id, (degree.get(e.target_id) || 0) + 1);
|
|
1466
|
-
}
|
|
1467
|
-
// When filtering by labels, return all matching nodes (skip degree sort+slice)
|
|
1468
|
-
const topNodes = labelFilter
|
|
1469
|
-
? rawNodes
|
|
1470
|
-
: [...rawNodes].sort((a, b) => (degree.get(b.id) || 0) - (degree.get(a.id) || 0)).slice(0, nodeLimit);
|
|
1471
|
-
const topIds = new Set(topNodes.map(n => n.id));
|
|
1472
|
-
nodes = topNodes.map(n => ({ id: n.id, label: n.name || n.id, type: n.label || 'unknown', degree: degree.get(n.id) || 0 }));
|
|
1473
|
-
edges = rawEdges.filter(e => topIds.has(e.source_id) && topIds.has(e.target_id)).slice(0, 2000).map(e => ({ source: e.source_id, target: e.target_id, relation: e.relation || 'REF' }));
|
|
1474
|
-
} finally { closeDb(db); }
|
|
1475
|
-
}
|
|
1476
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
1477
|
-
res.end(JSON.stringify({ nodes, edges }));
|
|
1478
|
-
} catch (err) {
|
|
1479
|
-
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
1480
|
-
}
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// -------------------------------------------------- POST /api/ua-enrich
|
|
1485
|
-
// Trigger semantic enrichment on an existing monograph DB.
|
|
1486
|
-
// Imports understand graph.json if present; falls back to structural-only pass.
|
|
1487
|
-
if (req.method === 'POST' && url === '/api/ua-enrich') {
|
|
1488
|
-
try {
|
|
1489
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1490
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1491
|
-
const d = path.resolve(dir || process.cwd());
|
|
1492
|
-
const dbFilePath = path.join(d, '.monomind', 'monograph.db');
|
|
1493
|
-
|
|
1494
|
-
// Check for UA graph.json first
|
|
1495
|
-
const uaGraphCandidates = [
|
|
1496
|
-
path.join(d, '.understand-anything', 'knowledge-graph.json'),
|
|
1497
|
-
path.join(d, '.understand-anything', 'graph.json'),
|
|
1498
|
-
path.join(d, '.ua', 'knowledge-graph.json'),
|
|
1499
|
-
path.join(d, '.ua', 'graph.json'),
|
|
1500
|
-
];
|
|
1501
|
-
const uaGraph = uaGraphCandidates.find(p => fs.existsSync(p));
|
|
1502
|
-
const importScript = path.join(process.cwd(), 'scripts', 'ua-import.mjs');
|
|
1503
|
-
const enrichScript = path.join(process.cwd(), 'scripts', 'ua-enrich.mjs');
|
|
1504
|
-
|
|
1505
|
-
res.writeHead(202, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1506
|
-
|
|
1507
|
-
if (uaGraph && fs.existsSync(importScript)) {
|
|
1508
|
-
res.end(JSON.stringify({ status: 'importing', source: uaGraph }));
|
|
1509
|
-
const { spawn: sp } = await import('child_process');
|
|
1510
|
-
const child = sp(process.execPath, [importScript, uaGraph, dbFilePath], { stdio: 'ignore', detached: true, cwd: d });
|
|
1511
|
-
child.unref();
|
|
1512
|
-
} else if (fs.existsSync(enrichScript)) {
|
|
1513
|
-
res.end(JSON.stringify({ status: 'enriching', mode: 'structural-only' }));
|
|
1514
|
-
const { spawn: sp } = await import('child_process');
|
|
1515
|
-
const child = sp(process.execPath, [enrichScript, '--dir', d, '--db', dbFilePath, '--full'], { stdio: 'ignore', detached: true, cwd: d });
|
|
1516
|
-
child.unref();
|
|
1517
|
-
} else {
|
|
1518
|
-
res.end(JSON.stringify({ status: 'skipped', reason: 'No understand graph.json found. Run /monomind:understand in Claude Code first.' }));
|
|
1519
|
-
}
|
|
1520
|
-
} catch (err) {
|
|
1521
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1522
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1523
|
-
}
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// -------------------------------------------------- POST /api/monograph-build
|
|
1528
|
-
if (req.method === 'POST' && url === '/api/monograph-build') {
|
|
1529
|
-
try {
|
|
1530
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1531
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1532
|
-
const d = path.resolve(dir || process.cwd());
|
|
1533
|
-
|
|
1534
|
-
res.writeHead(202, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1535
|
-
res.end(JSON.stringify({ status: 'building', dir: d }));
|
|
1536
|
-
|
|
1537
|
-
// Build via monograph in background
|
|
1538
|
-
const { spawn: sp } = await import('child_process');
|
|
1539
|
-
const script = `import { buildAsync } from '@monoes/monograph'; await buildAsync(${JSON.stringify(d)});`;
|
|
1540
|
-
const child = sp(process.execPath, ['--input-type=module', '--eval', script], { stdio: 'ignore', detached: true, cwd: d });
|
|
1541
|
-
child.unref();
|
|
1542
|
-
console.log(`[graph] build started for ${d} via monograph`);
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1545
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1546
|
-
}
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// -------------------------------------------------- GET /api/monograph-build-docs-status
|
|
1551
|
-
if (req.method === 'GET' && url === '/api/monograph-build-docs-status') {
|
|
1552
|
-
const qs2 = new URL(req.url, 'http://localhost').searchParams;
|
|
1553
|
-
const d2 = path.resolve(qs2.get('dir') || projectDir || process.cwd());
|
|
1554
|
-
const state = buildDocsState.get(d2) || { status: 'idle' };
|
|
1555
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1556
|
-
res.end(JSON.stringify(state));
|
|
1557
|
-
return;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// -------------------------------------------------- POST /api/monograph-build-docs
|
|
1561
|
-
if (req.method === 'POST' && url === '/api/monograph-build-docs') {
|
|
1562
|
-
try {
|
|
1563
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1564
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1565
|
-
const d = path.resolve(dir || process.cwd());
|
|
1566
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1567
|
-
if (!fs.existsSync(dbPath)) {
|
|
1568
|
-
res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1569
|
-
res.end(JSON.stringify({ error: 'monograph.db not found — run BUILD GRAPH first' }));
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// Reject if already running
|
|
1574
|
-
const existing = buildDocsState.get(d);
|
|
1575
|
-
if (existing && existing.status === 'pending') {
|
|
1576
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1577
|
-
res.end(JSON.stringify({ status: 'pending', message: 'Build already in progress' }));
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
const startedAt = Date.now();
|
|
1582
|
-
buildDocsState.set(d, { status: 'pending', sections: 0, files: 0, error: null, startedAt });
|
|
1583
|
-
res.writeHead(202, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1584
|
-
res.end(JSON.stringify({ status: 'pending', dir: d }));
|
|
1585
|
-
|
|
1586
|
-
// Run doc parsing in background
|
|
1587
|
-
(async () => {
|
|
1588
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1589
|
-
const { isFileCached, updateFileCache, hashFileContent } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/file-cache.js');
|
|
1590
|
-
const { readFileSync, readdirSync, statSync } = fs;
|
|
1591
|
-
|
|
1592
|
-
const docExts = new Set(['.md', '.mdx', '.txt', '.rst']);
|
|
1593
|
-
const ignoreDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'coverage', '.monomind', '__pycache__', 'vendor']);
|
|
1594
|
-
const docFiles = [];
|
|
1595
|
-
function walk(dir2, depth = 0) {
|
|
1596
|
-
if (depth > 12) return;
|
|
1597
|
-
let entries;
|
|
1598
|
-
try { entries = readdirSync(dir2); } catch { return; }
|
|
1599
|
-
for (const e of entries) {
|
|
1600
|
-
if (ignoreDirs.has(e) || e.startsWith('.')) continue;
|
|
1601
|
-
const full = path.join(dir2, e);
|
|
1602
|
-
let st;
|
|
1603
|
-
try { st = statSync(full); } catch { continue; }
|
|
1604
|
-
if (st.isDirectory()) { walk(full, depth + 1); }
|
|
1605
|
-
else if (docExts.has(path.extname(e).toLowerCase()) && st.size < 600000) docFiles.push(full);
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
walk(d);
|
|
1609
|
-
|
|
1610
|
-
const db = openDb(dbPath);
|
|
1611
|
-
try {
|
|
1612
|
-
const insertNode = db.prepare(`INSERT OR REPLACE INTO nodes (id, label, name, norm_label, file_path, start_line, end_line, language, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)`);
|
|
1613
|
-
const insertEdge = db.prepare(`INSERT OR IGNORE INTO edges (id, source_id, target_id, relation, confidence, confidence_score, weight) VALUES (?, ?, ?, ?, 'EXTRACTED', 1.0, 1.0)`);
|
|
1614
|
-
|
|
1615
|
-
const insertAll = db.transaction((nodes, edges) => {
|
|
1616
|
-
for (const n of nodes) {
|
|
1617
|
-
try { insertNode.run(n.id, n.label, n.name, n.norm_label, n.file_path, n.start_line, n.end_line, n.language); } catch {}
|
|
1618
|
-
}
|
|
1619
|
-
for (const e of edges) { try { insertEdge.run(e.id, e.src, e.dst, e.rel); } catch {} }
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
const normTitle = t => t.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
1623
|
-
|
|
1624
|
-
let totalSections = 0;
|
|
1625
|
-
let skipped = 0;
|
|
1626
|
-
for (const filePath of docFiles) {
|
|
1627
|
-
let content;
|
|
1628
|
-
try { content = readFileSync(filePath, 'utf-8'); } catch { continue; }
|
|
1629
|
-
|
|
1630
|
-
// Skip unchanged files using file cache
|
|
1631
|
-
let isCached = false;
|
|
1632
|
-
let contentHash = '';
|
|
1633
|
-
try {
|
|
1634
|
-
contentHash = hashFileContent(content);
|
|
1635
|
-
isCached = isFileCached(db, filePath, contentHash);
|
|
1636
|
-
} catch {}
|
|
1637
|
-
if (isCached) { skipped++; continue; }
|
|
1638
|
-
const relPath = path.relative(d, filePath);
|
|
1639
|
-
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
1640
|
-
const fileId = 'doc:' + relPath;
|
|
1641
|
-
const lineCount = content.split('\n').length;
|
|
1642
|
-
|
|
1643
|
-
const nodes = [{ id: fileId, label: 'File', name: relPath, norm_label: normTitle(relPath), file_path: relPath, start_line: 1, end_line: lineCount, language: ext }];
|
|
1644
|
-
const edges = [];
|
|
1645
|
-
const lines = content.split('\n');
|
|
1646
|
-
const sectionStack = [{ id: fileId, depth: 0 }];
|
|
1647
|
-
let inCodeBlock = false;
|
|
1648
|
-
let codeBlockLang = null;
|
|
1649
|
-
|
|
1650
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1651
|
-
const line = lines[i];
|
|
1652
|
-
|
|
1653
|
-
// Track fenced code blocks — don't parse headings inside them
|
|
1654
|
-
const fenceMatch = line.match(/^```([a-zA-Z0-9_+-]*)$/);
|
|
1655
|
-
if (fenceMatch) {
|
|
1656
|
-
if (!inCodeBlock) {
|
|
1657
|
-
inCodeBlock = true;
|
|
1658
|
-
codeBlockLang = fenceMatch[1].trim() || null;
|
|
1659
|
-
if (codeBlockLang) {
|
|
1660
|
-
const cId = 'concept:lang:' + codeBlockLang.toLowerCase();
|
|
1661
|
-
if (!nodes.find(n => n.id === cId)) {
|
|
1662
|
-
nodes.push({ id: cId, label: 'Concept', name: codeBlockLang, norm_label: normTitle(codeBlockLang), file_path: null, start_line: 0, end_line: 0, language: null });
|
|
1663
|
-
}
|
|
1664
|
-
const curSec = sectionStack[sectionStack.length - 1].id;
|
|
1665
|
-
edges.push({ id: 'e:' + curSec + ':' + cId + ':code', src: curSec, dst: cId, rel: 'TAGGED_AS' });
|
|
1666
|
-
}
|
|
1667
|
-
} else { inCodeBlock = false; codeBlockLang = null; }
|
|
1668
|
-
continue;
|
|
1669
|
-
}
|
|
1670
|
-
if (inCodeBlock) continue;
|
|
1671
|
-
|
|
1672
|
-
// ATX headings: # Title
|
|
1673
|
-
const hMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
1674
|
-
if (hMatch) {
|
|
1675
|
-
const depth = hMatch[1].length;
|
|
1676
|
-
const title = hMatch[2].trim().replace(/\s+#+\s*$/, '').trim();
|
|
1677
|
-
const secId = 'sec:' + relPath + ':' + (i + 1);
|
|
1678
|
-
nodes.push({ id: secId, label: 'Section', name: title, norm_label: normTitle(title), file_path: relPath, start_line: i + 1, end_line: i + 1, language: ext });
|
|
1679
|
-
totalSections++;
|
|
1680
|
-
while (sectionStack.length > 1 && sectionStack[sectionStack.length - 1].depth >= depth) sectionStack.pop();
|
|
1681
|
-
const parentId = sectionStack[sectionStack.length - 1].id;
|
|
1682
|
-
edges.push({ id: 'e:' + secId + ':' + parentId + ':parent', src: parentId, dst: secId, rel: 'DEFINES' });
|
|
1683
|
-
sectionStack.push({ id: secId, depth });
|
|
1684
|
-
continue;
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// RST-style headings: line followed by ===, ---, ~~~, ^^^, etc.
|
|
1688
|
-
if (i + 1 < lines.length && lines[i + 1].match(/^[=\-~^"'`#*+!]{3,}\s*$/) && line.trim().length > 0 && line.trim().length <= lines[i + 1].trim().length + 2) {
|
|
1689
|
-
const underlineChar = lines[i + 1].trim()[0];
|
|
1690
|
-
const rstDepth = '=-~^"\'`#*+!'.indexOf(underlineChar) + 1 || 3;
|
|
1691
|
-
const title = line.trim();
|
|
1692
|
-
const secId = 'sec:' + relPath + ':' + (i + 1);
|
|
1693
|
-
nodes.push({ id: secId, label: 'Section', name: title, norm_label: normTitle(title), file_path: relPath, start_line: i + 1, end_line: i + 1, language: ext });
|
|
1694
|
-
totalSections++;
|
|
1695
|
-
const depth = Math.min(6, Math.ceil(rstDepth / 2));
|
|
1696
|
-
while (sectionStack.length > 1 && sectionStack[sectionStack.length - 1].depth >= depth) sectionStack.pop();
|
|
1697
|
-
const parentId = sectionStack[sectionStack.length - 1].id;
|
|
1698
|
-
edges.push({ id: 'e:' + secId + ':' + parentId + ':parent', src: parentId, dst: secId, rel: 'DEFINES' });
|
|
1699
|
-
sectionStack.push({ id: secId, depth });
|
|
1700
|
-
i++; // skip underline line
|
|
1701
|
-
continue;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
// #hashtag concepts (skip markdown headings already matched)
|
|
1705
|
-
const tags = line.match(/#([a-zA-Z][a-zA-Z0-9_-]{2,})/g);
|
|
1706
|
-
if (tags) {
|
|
1707
|
-
for (const tag of tags) {
|
|
1708
|
-
const concept = tag.slice(1);
|
|
1709
|
-
const cId = 'concept:tag:' + concept.toLowerCase();
|
|
1710
|
-
if (!nodes.find(n => n.id === cId)) {
|
|
1711
|
-
nodes.push({ id: cId, label: 'Concept', name: concept, norm_label: normTitle(concept), file_path: null, start_line: 0, end_line: 0, language: null });
|
|
1712
|
-
}
|
|
1713
|
-
const curSec = sectionStack[sectionStack.length - 1].id;
|
|
1714
|
-
edges.push({ id: 'e:' + curSec + ':' + cId + ':tag', src: curSec, dst: cId, rel: 'TAGGED_AS' });
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
try {
|
|
1720
|
-
insertAll(nodes, edges);
|
|
1721
|
-
// Update file cache so we skip unchanged files next run
|
|
1722
|
-
try {
|
|
1723
|
-
updateFileCache(db, { filePath, contentHash, lastParsed: Date.now(), nodeCount: nodes.length, edgeCount: edges.length });
|
|
1724
|
-
} catch {}
|
|
1725
|
-
} catch (e) { console.error('[docs-build] error inserting', relPath, e.message); }
|
|
1726
|
-
}
|
|
1727
|
-
console.log(`[docs-build] indexed ${docFiles.length - skipped} docs (${skipped} cached), ${totalSections} sections → ${dbPath}`);
|
|
1728
|
-
buildDocsState.set(d, { status: 'done', sections: totalSections, files: docFiles.length - skipped, cached: skipped, error: null, startedAt, completedAt: Date.now() });
|
|
1729
|
-
} finally { closeDb(db); }
|
|
1730
|
-
})().catch(e => {
|
|
1731
|
-
console.error('[docs-build] fatal:', e.message);
|
|
1732
|
-
buildDocsState.set(d, { status: 'error', sections: 0, files: 0, error: e.message, startedAt, completedAt: Date.now() });
|
|
1733
|
-
});
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1736
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1737
|
-
}
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
// -------------------------------------------------- GET /api/monograph-content
|
|
1742
|
-
// Returns actual file content for a node (properties.content or file slice)
|
|
1743
|
-
if (req.method === 'GET' && url === '/api/monograph-content') {
|
|
1744
|
-
try {
|
|
1745
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1746
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1747
|
-
const id = qs.get('id') || '';
|
|
1748
|
-
const d = path.resolve(dir || process.cwd());
|
|
1749
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1750
|
-
if (!id) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?id=' })); return; }
|
|
1751
|
-
if (!fs.existsSync(dbPath)) { res.writeHead(404); res.end(JSON.stringify({ error: 'Graph not built' })); return; }
|
|
1752
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1753
|
-
const db = openDb(dbPath);
|
|
1754
|
-
let content = '', filePath = '', startLine = 0, endLine = 0, language = '', name = '', type = '';
|
|
1755
|
-
try {
|
|
1756
|
-
const node = db.prepare('SELECT * FROM nodes WHERE id=?').get(id);
|
|
1757
|
-
if (!node) { res.writeHead(404); res.end(JSON.stringify({ error: 'Node not found' })); return; }
|
|
1758
|
-
name = node.name || id;
|
|
1759
|
-
type = node.label || 'Unknown';
|
|
1760
|
-
filePath = node.file_path || '';
|
|
1761
|
-
startLine = node.start_line || 0;
|
|
1762
|
-
endLine = node.end_line || 0;
|
|
1763
|
-
language = node.language || '';
|
|
1764
|
-
// Try properties.content first (from official monograph pipeline)
|
|
1765
|
-
if (node.properties) {
|
|
1766
|
-
try {
|
|
1767
|
-
const props = JSON.parse(node.properties);
|
|
1768
|
-
if (props.content && props.content.trim()) { content = props.content; }
|
|
1769
|
-
} catch {}
|
|
1770
|
-
}
|
|
1771
|
-
// Fallback: read from actual file
|
|
1772
|
-
if (!content && filePath) {
|
|
1773
|
-
const absPath = path.isAbsolute(filePath) ? filePath : path.join(d, filePath);
|
|
1774
|
-
try {
|
|
1775
|
-
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
1776
|
-
const sl = Math.max(0, (startLine || 1) - 1);
|
|
1777
|
-
const el = Math.min(lines.length, (endLine || startLine || lines.length) + 5);
|
|
1778
|
-
content = lines.slice(sl, Math.min(el, sl + 120)).join('\n');
|
|
1779
|
-
} catch {}
|
|
1780
|
-
}
|
|
1781
|
-
} finally { closeDb(db); }
|
|
1782
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1783
|
-
res.end(JSON.stringify({ content, filePath, startLine, endLine, language, name, type }));
|
|
1784
|
-
} catch (err) {
|
|
1785
|
-
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
1786
|
-
}
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
// -------------------------------------------------- GET /api/monograph-fts
|
|
1791
|
-
// Full-text search with content snippets — powers the wiki search box
|
|
1792
|
-
if (req.method === 'GET' && url === '/api/monograph-fts') {
|
|
1793
|
-
try {
|
|
1794
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1795
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1796
|
-
const q = (qs.get('q') || '').trim();
|
|
1797
|
-
const limit = Math.min(100, parseInt(qs.get('limit') || '50', 10));
|
|
1798
|
-
const d = path.resolve(dir || process.cwd());
|
|
1799
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1800
|
-
if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q=' })); return; }
|
|
1801
|
-
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ nodes: [] })); return; }
|
|
1802
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1803
|
-
const { ftsSearch } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/fts-store.js');
|
|
1804
|
-
const db = openDb(dbPath);
|
|
1805
|
-
let nodes = [];
|
|
1806
|
-
try {
|
|
1807
|
-
const hits = ftsSearch(db, q, limit);
|
|
1808
|
-
nodes = hits.map(h => {
|
|
1809
|
-
let snippet = '';
|
|
1810
|
-
if (h.properties) { try { const p = JSON.parse(h.properties); snippet = (p.content || '').slice(0, 200); } catch {} }
|
|
1811
|
-
return { id: h.id, label: h.name, type: h.label, degree: 0, filePath: h.filePath || h.file_path, startLine: h.startLine || h.start_line, snippet };
|
|
1812
|
-
});
|
|
1813
|
-
} finally { closeDb(db); }
|
|
1814
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1815
|
-
res.end(JSON.stringify({ nodes }));
|
|
1816
|
-
} catch (err) {
|
|
1817
|
-
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
1818
|
-
}
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// -------------------------------------------------- GET /api/monograph-related
|
|
1823
|
-
// BFS from a node — returns node IDs sorted by graph distance (for re-ranking)
|
|
1824
|
-
if (req.method === 'GET' && url === '/api/monograph-related') {
|
|
1825
|
-
try {
|
|
1826
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1827
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1828
|
-
const id = qs.get('id') || '';
|
|
1829
|
-
const limit = Math.min(200, parseInt(qs.get('limit') || '60', 10));
|
|
1830
|
-
const maxDepth = Math.min(4, parseInt(qs.get('depth') || '3', 10));
|
|
1831
|
-
const d = path.resolve(dir || process.cwd());
|
|
1832
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1833
|
-
if (!id || !fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ related: [] })); return; }
|
|
1834
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1835
|
-
const db = openDb(dbPath);
|
|
1836
|
-
const related = [];
|
|
1837
|
-
try {
|
|
1838
|
-
const visited = new Set([id]);
|
|
1839
|
-
let frontier = [id];
|
|
1840
|
-
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && related.length < limit; depth++) {
|
|
1841
|
-
const next = [];
|
|
1842
|
-
for (const nodeId of frontier) {
|
|
1843
|
-
const rows = db.prepare(`SELECT DISTINCT target_id as nid FROM edges WHERE source_id=? UNION SELECT DISTINCT source_id as nid FROM edges WHERE target_id=? LIMIT 30`).all(nodeId, nodeId);
|
|
1844
|
-
for (const r of rows) {
|
|
1845
|
-
if (!visited.has(r.nid)) {
|
|
1846
|
-
visited.add(r.nid);
|
|
1847
|
-
next.push(r.nid);
|
|
1848
|
-
related.push({ id: r.nid, distance: depth });
|
|
1849
|
-
if (related.length >= limit) break;
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
if (related.length >= limit) break;
|
|
1853
|
-
}
|
|
1854
|
-
frontier = next;
|
|
1855
|
-
}
|
|
1856
|
-
} finally { closeDb(db); }
|
|
1857
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1858
|
-
res.end(JSON.stringify({ related }));
|
|
1859
|
-
} catch (err) {
|
|
1860
|
-
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
1861
|
-
}
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// -------------------------------------------------- GET /api/monograph-ai-context
|
|
1866
|
-
// Builds a rich AI context bundle for a node: content + 1-hop neighbors
|
|
1867
|
-
if (req.method === 'GET' && url === '/api/monograph-ai-context') {
|
|
1868
|
-
try {
|
|
1869
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1870
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1871
|
-
const id = qs.get('id') || '';
|
|
1872
|
-
const d = path.resolve(dir || process.cwd());
|
|
1873
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1874
|
-
if (!id || !fs.existsSync(dbPath)) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; }
|
|
1875
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1876
|
-
const db = openDb(dbPath);
|
|
1877
|
-
let result = { node: null, content: '', neighbors: [], markdown: '' };
|
|
1878
|
-
try {
|
|
1879
|
-
const node = db.prepare('SELECT * FROM nodes WHERE id=?').get(id);
|
|
1880
|
-
if (!node) { res.writeHead(404); res.end(JSON.stringify({ error: 'Node not found' })); return; }
|
|
1881
|
-
result.node = { id: node.id, name: node.name, type: node.label, filePath: node.file_path, startLine: node.start_line, endLine: node.end_line };
|
|
1882
|
-
// Get content
|
|
1883
|
-
let content = '';
|
|
1884
|
-
if (node.properties) { try { const p = JSON.parse(node.properties); content = p.content || ''; } catch {} }
|
|
1885
|
-
if (!content && node.file_path) {
|
|
1886
|
-
const absPath = path.isAbsolute(node.file_path) ? node.file_path : path.join(d, node.file_path);
|
|
1887
|
-
try {
|
|
1888
|
-
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
1889
|
-
const sl = Math.max(0, (node.start_line || 1) - 1);
|
|
1890
|
-
const el = Math.min(lines.length, (node.end_line || node.start_line || lines.length) + 5);
|
|
1891
|
-
content = lines.slice(sl, Math.min(el, sl + 80)).join('\n');
|
|
1892
|
-
} catch {}
|
|
1893
|
-
}
|
|
1894
|
-
result.content = content;
|
|
1895
|
-
// Get 1-hop neighbors
|
|
1896
|
-
const outEdges = db.prepare('SELECT e.relation, n.id, n.name, n.label, n.file_path FROM edges e JOIN nodes n ON n.id=e.target_id WHERE e.source_id=? LIMIT 20').all(id);
|
|
1897
|
-
const inEdges = db.prepare('SELECT e.relation, n.id, n.name, n.label, n.file_path FROM edges e JOIN nodes n ON n.id=e.source_id WHERE e.target_id=? LIMIT 20').all(id);
|
|
1898
|
-
result.neighbors = [
|
|
1899
|
-
...outEdges.map(e => ({ direction: 'out', relation: e.relation, id: e.id, name: e.name, type: e.label, filePath: e.file_path })),
|
|
1900
|
-
...inEdges.map(e => ({ direction: 'in', relation: e.relation, id: e.id, name: e.name, type: e.label, filePath: e.file_path })),
|
|
1901
|
-
];
|
|
1902
|
-
// Build markdown for clipboard/AI
|
|
1903
|
-
const lines2 = [];
|
|
1904
|
-
lines2.push(`# ${node.name} [${node.label}]`);
|
|
1905
|
-
if (node.file_path) lines2.push(`**File:** \`${node.file_path}\`${node.start_line ? ` (line ${node.start_line})` : ''}`);
|
|
1906
|
-
if (content) lines2.push(`\n\`\`\`${node.language || ''}\n${content.slice(0, 3000)}\n\`\`\``);
|
|
1907
|
-
if (outEdges.length) {
|
|
1908
|
-
lines2.push(`\n**Depends on (${outEdges.length}):**`);
|
|
1909
|
-
outEdges.forEach(e => lines2.push(`- ${e.relation} → ${e.name} [${e.label}]${e.file_path ? ' `' + e.file_path + '`' : ''}`));
|
|
1910
|
-
}
|
|
1911
|
-
if (inEdges.length) {
|
|
1912
|
-
lines2.push(`\n**Used by (${inEdges.length}):**`);
|
|
1913
|
-
inEdges.forEach(e => lines2.push(`- ${e.relation} ← ${e.name} [${e.label}]${e.file_path ? ' `' + e.file_path + '`' : ''}`));
|
|
1914
|
-
}
|
|
1915
|
-
result.markdown = lines2.join('\n');
|
|
1916
|
-
} finally { closeDb(db); }
|
|
1917
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1918
|
-
res.end(JSON.stringify(result));
|
|
1919
|
-
} catch (err) {
|
|
1920
|
-
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
1921
|
-
}
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
// -------------------------------------------------- GET /api/monograph-query
|
|
1926
|
-
if (req.method === 'GET' && url === '/api/monograph-query') {
|
|
1927
|
-
try {
|
|
1928
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1929
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1930
|
-
const q = qs.get('q') || '';
|
|
1931
|
-
const d = path.resolve(dir || process.cwd());
|
|
1932
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1933
|
-
if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q= parameter' })); return; }
|
|
1934
|
-
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, result: 'Graph not built yet. Run: monomind monograph build' })); return; }
|
|
1935
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1936
|
-
const { ftsSearch } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/fts-store.js');
|
|
1937
|
-
const db = openDb(dbPath);
|
|
1938
|
-
let result = '';
|
|
1939
|
-
try {
|
|
1940
|
-
const hits = ftsSearch(db, q, 20);
|
|
1941
|
-
if (!hits.length) {
|
|
1942
|
-
result = `No matches found for: "${q}"`;
|
|
1943
|
-
} else {
|
|
1944
|
-
result = hits.map((h, i) => `${String(i+1).padStart(3,' ')}. ${h.name} [${h.normLabel}]${h.filePath ? '\n ' + h.filePath : ''}`).join('\n');
|
|
1945
|
-
// Show outgoing edges for top hit
|
|
1946
|
-
const topHit = hits[0];
|
|
1947
|
-
const neighbors = db.prepare('SELECT target_id, relation FROM edges WHERE source_id=? LIMIT 10').all(topHit.id);
|
|
1948
|
-
if (neighbors.length) {
|
|
1949
|
-
result += `\n\n── ${topHit.name} references:\n` + neighbors.map(n => ` ${n.relation} → ${n.target_id.split('/').pop() || n.target_id}`).join('\n');
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
} finally { closeDb(db); }
|
|
1953
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1954
|
-
res.end(JSON.stringify({ success: true, query: q, result }));
|
|
1955
|
-
} catch (err) {
|
|
1956
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1957
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
1958
|
-
}
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
// -------------------------------------------------- GET /api/monograph-explain
|
|
1963
|
-
if (req.method === 'GET' && url === '/api/monograph-explain') {
|
|
1964
|
-
try {
|
|
1965
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
1966
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
1967
|
-
const nodeQ = qs.get('node') || '';
|
|
1968
|
-
const d = path.resolve(dir || process.cwd());
|
|
1969
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
1970
|
-
if (!nodeQ) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?node= parameter' })); return; }
|
|
1971
|
-
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, explanation: 'Graph not built yet. Run: monomind monograph build' })); return; }
|
|
1972
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
1973
|
-
const { ftsSearch } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/fts-store.js');
|
|
1974
|
-
const db = openDb(dbPath);
|
|
1975
|
-
let explanation = '';
|
|
1976
|
-
try {
|
|
1977
|
-
let nd = db.prepare('SELECT * FROM nodes WHERE id=?').get(nodeQ) || db.prepare('SELECT * FROM nodes WHERE name=?').get(nodeQ);
|
|
1978
|
-
if (!nd) { const hits = ftsSearch(db, nodeQ, 1); if (hits[0]) nd = db.prepare('SELECT * FROM nodes WHERE id=?').get(hits[0].id); }
|
|
1979
|
-
if (!nd) {
|
|
1980
|
-
explanation = `No node found matching: "${nodeQ}"`;
|
|
1981
|
-
} else {
|
|
1982
|
-
const outEdges = db.prepare('SELECT target_id, relation FROM edges WHERE source_id=? LIMIT 20').all(nd.id);
|
|
1983
|
-
const inEdges = db.prepare('SELECT source_id, relation FROM edges WHERE target_id=? LIMIT 20').all(nd.id);
|
|
1984
|
-
explanation = [
|
|
1985
|
-
`## ${nd.name} [${nd.label}]`,
|
|
1986
|
-
nd.file_path ? `File: ${nd.file_path}${nd.start_line ? ':' + nd.start_line : ''}` : '',
|
|
1987
|
-
nd.language ? `Language: ${nd.language}` : '',
|
|
1988
|
-
nd.is_exported ? 'Exported: yes' : 'Exported: no',
|
|
1989
|
-
'',
|
|
1990
|
-
outEdges.length ? `References (${outEdges.length}):\n` + outEdges.map(e => ` ${e.relation} → ${e.target_id.split('/').pop() || e.target_id}`).join('\n') : 'No outgoing references.',
|
|
1991
|
-
inEdges.length ? `\nReferenced by (${inEdges.length}):\n` + inEdges.map(e => ` ${e.source_id.split('/').pop() || e.source_id} [${e.relation}]`).join('\n') : '',
|
|
1992
|
-
].filter(Boolean).join('\n');
|
|
1993
|
-
}
|
|
1994
|
-
} finally { closeDb(db); }
|
|
1995
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
1996
|
-
res.end(JSON.stringify({ success: true, node: nodeQ, explanation }));
|
|
1997
|
-
} catch (err) {
|
|
1998
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1999
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2000
|
-
}
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
// -------------------------------------------------- GET /api/monograph-path
|
|
2005
|
-
if (req.method === 'GET' && url === '/api/monograph-path') {
|
|
2006
|
-
try {
|
|
2007
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2008
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2009
|
-
const from = qs.get('from') || '';
|
|
2010
|
-
const to = qs.get('to') || '';
|
|
2011
|
-
const d = path.resolve(dir || process.cwd());
|
|
2012
|
-
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
2013
|
-
if (!from || !to) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?from= and ?to= parameters' })); return; }
|
|
2014
|
-
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, path: 'Graph not built yet.' })); return; }
|
|
2015
|
-
// Import only graphology-free storage modules to avoid broken graphology dep
|
|
2016
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
2017
|
-
const { ftsSearch } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/fts-store.js');
|
|
2018
|
-
// SQL-based BFS for shortest path (avoids graphology)
|
|
2019
|
-
const getShortestPath = (db, fromId, toId, maxDepth = 6) => {
|
|
2020
|
-
if (fromId === toId) return [fromId];
|
|
2021
|
-
const visited = new Set([fromId]);
|
|
2022
|
-
let frontier = [[fromId]];
|
|
2023
|
-
for (let depth = 0; depth < maxDepth; depth++) {
|
|
2024
|
-
const next = [];
|
|
2025
|
-
for (const chain of frontier) {
|
|
2026
|
-
const cur = chain[chain.length - 1];
|
|
2027
|
-
const neighbors = db.prepare('SELECT target_id AS id FROM edges WHERE source_id=? UNION SELECT source_id AS id FROM edges WHERE target_id=?').all(cur, cur);
|
|
2028
|
-
for (const { id } of neighbors) {
|
|
2029
|
-
if (!visited.has(id)) {
|
|
2030
|
-
const newChain = [...chain, id];
|
|
2031
|
-
if (id === toId) return newChain;
|
|
2032
|
-
visited.add(id);
|
|
2033
|
-
next.push(newChain);
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
if (!next.length) break;
|
|
2038
|
-
frontier = next;
|
|
2039
|
-
}
|
|
2040
|
-
return null;
|
|
2041
|
-
};
|
|
2042
|
-
const db = openDb(dbPath);
|
|
2043
|
-
let pathResult = '';
|
|
2044
|
-
try {
|
|
2045
|
-
const resolveId = (q) => {
|
|
2046
|
-
const direct = db.prepare('SELECT id FROM nodes WHERE id=? OR name=?').get(q, q);
|
|
2047
|
-
if (direct) return direct.id;
|
|
2048
|
-
const hits = ftsSearch(db, q, 1);
|
|
2049
|
-
return hits[0]?.id || q;
|
|
2050
|
-
};
|
|
2051
|
-
const fromId = resolveId(from);
|
|
2052
|
-
const toId = resolveId(to);
|
|
2053
|
-
const p = getShortestPath(db, fromId, toId);
|
|
2054
|
-
if (!p || !p.length) {
|
|
2055
|
-
pathResult = `No path found between "${from}" and "${to}"`;
|
|
2056
|
-
} else {
|
|
2057
|
-
const names = p.map(id => { const n = db.prepare('SELECT name FROM nodes WHERE id=?').get(id); return n ? n.name : id.split('/').pop() || id; });
|
|
2058
|
-
pathResult = names.join(' → ') + ` (${p.length - 1} hop${p.length !== 2 ? 's' : ''})`;
|
|
2059
|
-
}
|
|
2060
|
-
} finally { closeDb(db); }
|
|
2061
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2062
|
-
res.end(JSON.stringify({ success: true, from, to, path: pathResult }));
|
|
2063
|
-
} catch (err) {
|
|
2064
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2065
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2066
|
-
}
|
|
2067
|
-
return;
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
// -------------------------------------------------- GET /api/monograph-watch-status
|
|
2071
|
-
if (req.method === 'GET' && url === '/api/monograph-watch-status') {
|
|
2072
|
-
try {
|
|
2073
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2074
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2075
|
-
const d = path.resolve(dir || process.cwd());
|
|
2076
|
-
const pidPath = path.join(d, '.monomind', 'monograph.watch.pid');
|
|
2077
|
-
let running = false, pid = null;
|
|
2078
|
-
try {
|
|
2079
|
-
pid = parseInt(fs.readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
2080
|
-
process.kill(pid, 0);
|
|
2081
|
-
running = true;
|
|
2082
|
-
} catch {}
|
|
2083
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2084
|
-
res.end(JSON.stringify({ running, pid }));
|
|
2085
|
-
} catch (err) {
|
|
2086
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2087
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2088
|
-
}
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
// -------------------------------------------------- POST /api/monograph-watch-toggle
|
|
2093
|
-
if (req.method === 'POST' && url === '/api/monograph-watch-toggle') {
|
|
2094
|
-
try {
|
|
2095
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2096
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2097
|
-
const d = path.resolve(dir || process.cwd());
|
|
2098
|
-
const pidPath = path.join(d, '.monomind', 'monograph.watch.pid');
|
|
2099
|
-
let wasRunning = false;
|
|
2100
|
-
try {
|
|
2101
|
-
const pid = parseInt(fs.readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
2102
|
-
process.kill(pid, 0);
|
|
2103
|
-
wasRunning = true;
|
|
2104
|
-
process.kill(pid, 'SIGTERM');
|
|
2105
|
-
try { fs.unlinkSync(pidPath); } catch {}
|
|
2106
|
-
} catch {}
|
|
2107
|
-
|
|
2108
|
-
if (wasRunning) {
|
|
2109
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2110
|
-
res.end(JSON.stringify({ running: false, action: 'stopped' }));
|
|
2111
|
-
} else {
|
|
2112
|
-
const { spawn: sp } = await import('child_process');
|
|
2113
|
-
const child = sp(process.execPath, [process.argv[1], 'monograph', 'watch'], { stdio: 'ignore', detached: true, cwd: d, env: process.env });
|
|
2114
|
-
child.unref();
|
|
2115
|
-
try { fs.mkdirSync(path.join(d, '.monomind'), { recursive: true }); } catch {}
|
|
2116
|
-
try { fs.writeFileSync(pidPath, String(child.pid)); } catch {}
|
|
2117
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2118
|
-
res.end(JSON.stringify({ running: true, pid: child.pid, action: 'started' }));
|
|
2119
|
-
}
|
|
2120
|
-
} catch (err) {
|
|
2121
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2122
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2123
|
-
}
|
|
2124
|
-
return;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
// -------------------------------------------------- POST /api/mcp/call
|
|
2128
|
-
if (req.method === 'POST' && url === '/api/mcp/call') {
|
|
2129
|
-
let body = '';
|
|
2130
|
-
req.on('data', c => body += c);
|
|
2131
|
-
req.on('end', async () => {
|
|
2132
|
-
const json = res => { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); };
|
|
2133
|
-
const ok = (data) => { json(res); res.end(JSON.stringify({ content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] })); };
|
|
2134
|
-
const err = (msg) => { json(res); res.end(JSON.stringify({ error: msg })); };
|
|
2135
|
-
try {
|
|
2136
|
-
const { tool, input = {}, args = {} } = JSON.parse(body);
|
|
2137
|
-
const qs2 = new URL(req.url, 'http://localhost').searchParams;
|
|
2138
|
-
// dir can come from: URL query string, body.args.dir, body.input.dir, or server default
|
|
2139
|
-
const dir2 = qs2.get('dir') || args.dir || input.dir || projectDir;
|
|
2140
|
-
const d2 = path.resolve(dir2 || process.cwd());
|
|
2141
|
-
const dbPath2 = path.join(d2, '.monomind', 'monograph.db');
|
|
2142
|
-
if (!fs.existsSync(dbPath2)) { err('monograph.db not found — run monograph build first'); return; }
|
|
2143
|
-
// Import only graphology-free storage modules to avoid broken graphology dep
|
|
2144
|
-
const { openDb, closeDb } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/db.js');
|
|
2145
|
-
const { ftsSearch } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/fts-store.js');
|
|
2146
|
-
const { countNodes } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/node-store.js');
|
|
2147
|
-
const { countEdges } = await import('/Users/morteza/Desktop/tools/monomind/packages/@monomind/monograph/dist/src/storage/edge-store.js');
|
|
2148
|
-
const getShortestPath = (db, fromId, toId, maxDepth = 6) => {
|
|
2149
|
-
if (fromId === toId) return [fromId];
|
|
2150
|
-
const visited = new Set([fromId]);
|
|
2151
|
-
let frontier = [[fromId]];
|
|
2152
|
-
for (let depth = 0; depth < maxDepth; depth++) {
|
|
2153
|
-
const next = [];
|
|
2154
|
-
for (const chain of frontier) {
|
|
2155
|
-
const cur = chain[chain.length - 1];
|
|
2156
|
-
const neighbors = db.prepare('SELECT target_id AS id FROM edges WHERE source_id=? UNION SELECT source_id AS id FROM edges WHERE target_id=?').all(cur, cur);
|
|
2157
|
-
for (const { id } of neighbors) {
|
|
2158
|
-
if (!visited.has(id)) {
|
|
2159
|
-
const newChain = [...chain, id];
|
|
2160
|
-
if (id === toId) return newChain;
|
|
2161
|
-
visited.add(id);
|
|
2162
|
-
next.push(newChain);
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
if (!next.length) break;
|
|
2167
|
-
frontier = next;
|
|
2168
|
-
}
|
|
2169
|
-
return null;
|
|
2170
|
-
};
|
|
2171
|
-
const db2 = openDb(dbPath2);
|
|
2172
|
-
try {
|
|
2173
|
-
if (tool === 'monograph_stats') {
|
|
2174
|
-
const n = countNodes(db2), e = countEdges(db2);
|
|
2175
|
-
ok(`nodes: ${n}\nedges: ${e}`);
|
|
2176
|
-
} else if (tool === 'monograph_cypher') {
|
|
2177
|
-
// Translate basic MATCH (n:Label) queries to SQL
|
|
2178
|
-
const q = (input.query || '').trim();
|
|
2179
|
-
const labelMatch = q.match(/MATCH\s+\(n:(\w+)\)/i);
|
|
2180
|
-
if (labelMatch) {
|
|
2181
|
-
const label = labelMatch[1];
|
|
2182
|
-
const rows = db2.prepare('SELECT name FROM nodes WHERE label = ? LIMIT 5000').all(label);
|
|
2183
|
-
ok(rows.map(r => r.name).join('\n'));
|
|
2184
|
-
} else {
|
|
2185
|
-
ok('Cypher: unsupported query pattern');
|
|
2186
|
-
}
|
|
2187
|
-
} else if (tool === 'monograph_cohesion') {
|
|
2188
|
-
const limit = input.limit || 30;
|
|
2189
|
-
// Check if community_id is populated
|
|
2190
|
-
const hasCommunities = db2.prepare('SELECT COUNT(*) as c FROM nodes WHERE community_id IS NOT NULL').get().c > 0;
|
|
2191
|
-
if (hasCommunities) {
|
|
2192
|
-
const rows = db2.prepare('SELECT community_id, COUNT(*) as size FROM nodes GROUP BY community_id ORDER BY size DESC LIMIT ?').all(limit);
|
|
2193
|
-
ok(rows.map(r => `community ${r.community_id}: ${r.size} nodes`).join('\n'));
|
|
2194
|
-
} else {
|
|
2195
|
-
// Fallback: group by type (label)
|
|
2196
|
-
const rows = db2.prepare('SELECT label, COUNT(*) as cnt FROM nodes GROUP BY label ORDER BY cnt DESC LIMIT ?').all(limit);
|
|
2197
|
-
const total = db2.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
2198
|
-
const lines = rows.map(r => {
|
|
2199
|
-
const pct = ((r.cnt / total) * 100).toFixed(1);
|
|
2200
|
-
const bar = '█'.repeat(Math.round(pct / 3));
|
|
2201
|
-
return `${(r.label || 'unknown').padEnd(12)} ${r.cnt.toString().padStart(6)} nodes (${pct}%) ${bar}`;
|
|
2202
|
-
});
|
|
2203
|
-
ok(`Type Distribution (community clustering not yet run)\n${'─'.repeat(50)}\n${lines.join('\n')}`);
|
|
2204
|
-
}
|
|
2205
|
-
} else if (tool === 'monograph_bridge') {
|
|
2206
|
-
const limit = input.limit || 20;
|
|
2207
|
-
// Find hub nodes that connect many different directories (cross-module connectors)
|
|
2208
|
-
const rows = db2.prepare(`
|
|
2209
|
-
SELECT n.name, n.label, n.file_path,
|
|
2210
|
-
COUNT(DISTINCT CASE WHEN e.source_id = n.id THEN n2.file_path ELSE NULL END) +
|
|
2211
|
-
COUNT(DISTINCT CASE WHEN e.target_id = n.id THEN n2.file_path ELSE NULL END) as cross_file_count,
|
|
2212
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id OR target_id = n.id) as total_degree
|
|
2213
|
-
FROM nodes n
|
|
2214
|
-
JOIN edges e ON e.source_id = n.id OR e.target_id = n.id
|
|
2215
|
-
JOIN nodes n2 ON (e.source_id = n2.id OR e.target_id = n2.id) AND n2.id != n.id
|
|
2216
|
-
GROUP BY n.id
|
|
2217
|
-
HAVING cross_file_count > 2
|
|
2218
|
-
ORDER BY cross_file_count DESC, total_degree DESC
|
|
2219
|
-
LIMIT ?`).all(limit);
|
|
2220
|
-
if (!rows.length) {
|
|
2221
|
-
ok('No cross-module bridge nodes found in top results. Try running monograph build to index more files.');
|
|
2222
|
-
} else {
|
|
2223
|
-
const lines = rows.map(r =>
|
|
2224
|
-
`${r.name} (${r.label})\n → connects ${r.cross_file_count} files, degree ${r.total_degree}\n ${r.file_path || '?'}`
|
|
2225
|
-
);
|
|
2226
|
-
ok(`Cross-Module Bridge Nodes (${rows.length})\n${'─'.repeat(50)}\n${lines.join('\n\n')}`);
|
|
2227
|
-
}
|
|
2228
|
-
} else if (tool === 'monograph_detect_changes') {
|
|
2229
|
-
const { execSync } = await import('child_process');
|
|
2230
|
-
let changed = '';
|
|
2231
|
-
try { changed = execSync('git diff --name-only HEAD', { cwd: d2, encoding: 'utf-8' }); } catch { changed = '(git not available)'; }
|
|
2232
|
-
ok(changed.trim() || 'No changed files detected');
|
|
2233
|
-
} else if (tool === 'monograph_diff') {
|
|
2234
|
-
ok('Graph diff: compare two snapshots using monograph snapshot + monograph diff commands');
|
|
2235
|
-
} else if (tool === 'monograph_rename') {
|
|
2236
|
-
const sym = input.symbolName || '';
|
|
2237
|
-
if (!sym) { ok('Provide symbolName to rename'); return; }
|
|
2238
|
-
const hits = ftsSearch(db2, sym, 20);
|
|
2239
|
-
ok(`Found ${hits.length} occurrences of "${sym}":\n` + hits.map(h => ` ${h.filePath || '?'}:${h.startLine || '?'} — ${h.name}`).join('\n'));
|
|
2240
|
-
} else if (tool === 'monograph_impact') {
|
|
2241
|
-
const target = input.target || '';
|
|
2242
|
-
const dir3 = input.direction || 'both';
|
|
2243
|
-
const depth = input.maxDepth || 4;
|
|
2244
|
-
const hits = ftsSearch(db2, target, 5);
|
|
2245
|
-
if (!hits.length) { ok(`Node not found: ${target}`); return; }
|
|
2246
|
-
const nodeId = hits[0].id;
|
|
2247
|
-
const visited = new Set([nodeId]);
|
|
2248
|
-
const frontier = [nodeId];
|
|
2249
|
-
const results = [];
|
|
2250
|
-
for (let d3 = 0; d3 < depth && frontier.length; d3++) {
|
|
2251
|
-
const next = [];
|
|
2252
|
-
for (const id of frontier) {
|
|
2253
|
-
const outgoing = dir3 !== 'upstream' ? db2.prepare('SELECT target_id, relation FROM edges WHERE source_id = ?').all(id) : [];
|
|
2254
|
-
const incoming = dir3 !== 'downstream' ? db2.prepare('SELECT source_id as target_id, relation FROM edges WHERE target_id = ?').all(id) : [];
|
|
2255
|
-
for (const e of [...outgoing, ...incoming]) {
|
|
2256
|
-
if (!visited.has(e.target_id)) {
|
|
2257
|
-
visited.add(e.target_id);
|
|
2258
|
-
next.push(e.target_id);
|
|
2259
|
-
const n3 = db2.prepare('SELECT name, label FROM nodes WHERE id = ?').get(e.target_id);
|
|
2260
|
-
if (n3) results.push(` [hop ${d3+1}] ${n3.name} (${n3.label}) via ${e.relation}`);
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
frontier.length = 0; frontier.push(...next);
|
|
2265
|
-
}
|
|
2266
|
-
ok(`Impact of "${hits[0].name}" (${dir3}, depth=${depth}):\n` + (results.join('\n') || ' (no dependencies found)'));
|
|
2267
|
-
} else if (tool === 'monograph_context') {
|
|
2268
|
-
const id = input.id || '';
|
|
2269
|
-
const hits = ftsSearch(db2, id, 5);
|
|
2270
|
-
if (!hits.length) { ok(`Node not found: ${id}`); return; }
|
|
2271
|
-
const node = hits[0];
|
|
2272
|
-
const outEdges = db2.prepare('SELECT e.relation, n.name FROM edges e JOIN nodes n ON n.id = e.target_id WHERE e.source_id = ? LIMIT 20').all(node.id);
|
|
2273
|
-
const inEdges = db2.prepare('SELECT e.relation, n.name FROM edges e JOIN nodes n ON n.id = e.source_id WHERE e.target_id = ? LIMIT 20').all(node.id);
|
|
2274
|
-
ok(`# ${node.name} (${node.label})\nFile: ${node.filePath || '?'}\n\n**Imports / depends on (${outEdges.length}):**\n${outEdges.map(e => ` → ${e.name} [${e.relation}]`).join('\n') || ' (none)'}\n\n**Used by / depended on by (${inEdges.length}):**\n${inEdges.map(e => ` ← ${e.name} [${e.relation}]`).join('\n') || ' (none)'}`);
|
|
2275
|
-
} else if (tool === 'monograph_query' || tool === 'monograph_suggest') {
|
|
2276
|
-
const q2 = input.query || input.task || '';
|
|
2277
|
-
const hits2 = ftsSearch(db2, q2, 20);
|
|
2278
|
-
ok(hits2.map(h => `${h.name} (${h.label}) — ${h.filePath || '?'}:${h.startLine || '?'}`).join('\n') || 'No results');
|
|
2279
|
-
|
|
2280
|
-
} else if (tool === 'monograph_unlinked_refs') {
|
|
2281
|
-
const limit = input.limit || 50;
|
|
2282
|
-
const rows = db2.prepare(`SELECT n.name, n.label, n.file_path FROM nodes n LEFT JOIN edges e ON e.target_id = n.id WHERE e.target_id IS NULL AND n.label IN ('Function','Class','Variable','Interface','Method','Module') ORDER BY n.name LIMIT ?`).all(limit);
|
|
2283
|
-
if (!rows.length) { ok('No unlinked symbols found — all exports appear to be referenced.'); }
|
|
2284
|
-
else { ok(`Unlinked Symbols (${rows.length}) — potentially unused exports:\n${'─'.repeat(50)}\n${rows.map(r => ` ${r.name} (${r.label})\n ${r.file_path || '?'}`).join('\n\n')}`); }
|
|
2285
|
-
|
|
2286
|
-
} else if (tool === 'monograph_reachability') {
|
|
2287
|
-
const limit = input.limit || 30;
|
|
2288
|
-
const unreachable = db2.prepare(`SELECT n.name, n.file_path, (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as out_deg FROM nodes n LEFT JOIN edges e ON e.target_id = n.id WHERE e.target_id IS NULL AND n.label = 'File' ORDER BY out_deg DESC LIMIT ?`).all(limit);
|
|
2289
|
-
const total = db2.prepare("SELECT COUNT(*) as c FROM nodes WHERE label = 'File'").get().c;
|
|
2290
|
-
if (!unreachable.length) { ok(`All ${total} files are reachable from at least one other file.`); }
|
|
2291
|
-
else { ok(`Unreachable Files (${unreachable.length} of ${total} total):\n${'─'.repeat(50)}\n${unreachable.map(r => ` ${r.name}${r.out_deg ? ` (imports ${r.out_deg} others)` : ''}\n ${r.file_path || '?'}`).join('\n\n')}`); }
|
|
2292
|
-
|
|
2293
|
-
} else if (tool === 'monograph_boundary_check') {
|
|
2294
|
-
const limit = input.limit || 40;
|
|
2295
|
-
const rows = db2.prepare(`SELECT n1.file_path as src, n2.file_path as dst, e.relation, COUNT(*) as cnt FROM edges e JOIN nodes n1 ON n1.id = e.source_id JOIN nodes n2 ON n2.id = e.target_id WHERE n1.file_path IS NOT NULL AND n2.file_path IS NOT NULL AND n1.file_path != n2.file_path GROUP BY n1.file_path, n2.file_path ORDER BY cnt DESC LIMIT ?`).all(limit);
|
|
2296
|
-
const suspicious = rows.filter(r => { const s = (r.src||'').toLowerCase(), t = (r.dst||'').toLowerCase(); return (s.includes('test') && !t.includes('test')) || (s.includes('spec') && !t.includes('spec')) || (s.includes('/ui/') && t.includes('/db/')) || (s.includes('/view') && t.includes('/model')); });
|
|
2297
|
-
if (!suspicious.length) { ok(`Boundary check: ${rows.length} cross-file edge groups — no obvious violations.\nTop connections:\n${rows.slice(0,10).map(r => ` ${r.src} → ${r.dst} [${r.cnt}x]`).join('\n')}`); }
|
|
2298
|
-
else { ok(`Boundary Violations (${suspicious.length} suspicious):\n${'─'.repeat(50)}\n${suspicious.map(r => ` ⚠ ${r.src}\n → ${r.dst} [${r.cnt} edges]`).join('\n\n')}`); }
|
|
2299
|
-
|
|
2300
|
-
} else if (tool === 'monograph_regression_check' || tool === 'monograph_baseline_compare') {
|
|
2301
|
-
const n = countNodes(db2), e = countEdges(db2);
|
|
2302
|
-
const bPath = path.join(d2, '.monomind', 'monograph-baseline.json');
|
|
2303
|
-
if (!fs.existsSync(bPath)) {
|
|
2304
|
-
fs.writeFileSync(bPath, JSON.stringify({ nodes: n, edges: e, savedAt: new Date().toISOString() }), 'utf-8');
|
|
2305
|
-
ok(`Baseline saved (${n} nodes, ${e} edges). Run again to compare.`);
|
|
2306
|
-
} else {
|
|
2307
|
-
const base = JSON.parse(fs.readFileSync(bPath, 'utf-8'));
|
|
2308
|
-
const dn = n - base.nodes, de = e - base.edges;
|
|
2309
|
-
const sign = v => v > 0 ? `+${v}` : String(v);
|
|
2310
|
-
ok(`Comparison vs baseline (${base.savedAt || 'unknown'}):\n${'─'.repeat(50)}\n Nodes: ${base.nodes} → ${n} (${sign(dn)})\n Edges: ${base.edges} → ${e} (${sign(de)})\n\n${dn === 0 && de === 0 ? '✓ No structural regressions detected.' : '⚠ Graph has changed since baseline.'}`);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
} else if (tool === 'monograph_clone_detect' || tool === 'monograph_similar_files') {
|
|
2314
|
-
const limit = input.limit || 20;
|
|
2315
|
-
const fileNodes = db2.prepare("SELECT id, name, file_path FROM nodes WHERE label = 'File' LIMIT 300").all();
|
|
2316
|
-
const deps = {};
|
|
2317
|
-
for (const f of fileNodes) { deps[f.id] = { name: f.name, set: new Set(db2.prepare('SELECT target_id FROM edges WHERE source_id = ?').all(f.id).map(r => r.target_id)) }; }
|
|
2318
|
-
const keys = Object.keys(deps), pairs = [];
|
|
2319
|
-
for (let i = 0; i < Math.min(keys.length, 150); i++) {
|
|
2320
|
-
for (let j = i + 1; j < Math.min(keys.length, 150); j++) {
|
|
2321
|
-
const a = deps[keys[i]], b = deps[keys[j]];
|
|
2322
|
-
if (!a.set.size && !b.set.size) continue;
|
|
2323
|
-
const inter = [...a.set].filter(x => b.set.has(x)).length;
|
|
2324
|
-
const union = new Set([...a.set, ...b.set]).size;
|
|
2325
|
-
const jac = union ? inter / union : 0;
|
|
2326
|
-
if (jac > 0.5) pairs.push({ a: a.name, b: b.name, jac });
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
pairs.sort((x, y) => y.jac - x.jac);
|
|
2330
|
-
const top = pairs.slice(0, limit);
|
|
2331
|
-
if (!top.length) { ok('No similar file pairs found (Jaccard threshold: 0.5).'); }
|
|
2332
|
-
else { ok(`Similar File Pairs (${top.length}, by import pattern):\n${'─'.repeat(50)}\n${top.map(p => ` ${(p.jac*100).toFixed(0)}% similar\n ${p.a}\n ${p.b}`).join('\n\n')}`); }
|
|
2333
|
-
|
|
2334
|
-
} else if (tool === 'monograph_mirrored_dirs') {
|
|
2335
|
-
const fileNodes = db2.prepare("SELECT file_path FROM nodes WHERE label = 'File' AND file_path IS NOT NULL").all();
|
|
2336
|
-
const dirFiles = {};
|
|
2337
|
-
for (const f of fileNodes) { const dir = path.dirname(f.file_path), base = path.basename(f.file_path); if (!dirFiles[dir]) dirFiles[dir] = new Set(); dirFiles[dir].add(base); }
|
|
2338
|
-
const dirs = Object.keys(dirFiles), pairs = [];
|
|
2339
|
-
for (let i = 0; i < dirs.length; i++) {
|
|
2340
|
-
for (let j = i + 1; j < dirs.length; j++) {
|
|
2341
|
-
const a = dirFiles[dirs[i]], b = dirFiles[dirs[j]];
|
|
2342
|
-
const inter = [...a].filter(x => b.has(x)).length;
|
|
2343
|
-
const union = new Set([...a, ...b]).size;
|
|
2344
|
-
const jac = union ? inter / union : 0;
|
|
2345
|
-
if (jac >= 0.5 && inter >= 2) pairs.push({ a: dirs[i], b: dirs[j], overlap: inter, jac });
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
pairs.sort((x, y) => y.jac - x.jac);
|
|
2349
|
-
if (!pairs.length) { ok('No mirrored directory pairs detected (Jaccard ≥ 0.5, min 2 shared files).'); }
|
|
2350
|
-
else { ok(`Mirrored Directories (${pairs.length} pairs):\n${'─'.repeat(50)}\n${pairs.slice(0,20).map(p => ` ${(p.jac*100).toFixed(0)}% overlap (${p.overlap} shared files)\n ${p.a}\n ${p.b}`).join('\n\n')}`); }
|
|
2351
|
-
|
|
2352
|
-
} else if (tool === 'monograph_health_score' || tool === 'monograph_vital_signs_snapshot') {
|
|
2353
|
-
const n = countNodes(db2), e = countEdges(db2);
|
|
2354
|
-
const dead = db2.prepare("SELECT COUNT(*) as c FROM nodes n LEFT JOIN edges e ON e.target_id = n.id WHERE e.target_id IS NULL AND n.label IN ('Function','Class','Method')").get().c;
|
|
2355
|
-
const hubs = db2.prepare('SELECT COUNT(*) as c FROM (SELECT source_id FROM edges GROUP BY source_id HAVING COUNT(*) > 20)').get().c;
|
|
2356
|
-
const density = n > 1 ? (2 * e / (n * (n - 1))).toFixed(4) : '0';
|
|
2357
|
-
const deadRatio = n ? (dead / n * 100).toFixed(1) : '0';
|
|
2358
|
-
const score = Math.max(0, Math.min(100, 100 - Math.min(30, parseFloat(deadRatio) * 0.5) - Math.min(20, hubs * 2))).toFixed(0);
|
|
2359
|
-
const status = parseInt(score) >= 70 ? '✓ OK' : parseInt(score) >= 40 ? '⚠ WARNING' : '✗ CRITICAL';
|
|
2360
|
-
ok(`Vital Signs — ${new Date().toISOString()}\n${'─'.repeat(50)}\n Health Score: ${score}/100 ${status}\n Nodes: ${n}\n Edges: ${e}\n Density: ${density}\n Dead symbols: ${dead} (${deadRatio}%)\n Hub nodes: ${hubs} nodes with >20 edges`);
|
|
2361
|
-
|
|
2362
|
-
} else if (tool === 'monograph_health_trend') {
|
|
2363
|
-
const bPath = path.join(d2, '.monomind', 'monograph-baseline.json');
|
|
2364
|
-
if (!fs.existsSync(bPath)) { ok('No trend data yet. Run "Health Score" or "Regression Check" first to save a baseline.'); }
|
|
2365
|
-
else {
|
|
2366
|
-
const base = JSON.parse(fs.readFileSync(bPath, 'utf-8'));
|
|
2367
|
-
const n = countNodes(db2), e = countEdges(db2);
|
|
2368
|
-
const dn = n - base.nodes, de = e - base.edges;
|
|
2369
|
-
const sign = v => v > 0 ? `+${v}` : String(v);
|
|
2370
|
-
ok(`Health Trend (vs ${base.savedAt || 'unknown'}):\n${'─'.repeat(50)}\n Nodes: ${base.nodes} → ${n} (${sign(dn)})\n Edges: ${base.edges} → ${e} (${sign(de)})\n Trend: ${dn === 0 && de === 0 ? 'stable' : dn > 0 ? 'growing' : 'shrinking'}`);
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
} else if (tool === 'monograph_hotspots') {
|
|
2374
|
-
const limit = input.limit || 20;
|
|
2375
|
-
const rows = db2.prepare(`SELECT n.name, n.file_path, (SELECT COUNT(*) FROM edges WHERE source_id = n.id OR target_id = n.id) as degree, (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out, (SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in FROM nodes n WHERE n.label = 'File' ORDER BY degree DESC LIMIT ?`).all(limit);
|
|
2376
|
-
if (!rows.length) { ok('No file hotspots found.'); }
|
|
2377
|
-
else { ok(`Hotspot Files (top ${rows.length} by degree):\n${'─'.repeat(50)}\n${rows.map((r,i) => ` ${i+1}. ${r.name} [degree ${r.degree}: ↑${r.fan_in} in, ↓${r.fan_out} out]\n ${r.file_path || '?'}`).join('\n')}`); }
|
|
2378
|
-
|
|
2379
|
-
} else if (tool === 'monograph_maintainability') {
|
|
2380
|
-
const limit = input.limit || 25;
|
|
2381
|
-
const rows = db2.prepare(`SELECT n.name, n.file_path, (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out, (SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in FROM nodes n WHERE n.label = 'File' ORDER BY fan_out DESC LIMIT ?`).all(limit);
|
|
2382
|
-
if (!rows.length) { ok('No file data for maintainability analysis.'); }
|
|
2383
|
-
else {
|
|
2384
|
-
const maxOut = Math.max(...rows.map(r => r.fan_out), 1);
|
|
2385
|
-
const lines = rows.map(r => { const mi = Math.max(0, 100 - (r.fan_out / maxOut) * 60 - (r.fan_in > 10 ? 20 : 0)).toFixed(0); return ` ${parseInt(mi) >= 70 ? '✓' : parseInt(mi) >= 40 ? '⚠' : '✗'} MI:${mi.padStart(3)} out:${String(r.fan_out).padStart(4)} in:${String(r.fan_in).padStart(4)} ${r.name}`; });
|
|
2386
|
-
ok(`Maintainability Index (estimated from fan-out/fan-in):\n${'─'.repeat(60)}\n${lines.join('\n')}`);
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
} else if (tool === 'monograph_complexity' || tool === 'monograph_crap_score') {
|
|
2390
|
-
const limit = input.limit || 25;
|
|
2391
|
-
const rows = db2.prepare(`SELECT n.name, n.label, n.file_path, (SELECT COUNT(*) FROM edges WHERE source_id = n.id) as out_deg FROM nodes n WHERE n.label IN ('Function','Method','Class') ORDER BY out_deg DESC LIMIT ?`).all(limit);
|
|
2392
|
-
if (!rows.length) { ok('No function/method nodes found. Build the graph first.'); }
|
|
2393
|
-
else {
|
|
2394
|
-
const isCrap = tool === 'monograph_crap_score';
|
|
2395
|
-
const header = isCrap ? 'CRAP Score proxy (degree² — lower is better)' : 'Complexity by Out-Degree';
|
|
2396
|
-
ok(`${header}:\n${'─'.repeat(50)}\n${rows.map(r => ` ${r.name} (${r.label}) ${isCrap ? 'CRAP' : 'complexity'}: ${isCrap ? Math.pow(r.out_deg,2) : r.out_deg}\n ${r.file_path || '?'}`).join('\n')}`);
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
} else if (tool === 'monograph_risk_profile') {
|
|
2400
|
-
const n = countNodes(db2), e = countEdges(db2);
|
|
2401
|
-
const dead = db2.prepare("SELECT COUNT(*) as c FROM nodes n LEFT JOIN edges e ON e.target_id = n.id WHERE e.target_id IS NULL AND n.label IN ('Function','Class','Method')").get().c;
|
|
2402
|
-
const hubs = db2.prepare('SELECT COUNT(*) as c FROM (SELECT source_id FROM edges GROUP BY source_id HAVING COUNT(*) > 15)').get().c;
|
|
2403
|
-
const files = db2.prepare("SELECT COUNT(*) as c FROM nodes WHERE label = 'File'").get().c;
|
|
2404
|
-
const orphans = db2.prepare("SELECT COUNT(*) as c FROM nodes n LEFT JOIN edges e ON e.target_id = n.id WHERE e.target_id IS NULL AND n.label = 'File'").get().c;
|
|
2405
|
-
const risks = [];
|
|
2406
|
-
if (dead > 10) risks.push(` HIGH Dead symbols: ${dead} unreferenced nodes`);
|
|
2407
|
-
if (hubs > 3) risks.push(` MEDIUM Hub nodes: ${hubs} nodes with >15 dependencies`);
|
|
2408
|
-
if (orphans > files * 0.3) risks.push(` MEDIUM Orphan files: ${orphans} of ${files} files unreachable`);
|
|
2409
|
-
if (n > 0 && e / n < 0.5) risks.push(` LOW Sparse graph: avg degree ${(e/n).toFixed(2)}`);
|
|
2410
|
-
ok(`Risk Profile — ${new Date().toISOString().split('T')[0]}\n${'─'.repeat(50)}\n${risks.length ? risks.join('\n') : ' No significant risks detected.'}\n\nSummary: ${n} nodes · ${e} edges · ${files} files`);
|
|
2411
|
-
|
|
2412
|
-
} else if (tool === 'monograph_author_analytics') {
|
|
2413
|
-
const limit = input.limit || 20;
|
|
2414
|
-
const { execSync: execS } = await import('child_process');
|
|
2415
|
-
try {
|
|
2416
|
-
const log = execS(`git log --format="%ae" --no-merges -- . 2>/dev/null | sort | uniq -c | sort -rn | head -${limit}`, { cwd: d2, encoding: 'utf-8', timeout: 5000 });
|
|
2417
|
-
if (!log.trim()) { ok('No git history found for this project directory.'); }
|
|
2418
|
-
else { ok(`Author Analytics (by commit count):\n${'─'.repeat(50)}\n${log.trim().split('\n').map(l => { const m = l.trim().match(/^(\d+)\s+(.+)$/); return m ? ` ${m[2].padEnd(45)} ${m[1]} commits` : l; }).join('\n')}`); }
|
|
2419
|
-
} catch { ok('Author analytics requires git. Ensure this directory is a git repository.'); }
|
|
2420
|
-
|
|
2421
|
-
} else if (tool === 'monograph_reachability') {
|
|
2422
|
-
// Files with no inbound edges (nothing imports them)
|
|
2423
|
-
const allNodes = db2.prepare(`SELECT id, name, file_path FROM nodes WHERE label IN ('File','Module') LIMIT 5000`).all();
|
|
2424
|
-
const inboundSet = new Set(db2.prepare(`SELECT DISTINCT target_id FROM edges`).all().map(r => r.target_id));
|
|
2425
|
-
const unreachable = allNodes.filter(n => !inboundSet.has(n.id)).slice(0, 40);
|
|
2426
|
-
const outdeg = db2.prepare(`SELECT source_id, COUNT(*) as c FROM edges GROUP BY source_id`);
|
|
2427
|
-
const degMap = {};
|
|
2428
|
-
for (const r of outdeg.all()) degMap[r.source_id] = r.c;
|
|
2429
|
-
if (!unreachable.length) { ok('All files have at least one inbound reference.'); }
|
|
2430
|
-
else ok(`Unreachable Files (${unreachable.length} of ${allNodes.length} total):\n${'─'.repeat(50)}\n${unreachable.slice(0,30).map(n => ` ${n.name || n.id.split('/').pop()} (imports ${degMap[n.id]||0} others)\n ${n.file_path||''}`).join('\n\n')}`);
|
|
2431
|
-
|
|
2432
|
-
} else if (tool === 'monograph_vital_signs_snapshot') {
|
|
2433
|
-
// Same as health_score — kept for backward compatibility
|
|
2434
|
-
const n = db2.prepare('SELECT COUNT(*) as c FROM nodes').get().c;
|
|
2435
|
-
const e = db2.prepare('SELECT COUNT(*) as c FROM edges').get().c;
|
|
2436
|
-
const dead = db2.prepare(`SELECT COUNT(*) as c FROM nodes n WHERE NOT EXISTS (SELECT 1 FROM edges WHERE source_id=n.id OR target_id=n.id)`).get().c;
|
|
2437
|
-
const hubs = db2.prepare(`SELECT COUNT(*) as c FROM (SELECT source_id FROM edges GROUP BY source_id HAVING COUNT(*)>20)`).get().c;
|
|
2438
|
-
const density = n > 1 ? (e / (n * (n-1))).toFixed(6) : '0';
|
|
2439
|
-
const score = Math.max(0, Math.min(100, Math.round(100 - (dead/Math.max(n,1)*30) - (hubs/Math.max(n,1)*500))));
|
|
2440
|
-
ok(`Vital Signs — ${new Date().toISOString()}\n${'─'.repeat(50)}\n Health Score: ${score}/100 ${score>=80?'✓ OK':score>=60?'⚠ Warning':'✗ Critical'}\n Nodes: ${n}\n Edges: ${e}\n Density: ${density}\n Dead symbols: ${dead} (${(dead/Math.max(n,1)*100).toFixed(1)}%)\n Hub nodes: ${hubs} nodes with >20 edges`);
|
|
2441
|
-
|
|
2442
|
-
} else if (tool === 'monograph_circular_deps') {
|
|
2443
|
-
// Find import cycles using iterative DFS
|
|
2444
|
-
const limit = Math.min(parseInt(input.limit||'10'), 20);
|
|
2445
|
-
const importEdges = db2.prepare(`SELECT source_id, target_id FROM edges WHERE relation IN ('IMPORTS','REQUIRES','USES','DEPENDS_ON') LIMIT 50000`).all();
|
|
2446
|
-
const adj = {};
|
|
2447
|
-
for (const e of importEdges) { (adj[e.source_id] = adj[e.source_id]||[]).push(e.target_id); }
|
|
2448
|
-
const cycles = [];
|
|
2449
|
-
const visited = new Set(), inStack = new Set();
|
|
2450
|
-
function dfs(node, path) {
|
|
2451
|
-
if (cycles.length >= limit) return;
|
|
2452
|
-
if (inStack.has(node)) {
|
|
2453
|
-
const cycleStart = path.indexOf(node);
|
|
2454
|
-
if (cycleStart >= 0) cycles.push(path.slice(cycleStart).concat(node));
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
if (visited.has(node)) return;
|
|
2458
|
-
visited.add(node); inStack.add(node); path.push(node);
|
|
2459
|
-
for (const nb of (adj[node]||[])) dfs(nb, path);
|
|
2460
|
-
path.pop(); inStack.delete(node);
|
|
2461
|
-
}
|
|
2462
|
-
for (const node of Object.keys(adj).slice(0, 2000)) dfs(node, []);
|
|
2463
|
-
const getName = id => id.split('/').slice(-2).join('/');
|
|
2464
|
-
if (!cycles.length) ok(`No circular dependencies found among ${Object.keys(adj).length} nodes with import edges.`);
|
|
2465
|
-
else ok(`Circular Dependencies (${cycles.length} found):\n${'─'.repeat(50)}\n${cycles.slice(0,limit).map((c,i) => ` ${i+1}. ${c.map(getName).join(' → ')}`).join('\n')}`);
|
|
2466
|
-
|
|
2467
|
-
} else if (tool === 'monograph_largest_files') {
|
|
2468
|
-
const limit2 = Math.min(parseInt(input.limit||'25'), 50);
|
|
2469
|
-
const rows = db2.prepare(`SELECT file_path, MAX(end_line) as lines, COUNT(*) as symbols FROM nodes WHERE file_path IS NOT NULL AND end_line IS NOT NULL AND end_line > 0 GROUP BY file_path ORDER BY lines DESC LIMIT ${limit2}`).all();
|
|
2470
|
-
if (!rows.length) ok('No line-count data available. Ensure the index was built with source parsing enabled.');
|
|
2471
|
-
else ok(`Largest Files by Line Count:\n${'─'.repeat(50)}\n${rows.map((r,i) => ` ${String(i+1).padStart(2)}. ${r.lines.toString().padStart(5)} lines ${r.symbols} symbols ${r.file_path.split('/').slice(-2).join('/')}`).join('\n')}`);
|
|
2472
|
-
|
|
2473
|
-
} else if (tool === 'monograph_coupling_balance') {
|
|
2474
|
-
// Fan-out (what this file uses) vs Fan-in (what uses this file)
|
|
2475
|
-
const limit3 = Math.min(parseInt(input.limit||'20'), 40);
|
|
2476
|
-
const fanOut = db2.prepare(`SELECT source_id, COUNT(*) as c FROM edges GROUP BY source_id`).all();
|
|
2477
|
-
const fanIn = db2.prepare(`SELECT target_id, COUNT(*) as c FROM edges GROUP BY target_id`).all();
|
|
2478
|
-
const outMap = {}, inMap = {};
|
|
2479
|
-
for (const r of fanOut) outMap[r.source_id] = r.c;
|
|
2480
|
-
for (const r of fanIn) inMap[r.target_id] = r.c;
|
|
2481
|
-
const allIds = new Set([...Object.keys(outMap), ...Object.keys(inMap)]);
|
|
2482
|
-
const nodes3 = db2.prepare(`SELECT id, name, file_path FROM nodes WHERE label='File' LIMIT 10000`).all();
|
|
2483
|
-
const fileSet = new Set(nodes3.map(n => n.id));
|
|
2484
|
-
const entries = [...allIds].filter(id => fileSet.has(id)).map(id => {
|
|
2485
|
-
const o = outMap[id]||0, i = inMap[id]||0;
|
|
2486
|
-
const n = nodes3.find(x=>x.id===id);
|
|
2487
|
-
return { name: n?.name || id.split('/').pop(), path: n?.file_path||'', out: o, inn: i, ratio: i > 0 ? (o/i).toFixed(1) : '∞' };
|
|
2488
|
-
}).filter(x => x.out > 0 || x.inn > 0).sort((a,b) => (b.out+b.inn) - (a.out+a.inn)).slice(0, limit3);
|
|
2489
|
-
ok(`Coupling Balance (Fan-out vs Fan-in, top ${limit3} by activity):\n${'─'.repeat(60)}\n ${'File'.padEnd(35)} Out In Ratio\n${'─'.repeat(60)}\n${entries.map(e => ` ${e.name.slice(0,35).padEnd(35)} ${String(e.out).padStart(3)} ${String(e.inn).padStart(2)} ${e.ratio}`).join('\n')}`);
|
|
2490
|
-
|
|
2491
|
-
} else if (tool === 'monograph_dead_exports') {
|
|
2492
|
-
// Exported symbols with zero inbound edges
|
|
2493
|
-
const exported = db2.prepare(`SELECT id, name, label, file_path FROM nodes WHERE is_exported=1 LIMIT 10000`).all();
|
|
2494
|
-
const inbound = new Set(db2.prepare(`SELECT DISTINCT target_id FROM edges`).all().map(r => r.target_id));
|
|
2495
|
-
const dead2 = exported.filter(n => !inbound.has(n.id));
|
|
2496
|
-
if (!dead2.length) ok('No dead exports found — all exported symbols have at least one inbound reference.');
|
|
2497
|
-
else ok(`Dead Exports — exported but never imported (${dead2.length} of ${exported.length} exported symbols):\n${'─'.repeat(50)}\n${dead2.slice(0,30).map(n => ` ${n.label.padEnd(12)} ${n.name} → ${(n.file_path||'').split('/').slice(-2).join('/')}`).join('\n')}`);
|
|
2498
|
-
|
|
2499
|
-
} else if (tool === 'monograph_language_breakdown') {
|
|
2500
|
-
const rows2 = db2.prepare(`SELECT language, COUNT(*) as c FROM nodes WHERE language IS NOT NULL AND language != '' GROUP BY language ORDER BY c DESC`).all();
|
|
2501
|
-
if (!rows2.length) ok('No language metadata available in this graph index.');
|
|
2502
|
-
else {
|
|
2503
|
-
const total2 = rows2.reduce((s,r) => s+r.c, 0);
|
|
2504
|
-
const maxC = rows2[0].c;
|
|
2505
|
-
ok(`Language Breakdown:\n${'─'.repeat(50)}\n${rows2.map(r => { const bar = '█'.repeat(Math.round(r.c/maxC*20)); const pct = (r.c/total2*100).toFixed(1); return ` ${r.language.padEnd(15)} ${bar.padEnd(20)} ${String(r.c).padStart(6)} (${pct}%)`; }).join('\n')}\n\n Total nodes: ${total2}`);
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
} else if (tool === 'monograph_instability') {
|
|
2509
|
-
// Robert Martin's Instability = Ce / (Ca + Ce)
|
|
2510
|
-
// Ca = afferent coupling (in-degree), Ce = efferent coupling (out-degree)
|
|
2511
|
-
const limit4 = Math.min(parseInt(input.limit||'25'), 50);
|
|
2512
|
-
const outRows = db2.prepare(`SELECT source_id, COUNT(*) as c FROM edges GROUP BY source_id`).all();
|
|
2513
|
-
const inRows = db2.prepare(`SELECT target_id, COUNT(*) as c FROM edges GROUP BY target_id`).all();
|
|
2514
|
-
const Ce = {}, Ca = {};
|
|
2515
|
-
for (const r of outRows) Ce[r.source_id] = r.c;
|
|
2516
|
-
for (const r of inRows) Ca[r.target_id] = r.c;
|
|
2517
|
-
const fileNodes = db2.prepare(`SELECT id, name, file_path FROM nodes WHERE label='File' LIMIT 10000`).all();
|
|
2518
|
-
const entries4 = fileNodes.map(n => {
|
|
2519
|
-
const ca = Ca[n.id]||0, ce = Ce[n.id]||0;
|
|
2520
|
-
const total = ca + ce;
|
|
2521
|
-
const inst = total > 0 ? ce / total : 0;
|
|
2522
|
-
return { name: n.name||n.id.split('/').pop(), ca, ce, inst };
|
|
2523
|
-
}).filter(x => x.ca+x.ce > 0).sort((a,b) => b.inst - a.inst);
|
|
2524
|
-
const risky = entries4.filter(x => x.inst > 0.7 && x.ca > 3);
|
|
2525
|
-
const stable = entries4.filter(x => x.inst < 0.2 && x.ce > 3);
|
|
2526
|
-
ok(`Instability Index (Ce÷(Ca+Ce), 0=stable 1=unstable):\n${'─'.repeat(60)}\n\n ⚠ High instability + high dependents (blast radius risk):\n${risky.slice(0,10).map(x => ` ${x.name.slice(0,40).padEnd(40)} I=${x.inst.toFixed(2)} Ca=${x.ca} Ce=${x.ce}`).join('\n')||' none'}\n\n ✓ Stable (low instability, many dependents on them):\n${stable.slice(0,8).map(x => ` ${x.name.slice(0,40).padEnd(40)} I=${x.inst.toFixed(2)} Ca=${x.ca} Ce=${x.ce}`).join('\n')||' none'}\n\n Total files analyzed: ${entries4.length}`);
|
|
2527
|
-
|
|
2528
|
-
} else if (tool === 'monograph_churn_hotspots') {
|
|
2529
|
-
// Combines git churn frequency with structural complexity (out-degree)
|
|
2530
|
-
const limit5 = Math.min(parseInt(input.limit||'15'), 30);
|
|
2531
|
-
const { execSync: execS2 } = await import('child_process');
|
|
2532
|
-
let churnMap = {};
|
|
2533
|
-
try {
|
|
2534
|
-
const since = input.since || '6 months ago';
|
|
2535
|
-
const log2 = execS2(`git log --since="${since}" --name-only --format="" -- . 2>/dev/null | grep -v '^$' | sort | uniq -c | sort -rn | head -200`, { cwd: d2, encoding: 'utf-8', timeout: 8000 });
|
|
2536
|
-
for (const line of log2.trim().split('\n')) {
|
|
2537
|
-
const m = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
2538
|
-
if (m) churnMap[m[2]] = parseInt(m[1]);
|
|
2539
|
-
}
|
|
2540
|
-
} catch {}
|
|
2541
|
-
if (!Object.keys(churnMap).length) { ok('No git history found — churn analysis requires a git repository.'); }
|
|
2542
|
-
else {
|
|
2543
|
-
const outDeg = db2.prepare(`SELECT source_id, COUNT(*) as c FROM edges GROUP BY source_id`).all();
|
|
2544
|
-
const degMap2 = {};
|
|
2545
|
-
for (const r of outDeg) degMap2[r.source_id] = r.c;
|
|
2546
|
-
const fileNodes2 = db2.prepare(`SELECT id, name, file_path FROM nodes WHERE label='File' LIMIT 10000`).all();
|
|
2547
|
-
const maxChurn = Math.max(...Object.values(churnMap), 1);
|
|
2548
|
-
const maxDeg2 = Math.max(...Object.values(degMap2), 1);
|
|
2549
|
-
const scored = fileNodes2.map(n => {
|
|
2550
|
-
const fp = n.file_path || '';
|
|
2551
|
-
const churn = churnMap[fp] || Object.entries(churnMap).find(([k]) => fp.endsWith(k))?.[1] || 0;
|
|
2552
|
-
const deg = degMap2[n.id] || 0;
|
|
2553
|
-
const score2 = (churn/maxChurn * 0.6) + (deg/maxDeg2 * 0.4);
|
|
2554
|
-
return { name: n.name||fp.split('/').pop(), fp, churn, deg, score: score2 };
|
|
2555
|
-
}).filter(x => x.churn > 0 || x.deg > 5).sort((a,b) => b.score - a.score).slice(0, limit5);
|
|
2556
|
-
if (!scored.length) ok('No files matched both churn and complexity criteria.');
|
|
2557
|
-
else ok(`Churn × Complexity Hotspots (60% churn weight + 40% coupling weight):\n${'─'.repeat(60)}\n ${'File'.padEnd(38)} Churn Deps Score\n${'─'.repeat(60)}\n${scored.map(x => ` ${x.name.slice(0,38).padEnd(38)} ${String(x.churn).padStart(5)} ${String(x.deg).padStart(4)} ${(x.score*100).toFixed(0)}%`).join('\n')}\n\n Analyzed: ${scored.length} hotspot candidates from last ${input.since||'6 months'}`);
|
|
2558
|
-
}
|
|
2559
|
-
|
|
2560
|
-
} else {
|
|
2561
|
-
ok(`Tool "${tool}" not implemented in control panel`);
|
|
2562
|
-
}
|
|
2563
|
-
} finally { closeDb(db2); }
|
|
2564
|
-
} catch(e2) { err(String(e2)); }
|
|
2565
|
-
});
|
|
2566
|
-
return;
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
// -------------------------------------------------- GET /api/monograph-benchmark
|
|
2570
|
-
if (req.method === 'GET' && url === '/api/monograph-benchmark') {
|
|
2571
|
-
try {
|
|
2572
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2573
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2574
|
-
const d = path.resolve(dir || process.cwd());
|
|
2575
|
-
const graphPath = path.join(d, '.monomind', 'graph', 'graph.json');
|
|
2576
|
-
const legacyPath = path.join(d, 'graphify-out', 'graph.json');
|
|
2577
|
-
const gp = fs.existsSync(graphPath) ? graphPath : (fs.existsSync(legacyPath) ? legacyPath : null);
|
|
2578
|
-
|
|
2579
|
-
if (!gp) {
|
|
2580
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2581
|
-
res.end(JSON.stringify({ available: false }));
|
|
2582
|
-
return;
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
const { execSync: ex } = await import('child_process');
|
|
2586
|
-
const out = ex(`graphify benchmark ${gp}`, { encoding: 'utf8', cwd: d, timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
2587
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2588
|
-
res.end(JSON.stringify({ available: true, result: out }));
|
|
2589
|
-
} catch (err) {
|
|
2590
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2591
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2592
|
-
}
|
|
2593
|
-
return;
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
// ------------------------------------------------------- GET /api/graph
|
|
2598
|
-
if (req.method === 'GET' && url === '/api/graph') {
|
|
2599
|
-
try {
|
|
2600
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2601
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2602
|
-
const d = path.resolve(dir || process.cwd());
|
|
2603
|
-
|
|
2604
|
-
// Find session files — sort by mtime descending before processing
|
|
2605
|
-
const homeDir = os.homedir();
|
|
2606
|
-
const slug = d.replace(/\//g, '-');
|
|
2607
|
-
const sessionsDir = fs.existsSync(path.join(homeDir, '.claude', 'projects', slug))
|
|
2608
|
-
? path.join(homeDir, '.claude', 'projects', slug)
|
|
2609
|
-
: path.join(d, '.claude', 'sessions');
|
|
2610
|
-
|
|
2611
|
-
let sessionFiles = [];
|
|
2612
|
-
try {
|
|
2613
|
-
sessionFiles = fs.readdirSync(sessionsDir)
|
|
2614
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
2615
|
-
.map(f => ({ f, mtime: (() => { try { return fs.statSync(path.join(sessionsDir, f)).mtimeMs; } catch { return 0; } })() }))
|
|
2616
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
2617
|
-
.map(({ f }) => f);
|
|
2618
|
-
} catch {}
|
|
2619
|
-
|
|
2620
|
-
// Parse each session: count tool categories + agent type spawns
|
|
2621
|
-
const TOOL_CAT = name => {
|
|
2622
|
-
if (['Read','Write','Edit','MultiEdit','Glob','Grep','LS'].includes(name)) return 'file';
|
|
2623
|
-
if (name === 'Bash') return 'bash';
|
|
2624
|
-
if (['Agent','Task'].includes(name)) return 'agent';
|
|
2625
|
-
if (name.startsWith('mcp__monobrain__memory') || name.startsWith('mcp__monobrain__agentdb')) return 'memory';
|
|
2626
|
-
if (['WebFetch','WebSearch'].includes(name)) return 'web';
|
|
2627
|
-
if (name === 'Skill') return 'skill';
|
|
2628
|
-
return 'other';
|
|
2629
|
-
};
|
|
2630
|
-
|
|
2631
|
-
const nodes = [];
|
|
2632
|
-
const edges = [];
|
|
2633
|
-
const agentTypeNodes = {}; // subagent_type → node id
|
|
2634
|
-
|
|
2635
|
-
for (const fname of sessionFiles) {
|
|
2636
|
-
const sid = fname.replace('.jsonl','');
|
|
2637
|
-
const fp = path.join(sessionsDir, fname);
|
|
2638
|
-
let stat = null;
|
|
2639
|
-
try { stat = fs.statSync(fp); } catch { continue; }
|
|
2640
|
-
|
|
2641
|
-
// Skip files over size cap to avoid memory spikes on large sessions
|
|
2642
|
-
if (stat.size > JSONL_SIZE_CAP) {
|
|
2643
|
-
nodes.push({ id: sid, type: 'session', label: sid.slice(0,8), turns: 0, totalTools: 0,
|
|
2644
|
-
toolCounts: {}, cost: 0, mtime: stat.mtimeMs, size: stat.size, agentSpawns: {}, truncated: true });
|
|
2645
|
-
continue;
|
|
2646
|
-
}
|
|
2647
|
-
|
|
2648
|
-
const toolCounts = {};
|
|
2649
|
-
const agentSpawns = {}; // subagent_type → count
|
|
2650
|
-
let turns = 0, totalCost = 0;
|
|
2651
|
-
|
|
2652
|
-
try {
|
|
2653
|
-
const raw = fs.readFileSync(fp, 'utf8').replace(/\r\n/g, '\n');
|
|
2654
|
-
const lines = raw.split('\n').filter(Boolean);
|
|
2655
|
-
for (const line of lines) {
|
|
2656
|
-
let e; try { e = JSON.parse(line); } catch { continue; }
|
|
2657
|
-
if (e.type === 'user') turns++;
|
|
2658
|
-
if (e.type === 'assistant') {
|
|
2659
|
-
for (const block of (e.message?.content || [])) {
|
|
2660
|
-
if (!block || block.type !== 'tool_use') continue;
|
|
2661
|
-
const cat = TOOL_CAT(block.name);
|
|
2662
|
-
toolCounts[cat] = (toolCounts[cat] || 0) + 1;
|
|
2663
|
-
if (cat === 'agent') {
|
|
2664
|
-
const sub = block.input?.subagent_type || block.input?.description || '?';
|
|
2665
|
-
agentSpawns[sub] = (agentSpawns[sub] || 0) + 1;
|
|
2666
|
-
}
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
if (e.costUSD) totalCost += e.costUSD;
|
|
2670
|
-
}
|
|
2671
|
-
} catch {}
|
|
2672
|
-
|
|
2673
|
-
const totalTools = Object.values(toolCounts).reduce((a,b)=>a+b,0);
|
|
2674
|
-
nodes.push({
|
|
2675
|
-
id: sid, type: 'session', label: sid.slice(0,8),
|
|
2676
|
-
turns, totalTools, toolCounts,
|
|
2677
|
-
cost: totalCost, mtime: stat.mtimeMs, size: stat.size,
|
|
2678
|
-
agentSpawns
|
|
2679
|
-
});
|
|
2680
|
-
|
|
2681
|
-
// Create/link agent type nodes
|
|
2682
|
-
for (const [subType, count] of Object.entries(agentSpawns)) {
|
|
2683
|
-
const nodeId = 'agent::' + subType;
|
|
2684
|
-
if (!agentTypeNodes[subType]) {
|
|
2685
|
-
agentTypeNodes[subType] = true;
|
|
2686
|
-
nodes.push({ id: nodeId, type: 'agenttype', label: subType, totalSpawns: 0 });
|
|
2687
|
-
}
|
|
2688
|
-
const aNode = nodes.find(n => n.id === nodeId);
|
|
2689
|
-
if (aNode) aNode.totalSpawns = (aNode.totalSpawns || 0) + count;
|
|
2690
|
-
edges.push({ source: sid, target: nodeId, weight: count, label: String(count) });
|
|
2691
|
-
}
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
2695
|
-
res.end(JSON.stringify({ nodes, edges }));
|
|
2696
|
-
} catch (err) {
|
|
2697
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2698
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2699
|
-
}
|
|
2700
|
-
return;
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
// ------------------------------------------------- GET /api/swarm-history
|
|
2704
|
-
if (req.method === 'GET' && url === '/api/swarm-history') {
|
|
2705
|
-
try {
|
|
2706
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2707
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2708
|
-
const entries = collectSwarmHistory(path.resolve(dir));
|
|
2709
|
-
res.writeHead(200, {
|
|
2710
|
-
'Content-Type': 'application/json',
|
|
2711
|
-
'Access-Control-Allow-Origin': '*',
|
|
2712
|
-
'Cache-Control': 'no-cache',
|
|
2713
|
-
});
|
|
2714
|
-
res.end(JSON.stringify({ entries }));
|
|
2715
|
-
} catch (err) {
|
|
2716
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2717
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2718
|
-
}
|
|
2719
|
-
return;
|
|
2720
|
-
}
|
|
2721
|
-
|
|
2722
|
-
// ------------------------------------------------- GET /api/swarm-events
|
|
2723
|
-
if (req.method === 'GET' && url === '/api/swarm-events') {
|
|
2724
|
-
try {
|
|
2725
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2726
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2727
|
-
const swarmId = qs.get('swarmId') || undefined;
|
|
2728
|
-
const agentId = qs.get('agentId') || undefined;
|
|
2729
|
-
const last = qs.get('last') ? parseInt(qs.get('last')) : undefined;
|
|
2730
|
-
const events = collectSwarmEvents(path.resolve(dir), { swarmId, agentId, last });
|
|
2731
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
2732
|
-
res.end(JSON.stringify({ events, count: events.length }));
|
|
2733
|
-
} catch (err) {
|
|
2734
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2735
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2736
|
-
}
|
|
2737
|
-
return;
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
// ------------------------------------------------- GET /api/swarm-data-size
|
|
2741
|
-
if (req.method === 'GET' && url === '/api/swarm-data-size') {
|
|
2742
|
-
try {
|
|
2743
|
-
const dir = new URL(req.url, 'http://localhost').searchParams.get('dir') || projectDir || process.cwd();
|
|
2744
|
-
const size = getSwarmDataSize(path.resolve(dir));
|
|
2745
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2746
|
-
res.end(JSON.stringify(size));
|
|
2747
|
-
} catch (err) {
|
|
2748
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2749
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2750
|
-
}
|
|
2751
|
-
return;
|
|
2752
|
-
}
|
|
2753
|
-
|
|
2754
|
-
// ------------------------------------------------- DELETE /api/swarm-clean
|
|
2755
|
-
if (req.method === 'DELETE' && url === '/api/swarm-clean') {
|
|
2756
|
-
try {
|
|
2757
|
-
const dir = new URL(req.url, 'http://localhost').searchParams.get('dir') || projectDir || process.cwd();
|
|
2758
|
-
const result = cleanSwarmData(path.resolve(dir));
|
|
2759
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
2760
|
-
res.end(JSON.stringify({ success: true, ...result }));
|
|
2761
|
-
} catch (err) {
|
|
2762
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2763
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2764
|
-
}
|
|
2765
|
-
return;
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
// -------------------------------------------------- GET /api/token-usage
|
|
2769
|
-
if (req.method === 'GET' && url.startsWith('/api/token-usage')) {
|
|
2770
|
-
try {
|
|
2771
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2772
|
-
const period = ['today','week','30days','month'].includes(qs.get('period')) ? qs.get('period') : 'today';
|
|
2773
|
-
const dir = path.resolve(qs.get('dir') || projectDir || process.cwd());
|
|
2774
|
-
const trackerPath = path.join(dir, '.claude', 'helpers', 'token-tracker.cjs');
|
|
2775
|
-
const fallback = () => {
|
|
2776
|
-
const summary = (() => { try { return JSON.parse(fs.readFileSync(path.join(dir, '.monomind', 'metrics', 'token-summary.json'), 'utf8')); } catch { return {}; } })();
|
|
2777
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
2778
|
-
res.end(JSON.stringify({ totalCost: summary.todayCost || 0, totalCalls: summary.todayCalls || 0, totalIn: 0, totalOut: 0, totalCR: 0, totalCW: 0, projects: [], modelBreakdown: {}, categoryBreakdown: {}, toolBreakdown: {}, mcpBreakdown: {}, periodLabel: period }));
|
|
2779
|
-
};
|
|
2780
|
-
if (!fs.existsSync(trackerPath)) { fallback(); return; }
|
|
2781
|
-
try {
|
|
2782
|
-
const _req = createRequire(import.meta.url);
|
|
2783
|
-
const tracker = _req(trackerPath);
|
|
2784
|
-
const range = tracker.getDateRange(period);
|
|
2785
|
-
const projects = tracker.parseAllSessions(range.start, range.end);
|
|
2786
|
-
let totalCost = 0, totalIn = 0, totalOut = 0, totalCR = 0, totalCW = 0, totalCalls = 0;
|
|
2787
|
-
const modelBreakdown = {}, categoryBreakdown = {}, toolBreakdown = {}, mcpBreakdown = {};
|
|
2788
|
-
for (const p of projects) {
|
|
2789
|
-
totalCost += p.totalCost || 0;
|
|
2790
|
-
for (const s of (p.sessions || [])) {
|
|
2791
|
-
totalIn += s.totalInputTokens || 0;
|
|
2792
|
-
totalOut += s.totalOutputTokens || 0;
|
|
2793
|
-
totalCR += s.totalCacheRead || 0;
|
|
2794
|
-
totalCW += s.totalCacheWrite || 0;
|
|
2795
|
-
totalCalls += s.apiCalls || 0;
|
|
2796
|
-
for (const [mn, m] of Object.entries(s.modelBreakdown || {})) {
|
|
2797
|
-
if (!modelBreakdown[mn]) modelBreakdown[mn] = { calls: 0, cost: 0, tokens: 0 };
|
|
2798
|
-
modelBreakdown[mn].calls += m.calls || 0;
|
|
2799
|
-
modelBreakdown[mn].cost += m.cost || 0;
|
|
2800
|
-
modelBreakdown[mn].tokens += m.tokens || 0;
|
|
2801
|
-
}
|
|
2802
|
-
for (const [cat, c] of Object.entries(s.categoryBreakdown || {})) {
|
|
2803
|
-
if (!categoryBreakdown[cat]) categoryBreakdown[cat] = { turns: 0, cost: 0 };
|
|
2804
|
-
categoryBreakdown[cat].turns += c.turns || 0;
|
|
2805
|
-
categoryBreakdown[cat].cost += c.cost || 0;
|
|
2806
|
-
}
|
|
2807
|
-
for (const [tool, t] of Object.entries(s.toolBreakdown || {})) {
|
|
2808
|
-
if (!toolBreakdown[tool]) toolBreakdown[tool] = { calls: 0 };
|
|
2809
|
-
toolBreakdown[tool].calls += t.calls || 0;
|
|
2810
|
-
}
|
|
2811
|
-
for (const [srv, m] of Object.entries(s.mcpBreakdown || {})) {
|
|
2812
|
-
if (!mcpBreakdown[srv]) mcpBreakdown[srv] = { calls: 0 };
|
|
2813
|
-
mcpBreakdown[srv].calls += m.calls || 0;
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
}
|
|
2817
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
2818
|
-
res.end(JSON.stringify({ totalCost, totalCalls, totalIn, totalOut, totalCR, totalCW, projects, modelBreakdown, categoryBreakdown, toolBreakdown, mcpBreakdown, periodLabel: period }));
|
|
2819
|
-
} catch (e) { fallback(); }
|
|
2820
|
-
} catch (err) {
|
|
2821
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2822
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2823
|
-
}
|
|
2824
|
-
return;
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
|
-
// ------------------------------------------------------- GET /api/section
|
|
2828
|
-
if (req.method === 'GET' && url === '/api/section') {
|
|
2829
|
-
try {
|
|
2830
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
2831
|
-
const name = qs.get('name') || '';
|
|
2832
|
-
const dir = qs.get('dir') || projectDir || process.cwd();
|
|
2833
|
-
const full = qs.get('full') === '1';
|
|
2834
|
-
let partial = buildSectionData(name, dir || process.cwd());
|
|
2835
|
-
// For full knowledge request, include all chunks
|
|
2836
|
-
if (name === 'knowledge' && full) {
|
|
2837
|
-
const chunksPath = path.join(path.resolve(dir || process.cwd()), '.monomind', 'knowledge', 'chunks.jsonl');
|
|
2838
|
-
let allChunks = [];
|
|
2839
|
-
try {
|
|
2840
|
-
const raw = fs.readFileSync(chunksPath, 'utf8');
|
|
2841
|
-
allChunks = raw.split('\n').filter(Boolean).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
2842
|
-
} catch {}
|
|
2843
|
-
partial = { knowledge: { ...partial.knowledge, allChunks } };
|
|
2844
|
-
}
|
|
2845
|
-
res.writeHead(200, {
|
|
2846
|
-
'Content-Type': 'application/json',
|
|
2847
|
-
'Access-Control-Allow-Origin': '*',
|
|
2848
|
-
'Cache-Control': 'no-cache',
|
|
2849
|
-
});
|
|
2850
|
-
res.end(JSON.stringify(partial));
|
|
2851
|
-
} catch (err) {
|
|
2852
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2853
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2854
|
-
}
|
|
2855
|
-
return;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
// ------------------------------------------------------- GET /api/stream
|
|
2859
|
-
if (req.method === 'GET' && url === '/api/stream') {
|
|
2860
|
-
res.writeHead(200, {
|
|
2861
|
-
'Content-Type': 'text/event-stream',
|
|
2862
|
-
'Cache-Control': 'no-cache',
|
|
2863
|
-
Connection: 'keep-alive',
|
|
2864
|
-
'Access-Control-Allow-Origin': '*',
|
|
2865
|
-
'X-Accel-Buffering': 'no',
|
|
2866
|
-
});
|
|
2867
|
-
|
|
2868
|
-
// Keep the connection alive with periodic comments
|
|
2869
|
-
const keepAlive = setInterval(() => {
|
|
2870
|
-
try {
|
|
2871
|
-
res.write(': ping\n\n');
|
|
2872
|
-
} catch {
|
|
2873
|
-
clearInterval(keepAlive);
|
|
2874
|
-
}
|
|
2875
|
-
}, 20_000);
|
|
2876
|
-
|
|
2877
|
-
sseClients.add(res);
|
|
2878
|
-
|
|
2879
|
-
req.on('close', () => {
|
|
2880
|
-
clearInterval(keepAlive);
|
|
2881
|
-
sseClients.delete(res);
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
// Send the initial snapshot immediately
|
|
2885
|
-
try {
|
|
2886
|
-
const snapshot = await collectAll(projectDir);
|
|
2887
|
-
res.write(`data: ${JSON.stringify(snapshot)}\n\n`);
|
|
2888
|
-
} catch (err) {
|
|
2889
|
-
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
2890
|
-
}
|
|
2891
|
-
return;
|
|
2892
|
-
}
|
|
2893
|
-
|
|
2894
|
-
// ---------------------------------------------------- GET /favicon.ico
|
|
2895
|
-
if (req.method === 'GET' && url === '/favicon.ico') {
|
|
2896
|
-
res.writeHead(204);
|
|
2897
|
-
res.end();
|
|
2898
|
-
return;
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
// ------------------------------------------------- Monograph
|
|
2902
|
-
// GET /api/monograph — node/edge counts, top god nodes, type distribution.
|
|
2903
|
-
// (Distinct from /api/graph which serves session/journal graph data.)
|
|
2904
|
-
// Reads .monomind/monograph.db via sqlite3 CLI to avoid bundling better-sqlite3.
|
|
2905
|
-
if (req.method === 'GET' && url === '/api/monograph') {
|
|
2906
|
-
try {
|
|
2907
|
-
const dbPath = path.join(projectDir || process.cwd(), '.monomind', 'monograph.db');
|
|
2908
|
-
if (!fs.existsSync(dbPath)) {
|
|
2909
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2910
|
-
res.end(JSON.stringify({ exists: false }));
|
|
2911
|
-
return;
|
|
2912
|
-
}
|
|
2913
|
-
const { execSync } = await import('child_process');
|
|
2914
|
-
// Pipe SQL via stdin to avoid shell quoting issues with single-quoted SQL strings.
|
|
2915
|
-
const runSql = (sql, timeout = 5000) => {
|
|
2916
|
-
try {
|
|
2917
|
-
return execSync(`sqlite3 -json "${dbPath}"`,
|
|
2918
|
-
{ encoding: 'utf-8', timeout: timeout, input: sql + ';' });
|
|
2919
|
-
} catch (e) { return '[]'; }
|
|
2920
|
-
};
|
|
2921
|
-
const counts = JSON.parse(runSql(
|
|
2922
|
-
"SELECT (SELECT COUNT(*) FROM nodes) AS nodes, (SELECT COUNT(*) FROM edges) AS edges;"
|
|
2923
|
-
) || '[{}]')[0] || { nodes: 0, edges: 0 };
|
|
2924
|
-
// Compute degree in one pass via GROUP BY (much faster than per-row subquery).
|
|
2925
|
-
const gods = JSON.parse(runSql(
|
|
2926
|
-
"WITH deg(node_id, d) AS (" +
|
|
2927
|
-
" SELECT source_id, COUNT(*) FROM edges GROUP BY source_id " +
|
|
2928
|
-
" UNION ALL " +
|
|
2929
|
-
" SELECT target_id, COUNT(*) FROM edges GROUP BY target_id" +
|
|
2930
|
-
"), totals AS (" +
|
|
2931
|
-
" SELECT node_id, SUM(d) AS deg FROM deg GROUP BY node_id" +
|
|
2932
|
-
") " +
|
|
2933
|
-
"SELECT n.name, n.label, n.file_path, t.deg " +
|
|
2934
|
-
"FROM nodes n JOIN totals t ON t.node_id = n.id " +
|
|
2935
|
-
"WHERE n.label NOT IN ('Concept') " +
|
|
2936
|
-
"AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
2937
|
-
"AND n.name NOT LIKE '(%' AND length(n.name) >= 3 " +
|
|
2938
|
-
"ORDER BY t.deg DESC LIMIT 20",
|
|
2939
|
-
10000
|
|
2940
|
-
) || '[]');
|
|
2941
|
-
const types = JSON.parse(runSql(
|
|
2942
|
-
"SELECT label, COUNT(*) AS count FROM nodes GROUP BY label ORDER BY count DESC LIMIT 12"
|
|
2943
|
-
) || '[]');
|
|
2944
|
-
const relations = JSON.parse(runSql(
|
|
2945
|
-
"SELECT relation, COUNT(*) AS count FROM edges GROUP BY relation ORDER BY count DESC"
|
|
2946
|
-
) || '[]');
|
|
2947
|
-
|
|
2948
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2949
|
-
res.end(JSON.stringify({
|
|
2950
|
-
exists: true,
|
|
2951
|
-
nodes: counts.nodes,
|
|
2952
|
-
edges: counts.edges,
|
|
2953
|
-
godNodes: gods,
|
|
2954
|
-
typeDistribution: types,
|
|
2955
|
-
relationDistribution: relations,
|
|
2956
|
-
updatedAt: fs.statSync(dbPath).mtime,
|
|
2957
|
-
}));
|
|
2958
|
-
} catch (err) {
|
|
2959
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2960
|
-
res.end(JSON.stringify({ error: String(err) }));
|
|
2961
|
-
}
|
|
2962
|
-
return;
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
|
-
// ------------------------------------------------- Org management
|
|
2966
|
-
// GET /api/orgs — list all saved org configs
|
|
2967
|
-
if (req.method === 'GET' && url === '/api/orgs') {
|
|
2968
|
-
try {
|
|
2969
|
-
const _orgsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
2970
|
-
const orgsDir = path.join(path.resolve(_orgsQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
|
|
2971
|
-
let orgs = [];
|
|
2972
|
-
if (fs.existsSync(orgsDir)) {
|
|
2973
|
-
const _sidecarSuffixRe = /-(approvals|state|activity|goals|routines|projects|members|issues|workspaces|worktrees|environments|plugins|adapters|bootstrap|threads|budgets|project-workspaces|approval-comments|secrets)\.json$/;
|
|
2974
|
-
const files = fs.readdirSync(orgsDir).filter(f => f.endsWith('.json') && !_sidecarSuffixRe.test(f));
|
|
2975
|
-
// Read events file once, outside the per-org loop
|
|
2976
|
-
let recentLines = [];
|
|
2977
|
-
try {
|
|
2978
|
-
const evFile = path.join(path.resolve(_orgsQs.get('dir') || projectDir || process.cwd()), 'data', 'mastermind-events.jsonl');
|
|
2979
|
-
if (fs.existsSync(evFile)) {
|
|
2980
|
-
// Read only the last 64 KB to bound cost on large files
|
|
2981
|
-
const stat = fs.statSync(evFile);
|
|
2982
|
-
const TAIL = 65536;
|
|
2983
|
-
const fd = fs.openSync(evFile, 'r');
|
|
2984
|
-
const buf = Buffer.alloc(Math.min(TAIL, stat.size));
|
|
2985
|
-
try {
|
|
2986
|
-
fs.readSync(fd, buf, 0, buf.length, Math.max(0, stat.size - buf.length));
|
|
2987
|
-
} finally {
|
|
2988
|
-
fs.closeSync(fd);
|
|
2989
|
-
}
|
|
2990
|
-
recentLines = buf.toString('utf8').split('\n').filter(Boolean).reverse();
|
|
2991
|
-
}
|
|
2992
|
-
} catch(_) {}
|
|
2993
|
-
for (const f of files) {
|
|
2994
|
-
try {
|
|
2995
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(orgsDir, f), 'utf8'));
|
|
2996
|
-
let running = false;
|
|
2997
|
-
const lastStart = recentLines.find(l => { try { const e = JSON.parse(l); return e.type === 'org:start' && e.org === cfg.name; } catch(_) { return false; } });
|
|
2998
|
-
const lastStop = recentLines.find(l => { try { const e = JSON.parse(l); return (e.type === 'org:stop' || e.type === 'org:complete') && e.org === cfg.name; } catch(_) { return false; } });
|
|
2999
|
-
if (lastStart) {
|
|
3000
|
-
const startTs = JSON.parse(lastStart).ts || 0;
|
|
3001
|
-
const stopTs = lastStop ? (JSON.parse(lastStop).ts || 0) : 0;
|
|
3002
|
-
running = startTs > stopTs;
|
|
3003
|
-
}
|
|
3004
|
-
orgs.push({ name: cfg.name, goal: cfg.goal, roles: cfg.roles?.length || 0, topology: cfg.topology, created_at: cfg.created_at, running, status: cfg.status, loop: cfg.loop ? { poll_interval_minutes: cfg.loop.poll_interval_minutes, last_run: cfg.loop.last_run, next_run: cfg.loop.next_run } : undefined });
|
|
3005
|
-
} catch(_) {}
|
|
3006
|
-
}
|
|
3007
|
-
}
|
|
3008
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3009
|
-
res.end(JSON.stringify(orgs));
|
|
3010
|
-
} catch(_) { res.writeHead(500); res.end('[]'); }
|
|
3011
|
-
return;
|
|
3012
|
-
}
|
|
3013
|
-
|
|
3014
|
-
// GET /api/orgs/:name — get specific org config (exact path: /api/orgs/<slug>)
|
|
3015
|
-
if (req.method === 'GET' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
|
|
3016
|
-
try {
|
|
3017
|
-
const orgName = decodeURIComponent(url.slice('/api/orgs/'.length));
|
|
3018
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3019
|
-
const f = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}.json`);
|
|
3020
|
-
if (!fs.existsSync(f)) { res.writeHead(404); res.end('{"error":"not found"}'); return; }
|
|
3021
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3022
|
-
res.end(fs.readFileSync(f, 'utf8'));
|
|
3023
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3024
|
-
return;
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
// GET /api/org/:name — ORG ROOM: rich org data (config + state + tasks + routines + goals)
|
|
3028
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
|
|
3029
|
-
try {
|
|
3030
|
-
const orgName = decodeURIComponent(url.slice('/api/org/'.length));
|
|
3031
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3032
|
-
const _orgQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3033
|
-
const d = path.resolve(_orgQs.get('dir') || projectDir || process.cwd());
|
|
3034
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
3035
|
-
|
|
3036
|
-
const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3037
|
-
|
|
3038
|
-
const configFile = path.join(orgsDir, `${orgName}.json`);
|
|
3039
|
-
if (!fs.existsSync(configFile)) { res.writeHead(404); res.end('{"error":"org not found"}'); return; }
|
|
3040
|
-
const config = readJsonSafe(configFile);
|
|
3041
|
-
|
|
3042
|
-
const state = readJsonSafe(path.join(orgsDir, `${orgName}-state.json`)) || { agents: {} };
|
|
3043
|
-
const goalsData = readJsonSafe(path.join(orgsDir, `${orgName}-goals.json`)) || { goals: [] };
|
|
3044
|
-
const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || { routines: [] };
|
|
3045
|
-
const approvalsData = readJsonSafe(path.join(orgsDir, `${orgName}-approvals.json`)) || { approvals: [] };
|
|
3046
|
-
|
|
3047
|
-
// Check running status from stop file absence + state
|
|
3048
|
-
const stopFile = path.join(orgsDir, '.stops', `${orgName}.stop`);
|
|
3049
|
-
const running = !fs.existsSync(stopFile) && Object.values(state.agents || {}).some(a => a.status === 'running');
|
|
3050
|
-
|
|
3051
|
-
const result = { config, state, goals: goalsData.goals, routines: routinesData.routines,
|
|
3052
|
-
approvals: approvalsData.approvals, running, tasks: { todo: [], doing: [], done: [] } };
|
|
3053
|
-
|
|
3054
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3055
|
-
res.end(JSON.stringify(result));
|
|
3056
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3057
|
-
return;
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
// GET /api/org/:name/activity — recent org events from mastermind-events.jsonl
|
|
3061
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/activity$/i.test(url)) {
|
|
3062
|
-
try {
|
|
3063
|
-
const parts = url.split('/');
|
|
3064
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3065
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('[]'); return; }
|
|
3066
|
-
const _actQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3067
|
-
const d = path.resolve(_actQs.get('dir') || projectDir || process.cwd());
|
|
3068
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
3069
|
-
const readJ = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3070
|
-
const events = [];
|
|
3071
|
-
|
|
3072
|
-
// 1) Global mastermind events that EXPLICITLY belong to this org (strict — no untagged leak)
|
|
3073
|
-
const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
|
|
3074
|
-
if (fs.existsSync(eventsFile)) {
|
|
3075
|
-
const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean);
|
|
3076
|
-
for (const l of lines.slice(-1000)) {
|
|
3077
|
-
try { const e = JSON.parse(l); if (e && e.org === orgName) events.push(e); } catch(_) {}
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
|
|
3081
|
-
// 2) Synthesize an org-scoped timeline from this org's own records (real data, distinct per org)
|
|
3082
|
-
const cfg = readJ(path.join(orgsDir, `${orgName}.json`));
|
|
3083
|
-
if (cfg) {
|
|
3084
|
-
const createdMs = cfg.created_at ? Date.parse(cfg.created_at) : null;
|
|
3085
|
-
if (createdMs) events.push({ type: 'org:create', ts: createdMs, msg: String(cfg.goal || 'Org created').slice(0, 80) });
|
|
3086
|
-
(cfg.roles || []).forEach((r, i) => {
|
|
3087
|
-
events.push({ type: 'role:defined', ts: createdMs ? createdMs + (i + 1) * 1000 : null, role: r.title || r.id, msg: r.agent_type || '' });
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
|
|
3091
|
-
(goals?.goals || []).forEach(g => events.push({ type: 'goal', ts: Date.parse(g.created_at || g.updated_at || '') || null, role: g.status || '', msg: String(g.text || g.title || g.goal || '').slice(0, 80) }));
|
|
3092
|
-
const appr = readJ(path.join(orgsDir, `${orgName}-approvals.json`));
|
|
3093
|
-
(appr?.approvals || []).forEach(a => { const ts = (typeof a.ts === 'number') ? a.ts : (Date.parse(a.created_at || a.ts || '') || null); events.push({ type: 'approval', ts, role: a.agent_id || a.requester || '', msg: String(a.title || a.action || '').slice(0, 80) }); });
|
|
3094
|
-
const state = readJ(path.join(orgsDir, `${orgName}-state.json`));
|
|
3095
|
-
if (state && state.agents) {
|
|
3096
|
-
for (const [aid, a] of Object.entries(state.agents)) {
|
|
3097
|
-
const raw = a.lastHeartbeat || a.last_seen || a.updated_at || null;
|
|
3098
|
-
const ts = (typeof raw === 'number') ? raw : (raw ? Date.parse(raw) : null);
|
|
3099
|
-
events.push({ type: 'org:heartbeat', ts, agent: aid, msg: a.status || '' });
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
const out = events.filter(e => e && e.ts).sort((a, b) => b.ts - a.ts).slice(0, 100);
|
|
3104
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3105
|
-
res.end(JSON.stringify(out));
|
|
3106
|
-
} catch(_) { res.writeHead(500); res.end('[]'); }
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
|
-
|
|
3110
|
-
// GET /api/org/:name/projects — org projects from projects json file
|
|
3111
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/projects$/i.test(url)) {
|
|
3112
|
-
try {
|
|
3113
|
-
const parts = url.split('/');
|
|
3114
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3115
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('[]'); return; }
|
|
3116
|
-
const d = projectDir || process.cwd();
|
|
3117
|
-
const projFile = path.join(d, '.monomind', 'orgs', `${orgName}-projects.json`);
|
|
3118
|
-
if (!fs.existsSync(projFile)) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('[]'); return; }
|
|
3119
|
-
const data = JSON.parse(fs.readFileSync(projFile, 'utf8'));
|
|
3120
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3121
|
-
res.end(JSON.stringify(data.projects || []));
|
|
3122
|
-
} catch(_) { res.writeHead(500); res.end('[]'); }
|
|
3123
|
-
return;
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// GET /api/org/:name/members — org member list and join requests
|
|
3127
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/members$/i.test(url)) {
|
|
3128
|
-
try {
|
|
3129
|
-
const parts = url.split('/');
|
|
3130
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3131
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
|
|
3132
|
-
const d = projectDir || process.cwd();
|
|
3133
|
-
const membersFile = path.join(d, '.monomind', 'orgs', `${orgName}-members.json`);
|
|
3134
|
-
if (!fs.existsSync(membersFile)) {
|
|
3135
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3136
|
-
res.end('{"members":[],"join_requests":[]}');
|
|
3137
|
-
return;
|
|
3138
|
-
}
|
|
3139
|
-
const data = JSON.parse(fs.readFileSync(membersFile, 'utf8'));
|
|
3140
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3141
|
-
res.end(JSON.stringify(data));
|
|
3142
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3143
|
-
return;
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
// GET /api/org/:name/adapters — org adapter registry
|
|
3147
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/adapters$/i.test(url)) {
|
|
3148
|
-
try {
|
|
3149
|
-
const parts = url.split('/');
|
|
3150
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3151
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
|
|
3152
|
-
const d = projectDir || process.cwd();
|
|
3153
|
-
const adaptersFile = path.join(d, '.monomind', 'orgs', `${orgName}-adapters.json`);
|
|
3154
|
-
if (!fs.existsSync(adaptersFile)) {
|
|
3155
|
-
// Return defaults derived from org config if available
|
|
3156
|
-
const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
|
|
3157
|
-
let defaultAdapter = 'claude-sonnet-4-6';
|
|
3158
|
-
try { defaultAdapter = JSON.parse(fs.readFileSync(orgFile, 'utf8'))?.run_config?.ceo_adapter || defaultAdapter; } catch(_) {}
|
|
3159
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3160
|
-
res.end(JSON.stringify({ default_adapter: defaultAdapter, adapters: [
|
|
3161
|
-
{ type: 'claude-local', label: 'Claude (local CLI)', source: 'built-in', disabled: false, modelsCount: 3 },
|
|
3162
|
-
{ type: 'gemini-local', label: 'Gemini (local)', source: 'built-in', disabled: false, modelsCount: 1 },
|
|
3163
|
-
{ type: 'http', label: 'HTTP Adapter', source: 'built-in', disabled: true, modelsCount: 0 },
|
|
3164
|
-
]}));
|
|
3165
|
-
return;
|
|
3166
|
-
}
|
|
3167
|
-
const data = JSON.parse(fs.readFileSync(adaptersFile, 'utf8'));
|
|
3168
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3169
|
-
res.end(JSON.stringify(data));
|
|
3170
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3171
|
-
return;
|
|
3172
|
-
}
|
|
3173
|
-
|
|
3174
|
-
// GET /api/org/:name/skills — list skills from .claude/skills/ mapped to org roles
|
|
3175
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/skills$/i.test(url)) {
|
|
3176
|
-
try {
|
|
3177
|
-
const parts = url.split('/');
|
|
3178
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3179
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
|
|
3180
|
-
const d = projectDir || process.cwd();
|
|
3181
|
-
const skillsDir = path.join(d, '.claude', 'skills');
|
|
3182
|
-
const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
|
|
3183
|
-
|
|
3184
|
-
// Scan skills directory
|
|
3185
|
-
const skills = [];
|
|
3186
|
-
if (fs.existsSync(skillsDir)) {
|
|
3187
|
-
const scanDir = (dir, prefix) => {
|
|
3188
|
-
try {
|
|
3189
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
3190
|
-
if (entry.isDirectory()) { scanDir(path.join(dir, entry.name), `${entry.name}:`); }
|
|
3191
|
-
else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
|
|
3192
|
-
const slug = entry.name.replace(/\.md$/, '');
|
|
3193
|
-
const content = fs.readFileSync(path.join(dir, entry.name), 'utf8').slice(0, 500);
|
|
3194
|
-
const typeMatch = content.match(/^type:\s*(.+)$/m);
|
|
3195
|
-
const modeMatch = content.match(/^default_mode:\s*(.+)$/m);
|
|
3196
|
-
const descMatch = content.match(/^description:\s*(.+)$/m);
|
|
3197
|
-
skills.push({
|
|
3198
|
-
name: `${prefix}${slug}`,
|
|
3199
|
-
slug,
|
|
3200
|
-
type: typeMatch ? typeMatch[1].trim() : 'skill',
|
|
3201
|
-
default_mode: modeMatch ? modeMatch[1].trim() : 'auto',
|
|
3202
|
-
description: descMatch ? descMatch[1].trim() : '',
|
|
3203
|
-
});
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
} catch(_) {}
|
|
3207
|
-
};
|
|
3208
|
-
scanDir(skillsDir, '');
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
// Map skills enabled per role from org config
|
|
3212
|
-
let roleSkillMap = {};
|
|
3213
|
-
if (fs.existsSync(orgFile)) {
|
|
3214
|
-
try {
|
|
3215
|
-
const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
|
|
3216
|
-
for (const role of (config.roles || [])) {
|
|
3217
|
-
roleSkillMap[role.id] = role.skills || [];
|
|
3218
|
-
}
|
|
3219
|
-
} catch(_) {}
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3223
|
-
res.end(JSON.stringify({ skills, role_skill_map: roleSkillMap }));
|
|
3224
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3225
|
-
return;
|
|
3226
|
-
}
|
|
3227
|
-
|
|
3228
|
-
// GET /api/org/:name/agent/:roleId — full agent detail: org role + .claude/agents definition
|
|
3229
|
-
// (characteristics, skills/expertise, responsibilities, instructions document)
|
|
3230
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agent\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
|
|
3231
|
-
try {
|
|
3232
|
-
const parts = url.split('/');
|
|
3233
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3234
|
-
const roleId = decodeURIComponent(parts[5]);
|
|
3235
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
|
|
3236
|
-
if (roleId.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(roleId)) { res.writeHead(400); res.end('{}'); return; }
|
|
3237
|
-
const _agentQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3238
|
-
const d = path.resolve(_agentQs.get('dir') || projectDir || process.cwd());
|
|
3239
|
-
const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
|
|
3240
|
-
if (!fs.existsSync(orgFile)) { res.writeHead(404); res.end('{}'); return; }
|
|
3241
|
-
const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
|
|
3242
|
-
const role = (config.roles || []).find(r => r.id === roleId);
|
|
3243
|
-
if (!role) { res.writeHead(404); res.end('{}'); return; }
|
|
3244
|
-
|
|
3245
|
-
const agentType = String(role.agent_type || role.type || '').toLowerCase();
|
|
3246
|
-
const wanted = [agentType, String(role.id).toLowerCase()].filter(Boolean);
|
|
3247
|
-
|
|
3248
|
-
// Find a matching agent definition under .claude/agents (recursive); match frontmatter name then filename.
|
|
3249
|
-
const agentsDir = path.join(d, '.claude', 'agents');
|
|
3250
|
-
let definition = { found: false };
|
|
3251
|
-
if (wanted.length && fs.existsSync(agentsDir)) {
|
|
3252
|
-
const stack = [agentsDir];
|
|
3253
|
-
let nameMatch = null, slugMatch = null;
|
|
3254
|
-
while (stack.length && !nameMatch) {
|
|
3255
|
-
const dir = stack.pop();
|
|
3256
|
-
let entries = [];
|
|
3257
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { continue; }
|
|
3258
|
-
for (const e of entries) {
|
|
3259
|
-
const full = path.join(dir, e.name);
|
|
3260
|
-
if (e.isDirectory()) { stack.push(full); continue; }
|
|
3261
|
-
if (!e.name.endsWith('.md') || e.name.startsWith('_')) continue;
|
|
3262
|
-
const slug = e.name.replace(/\.md$/, '').toLowerCase();
|
|
3263
|
-
let raw = '';
|
|
3264
|
-
try { raw = fs.readFileSync(full, 'utf8'); } catch (_) { continue; }
|
|
3265
|
-
const fmName = ((raw.match(/^name:\s*(.+)$/m) || [])[1] || '').trim().toLowerCase();
|
|
3266
|
-
if (fmName && wanted.includes(fmName)) { nameMatch = { full, raw }; break; }
|
|
3267
|
-
if (!slugMatch && wanted.includes(slug)) slugMatch = { full, raw };
|
|
3268
|
-
}
|
|
3269
|
-
}
|
|
3270
|
-
const match = nameMatch || slugMatch;
|
|
3271
|
-
if (match) {
|
|
3272
|
-
definition = parseAgentDef(match.raw);
|
|
3273
|
-
definition.found = true;
|
|
3274
|
-
definition.file = path.relative(d, match.full);
|
|
3275
|
-
}
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
|
|
3279
|
-
res.end(JSON.stringify({ role, definition }));
|
|
3280
|
-
} catch (_) { res.writeHead(500); res.end('{}'); }
|
|
3281
|
-
return;
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
// GET /api/org/:name/search?q=<query> — fuzzy search across org data
|
|
3285
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/search(\?.*)?$/i.test(url)) {
|
|
3286
|
-
try {
|
|
3287
|
-
const urlObj = new URL(`http://x${url}`);
|
|
3288
|
-
const orgName = decodeURIComponent(urlObj.pathname.split('/')[3]);
|
|
3289
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('{}'); return; }
|
|
3290
|
-
const q = (urlObj.searchParams.get('q') || '').toLowerCase().trim();
|
|
3291
|
-
if (!q || q.length < 2) { res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end('{"hits":[]}'); return; }
|
|
3292
|
-
|
|
3293
|
-
const d = projectDir || process.cwd();
|
|
3294
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
3295
|
-
const readJ = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3296
|
-
|
|
3297
|
-
const hits = [];
|
|
3298
|
-
const match = (str) => str && str.toLowerCase().includes(q);
|
|
3299
|
-
|
|
3300
|
-
// Agents
|
|
3301
|
-
const config = readJ(path.join(orgsDir, `${orgName}.json`));
|
|
3302
|
-
for (const role of (config?.roles || [])) {
|
|
3303
|
-
if (match(role.id) || match(role.title) || (role.responsibilities || []).some(r => match(r))) {
|
|
3304
|
-
hits.push({ type: 'agent', id: role.id, title: role.title, meta: role.agent_type });
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
|
|
3308
|
-
// Goals
|
|
3309
|
-
const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
|
|
3310
|
-
for (const g of (goals?.goals || [])) {
|
|
3311
|
-
if (match(g.title) || match(g.description)) {
|
|
3312
|
-
hits.push({ type: 'goal', id: g.id, title: g.title, meta: g.status || 'open' });
|
|
3313
|
-
}
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
// Routines
|
|
3317
|
-
const routines = readJ(path.join(orgsDir, `${orgName}-routines.json`));
|
|
3318
|
-
for (const r of (routines?.routines || [])) {
|
|
3319
|
-
if (match(r.name) || match(r.description)) {
|
|
3320
|
-
hits.push({ type: 'routine', id: r.name, title: r.name, meta: r.schedule || '' });
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
// Approvals
|
|
3325
|
-
const approvals = readJ(path.join(orgsDir, `${orgName}-approvals.json`));
|
|
3326
|
-
for (const a of (approvals?.approvals || [])) {
|
|
3327
|
-
if (match(a.title) || match(a.action) || match(a.agent_id)) {
|
|
3328
|
-
hits.push({ type: 'approval', id: a.id, title: a.title, meta: a.status });
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
|
|
3332
|
-
// Projects
|
|
3333
|
-
const projects = readJ(path.join(orgsDir, `${orgName}-projects.json`));
|
|
3334
|
-
for (const p of (projects?.projects || [])) {
|
|
3335
|
-
if (match(p.name) || match(p.description)) {
|
|
3336
|
-
hits.push({ type: 'project', id: p.id || p.name, title: p.name, meta: p.status || 'active' });
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
// Recent activity events
|
|
3341
|
-
const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
|
|
3342
|
-
if (fs.existsSync(eventsFile)) {
|
|
3343
|
-
const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean).slice(-500);
|
|
3344
|
-
for (const l of lines) {
|
|
3345
|
-
try {
|
|
3346
|
-
const e = JSON.parse(l);
|
|
3347
|
-
if (e.org === orgName && match(JSON.stringify(e))) {
|
|
3348
|
-
hits.push({ type: 'event', id: String(e.ts), title: e.type, meta: e.role || e.task || '' });
|
|
3349
|
-
if (hits.length >= 50) break;
|
|
3350
|
-
}
|
|
3351
|
-
} catch(_) {}
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3356
|
-
res.end(JSON.stringify({ q, hits: hits.slice(0, 50) }));
|
|
3357
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3358
|
-
return;
|
|
3359
|
-
}
|
|
3360
|
-
|
|
3361
|
-
// GET /api/org/:name/issues — org task/issue list from issues file
|
|
3362
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/issues$/i)) {
|
|
3363
|
-
try {
|
|
3364
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3365
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3366
|
-
const issuesPath = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-issues.json`);
|
|
3367
|
-
let payload = { issues: [] };
|
|
3368
|
-
try {
|
|
3369
|
-
const raw = JSON.parse(fs.readFileSync(issuesPath, 'utf8'));
|
|
3370
|
-
payload.issues = (raw.issues || []).map(i => ({
|
|
3371
|
-
id: i.id, slug: i.slug, title: i.title, status: i.status || 'open',
|
|
3372
|
-
priority: i.priority || 'medium', assignee_id: i.assignee_id || null,
|
|
3373
|
-
project_id: i.project_id || null, parent_id: i.parent_id || null,
|
|
3374
|
-
created_at: i.created_at, updated_at: i.updated_at
|
|
3375
|
-
}));
|
|
3376
|
-
} catch(_) { /* file missing is fine */ }
|
|
3377
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3378
|
-
res.end(JSON.stringify(payload));
|
|
3379
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3380
|
-
return;
|
|
3381
|
-
}
|
|
3382
|
-
|
|
3383
|
-
// GET /api/org/:name/health — aggregate org health metrics
|
|
3384
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/health$/i)) {
|
|
3385
|
-
try {
|
|
3386
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3387
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3388
|
-
const _healthQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3389
|
-
const base = path.join(path.resolve(_healthQs.get('dir') || projectDir || process.cwd()), '.monomind', 'orgs');
|
|
3390
|
-
|
|
3391
|
-
let agentsRunning = 0, agentsIdle = 0, openIssues = 0, inProgressIssues = 0;
|
|
3392
|
-
let budgetUsedTokens = 0, budgetMaxTokens = 0;
|
|
3393
|
-
let successRuns = 0, totalRuns = 0;
|
|
3394
|
-
|
|
3395
|
-
// State: agent statuses
|
|
3396
|
-
try {
|
|
3397
|
-
const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
|
|
3398
|
-
const agents = state.agents || {};
|
|
3399
|
-
Object.values(agents).forEach(a => {
|
|
3400
|
-
if (a.status === 'running') agentsRunning++;
|
|
3401
|
-
else agentsIdle++;
|
|
3402
|
-
budgetUsedTokens += (a.tokens_used || ((a.tokens_in || 0) + (a.tokens_out || 0)));
|
|
3403
|
-
});
|
|
3404
|
-
} catch(_) {}
|
|
3405
|
-
|
|
3406
|
-
// Budget cap from org config
|
|
3407
|
-
try {
|
|
3408
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
|
|
3409
|
-
budgetMaxTokens = cfg.run_config?.budget_tokens || cfg.budget_tokens || 0;
|
|
3410
|
-
} catch(_) {}
|
|
3411
|
-
|
|
3412
|
-
// Issues: open count
|
|
3413
|
-
try {
|
|
3414
|
-
const iss = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-issues.json`), 'utf8'));
|
|
3415
|
-
openIssues = (iss.issues || []).filter(i => i.status === 'open').length;
|
|
3416
|
-
inProgressIssues = (iss.issues || []).filter(i => i.status === 'in_progress').length;
|
|
3417
|
-
} catch(_) {}
|
|
3418
|
-
|
|
3419
|
-
// Activity: 7-day success rate
|
|
3420
|
-
try {
|
|
3421
|
-
const actPath = path.join(base, `${orgName}-activity.jsonl`);
|
|
3422
|
-
const lines = fs.readFileSync(actPath, 'utf8').split('\n').filter(Boolean);
|
|
3423
|
-
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
3424
|
-
lines.forEach(line => {
|
|
3425
|
-
try {
|
|
3426
|
-
const ev = JSON.parse(line);
|
|
3427
|
-
if (!ev.ts || ev.ts < cutoff) return;
|
|
3428
|
-
totalRuns++;
|
|
3429
|
-
if (ev.type && ev.type.includes('complete')) successRuns++;
|
|
3430
|
-
} catch(_) {}
|
|
3431
|
-
});
|
|
3432
|
-
} catch(_) {}
|
|
3433
|
-
|
|
3434
|
-
const budgetUsedPct = budgetMaxTokens > 0 ? Math.round((budgetUsedTokens / budgetMaxTokens) * 100) : null;
|
|
3435
|
-
const successRate = totalRuns > 0 ? Math.round((successRuns / totalRuns) * 100) : null;
|
|
3436
|
-
|
|
3437
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3438
|
-
res.end(JSON.stringify({
|
|
3439
|
-
agents_running: agentsRunning,
|
|
3440
|
-
agents_idle: agentsIdle,
|
|
3441
|
-
agents_active: agentsRunning,
|
|
3442
|
-
open_issues: openIssues,
|
|
3443
|
-
in_progress_issues: inProgressIssues,
|
|
3444
|
-
tasks_pending: openIssues + inProgressIssues,
|
|
3445
|
-
budget_used_tokens: budgetUsedTokens,
|
|
3446
|
-
budget_max_tokens: budgetMaxTokens,
|
|
3447
|
-
budget_used_pct: budgetUsedPct,
|
|
3448
|
-
run_success_rate_7d: successRate,
|
|
3449
|
-
total_runs_7d: totalRuns,
|
|
3450
|
-
errors: [],
|
|
3451
|
-
}));
|
|
3452
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3453
|
-
return;
|
|
3454
|
-
}
|
|
3455
|
-
|
|
3456
|
-
// GET /api/org/:name/environments — org execution environments (strips key material)
|
|
3457
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/environments$/i)) {
|
|
3458
|
-
try {
|
|
3459
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3460
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3461
|
-
const envsPath = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-environments.json`);
|
|
3462
|
-
let payload = { environments: [], default_env: null };
|
|
3463
|
-
try {
|
|
3464
|
-
const raw = JSON.parse(fs.readFileSync(envsPath, 'utf8'));
|
|
3465
|
-
// Strip any accidental key_material or private_key fields — never send to browser
|
|
3466
|
-
payload.default_env = raw.default_env || null;
|
|
3467
|
-
payload.environments = (raw.environments || []).map(e => {
|
|
3468
|
-
const safe = { ...e };
|
|
3469
|
-
delete safe.key_material;
|
|
3470
|
-
delete safe.private_key;
|
|
3471
|
-
delete safe.ssh_key;
|
|
3472
|
-
delete safe.password;
|
|
3473
|
-
return safe;
|
|
3474
|
-
});
|
|
3475
|
-
} catch(_) { /* file missing is fine — return empty */ }
|
|
3476
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3477
|
-
res.end(JSON.stringify(payload));
|
|
3478
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3479
|
-
return;
|
|
3480
|
-
}
|
|
3481
|
-
|
|
3482
|
-
// GET /api/org/:name/workspaces — org workspaces cross-referenced with worktree registry
|
|
3483
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/workspaces$/i)) {
|
|
3484
|
-
try {
|
|
3485
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3486
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3487
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3488
|
-
let payload = { workspaces: [] };
|
|
3489
|
-
try {
|
|
3490
|
-
const wsRaw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-workspaces.json`), 'utf8'));
|
|
3491
|
-
const workspaces = wsRaw.workspaces || [];
|
|
3492
|
-
// Optionally cross-reference worktree registry for branch/status enrichment
|
|
3493
|
-
let worktreeMap = {};
|
|
3494
|
-
try {
|
|
3495
|
-
const wtRaw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-worktrees.json`), 'utf8'));
|
|
3496
|
-
(wtRaw.worktrees || []).forEach(wt => { worktreeMap[wt.path] = wt; });
|
|
3497
|
-
} catch(_) { /* no worktree registry, that's fine */ }
|
|
3498
|
-
payload.workspaces = workspaces.map(w => {
|
|
3499
|
-
const wt = w.worktree_path ? worktreeMap[w.worktree_path] : null;
|
|
3500
|
-
return wt ? { ...w, branch: w.branch || wt.branch || w.branch } : w;
|
|
3501
|
-
});
|
|
3502
|
-
} catch(_) { /* file missing is fine */ }
|
|
3503
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3504
|
-
res.end(JSON.stringify(payload));
|
|
3505
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3506
|
-
return;
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
// GET /api/org/:name/invites — active invites + pending join requests
|
|
3510
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/invites$/i)) {
|
|
3511
|
-
try {
|
|
3512
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3513
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3514
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3515
|
-
let payload = { invites: [], join_requests: [] };
|
|
3516
|
-
try {
|
|
3517
|
-
const raw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-members.json`), 'utf8'));
|
|
3518
|
-
const all = raw.join_requests || [];
|
|
3519
|
-
payload.invites = all.filter(r => r.type === 'invite' && r.status === 'pending')
|
|
3520
|
-
.map(r => ({ id: r.id, token: r.token ? r.token.slice(0, 8) + '…' : r.id, role: r.role || 'operator', createdAt: r.createdAt || null, status: r.status }));
|
|
3521
|
-
payload.join_requests = all.filter(r => r.type !== 'invite' && r.status === 'pending_approval')
|
|
3522
|
-
.map(r => ({ id: r.id, requestType: r.requestType || 'human', role: r.role || 'viewer', createdAt: r.createdAt || null, message: r.message || '' }));
|
|
3523
|
-
} catch(_) { /* members file missing */ }
|
|
3524
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3525
|
-
res.end(JSON.stringify(payload));
|
|
3526
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3527
|
-
return;
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
// GET /api/org/:name/plugins — plugins from registry filtered/merged with org overrides
|
|
3531
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/plugins$/i)) {
|
|
3532
|
-
try {
|
|
3533
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3534
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3535
|
-
const base = path.join(projectDir || process.cwd(), '.monomind');
|
|
3536
|
-
let plugins = [];
|
|
3537
|
-
try {
|
|
3538
|
-
const reg = JSON.parse(fs.readFileSync(path.join(base, 'plugins', 'registry.json'), 'utf8'));
|
|
3539
|
-
plugins = reg.plugins || [];
|
|
3540
|
-
// Strip sensitive config fields from output
|
|
3541
|
-
plugins = plugins.map(p => {
|
|
3542
|
-
const safe = { ...p };
|
|
3543
|
-
if (safe.config) {
|
|
3544
|
-
safe.config = Object.fromEntries(
|
|
3545
|
-
Object.entries(safe.config).map(([k, v]) =>
|
|
3546
|
-
(/key|token|secret|password|api/i.test(k) ? [k, '***'] : [k, v])
|
|
3547
|
-
)
|
|
3548
|
-
);
|
|
3549
|
-
}
|
|
3550
|
-
return safe;
|
|
3551
|
-
});
|
|
3552
|
-
} catch(_) { /* no global registry */ }
|
|
3553
|
-
// Merge org-level overrides
|
|
3554
|
-
try {
|
|
3555
|
-
const orgPlugins = JSON.parse(fs.readFileSync(path.join(base, 'orgs', `${orgName}-plugins.json`), 'utf8'));
|
|
3556
|
-
const overrideMap = {};
|
|
3557
|
-
(orgPlugins.plugins || []).forEach(p => { overrideMap[p.id] = p; });
|
|
3558
|
-
if (Object.keys(overrideMap).length) {
|
|
3559
|
-
plugins = plugins.map(p => overrideMap[p.id] ? { ...p, ...overrideMap[p.id], _orgOverride: true } : p);
|
|
3560
|
-
}
|
|
3561
|
-
} catch(_) { /* no org-level overrides */ }
|
|
3562
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3563
|
-
res.end(JSON.stringify({ plugins }));
|
|
3564
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3565
|
-
return;
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
// GET /api/org/:name/my-issues — open + in_progress issues (self-assignable queue)
|
|
3569
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/my-issues$/i)) {
|
|
3570
|
-
try {
|
|
3571
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3572
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3573
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3574
|
-
let payload = { issues: [] };
|
|
3575
|
-
try {
|
|
3576
|
-
const raw = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-issues.json`), 'utf8'));
|
|
3577
|
-
// Return open + in_progress issues — the "my issues" queue for the operator
|
|
3578
|
-
payload.issues = (raw.issues || [])
|
|
3579
|
-
.filter(i => i.status === 'open' || i.status === 'in_progress')
|
|
3580
|
-
.map(i => ({
|
|
3581
|
-
id: i.id,
|
|
3582
|
-
title: i.title || null,
|
|
3583
|
-
status: i.status || 'open',
|
|
3584
|
-
priority: i.priority || 'medium',
|
|
3585
|
-
assigneeId: i.assigneeId || i.assigned_to || null,
|
|
3586
|
-
projectId: i.projectId || i.project_id || null,
|
|
3587
|
-
createdAt: i.createdAt || null,
|
|
3588
|
-
lastActivityAt: i.lastActivityAt || null,
|
|
3589
|
-
}));
|
|
3590
|
-
} catch(_) { /* issues file missing */ }
|
|
3591
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3592
|
-
res.end(JSON.stringify(payload));
|
|
3593
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3594
|
-
return;
|
|
3595
|
-
}
|
|
3596
|
-
|
|
3597
|
-
// GET /api/org/:name/agents — agents from roles + merged heartbeat state
|
|
3598
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agents$/i)) {
|
|
3599
|
-
try {
|
|
3600
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3601
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3602
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3603
|
-
const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3604
|
-
const config = readJsonSafe(path.join(base, `${orgName}.json`)) || {};
|
|
3605
|
-
const stateData = readJsonSafe(path.join(base, `${orgName}-state.json`)) || {};
|
|
3606
|
-
const agentState = stateData.agents || stateData.roles
|
|
3607
|
-
? (stateData.agents || Object.fromEntries((stateData.roles||[]).map(r => [r.id, r])))
|
|
3608
|
-
: {};
|
|
3609
|
-
const roles = config.roles || [];
|
|
3610
|
-
const agents = roles.map(r => {
|
|
3611
|
-
const s = agentState[r.id] || {};
|
|
3612
|
-
return {
|
|
3613
|
-
id: r.id,
|
|
3614
|
-
title: r.title || r.id,
|
|
3615
|
-
adapterType: (r.adapter && r.adapter.type) || null,
|
|
3616
|
-
adapterModel: (r.adapter && r.adapter.model) || null,
|
|
3617
|
-
governance: r.governance || null,
|
|
3618
|
-
reportsTo: r.reports_to || null,
|
|
3619
|
-
status: s.status || 'idle',
|
|
3620
|
-
lastHeartbeat: s.last_heartbeat || s.lastHeartbeat || null,
|
|
3621
|
-
tokensIn: s.tokens_in || 0,
|
|
3622
|
-
tokensOut: s.tokens_out || 0,
|
|
3623
|
-
skills: r.skills || [],
|
|
3624
|
-
};
|
|
3625
|
-
});
|
|
3626
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3627
|
-
res.end(JSON.stringify({ agents }));
|
|
3628
|
-
} catch(_) { res.writeHead(500); res.end('{"agents":[]}'); }
|
|
3629
|
-
return;
|
|
3630
|
-
}
|
|
3631
|
-
|
|
3632
|
-
// GET /api/org/:name/approvals — full approvals list with status filter support
|
|
3633
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals(\?.*)?$/i)) {
|
|
3634
|
-
try {
|
|
3635
|
-
const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
|
|
3636
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3637
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3638
|
-
const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3639
|
-
const data = readJsonSafe(path.join(base, `${orgName}-approvals.json`)) || { approvals: [] };
|
|
3640
|
-
const approvals = (data.approvals || [])
|
|
3641
|
-
.sort((a, b) => new Date(b.createdAt || b.created_at || b.requested_at || 0) - new Date(a.createdAt || a.created_at || a.requested_at || 0))
|
|
3642
|
-
.map(a => ({
|
|
3643
|
-
id: a.id,
|
|
3644
|
-
title: a.title || a.action || null,
|
|
3645
|
-
action: a.action || a.title || null,
|
|
3646
|
-
description: a.description || a.action || a.title || null,
|
|
3647
|
-
status: a.status || 'pending',
|
|
3648
|
-
agentId: a.agentId || a.agent_id || null,
|
|
3649
|
-
agentTitle: a.agentTitle || null,
|
|
3650
|
-
requester: a.requester || a.agentTitle || a.agent_id || a.agentId || null,
|
|
3651
|
-
agent: a.agent || a.agent_id || a.agentId || null,
|
|
3652
|
-
payload: a.payload || null,
|
|
3653
|
-
risk_level: a.risk_level || 'medium',
|
|
3654
|
-
created_at: a.created_at || a.createdAt || a.requested_at || null,
|
|
3655
|
-
createdAt: a.createdAt || a.created_at || a.requested_at || null,
|
|
3656
|
-
updatedAt: a.updatedAt || null,
|
|
3657
|
-
resolvedAt: a.resolvedAt || null,
|
|
3658
|
-
resolvedBy: a.resolvedBy || null,
|
|
3659
|
-
ts: a.ts || null,
|
|
3660
|
-
}));
|
|
3661
|
-
const pending = approvals.filter(a => a.status === 'pending' || a.status === 'revision_requested').length;
|
|
3662
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3663
|
-
res.end(JSON.stringify({ approvals, pending }));
|
|
3664
|
-
} catch(_) { res.writeHead(500); res.end('{"approvals":[],"pending":0}'); }
|
|
3665
|
-
return;
|
|
3666
|
-
}
|
|
3667
|
-
|
|
3668
|
-
// POST /api/org/:name/approvals/:id — approve or reject a pending approval request
|
|
3669
|
-
// Body: { action: "approve" | "reject" | "revision_requested" }
|
|
3670
|
-
if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals\/[^/]+$/i)) {
|
|
3671
|
-
let body = '';
|
|
3672
|
-
for await (const chunk of req) body += chunk;
|
|
3673
|
-
try {
|
|
3674
|
-
const parts = url.split('/');
|
|
3675
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
3676
|
-
const approvalId = decodeURIComponent(parts[5]);
|
|
3677
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3678
|
-
if (!approvalId) { res.writeHead(400); res.end('{"error":"approval id required"}'); return; }
|
|
3679
|
-
const parsed = JSON.parse(body);
|
|
3680
|
-
const action = parsed.action;
|
|
3681
|
-
if (!['approve', 'reject', 'revision_requested'].includes(action)) {
|
|
3682
|
-
res.writeHead(400); res.end('{"error":"action must be approve, reject, or revision_requested"}'); return;
|
|
3683
|
-
}
|
|
3684
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3685
|
-
const approvalsFile = path.join(base, `${orgName}-approvals.json`);
|
|
3686
|
-
let data = { approvals: [] };
|
|
3687
|
-
try { data = JSON.parse(fs.readFileSync(approvalsFile, 'utf8')); } catch(_) {}
|
|
3688
|
-
const idx = (data.approvals || []).findIndex(a => a.id === approvalId);
|
|
3689
|
-
if (idx === -1) { res.writeHead(404); res.end('{"error":"approval not found"}'); return; }
|
|
3690
|
-
const status = action === 'approve' ? 'approved' : action === 'reject' ? 'rejected' : 'revision_requested';
|
|
3691
|
-
data.approvals[idx] = {
|
|
3692
|
-
...data.approvals[idx],
|
|
3693
|
-
status,
|
|
3694
|
-
resolvedAt: new Date().toISOString(),
|
|
3695
|
-
resolvedBy: 'operator',
|
|
3696
|
-
};
|
|
3697
|
-
const tmp = `${approvalsFile}.tmp`;
|
|
3698
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
|
|
3699
|
-
fs.renameSync(tmp, approvalsFile);
|
|
3700
|
-
// Emit org:approval:resolved event so boss agent unblocks
|
|
3701
|
-
const event = { type: 'org:approval:resolved', org: orgName, approval_id: approvalId, status, ts: Date.now() };
|
|
3702
|
-
try { fs.appendFileSync(path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch(_) {}
|
|
3703
|
-
const msg = `data: ${JSON.stringify(event)}\n\n`;
|
|
3704
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
|
|
3705
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3706
|
-
res.end(JSON.stringify({ ok: true, status }));
|
|
3707
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3708
|
-
return;
|
|
3709
|
-
}
|
|
3710
|
-
|
|
3711
|
-
// GET /api/org/:name/secrets — masked secrets list (NEVER exposes values)
|
|
3712
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/secrets$/i)) {
|
|
3713
|
-
try {
|
|
3714
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3715
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3716
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3717
|
-
const secretsDir = path.join(base, '.secrets');
|
|
3718
|
-
const readJsonSafe = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch(_) { return null; } };
|
|
3719
|
-
// Read secrets index — NEVER expose actual values
|
|
3720
|
-
const indexFile = path.join(secretsDir, `${orgName}-index.json`);
|
|
3721
|
-
const data = readJsonSafe(indexFile) || { secrets: [] };
|
|
3722
|
-
const secrets = (data.secrets || []).map(s => ({
|
|
3723
|
-
name: s.name,
|
|
3724
|
-
maskedRef: s.maskedRef || `${(s.name||'').substring(0,4)}***`,
|
|
3725
|
-
status: s.status || 'active',
|
|
3726
|
-
createdAt: s.createdAt || null,
|
|
3727
|
-
rotatedAt: s.rotatedAt || null,
|
|
3728
|
-
lastUsedAt: s.lastUsedAt || null,
|
|
3729
|
-
usageCount: s.usageCount || 0,
|
|
3730
|
-
}));
|
|
3731
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3732
|
-
res.end(JSON.stringify({ secrets }));
|
|
3733
|
-
} catch(_) { res.writeHead(500); res.end('{"secrets":[]}'); }
|
|
3734
|
-
return;
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3737
|
-
// GET /api/org/:name/budgets — org and per-agent budget data
|
|
3738
|
-
// Returns: { org_budget: {limit_tokens, limit_usd}, agent_budgets: {agentId: {limit_usd}}, agents: [{id, title, tokens_in, tokens_out, total_cost_usd}] }
|
|
3739
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/budgets$/i)) {
|
|
3740
|
-
try {
|
|
3741
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3742
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3743
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3744
|
-
let budgetData = { org_budget: {}, agent_budgets: {}, period: 'monthly', currency: 'USD' };
|
|
3745
|
-
try { budgetData = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-budgets.json`), 'utf8')); } catch(_) {}
|
|
3746
|
-
// Enrich with per-agent spend from state file.
|
|
3747
|
-
// State file format: { agents: { "<role_id>": { tokens_in, tokens_out, ... } } }
|
|
3748
|
-
let agents = [];
|
|
3749
|
-
try {
|
|
3750
|
-
const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
|
|
3751
|
-
const agentMap = state.agents || {};
|
|
3752
|
-
// Also load role titles from org config for enrichment
|
|
3753
|
-
let roleMap = {};
|
|
3754
|
-
try {
|
|
3755
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
|
|
3756
|
-
(cfg.roles || []).forEach(r => { roleMap[r.id] = r.title || r.id; });
|
|
3757
|
-
} catch(_) {}
|
|
3758
|
-
agents = Object.entries(agentMap).map(([id, s]) => ({
|
|
3759
|
-
id,
|
|
3760
|
-
title: roleMap[id] || s.title || id,
|
|
3761
|
-
tokens_in: s.tokens_in || 0,
|
|
3762
|
-
tokens_out: s.tokens_out || 0,
|
|
3763
|
-
tokens_used: s.tokens_used || (s.tokens_in || 0) + (s.tokens_out || 0),
|
|
3764
|
-
total_cost_usd: s.total_cost_usd || 0,
|
|
3765
|
-
}));
|
|
3766
|
-
} catch(_) {}
|
|
3767
|
-
// Also include roles from org config if state is empty
|
|
3768
|
-
if (!agents.length) {
|
|
3769
|
-
try {
|
|
3770
|
-
const org = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
|
|
3771
|
-
agents = (org.roles || []).map(r => ({ id: r.id, title: r.title, tokens_in: 0, tokens_out: 0, total_cost_usd: 0 }));
|
|
3772
|
-
} catch(_) {}
|
|
3773
|
-
}
|
|
3774
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3775
|
-
res.end(JSON.stringify({ ...budgetData, agents }));
|
|
3776
|
-
} catch(_) { res.writeHead(500); res.end('{"org_budget":{},"agent_budgets":{},"agents":[]}'); }
|
|
3777
|
-
return;
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
// GET /api/org/:name/threads — conversation threads from threads.jsonl
|
|
3781
|
-
// Returns: { threads: [{id, subject, authorId, authorName, issueId, createdAt, messages:[]}] }
|
|
3782
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/threads$/i)) {
|
|
3783
|
-
try {
|
|
3784
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3785
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3786
|
-
const threadsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-threads.jsonl`);
|
|
3787
|
-
let threads = [];
|
|
3788
|
-
try {
|
|
3789
|
-
const lines = fs.readFileSync(threadsFile, 'utf8').split('\n').filter(l => l.trim());
|
|
3790
|
-
threads = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
|
|
3791
|
-
threads = threads.filter(t => t.type === 'thread' || !t.type);
|
|
3792
|
-
} catch(_) {}
|
|
3793
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3794
|
-
res.end(JSON.stringify({ threads }));
|
|
3795
|
-
} catch(_) { res.writeHead(500); res.end('{"threads":[]}'); }
|
|
3796
|
-
return;
|
|
3797
|
-
}
|
|
3798
|
-
|
|
3799
|
-
// GET /api/org/:name/join-requests — pending join requests for this org
|
|
3800
|
-
// Returns: { requests: [{id, requesterId, requesterName, type, status, createdAt, resolvedAt}], pending: N }
|
|
3801
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/join-requests(\?.*)?$/i)) {
|
|
3802
|
-
try {
|
|
3803
|
-
const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
|
|
3804
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3805
|
-
const joinFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-join-requests.json`);
|
|
3806
|
-
let requests = [];
|
|
3807
|
-
try {
|
|
3808
|
-
const raw = fs.readFileSync(joinFile, 'utf8');
|
|
3809
|
-
const data = JSON.parse(raw);
|
|
3810
|
-
requests = (data.requests || []).map(r => ({
|
|
3811
|
-
id: r.id,
|
|
3812
|
-
requesterId: r.requesterId,
|
|
3813
|
-
requesterName: r.requesterName || r.requesterId,
|
|
3814
|
-
type: r.type || 'human',
|
|
3815
|
-
status: r.status || 'pending_approval',
|
|
3816
|
-
createdAt: r.createdAt,
|
|
3817
|
-
resolvedAt: r.resolvedAt || null,
|
|
3818
|
-
}));
|
|
3819
|
-
} catch(_) {}
|
|
3820
|
-
const pending = requests.filter(r => r.status === 'pending_approval').length;
|
|
3821
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3822
|
-
res.end(JSON.stringify({ requests, pending }));
|
|
3823
|
-
} catch(_) { res.writeHead(500); res.end('{"requests":[],"pending":0}'); }
|
|
3824
|
-
return;
|
|
3825
|
-
}
|
|
3826
|
-
|
|
3827
|
-
// GET /api/org/:name/goals — read org goals
|
|
3828
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/goals$/i)) {
|
|
3829
|
-
try {
|
|
3830
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3831
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3832
|
-
const goalsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-goals.json`);
|
|
3833
|
-
let data = { goals: [] };
|
|
3834
|
-
try { data = JSON.parse(fs.readFileSync(goalsFile, 'utf8')); } catch(_) {}
|
|
3835
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3836
|
-
res.end(JSON.stringify({ goals: data.goals || [] }));
|
|
3837
|
-
} catch(_) { res.writeHead(500); res.end('{"goals":[]}'); }
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
|
|
3841
|
-
// GET /api/org/:name/routines — read org routines
|
|
3842
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
|
|
3843
|
-
try {
|
|
3844
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3845
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3846
|
-
const routinesFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-routines.json`);
|
|
3847
|
-
let data = { routines: [] };
|
|
3848
|
-
try { data = JSON.parse(fs.readFileSync(routinesFile, 'utf8')); } catch(_) {}
|
|
3849
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3850
|
-
res.end(JSON.stringify({ routines: data.routines || [] }));
|
|
3851
|
-
} catch(_) { res.writeHead(500); res.end('{"routines":[]}'); }
|
|
3852
|
-
return;
|
|
3853
|
-
}
|
|
3854
|
-
|
|
3855
|
-
// POST /api/org/:name/goals — upsert the org goals file
|
|
3856
|
-
// Body: { goals: [{id, title, description, status, priority, assignee_id, created_at}] }
|
|
3857
|
-
if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/goals$/i)) {
|
|
3858
|
-
let body = '';
|
|
3859
|
-
for await (const chunk of req) body += chunk;
|
|
3860
|
-
try {
|
|
3861
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3862
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3863
|
-
const parsed = JSON.parse(body);
|
|
3864
|
-
if (!parsed || !Array.isArray(parsed.goals)) { res.writeHead(400); res.end('{"error":"goals array required"}'); return; }
|
|
3865
|
-
const goalsFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-goals.json`);
|
|
3866
|
-
const tmp = `${goalsFile}.tmp`;
|
|
3867
|
-
const payload = { org: orgName, updated_at: new Date().toISOString(), goals: parsed.goals };
|
|
3868
|
-
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
3869
|
-
fs.renameSync(tmp, goalsFile);
|
|
3870
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3871
|
-
res.end(JSON.stringify({ ok: true, count: parsed.goals.length }));
|
|
3872
|
-
} catch(_) { res.writeHead(500); res.end('{"error":"' + String(_).replace(/"/g, '\\"') + '"}'); }
|
|
3873
|
-
return;
|
|
3874
|
-
}
|
|
3875
|
-
|
|
3876
|
-
// POST /api/org/:name/routines — upsert the org routines file
|
|
3877
|
-
// Body: { routines: [{name, description, schedule, enabled, last_run, next_run}] }
|
|
3878
|
-
if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
|
|
3879
|
-
let body = '';
|
|
3880
|
-
for await (const chunk of req) body += chunk;
|
|
3881
|
-
try {
|
|
3882
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3883
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3884
|
-
const parsed = JSON.parse(body);
|
|
3885
|
-
if (!parsed || !Array.isArray(parsed.routines)) { res.writeHead(400); res.end('{"error":"routines array required"}'); return; }
|
|
3886
|
-
const routinesFile = path.join(projectDir || process.cwd(), '.monomind', 'orgs', `${orgName}-routines.json`);
|
|
3887
|
-
const tmp = `${routinesFile}.tmp`;
|
|
3888
|
-
const payload = { org: orgName, updated_at: new Date().toISOString(), routines: parsed.routines };
|
|
3889
|
-
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
3890
|
-
fs.renameSync(tmp, routinesFile);
|
|
3891
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3892
|
-
res.end(JSON.stringify({ ok: true, count: parsed.routines.length }));
|
|
3893
|
-
} catch(_) { res.writeHead(500); res.end('{"error":"' + String(_).replace(/"/g, '\\"') + '"}'); }
|
|
3894
|
-
return;
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
// DELETE /api/orgs/:name — delete an org config and all associated data files
|
|
3898
|
-
if (req.method === 'DELETE' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}$/i)) {
|
|
3899
|
-
try {
|
|
3900
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3901
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3902
|
-
const orgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3903
|
-
const configFile = path.join(orgsDir, `${orgName}.json`);
|
|
3904
|
-
if (!fs.existsSync(configFile)) { res.writeHead(404); res.end('{"error":"org not found"}'); return; }
|
|
3905
|
-
// Remove all org-associated files (config + state + data)
|
|
3906
|
-
const suffixes = ['', '-state', '-goals', '-routines', '-approvals', '-activity', '-issues', '-members', '-projects', '-workspaces', '-worktrees', '-environments', '-plugins', '-adapters'];
|
|
3907
|
-
for (const suf of suffixes) {
|
|
3908
|
-
const f = path.join(orgsDir, `${orgName}${suf}.json`);
|
|
3909
|
-
try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch(_) {}
|
|
3910
|
-
const fjsonl = path.join(orgsDir, `${orgName}${suf}.jsonl`);
|
|
3911
|
-
try { if (fs.existsSync(fjsonl)) fs.unlinkSync(fjsonl); } catch(_) {}
|
|
3912
|
-
}
|
|
3913
|
-
// Remove stop file if present
|
|
3914
|
-
try { fs.unlinkSync(path.join(orgsDir, '.stops', `${orgName}.stop`)); } catch(_) {}
|
|
3915
|
-
// Remove loop prompt file if present (created for scheduled orgs by createorg)
|
|
3916
|
-
try { const lpf = path.join(path.resolve(projectDir || process.cwd()), '.monomind', 'loops', `${orgName}.md`); if (fs.existsSync(lpf)) fs.unlinkSync(lpf); } catch(_) {}
|
|
3917
|
-
// Emit org:delete event
|
|
3918
|
-
const deleteEvent = { type: 'org:delete', org: orgName, ts: Date.now() };
|
|
3919
|
-
try { fs.appendFileSync(path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'), JSON.stringify(deleteEvent) + '\n'); } catch(_) {}
|
|
3920
|
-
const msg = `data: ${JSON.stringify(deleteEvent)}\n\n`;
|
|
3921
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
|
|
3922
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3923
|
-
res.end('{"ok":true}');
|
|
3924
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3925
|
-
return;
|
|
3926
|
-
}
|
|
3927
|
-
|
|
3928
|
-
// POST /api/orgs/:name/stop — send stop signal to a running org
|
|
3929
|
-
if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/stop$/i)) {
|
|
3930
|
-
try {
|
|
3931
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3932
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end('Invalid org name'); return; }
|
|
3933
|
-
const stopEvent = { type: 'org:stop', org: orgName, ts: Date.now() };
|
|
3934
|
-
const dataDir = path.join(projectDir || process.cwd(), 'data');
|
|
3935
|
-
try { fs.mkdirSync(dataDir, { recursive: true }); } catch(_) {}
|
|
3936
|
-
try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(stopEvent) + '\n'); } catch(_) {}
|
|
3937
|
-
// Write stop marker file for boss agent to detect
|
|
3938
|
-
try {
|
|
3939
|
-
const stopDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs', '.stops');
|
|
3940
|
-
fs.mkdirSync(stopDir, { recursive: true });
|
|
3941
|
-
fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
|
|
3942
|
-
} catch(_) {}
|
|
3943
|
-
const msg = `data: ${JSON.stringify(stopEvent)}\n\n`;
|
|
3944
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch(_) { mmSseClients.delete(c); } }
|
|
3945
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3946
|
-
res.end('{"ok":true}');
|
|
3947
|
-
} catch(_) { res.writeHead(500); res.end('{}'); }
|
|
3948
|
-
return;
|
|
3949
|
-
}
|
|
3950
|
-
|
|
3951
|
-
// POST /api/orgs/:name/copy — copy org config to another project directory
|
|
3952
|
-
if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/copy$/i)) {
|
|
3953
|
-
let body = '';
|
|
3954
|
-
for await (const chunk of req) body += chunk;
|
|
3955
|
-
try {
|
|
3956
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3957
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) { res.writeHead(400); res.end(JSON.stringify({ error: 'Invalid org name' })); return; }
|
|
3958
|
-
let payload = {};
|
|
3959
|
-
try { payload = JSON.parse(body); } catch(_) {}
|
|
3960
|
-
const destination = payload.destination ? String(payload.destination).trim() : '';
|
|
3961
|
-
if (!destination) { res.writeHead(400); res.end(JSON.stringify({ error: 'destination is required' })); return; }
|
|
3962
|
-
if (!path.isAbsolute(destination)) { res.writeHead(400); res.end(JSON.stringify({ error: 'destination must be an absolute path' })); return; }
|
|
3963
|
-
const srcOrgsDir = path.join(projectDir || process.cwd(), '.monomind', 'orgs');
|
|
3964
|
-
const srcFile = path.join(srcOrgsDir, `${orgName}.json`);
|
|
3965
|
-
if (!fs.existsSync(srcFile)) { res.writeHead(404); res.end(JSON.stringify({ error: 'org not found' })); return; }
|
|
3966
|
-
const destOrgsDir = path.join(path.resolve(destination), '.monomind', 'orgs');
|
|
3967
|
-
try { fs.mkdirSync(destOrgsDir, { recursive: true }); } catch(_) {}
|
|
3968
|
-
const destFile = path.join(destOrgsDir, `${orgName}.json`);
|
|
3969
|
-
fs.copyFileSync(srcFile, destFile);
|
|
3970
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
3971
|
-
res.end(JSON.stringify({ ok: true, destFile }));
|
|
3972
|
-
} catch(e) { res.writeHead(500); res.end(JSON.stringify({ error: String(e.message || e) })); }
|
|
3973
|
-
return;
|
|
3974
|
-
}
|
|
3975
|
-
|
|
3976
|
-
// ------------------------------------------------- Mastermind event system
|
|
3977
|
-
// POST /api/mastermind/event — ingest event from mastermind skill
|
|
3978
|
-
if (req.method === 'POST' && url === '/api/mastermind/event') {
|
|
3979
|
-
let body = '';
|
|
3980
|
-
for await (const chunk of req) body += chunk;
|
|
3981
|
-
let event = {};
|
|
3982
|
-
try { event = JSON.parse(body); } catch (_) {}
|
|
3983
|
-
event.ts = event.ts || Date.now();
|
|
3984
|
-
// Use project path from event if provided (multi-project support)
|
|
3985
|
-
const eventProject = event.project && path.isAbsolute(event.project) ? event.project : null;
|
|
3986
|
-
const root = eventProject || projectDir || process.cwd();
|
|
3987
|
-
const dataDir = path.join(root, 'data');
|
|
3988
|
-
try { fs.mkdirSync(dataDir, { recursive: true }); } catch (_) {}
|
|
3989
|
-
// Track known project dirs for aggregated session listing
|
|
3990
|
-
if (eventProject) {
|
|
3991
|
-
const knownFile = path.join(projectDir || process.cwd(), 'data', 'known-projects.json');
|
|
3992
|
-
try {
|
|
3993
|
-
let known = [];
|
|
3994
|
-
try { known = JSON.parse(fs.readFileSync(knownFile, 'utf8')); } catch (_) {}
|
|
3995
|
-
if (!known.includes(eventProject)) { known.push(eventProject); fs.writeFileSync(knownFile, JSON.stringify(known)); }
|
|
3996
|
-
} catch (_) {}
|
|
3997
|
-
}
|
|
3998
|
-
try { fs.appendFileSync(path.join(dataDir, 'mastermind-events.jsonl'), JSON.stringify(event) + '\n'); } catch (_) {}
|
|
3999
|
-
// Persist session
|
|
4000
|
-
try {
|
|
4001
|
-
const sessFile = path.join(dataDir, 'mastermind-sessions.json');
|
|
4002
|
-
let sessions = [];
|
|
4003
|
-
try { sessions = JSON.parse(fs.readFileSync(sessFile, 'utf8')); } catch (_) {}
|
|
4004
|
-
if (event.type === 'session:start') {
|
|
4005
|
-
sessions.unshift({ id: event.session, ts: event.ts, prompt: event.prompt || '',
|
|
4006
|
-
status: 'running', domains: [], events: [event], project: root });
|
|
4007
|
-
} else {
|
|
4008
|
-
const s = sessions.find(s => s.id === event.session);
|
|
4009
|
-
if (s) {
|
|
4010
|
-
(s.events = s.events || []).push(event);
|
|
4011
|
-
if (event.type === 'domain:dispatch' && event.domain && !s.domains.includes(event.domain))
|
|
4012
|
-
s.domains.push(event.domain);
|
|
4013
|
-
if (event.type === 'session:complete') { s.status = event.status || 'complete'; s.endTs = event.ts; }
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
fs.writeFileSync(sessFile, JSON.stringify(sessions.slice(0, 50), null, 2));
|
|
4017
|
-
// Also write individual session file for direct traceability
|
|
4018
|
-
const sessionObj = sessions.find(s => s.id === event.session);
|
|
4019
|
-
if (sessionObj) {
|
|
4020
|
-
const sessDir = path.join(dataDir, 'sessions');
|
|
4021
|
-
try { fs.mkdirSync(sessDir, { recursive: true }); } catch (_) {}
|
|
4022
|
-
try { fs.writeFileSync(path.join(sessDir, `${event.session}.json`), JSON.stringify(sessionObj, null, 2)); } catch (_) {}
|
|
4023
|
-
}
|
|
4024
|
-
} catch (_) {}
|
|
4025
|
-
// For org:stop events, write a stop marker the boss agent can detect
|
|
4026
|
-
if (event.type === 'org:stop' && event.org) {
|
|
4027
|
-
try {
|
|
4028
|
-
const orgName = String(event.org).trim();
|
|
4029
|
-
// Validate before any filesystem use — reject rather than strip
|
|
4030
|
-
if (orgName.length > 0 && orgName.length <= 64 && /^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
4031
|
-
const stopDir = path.join(root, '.monomind', 'orgs', '.stops');
|
|
4032
|
-
fs.mkdirSync(stopDir, { recursive: true });
|
|
4033
|
-
fs.writeFileSync(path.join(stopDir, `${orgName}.stop`), String(Date.now()));
|
|
4034
|
-
}
|
|
4035
|
-
} catch (_) {}
|
|
4036
|
-
}
|
|
4037
|
-
// Broadcast to all mastermind SSE clients
|
|
4038
|
-
const msg = `data: ${JSON.stringify(event)}\n\n`;
|
|
4039
|
-
for (const c of mmSseClients) { try { c.write(msg); } catch (_) { mmSseClients.delete(c); } }
|
|
4040
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4041
|
-
res.end('{"ok":true}');
|
|
4042
|
-
return;
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4045
|
-
// GET /api/mastermind-stream — SSE for real-time events
|
|
4046
|
-
if (req.method === 'GET' && url === '/api/mastermind-stream') {
|
|
4047
|
-
res.writeHead(200, {
|
|
4048
|
-
'Content-Type': 'text/event-stream',
|
|
4049
|
-
'Cache-Control': 'no-cache',
|
|
4050
|
-
'Connection': 'keep-alive',
|
|
4051
|
-
'Access-Control-Allow-Origin': '*',
|
|
4052
|
-
});
|
|
4053
|
-
res.write(': connected\n\n');
|
|
4054
|
-
mmSseClients.add(res);
|
|
4055
|
-
// Replay last 50 events from disk
|
|
4056
|
-
try {
|
|
4057
|
-
const root2 = projectDir || process.cwd();
|
|
4058
|
-
const evFile = path.join(root2, 'data', 'mastermind-events.jsonl');
|
|
4059
|
-
const lines = fs.readFileSync(evFile, 'utf8').trim().split('\n').filter(Boolean).slice(-50);
|
|
4060
|
-
for (const l of lines) res.write(`data: ${l}\n\n`);
|
|
4061
|
-
} catch (_) {}
|
|
4062
|
-
const ka = setInterval(() => { try { res.write(': ping\n\n'); } catch (_) { clearInterval(ka); mmSseClients.delete(res); } }, 20000);
|
|
4063
|
-
req.on('close', () => { mmSseClients.delete(res); clearInterval(ka); });
|
|
4064
|
-
return;
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
// GET /api/mastermind/sessions
|
|
4068
|
-
if (req.method === 'GET' && url.startsWith('/api/mastermind/sessions')) {
|
|
4069
|
-
try {
|
|
4070
|
-
const qp = new URL('http://x' + req.url).searchParams;
|
|
4071
|
-
const filterProject = qp.get('project');
|
|
4072
|
-
const serverRoot = projectDir || process.cwd();
|
|
4073
|
-
// Collect all project dirs to aggregate
|
|
4074
|
-
const projectDirs = new Set([serverRoot]);
|
|
4075
|
-
try {
|
|
4076
|
-
const known = JSON.parse(fs.readFileSync(path.join(serverRoot, 'data', 'known-projects.json'), 'utf8'));
|
|
4077
|
-
known.forEach(p => projectDirs.add(p));
|
|
4078
|
-
} catch (_) {}
|
|
4079
|
-
// Load and merge sessions from all dirs
|
|
4080
|
-
let allSessions = [];
|
|
4081
|
-
for (const pd of projectDirs) {
|
|
4082
|
-
if (filterProject && pd !== filterProject) continue;
|
|
4083
|
-
const f = path.join(pd, 'data', 'mastermind-sessions.json');
|
|
4084
|
-
if (!fs.existsSync(f)) continue;
|
|
4085
|
-
try {
|
|
4086
|
-
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
4087
|
-
// Tag each session with its project if not already tagged
|
|
4088
|
-
s.forEach(sess => { if (!sess.project) sess.project = pd; });
|
|
4089
|
-
allSessions = allSessions.concat(s);
|
|
4090
|
-
} catch (_) {}
|
|
4091
|
-
}
|
|
4092
|
-
// Sort by ts descending, cap at 100
|
|
4093
|
-
allSessions.sort((a,b) => (b.ts||0)-(a.ts||0));
|
|
4094
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4095
|
-
res.end(JSON.stringify(allSessions.slice(0,100)));
|
|
4096
|
-
} catch (_) { res.writeHead(200); res.end('[]'); }
|
|
4097
|
-
return;
|
|
4098
|
-
}
|
|
4099
|
-
|
|
4100
|
-
// GET /api/mastermind/session/:id/trace — human-readable markdown trace
|
|
4101
|
-
if (req.method === 'GET' && url.match(/^\/api\/mastermind\/session\/[^/]+\/trace$/)) {
|
|
4102
|
-
try {
|
|
4103
|
-
const sid = url.split('/')[4];
|
|
4104
|
-
const sessFile = path.join(projectDir || process.cwd(), 'data', 'sessions', `${sid}.json`);
|
|
4105
|
-
let s = null;
|
|
4106
|
-
if (fs.existsSync(sessFile)) {
|
|
4107
|
-
s = JSON.parse(fs.readFileSync(sessFile, 'utf8'));
|
|
4108
|
-
} else {
|
|
4109
|
-
const f = path.join(projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
|
|
4110
|
-
const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
4111
|
-
s = sessions.find(x => x.id === sid);
|
|
4112
|
-
}
|
|
4113
|
-
if (!s) { res.writeHead(404); res.end('Session not found'); return; }
|
|
4114
|
-
const fmt = (ts) => new Date(ts).toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
4115
|
-
const lines = [`# Mastermind Session Trace: ${s.id}`, ``, `**Prompt:** ${s.prompt || '(none)'}`, `**Status:** ${s.status}`, `**Started:** ${fmt(s.ts)}`, s.endTs ? `**Ended:** ${fmt(s.endTs)}` : '', `**Domains:** ${(s.domains || []).join(', ') || '(none yet)'}`, ``];
|
|
4116
|
-
for (const ev of (s.events || [])) {
|
|
4117
|
-
const t = fmt(ev.ts);
|
|
4118
|
-
if (ev.type === 'session:start') lines.push(`\`${t}\` **SESSION START** — prompt: "${ev.prompt || ''}"`);
|
|
4119
|
-
else if (ev.type === 'domain:dispatch') lines.push(`\`${t}\` **DOMAIN DISPATCH** → \`${ev.domain}\` — ${ev.cmd || ''}`);
|
|
4120
|
-
else if (ev.type === 'agent:spawn') lines.push(`\`${t}\` **AGENT SPAWN** [\`${ev.domain}\`] → agent: \`${ev.agent}\` — ${ev.task || ''}`);
|
|
4121
|
-
else if (ev.type === 'intercom') lines.push(`\`${t}\` **INTERCOM** \`${ev.from}\` → \`${ev.to}\`: ${ev.msg || ''}`);
|
|
4122
|
-
else if (ev.type === 'domain:complete') lines.push(`\`${t}\` **DOMAIN COMPLETE** [\`${ev.domain}\`] status: ${ev.status}${ev.artifacts?.length ? ` — artifacts: ${ev.artifacts.join(', ')}` : ''}`);
|
|
4123
|
-
else if (ev.type === 'session:complete') lines.push(`\`${t}\` **SESSION COMPLETE** — status: ${ev.status}, domains: ${(ev.domains || []).join(', ')}`);
|
|
4124
|
-
else lines.push(`\`${t}\` ${ev.type} ${JSON.stringify(ev)}`);
|
|
4125
|
-
}
|
|
4126
|
-
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Access-Control-Allow-Origin': '*' });
|
|
4127
|
-
res.end(lines.join('\n'));
|
|
4128
|
-
} catch (_) { res.writeHead(500); res.end('Error'); }
|
|
4129
|
-
return;
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
// GET /api/mastermind/session/:id
|
|
4133
|
-
if (req.method === 'GET' && url.startsWith('/api/mastermind/session/')) {
|
|
4134
|
-
try {
|
|
4135
|
-
const sid = url.slice('/api/mastermind/session/'.length);
|
|
4136
|
-
// Check individual session file first
|
|
4137
|
-
const sessFile = path.join(projectDir || process.cwd(), 'data', 'sessions', `${sid}.json`);
|
|
4138
|
-
if (fs.existsSync(sessFile)) {
|
|
4139
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4140
|
-
res.end(fs.readFileSync(sessFile, 'utf8'));
|
|
4141
|
-
return;
|
|
4142
|
-
}
|
|
4143
|
-
const f = path.join(projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
|
|
4144
|
-
const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
4145
|
-
const s = sessions.find(x => x.id === sid);
|
|
4146
|
-
res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
|
|
4147
|
-
res.end(JSON.stringify(s || null));
|
|
4148
|
-
} catch (_) { res.writeHead(200); res.end('null'); }
|
|
4149
|
-
return;
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
|
-
// -------------------------------------------------------- GET /mastermind
|
|
4153
|
-
if (req.method === 'GET' && url === '/mastermind') {
|
|
4154
|
-
// Serve local file if present (dev), otherwise fall back to bundled HTML
|
|
4155
|
-
const root = projectDir || process.cwd();
|
|
4156
|
-
const htmlPath = path.join(root, 'docs', 'mastermind-diagram.html');
|
|
4157
|
-
let html = MASTERMIND_DIAGRAM_HTML;
|
|
4158
|
-
try { html = fs.readFileSync(htmlPath, 'utf8'); } catch (_) {}
|
|
4159
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
4160
|
-
res.end(html);
|
|
4161
|
-
return;
|
|
4162
|
-
}
|
|
4163
|
-
|
|
4164
|
-
// ----------------------------------------------------------- GET /orgs
|
|
4165
|
-
if (req.method === 'GET' && url === '/orgs') {
|
|
4166
|
-
try {
|
|
4167
|
-
const htmlPath = path.join(__dirname, 'orgs.html');
|
|
4168
|
-
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
4169
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
4170
|
-
res.end(html);
|
|
4171
|
-
} catch (err) {
|
|
4172
|
-
res.writeHead(404);
|
|
4173
|
-
res.end(`orgs.html not found: ${err.message}`);
|
|
4174
|
-
}
|
|
4175
|
-
return;
|
|
4176
|
-
}
|
|
4177
|
-
|
|
4178
|
-
// GET /api/mastermind/loops — list all active loop state files
|
|
4179
|
-
if (req.method === 'GET' && url === '/api/mastermind/loops') {
|
|
4180
|
-
try {
|
|
4181
|
-
const loopsDir = path.join(projectDir || process.cwd(), '.monomind', 'loops');
|
|
4182
|
-
const loops = [];
|
|
4183
|
-
if (fs.existsSync(loopsDir)) {
|
|
4184
|
-
const files = fs.readdirSync(loopsDir).filter(f => f.endsWith('.json') && !f.includes('-hil'));
|
|
4185
|
-
for (const f of files) {
|
|
4186
|
-
try {
|
|
4187
|
-
const d = JSON.parse(fs.readFileSync(path.join(loopsDir, f), 'utf8'));
|
|
4188
|
-
loops.push(d);
|
|
4189
|
-
} catch(_) {}
|
|
4190
|
-
}
|
|
4191
|
-
}
|
|
4192
|
-
loops.sort((a, b) => (b.lastRunAt || 0) - (a.lastRunAt || 0));
|
|
4193
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4194
|
-
res.end(JSON.stringify({ loops }));
|
|
4195
|
-
} catch(_) { res.writeHead(500); res.end('{"loops":[]}'); }
|
|
4196
|
-
return;
|
|
4197
|
-
}
|
|
4198
|
-
|
|
4199
|
-
// GET /api/mastermind/metrics — aggregate system metrics from token-summary and swarm-activity
|
|
4200
|
-
if (req.method === 'GET' && url === '/api/mastermind/metrics') {
|
|
4201
|
-
try {
|
|
4202
|
-
const base = path.join(projectDir || process.cwd(), '.monomind', 'metrics');
|
|
4203
|
-
let tokens = {}, swarm = {}, events = [];
|
|
4204
|
-
try { tokens = JSON.parse(fs.readFileSync(path.join(base, 'token-summary.json'), 'utf8')); } catch(_) {}
|
|
4205
|
-
try { swarm = JSON.parse(fs.readFileSync(path.join(base, 'swarm-activity.json'), 'utf8')); } catch(_) {}
|
|
4206
|
-
try {
|
|
4207
|
-
const evPath = path.join(projectDir || process.cwd(), 'data', 'mastermind-events.jsonl');
|
|
4208
|
-
const lines = fs.readFileSync(evPath, 'utf8').split('\n').filter(l => l.trim()).slice(-20);
|
|
4209
|
-
events = lines.map(l => { try { return JSON.parse(l); } catch(_) { return null; } }).filter(Boolean);
|
|
4210
|
-
} catch(_) {}
|
|
4211
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4212
|
-
res.end(JSON.stringify({ tokens, swarm, recentEvents: events }));
|
|
4213
|
-
} catch(_) { res.writeHead(500); res.end('{"tokens":{},"swarm":{},"recentEvents":[]}'); }
|
|
4214
|
-
return;
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
// ------------------------------------------------------------------ 404
|
|
4218
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
4219
|
-
res.end('Not found');
|
|
4220
|
-
});
|
|
4221
|
-
|
|
4222
|
-
// Bind to available port
|
|
4223
|
-
const boundPort = await bindServer(server, port);
|
|
4224
|
-
const url = `http://localhost:${boundPort}`;
|
|
4225
|
-
|
|
4226
|
-
// ---------------------------------------------------------------- Watchers
|
|
4227
|
-
let debounceTimer = null;
|
|
4228
|
-
let pendingSections = new Set();
|
|
4229
|
-
|
|
4230
|
-
function scheduleRefresh(event, filename) {
|
|
4231
|
-
const sections = pathToSections(filename);
|
|
4232
|
-
if (sections) sections.forEach(s => pendingSections.add(s));
|
|
4233
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
4234
|
-
debounceTimer = setTimeout(() => {
|
|
4235
|
-
const changed = pendingSections.size > 0
|
|
4236
|
-
? Array.from(pendingSections)
|
|
4237
|
-
: ['sessions', 'swarm', 'agents', 'tokens', 'hooks', 'memory', 'knowledge', 'metrics'];
|
|
4238
|
-
pendingSections.clear();
|
|
4239
|
-
broadcast({ kind: 'changed', sections: changed });
|
|
4240
|
-
}, 500);
|
|
4241
|
-
}
|
|
4242
|
-
|
|
4243
|
-
// Watch .monomind directory
|
|
4244
|
-
const monomindDir = path.join(projectDir || process.cwd(), '.monomind');
|
|
4245
|
-
if (fs.existsSync(monomindDir)) {
|
|
4246
|
-
try {
|
|
4247
|
-
const w = fs.watch(monomindDir, { recursive: true }, scheduleRefresh);
|
|
4248
|
-
activeWatchers.push(w);
|
|
4249
|
-
} catch {
|
|
4250
|
-
// Directory may not support recursive watch on all platforms — ignore
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
|
|
4254
|
-
// Watch .claude/sessions/ if present
|
|
4255
|
-
const claudeSessionsDir = path.join(projectDir || process.cwd(), '.claude', 'sessions');
|
|
4256
|
-
if (fs.existsSync(claudeSessionsDir)) {
|
|
4257
|
-
try {
|
|
4258
|
-
const w = fs.watch(claudeSessionsDir, { recursive: true }, scheduleRefresh);
|
|
4259
|
-
activeWatchers.push(w);
|
|
4260
|
-
} catch {
|
|
4261
|
-
// Ignore unsupported watch
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
|
-
// Update module-level state
|
|
4266
|
-
running = true;
|
|
4267
|
-
currentPort = boundPort;
|
|
4268
|
-
currentUrl = url;
|
|
4269
|
-
activeServer = server;
|
|
4270
|
-
|
|
4271
|
-
// --------------------------------------------------------- Graceful shutdown
|
|
4272
|
-
function shutdown() {
|
|
4273
|
-
for (const w of activeWatchers) {
|
|
4274
|
-
try {
|
|
4275
|
-
w.close();
|
|
4276
|
-
} catch {
|
|
4277
|
-
// Already closed
|
|
4278
|
-
}
|
|
4279
|
-
}
|
|
4280
|
-
activeWatchers.length = 0;
|
|
4281
|
-
|
|
4282
|
-
// Close all SSE connections
|
|
4283
|
-
for (const client of sseClients) {
|
|
4284
|
-
try {
|
|
4285
|
-
client.end();
|
|
4286
|
-
} catch {
|
|
4287
|
-
// Already ended
|
|
4288
|
-
}
|
|
4289
|
-
}
|
|
4290
|
-
sseClients.clear();
|
|
4291
|
-
|
|
4292
|
-
server.close(() => {
|
|
4293
|
-
running = false;
|
|
4294
|
-
currentPort = null;
|
|
4295
|
-
currentUrl = null;
|
|
4296
|
-
activeServer = null;
|
|
4297
|
-
});
|
|
4298
|
-
}
|
|
4299
|
-
|
|
4300
|
-
process.once('SIGTERM', shutdown);
|
|
4301
|
-
process.once('SIGINT', shutdown);
|
|
4302
|
-
|
|
4303
|
-
// ---------------------------------------------------------- Auto-open
|
|
4304
|
-
if (openBrowser) {
|
|
4305
|
-
openUrl(url).catch(() => {
|
|
4306
|
-
// Non-fatal: browser open failure should not crash the server
|
|
4307
|
-
});
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
|
-
return { port: boundPort, url, server };
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
/**
|
|
4314
|
-
* Returns the current server status.
|
|
4315
|
-
*/
|
|
4316
|
-
export function getServerStatus() {
|
|
4317
|
-
return {
|
|
4318
|
-
running,
|
|
4319
|
-
port: currentPort,
|
|
4320
|
-
url: currentUrl,
|
|
4321
|
-
clientCount: sseClients.size,
|
|
4322
|
-
};
|
|
4323
|
-
}
|
|
4324
|
-
|
|
4325
|
-
// Auto-start when invoked directly: node server.mjs [port]
|
|
4326
|
-
const _isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
4327
|
-
if (_isMain) {
|
|
4328
|
-
const _port = parseInt(process.argv[2] || process.env.CONTROL_PORT || '4242', 10);
|
|
4329
|
-
const _dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
4330
|
-
startServer({ port: _port, openBrowser: false, projectDir: _dir }).catch(err => {
|
|
4331
|
-
process.stderr.write(`[server] failed to start: ${err.message}\n`);
|
|
4332
|
-
process.exit(1);
|
|
4333
|
-
});
|
|
4334
|
-
}
|