@monoes/monomindcli 2.11.12 → 2.13.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 (151) hide show
  1. package/.claude/skills/mastermind/SKILL.md +1 -15
  2. package/README.md +2 -2
  3. package/dist/src/commands/doctor-env-checks.d.ts +1 -1
  4. package/dist/src/commands/doctor-env-checks.d.ts.map +1 -1
  5. package/dist/src/commands/doctor-env-checks.js +20 -10
  6. package/dist/src/commands/doctor-env-checks.js.map +1 -1
  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 +19 -0
  12. package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
  13. package/dist/src/commands/doctor-project-checks.js +54 -0
  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 +11 -2
  17. package/dist/src/commands/doctor.js.map +1 -1
  18. package/dist/src/commands/init-upgrade.d.ts.map +1 -1
  19. package/dist/src/commands/init-upgrade.js +1 -1
  20. package/dist/src/commands/init-upgrade.js.map +1 -1
  21. package/dist/src/commands/init.d.ts.map +1 -1
  22. package/dist/src/commands/init.js +12 -0
  23. package/dist/src/commands/init.js.map +1 -1
  24. package/dist/src/commands/org.d.ts.map +1 -1
  25. package/dist/src/commands/org.js +9 -2
  26. package/dist/src/commands/org.js.map +1 -1
  27. package/dist/src/commands/ui.d.ts.map +1 -1
  28. package/dist/src/commands/ui.js +4 -0
  29. package/dist/src/commands/ui.js.map +1 -1
  30. package/dist/src/init/claudemd-generator.d.ts +4 -0
  31. package/dist/src/init/claudemd-generator.d.ts.map +1 -1
  32. package/dist/src/init/claudemd-generator.js +81 -77
  33. package/dist/src/init/claudemd-generator.js.map +1 -1
  34. package/dist/src/init/copy-assets.d.ts.map +1 -1
  35. package/dist/src/init/copy-assets.js +56 -18
  36. package/dist/src/init/copy-assets.js.map +1 -1
  37. package/dist/src/init/executor.d.ts.map +1 -1
  38. package/dist/src/init/executor.js +9 -1
  39. package/dist/src/init/executor.js.map +1 -1
  40. package/dist/src/init/generated-counts.d.ts +8 -0
  41. package/dist/src/init/generated-counts.d.ts.map +1 -0
  42. package/dist/src/init/generated-counts.js +43 -0
  43. package/dist/src/init/generated-counts.js.map +1 -0
  44. package/dist/src/init/mcp-generator.d.ts.map +1 -1
  45. package/dist/src/init/mcp-generator.js +7 -5
  46. package/dist/src/init/mcp-generator.js.map +1 -1
  47. package/dist/src/init/never-commit.d.ts +39 -0
  48. package/dist/src/init/never-commit.d.ts.map +1 -0
  49. package/dist/src/init/never-commit.js +15 -0
  50. package/dist/src/init/never-commit.js.map +1 -0
  51. package/dist/src/init/shared.d.ts +108 -1
  52. package/dist/src/init/shared.d.ts.map +1 -1
  53. package/dist/src/init/shared.js +203 -2
  54. package/dist/src/init/shared.js.map +1 -1
  55. package/dist/src/init/types.d.ts +5 -0
  56. package/dist/src/init/types.d.ts.map +1 -1
  57. package/dist/src/init/upgrade.d.ts.map +1 -1
  58. package/dist/src/init/upgrade.js +62 -10
  59. package/dist/src/init/upgrade.js.map +1 -1
  60. package/dist/src/init/write-capabilities.d.ts.map +1 -1
  61. package/dist/src/init/write-capabilities.js +95 -61
  62. package/dist/src/init/write-capabilities.js.map +1 -1
  63. package/dist/src/init/write-kimicode.d.ts.map +1 -1
  64. package/dist/src/init/write-kimicode.js +13 -9
  65. package/dist/src/init/write-kimicode.js.map +1 -1
  66. package/dist/src/init/write-opencode.d.ts.map +1 -1
  67. package/dist/src/init/write-opencode.js +44 -2
  68. package/dist/src/init/write-opencode.js.map +1 -1
  69. package/dist/src/init/write-runtime-config.d.ts +2 -0
  70. package/dist/src/init/write-runtime-config.d.ts.map +1 -1
  71. package/dist/src/init/write-runtime-config.js +55 -38
  72. package/dist/src/init/write-runtime-config.js.map +1 -1
  73. package/dist/src/mastermind/manifest-data.d.ts.map +1 -1
  74. package/dist/src/mastermind/manifest-data.js +14 -0
  75. package/dist/src/mastermind/manifest-data.js.map +1 -1
  76. package/dist/src/mcp/monoes-mcp-entry.mjs +86 -0
  77. package/dist/src/mcp-tools/guidance-tools.js +1 -1
  78. package/dist/src/mcp-tools/guidance-tools.js.map +1 -1
  79. package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
  80. package/dist/src/mcp-tools/terminal-tools.js +52 -9
  81. package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
  82. package/dist/src/memory/memory-bridge.d.ts +84 -3
  83. package/dist/src/memory/memory-bridge.d.ts.map +1 -1
  84. package/dist/src/memory/memory-bridge.js +215 -17
  85. package/dist/src/memory/memory-bridge.js.map +1 -1
  86. package/dist/src/orgrt/agent-exec.d.ts.map +1 -1
  87. package/dist/src/orgrt/agent-exec.js +17 -0
  88. package/dist/src/orgrt/agent-exec.js.map +1 -1
  89. package/dist/src/orgrt/agent-runner.d.ts +18 -0
  90. package/dist/src/orgrt/agent-runner.d.ts.map +1 -1
  91. package/dist/src/orgrt/agent-runner.js +20 -1
  92. package/dist/src/orgrt/agent-runner.js.map +1 -1
  93. package/dist/src/orgrt/antigravity-runner.d.ts.map +1 -1
  94. package/dist/src/orgrt/antigravity-runner.js +4 -1
  95. package/dist/src/orgrt/antigravity-runner.js.map +1 -1
  96. package/dist/src/orgrt/codex-runner.d.ts.map +1 -1
  97. package/dist/src/orgrt/codex-runner.js +4 -1
  98. package/dist/src/orgrt/codex-runner.js.map +1 -1
  99. package/dist/src/orgrt/copilot-runner.d.ts.map +1 -1
  100. package/dist/src/orgrt/copilot-runner.js +4 -1
  101. package/dist/src/orgrt/copilot-runner.js.map +1 -1
  102. package/dist/src/orgrt/crush-runner.d.ts.map +1 -1
  103. package/dist/src/orgrt/crush-runner.js +4 -1
  104. package/dist/src/orgrt/crush-runner.js.map +1 -1
  105. package/dist/src/orgrt/forwarder.d.ts.map +1 -1
  106. package/dist/src/orgrt/forwarder.js +5 -0
  107. package/dist/src/orgrt/forwarder.js.map +1 -1
  108. package/dist/src/orgrt/grok-runner.d.ts.map +1 -1
  109. package/dist/src/orgrt/grok-runner.js +4 -1
  110. package/dist/src/orgrt/grok-runner.js.map +1 -1
  111. package/dist/src/orgrt/hermes-runner.d.ts.map +1 -1
  112. package/dist/src/orgrt/hermes-runner.js +4 -1
  113. package/dist/src/orgrt/hermes-runner.js.map +1 -1
  114. package/dist/src/orgrt/kimicode-runner.d.ts.map +1 -1
  115. package/dist/src/orgrt/kimicode-runner.js +4 -1
  116. package/dist/src/orgrt/kimicode-runner.js.map +1 -1
  117. package/dist/src/orgrt/opencode-runner.d.ts.map +1 -1
  118. package/dist/src/orgrt/opencode-runner.js +5 -1
  119. package/dist/src/orgrt/opencode-runner.js.map +1 -1
  120. package/dist/src/orgrt/pi-rpc-runner.d.ts.map +1 -1
  121. package/dist/src/orgrt/pi-rpc-runner.js +9 -1
  122. package/dist/src/orgrt/pi-rpc-runner.js.map +1 -1
  123. package/dist/src/orgrt/pi-runner.d.ts.map +1 -1
  124. package/dist/src/orgrt/pi-runner.js +9 -1
  125. package/dist/src/orgrt/pi-runner.js.map +1 -1
  126. package/dist/src/orgrt/provider.d.ts +14 -0
  127. package/dist/src/orgrt/provider.d.ts.map +1 -1
  128. package/dist/src/orgrt/provider.js +54 -0
  129. package/dist/src/orgrt/provider.js.map +1 -1
  130. package/dist/src/orgrt/qwen-rpc-runner.d.ts.map +1 -1
  131. package/dist/src/orgrt/qwen-rpc-runner.js +4 -1
  132. package/dist/src/orgrt/qwen-rpc-runner.js.map +1 -1
  133. package/dist/src/orgrt/qwen-runner.d.ts.map +1 -1
  134. package/dist/src/orgrt/qwen-runner.js +4 -1
  135. package/dist/src/orgrt/qwen-runner.js.map +1 -1
  136. package/dist/src/orgrt/runner-registry.d.ts.map +1 -1
  137. package/dist/src/orgrt/runner-registry.js +7 -1
  138. package/dist/src/orgrt/runner-registry.js.map +1 -1
  139. package/dist/src/orgrt/tool-providers.d.ts.map +1 -1
  140. package/dist/src/orgrt/tool-providers.js +9 -4
  141. package/dist/src/orgrt/tool-providers.js.map +1 -1
  142. package/dist/src/platform-adapters/renderers/skills.d.ts +0 -6
  143. package/dist/src/platform-adapters/renderers/skills.d.ts.map +1 -1
  144. package/dist/src/platform-adapters/renderers/skills.js +12 -22
  145. package/dist/src/platform-adapters/renderers/skills.js.map +1 -1
  146. package/dist/src/ui/dashboard.html +7 -0
  147. package/dist/src/ui/orgs.html +7 -0
  148. package/dist/src/ui/routes-monoes.mjs +28 -3
  149. package/dist/src/ui/server.mjs +160 -14
  150. package/dist/tsconfig.tsbuildinfo +1 -1
  151. package/package.json +8 -8
