@monoes/monomindcli 2.12.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/.claude/skills/mastermind/SKILL.md +1 -15
  2. package/README.md +1 -1
  3. package/dist/src/commands/doctor-mcp-probe.d.ts +48 -0
  4. package/dist/src/commands/doctor-mcp-probe.d.ts.map +1 -0
  5. package/dist/src/commands/doctor-mcp-probe.js +205 -0
  6. package/dist/src/commands/doctor-mcp-probe.js.map +1 -0
  7. package/dist/src/commands/doctor-monoes-checks.d.ts +1 -0
  8. package/dist/src/commands/doctor-monoes-checks.d.ts.map +1 -1
  9. package/dist/src/commands/doctor-monoes-checks.js +96 -34
  10. package/dist/src/commands/doctor-monoes-checks.js.map +1 -1
  11. package/dist/src/commands/doctor-project-checks.d.ts +31 -1
  12. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  13. package/dist/src/commands/doctor-project-checks.js +109 -4
  14. package/dist/src/commands/doctor-project-checks.js.map +1 -1
  15. package/dist/src/commands/doctor.d.ts.map +1 -1
  16. package/dist/src/commands/doctor.js +21 -3
  17. package/dist/src/commands/doctor.js.map +1 -1
  18. package/dist/src/commands/init.d.ts.map +1 -1
  19. package/dist/src/commands/init.js +29 -0
  20. package/dist/src/commands/init.js.map +1 -1
  21. package/dist/src/commands/mcp.d.ts.map +1 -1
  22. package/dist/src/commands/mcp.js +4 -3
  23. package/dist/src/commands/mcp.js.map +1 -1
  24. package/dist/src/commands/org-observe.d.ts.map +1 -1
  25. package/dist/src/commands/org-observe.js +14 -0
  26. package/dist/src/commands/org-observe.js.map +1 -1
  27. package/dist/src/commands/org.d.ts.map +1 -1
  28. package/dist/src/commands/org.js +21 -4
  29. package/dist/src/commands/org.js.map +1 -1
  30. package/dist/src/commands/ui.d.ts.map +1 -1
  31. package/dist/src/commands/ui.js +4 -0
  32. package/dist/src/commands/ui.js.map +1 -1
  33. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  34. package/dist/src/init/claudemd-generator.js +2 -1
  35. package/dist/src/init/claudemd-generator.js.map +1 -1
  36. package/dist/src/init/copy-assets.d.ts.map +1 -1
  37. package/dist/src/init/copy-assets.js +56 -18
  38. package/dist/src/init/copy-assets.js.map +1 -1
  39. package/dist/src/init/executor.d.ts.map +1 -1
  40. package/dist/src/init/executor.js +9 -1
  41. package/dist/src/init/executor.js.map +1 -1
  42. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  43. package/dist/src/init/mcp-generator.js +3 -3
  44. package/dist/src/init/mcp-generator.js.map +1 -1
  45. package/dist/src/init/never-commit.d.ts +39 -0
  46. package/dist/src/init/never-commit.d.ts.map +1 -0
  47. package/dist/src/init/never-commit.js +15 -0
  48. package/dist/src/init/never-commit.js.map +1 -0
  49. package/dist/src/init/shared.d.ts +74 -1
  50. package/dist/src/init/shared.d.ts.map +1 -1
  51. package/dist/src/init/shared.js +166 -0
  52. package/dist/src/init/shared.js.map +1 -1
  53. package/dist/src/init/types.d.ts +11 -0
  54. package/dist/src/init/types.d.ts.map +1 -1
  55. package/dist/src/init/types.js.map +1 -1
  56. package/dist/src/init/upgrade.d.ts.map +1 -1
  57. package/dist/src/init/upgrade.js +1 -0
  58. package/dist/src/init/upgrade.js.map +1 -1
  59. package/dist/src/init/write-kimicode.d.ts.map +1 -1
  60. package/dist/src/init/write-kimicode.js +13 -9
  61. package/dist/src/init/write-kimicode.js.map +1 -1
  62. package/dist/src/init/write-opencode.d.ts.map +1 -1
  63. package/dist/src/init/write-opencode.js +44 -2
  64. package/dist/src/init/write-opencode.js.map +1 -1
  65. package/dist/src/init/write-runtime-config.d.ts +2 -0
  66. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  67. package/dist/src/init/write-runtime-config.js +65 -38
  68. package/dist/src/init/write-runtime-config.js.map +1 -1
  69. package/dist/src/mastermind/manifest-data.d.ts.map +1 -1
  70. package/dist/src/mastermind/manifest-data.js +14 -0
  71. package/dist/src/mastermind/manifest-data.js.map +1 -1
  72. package/dist/src/mcp/monoes-mcp-entry.mjs +86 -0
  73. package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
  74. package/dist/src/mcp-tools/terminal-tools.js +52 -9
  75. package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
  76. package/dist/src/memory/memory-bridge.d.ts +84 -3
  77. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  78. package/dist/src/memory/memory-bridge.js +215 -17
  79. package/dist/src/memory/memory-bridge.js.map +1 -1
  80. package/dist/src/orgrt/agent-runner.d.ts +38 -0
  81. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  82. package/dist/src/orgrt/agent-runner.js +47 -0
  83. package/dist/src/orgrt/agent-runner.js.map +1 -1
  84. package/dist/src/orgrt/checkpoint.d.ts +12 -1
  85. package/dist/src/orgrt/checkpoint.d.ts.map +1 -1
  86. package/dist/src/orgrt/checkpoint.js +15 -6
  87. package/dist/src/orgrt/checkpoint.js.map +1 -1
  88. package/dist/src/orgrt/completion-gate.d.ts +74 -0
  89. package/dist/src/orgrt/completion-gate.d.ts.map +1 -1
  90. package/dist/src/orgrt/completion-gate.js +45 -0
  91. package/dist/src/orgrt/completion-gate.js.map +1 -1
  92. package/dist/src/orgrt/cost-tier.d.ts +144 -0
  93. package/dist/src/orgrt/cost-tier.d.ts.map +1 -0
  94. package/dist/src/orgrt/cost-tier.js +183 -0
  95. package/dist/src/orgrt/cost-tier.js.map +1 -0
  96. package/dist/src/orgrt/cross-org.d.ts.map +1 -1
  97. package/dist/src/orgrt/cross-org.js +14 -0
  98. package/dist/src/orgrt/cross-org.js.map +1 -1
  99. package/dist/src/orgrt/daemon.d.ts +12 -1
  100. package/dist/src/orgrt/daemon.d.ts.map +1 -1
  101. package/dist/src/orgrt/daemon.js +216 -49
  102. package/dist/src/orgrt/daemon.js.map +1 -1
  103. package/dist/src/orgrt/decisions.d.ts +14 -2
  104. package/dist/src/orgrt/decisions.d.ts.map +1 -1
  105. package/dist/src/orgrt/decisions.js +216 -10
  106. package/dist/src/orgrt/decisions.js.map +1 -1
  107. package/dist/src/orgrt/file-roots.d.ts +7 -0
  108. package/dist/src/orgrt/file-roots.d.ts.map +1 -1
  109. package/dist/src/orgrt/file-roots.js +38 -1
  110. package/dist/src/orgrt/file-roots.js.map +1 -1
  111. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  112. package/dist/src/orgrt/forwarder.js +16 -2
  113. package/dist/src/orgrt/forwarder.js.map +1 -1
  114. package/dist/src/orgrt/idle-deadline.d.ts +74 -1
  115. package/dist/src/orgrt/idle-deadline.d.ts.map +1 -1
  116. package/dist/src/orgrt/idle-deadline.js +52 -2
  117. package/dist/src/orgrt/idle-deadline.js.map +1 -1
  118. package/dist/src/orgrt/loadouts.d.ts +38 -0
  119. package/dist/src/orgrt/loadouts.d.ts.map +1 -0
  120. package/dist/src/orgrt/loadouts.js +129 -0
  121. package/dist/src/orgrt/loadouts.js.map +1 -0
  122. package/dist/src/orgrt/mailbox.d.ts +17 -1
  123. package/dist/src/orgrt/mailbox.d.ts.map +1 -1
  124. package/dist/src/orgrt/mailbox.js +50 -3
  125. package/dist/src/orgrt/mailbox.js.map +1 -1
  126. package/dist/src/orgrt/policy.d.ts +42 -2
  127. package/dist/src/orgrt/policy.d.ts.map +1 -1
  128. package/dist/src/orgrt/policy.js +53 -8
  129. package/dist/src/orgrt/policy.js.map +1 -1
  130. package/dist/src/orgrt/prompt-vars.d.ts +11 -0
  131. package/dist/src/orgrt/prompt-vars.d.ts.map +1 -0
  132. package/dist/src/orgrt/prompt-vars.js +49 -0
  133. package/dist/src/orgrt/prompt-vars.js.map +1 -0
  134. package/dist/src/orgrt/questions.d.ts +22 -1
  135. package/dist/src/orgrt/questions.d.ts.map +1 -1
  136. package/dist/src/orgrt/questions.js +24 -3
  137. package/dist/src/orgrt/questions.js.map +1 -1
  138. package/dist/src/orgrt/review-packet.d.ts +29 -0
  139. package/dist/src/orgrt/review-packet.d.ts.map +1 -0
  140. package/dist/src/orgrt/review-packet.js +67 -0
  141. package/dist/src/orgrt/review-packet.js.map +1 -0
  142. package/dist/src/orgrt/role-sandbox.d.ts.map +1 -1
  143. package/dist/src/orgrt/role-sandbox.js +16 -3
  144. package/dist/src/orgrt/role-sandbox.js.map +1 -1
  145. package/dist/src/orgrt/session-ledger.d.ts +61 -0
  146. package/dist/src/orgrt/session-ledger.d.ts.map +1 -0
  147. package/dist/src/orgrt/session-ledger.js +113 -0
  148. package/dist/src/orgrt/session-ledger.js.map +1 -0
  149. package/dist/src/orgrt/session.d.ts +45 -5
  150. package/dist/src/orgrt/session.d.ts.map +1 -1
  151. package/dist/src/orgrt/session.js +384 -27
  152. package/dist/src/orgrt/session.js.map +1 -1
  153. package/dist/src/orgrt/task-dag.d.ts +34 -1
  154. package/dist/src/orgrt/task-dag.d.ts.map +1 -1
  155. package/dist/src/orgrt/task-dag.js +58 -7
  156. package/dist/src/orgrt/task-dag.js.map +1 -1
  157. package/dist/src/orgrt/tool-spill.d.ts +94 -0
  158. package/dist/src/orgrt/tool-spill.d.ts.map +1 -0
  159. package/dist/src/orgrt/tool-spill.js +180 -0
  160. package/dist/src/orgrt/tool-spill.js.map +1 -0
  161. package/dist/src/orgrt/types.d.ts +138 -0
  162. package/dist/src/orgrt/types.d.ts.map +1 -1
  163. package/dist/src/orgrt/types.js +148 -0
  164. package/dist/src/orgrt/types.js.map +1 -1
  165. package/dist/src/platform-adapters/renderers/mcp.d.ts +20 -2
  166. package/dist/src/platform-adapters/renderers/mcp.d.ts.map +1 -1
  167. package/dist/src/platform-adapters/renderers/mcp.js +25 -5
  168. package/dist/src/platform-adapters/renderers/mcp.js.map +1 -1
  169. package/dist/src/platform-adapters/renderers/skills.d.ts +0 -6
  170. package/dist/src/platform-adapters/renderers/skills.d.ts.map +1 -1
  171. package/dist/src/platform-adapters/renderers/skills.js +12 -22
  172. package/dist/src/platform-adapters/renderers/skills.js.map +1 -1
  173. package/dist/src/protocol-capabilities.d.ts +2 -1
  174. package/dist/src/protocol-capabilities.d.ts.map +1 -1
  175. package/dist/src/protocol-capabilities.js +2 -1
  176. package/dist/src/protocol-capabilities.js.map +1 -1
  177. package/dist/src/ui/dashboard.html +457 -164
  178. package/dist/src/ui/org-hil.mjs +276 -0
  179. package/dist/src/ui/org-runtime.mjs +350 -0
  180. package/dist/src/ui/orgs.html +22 -4
  181. package/dist/src/ui/routes-monoes.mjs +28 -3
  182. package/dist/src/ui/routes-org.mjs +290 -541
  183. package/dist/src/ui/server.mjs +185 -19
  184. package/dist/tsconfig.tsbuildinfo +1 -1
  185. package/package.json +7 -6
