@monoes/monomindcli 2.16.12 → 2.16.14
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/agents/core/coder.md +1 -1
- package/.claude/agents/engineering/engineering-devops-automator.md +1 -1
- package/.claude/agents/github/repo-architect.md +1 -1
- package/.claude/agents/testing/production-validator.md +1 -1
- package/.claude/agents/testing/testing-tool-evaluator.md +1 -1
- package/.claude/commands/github/pr-manager.md +1 -1
- package/.claude/helpers/pick-rank.cjs +34 -4
- package/.claude/skills/monolean-audit/SKILL.md +1 -1
- package/README.md +1 -1
- package/dist/src/commands/cleanup-data.d.ts +30 -0
- package/dist/src/commands/cleanup-data.d.ts.map +1 -0
- package/dist/src/commands/cleanup-data.js +138 -0
- package/dist/src/commands/cleanup-data.js.map +1 -0
- package/dist/src/commands/cleanup-scratch.d.ts +43 -0
- package/dist/src/commands/cleanup-scratch.d.ts.map +1 -0
- package/dist/src/commands/cleanup-scratch.js +125 -0
- package/dist/src/commands/cleanup-scratch.js.map +1 -0
- package/dist/src/commands/cleanup.d.ts +2 -39
- package/dist/src/commands/cleanup.d.ts.map +1 -1
- package/dist/src/commands/cleanup.js +5 -247
- package/dist/src/commands/cleanup.js.map +1 -1
- package/dist/src/commands/doctor-gitignore-checks.d.ts +10 -0
- package/dist/src/commands/doctor-gitignore-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-gitignore-checks.js +141 -0
- package/dist/src/commands/doctor-gitignore-checks.js.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts +10 -0
- package/dist/src/commands/doctor-helpers-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-helpers-checks.js +292 -0
- package/dist/src/commands/doctor-helpers-checks.js.map +1 -0
- package/dist/src/commands/doctor-memory-checks.d.ts +47 -0
- package/dist/src/commands/doctor-memory-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-memory-checks.js +298 -0
- package/dist/src/commands/doctor-memory-checks.js.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts +9 -0
- package/dist/src/commands/doctor-monograph-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-monograph-checks.js +331 -0
- package/dist/src/commands/doctor-monograph-checks.js.map +1 -0
- package/dist/src/commands/doctor-project-checks.d.ts +7 -66
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +10 -1331
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor-routing-checks.d.ts +10 -0
- package/dist/src/commands/doctor-routing-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-routing-checks.js +194 -0
- package/dist/src/commands/doctor-routing-checks.js.map +1 -0
- package/dist/src/commands/doctor-worker-checks.d.ts +17 -0
- package/dist/src/commands/doctor-worker-checks.d.ts.map +1 -0
- package/dist/src/commands/doctor-worker-checks.js +122 -0
- package/dist/src/commands/doctor-worker-checks.js.map +1 -0
- package/dist/src/commands/hooks-routing-commands.js +1 -1
- package/dist/src/commands/hooks-routing-commands.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +6 -15
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/org-control.d.ts +117 -0
- package/dist/src/commands/org-control.d.ts.map +1 -0
- package/dist/src/commands/org-control.js +274 -0
- package/dist/src/commands/org-control.js.map +1 -0
- package/dist/src/commands/org-lifecycle.d.ts +7 -0
- package/dist/src/commands/org-lifecycle.d.ts.map +1 -0
- package/dist/src/commands/org-lifecycle.js +298 -0
- package/dist/src/commands/org-lifecycle.js.map +1 -0
- package/dist/src/commands/org-manage.d.ts +7 -0
- package/dist/src/commands/org-manage.d.ts.map +1 -0
- package/dist/src/commands/org-manage.js +357 -0
- package/dist/src/commands/org-manage.js.map +1 -0
- package/dist/src/commands/org-memory-command.d.ts +3 -0
- package/dist/src/commands/org-memory-command.d.ts.map +1 -0
- package/dist/src/commands/org-memory-command.js +178 -0
- package/dist/src/commands/org-memory-command.js.map +1 -0
- package/dist/src/commands/org-observe-approvals.d.ts +8 -0
- package/dist/src/commands/org-observe-approvals.d.ts.map +1 -0
- package/dist/src/commands/org-observe-approvals.js +211 -0
- package/dist/src/commands/org-observe-approvals.js.map +1 -0
- package/dist/src/commands/org-observe-checkpoint.d.ts +26 -0
- package/dist/src/commands/org-observe-checkpoint.d.ts.map +1 -0
- package/dist/src/commands/org-observe-checkpoint.js +180 -0
- package/dist/src/commands/org-observe-checkpoint.js.map +1 -0
- package/dist/src/commands/org-observe-config.d.ts +8 -0
- package/dist/src/commands/org-observe-config.d.ts.map +1 -0
- package/dist/src/commands/org-observe-config.js +160 -0
- package/dist/src/commands/org-observe-config.js.map +1 -0
- package/dist/src/commands/org-observe-gates.d.ts +4 -0
- package/dist/src/commands/org-observe-gates.d.ts.map +1 -0
- package/dist/src/commands/org-observe-gates.js +182 -0
- package/dist/src/commands/org-observe-gates.js.map +1 -0
- package/dist/src/commands/org-observe-inbox.d.ts +18 -0
- package/dist/src/commands/org-observe-inbox.d.ts.map +1 -0
- package/dist/src/commands/org-observe-inbox.js +154 -0
- package/dist/src/commands/org-observe-inbox.js.map +1 -0
- package/dist/src/commands/org-observe-logs.d.ts +31 -0
- package/dist/src/commands/org-observe-logs.d.ts.map +1 -0
- package/dist/src/commands/org-observe-logs.js +272 -0
- package/dist/src/commands/org-observe-logs.js.map +1 -0
- package/dist/src/commands/org-observe-questions.d.ts +8 -0
- package/dist/src/commands/org-observe-questions.d.ts.map +1 -0
- package/dist/src/commands/org-observe-questions.js +201 -0
- package/dist/src/commands/org-observe-questions.js.map +1 -0
- package/dist/src/commands/org-observe-report.d.ts +8 -0
- package/dist/src/commands/org-observe-report.d.ts.map +1 -0
- package/dist/src/commands/org-observe-report.js +366 -0
- package/dist/src/commands/org-observe-report.js.map +1 -0
- package/dist/src/commands/org-observe-shared.d.ts +20 -0
- package/dist/src/commands/org-observe-shared.d.ts.map +1 -0
- package/dist/src/commands/org-observe-shared.js +55 -0
- package/dist/src/commands/org-observe-shared.js.map +1 -0
- package/dist/src/commands/org-observe.d.ts +9 -101
- package/dist/src/commands/org-observe.d.ts.map +1 -1
- package/dist/src/commands/org-observe.js +12 -1675
- package/dist/src/commands/org-observe.js.map +1 -1
- package/dist/src/commands/org-poll.d.ts +57 -0
- package/dist/src/commands/org-poll.d.ts.map +1 -0
- package/dist/src/commands/org-poll.js +194 -0
- package/dist/src/commands/org-poll.js.map +1 -0
- package/dist/src/commands/org-run.d.ts +3 -0
- package/dist/src/commands/org-run.d.ts.map +1 -0
- package/dist/src/commands/org-run.js +449 -0
- package/dist/src/commands/org-run.js.map +1 -0
- package/dist/src/commands/org-serve.d.ts +19 -0
- package/dist/src/commands/org-serve.d.ts.map +1 -0
- package/dist/src/commands/org-serve.js +395 -0
- package/dist/src/commands/org-serve.js.map +1 -0
- package/dist/src/commands/org-subcommands-config.d.ts +8 -0
- package/dist/src/commands/org-subcommands-config.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-config.js +93 -0
- package/dist/src/commands/org-subcommands-config.js.map +1 -0
- package/dist/src/commands/org-subcommands-hil.d.ts +11 -0
- package/dist/src/commands/org-subcommands-hil.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-hil.js +216 -0
- package/dist/src/commands/org-subcommands-hil.js.map +1 -0
- package/dist/src/commands/org-subcommands-observe.d.ts +12 -0
- package/dist/src/commands/org-subcommands-observe.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-observe.js +249 -0
- package/dist/src/commands/org-subcommands-observe.js.map +1 -0
- package/dist/src/commands/org-subcommands-runtime.d.ts +12 -0
- package/dist/src/commands/org-subcommands-runtime.d.ts.map +1 -0
- package/dist/src/commands/org-subcommands-runtime.js +163 -0
- package/dist/src/commands/org-subcommands-runtime.js.map +1 -0
- package/dist/src/commands/org.d.ts +3 -151
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +43 -2735
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/init/never-commit.d.ts +3 -3
- package/dist/src/init/types.d.ts +1 -1
- package/dist/src/init/types.js +1 -1
- package/dist/src/knowledge/document-store.js +1 -1
- package/dist/src/knowledge/document-store.js.map +1 -1
- package/dist/src/mcp-tools/hooks-edit-command.d.ts +10 -0
- package/dist/src/mcp-tools/hooks-edit-command.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-edit-command.js +247 -0
- package/dist/src/mcp-tools/hooks-edit-command.js.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-learning.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-learning.js +408 -0
- package/dist/src/mcp-tools/hooks-learning.js.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts +8 -0
- package/dist/src/mcp-tools/hooks-metrics-list.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js +205 -0
- package/dist/src/mcp-tools/hooks-metrics-list.js.map +1 -0
- package/dist/src/mcp-tools/hooks-route.d.ts +12 -0
- package/dist/src/mcp-tools/hooks-route.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-route.js +251 -0
- package/dist/src/mcp-tools/hooks-route.js.map +1 -0
- package/dist/src/mcp-tools/hooks-routing.d.ts +6 -27
- package/dist/src/mcp-tools/hooks-routing.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-routing.js +6 -1727
- package/dist/src/mcp-tools/hooks-routing.js.map +1 -1
- package/dist/src/mcp-tools/hooks-session.d.ts +9 -0
- package/dist/src/mcp-tools/hooks-session.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-session.js +243 -0
- package/dist/src/mcp-tools/hooks-session.js.map +1 -0
- package/dist/src/mcp-tools/hooks-task.d.ts +17 -0
- package/dist/src/mcp-tools/hooks-task.d.ts.map +1 -0
- package/dist/src/mcp-tools/hooks-task.js +427 -0
- package/dist/src/mcp-tools/hooks-task.js.map +1 -0
- package/dist/src/mcp-tools/hooks-tools.d.ts +2 -1
- package/dist/src/mcp-tools/hooks-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +2 -1
- package/dist/src/mcp-tools/hooks-tools.js.map +1 -1
- package/dist/src/mcp-tools/memory-tools.js +1 -1
- package/dist/src/mcp-tools/memory-tools.js.map +1 -1
- package/dist/src/memory/bm25-index.d.ts +2 -2
- package/dist/src/memory/bm25-index.js +2 -2
- package/dist/src/memory/embedding-operations.js +2 -2
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/memory/entity-name-key.d.ts +1 -1
- package/dist/src/memory/entity-name-key.js +1 -1
- package/dist/src/memory/hnsw-operations.d.ts +1 -1
- package/dist/src/memory/hnsw-operations.js +1 -1
- package/dist/src/memory/memory-bridge-backend.d.ts +43 -0
- package/dist/src/memory/memory-bridge-backend.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-backend.js +363 -0
- package/dist/src/memory/memory-bridge-backend.js.map +1 -0
- package/dist/src/memory/memory-bridge-core.d.ts +42 -0
- package/dist/src/memory/memory-bridge-core.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-core.js +126 -0
- package/dist/src/memory/memory-bridge-core.js.map +1 -0
- package/dist/src/memory/memory-bridge-entries.d.ts +131 -0
- package/dist/src/memory/memory-bridge-entries.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-entries.js +227 -0
- package/dist/src/memory/memory-bridge-entries.js.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts +60 -0
- package/dist/src/memory/memory-bridge-feedback.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-feedback.js +118 -0
- package/dist/src/memory/memory-bridge-feedback.js.map +1 -0
- package/dist/src/memory/memory-bridge-paths.d.ts +123 -0
- package/dist/src/memory/memory-bridge-paths.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-paths.js +335 -0
- package/dist/src/memory/memory-bridge-paths.js.map +1 -0
- package/dist/src/memory/memory-bridge-search.d.ts +48 -0
- package/dist/src/memory/memory-bridge-search.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-search.js +383 -0
- package/dist/src/memory/memory-bridge-search.js.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts +38 -0
- package/dist/src/memory/memory-bridge-sessions.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-sessions.js +109 -0
- package/dist/src/memory/memory-bridge-sessions.js.map +1 -0
- package/dist/src/memory/memory-bridge-store.d.ts +58 -0
- package/dist/src/memory/memory-bridge-store.d.ts.map +1 -0
- package/dist/src/memory/memory-bridge-store.js +201 -0
- package/dist/src/memory/memory-bridge-store.js.map +1 -0
- package/dist/src/memory/memory-bridge.d.ts +10 -438
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +14 -1787
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-kg-claims.d.ts +52 -0
- package/dist/src/memory/memory-kg-claims.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-claims.js +99 -0
- package/dist/src/memory/memory-kg-claims.js.map +1 -0
- package/dist/src/memory/memory-kg-index.d.ts +86 -0
- package/dist/src/memory/memory-kg-index.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-index.js +256 -0
- package/dist/src/memory/memory-kg-index.js.map +1 -0
- package/dist/src/memory/memory-kg-ingest.d.ts +44 -0
- package/dist/src/memory/memory-kg-ingest.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-ingest.js +361 -0
- package/dist/src/memory/memory-kg-ingest.js.map +1 -0
- package/dist/src/memory/memory-kg-model.d.ts +168 -0
- package/dist/src/memory/memory-kg-model.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-model.js +158 -0
- package/dist/src/memory/memory-kg-model.js.map +1 -0
- package/dist/src/memory/memory-kg-names.d.ts +64 -0
- package/dist/src/memory/memory-kg-names.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-names.js +187 -0
- package/dist/src/memory/memory-kg-names.js.map +1 -0
- package/dist/src/memory/memory-kg-promote.d.ts +79 -0
- package/dist/src/memory/memory-kg-promote.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-promote.js +248 -0
- package/dist/src/memory/memory-kg-promote.js.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts +77 -0
- package/dist/src/memory/memory-kg-rebuild.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rebuild.js +299 -0
- package/dist/src/memory/memory-kg-rebuild.js.map +1 -0
- package/dist/src/memory/memory-kg-rollback.d.ts +47 -0
- package/dist/src/memory/memory-kg-rollback.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rollback.js +193 -0
- package/dist/src/memory/memory-kg-rollback.js.map +1 -0
- package/dist/src/memory/memory-kg-rules.d.ts +64 -0
- package/dist/src/memory/memory-kg-rules.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-rules.js +251 -0
- package/dist/src/memory/memory-kg-rules.js.map +1 -0
- package/dist/src/memory/memory-kg-scan.d.ts +118 -0
- package/dist/src/memory/memory-kg-scan.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-scan.js +209 -0
- package/dist/src/memory/memory-kg-scan.js.map +1 -0
- package/dist/src/memory/memory-kg-search.d.ts +80 -0
- package/dist/src/memory/memory-kg-search.d.ts.map +1 -0
- package/dist/src/memory/memory-kg-search.js +239 -0
- package/dist/src/memory/memory-kg-search.js.map +1 -0
- package/dist/src/memory/memory-kg.d.ts +19 -547
- package/dist/src/memory/memory-kg.d.ts.map +1 -1
- package/dist/src/memory/memory-kg.js +10 -2389
- package/dist/src/memory/memory-kg.js.map +1 -1
- package/dist/src/orgrt/daemon-types.d.ts +196 -0
- package/dist/src/orgrt/daemon-types.d.ts.map +1 -0
- package/dist/src/orgrt/daemon-types.js +38 -0
- package/dist/src/orgrt/daemon-types.js.map +1 -0
- package/dist/src/orgrt/daemon.d.ts +70 -300
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +41 -2536
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/idle-watchdog.d.ts +39 -0
- package/dist/src/orgrt/idle-watchdog.d.ts.map +1 -0
- package/dist/src/orgrt/idle-watchdog.js +289 -0
- package/dist/src/orgrt/idle-watchdog.js.map +1 -0
- package/dist/src/orgrt/org-reload.d.ts +7 -0
- package/dist/src/orgrt/org-reload.d.ts.map +1 -0
- package/dist/src/orgrt/org-reload.js +111 -0
- package/dist/src/orgrt/org-reload.js.map +1 -0
- package/dist/src/orgrt/org-start-steps.d.ts +21 -0
- package/dist/src/orgrt/org-start-steps.d.ts.map +1 -0
- package/dist/src/orgrt/org-start-steps.js +305 -0
- package/dist/src/orgrt/org-start-steps.js.map +1 -0
- package/dist/src/orgrt/org-start.d.ts +7 -0
- package/dist/src/orgrt/org-start.d.ts.map +1 -0
- package/dist/src/orgrt/org-start.js +435 -0
- package/dist/src/orgrt/org-start.js.map +1 -0
- package/dist/src/orgrt/org-state-file.d.ts +8 -0
- package/dist/src/orgrt/org-state-file.d.ts.map +1 -0
- package/dist/src/orgrt/org-state-file.js +116 -0
- package/dist/src/orgrt/org-state-file.js.map +1 -0
- package/dist/src/orgrt/org-stop.d.ts +6 -0
- package/dist/src/orgrt/org-stop.d.ts.map +1 -0
- package/dist/src/orgrt/org-stop.js +332 -0
- package/dist/src/orgrt/org-stop.js.map +1 -0
- package/dist/src/orgrt/org-tools.d.ts +11 -0
- package/dist/src/orgrt/org-tools.d.ts.map +1 -0
- package/dist/src/orgrt/org-tools.js +305 -0
- package/dist/src/orgrt/org-tools.js.map +1 -0
- package/dist/src/orgrt/role-incarnation.d.ts +18 -0
- package/dist/src/orgrt/role-incarnation.d.ts.map +1 -0
- package/dist/src/orgrt/role-incarnation.js +374 -0
- package/dist/src/orgrt/role-incarnation.js.map +1 -0
- package/dist/src/orgrt/role-respawn.d.ts +7 -0
- package/dist/src/orgrt/role-respawn.d.ts.map +1 -0
- package/dist/src/orgrt/role-respawn.js +318 -0
- package/dist/src/orgrt/role-respawn.js.map +1 -0
- package/dist/src/orgrt/role-session-opts.d.ts +389 -0
- package/dist/src/orgrt/role-session-opts.d.ts.map +1 -0
- package/dist/src/orgrt/role-session-opts.js +289 -0
- package/dist/src/orgrt/role-session-opts.js.map +1 -0
- package/dist/src/orgrt/runner-resolve.d.ts +43 -0
- package/dist/src/orgrt/runner-resolve.d.ts.map +1 -0
- package/dist/src/orgrt/runner-resolve.js +77 -0
- package/dist/src/orgrt/runner-resolve.js.map +1 -0
- package/dist/src/orgrt/session-gate.d.ts +34 -0
- package/dist/src/orgrt/session-gate.d.ts.map +1 -0
- package/dist/src/orgrt/session-gate.js +74 -0
- package/dist/src/orgrt/session-gate.js.map +1 -0
- package/dist/src/orgrt/session-prompt.d.ts +31 -0
- package/dist/src/orgrt/session-prompt.d.ts.map +1 -0
- package/dist/src/orgrt/session-prompt.js +115 -0
- package/dist/src/orgrt/session-prompt.js.map +1 -0
- package/dist/src/orgrt/session-run.d.ts +18 -0
- package/dist/src/orgrt/session-run.d.ts.map +1 -0
- package/dist/src/orgrt/session-run.js +429 -0
- package/dist/src/orgrt/session-run.js.map +1 -0
- package/dist/src/orgrt/session-stream.d.ts +21 -0
- package/dist/src/orgrt/session-stream.d.ts.map +1 -0
- package/dist/src/orgrt/session-stream.js +173 -0
- package/dist/src/orgrt/session-stream.js.map +1 -0
- package/dist/src/orgrt/session-types.d.ts +219 -0
- package/dist/src/orgrt/session-types.d.ts.map +1 -0
- package/dist/src/orgrt/session-types.js +2 -0
- package/dist/src/orgrt/session-types.js.map +1 -0
- package/dist/src/orgrt/session-usage.d.ts +29 -0
- package/dist/src/orgrt/session-usage.d.ts.map +1 -0
- package/dist/src/orgrt/session-usage.js +64 -0
- package/dist/src/orgrt/session-usage.js.map +1 -0
- package/dist/src/orgrt/session.d.ts +5 -281
- package/dist/src/orgrt/session.d.ts.map +1 -1
- package/dist/src/orgrt/session.js +8 -1117
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/ui/org-hil.mjs +1 -1
- package/dist/src/ui/routes-org-agents.mjs +318 -0
- package/dist/src/ui/routes-org-approvals.mjs +133 -0
- package/dist/src/ui/routes-org-config.mjs +459 -0
- package/dist/src/ui/routes-org-control.mjs +300 -0
- package/dist/src/ui/routes-org-files.mjs +172 -0
- package/dist/src/ui/routes-org-helpers.mjs +84 -0
- package/dist/src/ui/routes-org-knowledge.mjs +262 -0
- package/dist/src/ui/routes-org-lifecycle.mjs +224 -0
- package/dist/src/ui/routes-org-live.mjs +278 -0
- package/dist/src/ui/routes-org-mastermind.mjs +346 -0
- package/dist/src/ui/routes-org-planning.mjs +398 -0
- package/dist/src/ui/routes-org-runs.mjs +430 -0
- package/dist/src/ui/routes-org-status.mjs +342 -0
- package/dist/src/ui/routes-org-workflows.mjs +212 -0
- package/dist/src/ui/routes-org.mjs +26 -3847
- package/dist/src/ui/server.mjs +1 -1
- package/dist/src/utils/native-error.d.ts +1 -1
- package/dist/src/utils/native-error.js +1 -1
- package/dist/src/utils/paths.d.ts +2 -1
- package/dist/src/utils/paths.d.ts.map +1 -1
- package/dist/src/utils/paths.js +2 -1
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/org-skills/adversarial-reviewer/SKILL.md +1 -1
- package/org-skills/board-deck-builder/SKILL.md +1 -1
- package/org-skills/code-reviewer/SKILL.md +1 -1
- package/org-skills/debugging-wizard/SKILL.md +1 -1
- package/org-skills/scientific-thinking-literature-review/SKILL.md +1 -1
- package/org-skills/scientific-thinking-scholar-evaluation/SKILL.md +1 -1
- package/package.json +5 -5
|
@@ -1,3851 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
/** listApprovals for read-only views, where an unreadable file shows as none. */
|
|
16
|
-
function approvalsOrEmpty(orgsDir, orgName) {
|
|
17
|
-
try {
|
|
18
|
-
return hil.listApprovals(path.dirname(path.dirname(orgsDir)), orgName);
|
|
19
|
-
} catch (_) {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const validOrgName = (n) => typeof n === 'string' && /^[a-z0-9][a-z0-9_-]{0,63}$/i.test(n);
|
|
25
|
-
|
|
26
|
-
/** Project root for a HIL route: ?dir= or the server's project. */
|
|
27
|
-
function hilRoot(req, ctx) {
|
|
28
|
-
const dir = new URL(req.url, 'http://localhost').searchParams.get('dir');
|
|
29
|
-
return path.resolve(dir || ctx.projectDir || process.cwd());
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const HIL_BODY_MAX = 65536;
|
|
33
|
-
|
|
34
|
-
/** JSON body (≤64KB); null when malformed. Call inside hilRespond: an
|
|
35
|
-
* oversized body throws 413 and an aborted upload 400, instead of an
|
|
36
|
-
* unhandled rejection that would take the dashboard process down. Bytes are
|
|
37
|
-
* decoded once at the end, so a character split across chunks survives. */
|
|
38
|
-
async function readHilBody(req) {
|
|
39
|
-
const chunks = [];
|
|
40
|
-
let size = 0;
|
|
41
|
-
try {
|
|
42
|
-
// Past the cap, keep draining (without keeping) so the 413 still reaches
|
|
43
|
-
// the client — leaving the iterator early destroys the socket.
|
|
44
|
-
for await (const chunk of req) {
|
|
45
|
-
size += chunk.length;
|
|
46
|
-
if (size <= HIL_BODY_MAX) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
47
|
-
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
throw Object.assign(new Error(`request body not received: ${err.message}`), { status: 400 });
|
|
50
|
-
}
|
|
51
|
-
if (size > HIL_BODY_MAX)
|
|
52
|
-
throw Object.assign(new Error('request body is over 64KB'), { status: 413 });
|
|
53
|
-
try {
|
|
54
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
55
|
-
} catch (_) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** decodeURIComponent for a path segment; a malformed escape is a 400. */
|
|
61
|
-
function decodeHilSegment(seg) {
|
|
62
|
-
try {
|
|
63
|
-
return decodeURIComponent(seg);
|
|
64
|
-
} catch (_) {
|
|
65
|
-
throw Object.assign(new Error(`malformed path segment: ${seg}`), { status: 400 });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Run `fn` and answer with its JSON result, or the HIL error's status. */
|
|
70
|
-
async function hilRespond(res, corsOrigin, fn) {
|
|
71
|
-
const cors = corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {};
|
|
72
|
-
try {
|
|
73
|
-
const data = await fn();
|
|
74
|
-
res.writeHead(200, { 'Content-Type': 'application/json', ...cors });
|
|
75
|
-
res.end(JSON.stringify(data));
|
|
76
|
-
} catch (err) {
|
|
77
|
-
const { status, error } = err.status
|
|
78
|
-
? { status: err.status, error: err.message }
|
|
79
|
-
: hil.hilErrorStatus(err);
|
|
80
|
-
res.writeHead(status, { 'Content-Type': 'application/json', ...cors });
|
|
81
|
-
res.end(JSON.stringify({ ok: false, error }));
|
|
82
|
-
}
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Record a human decision in the dashboard's event log and push it to live views. */
|
|
87
|
-
function hilEvent(ctx, root, ev) {
|
|
88
|
-
const event = { ...ev, resolvedBy: hil.DASHBOARD_RESOLVER, ts: Date.now() };
|
|
89
|
-
ctx
|
|
90
|
-
.appendToFile(path.join(root, 'data', 'mastermind-events.jsonl'), `${JSON.stringify(event)}\n`)
|
|
91
|
-
.catch(() => {});
|
|
92
|
-
ctx.broadcastMm(event);
|
|
93
|
-
}
|
|
1
|
+
import { handleOrgAgentRoutes } from './routes-org-agents.mjs';
|
|
2
|
+
import { handleOrgApprovalRoutes } from './routes-org-approvals.mjs';
|
|
3
|
+
import { handleOrgConfigRoutes } from './routes-org-config.mjs';
|
|
4
|
+
import { handleOrgControlRoutes } from './routes-org-control.mjs';
|
|
5
|
+
import { handleOrgFileRoutes } from './routes-org-files.mjs';
|
|
6
|
+
import { handleOrgKnowledgeRoutes } from './routes-org-knowledge.mjs';
|
|
7
|
+
import { handleOrgLifecycleRoutes } from './routes-org-lifecycle.mjs';
|
|
8
|
+
import { handleOrgLiveRoutes } from './routes-org-live.mjs';
|
|
9
|
+
import { handleOrgMastermindRoutes } from './routes-org-mastermind.mjs';
|
|
10
|
+
import { handleOrgPlanningRoutes } from './routes-org-planning.mjs';
|
|
11
|
+
import { handleOrgRunRoutes } from './routes-org-runs.mjs';
|
|
12
|
+
import { handleOrgStatusRoutes } from './routes-org-status.mjs';
|
|
13
|
+
import { handleOrgWorkflowRoutes } from './routes-org-workflows.mjs';
|
|
94
14
|
|
|
95
15
|
export async function handleOrgRoutes(req, res, url, corsOrigin, ctx) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (req
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
JSON.parse(fs.readFileSync(_knownOrgsFile, 'utf8')).forEach((p) =>
|
|
110
|
-
_orgsProjDirs.add(p),
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
} catch (_) {}
|
|
114
|
-
}
|
|
115
|
-
const _sidecarSuffixRe =
|
|
116
|
-
/-(approvals|state|activity|goals|routines|projects|members|issues|workspaces|worktrees|environments|plugins|adapters|bootstrap|threads|budgets|project-workspaces|approval-comments|secrets|join-requests|skills)\.json$/;
|
|
117
|
-
const _orgsSeen = new Set();
|
|
118
|
-
const orgs = [];
|
|
119
|
-
for (const _opd of _orgsProjDirs) {
|
|
120
|
-
const orgsDir = path.join(_opd, '.monomind', 'orgs');
|
|
121
|
-
if (!fs.existsSync(orgsDir)) continue;
|
|
122
|
-
const files = fs
|
|
123
|
-
.readdirSync(orgsDir)
|
|
124
|
-
.filter((f) => f.endsWith('.json') && !_sidecarSuffixRe.test(f));
|
|
125
|
-
for (const f of files) {
|
|
126
|
-
try {
|
|
127
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(orgsDir, f), 'utf8'));
|
|
128
|
-
const _lOrgName = cfg.name || '';
|
|
129
|
-
if (!_lOrgName || _orgsSeen.has(_lOrgName)) continue;
|
|
130
|
-
_orgsSeen.add(_lOrgName);
|
|
131
|
-
const _rs = ctx._readRunState(_lOrgName, _opd);
|
|
132
|
-
const _ttl = Math.max((_rs?.checkpointInterval || 600000) * 2, 7200000);
|
|
133
|
-
const running =
|
|
134
|
-
(_rs?.status === 'running' && Date.now() - (_rs?.lastEventAt || 0) < _ttl) ||
|
|
135
|
-
ctx.activeOrgRuns.has(_lOrgName);
|
|
136
|
-
orgs.push({
|
|
137
|
-
name: cfg.name,
|
|
138
|
-
goal: cfg.goal,
|
|
139
|
-
roles: Array.isArray(cfg.roles) ? cfg.roles : [],
|
|
140
|
-
topology: cfg.topology,
|
|
141
|
-
created_at: cfg.created_at,
|
|
142
|
-
running,
|
|
143
|
-
status: cfg.status,
|
|
144
|
-
projectDir: _opd,
|
|
145
|
-
lastEventAt: _rs?.lastEventAt || null,
|
|
146
|
-
loop: cfg.loop
|
|
147
|
-
? {
|
|
148
|
-
poll_interval_minutes: cfg.loop.poll_interval_minutes,
|
|
149
|
-
last_run: cfg.loop.last_run,
|
|
150
|
-
next_run: cfg.loop.next_run,
|
|
151
|
-
}
|
|
152
|
-
: undefined,
|
|
153
|
-
});
|
|
154
|
-
} catch (_) {}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
res.writeHead(200, {
|
|
158
|
-
'Content-Type': 'application/json',
|
|
159
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
160
|
-
});
|
|
161
|
-
res.end(JSON.stringify(orgs));
|
|
162
|
-
} catch (_) {
|
|
163
|
-
res.writeHead(500);
|
|
164
|
-
res.end('[]');
|
|
165
|
-
}
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// POST /api/orgs/:name/import — import an org config by name (orgs.html upload flow)
|
|
170
|
-
if (req.method === 'POST' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/import$/i.test(url)) {
|
|
171
|
-
let body = '';
|
|
172
|
-
req.on('data', (c) => {
|
|
173
|
-
body += c;
|
|
174
|
-
if (body.length > 2e6) req.destroy();
|
|
175
|
-
});
|
|
176
|
-
req.on('end', () => {
|
|
177
|
-
try {
|
|
178
|
-
const urlParts = url.split('/');
|
|
179
|
-
const orgName = decodeURIComponent(urlParts[3]);
|
|
180
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
181
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
182
|
-
res.end(JSON.stringify({ error: 'Invalid org name' }));
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
const cfg = JSON.parse(body);
|
|
186
|
-
const _importQs = new URL(req.url, 'http://localhost').searchParams;
|
|
187
|
-
const dir = path.resolve(_importQs.get('dir') || ctx.projectDir || process.cwd());
|
|
188
|
-
const orgsDir = path.join(dir, '.monomind', 'orgs');
|
|
189
|
-
fs.mkdirSync(orgsDir, { recursive: true });
|
|
190
|
-
const destFile = path.join(orgsDir, `${orgName}.json`);
|
|
191
|
-
fs.writeFileSync(destFile, JSON.stringify({ ...cfg, name: orgName }, null, 2), 'utf8');
|
|
192
|
-
res.writeHead(200, {
|
|
193
|
-
'Content-Type': 'application/json',
|
|
194
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
195
|
-
});
|
|
196
|
-
res.end(JSON.stringify({ ok: true, name: orgName, file: destFile }));
|
|
197
|
-
} catch (e) {
|
|
198
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
199
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// POST /api/orgs — import / create org from JSON body
|
|
206
|
-
if (req.method === 'POST' && url === '/api/orgs') {
|
|
207
|
-
let body = '';
|
|
208
|
-
req.on('data', (c) => {
|
|
209
|
-
body += c;
|
|
210
|
-
if (body.length > 2e6) req.destroy();
|
|
211
|
-
});
|
|
212
|
-
req.on('end', () => {
|
|
213
|
-
try {
|
|
214
|
-
const cfg = JSON.parse(body);
|
|
215
|
-
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
216
|
-
const dir = qs.get('dir') || cfg.dir || ctx.projectDir || process.cwd();
|
|
217
|
-
const name = (cfg.name || '')
|
|
218
|
-
.toLowerCase()
|
|
219
|
-
.replace(/[^a-z0-9_-]/g, '-')
|
|
220
|
-
.replace(/^-+|-+$/g, '')
|
|
221
|
-
.slice(0, 64);
|
|
222
|
-
if (!name) {
|
|
223
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
224
|
-
res.end(JSON.stringify({ error: 'Invalid org name' }));
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const orgsDir = path.join(path.resolve(dir), '.monomind', 'orgs');
|
|
228
|
-
fs.mkdirSync(orgsDir, { recursive: true });
|
|
229
|
-
const destFile = path.join(orgsDir, `${name}.json`);
|
|
230
|
-
const cleanCfg = Object.fromEntries(
|
|
231
|
-
Object.entries({ ...cfg, name }).filter(([k]) => !k.startsWith('_')),
|
|
232
|
-
);
|
|
233
|
-
fs.writeFileSync(destFile, JSON.stringify(cleanCfg, null, 2), 'utf8');
|
|
234
|
-
res.writeHead(200, {
|
|
235
|
-
'Content-Type': 'application/json',
|
|
236
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
237
|
-
});
|
|
238
|
-
res.end(JSON.stringify({ ok: true, name, file: destFile }));
|
|
239
|
-
} catch (e) {
|
|
240
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
241
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// GET /api/orgs/:name — get specific org config (exact path: /api/orgs/<slug>)
|
|
248
|
-
if (req.method === 'GET' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
|
|
249
|
-
try {
|
|
250
|
-
const orgName = decodeURIComponent(url.slice('/api/orgs/'.length));
|
|
251
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
252
|
-
res.writeHead(400);
|
|
253
|
-
res.end('Invalid org name');
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
const _orgsOneQs = new URL(req.url, 'http://localhost').searchParams;
|
|
257
|
-
const _orgsOneRoot = path.resolve(_orgsOneQs.get('dir') || ctx.projectDir || process.cwd());
|
|
258
|
-
const _orgsOneProjDir = ctx._resolveOrgProjectDir(orgName, _orgsOneRoot) || _orgsOneRoot;
|
|
259
|
-
const f = path.join(_orgsOneProjDir, '.monomind', 'orgs', `${orgName}.json`);
|
|
260
|
-
if (!fs.existsSync(f)) {
|
|
261
|
-
res.writeHead(404);
|
|
262
|
-
res.end('{"error":"not found"}');
|
|
263
|
-
return true;
|
|
264
|
-
}
|
|
265
|
-
res.writeHead(200, {
|
|
266
|
-
'Content-Type': 'application/json',
|
|
267
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
268
|
-
});
|
|
269
|
-
res.end(fs.readFileSync(f, 'utf8'));
|
|
270
|
-
} catch (_) {
|
|
271
|
-
res.writeHead(500);
|
|
272
|
-
res.end('{}');
|
|
273
|
-
}
|
|
274
|
-
return true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// GET /api/org/:name — ORG ROOM: rich org data (config + state + tasks + routines + goals)
|
|
278
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)) {
|
|
279
|
-
try {
|
|
280
|
-
const orgName = decodeURIComponent(url.slice('/api/org/'.length));
|
|
281
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
282
|
-
res.writeHead(400);
|
|
283
|
-
res.end('Invalid org name');
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
const _orgQs = new URL(req.url, 'http://localhost').searchParams;
|
|
287
|
-
const _orgServerRoot = path.resolve(_orgQs.get('dir') || ctx.projectDir || process.cwd());
|
|
288
|
-
// Resolve which project dir actually has this org's config
|
|
289
|
-
const d = ctx._resolveOrgProjectDir(orgName, _orgServerRoot) || _orgServerRoot;
|
|
290
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
291
|
-
|
|
292
|
-
const readJsonSafe = (f) => {
|
|
293
|
-
try {
|
|
294
|
-
return JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
295
|
-
} catch (_) {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const configFile = path.join(orgsDir, `${orgName}.json`);
|
|
301
|
-
if (!fs.existsSync(configFile)) {
|
|
302
|
-
res.writeHead(404);
|
|
303
|
-
res.end('{"error":"org not found"}');
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
const config = readJsonSafe(configFile);
|
|
307
|
-
|
|
308
|
-
const state = readJsonSafe(path.join(orgsDir, `${orgName}-state.json`)) || { agents: {} };
|
|
309
|
-
const goalsData = readJsonSafe(path.join(orgsDir, `${orgName}-goals.json`)) || { goals: [] };
|
|
310
|
-
const routinesData = readJsonSafe(path.join(orgsDir, `${orgName}-routines.json`)) || {
|
|
311
|
-
routines: [],
|
|
312
|
-
};
|
|
313
|
-
const approvalsData = { approvals: approvalsOrEmpty(orgsDir, orgName) };
|
|
314
|
-
|
|
315
|
-
// Check running status: stop file absence AND (in-memory ctx.activeOrgRuns OR state-file agents OR active loop file)
|
|
316
|
-
// Path must match what `org run`'s poll loop and `org serve`'s pollStopfiles()
|
|
317
|
-
// actually watch (.monomind/orgs/<name>/stop) — see the POST .../stop handler below.
|
|
318
|
-
const stopFile = path.join(orgsDir, orgName, 'stop');
|
|
319
|
-
const _loopsDir = path.join(d, '.monomind', 'loops');
|
|
320
|
-
const _loopRunning = (() => {
|
|
321
|
-
try {
|
|
322
|
-
if (!fs.existsSync(_loopsDir)) return false;
|
|
323
|
-
// Get the org's state file mtime to correlate with loop activity
|
|
324
|
-
const orgStateMtime = (() => {
|
|
325
|
-
try {
|
|
326
|
-
return fs.statSync(path.join(orgsDir, `${orgName}-state.json`)).mtimeMs;
|
|
327
|
-
} catch {
|
|
328
|
-
return 0;
|
|
329
|
-
}
|
|
330
|
-
})();
|
|
331
|
-
// Also check org's most recent run file mtime
|
|
332
|
-
const orgRunsDir = path.join(
|
|
333
|
-
ctx._getGitMonomindDir(d) || path.join(d, '.monomind'),
|
|
334
|
-
'orgs',
|
|
335
|
-
orgName,
|
|
336
|
-
'runs',
|
|
337
|
-
);
|
|
338
|
-
const orgLastRunMtime = (() => {
|
|
339
|
-
try {
|
|
340
|
-
if (!fs.existsSync(orgRunsDir)) return 0;
|
|
341
|
-
const runFiles = fs
|
|
342
|
-
.readdirSync(orgRunsDir)
|
|
343
|
-
.filter((f) => f.endsWith('.jsonl') && !f.startsWith('._'));
|
|
344
|
-
if (!runFiles.length) return 0;
|
|
345
|
-
return Math.max(
|
|
346
|
-
...runFiles.map((f) => {
|
|
347
|
-
try {
|
|
348
|
-
return fs.statSync(path.join(orgRunsDir, f)).mtimeMs;
|
|
349
|
-
} catch {
|
|
350
|
-
return 0;
|
|
351
|
-
}
|
|
352
|
-
}),
|
|
353
|
-
);
|
|
354
|
-
} catch {
|
|
355
|
-
return 0;
|
|
356
|
-
}
|
|
357
|
-
})();
|
|
358
|
-
const orgLastActivity = Math.max(orgStateMtime, orgLastRunMtime);
|
|
359
|
-
return fs.readdirSync(_loopsDir).some((f) => {
|
|
360
|
-
if (!f.endsWith('.json') || f.endsWith('.stop')) return false;
|
|
361
|
-
try {
|
|
362
|
-
const lp = JSON.parse(fs.readFileSync(path.join(_loopsDir, f), 'utf8'));
|
|
363
|
-
if (!lp.command?.includes('runorg')) return false;
|
|
364
|
-
if (!['running', 'paused'].includes(lp.status)) return false;
|
|
365
|
-
// Primary match: explicit orgName field (written by runorg command since v1.14.2)
|
|
366
|
-
if (lp.orgName === orgName) return true;
|
|
367
|
-
// Fallback: org name in prompt (early loop files that preserved --org flag)
|
|
368
|
-
if ((lp.prompt || '').includes(orgName)) return true;
|
|
369
|
-
// Heuristic: if loop's lastRunAt is within 3x wait interval of org's last activity
|
|
370
|
-
const waitMs = (lp.wait || 60) * 3 * 1000;
|
|
371
|
-
return (
|
|
372
|
-
orgLastActivity > 0 && Math.abs(orgLastActivity - (lp.lastRunAt || 0)) < waitMs
|
|
373
|
-
);
|
|
374
|
-
} catch {
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
} catch {
|
|
379
|
-
return false;
|
|
380
|
-
}
|
|
381
|
-
})();
|
|
382
|
-
const _runstateData = ctx._readRunState(orgName, d);
|
|
383
|
-
const _runstateTtl = Math.max((_runstateData?.checkpointInterval || 600000) * 2, 7200000);
|
|
384
|
-
const _runstateAlive =
|
|
385
|
-
_runstateData?.status === 'running' &&
|
|
386
|
-
Date.now() - (_runstateData?.lastEventAt || 0) < _runstateTtl;
|
|
387
|
-
const running =
|
|
388
|
-
!fs.existsSync(stopFile) &&
|
|
389
|
-
(_runstateAlive || ctx.activeOrgRuns.has(orgName) || _loopRunning);
|
|
390
|
-
|
|
391
|
-
// Read real tasks from the task store and group by status column
|
|
392
|
-
const taskStoreData = readJsonSafe(path.join(d, '.monomind', 'tasks', 'store.json'));
|
|
393
|
-
const allTasks = taskStoreData ? Object.values(taskStoreData.tasks || {}) : [];
|
|
394
|
-
const tasks = {
|
|
395
|
-
todo: allTasks
|
|
396
|
-
.filter((t) => t.status === 'pending')
|
|
397
|
-
.map((t) => ({
|
|
398
|
-
id: t.taskId,
|
|
399
|
-
description: t.description,
|
|
400
|
-
status: 'todo',
|
|
401
|
-
ts: t.createdAt,
|
|
402
|
-
})),
|
|
403
|
-
doing: allTasks
|
|
404
|
-
.filter((t) => t.status === 'in_progress')
|
|
405
|
-
.map((t) => ({
|
|
406
|
-
id: t.taskId,
|
|
407
|
-
description: t.description,
|
|
408
|
-
status: 'doing',
|
|
409
|
-
ts: t.startedAt || t.createdAt,
|
|
410
|
-
})),
|
|
411
|
-
done: allTasks
|
|
412
|
-
.filter(
|
|
413
|
-
(t) => t.status === 'completed' || t.status === 'failed' || t.status === 'cancelled',
|
|
414
|
-
)
|
|
415
|
-
.map((t) => ({
|
|
416
|
-
id: t.taskId,
|
|
417
|
-
description: t.description,
|
|
418
|
-
status: t.status,
|
|
419
|
-
ts: t.completedAt || t.createdAt,
|
|
420
|
-
})),
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
const result = {
|
|
424
|
-
config,
|
|
425
|
-
state,
|
|
426
|
-
goals: goalsData.goals,
|
|
427
|
-
routines: routinesData.routines,
|
|
428
|
-
approvals: approvalsData.approvals,
|
|
429
|
-
running,
|
|
430
|
-
tasks,
|
|
431
|
-
runId: _runstateData?.runId || null,
|
|
432
|
-
lastEventAt: _runstateData?.lastEventAt || null,
|
|
433
|
-
agentStates: _runstateData?.agentStates || {},
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
res.writeHead(200, {
|
|
437
|
-
'Content-Type': 'application/json',
|
|
438
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
439
|
-
});
|
|
440
|
-
res.end(JSON.stringify(result));
|
|
441
|
-
} catch (_) {
|
|
442
|
-
res.writeHead(500);
|
|
443
|
-
res.end('{}');
|
|
444
|
-
}
|
|
445
|
-
return true;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// GET /api/org/:name/activity — recent org events from mastermind-events.jsonl
|
|
449
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/activity$/i.test(url)) {
|
|
450
|
-
try {
|
|
451
|
-
const parts = url.split('/');
|
|
452
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
453
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
454
|
-
res.writeHead(400);
|
|
455
|
-
res.end('[]');
|
|
456
|
-
return true;
|
|
457
|
-
}
|
|
458
|
-
const _actQs = new URL(req.url, 'http://localhost').searchParams;
|
|
459
|
-
const _actServerRoot = path.resolve(_actQs.get('dir') || ctx.projectDir || process.cwd());
|
|
460
|
-
const d = ctx._resolveOrgProjectDir(orgName, _actServerRoot) || _actServerRoot;
|
|
461
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
462
|
-
const readJ = (f) => {
|
|
463
|
-
try {
|
|
464
|
-
return JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
465
|
-
} catch (_) {
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
const events = [];
|
|
470
|
-
|
|
471
|
-
// 1) Global mastermind events that EXPLICITLY belong to this org (strict — no untagged leak)
|
|
472
|
-
const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
|
|
473
|
-
if (fs.existsSync(eventsFile)) {
|
|
474
|
-
const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean);
|
|
475
|
-
for (const l of lines.slice(-1000)) {
|
|
476
|
-
try {
|
|
477
|
-
const e = JSON.parse(l);
|
|
478
|
-
if (e && e.org === orgName) events.push(e);
|
|
479
|
-
} catch (_) {}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// 2) Synthesize an org-scoped timeline from this org's own records (real data, distinct per org)
|
|
484
|
-
const cfg = readJ(path.join(orgsDir, `${orgName}.json`));
|
|
485
|
-
if (cfg) {
|
|
486
|
-
const createdMs = cfg.created_at ? Date.parse(cfg.created_at) : null;
|
|
487
|
-
if (createdMs)
|
|
488
|
-
events.push({
|
|
489
|
-
type: 'org:create',
|
|
490
|
-
ts: createdMs,
|
|
491
|
-
msg: String(cfg.goal || 'Org created').slice(0, 80),
|
|
492
|
-
});
|
|
493
|
-
// Roles are defined atomically at org creation — there is no per-role
|
|
494
|
-
// timestamp in the config, so every role:defined event uses the org's
|
|
495
|
-
// real created_at instead of a fabricated per-index offset.
|
|
496
|
-
(cfg.roles || []).forEach((r) => {
|
|
497
|
-
events.push({
|
|
498
|
-
type: 'role:defined',
|
|
499
|
-
ts: createdMs,
|
|
500
|
-
role: r.title || r.id,
|
|
501
|
-
msg: r.agent_type || '',
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
|
|
506
|
-
(goals?.goals || []).forEach((g) =>
|
|
507
|
-
events.push({
|
|
508
|
-
type: 'goal',
|
|
509
|
-
ts: Date.parse(g.created_at || g.updated_at || '') || null,
|
|
510
|
-
role: g.status || '',
|
|
511
|
-
msg: String(g.text || g.title || g.goal || '').slice(0, 80),
|
|
512
|
-
}),
|
|
513
|
-
);
|
|
514
|
-
approvalsOrEmpty(orgsDir, orgName).forEach((a) => {
|
|
515
|
-
events.push({
|
|
516
|
-
type: 'approval',
|
|
517
|
-
ts: a.ts,
|
|
518
|
-
role: a.roleId || '',
|
|
519
|
-
msg: `${a.action} — ${a.status}`.slice(0, 80),
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
const state = readJ(path.join(orgsDir, `${orgName}-state.json`));
|
|
523
|
-
if (state?.agents) {
|
|
524
|
-
for (const [aid, a] of Object.entries(state.agents)) {
|
|
525
|
-
const raw = a.lastHeartbeat || a.last_seen || a.updated_at || null;
|
|
526
|
-
const ts = typeof raw === 'number' ? raw : raw ? Date.parse(raw) : null;
|
|
527
|
-
events.push({ type: 'org:heartbeat', ts, agent: aid, msg: a.status || '' });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
const out = events
|
|
532
|
-
.filter((e) => e?.ts)
|
|
533
|
-
.sort((a, b) => b.ts - a.ts)
|
|
534
|
-
.slice(0, 100);
|
|
535
|
-
res.writeHead(200, {
|
|
536
|
-
'Content-Type': 'application/json',
|
|
537
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
538
|
-
});
|
|
539
|
-
res.end(JSON.stringify(out));
|
|
540
|
-
} catch (_) {
|
|
541
|
-
res.writeHead(500);
|
|
542
|
-
res.end('[]');
|
|
543
|
-
}
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// GET /api/org/:name/adapters — org adapter registry
|
|
548
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/adapters$/i.test(url)) {
|
|
549
|
-
try {
|
|
550
|
-
const parts = url.split('/');
|
|
551
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
552
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
553
|
-
res.writeHead(400);
|
|
554
|
-
res.end('{}');
|
|
555
|
-
return true;
|
|
556
|
-
}
|
|
557
|
-
const _adaptersQs = new URL(req.url, 'http://localhost').searchParams;
|
|
558
|
-
const _adaptersRoot = path.resolve(_adaptersQs.get('dir') || ctx.projectDir || process.cwd());
|
|
559
|
-
const d = ctx._resolveOrgProjectDir(orgName, _adaptersRoot) || _adaptersRoot;
|
|
560
|
-
const adaptersFile = path.join(d, '.monomind', 'orgs', `${orgName}-adapters.json`);
|
|
561
|
-
if (!fs.existsSync(adaptersFile)) {
|
|
562
|
-
// Return defaults derived from org config if available
|
|
563
|
-
const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
|
|
564
|
-
// claude-sonnet-5 = DEFAULT_CLAUDE_MODEL (src/orgrt/vercel-providers.ts); .mjs can't import TS.
|
|
565
|
-
let defaultAdapter = 'claude-sonnet-5';
|
|
566
|
-
try {
|
|
567
|
-
defaultAdapter =
|
|
568
|
-
JSON.parse(fs.readFileSync(orgFile, 'utf8'))?.run_config?.ceo_adapter || defaultAdapter;
|
|
569
|
-
} catch (_) {}
|
|
570
|
-
res.writeHead(200, {
|
|
571
|
-
'Content-Type': 'application/json',
|
|
572
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
573
|
-
});
|
|
574
|
-
// This list mirrors the runtimes orgrt/daemon.ts actually registers
|
|
575
|
-
// (resolveRunner()'s if-chain + the ClaudeAgentRunner default it falls
|
|
576
|
-
// through to) rather than an independent, hand-maintained catalog — a
|
|
577
|
-
// standalone 'gemini-local' entry used to be advertised here even
|
|
578
|
-
// though no runner is ever registered for it (autoRuntimeFromProvider
|
|
579
|
-
// has no 'gemini' case, so a role requesting it silently falls back to
|
|
580
|
-
// Claude — see daemon.ts#startOrg's fail-fast warning). Gemini is only
|
|
581
|
-
// actually reachable via the 'vercel' runtime (vendor: 'google') or via
|
|
582
|
-
// 'antigravity' (Google-account CLI), both listed below. 'http' maps to
|
|
583
|
-
// provider.kind === 'base-url' (ANTHROPIC_BASE_URL + optional auth
|
|
584
|
-
// token) — a real, usable path, not disabled.
|
|
585
|
-
res.end(
|
|
586
|
-
JSON.stringify({
|
|
587
|
-
default_adapter: defaultAdapter,
|
|
588
|
-
adapters: [
|
|
589
|
-
{
|
|
590
|
-
type: 'claude-local',
|
|
591
|
-
label: 'Claude (local CLI)',
|
|
592
|
-
source: 'built-in',
|
|
593
|
-
disabled: false,
|
|
594
|
-
modelsCount: 3,
|
|
595
|
-
},
|
|
596
|
-
{
|
|
597
|
-
type: 'codex-local',
|
|
598
|
-
label: 'Codex CLI',
|
|
599
|
-
source: 'built-in',
|
|
600
|
-
disabled: false,
|
|
601
|
-
modelsCount: 1,
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
type: 'antigravity',
|
|
605
|
-
label: 'Antigravity (Google CLI)',
|
|
606
|
-
source: 'built-in',
|
|
607
|
-
disabled: false,
|
|
608
|
-
modelsCount: 1,
|
|
609
|
-
},
|
|
610
|
-
{
|
|
611
|
-
type: 'grok',
|
|
612
|
-
label: 'Grok CLI',
|
|
613
|
-
source: 'built-in',
|
|
614
|
-
disabled: false,
|
|
615
|
-
modelsCount: 1,
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
type: 'qwen',
|
|
619
|
-
label: 'Qwen CLI',
|
|
620
|
-
source: 'built-in',
|
|
621
|
-
disabled: false,
|
|
622
|
-
modelsCount: 1,
|
|
623
|
-
},
|
|
624
|
-
{
|
|
625
|
-
type: 'crush',
|
|
626
|
-
label: 'Crush CLI',
|
|
627
|
-
source: 'built-in',
|
|
628
|
-
disabled: false,
|
|
629
|
-
modelsCount: 1,
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
type: 'copilot',
|
|
633
|
-
label: 'GitHub Copilot CLI',
|
|
634
|
-
source: 'built-in',
|
|
635
|
-
disabled: false,
|
|
636
|
-
modelsCount: 1,
|
|
637
|
-
},
|
|
638
|
-
{ type: 'pi', label: 'Pi CLI', source: 'built-in', disabled: false, modelsCount: 1 },
|
|
639
|
-
{
|
|
640
|
-
type: 'opencode',
|
|
641
|
-
label: 'OpenCode CLI',
|
|
642
|
-
source: 'built-in',
|
|
643
|
-
disabled: false,
|
|
644
|
-
modelsCount: 1,
|
|
645
|
-
},
|
|
646
|
-
{
|
|
647
|
-
type: 'kimicode',
|
|
648
|
-
label: 'KimiCode CLI',
|
|
649
|
-
source: 'built-in',
|
|
650
|
-
disabled: false,
|
|
651
|
-
modelsCount: 1,
|
|
652
|
-
},
|
|
653
|
-
{
|
|
654
|
-
type: 'vercel',
|
|
655
|
-
label: 'Vercel AI SDK (multi-vendor, incl. Gemini/OpenAI)',
|
|
656
|
-
source: 'built-in',
|
|
657
|
-
disabled: false,
|
|
658
|
-
modelsCount: 14,
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
type: 'http',
|
|
662
|
-
label: 'Custom HTTP (base-url provider)',
|
|
663
|
-
source: 'built-in',
|
|
664
|
-
disabled: false,
|
|
665
|
-
modelsCount: 0,
|
|
666
|
-
},
|
|
667
|
-
],
|
|
668
|
-
}),
|
|
669
|
-
);
|
|
670
|
-
return true;
|
|
671
|
-
}
|
|
672
|
-
const data = JSON.parse(fs.readFileSync(adaptersFile, 'utf8'));
|
|
673
|
-
res.writeHead(200, {
|
|
674
|
-
'Content-Type': 'application/json',
|
|
675
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
676
|
-
});
|
|
677
|
-
res.end(JSON.stringify(data));
|
|
678
|
-
} catch (_) {
|
|
679
|
-
res.writeHead(500);
|
|
680
|
-
res.end('{}');
|
|
681
|
-
}
|
|
682
|
-
return true;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// GET /api/org/:name/agent/:roleId — full agent detail: org role + .claude/agents definition
|
|
686
|
-
// (characteristics, responsibilities, instructions document)
|
|
687
|
-
if (
|
|
688
|
-
req.method === 'GET' &&
|
|
689
|
-
/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agent\/[a-z0-9][a-z0-9_-]{0,63}$/i.test(url)
|
|
690
|
-
) {
|
|
691
|
-
try {
|
|
692
|
-
const parts = url.split('/');
|
|
693
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
694
|
-
const roleId = decodeURIComponent(parts[5]);
|
|
695
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
696
|
-
res.writeHead(400);
|
|
697
|
-
res.end('{}');
|
|
698
|
-
return true;
|
|
699
|
-
}
|
|
700
|
-
if (roleId.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(roleId)) {
|
|
701
|
-
res.writeHead(400);
|
|
702
|
-
res.end('{}');
|
|
703
|
-
return true;
|
|
704
|
-
}
|
|
705
|
-
const _agentQs = new URL(req.url, 'http://localhost').searchParams;
|
|
706
|
-
const d = path.resolve(_agentQs.get('dir') || ctx.projectDir || process.cwd());
|
|
707
|
-
const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
|
|
708
|
-
if (!fs.existsSync(orgFile)) {
|
|
709
|
-
res.writeHead(404);
|
|
710
|
-
res.end('{}');
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
|
|
714
|
-
const role = (config.roles || []).find((r) => r.id === roleId);
|
|
715
|
-
if (!role) {
|
|
716
|
-
res.writeHead(404);
|
|
717
|
-
res.end('{}');
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const agentType = String(role.agent_type || role.type || '').toLowerCase();
|
|
722
|
-
const wanted = [agentType, String(role.id).toLowerCase()].filter(Boolean);
|
|
723
|
-
|
|
724
|
-
// Find a matching agent definition under .claude/agents (recursive); match frontmatter name then filename.
|
|
725
|
-
const agentsDir = path.join(d, '.claude', 'agents');
|
|
726
|
-
let definition = { found: false };
|
|
727
|
-
if (wanted.length && fs.existsSync(agentsDir)) {
|
|
728
|
-
const stack = [agentsDir];
|
|
729
|
-
let nameMatch = null,
|
|
730
|
-
slugMatch = null;
|
|
731
|
-
while (stack.length && !nameMatch) {
|
|
732
|
-
const dir = stack.pop();
|
|
733
|
-
let entries = [];
|
|
734
|
-
try {
|
|
735
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
736
|
-
} catch (_) {
|
|
737
|
-
continue;
|
|
738
|
-
}
|
|
739
|
-
for (const e of entries) {
|
|
740
|
-
const full = path.join(dir, e.name);
|
|
741
|
-
if (e.isDirectory()) {
|
|
742
|
-
stack.push(full);
|
|
743
|
-
continue;
|
|
744
|
-
}
|
|
745
|
-
if (!e.name.endsWith('.md') || e.name.startsWith('_')) continue;
|
|
746
|
-
const slug = e.name.replace(/\.md$/, '').toLowerCase();
|
|
747
|
-
let raw = '';
|
|
748
|
-
try {
|
|
749
|
-
raw = fs.readFileSync(full, 'utf8');
|
|
750
|
-
} catch (_) {
|
|
751
|
-
continue;
|
|
752
|
-
}
|
|
753
|
-
const fmName = ((raw.match(/^name:\s*(.+)$/m) || [])[1] || '').trim().toLowerCase();
|
|
754
|
-
if (fmName && wanted.includes(fmName)) {
|
|
755
|
-
nameMatch = { full, raw };
|
|
756
|
-
break;
|
|
757
|
-
}
|
|
758
|
-
if (!slugMatch && wanted.includes(slug)) slugMatch = { full, raw };
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
const match = nameMatch || slugMatch;
|
|
762
|
-
if (match) {
|
|
763
|
-
definition = ctx.parseAgentDef(match.raw);
|
|
764
|
-
definition.found = true;
|
|
765
|
-
definition.file = path.relative(d, match.full);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
res.writeHead(200, {
|
|
770
|
-
'Content-Type': 'application/json',
|
|
771
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
772
|
-
'Cache-Control': 'no-cache',
|
|
773
|
-
});
|
|
774
|
-
res.end(JSON.stringify({ role, definition }));
|
|
775
|
-
} catch (_) {
|
|
776
|
-
res.writeHead(500);
|
|
777
|
-
res.end('{}');
|
|
778
|
-
}
|
|
779
|
-
return true;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// POST /api/org/:name/agent/:roleId/avatar — set (or clear) a custom avatar for one
|
|
783
|
-
// role, stored inline as a data URL on the role itself so it travels with the org
|
|
784
|
-
// config export/import. Body: { avatarDataUrl } — null/omitted avatarDataUrl clears
|
|
785
|
-
// the custom avatar and reverts to the built-in library picture. `dir` (like every
|
|
786
|
-
// other dir-accepting route in this file, POST included — see /goals above) comes
|
|
787
|
-
// from the query string, not the body.
|
|
788
|
-
if (
|
|
789
|
-
req.method === 'POST' &&
|
|
790
|
-
/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agent\/[a-z0-9][a-z0-9_-]{0,63}\/avatar$/i.test(url)
|
|
791
|
-
) {
|
|
792
|
-
let body = '';
|
|
793
|
-
for await (const chunk of req) {
|
|
794
|
-
body += chunk;
|
|
795
|
-
if (body.length > 8388608) {
|
|
796
|
-
req.destroy();
|
|
797
|
-
break;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
try {
|
|
801
|
-
const parts = url.split('/');
|
|
802
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
803
|
-
const roleId = decodeURIComponent(parts[5]);
|
|
804
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
805
|
-
res.writeHead(400);
|
|
806
|
-
res.end('{"ok":false,"error":"Invalid org name"}');
|
|
807
|
-
return true;
|
|
808
|
-
}
|
|
809
|
-
if (roleId.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(roleId)) {
|
|
810
|
-
res.writeHead(400);
|
|
811
|
-
res.end('{"ok":false,"error":"Invalid role id"}');
|
|
812
|
-
return true;
|
|
813
|
-
}
|
|
814
|
-
const parsed = JSON.parse(body);
|
|
815
|
-
const avatarDataUrl = parsed.avatarDataUrl;
|
|
816
|
-
if (
|
|
817
|
-
avatarDataUrl != null &&
|
|
818
|
-
(typeof avatarDataUrl !== 'string' ||
|
|
819
|
-
avatarDataUrl.length > 2_000_000 ||
|
|
820
|
-
!/^data:image\/(png|jpe?g|webp|gif);base64,[A-Za-z0-9+/=]+$/.test(avatarDataUrl))
|
|
821
|
-
) {
|
|
822
|
-
res.writeHead(400);
|
|
823
|
-
res.end('{"ok":false,"error":"Invalid or oversized avatarDataUrl"}');
|
|
824
|
-
return true;
|
|
825
|
-
}
|
|
826
|
-
const _avatarQs = new URL(req.url, 'http://localhost').searchParams;
|
|
827
|
-
const d = path.resolve(_avatarQs.get('dir') || ctx.projectDir || process.cwd());
|
|
828
|
-
const orgFile = path.join(d, '.monomind', 'orgs', `${orgName}.json`);
|
|
829
|
-
if (!fs.existsSync(orgFile)) {
|
|
830
|
-
res.writeHead(404);
|
|
831
|
-
res.end('{"ok":false,"error":"org not found"}');
|
|
832
|
-
return true;
|
|
833
|
-
}
|
|
834
|
-
const config = JSON.parse(fs.readFileSync(orgFile, 'utf8'));
|
|
835
|
-
const role = (config.roles || []).find((r) => r.id === roleId);
|
|
836
|
-
if (!role) {
|
|
837
|
-
res.writeHead(404);
|
|
838
|
-
res.end('{"ok":false,"error":"role not found"}');
|
|
839
|
-
return true;
|
|
840
|
-
}
|
|
841
|
-
if (avatarDataUrl) role.avatar = avatarDataUrl;
|
|
842
|
-
else delete role.avatar;
|
|
843
|
-
const tmp = `${orgFile}.tmp`;
|
|
844
|
-
fs.writeFileSync(tmp, JSON.stringify(config, null, 2), 'utf-8');
|
|
845
|
-
fs.renameSync(tmp, orgFile);
|
|
846
|
-
res.writeHead(200, {
|
|
847
|
-
'Content-Type': 'application/json',
|
|
848
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
849
|
-
});
|
|
850
|
-
res.end(JSON.stringify({ ok: true }));
|
|
851
|
-
} catch (e) {
|
|
852
|
-
res.writeHead(500);
|
|
853
|
-
res.end(JSON.stringify({ ok: false, error: String(e.message || e) }));
|
|
854
|
-
}
|
|
855
|
-
return true;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// GET /api/org/:name/search?q=<query> — fuzzy search across org data
|
|
859
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/search(\?.*)?$/i.test(url)) {
|
|
860
|
-
try {
|
|
861
|
-
const urlObj = new URL(`http://x${req.url}`);
|
|
862
|
-
const orgName = decodeURIComponent(urlObj.pathname.split('/')[3]);
|
|
863
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
864
|
-
res.writeHead(400);
|
|
865
|
-
res.end('{}');
|
|
866
|
-
return true;
|
|
867
|
-
}
|
|
868
|
-
const q = (urlObj.searchParams.get('q') || '').toLowerCase().trim();
|
|
869
|
-
if (!q || q.length < 2) {
|
|
870
|
-
res.writeHead(200, {
|
|
871
|
-
'Content-Type': 'application/json',
|
|
872
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
873
|
-
});
|
|
874
|
-
res.end('{"hits":[]}');
|
|
875
|
-
return true;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const d = path.resolve(urlObj.searchParams.get('dir') || ctx.projectDir || process.cwd());
|
|
879
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
880
|
-
const readJ = (f) => {
|
|
881
|
-
try {
|
|
882
|
-
return JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
883
|
-
} catch (_) {
|
|
884
|
-
return null;
|
|
885
|
-
}
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
const hits = [];
|
|
889
|
-
const match = (str) => str?.toLowerCase().includes(q);
|
|
890
|
-
|
|
891
|
-
// Agents
|
|
892
|
-
const config = readJ(path.join(orgsDir, `${orgName}.json`));
|
|
893
|
-
for (const role of config?.roles || []) {
|
|
894
|
-
if (
|
|
895
|
-
match(role.id) ||
|
|
896
|
-
match(role.title) ||
|
|
897
|
-
(role.responsibilities || []).some((r) => match(r))
|
|
898
|
-
) {
|
|
899
|
-
hits.push({ type: 'agent', id: role.id, title: role.title, meta: role.agent_type });
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// Goals
|
|
904
|
-
const goals = readJ(path.join(orgsDir, `${orgName}-goals.json`));
|
|
905
|
-
for (const g of goals?.goals || []) {
|
|
906
|
-
if (match(g.title) || match(g.text) || match(g.goal) || match(g.description)) {
|
|
907
|
-
hits.push({
|
|
908
|
-
type: 'goal',
|
|
909
|
-
id: g.id,
|
|
910
|
-
title: g.title || g.text || g.goal,
|
|
911
|
-
meta: g.status || 'open',
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Routines
|
|
917
|
-
const routines = readJ(path.join(orgsDir, `${orgName}-routines.json`));
|
|
918
|
-
for (const r of routines?.routines || []) {
|
|
919
|
-
if (match(r.name) || match(r.description)) {
|
|
920
|
-
hits.push({ type: 'routine', id: r.name, title: r.name, meta: r.schedule || '' });
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Approvals
|
|
925
|
-
for (const a of approvalsOrEmpty(orgsDir, orgName)) {
|
|
926
|
-
if (match(a.action) || match(a.roleId)) {
|
|
927
|
-
hits.push({
|
|
928
|
-
type: 'approval',
|
|
929
|
-
id: a.id,
|
|
930
|
-
title: `${a.roleId}: ${a.action}`,
|
|
931
|
-
meta: a.status,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// Projects
|
|
937
|
-
const projects = readJ(path.join(orgsDir, `${orgName}-projects.json`));
|
|
938
|
-
for (const p of projects?.projects || []) {
|
|
939
|
-
if (match(p.name) || match(p.description)) {
|
|
940
|
-
hits.push({
|
|
941
|
-
type: 'project',
|
|
942
|
-
id: p.id || p.name,
|
|
943
|
-
title: p.name,
|
|
944
|
-
meta: p.status || 'active',
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Issues
|
|
950
|
-
const issuesData = readJ(path.join(orgsDir, `${orgName}-issues.json`));
|
|
951
|
-
for (const i of issuesData?.issues || []) {
|
|
952
|
-
if (match(i.title) || match(i.description) || match(i.slug)) {
|
|
953
|
-
hits.push({
|
|
954
|
-
type: 'issue',
|
|
955
|
-
id: i.id || i.slug,
|
|
956
|
-
title: i.title || i.slug,
|
|
957
|
-
meta: i.status || 'open',
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
// Recent activity events
|
|
963
|
-
const eventsFile = path.join(d, 'data', 'mastermind-events.jsonl');
|
|
964
|
-
if (fs.existsSync(eventsFile)) {
|
|
965
|
-
const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(Boolean).slice(-500);
|
|
966
|
-
for (const l of lines) {
|
|
967
|
-
try {
|
|
968
|
-
const e = JSON.parse(l);
|
|
969
|
-
if (e.org === orgName && match(JSON.stringify(e))) {
|
|
970
|
-
hits.push({
|
|
971
|
-
type: 'event',
|
|
972
|
-
id: String(e.ts),
|
|
973
|
-
title: e.type,
|
|
974
|
-
meta: e.role || e.task || '',
|
|
975
|
-
});
|
|
976
|
-
if (hits.length >= 50) break;
|
|
977
|
-
}
|
|
978
|
-
} catch (_) {}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
res.writeHead(200, {
|
|
983
|
-
'Content-Type': 'application/json',
|
|
984
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
985
|
-
});
|
|
986
|
-
res.end(JSON.stringify({ q, hits: hits.slice(0, 50) }));
|
|
987
|
-
} catch (_) {
|
|
988
|
-
res.writeHead(500);
|
|
989
|
-
res.end('{}');
|
|
990
|
-
}
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// GET /api/org/:name/health — aggregate org health, from the same runtime
|
|
995
|
-
// sources as /runtime: live role sessions, the task DAG, this run's usage on
|
|
996
|
-
// the budget's enforcement basis, and how the last 7 days of runs ended.
|
|
997
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/health$/i)) {
|
|
998
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
999
|
-
return hilRespond(res, corsOrigin, async () => {
|
|
1000
|
-
const v = await orgRuntime.runtimeView(hilRoot(req, ctx), orgName);
|
|
1001
|
-
if (!v) throw Object.assign(new Error(`org "${orgName}" has no definition`), { status: 404 });
|
|
1002
|
-
const running = v.roles.filter((r) => r.status === 'running').length;
|
|
1003
|
-
const openTasks = v.tasks.filter((t) =>
|
|
1004
|
-
['pending', 'ready', 'blocked'].includes(t.status),
|
|
1005
|
-
).length;
|
|
1006
|
-
const activeTasks = v.tasks.filter((t) => t.status === 'running').length;
|
|
1007
|
-
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
1008
|
-
// A resumed run keeps its run id and appends a history line per stop:
|
|
1009
|
-
// count each run once, by its latest line (history is newest first).
|
|
1010
|
-
const seenRuns = new Set();
|
|
1011
|
-
const week = v.history.filter(
|
|
1012
|
-
(h) => (h.endedAt || 0) >= weekAgo && !seenRuns.has(h.run) && seenRuns.add(h.run),
|
|
1013
|
-
);
|
|
1014
|
-
// Only a boss's gate-checked org_complete with 'achieved' counts as success.
|
|
1015
|
-
const achieved = week.filter(
|
|
1016
|
-
(h) => h.closedBy === 'org-complete' && h.outcome?.status === 'achieved',
|
|
1017
|
-
).length;
|
|
1018
|
-
const { used, tokens } = v.budget;
|
|
1019
|
-
return {
|
|
1020
|
-
agents_running: running,
|
|
1021
|
-
agents_idle: v.roles.length - running,
|
|
1022
|
-
agents_active: running,
|
|
1023
|
-
open_issues: openTasks,
|
|
1024
|
-
in_progress_issues: activeTasks,
|
|
1025
|
-
tasks_pending: openTasks + activeTasks,
|
|
1026
|
-
budget_used_tokens: used,
|
|
1027
|
-
budget_max_tokens: tokens,
|
|
1028
|
-
budget_basis: v.budget.basis,
|
|
1029
|
-
budget_used_pct: tokens && used !== null ? Math.round((used / tokens) * 100) : null,
|
|
1030
|
-
run_success_rate_7d: week.length ? Math.round((achieved / week.length) * 100) : null,
|
|
1031
|
-
total_runs_7d: week.length,
|
|
1032
|
-
errors: [...v.problems, ...(v.liveError ? [`daemon: ${v.liveError}`] : [])],
|
|
1033
|
-
};
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// GET /api/org/:name/plugins — plugins from registry filtered/merged with org overrides
|
|
1038
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/plugins$/i)) {
|
|
1039
|
-
try {
|
|
1040
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1041
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1042
|
-
res.writeHead(400);
|
|
1043
|
-
res.end('Invalid org name');
|
|
1044
|
-
return true;
|
|
1045
|
-
}
|
|
1046
|
-
const _pluginsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1047
|
-
const base = path.join(
|
|
1048
|
-
path.resolve(_pluginsQs.get('dir') || ctx.projectDir || process.cwd()),
|
|
1049
|
-
'.monomind',
|
|
1050
|
-
);
|
|
1051
|
-
let plugins = [];
|
|
1052
|
-
try {
|
|
1053
|
-
const reg = JSON.parse(
|
|
1054
|
-
fs.readFileSync(path.join(base, 'plugins', 'registry.json'), 'utf8'),
|
|
1055
|
-
);
|
|
1056
|
-
plugins = reg.plugins || [];
|
|
1057
|
-
// Strip sensitive config fields from output
|
|
1058
|
-
plugins = plugins.map((p) => {
|
|
1059
|
-
const safe = { ...p };
|
|
1060
|
-
if (safe.config) {
|
|
1061
|
-
safe.config = Object.fromEntries(
|
|
1062
|
-
Object.entries(safe.config).map(([k, v]) =>
|
|
1063
|
-
/key|token|secret|password|api/i.test(k) ? [k, '***'] : [k, v],
|
|
1064
|
-
),
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
return safe;
|
|
1068
|
-
});
|
|
1069
|
-
} catch (_) {
|
|
1070
|
-
/* no global registry */
|
|
1071
|
-
}
|
|
1072
|
-
// Merge org-level overrides
|
|
1073
|
-
try {
|
|
1074
|
-
const orgPlugins = JSON.parse(
|
|
1075
|
-
fs.readFileSync(path.join(base, 'orgs', `${orgName}-plugins.json`), 'utf8'),
|
|
1076
|
-
);
|
|
1077
|
-
const overrideMap = {};
|
|
1078
|
-
(orgPlugins.plugins || []).forEach((p) => {
|
|
1079
|
-
overrideMap[p.id] = p;
|
|
1080
|
-
});
|
|
1081
|
-
if (Object.keys(overrideMap).length) {
|
|
1082
|
-
plugins = plugins.map((p) =>
|
|
1083
|
-
overrideMap[p.id] ? { ...p, ...overrideMap[p.id], _orgOverride: true } : p,
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
} catch (_) {
|
|
1087
|
-
/* no org-level overrides */
|
|
1088
|
-
}
|
|
1089
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1090
|
-
res.end(JSON.stringify({ plugins }));
|
|
1091
|
-
} catch (_) {
|
|
1092
|
-
res.writeHead(500);
|
|
1093
|
-
res.end('{}');
|
|
1094
|
-
}
|
|
1095
|
-
return true;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// GET /api/org/:name/runtime — what the org is doing now, from the runtime's
|
|
1099
|
-
// own sources (live daemon status, run bus, idle watchdog, history).
|
|
1100
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runtime$/i)) {
|
|
1101
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1102
|
-
return hilRespond(res, corsOrigin, async () => {
|
|
1103
|
-
const view = await orgRuntime.runtimeView(hilRoot(req, ctx), orgName);
|
|
1104
|
-
if (!view)
|
|
1105
|
-
throw Object.assign(new Error(`org "${orgName}" has no definition`), { status: 404 });
|
|
1106
|
-
return view;
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
// POST /api/org/:name/config — edit the org definition's goal, schedule,
|
|
1111
|
-
// run_config and default cost tier; refused unless the result passes the
|
|
1112
|
-
// same OrgDefSchema parse `org run` does.
|
|
1113
|
-
if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/config$/i)) {
|
|
1114
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1115
|
-
const root = hilRoot(req, ctx);
|
|
1116
|
-
return hilRespond(res, corsOrigin, async () => {
|
|
1117
|
-
const body = await readHilBody(req);
|
|
1118
|
-
if (!body || typeof body !== 'object')
|
|
1119
|
-
throw Object.assign(new Error('JSON body required'), { status: 400 });
|
|
1120
|
-
try {
|
|
1121
|
-
return { ok: true, def: orgRuntime.patchOrgConfig(root, orgName, body) };
|
|
1122
|
-
} catch (err) {
|
|
1123
|
-
if (err instanceof orgRuntime.ConfigRejected)
|
|
1124
|
-
throw Object.assign(new Error(err.message), { status: 400 });
|
|
1125
|
-
throw err;
|
|
1126
|
-
}
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// GET /api/org/:name/agents — agents from roles + merged heartbeat state
|
|
1131
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/agents$/i)) {
|
|
1132
|
-
try {
|
|
1133
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1134
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1135
|
-
res.writeHead(400);
|
|
1136
|
-
res.end('Invalid org name');
|
|
1137
|
-
return true;
|
|
1138
|
-
}
|
|
1139
|
-
const _agentsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1140
|
-
const d = path.resolve(_agentsQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1141
|
-
const base = path.join(d, '.monomind', 'orgs');
|
|
1142
|
-
const readJsonSafe = (f) => {
|
|
1143
|
-
try {
|
|
1144
|
-
return JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
1145
|
-
} catch (_) {
|
|
1146
|
-
return null;
|
|
1147
|
-
}
|
|
1148
|
-
};
|
|
1149
|
-
const config = readJsonSafe(path.join(base, `${orgName}.json`)) || {};
|
|
1150
|
-
const stateData = readJsonSafe(path.join(base, `${orgName}-state.json`)) || {};
|
|
1151
|
-
const agentState =
|
|
1152
|
-
stateData.agents || stateData.roles
|
|
1153
|
-
? stateData.agents || Object.fromEntries((stateData.roles || []).map((r) => [r.id, r]))
|
|
1154
|
-
: {};
|
|
1155
|
-
const roles = config.roles || [];
|
|
1156
|
-
const agents = roles.map((r) => {
|
|
1157
|
-
const s = agentState[r.id] || {};
|
|
1158
|
-
// Runtime and model the role actually runs with (cost tiers included).
|
|
1159
|
-
const m = orgRuntime.describeRoleModel(r, config);
|
|
1160
|
-
return {
|
|
1161
|
-
id: r.id,
|
|
1162
|
-
title: r.title || r.id,
|
|
1163
|
-
adapterType: m.runtime,
|
|
1164
|
-
adapterModel: m.model,
|
|
1165
|
-
effort: m.effort,
|
|
1166
|
-
tier: m.tier,
|
|
1167
|
-
governance: r.governance || null,
|
|
1168
|
-
reportsTo: r.reports_to || null,
|
|
1169
|
-
status: s.status || 'idle',
|
|
1170
|
-
lastHeartbeat: s.last_heartbeat || s.lastHeartbeat || null,
|
|
1171
|
-
tokensIn: s.tokens_in || 0,
|
|
1172
|
-
tokensOut: s.tokens_out || 0,
|
|
1173
|
-
cacheRead: s.cache_read_tokens || 0,
|
|
1174
|
-
cacheCreation: s.cache_creation_tokens || 0,
|
|
1175
|
-
tokensUsed: s.tokens_used || 0,
|
|
1176
|
-
costUsd: s.total_cost_usd || 0,
|
|
1177
|
-
};
|
|
1178
|
-
});
|
|
1179
|
-
res.writeHead(200, {
|
|
1180
|
-
'Content-Type': 'application/json',
|
|
1181
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1182
|
-
});
|
|
1183
|
-
res.end(JSON.stringify({ agents }));
|
|
1184
|
-
} catch (_) {
|
|
1185
|
-
res.writeHead(500);
|
|
1186
|
-
res.end('{"agents":[]}');
|
|
1187
|
-
}
|
|
1188
|
-
return true;
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// GET /api/org/:name/approvals — the org's tool/action approvals
|
|
1192
|
-
// (<org>/approvals.json, written by orgrt/approvals.ts), newest first.
|
|
1193
|
-
if (
|
|
1194
|
-
req.method === 'GET' &&
|
|
1195
|
-
url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals(\?.*)?$/i)
|
|
1196
|
-
) {
|
|
1197
|
-
const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
|
|
1198
|
-
return hilRespond(res, corsOrigin, () => {
|
|
1199
|
-
const approvals = hil.listApprovals(hilRoot(req, ctx), orgName);
|
|
1200
|
-
return { approvals, pending: approvals.filter((a) => a.status === 'pending').length };
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// GET /api/org/:name/gates — the org's decision gates (<org>/gates.json).
|
|
1205
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates(\?.*)?$/i)) {
|
|
1206
|
-
const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
|
|
1207
|
-
return hilRespond(res, corsOrigin, () => {
|
|
1208
|
-
const gates = hil.listGates(hilRoot(req, ctx), orgName);
|
|
1209
|
-
return { gates, pending: gates.filter((g) => g.status === 'pending').length };
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// GET /api/questions?dir= — ask_human questions (pending and answered) for
|
|
1214
|
-
// every org in one project, each with its `blocking` flag.
|
|
1215
|
-
if (req.method === 'GET' && (url === '/api/questions' || url.startsWith('/api/questions?'))) {
|
|
1216
|
-
return hilRespond(res, corsOrigin, () => ({
|
|
1217
|
-
questions: hil.pendingForProject(hilRoot(req, ctx)).questions,
|
|
1218
|
-
}));
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
// GET /api/hil?dir= — everything in one project that waits on a human:
|
|
1222
|
-
// questions, approvals and decision gates across all its orgs.
|
|
1223
|
-
if (req.method === 'GET' && (url === '/api/hil' || url.startsWith('/api/hil?'))) {
|
|
1224
|
-
return hilRespond(res, corsOrigin, () => hil.pendingForProject(hilRoot(req, ctx)));
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
// POST /api/questions/answer { dir, org, questionId, answer } — live to the
|
|
1228
|
-
// hosting daemon (operator credential), else queued for the org's next run.
|
|
1229
|
-
if (req.method === 'POST' && url === '/api/questions/answer') {
|
|
1230
|
-
return hilRespond(res, corsOrigin, async () => {
|
|
1231
|
-
const body = await readHilBody(req);
|
|
1232
|
-
const { org, questionId, answer } = body || {};
|
|
1233
|
-
if (!validOrgName(org) || !questionId || typeof answer !== 'string' || !answer.trim())
|
|
1234
|
-
throw Object.assign(new Error('org, questionId and a non-empty answer are required'), {
|
|
1235
|
-
status: 400,
|
|
1236
|
-
});
|
|
1237
|
-
const root = path.resolve(body.dir || ctx.projectDir || process.cwd());
|
|
1238
|
-
const r = await hil.answerQuestion(root, org, String(questionId), answer.trim());
|
|
1239
|
-
hilEvent(ctx, root, {
|
|
1240
|
-
type: 'org:question-answered',
|
|
1241
|
-
org,
|
|
1242
|
-
role: r.role,
|
|
1243
|
-
questionId,
|
|
1244
|
-
delivery: r.delivery,
|
|
1245
|
-
});
|
|
1246
|
-
return { ok: true, ...r };
|
|
1247
|
-
});
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// POST /api/org/:name/gates/:id { approved: boolean, resolution? } — resolve
|
|
1251
|
-
// a decision gate, live or (org not running) in gates.json.
|
|
1252
|
-
if (
|
|
1253
|
-
req.method === 'POST' &&
|
|
1254
|
-
url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/gates\/[^/?]+(\?.*)?$/i)
|
|
1255
|
-
) {
|
|
1256
|
-
const parts = url.split('?')[0].split('/');
|
|
1257
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
1258
|
-
const root = hilRoot(req, ctx);
|
|
1259
|
-
return hilRespond(res, corsOrigin, async () => {
|
|
1260
|
-
const gateId = decodeHilSegment(parts[5]);
|
|
1261
|
-
const body = await readHilBody(req);
|
|
1262
|
-
const resolution =
|
|
1263
|
-
typeof body?.resolution === 'string' && body.resolution.trim()
|
|
1264
|
-
? body.resolution.trim().slice(0, 4000)
|
|
1265
|
-
: undefined;
|
|
1266
|
-
if (typeof body?.approved !== 'boolean')
|
|
1267
|
-
throw Object.assign(new Error('approved (boolean) is required'), { status: 400 });
|
|
1268
|
-
const r = await hil.resolveGate(root, orgName, gateId, body.approved, resolution);
|
|
1269
|
-
const status = body.approved ? 'approved' : 'rejected';
|
|
1270
|
-
hilEvent(ctx, root, {
|
|
1271
|
-
type: 'org:gate:resolved',
|
|
1272
|
-
org: orgName,
|
|
1273
|
-
gateId,
|
|
1274
|
-
status,
|
|
1275
|
-
delivery: r.delivery,
|
|
1276
|
-
});
|
|
1277
|
-
return { ok: true, status, ...r };
|
|
1278
|
-
});
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// POST /api/org/:name/approvals/:requestId { action: "approve" | "reject" } —
|
|
1282
|
-
// resolve one tool/action approval, live or (org not running) in approvals.json.
|
|
1283
|
-
if (
|
|
1284
|
-
req.method === 'POST' &&
|
|
1285
|
-
url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/approvals\/[^/?]+(\?.*)?$/i)
|
|
1286
|
-
) {
|
|
1287
|
-
const parts = url.split('?')[0].split('/');
|
|
1288
|
-
const orgName = decodeURIComponent(parts[3]);
|
|
1289
|
-
const root = hilRoot(req, ctx);
|
|
1290
|
-
return hilRespond(res, corsOrigin, async () => {
|
|
1291
|
-
const requestId = decodeHilSegment(parts[5]);
|
|
1292
|
-
const body = await readHilBody(req);
|
|
1293
|
-
if (body?.action !== 'approve' && body?.action !== 'reject')
|
|
1294
|
-
throw Object.assign(new Error('action must be approve or reject'), { status: 400 });
|
|
1295
|
-
const approved = body.action === 'approve';
|
|
1296
|
-
const r = await hil.resolveApproval(root, orgName, requestId, approved);
|
|
1297
|
-
const status = approved ? 'approved' : 'denied';
|
|
1298
|
-
hilEvent(ctx, root, {
|
|
1299
|
-
type: 'org:approval:resolved',
|
|
1300
|
-
org: orgName,
|
|
1301
|
-
requestId,
|
|
1302
|
-
status,
|
|
1303
|
-
delivery: r.delivery,
|
|
1304
|
-
});
|
|
1305
|
-
return { ok: true, status, ...r };
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// GET /api/org/:name/budgets — org and per-agent budget data
|
|
1310
|
-
// Returns: { org_budget: {limit_tokens, limit_usd}, agent_budgets: {agentId: {limit_usd}}, agents: [{id, title, tokens_in, tokens_out, total_cost_usd}] }
|
|
1311
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/budgets$/i)) {
|
|
1312
|
-
try {
|
|
1313
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1314
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1315
|
-
res.writeHead(400);
|
|
1316
|
-
res.end('Invalid org name');
|
|
1317
|
-
return true;
|
|
1318
|
-
}
|
|
1319
|
-
const _budgetsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1320
|
-
const base = path.join(
|
|
1321
|
-
path.resolve(_budgetsQs.get('dir') || ctx.projectDir || process.cwd()),
|
|
1322
|
-
'.monomind',
|
|
1323
|
-
'orgs',
|
|
1324
|
-
);
|
|
1325
|
-
let budgetData = { org_budget: {}, agent_budgets: {}, period: 'monthly', currency: 'USD' };
|
|
1326
|
-
try {
|
|
1327
|
-
budgetData = JSON.parse(
|
|
1328
|
-
fs.readFileSync(path.join(base, `${orgName}-budgets.json`), 'utf8'),
|
|
1329
|
-
);
|
|
1330
|
-
} catch (_) {}
|
|
1331
|
-
// Enrich with per-agent spend from state file.
|
|
1332
|
-
// State file format: { agents: { "<role_id>": { tokens_in, tokens_out, ... } } }
|
|
1333
|
-
let agents = [];
|
|
1334
|
-
try {
|
|
1335
|
-
const state = JSON.parse(fs.readFileSync(path.join(base, `${orgName}-state.json`), 'utf8'));
|
|
1336
|
-
const agentMap = state.agents || {};
|
|
1337
|
-
// Also load role titles from org config for enrichment
|
|
1338
|
-
const roleMap = {};
|
|
1339
|
-
try {
|
|
1340
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(base, `${orgName}.json`), 'utf8'));
|
|
1341
|
-
(cfg.roles || []).forEach((r) => {
|
|
1342
|
-
roleMap[r.id] = r.title || r.id;
|
|
1343
|
-
});
|
|
1344
|
-
} catch (_) {}
|
|
1345
|
-
agents = Object.entries(agentMap).map(([id, s]) => ({
|
|
1346
|
-
id,
|
|
1347
|
-
title: roleMap[id] || s.title || id,
|
|
1348
|
-
tokens_in: s.tokens_in || 0,
|
|
1349
|
-
tokens_out: s.tokens_out || 0,
|
|
1350
|
-
tokens_used: s.tokens_used || (s.tokens_in || 0) + (s.tokens_out || 0),
|
|
1351
|
-
total_cost_usd: s.total_cost_usd || 0,
|
|
1352
|
-
}));
|
|
1353
|
-
} catch (_) {}
|
|
1354
|
-
// Scan org run jsonl files for usage events (fallback when state.json has no token data).
|
|
1355
|
-
// Two event-type variants show up here in practice and must BOTH be matched, each with its
|
|
1356
|
-
// own field mapping — matching only one silently drops real v2 cost data from the UI:
|
|
1357
|
-
// - 'agent:usage': flattened { role, tokens_in, tokens_out, cost_usd } (legacy/direct writers).
|
|
1358
|
-
// - 'org:usage': orgrt's actual forwarded shape (attachForwarder's translate() default case
|
|
1359
|
-
// for a raw OrgBus 'usage' event) — { from, data: { tokens, cost_usd } }. orgrt never emits
|
|
1360
|
-
// 'agent:usage' itself, so scanning for that alone means this fallback never fires for real runs.
|
|
1361
|
-
const _hasTokenData = agents.some(
|
|
1362
|
-
(a) => a.tokens_in > 0 || a.tokens_out > 0 || a.total_cost_usd > 0,
|
|
1363
|
-
);
|
|
1364
|
-
if (!_hasTokenData) {
|
|
1365
|
-
try {
|
|
1366
|
-
const _runsDir = path.join(base, orgName, 'runs');
|
|
1367
|
-
if (fs.existsSync(_runsDir)) {
|
|
1368
|
-
const _usageByRole = {};
|
|
1369
|
-
const _bump = (role, tokensIn, tokensOut, tokensTotal, costUsd) => {
|
|
1370
|
-
if (!role) return;
|
|
1371
|
-
role = String(role).trim();
|
|
1372
|
-
if (!role) return;
|
|
1373
|
-
if (!_usageByRole[role])
|
|
1374
|
-
_usageByRole[role] = {
|
|
1375
|
-
tokens_in: 0,
|
|
1376
|
-
tokens_out: 0,
|
|
1377
|
-
tokens_used: 0,
|
|
1378
|
-
total_cost_usd: 0,
|
|
1379
|
-
};
|
|
1380
|
-
_usageByRole[role].tokens_in += tokensIn;
|
|
1381
|
-
_usageByRole[role].tokens_out += tokensOut;
|
|
1382
|
-
_usageByRole[role].tokens_used += tokensIn + tokensOut + tokensTotal;
|
|
1383
|
-
_usageByRole[role].total_cost_usd += costUsd;
|
|
1384
|
-
};
|
|
1385
|
-
for (const f of fs.readdirSync(_runsDir)) {
|
|
1386
|
-
if (!f.endsWith('.jsonl') || f.startsWith('._')) continue;
|
|
1387
|
-
const lines = fs
|
|
1388
|
-
.readFileSync(path.join(_runsDir, f), 'utf8')
|
|
1389
|
-
.split('\n')
|
|
1390
|
-
.filter(Boolean);
|
|
1391
|
-
for (const l of lines) {
|
|
1392
|
-
try {
|
|
1393
|
-
const ev = JSON.parse(l);
|
|
1394
|
-
if (ev.type === 'agent:usage' && ev.role) {
|
|
1395
|
-
_bump(
|
|
1396
|
-
ev.role,
|
|
1397
|
-
Number(ev.tokens_in) || 0,
|
|
1398
|
-
Number(ev.tokens_out) || 0,
|
|
1399
|
-
0,
|
|
1400
|
-
Number(ev.cost_usd) || 0,
|
|
1401
|
-
);
|
|
1402
|
-
} else if (ev.type === 'org:usage' && ev.from) {
|
|
1403
|
-
_bump(
|
|
1404
|
-
ev.from,
|
|
1405
|
-
0,
|
|
1406
|
-
0,
|
|
1407
|
-
Number(ev.data?.tokens) || 0,
|
|
1408
|
-
Number(ev.data?.cost_usd) || 0,
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
} catch (_) {}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
if (Object.keys(_usageByRole).length > 0) {
|
|
1415
|
-
// Merge usage into agents list; preserve role titles
|
|
1416
|
-
agents = agents.map((a) => {
|
|
1417
|
-
const u = _usageByRole[a.id] || {};
|
|
1418
|
-
return {
|
|
1419
|
-
...a,
|
|
1420
|
-
tokens_in: u.tokens_in || 0,
|
|
1421
|
-
tokens_out: u.tokens_out || 0,
|
|
1422
|
-
tokens_used: u.tokens_used || (u.tokens_in || 0) + (u.tokens_out || 0),
|
|
1423
|
-
total_cost_usd: u.total_cost_usd || 0,
|
|
1424
|
-
};
|
|
1425
|
-
});
|
|
1426
|
-
// Add any roles that appeared in events but aren't in config
|
|
1427
|
-
for (const [role, u] of Object.entries(_usageByRole)) {
|
|
1428
|
-
if (!agents.find((a) => a.id === role))
|
|
1429
|
-
agents.push({ id: role, title: role, ...u });
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
} catch (_) {}
|
|
1434
|
-
}
|
|
1435
|
-
// Do NOT fall back to zero-value role stubs — empty agents array is the honest signal
|
|
1436
|
-
// that no usage has been tracked yet; the UI shows "No cost data" rather than $0.0000 rows.
|
|
1437
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1438
|
-
res.end(JSON.stringify({ ...budgetData, agents }));
|
|
1439
|
-
} catch (_) {
|
|
1440
|
-
res.writeHead(500);
|
|
1441
|
-
res.end('{"org_budget":{},"agent_budgets":{},"agents":[]}');
|
|
1442
|
-
}
|
|
1443
|
-
return true;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// GET /api/org/:name/threads — conversation threads from threads.jsonl
|
|
1447
|
-
// Returns: { threads: [{id, subject, authorId, authorName, issueId, createdAt, messages:[]}] }
|
|
1448
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/threads$/i)) {
|
|
1449
|
-
try {
|
|
1450
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1451
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1452
|
-
res.writeHead(400);
|
|
1453
|
-
res.end('Invalid org name');
|
|
1454
|
-
return true;
|
|
1455
|
-
}
|
|
1456
|
-
const _threadsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1457
|
-
const _threadsRoot = path.resolve(_threadsQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1458
|
-
const _threadsProjDir = ctx._resolveOrgProjectDir(orgName, _threadsRoot) || _threadsRoot;
|
|
1459
|
-
const threadsFile = path.join(
|
|
1460
|
-
_threadsProjDir,
|
|
1461
|
-
'.monomind',
|
|
1462
|
-
'orgs',
|
|
1463
|
-
`${orgName}-threads.jsonl`,
|
|
1464
|
-
);
|
|
1465
|
-
let threads = [];
|
|
1466
|
-
try {
|
|
1467
|
-
const lines = fs
|
|
1468
|
-
.readFileSync(threadsFile, 'utf8')
|
|
1469
|
-
.split('\n')
|
|
1470
|
-
.filter((l) => l.trim());
|
|
1471
|
-
threads = lines
|
|
1472
|
-
.map((l) => {
|
|
1473
|
-
try {
|
|
1474
|
-
return JSON.parse(l);
|
|
1475
|
-
} catch (_) {
|
|
1476
|
-
return null;
|
|
1477
|
-
}
|
|
1478
|
-
})
|
|
1479
|
-
.filter(Boolean);
|
|
1480
|
-
// Group 'message' entries (from org:comms) by run_id into synthetic thread objects
|
|
1481
|
-
const msgsByRun = {};
|
|
1482
|
-
threads
|
|
1483
|
-
.filter((t) => t.type === 'message')
|
|
1484
|
-
.forEach((m) => {
|
|
1485
|
-
const rid = m.run_id || 'unknown';
|
|
1486
|
-
if (!msgsByRun[rid])
|
|
1487
|
-
msgsByRun[rid] = {
|
|
1488
|
-
id: `thread-${rid}`,
|
|
1489
|
-
type: 'thread',
|
|
1490
|
-
subject: `Run ${rid}`,
|
|
1491
|
-
run_id: rid,
|
|
1492
|
-
createdAt: m.ts,
|
|
1493
|
-
messages: [],
|
|
1494
|
-
};
|
|
1495
|
-
msgsByRun[rid].messages.push({ from: m.from, to: m.to, msg: m.msg, ts: m.ts });
|
|
1496
|
-
});
|
|
1497
|
-
const syntheticThreads = Object.values(msgsByRun).map((t) => ({
|
|
1498
|
-
...t,
|
|
1499
|
-
messageCount: t.messages.length,
|
|
1500
|
-
author: t.messages[0]?.from || null,
|
|
1501
|
-
}));
|
|
1502
|
-
threads = threads
|
|
1503
|
-
.filter((t) => t.type === 'thread' || !t.type)
|
|
1504
|
-
.map((t) => ({
|
|
1505
|
-
...t,
|
|
1506
|
-
author: t.author || t.authorName || t.createdBy || t.authorId || null,
|
|
1507
|
-
messageCount:
|
|
1508
|
-
t.messageCount != null
|
|
1509
|
-
? t.messageCount
|
|
1510
|
-
: Array.isArray(t.messages)
|
|
1511
|
-
? t.messages.length
|
|
1512
|
-
: typeof t.messages === 'number'
|
|
1513
|
-
? t.messages
|
|
1514
|
-
: null,
|
|
1515
|
-
}));
|
|
1516
|
-
threads = [...threads, ...syntheticThreads];
|
|
1517
|
-
} catch (_) {}
|
|
1518
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1519
|
-
res.end(JSON.stringify({ threads }));
|
|
1520
|
-
} catch (_) {
|
|
1521
|
-
res.writeHead(500);
|
|
1522
|
-
res.end('{"threads":[]}');
|
|
1523
|
-
}
|
|
1524
|
-
return true;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// GET /api/org/:name/routines — read org routines (falls back to synthesizing from org config's loop object)
|
|
1528
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
|
|
1529
|
-
try {
|
|
1530
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1531
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1532
|
-
res.writeHead(400);
|
|
1533
|
-
res.end('Invalid org name');
|
|
1534
|
-
return true;
|
|
1535
|
-
}
|
|
1536
|
-
const _routinesQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1537
|
-
const _routinesBase = path.resolve(_routinesQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1538
|
-
const _routinesProjDir = ctx._resolveOrgProjectDir(orgName, _routinesBase) || _routinesBase;
|
|
1539
|
-
const routinesFile = path.join(
|
|
1540
|
-
_routinesProjDir,
|
|
1541
|
-
'.monomind',
|
|
1542
|
-
'orgs',
|
|
1543
|
-
`${orgName}-routines.json`,
|
|
1544
|
-
);
|
|
1545
|
-
let data = { routines: [] };
|
|
1546
|
-
try {
|
|
1547
|
-
data = JSON.parse(fs.readFileSync(routinesFile, 'utf8'));
|
|
1548
|
-
} catch (_) {}
|
|
1549
|
-
// Synthesize routines from org config's loop/schedule settings when no explicit routines are defined
|
|
1550
|
-
if (!data.routines?.length) {
|
|
1551
|
-
try {
|
|
1552
|
-
const orgCfg = JSON.parse(
|
|
1553
|
-
fs.readFileSync(
|
|
1554
|
-
path.join(_routinesProjDir, '.monomind', 'orgs', `${orgName}.json`),
|
|
1555
|
-
'utf8',
|
|
1556
|
-
),
|
|
1557
|
-
);
|
|
1558
|
-
const loop = orgCfg.loop;
|
|
1559
|
-
if (loop && (loop.poll_interval_minutes || loop.interval_minutes)) {
|
|
1560
|
-
const intervalMin = loop.poll_interval_minutes || loop.interval_minutes;
|
|
1561
|
-
data.routines = [
|
|
1562
|
-
{
|
|
1563
|
-
name: `${orgName}-cycle`,
|
|
1564
|
-
description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org iteration cycle',
|
|
1565
|
-
schedule: `every ${intervalMin}m`,
|
|
1566
|
-
cron: null,
|
|
1567
|
-
enabled: orgCfg.status === 'active',
|
|
1568
|
-
status: orgCfg.status || 'stopped',
|
|
1569
|
-
prompt_file: loop.run_prompt_file || null,
|
|
1570
|
-
source: 'loop-config',
|
|
1571
|
-
lastRun: null,
|
|
1572
|
-
},
|
|
1573
|
-
];
|
|
1574
|
-
} else if (orgCfg.schedule) {
|
|
1575
|
-
data.routines = [
|
|
1576
|
-
{
|
|
1577
|
-
name: `${orgName}-schedule`,
|
|
1578
|
-
description: orgCfg.goal ? orgCfg.goal.slice(0, 120) : 'Org scheduled run',
|
|
1579
|
-
schedule: String(orgCfg.schedule),
|
|
1580
|
-
cron: null,
|
|
1581
|
-
enabled: orgCfg.status === 'active',
|
|
1582
|
-
status: orgCfg.status || 'stopped',
|
|
1583
|
-
source: 'schedule-config',
|
|
1584
|
-
lastRun: null,
|
|
1585
|
-
},
|
|
1586
|
-
];
|
|
1587
|
-
}
|
|
1588
|
-
} catch (_) {}
|
|
1589
|
-
}
|
|
1590
|
-
res.writeHead(200, {
|
|
1591
|
-
'Content-Type': 'application/json',
|
|
1592
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1593
|
-
});
|
|
1594
|
-
res.end(JSON.stringify({ routines: data.routines || [] }));
|
|
1595
|
-
} catch (_) {
|
|
1596
|
-
res.writeHead(500);
|
|
1597
|
-
res.end('{"routines":[]}');
|
|
1598
|
-
}
|
|
1599
|
-
return true;
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// POST /api/org/:name/goals — upsert the org goals file
|
|
1603
|
-
// Body: { goals: [{id, title, description, status, priority, assignee_id, created_at}] }
|
|
1604
|
-
if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/goals$/i)) {
|
|
1605
|
-
let body = '';
|
|
1606
|
-
for await (const chunk of req) {
|
|
1607
|
-
body += chunk;
|
|
1608
|
-
if (body.length > 2097152) {
|
|
1609
|
-
req.destroy();
|
|
1610
|
-
break;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
try {
|
|
1614
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1615
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1616
|
-
res.writeHead(400);
|
|
1617
|
-
res.end('Invalid org name');
|
|
1618
|
-
return true;
|
|
1619
|
-
}
|
|
1620
|
-
const parsed = JSON.parse(body);
|
|
1621
|
-
if (!parsed || !Array.isArray(parsed.goals)) {
|
|
1622
|
-
res.writeHead(400);
|
|
1623
|
-
res.end('{"error":"goals array required"}');
|
|
1624
|
-
return true;
|
|
1625
|
-
}
|
|
1626
|
-
const _postGoalsQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1627
|
-
const goalsFile = path.join(
|
|
1628
|
-
path.resolve(_postGoalsQs.get('dir') || ctx.projectDir || process.cwd()),
|
|
1629
|
-
'.monomind',
|
|
1630
|
-
'orgs',
|
|
1631
|
-
`${orgName}-goals.json`,
|
|
1632
|
-
);
|
|
1633
|
-
const tmp = `${goalsFile}.tmp`;
|
|
1634
|
-
const payload = { org: orgName, updated_at: new Date().toISOString(), goals: parsed.goals };
|
|
1635
|
-
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
1636
|
-
fs.renameSync(tmp, goalsFile);
|
|
1637
|
-
res.writeHead(200, {
|
|
1638
|
-
'Content-Type': 'application/json',
|
|
1639
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1640
|
-
});
|
|
1641
|
-
res.end(JSON.stringify({ ok: true, count: parsed.goals.length }));
|
|
1642
|
-
} catch (_) {
|
|
1643
|
-
res.writeHead(500);
|
|
1644
|
-
res.end(`{"error":"${String(_).replace(/"/g, '\\"')}"}`);
|
|
1645
|
-
}
|
|
1646
|
-
return true;
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
// POST /api/org/:name/routines — upsert the org routines file
|
|
1650
|
-
// Body: { routines: [{name, description, schedule, enabled, last_run, next_run}] }
|
|
1651
|
-
if (req.method === 'POST' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/routines$/i)) {
|
|
1652
|
-
let body = '';
|
|
1653
|
-
for await (const chunk of req) {
|
|
1654
|
-
body += chunk;
|
|
1655
|
-
if (body.length > 2097152) {
|
|
1656
|
-
req.destroy();
|
|
1657
|
-
break;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
try {
|
|
1661
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1662
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1663
|
-
res.writeHead(400);
|
|
1664
|
-
res.end('Invalid org name');
|
|
1665
|
-
return true;
|
|
1666
|
-
}
|
|
1667
|
-
const parsed = JSON.parse(body);
|
|
1668
|
-
if (!parsed || !Array.isArray(parsed.routines)) {
|
|
1669
|
-
res.writeHead(400);
|
|
1670
|
-
res.end('{"error":"routines array required"}');
|
|
1671
|
-
return true;
|
|
1672
|
-
}
|
|
1673
|
-
const _postRoutinesQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1674
|
-
const routinesFile = path.join(
|
|
1675
|
-
path.resolve(_postRoutinesQs.get('dir') || ctx.projectDir || process.cwd()),
|
|
1676
|
-
'.monomind',
|
|
1677
|
-
'orgs',
|
|
1678
|
-
`${orgName}-routines.json`,
|
|
1679
|
-
);
|
|
1680
|
-
const tmp = `${routinesFile}.tmp`;
|
|
1681
|
-
const payload = {
|
|
1682
|
-
org: orgName,
|
|
1683
|
-
updated_at: new Date().toISOString(),
|
|
1684
|
-
routines: parsed.routines,
|
|
1685
|
-
};
|
|
1686
|
-
fs.writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf-8');
|
|
1687
|
-
fs.renameSync(tmp, routinesFile);
|
|
1688
|
-
res.writeHead(200, {
|
|
1689
|
-
'Content-Type': 'application/json',
|
|
1690
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1691
|
-
});
|
|
1692
|
-
res.end(JSON.stringify({ ok: true, count: parsed.routines.length }));
|
|
1693
|
-
} catch (_) {
|
|
1694
|
-
res.writeHead(500);
|
|
1695
|
-
res.end(`{"error":"${String(_).replace(/"/g, '\\"')}"}`);
|
|
1696
|
-
}
|
|
1697
|
-
return true;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// GET /api/org/:name/files — all files related to an org
|
|
1701
|
-
if (req.method === 'GET' && url.match(/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/files$/i)) {
|
|
1702
|
-
try {
|
|
1703
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1704
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1705
|
-
res.writeHead(400);
|
|
1706
|
-
res.end('{"error":"Invalid org name"}');
|
|
1707
|
-
return true;
|
|
1708
|
-
}
|
|
1709
|
-
const _filesQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1710
|
-
const d = path.resolve(_filesQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1711
|
-
const orgsDir = path.join(d, '.monomind', 'orgs');
|
|
1712
|
-
const files = [];
|
|
1713
|
-
const seen = new Set();
|
|
1714
|
-
const addFile = (fp, type) => {
|
|
1715
|
-
if (seen.has(fp)) return;
|
|
1716
|
-
seen.add(fp);
|
|
1717
|
-
try {
|
|
1718
|
-
const st = fs.statSync(fp);
|
|
1719
|
-
files.push({
|
|
1720
|
-
name: path.basename(fp),
|
|
1721
|
-
path: fp,
|
|
1722
|
-
type,
|
|
1723
|
-
size: st.size,
|
|
1724
|
-
mtime: st.mtime.toISOString(),
|
|
1725
|
-
});
|
|
1726
|
-
} catch (_) {}
|
|
1727
|
-
};
|
|
1728
|
-
addFile(path.join(orgsDir, `${orgName}.json`), 'config');
|
|
1729
|
-
for (const s of [
|
|
1730
|
-
'-state',
|
|
1731
|
-
'-approvals',
|
|
1732
|
-
'-goals',
|
|
1733
|
-
'-routines',
|
|
1734
|
-
'-projects',
|
|
1735
|
-
'-members',
|
|
1736
|
-
'-issues',
|
|
1737
|
-
'-threads',
|
|
1738
|
-
'-budgets',
|
|
1739
|
-
]) {
|
|
1740
|
-
const fp = path.join(orgsDir, `${orgName + s}.json`);
|
|
1741
|
-
if (fs.existsSync(fp)) addFile(fp, s.slice(1));
|
|
1742
|
-
}
|
|
1743
|
-
const walkDir = (dir, depth) => {
|
|
1744
|
-
if (depth > 3) return;
|
|
1745
|
-
let entries;
|
|
1746
|
-
try {
|
|
1747
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1748
|
-
} catch (_) {
|
|
1749
|
-
return;
|
|
1750
|
-
}
|
|
1751
|
-
for (const e of entries) {
|
|
1752
|
-
if (e.name.startsWith('.')) continue;
|
|
1753
|
-
const fp = path.join(dir, e.name);
|
|
1754
|
-
if (e.isDirectory()) walkDir(fp, depth + 1);
|
|
1755
|
-
else addFile(fp, 'generated');
|
|
1756
|
-
}
|
|
1757
|
-
};
|
|
1758
|
-
const orgWorkDir = path.join(orgsDir, orgName);
|
|
1759
|
-
if (fs.existsSync(orgWorkDir)) walkDir(orgWorkDir, 0);
|
|
1760
|
-
let orgCfg = null;
|
|
1761
|
-
try {
|
|
1762
|
-
orgCfg = JSON.parse(fs.readFileSync(path.join(orgsDir, `${orgName}.json`), 'utf8'));
|
|
1763
|
-
} catch (_) {}
|
|
1764
|
-
if (orgCfg && Array.isArray(orgCfg.roles)) {
|
|
1765
|
-
const agentsDir = path.join(d, '.claude', 'agents');
|
|
1766
|
-
const walkAgents = (dir) => {
|
|
1767
|
-
let entries;
|
|
1768
|
-
try {
|
|
1769
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1770
|
-
} catch (_) {
|
|
1771
|
-
return;
|
|
1772
|
-
}
|
|
1773
|
-
for (const e of entries) {
|
|
1774
|
-
if (e.isDirectory()) {
|
|
1775
|
-
walkAgents(path.join(dir, e.name));
|
|
1776
|
-
continue;
|
|
1777
|
-
}
|
|
1778
|
-
if (!e.name.endsWith('.md')) continue;
|
|
1779
|
-
const fp = path.join(dir, e.name);
|
|
1780
|
-
const base = e.name.replace('.md', '').toLowerCase();
|
|
1781
|
-
if (
|
|
1782
|
-
orgCfg.roles.some(
|
|
1783
|
-
(r) =>
|
|
1784
|
-
base === (r.id || '').toLowerCase() ||
|
|
1785
|
-
base === (r.agent_type || '').toLowerCase() ||
|
|
1786
|
-
(r.instructions_file || '').endsWith(e.name),
|
|
1787
|
-
)
|
|
1788
|
-
)
|
|
1789
|
-
addFile(fp, 'agent-definition');
|
|
1790
|
-
}
|
|
1791
|
-
};
|
|
1792
|
-
if (fs.existsSync(agentsDir)) walkAgents(agentsDir);
|
|
1793
|
-
}
|
|
1794
|
-
files.sort((a, b) => new Date(b.mtime) - new Date(a.mtime));
|
|
1795
|
-
res.writeHead(200, {
|
|
1796
|
-
'Content-Type': 'application/json',
|
|
1797
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1798
|
-
'Cache-Control': 'no-cache',
|
|
1799
|
-
});
|
|
1800
|
-
res.end(JSON.stringify(files));
|
|
1801
|
-
} catch (e) {
|
|
1802
|
-
res.writeHead(500);
|
|
1803
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
1804
|
-
}
|
|
1805
|
-
return true;
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
// GET /api/file-content — return raw text content of a .monomind file
|
|
1809
|
-
if (req.method === 'GET' && url === '/api/file-content') {
|
|
1810
|
-
try {
|
|
1811
|
-
const _fcQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1812
|
-
const rawPath = _fcQs.get('path');
|
|
1813
|
-
const baseDir = path.resolve(_fcQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1814
|
-
if (!rawPath) {
|
|
1815
|
-
res.writeHead(400);
|
|
1816
|
-
res.end('Missing path');
|
|
1817
|
-
return true;
|
|
1818
|
-
}
|
|
1819
|
-
const resolved = path.resolve(rawPath);
|
|
1820
|
-
// Security: must be inside .monomind of the project dir
|
|
1821
|
-
const monomindDir = path.join(baseDir, '.monomind');
|
|
1822
|
-
if (!resolved.startsWith(monomindDir + path.sep) && resolved !== monomindDir) {
|
|
1823
|
-
res.writeHead(403);
|
|
1824
|
-
res.end('Forbidden');
|
|
1825
|
-
return true;
|
|
1826
|
-
}
|
|
1827
|
-
if (!fs.existsSync(resolved)) {
|
|
1828
|
-
res.writeHead(404);
|
|
1829
|
-
res.end('Not found');
|
|
1830
|
-
return true;
|
|
1831
|
-
}
|
|
1832
|
-
const stat = fs.statSync(resolved);
|
|
1833
|
-
if (!stat.isFile()) {
|
|
1834
|
-
res.writeHead(400);
|
|
1835
|
-
res.end('Not a file');
|
|
1836
|
-
return true;
|
|
1837
|
-
}
|
|
1838
|
-
if (stat.size > 524288) {
|
|
1839
|
-
res.writeHead(413);
|
|
1840
|
-
res.end('File too large');
|
|
1841
|
-
return true;
|
|
1842
|
-
}
|
|
1843
|
-
const _fcMime = ctx._detectMimeType(resolved);
|
|
1844
|
-
if (_fcMime.startsWith('image/')) {
|
|
1845
|
-
res.writeHead(200, {
|
|
1846
|
-
'Content-Type': _fcMime,
|
|
1847
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1848
|
-
});
|
|
1849
|
-
res.end(fs.readFileSync(resolved));
|
|
1850
|
-
return true;
|
|
1851
|
-
}
|
|
1852
|
-
const content = fs.readFileSync(resolved, 'utf8');
|
|
1853
|
-
res.writeHead(200, {
|
|
1854
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
1855
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1856
|
-
});
|
|
1857
|
-
res.end(content);
|
|
1858
|
-
} catch (_) {
|
|
1859
|
-
res.writeHead(500);
|
|
1860
|
-
res.end('Internal error');
|
|
1861
|
-
}
|
|
1862
|
-
return true;
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
// DELETE /api/orgs/:name — delete an org config and all associated data files
|
|
1866
|
-
if (req.method === 'DELETE' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}(\?.*)?$/i)) {
|
|
1867
|
-
try {
|
|
1868
|
-
const orgName = decodeURIComponent(url.split('/')[3].split('?')[0]);
|
|
1869
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1870
|
-
res.writeHead(400);
|
|
1871
|
-
res.end('Invalid org name');
|
|
1872
|
-
return true;
|
|
1873
|
-
}
|
|
1874
|
-
const _delOrgQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1875
|
-
const orgsDir = path.join(
|
|
1876
|
-
path.resolve(_delOrgQs.get('dir') || ctx.projectDir || process.cwd()),
|
|
1877
|
-
'.monomind',
|
|
1878
|
-
'orgs',
|
|
1879
|
-
);
|
|
1880
|
-
const configFile = path.join(orgsDir, `${orgName}.json`);
|
|
1881
|
-
const v1ConfigFile = path.join(orgsDir, `${orgName}.v1.json`);
|
|
1882
|
-
if (!fs.existsSync(configFile) && !fs.existsSync(v1ConfigFile)) {
|
|
1883
|
-
res.writeHead(404);
|
|
1884
|
-
res.end('{"error":"org not found"}');
|
|
1885
|
-
return true;
|
|
1886
|
-
}
|
|
1887
|
-
// Remove all org-associated files (config + state + data)
|
|
1888
|
-
try {
|
|
1889
|
-
if (fs.existsSync(v1ConfigFile)) fs.unlinkSync(v1ConfigFile);
|
|
1890
|
-
} catch (_) {}
|
|
1891
|
-
const suffixes = [
|
|
1892
|
-
'',
|
|
1893
|
-
'-state',
|
|
1894
|
-
'-goals',
|
|
1895
|
-
'-routines',
|
|
1896
|
-
'-approvals',
|
|
1897
|
-
'-activity',
|
|
1898
|
-
'-issues',
|
|
1899
|
-
'-members',
|
|
1900
|
-
'-projects',
|
|
1901
|
-
'-workspaces',
|
|
1902
|
-
'-worktrees',
|
|
1903
|
-
'-environments',
|
|
1904
|
-
'-plugins',
|
|
1905
|
-
'-adapters',
|
|
1906
|
-
'-budgets',
|
|
1907
|
-
'-threads',
|
|
1908
|
-
'-secrets',
|
|
1909
|
-
'-join-requests',
|
|
1910
|
-
'-bootstrap',
|
|
1911
|
-
'-project-workspaces',
|
|
1912
|
-
'-approval-comments',
|
|
1913
|
-
'-skills',
|
|
1914
|
-
];
|
|
1915
|
-
for (const suf of suffixes) {
|
|
1916
|
-
const f = path.join(orgsDir, `${orgName}${suf}.json`);
|
|
1917
|
-
try {
|
|
1918
|
-
if (fs.existsSync(f)) fs.unlinkSync(f);
|
|
1919
|
-
} catch (_) {}
|
|
1920
|
-
const fjsonl = path.join(orgsDir, `${orgName}${suf}.jsonl`);
|
|
1921
|
-
try {
|
|
1922
|
-
if (fs.existsSync(fjsonl)) fs.unlinkSync(fjsonl);
|
|
1923
|
-
} catch (_) {}
|
|
1924
|
-
}
|
|
1925
|
-
// Remove stop file if present
|
|
1926
|
-
try {
|
|
1927
|
-
fs.unlinkSync(path.join(orgsDir, '.stops', `${orgName}.stop`));
|
|
1928
|
-
} catch (_) {}
|
|
1929
|
-
// Remove org subdirectory under .monomind/orgs/ (legacy flat-file location)
|
|
1930
|
-
try {
|
|
1931
|
-
const orgWorkDir = path.join(orgsDir, orgName);
|
|
1932
|
-
if (fs.existsSync(orgWorkDir)) fs.rmSync(orgWorkDir, { recursive: true, force: true });
|
|
1933
|
-
} catch (_) {}
|
|
1934
|
-
// Remove org subdirectory under git-safe location (.git/monomind/orgs/<name>/) so run
|
|
1935
|
-
// files written by the worktree-aware path (feat 880f034e) are also cleaned up on delete
|
|
1936
|
-
try {
|
|
1937
|
-
const _delWorkDir = path.resolve(_delOrgQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1938
|
-
const _delGitMonoDir = ctx._getGitMonomindDir(_delWorkDir);
|
|
1939
|
-
if (_delGitMonoDir) {
|
|
1940
|
-
const gitOrgDir = path.join(_delGitMonoDir, 'orgs', orgName);
|
|
1941
|
-
if (fs.existsSync(gitOrgDir)) fs.rmSync(gitOrgDir, { recursive: true, force: true });
|
|
1942
|
-
}
|
|
1943
|
-
} catch (_) {}
|
|
1944
|
-
// Remove loop prompt file if present (created for scheduled orgs by createorg)
|
|
1945
|
-
try {
|
|
1946
|
-
const lpf = path.join(
|
|
1947
|
-
path.resolve(ctx.projectDir || process.cwd()),
|
|
1948
|
-
'.monomind',
|
|
1949
|
-
'loops',
|
|
1950
|
-
`${orgName}.md`,
|
|
1951
|
-
);
|
|
1952
|
-
if (fs.existsSync(lpf)) fs.unlinkSync(lpf);
|
|
1953
|
-
} catch (_) {}
|
|
1954
|
-
// Emit org:delete event
|
|
1955
|
-
const deleteEvent = { type: 'org:delete', org: orgName, ts: Date.now() };
|
|
1956
|
-
ctx
|
|
1957
|
-
.appendToFile(
|
|
1958
|
-
path.join(ctx.projectDir || process.cwd(), 'data', 'mastermind-events.jsonl'),
|
|
1959
|
-
`${JSON.stringify(deleteEvent)}\n`,
|
|
1960
|
-
)
|
|
1961
|
-
.catch(() => {});
|
|
1962
|
-
ctx.broadcastMm(deleteEvent);
|
|
1963
|
-
res.writeHead(200, {
|
|
1964
|
-
'Content-Type': 'application/json',
|
|
1965
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
1966
|
-
});
|
|
1967
|
-
res.end('{"ok":true}');
|
|
1968
|
-
} catch (_) {
|
|
1969
|
-
res.writeHead(500);
|
|
1970
|
-
res.end('{}');
|
|
1971
|
-
}
|
|
1972
|
-
return true;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
// POST /api/orgs/:name/stop — send stop signal to a running org
|
|
1976
|
-
if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/stop$/i)) {
|
|
1977
|
-
try {
|
|
1978
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
1979
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
1980
|
-
res.writeHead(400);
|
|
1981
|
-
res.end('Invalid org name');
|
|
1982
|
-
return true;
|
|
1983
|
-
}
|
|
1984
|
-
const _stopOrgQs = new URL(req.url, 'http://localhost').searchParams;
|
|
1985
|
-
const _stopOrgBase = path.resolve(_stopOrgQs.get('dir') || ctx.projectDir || process.cwd());
|
|
1986
|
-
const stopEvent = { type: 'org:stop', org: orgName, ts: Date.now() };
|
|
1987
|
-
const dataDir = path.join(_stopOrgBase, 'data');
|
|
1988
|
-
try {
|
|
1989
|
-
fs.mkdirSync(dataDir, { recursive: true });
|
|
1990
|
-
} catch (_) {}
|
|
1991
|
-
ctx
|
|
1992
|
-
.appendToFile(
|
|
1993
|
-
path.join(dataDir, 'mastermind-events.jsonl'),
|
|
1994
|
-
`${JSON.stringify(stopEvent)}\n`,
|
|
1995
|
-
)
|
|
1996
|
-
.catch(() => {});
|
|
1997
|
-
// Write stop marker file at the path the REAL stop mechanisms poll:
|
|
1998
|
-
// `org run`'s own poll loop and `org serve`'s pollStopfiles() both watch
|
|
1999
|
-
// .monomind/orgs/<name>/stop (see clearStopfile/stopAction/pollStopfiles in
|
|
2000
|
-
// commands/org.ts) — NOT .monomind/orgs/.stops/<name>.stop, which nothing
|
|
2001
|
-
// in the CLI/daemon ever reads.
|
|
2002
|
-
try {
|
|
2003
|
-
const stopDir = path.join(_stopOrgBase, '.monomind', 'orgs', orgName);
|
|
2004
|
-
fs.mkdirSync(stopDir, { recursive: true });
|
|
2005
|
-
fs.writeFileSync(path.join(stopDir, 'stop'), new Date().toISOString());
|
|
2006
|
-
} catch (_) {}
|
|
2007
|
-
ctx.broadcastMm(stopEvent);
|
|
2008
|
-
res.writeHead(200, {
|
|
2009
|
-
'Content-Type': 'application/json',
|
|
2010
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2011
|
-
});
|
|
2012
|
-
res.end('{"ok":true}');
|
|
2013
|
-
} catch (_) {
|
|
2014
|
-
res.writeHead(500);
|
|
2015
|
-
res.end('{}');
|
|
2016
|
-
}
|
|
2017
|
-
return true;
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
// POST /api/orgs/:name/copy — copy org config to another project directory
|
|
2021
|
-
if (req.method === 'POST' && url.match(/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/copy$/i)) {
|
|
2022
|
-
let body = '';
|
|
2023
|
-
for await (const chunk of req) {
|
|
2024
|
-
body += chunk;
|
|
2025
|
-
if (body.length > 2097152) {
|
|
2026
|
-
req.destroy();
|
|
2027
|
-
break;
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
try {
|
|
2031
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
2032
|
-
if (orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
2033
|
-
res.writeHead(400);
|
|
2034
|
-
res.end(JSON.stringify({ error: 'Invalid org name' }));
|
|
2035
|
-
return true;
|
|
2036
|
-
}
|
|
2037
|
-
let payload = {};
|
|
2038
|
-
try {
|
|
2039
|
-
payload = JSON.parse(body);
|
|
2040
|
-
} catch (_) {}
|
|
2041
|
-
const destination = payload.destination ? String(payload.destination).trim() : '';
|
|
2042
|
-
if (!destination) {
|
|
2043
|
-
res.writeHead(400);
|
|
2044
|
-
res.end(JSON.stringify({ error: 'destination is required' }));
|
|
2045
|
-
return true;
|
|
2046
|
-
}
|
|
2047
|
-
if (!path.isAbsolute(destination)) {
|
|
2048
|
-
res.writeHead(400);
|
|
2049
|
-
res.end(JSON.stringify({ error: 'destination must be an absolute path' }));
|
|
2050
|
-
return true;
|
|
2051
|
-
}
|
|
2052
|
-
const _copyOrgQs = new URL(req.url, 'http://localhost').searchParams;
|
|
2053
|
-
const srcOrgsDir = path.join(
|
|
2054
|
-
path.resolve(_copyOrgQs.get('dir') || ctx.projectDir || process.cwd()),
|
|
2055
|
-
'.monomind',
|
|
2056
|
-
'orgs',
|
|
2057
|
-
);
|
|
2058
|
-
const srcFile = path.join(srcOrgsDir, `${orgName}.json`);
|
|
2059
|
-
if (!fs.existsSync(srcFile)) {
|
|
2060
|
-
res.writeHead(404);
|
|
2061
|
-
res.end(JSON.stringify({ error: 'org not found' }));
|
|
2062
|
-
return true;
|
|
2063
|
-
}
|
|
2064
|
-
const destOrgsDir = path.join(path.resolve(destination), '.monomind', 'orgs');
|
|
2065
|
-
try {
|
|
2066
|
-
fs.mkdirSync(destOrgsDir, { recursive: true });
|
|
2067
|
-
} catch (_) {}
|
|
2068
|
-
const destFile = path.join(destOrgsDir, `${orgName}.json`);
|
|
2069
|
-
fs.copyFileSync(srcFile, destFile);
|
|
2070
|
-
res.writeHead(200, {
|
|
2071
|
-
'Content-Type': 'application/json',
|
|
2072
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2073
|
-
});
|
|
2074
|
-
res.end(JSON.stringify({ ok: true, destFile }));
|
|
2075
|
-
} catch (e) {
|
|
2076
|
-
res.writeHead(500);
|
|
2077
|
-
res.end(JSON.stringify({ error: String(e.message || e) }));
|
|
2078
|
-
}
|
|
2079
|
-
return true;
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
// GET /api/org/:name/runs — list structured run files for an org
|
|
2083
|
-
if (req.method === 'GET' && /^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runs(\?.*)?$/i.test(url)) {
|
|
2084
|
-
try {
|
|
2085
|
-
const _rQs = new URL(req.url, 'http://localhost').searchParams;
|
|
2086
|
-
const _rOrgName = decodeURIComponent(url.split('/')[3] || '');
|
|
2087
|
-
if (_rOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_rOrgName)) {
|
|
2088
|
-
res.writeHead(400);
|
|
2089
|
-
res.end('{"error":"Invalid org name"}');
|
|
2090
|
-
return true;
|
|
2091
|
-
}
|
|
2092
|
-
const _rExplicitDir = _rQs.get('dir');
|
|
2093
|
-
const _rServerRoot = path.resolve(_rExplicitDir || ctx.projectDir || process.cwd());
|
|
2094
|
-
// Search across known projects (same logic as /api/orgs) unless explicit dir given
|
|
2095
|
-
const _rProjDirs = new Set([_rServerRoot]);
|
|
2096
|
-
if (!_rExplicitDir) {
|
|
2097
|
-
try {
|
|
2098
|
-
const _rKnown = JSON.parse(
|
|
2099
|
-
fs.readFileSync(path.join(_rServerRoot, 'data', 'known-projects.json'), 'utf8'),
|
|
2100
|
-
);
|
|
2101
|
-
_rKnown.forEach((p) => _rProjDirs.add(p));
|
|
2102
|
-
} catch (_) {}
|
|
2103
|
-
}
|
|
2104
|
-
const _rSeenFiles = new Set();
|
|
2105
|
-
const runs = [];
|
|
2106
|
-
const _parseRun = (filePath, f) => {
|
|
2107
|
-
try {
|
|
2108
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
2109
|
-
const allLines = raw.split('\n').filter(Boolean);
|
|
2110
|
-
const parse = (l) => {
|
|
2111
|
-
try {
|
|
2112
|
-
return JSON.parse(l);
|
|
2113
|
-
} catch {
|
|
2114
|
-
return null;
|
|
2115
|
-
}
|
|
2116
|
-
};
|
|
2117
|
-
// Merge .warm.jsonl (promoted pre-complete events) for accurate event count + metadata
|
|
2118
|
-
const warmFile = filePath.replace(/\.jsonl$/, '.warm.jsonl');
|
|
2119
|
-
let warmLines = [];
|
|
2120
|
-
let warmEvents = [];
|
|
2121
|
-
try {
|
|
2122
|
-
if (fs.existsSync(warmFile)) {
|
|
2123
|
-
warmLines = fs.readFileSync(warmFile, 'utf8').split('\n').filter(Boolean);
|
|
2124
|
-
warmEvents = warmLines.map(parse).filter(Boolean);
|
|
2125
|
-
}
|
|
2126
|
-
} catch (_) {}
|
|
2127
|
-
const combinedLines = [...warmLines, ...allLines];
|
|
2128
|
-
const eventCount = combinedLines.length;
|
|
2129
|
-
const headEvents = (
|
|
2130
|
-
warmEvents.length ? warmEvents : allLines.map(parse).filter(Boolean)
|
|
2131
|
-
).slice(0, 10);
|
|
2132
|
-
const _tailEvents = allLines.map(parse).filter(Boolean).slice(-5).length
|
|
2133
|
-
? allLines.map(parse).filter(Boolean).slice(-5)
|
|
2134
|
-
: warmEvents.slice(-5);
|
|
2135
|
-
const first = headEvents.find((e) => e.type === 'run:start') || headEvents[0];
|
|
2136
|
-
const last = [...warmEvents.slice(-5), ...allLines.map(parse).filter(Boolean).slice(-3)]
|
|
2137
|
-
.slice()
|
|
2138
|
-
.reverse()
|
|
2139
|
-
.find((e) => e.type === 'run:complete' || e.type === 'org:complete');
|
|
2140
|
-
const cycles = combinedLines.filter((l) => l.includes('"org:checkpoint"')).length;
|
|
2141
|
-
const lastEvent =
|
|
2142
|
-
allLines.map(parse).filter(Boolean).slice(-1)[0] || warmEvents.slice(-1)[0];
|
|
2143
|
-
const ageMs = lastEvent?.ts ? Date.now() - lastEvent.ts : Infinity;
|
|
2144
|
-
const isStale = !last && ageMs > 30 * 60 * 1000;
|
|
2145
|
-
const firstBossComms = headEvents.find(
|
|
2146
|
-
(e) => e.type === 'org:comms' && (e.from === 'boss' || e.role === 'boss') && e.msg,
|
|
2147
|
-
);
|
|
2148
|
-
const derivedGoal = first?.goal || firstBossComms?.msg?.slice(0, 80) || '';
|
|
2149
|
-
return {
|
|
2150
|
-
runId: f.replace(/\.warm\.jsonl$|\.jsonl$/, ''),
|
|
2151
|
-
startedAt: first?.ts || 0,
|
|
2152
|
-
endedAt: last?.ts || 0,
|
|
2153
|
-
status: last ? 'complete' : isStale ? 'stale' : 'running',
|
|
2154
|
-
eventCount,
|
|
2155
|
-
cycleCount: cycles,
|
|
2156
|
-
goal: derivedGoal,
|
|
2157
|
-
bossRole: first?.bossRole || '',
|
|
2158
|
-
};
|
|
2159
|
-
} catch (_) {
|
|
2160
|
-
return null;
|
|
2161
|
-
}
|
|
2162
|
-
};
|
|
2163
|
-
// Org Runtime v2 (#238): each run's raw BusEvents live under a
|
|
2164
|
-
// per-run directory named for the run itself — orgs/<org>/<runId>/bus.jsonl
|
|
2165
|
-
// — never the flat orgs/<org>/runs/<runId>.jsonl layout _parseRun above
|
|
2166
|
-
// expects. Translate through the same translate()/companionEvents()
|
|
2167
|
-
// forwarder.ts uses for the live view, so historical and live event
|
|
2168
|
-
// shapes always match.
|
|
2169
|
-
const _parseV2RunDir = (orgDir, runId) => {
|
|
2170
|
-
try {
|
|
2171
|
-
const busFile = path.join(orgDir, runId, 'bus.jsonl');
|
|
2172
|
-
if (!fs.existsSync(busFile)) return null;
|
|
2173
|
-
const raw = fs
|
|
2174
|
-
.readFileSync(busFile, 'utf8')
|
|
2175
|
-
.split('\n')
|
|
2176
|
-
.filter(Boolean)
|
|
2177
|
-
.map((l) => {
|
|
2178
|
-
try {
|
|
2179
|
-
return JSON.parse(l);
|
|
2180
|
-
} catch {
|
|
2181
|
-
return null;
|
|
2182
|
-
}
|
|
2183
|
-
})
|
|
2184
|
-
.filter(Boolean);
|
|
2185
|
-
if (!raw.length) return null;
|
|
2186
|
-
const translated = raw.flatMap((e) => [...companionEvents(e), translate(e)]);
|
|
2187
|
-
const done = translated.find(
|
|
2188
|
-
(e) => e.type === 'session:complete' || e.type === 'org:complete',
|
|
2189
|
-
);
|
|
2190
|
-
const start = translated.find((e) => e.type === 'org:start');
|
|
2191
|
-
const firstComms = translated.find((e) => e.type === 'org:comms' && e.msg);
|
|
2192
|
-
return {
|
|
2193
|
-
runId,
|
|
2194
|
-
startedAt: raw[0]?.ts || 0,
|
|
2195
|
-
endedAt: done?.ts || 0,
|
|
2196
|
-
status: done ? 'complete' : 'running',
|
|
2197
|
-
eventCount: raw.length,
|
|
2198
|
-
cycleCount: translated.filter((e) => e.type === 'org:checkpoint').length,
|
|
2199
|
-
goal: start?.goal || firstComms?.msg?.slice(0, 80) || '',
|
|
2200
|
-
bossRole: '',
|
|
2201
|
-
};
|
|
2202
|
-
} catch (_) {
|
|
2203
|
-
return null;
|
|
2204
|
-
}
|
|
2205
|
-
};
|
|
2206
|
-
for (const _rpd of _rProjDirs) {
|
|
2207
|
-
// Check both .monomind and .git/monomind locations
|
|
2208
|
-
const _rMonoDir = ctx._getGitMonomindDir(_rpd) || path.join(_rpd, '.monomind');
|
|
2209
|
-
const _rOrgDirs = [path.join(_rMonoDir, 'orgs', _rOrgName)];
|
|
2210
|
-
if (_rMonoDir !== path.join(_rpd, '.monomind'))
|
|
2211
|
-
_rOrgDirs.push(path.join(_rpd, '.monomind', 'orgs', _rOrgName));
|
|
2212
|
-
for (const _rOrgDir of _rOrgDirs) {
|
|
2213
|
-
if (!fs.existsSync(_rOrgDir)) continue;
|
|
2214
|
-
const runDirs = fs
|
|
2215
|
-
.readdirSync(_rOrgDir)
|
|
2216
|
-
.filter(
|
|
2217
|
-
(d) => d.startsWith('run-') && fs.statSync(path.join(_rOrgDir, d)).isDirectory(),
|
|
2218
|
-
)
|
|
2219
|
-
.sort()
|
|
2220
|
-
.reverse();
|
|
2221
|
-
for (const runId of runDirs.slice(0, 50)) {
|
|
2222
|
-
if (_rSeenFiles.has(runId)) continue;
|
|
2223
|
-
const r = _parseV2RunDir(_rOrgDir, runId);
|
|
2224
|
-
if (!r) continue;
|
|
2225
|
-
_rSeenFiles.add(runId);
|
|
2226
|
-
runs.push(r);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
const _rSearchDirs = [path.join(_rMonoDir, 'orgs', _rOrgName, 'runs')];
|
|
2230
|
-
if (_rMonoDir !== path.join(_rpd, '.monomind'))
|
|
2231
|
-
_rSearchDirs.push(path.join(_rpd, '.monomind', 'orgs', _rOrgName, 'runs'));
|
|
2232
|
-
for (const _rDir of _rSearchDirs) {
|
|
2233
|
-
if (!fs.existsSync(_rDir)) continue;
|
|
2234
|
-
// Include .warm.jsonl — completed runs are renamed hot→warm on org:complete and
|
|
2235
|
-
// must stay visible in the chat history dropdown.
|
|
2236
|
-
const files = fs
|
|
2237
|
-
.readdirSync(_rDir)
|
|
2238
|
-
.filter(
|
|
2239
|
-
(f) =>
|
|
2240
|
-
f.endsWith('.jsonl') &&
|
|
2241
|
-
!f.startsWith('._') &&
|
|
2242
|
-
!f.endsWith('.convs.jsonl') &&
|
|
2243
|
-
!f.endsWith('.cold.jsonl'),
|
|
2244
|
-
)
|
|
2245
|
-
.sort()
|
|
2246
|
-
.reverse();
|
|
2247
|
-
for (const f of files.slice(0, 50)) {
|
|
2248
|
-
const _rFileId = f.replace(/\.warm\.jsonl$|\.jsonl$/, '');
|
|
2249
|
-
if (_rSeenFiles.has(_rFileId)) continue;
|
|
2250
|
-
_rSeenFiles.add(_rFileId);
|
|
2251
|
-
const r = _parseRun(path.join(_rDir, f), f);
|
|
2252
|
-
if (r) runs.push(r);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
runs.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
|
|
2257
|
-
// Threads fallback: if no run files found, synthesize a run from the org threads file
|
|
2258
|
-
// so orgs whose boss never emitted runId-tagged events still show in the chat dropdown.
|
|
2259
|
-
if (runs.length === 0) {
|
|
2260
|
-
for (const _rpd of _rProjDirs) {
|
|
2261
|
-
const _tf = path.join(_rpd, '.monomind', 'orgs', `${_rOrgName}-threads.jsonl`);
|
|
2262
|
-
if (!fs.existsSync(_tf)) continue;
|
|
2263
|
-
const _tLines = fs.readFileSync(_tf, 'utf8').split('\n').filter(Boolean);
|
|
2264
|
-
if (!_tLines.length) continue;
|
|
2265
|
-
const _tEvs = _tLines
|
|
2266
|
-
.map((l) => {
|
|
2267
|
-
try {
|
|
2268
|
-
return JSON.parse(l);
|
|
2269
|
-
} catch {
|
|
2270
|
-
return null;
|
|
2271
|
-
}
|
|
2272
|
-
})
|
|
2273
|
-
.filter(Boolean);
|
|
2274
|
-
if (!_tEvs.length) continue;
|
|
2275
|
-
const _firstTs = _tEvs[0].ts
|
|
2276
|
-
? typeof _tEvs[0].ts === 'number'
|
|
2277
|
-
? _tEvs[0].ts
|
|
2278
|
-
: new Date(_tEvs[0].ts).getTime()
|
|
2279
|
-
: Date.now();
|
|
2280
|
-
const _lastTs = _tEvs[_tEvs.length - 1].ts
|
|
2281
|
-
? typeof _tEvs[_tEvs.length - 1].ts === 'number'
|
|
2282
|
-
? _tEvs[_tEvs.length - 1].ts
|
|
2283
|
-
: new Date(_tEvs[_tEvs.length - 1].ts).getTime()
|
|
2284
|
-
: _firstTs;
|
|
2285
|
-
runs.push({
|
|
2286
|
-
id: `threads-${_rOrgName}`,
|
|
2287
|
-
orgName: _rOrgName,
|
|
2288
|
-
goal: `${_rOrgName} threads`,
|
|
2289
|
-
status: 'complete',
|
|
2290
|
-
startedAt: _firstTs,
|
|
2291
|
-
endedAt: _lastTs,
|
|
2292
|
-
eventCount: _tEvs.length,
|
|
2293
|
-
_threadsFile: _tf,
|
|
2294
|
-
});
|
|
2295
|
-
break;
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
res.writeHead(200, {
|
|
2299
|
-
'Content-Type': 'application/json',
|
|
2300
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2301
|
-
'Cache-Control': 'no-cache',
|
|
2302
|
-
});
|
|
2303
|
-
res.end(JSON.stringify(runs));
|
|
2304
|
-
} catch (_) {
|
|
2305
|
-
res.writeHead(500);
|
|
2306
|
-
res.end('[]');
|
|
2307
|
-
}
|
|
2308
|
-
return true;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
// GET /api/org/:name/runs/:runId — get all events for a specific run
|
|
2312
|
-
if (
|
|
2313
|
-
req.method === 'GET' &&
|
|
2314
|
-
/^\/api\/org\/[a-z0-9][a-z0-9_-]{0,63}\/runs\/[a-z0-9][a-z0-9_-]{0,79}(\?.*)?$/i.test(url)
|
|
2315
|
-
) {
|
|
2316
|
-
try {
|
|
2317
|
-
const _rvQs = new URL(req.url, 'http://localhost').searchParams;
|
|
2318
|
-
const _rvParts = url.replace(/\?.*$/, '').split('/');
|
|
2319
|
-
const _rvOrgName = decodeURIComponent(_rvParts[3] || '');
|
|
2320
|
-
const _rvRunId = decodeURIComponent(_rvParts[5] || '');
|
|
2321
|
-
if (
|
|
2322
|
-
_rvOrgName.length > 64 ||
|
|
2323
|
-
!/^[a-z0-9][a-z0-9_-]*$/i.test(_rvOrgName) ||
|
|
2324
|
-
_rvRunId.length > 80 ||
|
|
2325
|
-
!/^[a-z0-9][a-z0-9_-]*$/i.test(_rvRunId)
|
|
2326
|
-
) {
|
|
2327
|
-
res.writeHead(400);
|
|
2328
|
-
res.end('{"error":"Invalid org or run id"}');
|
|
2329
|
-
return true;
|
|
2330
|
-
}
|
|
2331
|
-
const _rvExplicitDir = _rvQs.get('dir');
|
|
2332
|
-
const _rvServerRoot = path.resolve(_rvExplicitDir || ctx.projectDir || process.cwd());
|
|
2333
|
-
// Threads fallback: threads-${orgName} is a synthetic runId served from threads file
|
|
2334
|
-
if (_rvRunId === `threads-${_rvOrgName}`) {
|
|
2335
|
-
const _rvProjDirsT = new Set([_rvServerRoot]);
|
|
2336
|
-
if (!_rvExplicitDir) {
|
|
2337
|
-
try {
|
|
2338
|
-
JSON.parse(
|
|
2339
|
-
fs.readFileSync(path.join(_rvServerRoot, 'data', 'known-projects.json'), 'utf8'),
|
|
2340
|
-
).forEach((p) => _rvProjDirsT.add(p));
|
|
2341
|
-
} catch (_) {}
|
|
2342
|
-
}
|
|
2343
|
-
for (const _rvpd of _rvProjDirsT) {
|
|
2344
|
-
const _tf = path.join(_rvpd, '.monomind', 'orgs', `${_rvOrgName}-threads.jsonl`);
|
|
2345
|
-
if (!fs.existsSync(_tf)) continue;
|
|
2346
|
-
const _tLines = fs.readFileSync(_tf, 'utf8').split('\n').filter(Boolean);
|
|
2347
|
-
const _tEvs = _tLines
|
|
2348
|
-
.map((l) => {
|
|
2349
|
-
try {
|
|
2350
|
-
const e = JSON.parse(l);
|
|
2351
|
-
return {
|
|
2352
|
-
type: 'org:comms',
|
|
2353
|
-
from: e.role || e.from || 'agent',
|
|
2354
|
-
to: e.to || 'all',
|
|
2355
|
-
msg: e.message || e.msg || '',
|
|
2356
|
-
ts: e.ts
|
|
2357
|
-
? typeof e.ts === 'number'
|
|
2358
|
-
? e.ts
|
|
2359
|
-
: new Date(e.ts).getTime()
|
|
2360
|
-
: Date.now(),
|
|
2361
|
-
org: _rvOrgName,
|
|
2362
|
-
runId: _rvRunId,
|
|
2363
|
-
};
|
|
2364
|
-
} catch {
|
|
2365
|
-
return null;
|
|
2366
|
-
}
|
|
2367
|
-
})
|
|
2368
|
-
.filter(Boolean);
|
|
2369
|
-
res.writeHead(200, {
|
|
2370
|
-
'Content-Type': 'application/json',
|
|
2371
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2372
|
-
});
|
|
2373
|
-
res.end(JSON.stringify(_tEvs));
|
|
2374
|
-
return true;
|
|
2375
|
-
}
|
|
2376
|
-
res.writeHead(404);
|
|
2377
|
-
res.end('{"error":"threads file not found"}');
|
|
2378
|
-
return true;
|
|
2379
|
-
}
|
|
2380
|
-
// Search across known projects
|
|
2381
|
-
const _rvProjDirs = new Set([_rvServerRoot]);
|
|
2382
|
-
if (!_rvExplicitDir) {
|
|
2383
|
-
try {
|
|
2384
|
-
JSON.parse(
|
|
2385
|
-
fs.readFileSync(path.join(_rvServerRoot, 'data', 'known-projects.json'), 'utf8'),
|
|
2386
|
-
).forEach((p) => _rvProjDirs.add(p));
|
|
2387
|
-
} catch (_) {}
|
|
2388
|
-
}
|
|
2389
|
-
// Org Runtime v2 (#238): try the per-run-directory bus.jsonl layout
|
|
2390
|
-
// first — orgs/<org>/<runId>/bus.jsonl — translated to the same
|
|
2391
|
-
// dashboard vocabulary the live view gets from forwarder.ts, before
|
|
2392
|
-
// falling back to the v1 flat orgs/<org>/runs/<runId>.jsonl lookup
|
|
2393
|
-
// below (v1 files are already stored pre-translated).
|
|
2394
|
-
for (const _rvpd of _rvProjDirs) {
|
|
2395
|
-
const _rvMonoDir = ctx._getGitMonomindDir(_rvpd) || path.join(_rvpd, '.monomind');
|
|
2396
|
-
const _v2Candidates = [path.join(_rvMonoDir, 'orgs', _rvOrgName, _rvRunId, 'bus.jsonl')];
|
|
2397
|
-
if (_rvMonoDir !== path.join(_rvpd, '.monomind'))
|
|
2398
|
-
_v2Candidates.push(
|
|
2399
|
-
path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, _rvRunId, 'bus.jsonl'),
|
|
2400
|
-
);
|
|
2401
|
-
for (const _v2File of _v2Candidates) {
|
|
2402
|
-
if (!fs.existsSync(_v2File)) continue;
|
|
2403
|
-
const raw = fs
|
|
2404
|
-
.readFileSync(_v2File, 'utf8')
|
|
2405
|
-
.split('\n')
|
|
2406
|
-
.filter(Boolean)
|
|
2407
|
-
.map((l) => {
|
|
2408
|
-
try {
|
|
2409
|
-
return JSON.parse(l);
|
|
2410
|
-
} catch {
|
|
2411
|
-
return null;
|
|
2412
|
-
}
|
|
2413
|
-
})
|
|
2414
|
-
.filter(Boolean);
|
|
2415
|
-
const events = raw.flatMap((e) => [...companionEvents(e), translate(e)]);
|
|
2416
|
-
events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
2417
|
-
res.writeHead(200, {
|
|
2418
|
-
'Content-Type': 'application/json',
|
|
2419
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2420
|
-
'Cache-Control': 'no-cache',
|
|
2421
|
-
});
|
|
2422
|
-
res.end(JSON.stringify(events));
|
|
2423
|
-
return true;
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
let _rvFile = null;
|
|
2427
|
-
for (const _rvpd of _rvProjDirs) {
|
|
2428
|
-
const _rvMonoDir = ctx._getGitMonomindDir(_rvpd) || path.join(_rvpd, '.monomind');
|
|
2429
|
-
const _candidates = [
|
|
2430
|
-
path.join(_rvMonoDir, 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`),
|
|
2431
|
-
path.join(_rvMonoDir, 'orgs', _rvOrgName, 'runs', `${_rvRunId}.warm.jsonl`),
|
|
2432
|
-
];
|
|
2433
|
-
if (_rvMonoDir !== path.join(_rvpd, '.monomind')) {
|
|
2434
|
-
_candidates.push(
|
|
2435
|
-
path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, 'runs', `${_rvRunId}.jsonl`),
|
|
2436
|
-
);
|
|
2437
|
-
_candidates.push(
|
|
2438
|
-
path.join(_rvpd, '.monomind', 'orgs', _rvOrgName, 'runs', `${_rvRunId}.warm.jsonl`),
|
|
2439
|
-
);
|
|
2440
|
-
}
|
|
2441
|
-
for (const c of _candidates) {
|
|
2442
|
-
if (fs.existsSync(c)) {
|
|
2443
|
-
_rvFile = c;
|
|
2444
|
-
break;
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
if (_rvFile) break;
|
|
2448
|
-
}
|
|
2449
|
-
if (!_rvFile) {
|
|
2450
|
-
res.writeHead(404);
|
|
2451
|
-
res.end('{"error":"run not found"}');
|
|
2452
|
-
return true;
|
|
2453
|
-
}
|
|
2454
|
-
const _parseLines = (p) => {
|
|
2455
|
-
try {
|
|
2456
|
-
return fs
|
|
2457
|
-
.readFileSync(p, 'utf8')
|
|
2458
|
-
.split('\n')
|
|
2459
|
-
.filter(Boolean)
|
|
2460
|
-
.map((l) => {
|
|
2461
|
-
try {
|
|
2462
|
-
return JSON.parse(l);
|
|
2463
|
-
} catch {
|
|
2464
|
-
return null;
|
|
2465
|
-
}
|
|
2466
|
-
})
|
|
2467
|
-
.filter(Boolean);
|
|
2468
|
-
} catch {
|
|
2469
|
-
return [];
|
|
2470
|
-
}
|
|
2471
|
-
};
|
|
2472
|
-
const events = _parseLines(_rvFile);
|
|
2473
|
-
// Merge .warm.jsonl (pre-run:complete events, including org:comms) if it exists.
|
|
2474
|
-
// When run:complete fires, the hot .jsonl is renamed to .warm.jsonl so all pre-complete
|
|
2475
|
-
// events live there. The current .jsonl then only holds post-complete events (e.g. org:stop).
|
|
2476
|
-
const _rvWarmFile = _rvFile.endsWith('.warm.jsonl')
|
|
2477
|
-
? _rvFile
|
|
2478
|
-
: _rvFile.replace(/\.jsonl$/, '.warm.jsonl');
|
|
2479
|
-
if (_rvWarmFile !== _rvFile && fs.existsSync(_rvWarmFile)) {
|
|
2480
|
-
events.push(..._parseLines(_rvWarmFile));
|
|
2481
|
-
}
|
|
2482
|
-
// For in-progress runs (no .warm.jsonl), org:comms also go to .convs.jsonl (stripped form).
|
|
2483
|
-
// They're already in .jsonl as full events, so .convs.jsonl would duplicate — skip it.
|
|
2484
|
-
events.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
2485
|
-
res.writeHead(200, {
|
|
2486
|
-
'Content-Type': 'application/json',
|
|
2487
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2488
|
-
'Cache-Control': 'no-cache',
|
|
2489
|
-
});
|
|
2490
|
-
res.end(JSON.stringify(events));
|
|
2491
|
-
} catch (_) {
|
|
2492
|
-
res.writeHead(500);
|
|
2493
|
-
res.end('[]');
|
|
2494
|
-
}
|
|
2495
|
-
return true;
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
// GET /api/org/:name/artifact — serve file content for chat "View" button
|
|
2499
|
-
if (req.method === 'GET' && /^\/api\/org\/[^/]+\/artifact/.test(url)) {
|
|
2500
|
-
try {
|
|
2501
|
-
const _artOrgName = decodeURIComponent(url.split('/')[3] || '');
|
|
2502
|
-
if (_artOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_artOrgName)) {
|
|
2503
|
-
res.writeHead(400);
|
|
2504
|
-
res.end(JSON.stringify({ error: 'Invalid org name' }));
|
|
2505
|
-
return true;
|
|
2506
|
-
}
|
|
2507
|
-
const _artQp = new URL(`http://x${req.url}`).searchParams;
|
|
2508
|
-
const _rawPath = _artQp.get('path');
|
|
2509
|
-
if (!_rawPath) {
|
|
2510
|
-
res.writeHead(400);
|
|
2511
|
-
res.end(JSON.stringify({ error: 'path required' }));
|
|
2512
|
-
return true;
|
|
2513
|
-
}
|
|
2514
|
-
const _filePath = path.resolve(decodeURIComponent(_rawPath));
|
|
2515
|
-
// Security: this endpoint used to allow reads anywhere under the project
|
|
2516
|
-
// root or any data/known-projects.json entry — letting any dashboard
|
|
2517
|
-
// client read .env, .git/config, or other arbitrary project files.
|
|
2518
|
-
// Restrict it, like /api/file-content above, to this org's own
|
|
2519
|
-
// .monomind/orgs/<name>/ directory — never outside .monomind.
|
|
2520
|
-
const _artBaseDir = path.resolve(_artQp.get('dir') || ctx.projectDir || process.cwd());
|
|
2521
|
-
const _artProjDir = ctx._resolveOrgProjectDir(_artOrgName, _artBaseDir) || _artBaseDir;
|
|
2522
|
-
const _artMonoDir =
|
|
2523
|
-
ctx._getGitMonomindDir(_artProjDir) || path.join(_artProjDir, '.monomind');
|
|
2524
|
-
const _artOrgDir = path.join(_artMonoDir, 'orgs', _artOrgName);
|
|
2525
|
-
const _safe = _filePath === _artOrgDir || _filePath.startsWith(_artOrgDir + path.sep);
|
|
2526
|
-
if (!_safe) {
|
|
2527
|
-
res.writeHead(403);
|
|
2528
|
-
res.end(JSON.stringify({ error: 'path not allowed' }));
|
|
2529
|
-
return true;
|
|
2530
|
-
}
|
|
2531
|
-
if (!fs.existsSync(_filePath)) {
|
|
2532
|
-
res.writeHead(404);
|
|
2533
|
-
res.end(JSON.stringify({ error: 'file not found' }));
|
|
2534
|
-
return true;
|
|
2535
|
-
}
|
|
2536
|
-
const _mime = ctx._detectMimeType(_filePath);
|
|
2537
|
-
const _size = fs.statSync(_filePath).size;
|
|
2538
|
-
// Reject files >2MB to avoid blocking the event loop
|
|
2539
|
-
if (_size > 2 * 1024 * 1024) {
|
|
2540
|
-
res.writeHead(413);
|
|
2541
|
-
res.end(JSON.stringify({ error: 'file too large', size: _size }));
|
|
2542
|
-
return true;
|
|
2543
|
-
}
|
|
2544
|
-
if (!_mime.startsWith('text/') && _mime !== 'application/json') {
|
|
2545
|
-
res.writeHead(200, {
|
|
2546
|
-
'Content-Type': 'application/json',
|
|
2547
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2548
|
-
});
|
|
2549
|
-
res.end(JSON.stringify({ binary: true, mimeType: _mime, size: _size }));
|
|
2550
|
-
return true;
|
|
2551
|
-
}
|
|
2552
|
-
const _content = fs.readFileSync(_filePath, 'utf8');
|
|
2553
|
-
res.writeHead(200, {
|
|
2554
|
-
'Content-Type': 'application/json',
|
|
2555
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2556
|
-
});
|
|
2557
|
-
res.end(JSON.stringify({ content: _content, mimeType: _mime, size: _size }));
|
|
2558
|
-
} catch (_e) {
|
|
2559
|
-
res.writeHead(500);
|
|
2560
|
-
res.end(JSON.stringify({ error: 'read failed' }));
|
|
2561
|
-
}
|
|
2562
|
-
return true;
|
|
2563
|
-
}
|
|
2564
|
-
|
|
2565
|
-
// ------------------------------------------------- Mastermind event system
|
|
2566
|
-
// POST /api/mastermind/event — ingest event from mastermind skill
|
|
2567
|
-
if (req.method === 'POST' && url === '/api/mastermind/event') {
|
|
2568
|
-
await ctx.handleMastermindEvent(req, res, corsOrigin);
|
|
2569
|
-
return true;
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
// GET /api/mastermind-stream — SSE for real-time events
|
|
2573
|
-
if (req.method === 'GET' && url === '/api/mastermind-stream') {
|
|
2574
|
-
res.writeHead(200, {
|
|
2575
|
-
'Content-Type': 'text/event-stream',
|
|
2576
|
-
'Cache-Control': 'no-cache',
|
|
2577
|
-
Connection: 'keep-alive',
|
|
2578
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2579
|
-
});
|
|
2580
|
-
res.write(': connected\n\n');
|
|
2581
|
-
ctx.addMmClient(res);
|
|
2582
|
-
// Replay last 50 events from disk (use ?project= param if provided)
|
|
2583
|
-
try {
|
|
2584
|
-
const _sseQp = new URL(`http://x${req.url}`).searchParams;
|
|
2585
|
-
const _sseProj = _sseQp.get('project');
|
|
2586
|
-
const root2 = _sseProj || ctx.projectDir || process.cwd();
|
|
2587
|
-
const evFile = path.join(root2, 'data', 'mastermind-events.jsonl');
|
|
2588
|
-
const lines = fs.readFileSync(evFile, 'utf8').trim().split('\n').filter(Boolean).slice(-50);
|
|
2589
|
-
for (const l of lines) res.write(`data: ${l}\n\n`);
|
|
2590
|
-
} catch (_) {}
|
|
2591
|
-
const ka = setInterval(() => {
|
|
2592
|
-
try {
|
|
2593
|
-
res.write(': ping\n\n');
|
|
2594
|
-
} catch (_) {
|
|
2595
|
-
clearInterval(ka);
|
|
2596
|
-
ctx.removeMmClient(res);
|
|
2597
|
-
}
|
|
2598
|
-
}, 20000);
|
|
2599
|
-
req.on('close', () => {
|
|
2600
|
-
ctx.removeMmClient(res);
|
|
2601
|
-
clearInterval(ka);
|
|
2602
|
-
});
|
|
2603
|
-
return true;
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
// GET /api/mastermind/sessions
|
|
2607
|
-
if (req.method === 'GET' && url.startsWith('/api/mastermind/sessions')) {
|
|
2608
|
-
try {
|
|
2609
|
-
const qp = new URL(`http://x${req.url}`).searchParams;
|
|
2610
|
-
const filterProject = qp.get('project');
|
|
2611
|
-
const limitParam = Math.min(parseInt(qp.get('limit') || '200', 10) || 200, 500);
|
|
2612
|
-
const serverRoot = ctx.projectDir || process.cwd();
|
|
2613
|
-
// Collect all project dirs to aggregate
|
|
2614
|
-
const projectDirs = new Set([serverRoot]);
|
|
2615
|
-
try {
|
|
2616
|
-
const known = JSON.parse(
|
|
2617
|
-
fs.readFileSync(path.join(serverRoot, 'data', 'known-projects.json'), 'utf8'),
|
|
2618
|
-
);
|
|
2619
|
-
known.forEach((p) => projectDirs.add(p));
|
|
2620
|
-
} catch (_) {}
|
|
2621
|
-
let allSessions = [];
|
|
2622
|
-
for (const pd of projectDirs) {
|
|
2623
|
-
if (filterProject && pd !== filterProject) continue;
|
|
2624
|
-
const sessDir = path.join(pd, 'data', 'sessions');
|
|
2625
|
-
const indexFile = path.join(sessDir, '_index.json');
|
|
2626
|
-
// ── New format: per-session JSONL + _index.json ──
|
|
2627
|
-
if (fs.existsSync(indexFile)) {
|
|
2628
|
-
try {
|
|
2629
|
-
const idx = JSON.parse(fs.readFileSync(indexFile, 'utf8'));
|
|
2630
|
-
const top = idx.slice(0, limitParam);
|
|
2631
|
-
for (const entry of top) {
|
|
2632
|
-
const _sid = String(entry.id || '').trim();
|
|
2633
|
-
if (!_sid || !ctx.SESSION_ID_RE.test(_sid)) continue;
|
|
2634
|
-
let events = [];
|
|
2635
|
-
try {
|
|
2636
|
-
const jl = fs.readFileSync(path.join(sessDir, `${_sid}.jsonl`), 'utf8');
|
|
2637
|
-
events = jl
|
|
2638
|
-
.trim()
|
|
2639
|
-
.split('\n')
|
|
2640
|
-
.filter(Boolean)
|
|
2641
|
-
.map((l) => {
|
|
2642
|
-
try {
|
|
2643
|
-
return JSON.parse(l);
|
|
2644
|
-
} catch {
|
|
2645
|
-
return null;
|
|
2646
|
-
}
|
|
2647
|
-
})
|
|
2648
|
-
.filter(Boolean);
|
|
2649
|
-
} catch (_) {}
|
|
2650
|
-
allSessions.push({ ...entry, events, project: pd });
|
|
2651
|
-
}
|
|
2652
|
-
} catch (_) {}
|
|
2653
|
-
} else {
|
|
2654
|
-
// ── Legacy fallback: mastermind-sessions.json ──
|
|
2655
|
-
const f = path.join(pd, 'data', 'mastermind-sessions.json');
|
|
2656
|
-
if (fs.existsSync(f)) {
|
|
2657
|
-
try {
|
|
2658
|
-
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
2659
|
-
s.forEach((sess) => {
|
|
2660
|
-
if (!sess.project) sess.project = pd;
|
|
2661
|
-
});
|
|
2662
|
-
allSessions = allSessions.concat(s);
|
|
2663
|
-
} catch (_) {}
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
allSessions.sort((a, b) => (b.ts || b.startedAt || 0) - (a.ts || a.startedAt || 0));
|
|
2668
|
-
res.writeHead(200, {
|
|
2669
|
-
'Content-Type': 'application/json',
|
|
2670
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2671
|
-
});
|
|
2672
|
-
res.end(JSON.stringify(allSessions.slice(0, limitParam)));
|
|
2673
|
-
} catch (_) {
|
|
2674
|
-
res.writeHead(200);
|
|
2675
|
-
res.end('[]');
|
|
2676
|
-
}
|
|
2677
|
-
return true;
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
// GET /api/mastermind/session/:id/trace — human-readable markdown trace
|
|
2681
|
-
if (req.method === 'GET' && url.match(/^\/api\/mastermind\/session\/[^/]+\/trace$/)) {
|
|
2682
|
-
try {
|
|
2683
|
-
const sid = url.split('/')[4];
|
|
2684
|
-
const sessFile = path.join(
|
|
2685
|
-
ctx.projectDir || process.cwd(),
|
|
2686
|
-
'data',
|
|
2687
|
-
'sessions',
|
|
2688
|
-
`${sid}.json`,
|
|
2689
|
-
);
|
|
2690
|
-
let s = null;
|
|
2691
|
-
if (fs.existsSync(sessFile)) {
|
|
2692
|
-
s = JSON.parse(fs.readFileSync(sessFile, 'utf8'));
|
|
2693
|
-
} else {
|
|
2694
|
-
const f = path.join(ctx.projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
|
|
2695
|
-
const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
2696
|
-
s = sessions.find((x) => x.id === sid);
|
|
2697
|
-
}
|
|
2698
|
-
if (!s) {
|
|
2699
|
-
res.writeHead(404);
|
|
2700
|
-
res.end('Session not found');
|
|
2701
|
-
return true;
|
|
2702
|
-
}
|
|
2703
|
-
const fmt = (ts) => `${new Date(ts).toISOString().replace('T', ' ').slice(0, 19)} UTC`;
|
|
2704
|
-
const lines = [
|
|
2705
|
-
`# Mastermind Session Trace: ${s.id}`,
|
|
2706
|
-
``,
|
|
2707
|
-
`**Prompt:** ${s.prompt || '(none)'}`,
|
|
2708
|
-
`**Status:** ${s.status}`,
|
|
2709
|
-
`**Started:** ${fmt(s.ts)}`,
|
|
2710
|
-
s.endTs ? `**Ended:** ${fmt(s.endTs)}` : '',
|
|
2711
|
-
`**Domains:** ${(s.domains || []).join(', ') || '(none yet)'}`,
|
|
2712
|
-
``,
|
|
2713
|
-
];
|
|
2714
|
-
for (const ev of s.events || []) {
|
|
2715
|
-
const t = fmt(ev.ts);
|
|
2716
|
-
if (ev.type === 'session:start')
|
|
2717
|
-
lines.push(`\`${t}\` **SESSION START** — prompt: "${ev.prompt || ''}"`);
|
|
2718
|
-
else if (ev.type === 'domain:dispatch')
|
|
2719
|
-
lines.push(`\`${t}\` **DOMAIN DISPATCH** → \`${ev.domain}\` — ${ev.cmd || ''}`);
|
|
2720
|
-
else if (ev.type === 'agent:spawn')
|
|
2721
|
-
lines.push(
|
|
2722
|
-
`\`${t}\` **AGENT SPAWN** [\`${ev.domain}\`] → agent: \`${ev.agent}\` — ${ev.task || ''}`,
|
|
2723
|
-
);
|
|
2724
|
-
else if (ev.type === 'intercom')
|
|
2725
|
-
lines.push(`\`${t}\` **INTERCOM** \`${ev.from}\` → \`${ev.to}\`: ${ev.msg || ''}`);
|
|
2726
|
-
else if (ev.type === 'domain:complete')
|
|
2727
|
-
lines.push(
|
|
2728
|
-
`\`${t}\` **DOMAIN COMPLETE** [\`${ev.domain}\`] status: ${ev.status}${ev.artifacts?.length ? ` — artifacts: ${ev.artifacts.join(', ')}` : ''}`,
|
|
2729
|
-
);
|
|
2730
|
-
else if (ev.type === 'session:complete')
|
|
2731
|
-
lines.push(
|
|
2732
|
-
`\`${t}\` **SESSION COMPLETE** — status: ${ev.status}, domains: ${(ev.domains || []).join(', ')}`,
|
|
2733
|
-
);
|
|
2734
|
-
else lines.push(`\`${t}\` ${ev.type} ${JSON.stringify(ev)}`);
|
|
2735
|
-
}
|
|
2736
|
-
res.writeHead(200, {
|
|
2737
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
2738
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2739
|
-
});
|
|
2740
|
-
res.end(lines.join('\n'));
|
|
2741
|
-
} catch (_) {
|
|
2742
|
-
res.writeHead(500);
|
|
2743
|
-
res.end('Error');
|
|
2744
|
-
}
|
|
2745
|
-
return true;
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
// GET /api/mastermind/session/:id
|
|
2749
|
-
if (req.method === 'GET' && url.startsWith('/api/mastermind/session/')) {
|
|
2750
|
-
try {
|
|
2751
|
-
const sid = url.slice('/api/mastermind/session/'.length);
|
|
2752
|
-
// Check individual session file first
|
|
2753
|
-
const sessFile = path.join(
|
|
2754
|
-
ctx.projectDir || process.cwd(),
|
|
2755
|
-
'data',
|
|
2756
|
-
'sessions',
|
|
2757
|
-
`${sid}.json`,
|
|
2758
|
-
);
|
|
2759
|
-
if (fs.existsSync(sessFile)) {
|
|
2760
|
-
res.writeHead(200, {
|
|
2761
|
-
'Content-Type': 'application/json',
|
|
2762
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2763
|
-
});
|
|
2764
|
-
res.end(fs.readFileSync(sessFile, 'utf8'));
|
|
2765
|
-
return true;
|
|
2766
|
-
}
|
|
2767
|
-
const f = path.join(ctx.projectDir || process.cwd(), 'data', 'mastermind-sessions.json');
|
|
2768
|
-
const sessions = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
2769
|
-
const s = sessions.find((x) => x.id === sid);
|
|
2770
|
-
res.writeHead(200, {
|
|
2771
|
-
'Content-Type': 'application/json',
|
|
2772
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2773
|
-
});
|
|
2774
|
-
res.end(JSON.stringify(s || null));
|
|
2775
|
-
} catch (_) {
|
|
2776
|
-
res.writeHead(200);
|
|
2777
|
-
res.end('null');
|
|
2778
|
-
}
|
|
2779
|
-
return true;
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
// -------------------------------------------------------- GET /mastermind
|
|
2783
|
-
if (req.method === 'GET' && url === '/mastermind') {
|
|
2784
|
-
// Serve local file if present (dev), otherwise fall back to bundled HTML
|
|
2785
|
-
const root = ctx.projectDir || process.cwd();
|
|
2786
|
-
const htmlPath = path.join(root, 'docs', 'mastermind-diagram.html');
|
|
2787
|
-
let html = ctx.MASTERMIND_DIAGRAM_HTML;
|
|
2788
|
-
try {
|
|
2789
|
-
html = fs.readFileSync(htmlPath, 'utf8');
|
|
2790
|
-
} catch (_) {}
|
|
2791
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
2792
|
-
res.end(html);
|
|
2793
|
-
return true;
|
|
2794
|
-
}
|
|
2795
|
-
|
|
2796
|
-
// ----------------------------------------------------------- GET /orgs
|
|
2797
|
-
if (req.method === 'GET' && url === '/orgs') {
|
|
2798
|
-
try {
|
|
2799
|
-
const htmlPath = path.join(ctx.__dirname, 'orgs.html');
|
|
2800
|
-
let html = fs.readFileSync(htmlPath, 'utf8');
|
|
2801
|
-
// Inject this process's auth credential the same way GET / does for
|
|
2802
|
-
// dashboard.html — orgs.html's fetch() calls hit the same now
|
|
2803
|
-
// default-closed /api/* routes and need a way to know the token.
|
|
2804
|
-
html = html.replace(
|
|
2805
|
-
'<head>',
|
|
2806
|
-
`<head>\n<meta name="mm-token" content="${ctx.dashboardAuthValue}">`,
|
|
2807
|
-
);
|
|
2808
|
-
res.writeHead(200, {
|
|
2809
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
2810
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
2811
|
-
});
|
|
2812
|
-
res.end(html);
|
|
2813
|
-
} catch (err) {
|
|
2814
|
-
res.writeHead(404);
|
|
2815
|
-
res.end(`orgs.html not found: ${err.message}`);
|
|
2816
|
-
}
|
|
2817
|
-
return true;
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
// ------------------------------------------------ GET /orgs-files.js
|
|
2821
|
-
// Files-tab + diff-view script, split out of orgs.html (see orgs.html's
|
|
2822
|
-
// script-src comment). Not a generic static handler — this UI serves each
|
|
2823
|
-
// sibling asset via its own hardcoded route, same as GET /orgs above.
|
|
2824
|
-
if (req.method === 'GET' && url === '/orgs-files.js') {
|
|
2825
|
-
try {
|
|
2826
|
-
const jsPath = path.join(ctx.__dirname, 'orgs-files.js');
|
|
2827
|
-
const js = fs.readFileSync(jsPath, 'utf8');
|
|
2828
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8' });
|
|
2829
|
-
res.end(js);
|
|
2830
|
-
} catch (err) {
|
|
2831
|
-
res.writeHead(404);
|
|
2832
|
-
res.end(`orgs-files.js not found: ${err.message}`);
|
|
2833
|
-
}
|
|
2834
|
-
return true;
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
// GET /api/mastermind/loops — list all active loop state files
|
|
2838
|
-
if (req.method === 'GET' && url === '/api/mastermind/loops') {
|
|
2839
|
-
try {
|
|
2840
|
-
const loopsDir = path.join(ctx.projectDir || process.cwd(), '.monomind', 'loops');
|
|
2841
|
-
const loops = [];
|
|
2842
|
-
if (fs.existsSync(loopsDir)) {
|
|
2843
|
-
const files = fs
|
|
2844
|
-
.readdirSync(loopsDir)
|
|
2845
|
-
.filter((f) => f.endsWith('.json') && !f.includes('-hil'));
|
|
2846
|
-
for (const f of files) {
|
|
2847
|
-
try {
|
|
2848
|
-
const d = JSON.parse(fs.readFileSync(path.join(loopsDir, f), 'utf8'));
|
|
2849
|
-
loops.push(d);
|
|
2850
|
-
} catch (_) {}
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
loops.sort((a, b) => (b.lastRunAt || 0) - (a.lastRunAt || 0));
|
|
2854
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2855
|
-
res.end(JSON.stringify({ loops }));
|
|
2856
|
-
} catch (_) {
|
|
2857
|
-
res.writeHead(500);
|
|
2858
|
-
res.end('{"loops":[]}');
|
|
2859
|
-
}
|
|
2860
|
-
return true;
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
// GET /api/status — live system snapshot for dashboard polling
|
|
2864
|
-
if (req.method === 'GET' && url === '/api/status') {
|
|
2865
|
-
try {
|
|
2866
|
-
const root = ctx.projectDir || process.cwd();
|
|
2867
|
-
// Active org runs: { orgName -> runId }
|
|
2868
|
-
const orgRuns = {};
|
|
2869
|
-
ctx.activeOrgRuns.forEach((runId, org) => {
|
|
2870
|
-
orgRuns[org] = runId;
|
|
2871
|
-
});
|
|
2872
|
-
// Recent events (last 10)
|
|
2873
|
-
let recentEvents = [];
|
|
2874
|
-
try {
|
|
2875
|
-
const evPath = path.join(root, 'data', 'mastermind-events.jsonl');
|
|
2876
|
-
const lines = fs
|
|
2877
|
-
.readFileSync(evPath, 'utf8')
|
|
2878
|
-
.split('\n')
|
|
2879
|
-
.filter((l) => l.trim())
|
|
2880
|
-
.slice(-10);
|
|
2881
|
-
recentEvents = lines
|
|
2882
|
-
.map((l) => {
|
|
2883
|
-
try {
|
|
2884
|
-
return JSON.parse(l);
|
|
2885
|
-
} catch (_) {
|
|
2886
|
-
return null;
|
|
2887
|
-
}
|
|
2888
|
-
})
|
|
2889
|
-
.filter(Boolean);
|
|
2890
|
-
} catch (_) {}
|
|
2891
|
-
res.writeHead(200, {
|
|
2892
|
-
'Content-Type': 'application/json',
|
|
2893
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
2894
|
-
});
|
|
2895
|
-
res.end(
|
|
2896
|
-
JSON.stringify({
|
|
2897
|
-
ts: Date.now(),
|
|
2898
|
-
pid: process.pid,
|
|
2899
|
-
uptime: process.uptime(),
|
|
2900
|
-
dir: root,
|
|
2901
|
-
sseClients: getMmClientCount(),
|
|
2902
|
-
activeOrgs: Object.keys(orgRuns).length,
|
|
2903
|
-
orgRuns,
|
|
2904
|
-
recentEvents,
|
|
2905
|
-
}),
|
|
2906
|
-
);
|
|
2907
|
-
} catch (err) {
|
|
2908
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2909
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
2910
|
-
}
|
|
2911
|
-
return true;
|
|
2912
|
-
}
|
|
2913
|
-
|
|
2914
|
-
// GET /api/orgs/:name/runs/current — events from the active run file for an org
|
|
2915
|
-
if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/runs\/current$/.test(url)) {
|
|
2916
|
-
try {
|
|
2917
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
2918
|
-
const _curQs = new URL(req.url, 'http://localhost').searchParams;
|
|
2919
|
-
const root = path.resolve(_curQs.get('dir') || ctx.projectDir || process.cwd());
|
|
2920
|
-
// Validate orgName
|
|
2921
|
-
if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
2922
|
-
res.writeHead(400);
|
|
2923
|
-
res.end('{"error":"invalid org name"}');
|
|
2924
|
-
return true;
|
|
2925
|
-
}
|
|
2926
|
-
const runId = ctx.activeOrgRuns.get(orgName);
|
|
2927
|
-
const monoDir = ctx._getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
2928
|
-
// Try active run first, then fall back to most recent run file.
|
|
2929
|
-
// Org Runtime v2 (#238): the active run's own events live under
|
|
2930
|
-
// orgs/<org>/<runId>/bus.jsonl, never the v1 flat orgs/<org>/runs/
|
|
2931
|
-
// layout — check that shape (and translate it) before the v1 lookup.
|
|
2932
|
-
let runFile = null;
|
|
2933
|
-
let isV2 = false;
|
|
2934
|
-
if (runId) {
|
|
2935
|
-
const v2Candidate = path.join(monoDir, 'orgs', orgName, runId, 'bus.jsonl');
|
|
2936
|
-
if (fs.existsSync(v2Candidate)) {
|
|
2937
|
-
runFile = v2Candidate;
|
|
2938
|
-
isV2 = true;
|
|
2939
|
-
} else {
|
|
2940
|
-
const candidate = path.join(monoDir, 'orgs', orgName, 'runs', `${runId}.jsonl`);
|
|
2941
|
-
if (fs.existsSync(candidate)) runFile = candidate;
|
|
2942
|
-
}
|
|
2943
|
-
}
|
|
2944
|
-
if (!runFile) {
|
|
2945
|
-
const orgDir = path.join(monoDir, 'orgs', orgName);
|
|
2946
|
-
const runDirs = fs.existsSync(orgDir)
|
|
2947
|
-
? fs
|
|
2948
|
-
.readdirSync(orgDir)
|
|
2949
|
-
.filter(
|
|
2950
|
-
(d) => d.startsWith('run-') && fs.existsSync(path.join(orgDir, d, 'bus.jsonl')),
|
|
2951
|
-
)
|
|
2952
|
-
.sort()
|
|
2953
|
-
.reverse()
|
|
2954
|
-
: [];
|
|
2955
|
-
if (runDirs.length) {
|
|
2956
|
-
runFile = path.join(orgDir, runDirs[0], 'bus.jsonl');
|
|
2957
|
-
isV2 = true;
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
if (!runFile) {
|
|
2961
|
-
const runsDir = path.join(monoDir, 'orgs', orgName, 'runs');
|
|
2962
|
-
if (fs.existsSync(runsDir)) {
|
|
2963
|
-
const files = fs
|
|
2964
|
-
.readdirSync(runsDir)
|
|
2965
|
-
.filter((f) => f.endsWith('.jsonl') && !f.startsWith('._'));
|
|
2966
|
-
if (files.length) {
|
|
2967
|
-
files.sort();
|
|
2968
|
-
runFile = path.join(runsDir, files[files.length - 1]);
|
|
2969
|
-
}
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
2972
|
-
if (!runFile) {
|
|
2973
|
-
res.writeHead(404);
|
|
2974
|
-
res.end('{"events":[],"runId":null}');
|
|
2975
|
-
return true;
|
|
2976
|
-
}
|
|
2977
|
-
const detectedRunId = isV2
|
|
2978
|
-
? path.basename(path.dirname(runFile))
|
|
2979
|
-
: path.basename(runFile, '.jsonl');
|
|
2980
|
-
const lines = fs
|
|
2981
|
-
.readFileSync(runFile, 'utf8')
|
|
2982
|
-
.split('\n')
|
|
2983
|
-
.filter((l) => l.trim())
|
|
2984
|
-
.slice(-100);
|
|
2985
|
-
const rawEvents = lines
|
|
2986
|
-
.map((l) => {
|
|
2987
|
-
try {
|
|
2988
|
-
return JSON.parse(l);
|
|
2989
|
-
} catch (_) {
|
|
2990
|
-
return null;
|
|
2991
|
-
}
|
|
2992
|
-
})
|
|
2993
|
-
.filter(Boolean);
|
|
2994
|
-
// v1's stored events are already dashboard-native; v2's raw BusEvents
|
|
2995
|
-
// need the same translate()/companionEvents() pass the live view gets.
|
|
2996
|
-
const events = isV2
|
|
2997
|
-
? rawEvents.flatMap((e) => [...companionEvents(e), translate(e)])
|
|
2998
|
-
: rawEvents;
|
|
2999
|
-
res.writeHead(200, {
|
|
3000
|
-
'Content-Type': 'application/json',
|
|
3001
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3002
|
-
});
|
|
3003
|
-
res.end(
|
|
3004
|
-
JSON.stringify({ runId: detectedRunId, events, active: ctx.activeOrgRuns.has(orgName) }),
|
|
3005
|
-
);
|
|
3006
|
-
} catch (err) {
|
|
3007
|
-
res.writeHead(500);
|
|
3008
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
3009
|
-
}
|
|
3010
|
-
return true;
|
|
3011
|
-
}
|
|
3012
|
-
|
|
3013
|
-
// GET /api/orgs/:name/history — per-run outcome summaries (history.jsonl, newest first)
|
|
3014
|
-
if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/history$/.test(url)) {
|
|
3015
|
-
try {
|
|
3016
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3017
|
-
const _histQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3018
|
-
const root = path.resolve(_histQs.get('dir') || ctx.projectDir || process.cwd());
|
|
3019
|
-
if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
3020
|
-
res.writeHead(400);
|
|
3021
|
-
res.end('{"error":"invalid org name"}');
|
|
3022
|
-
return true;
|
|
3023
|
-
}
|
|
3024
|
-
const monoDir = ctx._getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
3025
|
-
const histFile = path.join(monoDir, 'orgs', orgName, 'history.jsonl');
|
|
3026
|
-
let runs = [];
|
|
3027
|
-
if (fs.existsSync(histFile)) {
|
|
3028
|
-
runs = fs
|
|
3029
|
-
.readFileSync(histFile, 'utf8')
|
|
3030
|
-
.split('\n')
|
|
3031
|
-
.filter((l) => l.trim())
|
|
3032
|
-
.map((l) => {
|
|
3033
|
-
try {
|
|
3034
|
-
return JSON.parse(l);
|
|
3035
|
-
} catch (_) {
|
|
3036
|
-
return null;
|
|
3037
|
-
}
|
|
3038
|
-
})
|
|
3039
|
-
.filter(Boolean)
|
|
3040
|
-
.reverse()
|
|
3041
|
-
.slice(0, 50);
|
|
3042
|
-
}
|
|
3043
|
-
res.writeHead(200, {
|
|
3044
|
-
'Content-Type': 'application/json',
|
|
3045
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3046
|
-
});
|
|
3047
|
-
res.end(JSON.stringify({ runs }));
|
|
3048
|
-
} catch (err) {
|
|
3049
|
-
res.writeHead(500);
|
|
3050
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
3051
|
-
}
|
|
3052
|
-
return true;
|
|
3053
|
-
}
|
|
3054
|
-
|
|
3055
|
-
// GET /api/orgs/:name/memory — org knowledge-graph stats + glossary + rules
|
|
3056
|
-
if (req.method === 'GET' && /^\/api\/orgs\/[^/]+\/memory$/.test(url)) {
|
|
3057
|
-
try {
|
|
3058
|
-
const orgName = decodeURIComponent(url.split('/')[3]);
|
|
3059
|
-
const _memQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3060
|
-
const root = path.resolve(_memQs.get('dir') || ctx.projectDir || process.cwd());
|
|
3061
|
-
if (!orgName || orgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(orgName)) {
|
|
3062
|
-
res.writeHead(400);
|
|
3063
|
-
res.end('{"error":"invalid org name"}');
|
|
3064
|
-
return true;
|
|
3065
|
-
}
|
|
3066
|
-
const monoDir = ctx._getGitMonomindDir(root) || path.join(root, '.monomind');
|
|
3067
|
-
const dbPath = path.join(monoDir, 'org-memory');
|
|
3068
|
-
if (!fs.existsSync(dbPath)) {
|
|
3069
|
-
res.writeHead(200, {
|
|
3070
|
-
'Content-Type': 'application/json',
|
|
3071
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3072
|
-
});
|
|
3073
|
-
res.end(JSON.stringify({ nodes: 0, edges: 0, rules: 0, glossary: [], ruleTexts: [] }));
|
|
3074
|
-
return true;
|
|
3075
|
-
}
|
|
3076
|
-
const kg = await import('../memory/memory-kg.js');
|
|
3077
|
-
const [stats, glossary, ruleList] = await Promise.all([
|
|
3078
|
-
kg.kgStats({ dbPath }),
|
|
3079
|
-
kg.kgGlossary({ dbPath, limit: 12 }),
|
|
3080
|
-
kg.kgListRules({ dbPath, limit: 10 }),
|
|
3081
|
-
]);
|
|
3082
|
-
res.writeHead(200, {
|
|
3083
|
-
'Content-Type': 'application/json',
|
|
3084
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3085
|
-
});
|
|
3086
|
-
res.end(
|
|
3087
|
-
JSON.stringify({
|
|
3088
|
-
...stats,
|
|
3089
|
-
glossary,
|
|
3090
|
-
ruleTexts: ruleList.map((r) => r.rule.slice(0, 200)),
|
|
3091
|
-
}),
|
|
3092
|
-
);
|
|
3093
|
-
} catch (err) {
|
|
3094
|
-
res.writeHead(500);
|
|
3095
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
3096
|
-
}
|
|
3097
|
-
return true;
|
|
3098
|
-
}
|
|
3099
|
-
|
|
3100
|
-
// POST /api/knowledge/search — warm semantic knowledge search for hooks.
|
|
3101
|
-
// The dashboard server is the one long-lived process on the machine, so it
|
|
3102
|
-
// holds the local embedding model warm; per-prompt hook subprocesses (which
|
|
3103
|
-
// cannot afford a model load) POST here and fall back to keyword scoring
|
|
3104
|
-
// when this endpoint is cold/absent. Fully local — model + store on disk.
|
|
3105
|
-
if (req.method === 'POST' && url === '/api/knowledge/search') {
|
|
3106
|
-
let body = '';
|
|
3107
|
-
let overLimit = false;
|
|
3108
|
-
req.on('data', (c) => {
|
|
3109
|
-
if (overLimit) return;
|
|
3110
|
-
body += c;
|
|
3111
|
-
if (body.length > 64 * 1024) {
|
|
3112
|
-
// queries are prompts, not documents
|
|
3113
|
-
overLimit = true;
|
|
3114
|
-
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
3115
|
-
res.end('{"error":"request body too large"}');
|
|
3116
|
-
req.destroy();
|
|
3117
|
-
}
|
|
3118
|
-
});
|
|
3119
|
-
req.on('end', async () => {
|
|
3120
|
-
if (overLimit) return;
|
|
3121
|
-
try {
|
|
3122
|
-
const payload = JSON.parse(body || '{}');
|
|
3123
|
-
const query = String(payload.query || '').slice(0, 2000);
|
|
3124
|
-
const limit = Math.min(Math.max(parseInt(payload.limit, 10) || 3, 1), 10);
|
|
3125
|
-
const namespace =
|
|
3126
|
-
typeof payload.namespace === 'string' && /^[A-Za-z0-9:_-]{1,128}$/.test(payload.namespace)
|
|
3127
|
-
? payload.namespace
|
|
3128
|
-
: 'knowledge:shared';
|
|
3129
|
-
if (!query) {
|
|
3130
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3131
|
-
res.end('{"error":"query required"}');
|
|
3132
|
-
return;
|
|
3133
|
-
}
|
|
3134
|
-
const bridge = await ctx._getKnowledgeBridge();
|
|
3135
|
-
if (!bridge) {
|
|
3136
|
-
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
3137
|
-
res.end('{"error":"knowledge bridge unavailable"}');
|
|
3138
|
-
return;
|
|
3139
|
-
}
|
|
3140
|
-
// scope: project | global | all (default all) — project results get a
|
|
3141
|
-
// small tie boost; global hits are flagged so callers can show origin.
|
|
3142
|
-
const scope =
|
|
3143
|
-
payload.scope === 'project' || payload.scope === 'global' ? payload.scope : 'all';
|
|
3144
|
-
// Rule-based router (no LLM): chunks always run (this endpoint's
|
|
3145
|
-
// bread and butter); the auxiliary surfaces — distilled rules,
|
|
3146
|
-
// knowledge-graph triplets, pattern memories — only spend a query
|
|
3147
|
-
// when the router votes for them or routing is unconfident.
|
|
3148
|
-
let wantRules = true,
|
|
3149
|
-
wantKg = true,
|
|
3150
|
-
wantMemory = true;
|
|
3151
|
-
let rrfFuse = null;
|
|
3152
|
-
try {
|
|
3153
|
-
const routerMod = await import('../memory/query-router.js');
|
|
3154
|
-
const route = routerMod.routeQuery(query);
|
|
3155
|
-
rrfFuse = routerMod.rrfFuse;
|
|
3156
|
-
wantRules = !route.confident || route.surfaces.includes('rules');
|
|
3157
|
-
wantKg = !route.confident || route.surfaces.includes('kg');
|
|
3158
|
-
wantMemory = !route.confident || route.surfaces.includes('memory');
|
|
3159
|
-
} catch {
|
|
3160
|
-
/* router unavailable — keep all surfaces on */
|
|
3161
|
-
}
|
|
3162
|
-
let kgSearch = null;
|
|
3163
|
-
if (wantKg && scope !== 'global') {
|
|
3164
|
-
try {
|
|
3165
|
-
kgSearch = (await import('../memory/memory-kg.js')).kgSearch;
|
|
3166
|
-
} catch {
|
|
3167
|
-
/* kg unavailable */
|
|
3168
|
-
}
|
|
3169
|
-
}
|
|
3170
|
-
// Superseded-version filtering — the SAME rule `searchKnowledge` (and
|
|
3171
|
-
// therefore `monomind doc search` / `knowledge_search`) applies. This
|
|
3172
|
-
// endpoint queries the bridge directly for warmth, so without this it
|
|
3173
|
-
// would inject old document versions the Second Brain itself hides.
|
|
3174
|
-
let liveProj = new Set(),
|
|
3175
|
-
liveGlob = new Set(),
|
|
3176
|
-
isSuperseded = () => false,
|
|
3177
|
-
overfetch = (n) => n;
|
|
3178
|
-
// metaProj/metaGlob distinguish "no metadata log" (cannot judge, keep
|
|
3179
|
-
// everything) from "log exists and is empty" (every document was
|
|
3180
|
-
// removed, keep nothing). Without them, `doc remove` of the last
|
|
3181
|
-
// document left its chunks being injected into every prompt.
|
|
3182
|
-
let metaProj = false,
|
|
3183
|
-
metaGlob = false;
|
|
3184
|
-
try {
|
|
3185
|
-
const dp = await import('../knowledge/document-pipeline.js');
|
|
3186
|
-
// CLAUDE_PROJECT_DIR is whatever directory the user opened, which
|
|
3187
|
-
// can be a package subdirectory, while the CLI writes the metadata
|
|
3188
|
-
// log at the project root. Walk up to the nearest one that has it:
|
|
3189
|
-
// resolving the wrong root yields an empty live set, which silently
|
|
3190
|
-
// turns filtering OFF — and superseded rows are the large majority
|
|
3191
|
-
// of a long-lived store, so injection would be dominated by stale
|
|
3192
|
-
// document versions.
|
|
3193
|
-
const resolveKnowledgeRoot = (start) => {
|
|
3194
|
-
let probe = path.resolve(start);
|
|
3195
|
-
for (let up = 0; up < 8; up++) {
|
|
3196
|
-
if (dp.hasKnowledgeMetadata?.(probe)) return probe;
|
|
3197
|
-
const parent = path.dirname(probe);
|
|
3198
|
-
if (parent === probe) break;
|
|
3199
|
-
probe = parent;
|
|
3200
|
-
}
|
|
3201
|
-
return path.resolve(start);
|
|
3202
|
-
};
|
|
3203
|
-
const projRoot = resolveKnowledgeRoot(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
3204
|
-
const globRoot =
|
|
3205
|
-
process.env.MONOMIND_GLOBAL_BRAIN_DIR ||
|
|
3206
|
-
path.join(os.homedir(), '.monomind', 'global-brain');
|
|
3207
|
-
liveProj = dp.liveContentHashes(projRoot);
|
|
3208
|
-
liveGlob = dp.liveContentHashes(globRoot);
|
|
3209
|
-
metaProj = dp.hasKnowledgeMetadata?.(projRoot) ?? liveProj.size > 0;
|
|
3210
|
-
metaGlob = dp.hasKnowledgeMetadata?.(globRoot) ?? liveGlob.size > 0;
|
|
3211
|
-
isSuperseded = dp.isSupersededKey;
|
|
3212
|
-
overfetch = dp.supersededOverfetchLimit;
|
|
3213
|
-
} catch {
|
|
3214
|
-
/* pipeline unavailable — no filtering, same as before */
|
|
3215
|
-
}
|
|
3216
|
-
const keepLive = (rows, live, hasMeta) =>
|
|
3217
|
-
(rows || [])
|
|
3218
|
-
.filter((r) => !isSuperseded(String(r.key || ''), live, hasMeta))
|
|
3219
|
-
.slice(0, limit);
|
|
3220
|
-
const [projRaw, globRaw, rules, graph, mems] = await Promise.all([
|
|
3221
|
-
scope !== 'global'
|
|
3222
|
-
? bridge
|
|
3223
|
-
.bridgeSearchEntries({ query, namespace, limit: overfetch(limit, liveProj) })
|
|
3224
|
-
.catch(() => null)
|
|
3225
|
-
: null,
|
|
3226
|
-
scope !== 'project'
|
|
3227
|
-
? bridge
|
|
3228
|
-
.bridgeSearchEntries({
|
|
3229
|
-
query,
|
|
3230
|
-
namespace: 'knowledge:global',
|
|
3231
|
-
limit: overfetch(limit, liveGlob),
|
|
3232
|
-
dbPath: '@global',
|
|
3233
|
-
})
|
|
3234
|
-
.catch(() => null)
|
|
3235
|
-
: null,
|
|
3236
|
-
// Distilled rules ("when X do Y") learned from sessions/runs —
|
|
3237
|
-
// small namespace, high injection value, threshold keeps it quiet.
|
|
3238
|
-
scope !== 'global' && wantRules
|
|
3239
|
-
? bridge
|
|
3240
|
-
.bridgeSearchEntries({ query, namespace: 'rules', limit: 2, threshold: 0.45 })
|
|
3241
|
-
.catch(() => null)
|
|
3242
|
-
: null,
|
|
3243
|
-
// Knowledge-graph triplets: relationship-shaped queries surface
|
|
3244
|
-
// facts no chunk contains. Triplet scores derive from seeded
|
|
3245
|
-
// cosine matches (≥0.35), so they clear callers' relevance floors.
|
|
3246
|
-
scope !== 'global' && kgSearch ? kgSearch({ query, limit: 4 }).catch(() => null) : null,
|
|
3247
|
-
// Past patterns/decisions for "last time / previously" queries.
|
|
3248
|
-
scope !== 'global' && wantMemory
|
|
3249
|
-
? bridge
|
|
3250
|
-
.bridgeSearchEntries({ query, namespace: 'patterns', limit: 2, threshold: 0.4 })
|
|
3251
|
-
.catch(() => null)
|
|
3252
|
-
: null,
|
|
3253
|
-
]);
|
|
3254
|
-
const lists = [
|
|
3255
|
-
keepLive(projRaw?.results, liveProj, metaProj).map((r) => ({
|
|
3256
|
-
id: r.id,
|
|
3257
|
-
key: r.key,
|
|
3258
|
-
content: String(r.content || '').slice(0, 2000),
|
|
3259
|
-
score: r.score + 0.05,
|
|
3260
|
-
global: false,
|
|
3261
|
-
tags: r.tags,
|
|
3262
|
-
importance: 0.6,
|
|
3263
|
-
})),
|
|
3264
|
-
keepLive(globRaw?.results, liveGlob, metaGlob).map((r) => ({
|
|
3265
|
-
id: r.id,
|
|
3266
|
-
key: r.key,
|
|
3267
|
-
content: String(r.content || '').slice(0, 2000),
|
|
3268
|
-
score: r.score,
|
|
3269
|
-
global: true,
|
|
3270
|
-
tags: r.tags,
|
|
3271
|
-
})),
|
|
3272
|
-
(rules?.results || []).map((r) => ({
|
|
3273
|
-
id: r.id,
|
|
3274
|
-
key: r.key,
|
|
3275
|
-
content: String(r.content || '').slice(0, 2000),
|
|
3276
|
-
score: r.score + 0.05,
|
|
3277
|
-
global: false,
|
|
3278
|
-
rule: true,
|
|
3279
|
-
tags: r.tags,
|
|
3280
|
-
importance: 0.7,
|
|
3281
|
-
})),
|
|
3282
|
-
(graph?.triplets || []).map((t, i) => ({
|
|
3283
|
-
id: `kg:${i}:${t.source}|${t.relation}|${t.target}`,
|
|
3284
|
-
key: `kg:${t.source}`,
|
|
3285
|
-
content: String(
|
|
3286
|
-
t.source +
|
|
3287
|
-
' —' +
|
|
3288
|
-
t.relation +
|
|
3289
|
-
'→ ' +
|
|
3290
|
-
t.target +
|
|
3291
|
-
(t.fact && t.fact !== `${t.source} ${t.relation} ${t.target}`
|
|
3292
|
-
? ` (${String(t.fact).slice(0, 300)})`
|
|
3293
|
-
: ''),
|
|
3294
|
-
).slice(0, 2000),
|
|
3295
|
-
score: t.score,
|
|
3296
|
-
global: false,
|
|
3297
|
-
triplet: true,
|
|
3298
|
-
})),
|
|
3299
|
-
(mems?.results || []).map((r) => ({
|
|
3300
|
-
id: r.id,
|
|
3301
|
-
key: r.key,
|
|
3302
|
-
content: String(r.content || '').slice(0, 2000),
|
|
3303
|
-
score: r.score,
|
|
3304
|
-
global: false,
|
|
3305
|
-
memory: true,
|
|
3306
|
-
tags: r.tags,
|
|
3307
|
-
})),
|
|
3308
|
-
];
|
|
3309
|
-
// Rank-fuse across surfaces (raw scores aren't comparable between
|
|
3310
|
-
// cosine chunks and blended triplets); each item keeps its native
|
|
3311
|
-
// score so downstream relevance floors still apply. Fallback: flat
|
|
3312
|
-
// score sort when the router module failed to load.
|
|
3313
|
-
const merged = (
|
|
3314
|
-
rrfFuse
|
|
3315
|
-
? rrfFuse(lists, limit)
|
|
3316
|
-
: lists
|
|
3317
|
-
.flat()
|
|
3318
|
-
.sort((a, b) => b.score - a.score)
|
|
3319
|
-
.slice(0, limit)
|
|
3320
|
-
).map(({ importance, rrf, ...r }) => r);
|
|
3321
|
-
// Served excerpts are (very likely) injected into the caller's prompt —
|
|
3322
|
-
// that IS usage. Reinforce frequency_weight, per-store, fire-and-forget.
|
|
3323
|
-
try {
|
|
3324
|
-
const projIds = merged.filter((m) => !m.global && !m.triplet && m.id).map((m) => m.id);
|
|
3325
|
-
const globIds = merged.filter((m) => m.global && m.id).map((m) => m.id);
|
|
3326
|
-
if (projIds.length) bridge.bridgeRecordUsage?.({ entryIds: projIds }).catch(() => {});
|
|
3327
|
-
if (globIds.length)
|
|
3328
|
-
bridge.bridgeRecordUsage?.({ entryIds: globIds, dbPath: '@global' }).catch(() => {});
|
|
3329
|
-
} catch {
|
|
3330
|
-
/* best effort */
|
|
3331
|
-
}
|
|
3332
|
-
res.writeHead(200, {
|
|
3333
|
-
'Content-Type': 'application/json',
|
|
3334
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3335
|
-
});
|
|
3336
|
-
res.end(
|
|
3337
|
-
JSON.stringify({
|
|
3338
|
-
method:
|
|
3339
|
-
projRaw?.searchMethod ||
|
|
3340
|
-
globRaw?.searchMethod ||
|
|
3341
|
-
rules?.searchMethod ||
|
|
3342
|
-
mems?.searchMethod ||
|
|
3343
|
-
(merged.length ? 'mixed' : 'none'),
|
|
3344
|
-
results: merged,
|
|
3345
|
-
}),
|
|
3346
|
-
);
|
|
3347
|
-
} catch (err) {
|
|
3348
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3349
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
3350
|
-
}
|
|
3351
|
-
});
|
|
3352
|
-
return true;
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
// GET /api/mastermind/metrics — aggregate system metrics from token-summary and monoswarm-activity
|
|
3356
|
-
if (req.method === 'GET' && url === '/api/mastermind/metrics') {
|
|
3357
|
-
try {
|
|
3358
|
-
const base = path.join(ctx.projectDir || process.cwd(), '.monomind', 'metrics');
|
|
3359
|
-
let tokens = {},
|
|
3360
|
-
monoswarm = {},
|
|
3361
|
-
events = [];
|
|
3362
|
-
try {
|
|
3363
|
-
tokens = JSON.parse(fs.readFileSync(path.join(base, 'token-summary.json'), 'utf8'));
|
|
3364
|
-
} catch (_) {}
|
|
3365
|
-
try {
|
|
3366
|
-
monoswarm = JSON.parse(fs.readFileSync(path.join(base, 'monoswarm-activity.json'), 'utf8'));
|
|
3367
|
-
} catch (_) {}
|
|
3368
|
-
try {
|
|
3369
|
-
const evPath = path.join(
|
|
3370
|
-
ctx.projectDir || process.cwd(),
|
|
3371
|
-
'data',
|
|
3372
|
-
'mastermind-events.jsonl',
|
|
3373
|
-
);
|
|
3374
|
-
const lines = fs
|
|
3375
|
-
.readFileSync(evPath, 'utf8')
|
|
3376
|
-
.split('\n')
|
|
3377
|
-
.filter((l) => l.trim())
|
|
3378
|
-
.slice(-20);
|
|
3379
|
-
events = lines
|
|
3380
|
-
.map((l) => {
|
|
3381
|
-
try {
|
|
3382
|
-
return JSON.parse(l);
|
|
3383
|
-
} catch (_) {
|
|
3384
|
-
return null;
|
|
3385
|
-
}
|
|
3386
|
-
})
|
|
3387
|
-
.filter(Boolean);
|
|
3388
|
-
} catch (_) {}
|
|
3389
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3390
|
-
res.end(JSON.stringify({ tokens, monoswarm, recentEvents: events }));
|
|
3391
|
-
} catch (_) {
|
|
3392
|
-
res.writeHead(500);
|
|
3393
|
-
res.end('{"tokens":{},"monoswarm":{},"recentEvents":[]}');
|
|
3394
|
-
}
|
|
3395
|
-
return true;
|
|
3396
|
-
}
|
|
3397
|
-
|
|
3398
|
-
// ------------------------------------------------- GET /api/playbooks
|
|
3399
|
-
// List playbook definitions from <dir>/.monomind/playbooks/*.json.
|
|
3400
|
-
// (Previously only POST was registered, so GET /api/playbooks?dir=... fell
|
|
3401
|
-
// through to the 404 handler.)
|
|
3402
|
-
if (req.method === 'GET' && url === '/api/playbooks') {
|
|
3403
|
-
try {
|
|
3404
|
-
const qp = new URL(req.url, 'http://localhost').searchParams;
|
|
3405
|
-
const dir = qp.get('dir') || ctx.projectDir || process.cwd();
|
|
3406
|
-
const playbookDir = path.join(path.resolve(dir), '.monomind', 'playbooks');
|
|
3407
|
-
const result = [];
|
|
3408
|
-
if (fs.existsSync(playbookDir)) {
|
|
3409
|
-
const files = fs
|
|
3410
|
-
.readdirSync(playbookDir)
|
|
3411
|
-
.filter((f) => f.endsWith('.json') && !f.startsWith('._'));
|
|
3412
|
-
for (const file of files) {
|
|
3413
|
-
try {
|
|
3414
|
-
const fpath = path.join(playbookDir, file);
|
|
3415
|
-
const def = JSON.parse(fs.readFileSync(fpath, 'utf8'));
|
|
3416
|
-
const stat = fs.statSync(fpath);
|
|
3417
|
-
result.push({
|
|
3418
|
-
...def,
|
|
3419
|
-
id: def.id || file.replace('.json', ''),
|
|
3420
|
-
file,
|
|
3421
|
-
modifiedAt: stat.mtimeMs,
|
|
3422
|
-
});
|
|
3423
|
-
} catch (_) {}
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
res.writeHead(200, {
|
|
3427
|
-
'Content-Type': 'application/json',
|
|
3428
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3429
|
-
'Cache-Control': 'no-cache',
|
|
3430
|
-
});
|
|
3431
|
-
res.end(JSON.stringify(result));
|
|
3432
|
-
} catch (e) {
|
|
3433
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3434
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
3435
|
-
}
|
|
3436
|
-
return true;
|
|
3437
|
-
}
|
|
3438
|
-
|
|
3439
|
-
// ------------------------------------------------- POST /api/playbooks
|
|
3440
|
-
// Save a playbook definition to .monomind/playbooks/<id>.json
|
|
3441
|
-
if (req.method === 'POST' && url === '/api/playbooks') {
|
|
3442
|
-
try {
|
|
3443
|
-
let body = '';
|
|
3444
|
-
await new Promise((resolve, reject) => {
|
|
3445
|
-
req.on('data', (d) => {
|
|
3446
|
-
body += d;
|
|
3447
|
-
});
|
|
3448
|
-
req.on('end', resolve);
|
|
3449
|
-
req.on('error', reject);
|
|
3450
|
-
});
|
|
3451
|
-
const pb = JSON.parse(body);
|
|
3452
|
-
if (!pb.id || !pb.name) {
|
|
3453
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3454
|
-
res.end(JSON.stringify({ error: 'id and name are required' }));
|
|
3455
|
-
return true;
|
|
3456
|
-
}
|
|
3457
|
-
const dir = ctx.projectDir || process.cwd();
|
|
3458
|
-
const playbookDir = path.join(dir, '.monomind', 'playbooks');
|
|
3459
|
-
fs.mkdirSync(playbookDir, { recursive: true });
|
|
3460
|
-
const filePath = path.join(playbookDir, `${pb.id}.json`);
|
|
3461
|
-
fs.writeFileSync(filePath, JSON.stringify(pb, null, 2));
|
|
3462
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3463
|
-
res.end(JSON.stringify({ ok: true, id: pb.id, file: `${pb.id}.json` }));
|
|
3464
|
-
} catch (e) {
|
|
3465
|
-
res.writeHead(500);
|
|
3466
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
3467
|
-
}
|
|
3468
|
-
return true;
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
// ------------------------------------------------- GET /api/workflow-defs
|
|
3472
|
-
if (req.method === 'GET' && url === '/api/workflow-defs') {
|
|
3473
|
-
try {
|
|
3474
|
-
const qp = new URL(req.url, 'http://x').searchParams;
|
|
3475
|
-
const dir = qp.get('dir') || ctx.projectDir || process.cwd();
|
|
3476
|
-
const playbookDir = path.join(dir, '.monomind', 'playbooks');
|
|
3477
|
-
const result = [];
|
|
3478
|
-
if (fs.existsSync(playbookDir)) {
|
|
3479
|
-
const files = fs.readdirSync(playbookDir).filter((f) => f.endsWith('.json'));
|
|
3480
|
-
for (const file of files) {
|
|
3481
|
-
try {
|
|
3482
|
-
const fpath = path.join(playbookDir, file);
|
|
3483
|
-
const stat = fs.statSync(fpath);
|
|
3484
|
-
const def = JSON.parse(fs.readFileSync(fpath, 'utf8'));
|
|
3485
|
-
const params = (def.params || []).map((p) =>
|
|
3486
|
-
typeof p === 'string' ? p : p.name || p.key || '',
|
|
3487
|
-
);
|
|
3488
|
-
result.push({
|
|
3489
|
-
id: def.id || file.replace('.json', ''),
|
|
3490
|
-
name: def.name || file.replace('.json', ''),
|
|
3491
|
-
description: def.description || null,
|
|
3492
|
-
file,
|
|
3493
|
-
nodeCount: Array.isArray(def.nodes) ? def.nodes.length : 0,
|
|
3494
|
-
params,
|
|
3495
|
-
modifiedAt: stat.mtimeMs,
|
|
3496
|
-
});
|
|
3497
|
-
} catch (_) {}
|
|
3498
|
-
}
|
|
3499
|
-
}
|
|
3500
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3501
|
-
res.end(JSON.stringify(result));
|
|
3502
|
-
} catch (e) {
|
|
3503
|
-
res.writeHead(500);
|
|
3504
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
3505
|
-
}
|
|
3506
|
-
return true;
|
|
3507
|
-
}
|
|
3508
|
-
|
|
3509
|
-
// ------------------------------------------------- GET /api/org-coverage
|
|
3510
|
-
// Audit coverage report written by the audit loop to .monomind/audit/coverage.json
|
|
3511
|
-
if (req.method === 'GET' && url === '/api/org-coverage') {
|
|
3512
|
-
try {
|
|
3513
|
-
const qp = new URL(req.url, 'http://localhost').searchParams;
|
|
3514
|
-
const dir = path.resolve(qp.get('dir') || ctx.projectDir || process.cwd());
|
|
3515
|
-
const covPath = path.join(dir, '.monomind', 'audit', 'coverage.json');
|
|
3516
|
-
if (!fs.existsSync(covPath)) {
|
|
3517
|
-
res.writeHead(404, {
|
|
3518
|
-
'Content-Type': 'application/json',
|
|
3519
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3520
|
-
});
|
|
3521
|
-
res.end('{}');
|
|
3522
|
-
return true;
|
|
3523
|
-
}
|
|
3524
|
-
const coverage = JSON.parse(fs.readFileSync(covPath, 'utf8'));
|
|
3525
|
-
res.writeHead(200, {
|
|
3526
|
-
'Content-Type': 'application/json',
|
|
3527
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3528
|
-
'Cache-Control': 'no-cache',
|
|
3529
|
-
});
|
|
3530
|
-
res.end(JSON.stringify(coverage));
|
|
3531
|
-
} catch (e) {
|
|
3532
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3533
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
3534
|
-
}
|
|
3535
|
-
return true;
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
|
-
// ------------------------------------------------- GET /api/workflow-runs
|
|
3539
|
-
if (req.method === 'GET' && url === '/api/workflow-runs') {
|
|
3540
|
-
// Reads from ~/.monomind/browse-runs.json written by the monobrowse dashboard server.
|
|
3541
|
-
try {
|
|
3542
|
-
const runsFile = path.join(os.homedir(), '.monomind', 'browse-runs.json');
|
|
3543
|
-
if (fs.existsSync(runsFile)) {
|
|
3544
|
-
const raw = fs.readFileSync(runsFile, 'utf-8');
|
|
3545
|
-
const runs = JSON.parse(raw);
|
|
3546
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3547
|
-
res.end(JSON.stringify(Array.isArray(runs) ? runs : []));
|
|
3548
|
-
} else {
|
|
3549
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3550
|
-
res.end('[]');
|
|
3551
|
-
}
|
|
3552
|
-
} catch {
|
|
3553
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3554
|
-
res.end('[]');
|
|
3555
|
-
}
|
|
3556
|
-
return true;
|
|
3557
|
-
}
|
|
3558
|
-
|
|
3559
|
-
// ---- POST /api/orgs/:name/mark-complete — manual STALE recovery ----
|
|
3560
|
-
if (
|
|
3561
|
-
req.method === 'POST' &&
|
|
3562
|
-
/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/mark-complete$/i.test(url)
|
|
3563
|
-
) {
|
|
3564
|
-
const _mcOrgName = decodeURIComponent(url.split('/')[3]);
|
|
3565
|
-
if (_mcOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_mcOrgName)) {
|
|
3566
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3567
|
-
res.end(JSON.stringify({ error: 'Invalid org name' }));
|
|
3568
|
-
return true;
|
|
3569
|
-
}
|
|
3570
|
-
const _mcRoot = ctx.projectDir || process.cwd();
|
|
3571
|
-
const _mcMonoDir = ctx._getGitMonomindDir(_mcRoot) || path.join(_mcRoot, '.monomind');
|
|
3572
|
-
const _mcRunId = ctx.activeOrgRuns.get(_mcOrgName) || ctx._getActiveRunId(_mcOrgName, _mcRoot);
|
|
3573
|
-
if (!_mcRunId) {
|
|
3574
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
3575
|
-
res.end(JSON.stringify({ error: `No active run for org: ${_mcOrgName}` }));
|
|
3576
|
-
return true;
|
|
3577
|
-
}
|
|
3578
|
-
const _mcEvent = {
|
|
3579
|
-
type: 'run:complete',
|
|
3580
|
-
org: _mcOrgName,
|
|
3581
|
-
runId: _mcRunId,
|
|
3582
|
-
ts: Date.now(),
|
|
3583
|
-
reason: 'manual',
|
|
3584
|
-
};
|
|
3585
|
-
try {
|
|
3586
|
-
const _mcRunFile = path.join(_mcMonoDir, 'orgs', _mcOrgName, 'runs', `${_mcRunId}.jsonl`);
|
|
3587
|
-
if (fs.existsSync(_mcRunFile))
|
|
3588
|
-
await ctx.appendToFile(_mcRunFile, `${JSON.stringify(_mcEvent)}\n`);
|
|
3589
|
-
ctx.activeOrgRuns.delete(_mcOrgName);
|
|
3590
|
-
// Clean up ppid-keyed active-run files for this org
|
|
3591
|
-
const _mcCapDir = path.join(ctx.MONOMIND_HOME, '.monomind', 'capture');
|
|
3592
|
-
try {
|
|
3593
|
-
const _mcPpidDir = path.join(_mcCapDir, 'active-runs');
|
|
3594
|
-
if (fs.existsSync(_mcPpidDir)) {
|
|
3595
|
-
fs.readdirSync(_mcPpidDir)
|
|
3596
|
-
.filter((f) => f.endsWith('.json'))
|
|
3597
|
-
.forEach((_pf) => {
|
|
3598
|
-
try {
|
|
3599
|
-
const _pd = JSON.parse(fs.readFileSync(path.join(_mcPpidDir, _pf), 'utf8'));
|
|
3600
|
-
if (_pd.org === _mcOrgName) fs.unlinkSync(path.join(_mcPpidDir, _pf));
|
|
3601
|
-
} catch (_) {}
|
|
3602
|
-
});
|
|
3603
|
-
}
|
|
3604
|
-
const _mcActiveFile = path.join(_mcCapDir, 'active-run.json');
|
|
3605
|
-
if (fs.existsSync(_mcActiveFile)) {
|
|
3606
|
-
try {
|
|
3607
|
-
const _a = JSON.parse(fs.readFileSync(_mcActiveFile, 'utf8'));
|
|
3608
|
-
if (_a.org === _mcOrgName) fs.unlinkSync(_mcActiveFile);
|
|
3609
|
-
} catch (_) {}
|
|
3610
|
-
}
|
|
3611
|
-
} catch (_) {}
|
|
3612
|
-
ctx._updateRunState(_mcEvent, _mcRoot);
|
|
3613
|
-
ctx.broadcastMm(_mcEvent);
|
|
3614
|
-
const _mcFwdClients = ctx.runStreamClients.get(_mcOrgName);
|
|
3615
|
-
if (_mcFwdClients && _mcFwdClients.size > 0) {
|
|
3616
|
-
const _mcLine = `data: ${JSON.stringify(_mcEvent)}\n\n`;
|
|
3617
|
-
for (const _cl of _mcFwdClients) {
|
|
3618
|
-
try {
|
|
3619
|
-
_cl.write(_mcLine);
|
|
3620
|
-
} catch (_) {
|
|
3621
|
-
_mcFwdClients.delete(_cl);
|
|
3622
|
-
}
|
|
3623
|
-
}
|
|
3624
|
-
}
|
|
3625
|
-
res.writeHead(200, {
|
|
3626
|
-
'Content-Type': 'application/json',
|
|
3627
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3628
|
-
});
|
|
3629
|
-
res.end(JSON.stringify({ ok: true, runId: _mcRunId }));
|
|
3630
|
-
} catch (e) {
|
|
3631
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3632
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
3633
|
-
}
|
|
3634
|
-
return true;
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
|
-
// ---- GET /api/orgs/:name/runs/current/stream — Phase 3 streaming tail ----
|
|
3638
|
-
if (
|
|
3639
|
-
req.method === 'GET' &&
|
|
3640
|
-
/^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/runs\/current\/stream$/i.test(url)
|
|
3641
|
-
) {
|
|
3642
|
-
const _stOrgName = decodeURIComponent(url.split('/')[3]);
|
|
3643
|
-
if (_stOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_stOrgName)) {
|
|
3644
|
-
res.writeHead(400);
|
|
3645
|
-
res.end('Invalid org name');
|
|
3646
|
-
return true;
|
|
3647
|
-
}
|
|
3648
|
-
const _stQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3649
|
-
const _stSince = Math.max(0, parseInt(_stQs.get('since') || '0', 10) || 0);
|
|
3650
|
-
res.writeHead(200, {
|
|
3651
|
-
'Content-Type': 'text/event-stream',
|
|
3652
|
-
'Cache-Control': 'no-cache',
|
|
3653
|
-
Connection: 'keep-alive',
|
|
3654
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3655
|
-
'X-Accel-Buffering': 'no',
|
|
3656
|
-
});
|
|
3657
|
-
res.write(': connected\n\n');
|
|
3658
|
-
// Register client for live events
|
|
3659
|
-
if (!ctx.runStreamClients.has(_stOrgName)) ctx.runStreamClients.set(_stOrgName, new Set());
|
|
3660
|
-
ctx.runStreamClients.get(_stOrgName).add(res);
|
|
3661
|
-
// Replay events since `since` (SQLite row id cursor; falls back to JSONL line offset)
|
|
3662
|
-
try {
|
|
3663
|
-
let _stReplayedViaSqlite = false;
|
|
3664
|
-
if (ctx._runDb) {
|
|
3665
|
-
// SQLite path: cursor is last row id seen (client sends 0 on first connect)
|
|
3666
|
-
const _stStmt = ctx._runDb.prepare(
|
|
3667
|
-
'SELECT id, raw FROM run_events WHERE org=? AND id > ? ORDER BY id LIMIT 2000',
|
|
3668
|
-
);
|
|
3669
|
-
_stStmt.bind([_stOrgName, _stSince]);
|
|
3670
|
-
let _stLastId = _stSince;
|
|
3671
|
-
while (_stStmt.step()) {
|
|
3672
|
-
const _stRow = _stStmt.getAsObject();
|
|
3673
|
-
try {
|
|
3674
|
-
res.write(`data: ${_stRow.raw}\n\n`);
|
|
3675
|
-
_stLastId = _stRow.id;
|
|
3676
|
-
} catch (_) {
|
|
3677
|
-
break;
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
_stStmt.free();
|
|
3681
|
-
if (_stLastId > _stSince) {
|
|
3682
|
-
// SQLite had rows — send cursor and skip JSONL fallback
|
|
3683
|
-
res.write(
|
|
3684
|
-
`data: ${JSON.stringify({ type: 'stream:replay-done', count: _stLastId })}\n\n`,
|
|
3685
|
-
);
|
|
3686
|
-
_stReplayedViaSqlite = true;
|
|
3687
|
-
}
|
|
3688
|
-
}
|
|
3689
|
-
if (!_stReplayedViaSqlite) {
|
|
3690
|
-
// JSONL fallback: SQLite absent or returned 0 rows — read directly from run file.
|
|
3691
|
-
// `since` is a 0-based line offset in this path.
|
|
3692
|
-
const _stRoot = ctx.projectDir || process.cwd();
|
|
3693
|
-
const _stRunId =
|
|
3694
|
-
ctx.activeOrgRuns.get(_stOrgName) || ctx._getActiveRunId(_stOrgName, _stRoot);
|
|
3695
|
-
if (_stRunId) {
|
|
3696
|
-
const _stMono = ctx._getGitMonomindDir(_stRoot) || path.join(_stRoot, '.monomind');
|
|
3697
|
-
const _stRunFile = path.join(_stMono, 'orgs', _stOrgName, 'runs', `${_stRunId}.jsonl`);
|
|
3698
|
-
if (fs.existsSync(_stRunFile)) {
|
|
3699
|
-
const _stLines = fs.readFileSync(_stRunFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
3700
|
-
for (let _i = _stSince; _i < _stLines.length; _i++) {
|
|
3701
|
-
try {
|
|
3702
|
-
res.write(`data: ${_stLines[_i]}\n\n`);
|
|
3703
|
-
} catch (_) {
|
|
3704
|
-
break;
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
res.write(
|
|
3708
|
-
`data: ${JSON.stringify({ type: 'stream:replay-done', count: _stLines.length })}\n\n`,
|
|
3709
|
-
);
|
|
3710
|
-
} else {
|
|
3711
|
-
res.write(`data: ${JSON.stringify({ type: 'stream:replay-done', count: 0 })}\n\n`);
|
|
3712
|
-
}
|
|
3713
|
-
} else {
|
|
3714
|
-
res.write(`data: ${JSON.stringify({ type: 'stream:replay-done', count: 0 })}\n\n`);
|
|
3715
|
-
}
|
|
3716
|
-
}
|
|
3717
|
-
} catch (_) {}
|
|
3718
|
-
const _stKa = setInterval(() => {
|
|
3719
|
-
try {
|
|
3720
|
-
res.write(': ping\n\n');
|
|
3721
|
-
} catch (_) {
|
|
3722
|
-
clearInterval(_stKa);
|
|
3723
|
-
}
|
|
3724
|
-
}, 20000);
|
|
3725
|
-
req.on('close', () => {
|
|
3726
|
-
clearInterval(_stKa);
|
|
3727
|
-
const _stClients = ctx.runStreamClients.get(_stOrgName);
|
|
3728
|
-
if (_stClients) {
|
|
3729
|
-
_stClients.delete(res);
|
|
3730
|
-
if (_stClients.size === 0) ctx.runStreamClients.delete(_stOrgName);
|
|
3731
|
-
}
|
|
3732
|
-
});
|
|
3733
|
-
return true;
|
|
3734
|
-
}
|
|
3735
|
-
|
|
3736
|
-
// ---- POST /api/orgs/:name/chat — user sends a message to a running org ----
|
|
3737
|
-
if (req.method === 'POST' && /^\/api\/orgs\/[a-z0-9][a-z0-9_-]{0,63}\/chat$/i.test(url)) {
|
|
3738
|
-
let _chBody = '';
|
|
3739
|
-
for await (const chunk of req) {
|
|
3740
|
-
_chBody += chunk;
|
|
3741
|
-
if (_chBody.length > 65536) {
|
|
3742
|
-
req.destroy();
|
|
3743
|
-
break;
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
try {
|
|
3747
|
-
const _chOrgName = decodeURIComponent(url.split('/')[3]);
|
|
3748
|
-
if (_chOrgName.length > 64 || !/^[a-z0-9][a-z0-9_-]*$/i.test(_chOrgName)) {
|
|
3749
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3750
|
-
res.end(JSON.stringify({ error: 'Invalid org name' }));
|
|
3751
|
-
return true;
|
|
3752
|
-
}
|
|
3753
|
-
let _chPayload = {};
|
|
3754
|
-
try {
|
|
3755
|
-
_chPayload = JSON.parse(_chBody);
|
|
3756
|
-
} catch (_) {}
|
|
3757
|
-
const _chText = String(_chPayload.text || '')
|
|
3758
|
-
.trim()
|
|
3759
|
-
.slice(0, 4096);
|
|
3760
|
-
if (!_chText) {
|
|
3761
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3762
|
-
res.end(JSON.stringify({ error: 'text is required' }));
|
|
3763
|
-
return true;
|
|
3764
|
-
}
|
|
3765
|
-
const _chQs = new URL(req.url, 'http://localhost').searchParams;
|
|
3766
|
-
const _chServerRoot = path.resolve(_chQs.get('dir') || ctx.projectDir || process.cwd());
|
|
3767
|
-
const _chRoot = ctx._resolveOrgProjectDir(_chOrgName, _chServerRoot) || _chServerRoot;
|
|
3768
|
-
const _chMonoDir = ctx._getGitMonomindDir(_chRoot) || path.join(_chRoot, '.monomind');
|
|
3769
|
-
const _chRunId =
|
|
3770
|
-
ctx.activeOrgRuns.get(_chOrgName) || ctx._getActiveRunId(_chOrgName, _chRoot);
|
|
3771
|
-
// Deliver to the target role (default: the root role) — live into its
|
|
3772
|
-
// mailbox, or queued in the org's inbox for its next run.
|
|
3773
|
-
let _chRoles = [];
|
|
3774
|
-
try {
|
|
3775
|
-
const _chCfg = JSON.parse(
|
|
3776
|
-
fs.readFileSync(path.join(_chRoot, '.monomind', 'orgs', `${_chOrgName}.json`), 'utf8'),
|
|
3777
|
-
);
|
|
3778
|
-
if (Array.isArray(_chCfg.roles)) _chRoles = _chCfg.roles;
|
|
3779
|
-
} catch (_) {}
|
|
3780
|
-
const _chTargetRole =
|
|
3781
|
-
typeof _chPayload.role === 'string' && _chPayload.role
|
|
3782
|
-
? _chRoles.find((r) => r.id === _chPayload.role)?.id
|
|
3783
|
-
: (_chRoles.find((r) => !r.reports_to) || _chRoles[0])?.id;
|
|
3784
|
-
if (!_chTargetRole) {
|
|
3785
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
3786
|
-
res.end(
|
|
3787
|
-
JSON.stringify({
|
|
3788
|
-
ok: false,
|
|
3789
|
-
error: _chPayload.role
|
|
3790
|
-
? `org "${_chOrgName}" has no role "${_chPayload.role}"`
|
|
3791
|
-
: `org "${_chOrgName}" has no role to receive it`,
|
|
3792
|
-
}),
|
|
3793
|
-
);
|
|
3794
|
-
return true;
|
|
3795
|
-
}
|
|
3796
|
-
let _chDelivery;
|
|
3797
|
-
try {
|
|
3798
|
-
_chDelivery = (await hil.sendHumanMessage(_chRoot, _chOrgName, _chTargetRole, _chText))
|
|
3799
|
-
.delivery;
|
|
3800
|
-
} catch (err) {
|
|
3801
|
-
const { status, error } = hil.hilErrorStatus(err);
|
|
3802
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
3803
|
-
res.end(JSON.stringify({ ok: false, error }));
|
|
3804
|
-
return true;
|
|
3805
|
-
}
|
|
3806
|
-
const _chEvent = {
|
|
3807
|
-
type: 'user:message',
|
|
3808
|
-
org: _chOrgName,
|
|
3809
|
-
runId: _chRunId || null,
|
|
3810
|
-
text: _chText,
|
|
3811
|
-
ts: Date.now(),
|
|
3812
|
-
};
|
|
3813
|
-
// Write to run JSONL if active run exists
|
|
3814
|
-
if (_chRunId) {
|
|
3815
|
-
const _chRunFile = path.join(_chMonoDir, 'orgs', _chOrgName, 'runs', `${_chRunId}.jsonl`);
|
|
3816
|
-
if (fs.existsSync(_chRunFile))
|
|
3817
|
-
await ctx.appendToFile(_chRunFile, `${JSON.stringify(_chEvent)}\n`);
|
|
3818
|
-
}
|
|
3819
|
-
// Broadcast to SSE stream clients
|
|
3820
|
-
ctx.broadcastMm(_chEvent);
|
|
3821
|
-
const _chFwdClients = ctx.runStreamClients.get(_chOrgName);
|
|
3822
|
-
if (_chFwdClients && _chFwdClients.size > 0) {
|
|
3823
|
-
const _chLine = `data: ${JSON.stringify(_chEvent)}\n\n`;
|
|
3824
|
-
for (const _cl of _chFwdClients) {
|
|
3825
|
-
try {
|
|
3826
|
-
_cl.write(_chLine);
|
|
3827
|
-
} catch (_) {
|
|
3828
|
-
_chFwdClients.delete(_cl);
|
|
3829
|
-
}
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
res.writeHead(200, {
|
|
3833
|
-
'Content-Type': 'application/json',
|
|
3834
|
-
...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
|
|
3835
|
-
});
|
|
3836
|
-
res.end(
|
|
3837
|
-
JSON.stringify({
|
|
3838
|
-
ok: true,
|
|
3839
|
-
delivered: _chDelivery === 'live',
|
|
3840
|
-
delivery: _chDelivery,
|
|
3841
|
-
role: _chTargetRole,
|
|
3842
|
-
}),
|
|
3843
|
-
);
|
|
3844
|
-
} catch (e) {
|
|
3845
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3846
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
3847
|
-
}
|
|
3848
|
-
return true;
|
|
3849
|
-
}
|
|
16
|
+
if (await handleOrgConfigRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
17
|
+
if (await handleOrgAgentRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
18
|
+
if (await handleOrgStatusRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
19
|
+
if (await handleOrgApprovalRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
20
|
+
if (await handleOrgPlanningRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
21
|
+
if (await handleOrgFileRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
22
|
+
if (await handleOrgLifecycleRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
23
|
+
if (await handleOrgRunRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
24
|
+
if (await handleOrgMastermindRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
25
|
+
if (await handleOrgLiveRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
26
|
+
if (await handleOrgKnowledgeRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
27
|
+
if (await handleOrgWorkflowRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
28
|
+
if (await handleOrgControlRoutes(req, res, url, corsOrigin, ctx)) return true;
|
|
3850
29
|
return false;
|
|
3851
30
|
}
|