@@ -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 */
@@ -1771,7 +1861,16 @@ export async function startServer({
1771
1861
 
1772
1862
  const _checkAuth = (req) => {
1773
1863
  let _suppliedAuth = req.headers['x-monomind-token'] || '';
1774
- if (!_suppliedAuth) {
1864
+ // i-073: the query-token fallback is for EventSource (which cannot send
1865
+ // headers) and other subresource GETs ONLY. It used to apply
1866
+ // unconditionally, which meant a mutation needed only the header OR the
1867
+ // query param — and the header is what forces a CORS preflight (a
1868
+ // mutation becomes a "simple request" without it). A plain
1869
+ // `<form method=POST action="...?token=LEAKED">` has no header to omit,
1870
+ // so accepting the query param on POST turned every mutation into a
1871
+ // CSRF target reachable with no preflight and no JS. Restoring the
1872
+ // unconditional fallback here reopens that hole — see i-073.
1873
+ if (!_suppliedAuth && (req.method === 'GET' || req.method === 'HEAD')) {
1775
1874
  try {
1776
1875
  _suppliedAuth = new URL(req.url, 'http://localhost').searchParams.get('token') || '';
1777
1876
  } catch (_) {}
@@ -1855,6 +1954,32 @@ export async function startServer({
1855
1954
  _sendUnauthorized(res, corsOrigin);
1856
1955
  return;
1857
1956
  }
1957
+ // ── Security: Sec-Fetch-Site CSRF guard on mutations (i-073) ──────────
1958
+ // The token check alone cannot stop CSRF if the token ever leaks (a
1959
+ // browser cache, a proxy log, `ps` on a shared host). Reject when the
1960
+ // header is PRESENT and not `same-origin` — deliberately NOT "reject
1961
+ // cross-site": `Sec-Fetch-Site: same-site` covers a *different port on
1962
+ // the same host*, i.e. a page served by another local server (another
1963
+ // monomind dashboard among them — o-04 shows there can be eleven).
1964
+ // Rejecting anything that isn't `same-origin` closes that case too, at
1965
+ // no cost to the dashboard's own pages.
1966
+ // Header ABSENT must stay allowed, or every first-party non-browser
1967
+ // caller breaks: control-start.cjs, event-logger.cjs,
1968
+ // capture-handler.cjs, route-handler.cjs, forwarder.ts and the
1969
+ // `/mastermind:*` commands all POST with the token header and send no
1970
+ // Sec-Fetch-* at all. A browser too old to send Sec-Fetch-Site is
1971
+ // treated the same as a non-browser client — it still needs the token.
1972
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
1973
+ const _secFetchSite = req.headers['sec-fetch-site'];
1974
+ if (_secFetchSite && _secFetchSite !== 'same-origin') {
1975
+ res.writeHead(403, {
1976
+ 'Content-Type': 'application/json',
1977
+ ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}),
1978
+ });
1979
+ res.end(JSON.stringify({ error: 'Forbidden: cross-origin mutation' }));
1980
+ return;
1981
+ }
1982
+ }
1858
1983
  }
1859
1984
 
1860
1985
  // ------------------------------------------------------------------ GET /
@@ -5911,6 +6036,21 @@ export async function startServer({
5911
6036
  // Close all SSE connections
5912
6037
  closeSseClients();
5913
6038
 
6039
+ // i-052 commit 4: best-effort cleanup of the credential THIS process
6040
+ // wrote (primary `dashboard-token`, or this process's own
6041
+ // `dashboard-token-<port>` secondary) — belt-and-braces only.
6042
+ // writeDashboardToken's age-based sweep is the actual backstop for a
6043
+ // dirty death: SIGKILL never reaches this handler at all, and a
6044
+ // failed unlink here (permissions, already-gone) must not block the
6045
+ // rest of shutdown.
6046
+ if (_dashboardTokenFilePath) {
6047
+ try {
6048
+ fs.unlinkSync(_dashboardTokenFilePath);
6049
+ } catch (_) {
6050
+ /* already gone, or unremovable — the age sweep is the backstop */
6051
+ }
6052
+ }
6053
+
5914
6054
  // Drain in-flight JSONL appends before closing (prevents truncated writes on fast SIGTERM)
5915
6055
  Promise.all([..._writeQueue.values()])
5916
6056
  .catch(() => {})
@@ -5955,8 +6095,14 @@ export function getServerStatus() {
5955
6095
  const _isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
5956
6096
  if (_isMain) {
5957
6097
  const _port = parseInt(process.argv[2] || process.env.CONTROL_PORT || '4242', 10);
6098
+ // CLAUDE_PROJECT_DIR names a project; the bare cwd fallback does not (#308).
5958
6099
  const _dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
5959
- startServer({ port: _port, openBrowser: false, projectDir: _dir }).catch((err) => {
6100
+ startServer({
6101
+ port: _port,
6102
+ openBrowser: false,
6103
+ projectDir: _dir,
6104
+ projectDirExplicit: Boolean(process.env.CLAUDE_PROJECT_DIR),
6105
+ }).catch((err) => {
5960
6106
  process.stderr.write(`[server] failed to start: ${err.message}\n`);
5961
6107
  process.exit(1);
5962
6108
  });