@@ -6,6 +6,10 @@ import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import zlib from 'node:zlib';
9
+ import {
10
+ detectDashboardTokenLeak,
11
+ formatDashboardTokenLeakWarning,
12
+ } from '../mcp/monoes-mcp-entry.mjs';
9
13
  import {
10
14
  collectAgents,
11
15
  collectAll,
@@ -574,17 +578,34 @@ function _getGitMonomindDir(workDir) {
574
578
  }
575
579
 
576
580
  // Returns the monomind home directory for server-level data (capture, control.json, loops).
577
- // Priority: MONOMIND_HOME env var > walk up from cwd finding .monomind/control.json > cwd fallback
578
- function getMonomindHome() {
581
+ // Priority: an EXPLICIT project dir (--project-dir / CLAUDE_PROJECT_DIR) > MONOMIND_HOME env
582
+ // var > walk up from cwd finding .monomind/control.json, never past the enclosing git root >
583
+ // cwd fallback.
584
+ //
585
+ // #308: the walk used to run unconditionally, ignoring --project-dir, so it could resolve to
586
+ // any ancestor that happened to hold a control.json from an earlier dashboard run. Per-project
587
+ // state written under this home — the monoes.me connection file with its OAuth tokens,
588
+ // capture/, orgs/ — then landed in that ancestor instead of the project the dashboard is for.
589
+ //
590
+ // `projectDirExplicit` is what keeps the walk alive: both entry points always hand startServer
591
+ // a projectDir (`monomind ui` defaults the flag to cwd, the run-directly block below defaults
592
+ // CLAUDE_PROJECT_DIR to cwd), so short-circuiting on the dir alone would silently move the home
593
+ // of every dashboard started from a subdirectory of its project down into that subdirectory.
594
+ // Only a dir the caller actually named counts.
595
+ export function getMonomindHome(projectDir, projectDirExplicit) {
596
+ if (projectDirExplicit && projectDir) return path.resolve(projectDir);
579
597
  if (process.env.MONOMIND_HOME) return path.resolve(process.env.MONOMIND_HOME);
580
598
  let dir = process.cwd();
581
599
  while (dir !== path.dirname(dir)) {
582
600
  if (fs.existsSync(path.join(dir, '.monomind', 'control.json'))) return dir;
601
+ if (fs.existsSync(path.join(dir, '.git'))) break;
583
602
  dir = path.dirname(dir);
584
603
  }
585
604
  return process.cwd();
586
605
  }
587
- const MONOMIND_HOME = getMonomindHome();
606
+ // Re-resolved from the options at startServer() time (the projectDir only exists then); the
607
+ // call sites below all read it from inside startServer, after that assignment.
608
+ let MONOMIND_HOME = getMonomindHome();
588
609
 
589
610
  // Resolve an org's project directory by searching across known projects.
590
611
  // Returns the first project dir where {dir}/.monomind/orgs/{orgName}.json exists, or null.
@@ -878,6 +899,9 @@ function bindServer(server, port) {
878
899
  * @param {object} [options]
879
900
  * @param {number} [options.port=4242] - Preferred port. Tries up to port+10 on collision.
880
901
  * @param {string} [options.projectDir] - Root of the project to collect data from.
902
+ * @param {boolean} [options.projectDirExplicit=false] - True when projectDir was named by the
903
+ * caller (--project-dir / CLAUDE_PROJECT_DIR) rather than defaulted to cwd; only then does it
904
+ * set the monomind home (see getMonomindHome, #308).
881
905
  * @param {boolean} [options.openBrowser=true] - Whether to open the dashboard in the default browser.
882
906
  * @returns {Promise<{port: number, url: string, server: http.Server}>}
883
907
  */
@@ -973,9 +997,24 @@ function _resolveSlugToPathUncached(slug, projDir) {
973
997
  export async function startServer({
974
998
  port = 4242,
975
999
  projectDir,
1000
+ projectDirExplicit = false,
976
1001
  openBrowser = true,
977
1002
  allowedHosts,
978
1003
  } = {}) {
1004
+ // #308: resolve the home now that the caller's project dir is in hand.
1005
+ MONOMIND_HOME = getMonomindHome(projectDir, projectDirExplicit);
1006
+ // i-052 commit 3: warn on every start, not only at `init` (executor.ts).
1007
+ // The dashboard is what WRITES dashboard-token — a user who never
1008
+ // re-runs `init` after the file got committed (e.g. before commits 1-2
1009
+ // shipped `.gitignore` coverage) would otherwise never see this warning
1010
+ // at all. Same "name the file, never the value" contract as the
1011
+ // monoes.me leak warning; best-effort (never blocks startup on a git
1012
+ // failure — detectDashboardTokenLeak already treats git-unavailable as
1013
+ // "not a leak signal", not an error).
1014
+ const dashboardTokenStartupWarning = formatDashboardTokenLeakWarning(
1015
+ await detectDashboardTokenLeak(projectDir || process.cwd()),
1016
+ );
1017
+ if (dashboardTokenStartupWarning) console.error(dashboardTokenStartupWarning);
979
1018
  // Extra Host names accepted beyond loopback (see isAllowedHost above).
980
1019
  const _allowedHosts = resolveAllowedHosts(allowedHosts);
981
1020
  // ── Security: per-process auth credential for mutating (non-GET) requests ─
@@ -991,6 +1030,17 @@ export async function startServer({
991
1030
  // silently degrading per-prompt Second Brain injection to keyword fallback.
992
1031
  // So the write is deferred to bind time and gated: primary instances write
993
1032
  // `dashboard-token`; secondaries write `dashboard-token-<port>` instead.
1033
+ // i-052 commit 4: the path THIS process actually wrote (primary
1034
+ // `dashboard-token` xor its own `dashboard-token-<port>` secondary) — set
1035
+ // once, at the bottom of writeDashboardToken(), and read by shutdown()
1036
+ // below for best-effort cleanup on a clean exit. `mode` is create-only
1037
+ // on `fs.writeFileSync` (POSIX `open(2)` ignores it when the file
1038
+ // exists) — measured on this machine: 2 of 11 token files were 644
1039
+ // despite every write site passing `{ mode: 0o600 }`, because a prior
1040
+ // run had already created them. `fs.chmodSync` after every write, here
1041
+ // and in propagateDashboardToken() below, enforces the mode on rewrite
1042
+ // too, not just on first creation.
1043
+ let _dashboardTokenFilePath = null;
994
1044
  async function writeDashboardToken(actualPort) {
995
1045
  try {
996
1046
  actualPort = Number(actualPort);
@@ -1032,14 +1082,17 @@ export async function startServer({
1032
1082
  } catch (_) {
1033
1083
  /* no readable control.json — treat as primary */
1034
1084
  }
1035
- fs.writeFileSync(
1036
- path.join(authFileDir, primary ? 'dashboard-token' : `dashboard-token-${actualPort}`),
1037
- dashboardAuthValue,
1038
- { mode: 0o600 },
1085
+ const tokenPath = path.join(
1086
+ authFileDir,
1087
+ primary ? 'dashboard-token' : `dashboard-token-${actualPort}`,
1039
1088
  );
1089
+ fs.writeFileSync(tokenPath, dashboardAuthValue, { mode: 0o600 });
1090
+ fs.chmodSync(tokenPath, 0o600); // enforce on rewrite — {mode} above is create-only
1091
+ _dashboardTokenFilePath = tokenPath;
1040
1092
  // Sweep stale secondary tokens (dead scratch/test instances) so they
1041
1093
  // don't accumulate as orphaned credential files.
1042
- // monolean: age-based sweep only — no on-exit unlink plumbing.
1094
+ // Age-based sweep — the belt-and-braces backstop for a dirty death
1095
+ // (kill -9) that never reaches shutdown()'s cleanup below.
1043
1096
  try {
1044
1097
  const weekMs = 7 * 24 * 3600 * 1000;
1045
1098
  for (const f of fs.readdirSync(authFileDir)) {
@@ -1050,9 +1103,45 @@ export async function startServer({
1050
1103
  } catch (_) {}
1051
1104
  } catch (_) {}
1052
1105
  }
1106
+ // i-052 commit 4: this is the code path that actually planted
1107
+ // dashboard-token in the incident's other repos — a paired project may
1108
+ // never have run `init` (so it never got commits 1-2's `.gitignore`
1109
+ // coverage) yet still receives a live token here on every restart.
1110
+ // Ensures the SAME coverage `init` would have given it before writing
1111
+ // the token, content-guarded exactly like write-runtime-config.ts's
1112
+ // append (creates the file if absent; appends the one missing line if
1113
+ // present; no-ops if already covered) — duplicated locally rather than
1114
+ // imported because this file ships as plain ESM with no build step and
1115
+ // cannot import compiled TypeScript (see monoes-mcp-entry.mjs's own doc
1116
+ // comment for the identical constraint). Best-effort: a coverage-ensure
1117
+ // failure must never block token delivery, which is the more urgent
1118
+ // property (a stale token 401s every cross-project caller).
1119
+ function ensureDashboardTokenGitignoreCoverage(kpMonoDir) {
1120
+ try {
1121
+ const gitignorePath = path.join(kpMonoDir, '.gitignore');
1122
+ const reason =
1123
+ 'live monomind dashboard credential (i-052) — grants cross-project file reads and agent execution; rewritten on every dashboard restart, so an already-tracked path re-commits a fresh live token on every restart';
1124
+ if (!fs.existsSync(gitignorePath)) {
1125
+ fs.mkdirSync(kpMonoDir, { recursive: true });
1126
+ fs.writeFileSync(gitignorePath, `# ${reason}\ndashboard-token\n`);
1127
+ return;
1128
+ }
1129
+ const existing = fs.readFileSync(gitignorePath, 'utf8');
1130
+ const existingLines = new Set(existing.split('\n').map((l) => l.trim()));
1131
+ if (!existingLines.has('dashboard-token')) {
1132
+ fs.writeFileSync(gitignorePath, `${existing.trimEnd()}\n# ${reason}\ndashboard-token\n`);
1133
+ }
1134
+ } catch (_) {
1135
+ /* best effort — see doc comment above */
1136
+ }
1137
+ }
1138
+
1053
1139
  // Propagate the fresh token to every known project whose control.json points
1054
1140
  // at this server — otherwise each restart silently orphans cross-project
1055
- // callers (their curls/CLI reads a stale token and 401s forever).
1141
+ // callers (their curls/CLI reads a stale token and 401s forever). The
1142
+ // population is `data/known-projects.json`'s own content, never a
1143
+ // directory glob (i-052 owner correction: the incident's affected-repo
1144
+ // count included projects outside any assumed directory layout).
1056
1145
  // Called after bind so the match uses the ACTUAL bound port.
1057
1146
  function propagateDashboardToken(actualPort) {
1058
1147
  try {
@@ -1064,9 +1153,10 @@ export async function startServer({
1064
1153
  const _kpCtl = JSON.parse(fs.readFileSync(path.join(_kpMono, 'control.json'), 'utf8'));
1065
1154
  // Only pair projects that direct their traffic to this server's port.
1066
1155
  if (_kpCtl && String(_kpCtl.url || '').includes(`:${actualPort}`)) {
1067
- fs.writeFileSync(path.join(_kpMono, 'dashboard-token'), dashboardAuthValue, {
1068
- mode: 0o600,
1069
- });
1156
+ ensureDashboardTokenGitignoreCoverage(_kpMono);
1157
+ const _kpTokenPath = path.join(_kpMono, 'dashboard-token');
1158
+ fs.writeFileSync(_kpTokenPath, dashboardAuthValue, { mode: 0o600 });
1159
+ fs.chmodSync(_kpTokenPath, 0o600); // enforce on rewrite — {mode} above is create-only
1070
1160
  }
1071
1161
  } catch (_) {
1072
1162
  /* project missing/unreadable — skip */
@@ -1342,10 +1432,26 @@ export async function startServer({
1342
1432
  } catch (_e) {}
1343
1433
  if (!_st.agents) _st.agents = {};
1344
1434
  const _ex = _st.agents[_arole] || {};
1345
- const _tokensIn = event.type === 'agent:usage' ? Number(event.tokens_in) || 0 : 0;
1346
- const _tokensOut = event.type === 'agent:usage' ? Number(event.tokens_out) || 0 : 0;
1347
- // 'org:usage' carries a single total (data.tokens), not an in/out split —
1348
- // counted toward tokens_used so the budget total still reflects it honestly.
1435
+ // ADR-O001 D1: orgrt's 'org:usage' now carries the four billable
1436
+ // quantities separately (tokens_in / tokens_out / cache_read /
1437
+ // cache_creation) alongside the `tokens` total, so the per-role rows
1438
+ // record real values instead of the 0s they used to persist.
1439
+ const _tokensIn =
1440
+ event.type === 'agent:usage'
1441
+ ? Number(event.tokens_in) || 0
1442
+ : Number(event.data?.tokens_in) || 0;
1443
+ const _tokensOut =
1444
+ event.type === 'agent:usage'
1445
+ ? Number(event.tokens_out) || 0
1446
+ : Number(event.data?.tokens_out) || 0;
1447
+ const _cacheRead =
1448
+ event.type === 'org:usage' ? Number(event.data?.cache_read) || 0 : 0;
1449
+ const _cacheCreation =
1450
+ event.type === 'org:usage' ? Number(event.data?.cache_creation) || 0 : 0;
1451
+ // 'org:usage' carries the billable total in data.tokens (cache
1452
+ // reads and writes included) — counted toward tokens_used, and NOT
1453
+ // re-added from the split fields, which are that same total broken
1454
+ // down rather than an extra amount.
1349
1455
  const _tokensTotal =
1350
1456
  event.type === 'org:usage' ? Number(event.data?.tokens) || 0 : 0;
1351
1457
  const _costUsd =
@@ -1356,7 +1462,11 @@ export async function startServer({
1356
1462
  ..._ex,
1357
1463
  tokens_in: (_ex.tokens_in || 0) + _tokensIn,
1358
1464
  tokens_out: (_ex.tokens_out || 0) + _tokensOut,
1359
- tokens_used: (_ex.tokens_used || 0) + _tokensIn + _tokensOut + _tokensTotal,
1465
+ cache_read_tokens: (_ex.cache_read_tokens || 0) + _cacheRead,
1466
+ cache_creation_tokens: (_ex.cache_creation_tokens || 0) + _cacheCreation,
1467
+ tokens_used:
1468
+ (_ex.tokens_used || 0) +
1469
+ (event.type === 'org:usage' ? _tokensTotal : _tokensIn + _tokensOut),
1360
1470
  total_cost_usd: (_ex.total_cost_usd || 0) + _costUsd,
1361
1471
  lastUpdated: event.ts,
1362
1472
  };
@@ -1771,7 +1881,16 @@ export async function startServer({
1771
1881
 
1772
1882
  const _checkAuth = (req) => {
1773
1883
  let _suppliedAuth = req.headers['x-monomind-token'] || '';
1774
- if (!_suppliedAuth) {
1884
+ // i-073: the query-token fallback is for EventSource (which cannot send
1885
+ // headers) and other subresource GETs ONLY. It used to apply
1886
+ // unconditionally, which meant a mutation needed only the header OR the
1887
+ // query param — and the header is what forces a CORS preflight (a
1888
+ // mutation becomes a "simple request" without it). A plain
1889
+ // `<form method=POST action="...?token=LEAKED">` has no header to omit,
1890
+ // so accepting the query param on POST turned every mutation into a
1891
+ // CSRF target reachable with no preflight and no JS. Restoring the
1892
+ // unconditional fallback here reopens that hole — see i-073.
1893
+ if (!_suppliedAuth && (req.method === 'GET' || req.method === 'HEAD')) {
1775
1894
  try {
1776
1895
  _suppliedAuth = new URL(req.url, 'http://localhost').searchParams.get('token') || '';
1777
1896
  } catch (_) {}
@@ -1855,6 +1974,32 @@ export async function startServer({
1855
1974
  _sendUnauthorized(res, corsOrigin);
1856
1975
  return;
1857
1976
  }
1977
+ // ── Security: Sec-Fetch-Site CSRF guard on mutations (i-073) ──────────
1978
+ // The token check alone cannot stop CSRF if the token ever leaks (a
1979
+ // browser cache, a proxy log, `ps` on a shared host). Reject when the
1980
+ // header is PRESENT and not `same-origin` — deliberately NOT "reject
1981
+ // cross-site": `Sec-Fetch-Site: same-site` covers a *different port on
1982
+ // the same host*, i.e. a page served by another local server (another
1983
+ // monomind dashboard among them — o-04 shows there can be eleven).
1984
+ // Rejecting anything that isn't `same-origin` closes that case too, at
1985
+ // no cost to the dashboard's own pages.
1986
+ // Header ABSENT must stay allowed, or every first-party non-browser
1987
+ // caller breaks: control-start.cjs, event-logger.cjs,
1988
+ // capture-handler.cjs, route-handler.cjs, forwarder.ts and the
1989
+ // `/mastermind:*` commands all POST with the token header and send no
1990
+ // Sec-Fetch-* at all. A browser too old to send Sec-Fetch-Site is
1991
+ // treated the same as a non-browser client — it still needs the token.
1992
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1993
+ const _secFetchSite = req.headers['sec-fetch-site'];
1994
+ if (_secFetchSite && _secFetchSite !== 'same-origin') {
1995
+ res.writeHead(403, {
1996
+ 'Content-Type': 'application/json',
1997
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1998
+ });
1999
+ res.end(JSON.stringify({ error: 'Forbidden: cross-origin mutation' }));
2000
+ return;
2001
+ }
2002
+ }
1858
2003
  }
1859
2004
 
1860
2005
  // ------------------------------------------------------------------ GET /
@@ -5911,6 +6056,21 @@ export async function startServer({
5911
6056
  // Close all SSE connections
5912
6057
  closeSseClients();
5913
6058
 
6059
+ // i-052 commit 4: best-effort cleanup of the credential THIS process
6060
+ // wrote (primary `dashboard-token`, or this process's own
6061
+ // `dashboard-token-<port>` secondary) — belt-and-braces only.
6062
+ // writeDashboardToken's age-based sweep is the actual backstop for a
6063
+ // dirty death: SIGKILL never reaches this handler at all, and a
6064
+ // failed unlink here (permissions, already-gone) must not block the
6065
+ // rest of shutdown.
6066
+ if (_dashboardTokenFilePath) {
6067
+ try {
6068
+ fs.unlinkSync(_dashboardTokenFilePath);
6069
+ } catch (_) {
6070
+ /* already gone, or unremovable — the age sweep is the backstop */
6071
+ }
6072
+ }
6073
+
5914
6074
  // Drain in-flight JSONL appends before closing (prevents truncated writes on fast SIGTERM)
5915
6075
  Promise.all([..._writeQueue.values()])
5916
6076
  .catch(() => {})
@@ -5955,8 +6115,14 @@ export function getServerStatus() {
5955
6115
  const _isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
5956
6116
  if (_isMain) {
5957
6117
  const _port = parseInt(process.argv[2] || process.env.CONTROL_PORT || '4242', 10);
6118
+ // CLAUDE_PROJECT_DIR names a project; the bare cwd fallback does not (#308).
5958
6119
  const _dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
5959
- startServer({ port: _port, openBrowser: false, projectDir: _dir }).catch((err) => {
6120
+ startServer({
6121
+ port: _port,
6122
+ openBrowser: false,
6123
+ projectDir: _dir,
6124
+ projectDirExplicit: Boolean(process.env.CLAUDE_PROJECT_DIR),
6125
+ }).catch((err) => {
5960
6126
  process.stderr.write(`[server] failed to start: ${err.message}\n`);
5961
6127
  process.exit(1);
5962
6128
  });