crewswarm 0.9.1 → 0.9.3

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 (210) hide show
  1. package/README.md +22 -9
  2. package/apps/dashboard/dist/assets/{chat-core-Cx4sTxDd.js → chat-core-3KirthZA.js} +1 -1
  3. package/apps/dashboard/dist/assets/index-GSWxxEPO.js +2 -0
  4. package/apps/dashboard/dist/assets/{tab-pm-loop-tab-Bfd449B4.js → tab-pm-loop-tab-DiAPTJXu.js} +1 -1
  5. package/apps/dashboard/dist/assets/{tab-projects-tab-DhNWnlzt.js → tab-projects-tab-SFH4E--a.js} +1 -1
  6. package/apps/dashboard/dist/assets/tab-settings-tab-BselH1c0.js +1 -0
  7. package/apps/dashboard/dist/index.html +82 -11
  8. package/apps/vibe/README.md +2 -2
  9. package/apps/vibe/package.json +1 -1
  10. package/apps/vibe/server.mjs +3 -3
  11. package/crew-lead.mjs +48 -5
  12. package/lib/bridges/gateway-ws.mjs +4 -0
  13. package/lib/bridges/tmux-bridge.mjs +200 -0
  14. package/lib/cli-process-tracker.mjs +2 -1
  15. package/lib/crew-lead/chat-handler.mjs +34 -0
  16. package/lib/crew-lead/http-server.mjs +340 -14
  17. package/lib/crew-lead/llm-caller.mjs +24 -8
  18. package/lib/crew-lead/prompts.mjs +7 -0
  19. package/lib/crew-lead/wave-dispatcher.mjs +53 -3
  20. package/lib/crew-lead/ws-router.mjs +219 -27
  21. package/lib/engines/engine-registry.mjs +9 -0
  22. package/lib/engines/rt-envelope.mjs +1 -0
  23. package/lib/engines/runners.mjs +26 -2
  24. package/lib/runtime/config.mjs +7 -0
  25. package/lib/runtime/paths.mjs +12 -8
  26. package/lib/sessions/session-manager.mjs +287 -0
  27. package/package.json +35 -15
  28. package/scripts/capture-build-flow.mjs +118 -0
  29. package/scripts/coverage-report.mjs +209 -0
  30. package/scripts/coverage-summary.mjs +47 -0
  31. package/scripts/dashboard-validation.mjs +74 -0
  32. package/scripts/dashboard.mjs +560 -70
  33. package/scripts/live-bridge-matrix.mjs +79 -0
  34. package/scripts/live-cli-matrix.mjs +166 -0
  35. package/scripts/live-crewchat-check.mjs +42 -0
  36. package/scripts/live-engine-matrix.mjs +50 -0
  37. package/scripts/live-provider-failover-matrix.mjs +107 -0
  38. package/scripts/live-provider-matrix.mjs +228 -0
  39. package/scripts/restart-all-from-repo.sh +4 -4
  40. package/scripts/smoke-dispatch.mjs +4 -1
  41. package/scripts/test-blast-radius.mjs +204 -0
  42. package/scripts/test-report-summary.mjs +88 -0
  43. package/scripts/test-reporter.mjs +651 -0
  44. package/scripts/test-rerun.mjs +136 -0
  45. package/scripts/tmux-bridge +130 -0
  46. package/apps/dashboard/dist/assets/chat-core-Cx4sTxDd.js.br +0 -0
  47. package/apps/dashboard/dist/assets/cli-process-COMRNPqr.js.br +0 -0
  48. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  49. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  50. package/apps/dashboard/dist/assets/index-CF0aJRtC.css.br +0 -0
  51. package/apps/dashboard/dist/assets/index-DnClJ1ee.js +0 -2
  52. package/apps/dashboard/dist/assets/index-DnClJ1ee.js.br +0 -0
  53. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  54. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  56. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  60. package/apps/dashboard/dist/assets/tab-models-tab-BLEjmd19.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-pm-loop-tab-Bfd449B4.js.br +0 -0
  62. package/apps/dashboard/dist/assets/tab-projects-tab-DhNWnlzt.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  64. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  65. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js +0 -1
  66. package/apps/dashboard/dist/assets/tab-settings-tab-Bn4nXtDe.js.br +0 -0
  67. package/apps/dashboard/dist/assets/tab-skills-tab-BpY0uZHW.js.br +0 -0
  68. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  69. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  70. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  71. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  72. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  73. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  74. package/apps/dashboard/dist/index.html.br +0 -0
  75. package/apps/dashboard/index.html +0 -6459
  76. package/apps/dashboard/package.json +0 -15
  77. package/apps/dashboard/src/app.js +0 -2823
  78. package/apps/dashboard/src/app.js.br +0 -0
  79. package/apps/dashboard/src/app.js.gz +0 -0
  80. package/apps/dashboard/src/chat/chat-actions.js +0 -1847
  81. package/apps/dashboard/src/chat/chat-actions.js.br +0 -0
  82. package/apps/dashboard/src/chat/unified-messages.js +0 -327
  83. package/apps/dashboard/src/chat/unified-messages.js.br +0 -0
  84. package/apps/dashboard/src/cli-process.js +0 -208
  85. package/apps/dashboard/src/cli-process.js.br +0 -0
  86. package/apps/dashboard/src/cli-process.js.gz +0 -0
  87. package/apps/dashboard/src/components/active-tasks-panel.js +0 -175
  88. package/apps/dashboard/src/components/active-tasks-panel.js.br +0 -0
  89. package/apps/dashboard/src/core/api.js +0 -18
  90. package/apps/dashboard/src/core/api.js.br +0 -0
  91. package/apps/dashboard/src/core/dom.js +0 -228
  92. package/apps/dashboard/src/core/dom.js.br +0 -0
  93. package/apps/dashboard/src/core/state.js +0 -91
  94. package/apps/dashboard/src/core/state.js.br +0 -0
  95. package/apps/dashboard/src/core/task-manager.js +0 -134
  96. package/apps/dashboard/src/core/task-manager.js.br +0 -0
  97. package/apps/dashboard/src/orchestration-status.js +0 -127
  98. package/apps/dashboard/src/orchestration-status.js.br +0 -0
  99. package/apps/dashboard/src/setup-wizard.js +0 -562
  100. package/apps/dashboard/src/setup-wizard.js.br +0 -0
  101. package/apps/dashboard/src/styles.css +0 -2085
  102. package/apps/dashboard/src/styles.css.br +0 -0
  103. package/apps/dashboard/src/styles.css.gz +0 -0
  104. package/apps/dashboard/src/tabs/agents-tab.js +0 -2237
  105. package/apps/dashboard/src/tabs/agents-tab.js.br +0 -0
  106. package/apps/dashboard/src/tabs/benchmarks-tab.js +0 -229
  107. package/apps/dashboard/src/tabs/benchmarks-tab.js.br +0 -0
  108. package/apps/dashboard/src/tabs/comms-tab.js +0 -955
  109. package/apps/dashboard/src/tabs/comms-tab.js.br +0 -0
  110. package/apps/dashboard/src/tabs/contacts-tab.js +0 -654
  111. package/apps/dashboard/src/tabs/contacts-tab.js.br +0 -0
  112. package/apps/dashboard/src/tabs/engines-tab.js +0 -175
  113. package/apps/dashboard/src/tabs/engines-tab.js.br +0 -0
  114. package/apps/dashboard/src/tabs/memory-tab.js +0 -182
  115. package/apps/dashboard/src/tabs/memory-tab.js.br +0 -0
  116. package/apps/dashboard/src/tabs/models-tab.js +0 -450
  117. package/apps/dashboard/src/tabs/models-tab.js.br +0 -0
  118. package/apps/dashboard/src/tabs/pm-loop-tab.js +0 -185
  119. package/apps/dashboard/src/tabs/pm-loop-tab.js.br +0 -0
  120. package/apps/dashboard/src/tabs/projects-tab.js +0 -663
  121. package/apps/dashboard/src/tabs/projects-tab.js.br +0 -0
  122. package/apps/dashboard/src/tabs/projects-tab.js.gz +0 -0
  123. package/apps/dashboard/src/tabs/prompts-tab.js +0 -160
  124. package/apps/dashboard/src/tabs/prompts-tab.js.br +0 -0
  125. package/apps/dashboard/src/tabs/services-tab.js +0 -202
  126. package/apps/dashboard/src/tabs/services-tab.js.br +0 -0
  127. package/apps/dashboard/src/tabs/settings-tab.js +0 -803
  128. package/apps/dashboard/src/tabs/settings-tab.js.br +0 -0
  129. package/apps/dashboard/src/tabs/skills-tab.js +0 -284
  130. package/apps/dashboard/src/tabs/skills-tab.js.br +0 -0
  131. package/apps/dashboard/src/tabs/spending-tab.js +0 -173
  132. package/apps/dashboard/src/tabs/spending-tab.js.br +0 -0
  133. package/apps/dashboard/src/tabs/swarm-chat-tab.js +0 -660
  134. package/apps/dashboard/src/tabs/swarm-chat-tab.js.br +0 -0
  135. package/apps/dashboard/src/tabs/swarm-tab.js +0 -538
  136. package/apps/dashboard/src/tabs/swarm-tab.js.br +0 -0
  137. package/apps/dashboard/src/tabs/usage-tab.js +0 -390
  138. package/apps/dashboard/src/tabs/usage-tab.js.br +0 -0
  139. package/apps/dashboard/src/tabs/waves-tab.js +0 -238
  140. package/apps/dashboard/src/tabs/waves-tab.js.br +0 -0
  141. package/apps/dashboard/src/tabs/workflows-tab.js +0 -747
  142. package/apps/dashboard/src/tabs/workflows-tab.js.br +0 -0
  143. package/apps/vibe/.crew/agent-memory/pipeline.json +0 -304
  144. package/apps/vibe/.crew/cost.json +0 -17
  145. package/apps/vibe/.crew/json-parse-metrics.jsonl +0 -27
  146. package/apps/vibe/.crew/pipeline-metrics.jsonl +0 -27
  147. package/apps/vibe/.crew/pipeline-runs/pipeline-0f90c392-2425-4ae5-850c-bd9d17b1d690.jsonl +0 -5
  148. package/apps/vibe/.crew/pipeline-runs/pipeline-1c269dd9-a63f-4fba-af81-5cf08048ef06.jsonl +0 -5
  149. package/apps/vibe/.crew/pipeline-runs/pipeline-288a7765-da24-4a22-89bc-1f3cc9b0562c.jsonl +0 -5
  150. package/apps/vibe/.crew/pipeline-runs/pipeline-2c78fd22-a657-4bd1-bc49-0679fb384409.jsonl +0 -5
  151. package/apps/vibe/.crew/pipeline-runs/pipeline-3da23550-22ed-4904-9a0a-8e79c1f3024c.jsonl +0 -5
  152. package/apps/vibe/.crew/pipeline-runs/pipeline-3e6fe08d-3264-404a-8df3-aab7efef10e7.jsonl +0 -5
  153. package/apps/vibe/.crew/pipeline-runs/pipeline-42eec610-57fe-4e09-9e7e-b315038495c2.jsonl +0 -5
  154. package/apps/vibe/.crew/pipeline-runs/pipeline-4438eb4c-ae13-42b1-90e2-b043d8983be8.jsonl +0 -5
  155. package/apps/vibe/.crew/pipeline-runs/pipeline-4740a9f5-86e7-44b6-a394-de433e291727.jsonl +0 -5
  156. package/apps/vibe/.crew/pipeline-runs/pipeline-49e1da6a-957e-48fd-9220-415019e4f8e2.jsonl +0 -5
  157. package/apps/vibe/.crew/pipeline-runs/pipeline-4c9251db-be68-427b-a3fc-a264f2b5778d.jsonl +0 -5
  158. package/apps/vibe/.crew/pipeline-runs/pipeline-6413fa33-a802-4b57-a8c0-a9056ad67842.jsonl +0 -5
  159. package/apps/vibe/.crew/pipeline-runs/pipeline-65e29a57-664d-4196-8109-017e364f182e.jsonl +0 -5
  160. package/apps/vibe/.crew/pipeline-runs/pipeline-6aa04bc5-9593-4b1f-b58d-3bf2978cb602.jsonl +0 -5
  161. package/apps/vibe/.crew/pipeline-runs/pipeline-6e1cba53-9b70-457e-99e0-59199149dd21.jsonl +0 -5
  162. package/apps/vibe/.crew/pipeline-runs/pipeline-749f41cc-4dac-4204-be64-873a6080a0d2.jsonl +0 -5
  163. package/apps/vibe/.crew/pipeline-runs/pipeline-74d68121-e181-4864-bd9a-c3211341dfaf.jsonl +0 -5
  164. package/apps/vibe/.crew/pipeline-runs/pipeline-8509bc24-142d-4e07-b44a-a50bf99d1103.jsonl +0 -5
  165. package/apps/vibe/.crew/pipeline-runs/pipeline-960339c6-07ca-43ce-9900-f6e1702b39b9.jsonl +0 -5
  166. package/apps/vibe/.crew/pipeline-runs/pipeline-9bef2dd2-6122-42e5-b3d9-19f4d80f9e40.jsonl +0 -5
  167. package/apps/vibe/.crew/pipeline-runs/pipeline-9c6480a9-7031-4146-b241-825b9a2d1de1.jsonl +0 -5
  168. package/apps/vibe/.crew/pipeline-runs/pipeline-9fd42426-8492-4157-9d5f-e1537c060489.jsonl +0 -2
  169. package/apps/vibe/.crew/pipeline-runs/pipeline-ad6d40a3-2f5e-46a9-a345-47caaccc51aa.jsonl +0 -5
  170. package/apps/vibe/.crew/pipeline-runs/pipeline-bc606133-8d5b-4535-8d85-f1a29cdaa981.jsonl +0 -5
  171. package/apps/vibe/.crew/pipeline-runs/pipeline-c1418f4e-b773-4ca1-84a3-216acf36e2f2.jsonl +0 -5
  172. package/apps/vibe/.crew/pipeline-runs/pipeline-c1a13ccd-634a-4d01-a4a7-1177b8a752ff.jsonl +0 -5
  173. package/apps/vibe/.crew/pipeline-runs/pipeline-c7d27b42-249e-4bd4-8f26-6aa998110b8a.jsonl +0 -5
  174. package/apps/vibe/.crew/pipeline-runs/pipeline-cca2e9b9-4a34-4d25-a311-5c793fa7e91e.jsonl +0 -5
  175. package/apps/vibe/.crew/sandbox.json +0 -7
  176. package/apps/vibe/.crew/session.json +0 -330
  177. package/apps/vibe/.crew/training-data.jsonl +0 -0
  178. package/apps/vibe/.github/workflows/studio-quality.yml +0 -37
  179. package/apps/vibe/.studio-data/project-messages/chuck-norris.jsonl +0 -18
  180. package/apps/vibe/.studio-data/project-messages/general.jsonl +0 -81
  181. package/apps/vibe/.studio-data/project-messages/studio-local.jsonl +0 -18
  182. package/apps/vibe/ARCHITECTURE.md +0 -3393
  183. package/apps/vibe/QUICK-REFERENCE.md +0 -211
  184. package/apps/vibe/ROADMAP.md +0 -41
  185. package/apps/vibe/STUDIO-SETUP-COMPLETE.md +0 -35
  186. package/apps/vibe/VISUAL-GUIDE.md +0 -378
  187. package/apps/vibe/capture-demo.mjs +0 -160
  188. package/apps/vibe/capture-full-demo.mjs +0 -255
  189. package/apps/vibe/capture-quickstart.mjs +0 -256
  190. package/apps/vibe/capture-vibe-assets.mjs +0 -71
  191. package/apps/vibe/capture-vibe-video.mjs +0 -260
  192. package/apps/vibe/check-buttons.js +0 -41
  193. package/apps/vibe/diagnose.html +0 -106
  194. package/apps/vibe/fix-buttons.js +0 -103
  195. package/apps/vibe/index.html +0 -3404
  196. package/apps/vibe/package-lock.json +0 -920
  197. package/apps/vibe/scripts/studio-pty-host.py +0 -117
  198. package/apps/vibe/src/main.js +0 -2940
  199. package/apps/vibe/src/register-all-languages.js +0 -98
  200. package/apps/vibe/start-studio.sh +0 -11
  201. package/apps/vibe/test/accessibility-tests.js +0 -77
  202. package/apps/vibe/test/browser-performance-audit.mjs +0 -205
  203. package/apps/vibe/test/performance-tests.js +0 -120
  204. package/apps/vibe/test/security-tests.js +0 -213
  205. package/apps/vibe/tests/e2e.local.mjs +0 -54
  206. package/apps/vibe/tests/server.smoke.mjs +0 -106
  207. package/apps/vibe/update_website.mjs +0 -74
  208. package/apps/vibe/vite.config.js +0 -19
  209. package/lib/crew-lead/chat-handler.mjs.bak +0 -1274
  210. package/lib/engines/rt-envelope.mjs.backup-current +0 -870
@@ -0,0 +1,200 @@
1
+ /**
2
+ * tmux-bridge adapter — thin wrapper around smux's tmux-bridge CLI
3
+ *
4
+ * Provides agent-to-agent pane communication when running inside a tmux session
5
+ * with smux installed. All functions degrade to no-ops when unavailable.
6
+ *
7
+ * Opt-in: requires $TMUX set, `tmux-bridge` on PATH, and
8
+ * CREWSWARM_TMUX_BRIDGE=1 env var (or tmuxBridge: true in system config).
9
+ */
10
+
11
+ import { execFileSync, execSync } from "node:child_process";
12
+
13
+ function which(bin) {
14
+ try { execSync(`which ${bin}`, { stdio: "ignore" }); return true; } catch { return false; }
15
+ }
16
+
17
+ // ── Detection & caching ─────────────────────────────────────────────────────
18
+
19
+ let _available = null;
20
+ let _ownPaneId = null;
21
+ const _resolveCache = new Map(); // label → { paneId, ts }
22
+ const RESOLVE_TTL_MS = 30_000;
23
+ const TMUX_BRIDGE_BIN = process.env.SMUX_BRIDGE_BIN || "tmux-bridge";
24
+
25
+ function exec(args, { timeout = 5000 } = {}) {
26
+ try {
27
+ return execFileSync(TMUX_BRIDGE_BIN, args, {
28
+ encoding: "utf8",
29
+ timeout,
30
+ stdio: ["ignore", "pipe", "pipe"],
31
+ }).trim();
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Check if tmux-bridge is available and opted-in.
39
+ * Result is cached for the process lifetime.
40
+ */
41
+ export function detect() {
42
+ if (_available !== null) return _available;
43
+
44
+ // Must be inside a tmux session
45
+ if (!process.env.TMUX) {
46
+ _available = false;
47
+ return false;
48
+ }
49
+
50
+ // Must have tmux-bridge binary
51
+ if (!which(TMUX_BRIDGE_BIN)) {
52
+ _available = false;
53
+ return false;
54
+ }
55
+
56
+ // Must be opted-in via env var or config
57
+ const envFlag = process.env.CREWSWARM_TMUX_BRIDGE;
58
+ if (!envFlag || envFlag === "0" || envFlag === "false") {
59
+ _available = false;
60
+ return false;
61
+ }
62
+
63
+ _available = true;
64
+ return true;
65
+ }
66
+
67
+ /**
68
+ * Get this process's own tmux pane ID.
69
+ * @returns {string|null} e.g. "%3"
70
+ */
71
+ export function id() {
72
+ if (!detect()) return null;
73
+ if (_ownPaneId) return _ownPaneId;
74
+ _ownPaneId = exec(["id"]);
75
+ return _ownPaneId;
76
+ }
77
+
78
+ /**
79
+ * Label a pane with an agent ID so other agents can discover it.
80
+ * @param {string} agentId - CrewSwarm agent ID (e.g. "crew-coder")
81
+ * @param {string} [paneId] - Target pane ID. Defaults to own pane.
82
+ * @returns {boolean} success
83
+ */
84
+ export function label(agentId, paneId) {
85
+ if (!detect()) return false;
86
+ const target = paneId || id();
87
+ if (!target) return false;
88
+ const result = exec(["name", target, agentId]);
89
+ if (result !== null) {
90
+ // Update cache
91
+ _resolveCache.set(agentId, { paneId: target, ts: Date.now() });
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+
97
+ /**
98
+ * Resolve an agent ID to a tmux pane ID.
99
+ * @param {string} agentId
100
+ * @returns {string|null} pane ID or null
101
+ */
102
+ export function resolve(agentId) {
103
+ if (!detect()) return null;
104
+
105
+ // Check cache
106
+ const cached = _resolveCache.get(agentId);
107
+ if (cached && (Date.now() - cached.ts) < RESOLVE_TTL_MS) {
108
+ return cached.paneId;
109
+ }
110
+
111
+ const paneId = exec(["resolve", agentId]);
112
+ if (paneId) {
113
+ _resolveCache.set(agentId, { paneId, ts: Date.now() });
114
+ }
115
+ return paneId;
116
+ }
117
+
118
+ /**
119
+ * Read the last N lines from an agent's pane.
120
+ * Also satisfies the read-guard requirement for subsequent sends.
121
+ * @param {string} agentId
122
+ * @param {number} [lines=50]
123
+ * @returns {string|null} pane content or null
124
+ */
125
+ export function read(agentId, lines = 50) {
126
+ if (!detect()) return null;
127
+ const paneId = resolve(agentId);
128
+ if (!paneId) return null;
129
+ const output = exec(["read", paneId, String(lines)]);
130
+ return output;
131
+ }
132
+
133
+ /**
134
+ * Send text to an agent's pane followed by Enter.
135
+ * Reads the pane before each write command to satisfy tmux-bridge's read-guard
136
+ * (the guard is consumed on every type/keys call).
137
+ * @param {string} agentId
138
+ * @param {string} text
139
+ * @returns {boolean} success
140
+ */
141
+ export function send(agentId, text) {
142
+ if (!detect()) return false;
143
+ const paneId = resolve(agentId);
144
+ if (!paneId) return false;
145
+
146
+ // Read-guard is consumed on each type/keys call, so read before each one
147
+ exec(["read", paneId, "1"]);
148
+ const typeResult = exec(["type", paneId, text]);
149
+ if (typeResult === null) return false;
150
+
151
+ exec(["read", paneId, "1"]);
152
+ const keyResult = exec(["keys", paneId, "Enter"]);
153
+ return keyResult !== null;
154
+ }
155
+
156
+ /**
157
+ * List all tmux panes with their labels and metadata.
158
+ * @returns {Array<{paneId: string, label: string, raw: string}>}
159
+ */
160
+ export function list() {
161
+ if (!detect()) return [];
162
+ const raw = exec(["list"]);
163
+ if (!raw) return [];
164
+
165
+ // Parse tmux-bridge list output:
166
+ // TARGET SESSION:WIN SIZE PROCESS LABEL CWD
167
+ // %0 crewtest:0 120x29 -zsh crew-coder ~/CrewSwarm
168
+ const lines = raw.split("\n").filter(Boolean);
169
+ // Skip header row (starts with "TARGET")
170
+ return lines
171
+ .filter(line => line.trimStart().startsWith("%"))
172
+ .map(line => {
173
+ const parts = line.split(/\s+/).filter(Boolean);
174
+ return {
175
+ paneId: parts[0] || "",
176
+ session: parts[1] || "",
177
+ size: parts[2] || "",
178
+ process: parts[3] || "",
179
+ label: (parts[4] && parts[4] !== "-") ? parts[4] : "",
180
+ cwd: parts[5] || "",
181
+ raw: line,
182
+ };
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Clear the resolve cache (useful after pane layout changes).
188
+ */
189
+ export function clearCache() {
190
+ _resolveCache.clear();
191
+ }
192
+
193
+ /**
194
+ * Reset detection state (for testing).
195
+ */
196
+ export function _reset() {
197
+ _available = null;
198
+ _ownPaneId = null;
199
+ clearCache();
200
+ }
@@ -160,7 +160,8 @@ export function getActiveProcesses() {
160
160
  idleFor: now - proc.lastActivity,
161
161
  outputLines: proc.outputLines,
162
162
  chatId: proc.chatId,
163
- sessionId: proc.sessionId
163
+ sessionId: proc.sessionId,
164
+ tmuxSessionId: proc.tmuxSessionId || null
164
165
  }));
165
166
  }
166
167
 
@@ -2577,6 +2577,40 @@ Reply with your answers and I'll turn this into a concrete build plan with file
2577
2577
  ...(usedFallback ? { fallbackReason } : {}),
2578
2578
  },
2579
2579
  });
2580
+
2581
+ // Route @mentions in assistant replies — crew-lead can autonomously dispatch
2582
+ if (replyMentions.length && channelMode) {
2583
+ try {
2584
+ const {
2585
+ handleAutonomousMentions: routeMentions,
2586
+ detectMentionTargets,
2587
+ } = await import("../chat/autonomous-mentions.mjs");
2588
+ const mentionTargets = detectMentionTargets(historyReply);
2589
+ const mentionRoute = classifySharedChatMention(historyReply);
2590
+ if (mentionTargets.length && mentionRoute.mode === "dispatch") {
2591
+ void routeMentions({
2592
+ message: { content: historyReply },
2593
+ sender: "crew-lead",
2594
+ channel: sharedChannel,
2595
+ projectId: sharedChannel,
2596
+ sessionId,
2597
+ projectDir: explicitProjectDir || activeProjectOutputDir || null,
2598
+ originMessageId: assistantMessageId,
2599
+ originThreadId: sharedThreadId,
2600
+ chatHistory: loadProjectMessages(sharedChannel, {
2601
+ limit: 10,
2602
+ threadId: sharedThreadId,
2603
+ excludeDirect: true,
2604
+ }),
2605
+ broadcastSSE: _deps.broadcastSSE,
2606
+ }).catch((err) => {
2607
+ console.warn(`[chat-handler] Assistant mention routing failed: ${err.message}`);
2608
+ });
2609
+ }
2610
+ } catch (mentionErr) {
2611
+ console.warn(`[chat-handler] Assistant mention dispatch error: ${mentionErr.message}`);
2612
+ }
2613
+ }
2580
2614
  } catch (e) {
2581
2615
  console.warn(
2582
2616
  `[chat-handler] Failed to save assistant message to project store: ${e.message}`,
@@ -20,7 +20,7 @@ import {
20
20
  import { shouldSkipGeminiPassthroughLine } from "../gemini-cli-passthrough-noise.mjs";
21
21
  import { applyProjectDirToPipelineSteps } from "../dispatch/parsers.mjs";
22
22
  import { normalizeProjectDir } from "../runtime/project-dir.mjs";
23
- import { CREWSWARM_REPO_ROOT } from "../runtime/config.mjs";
23
+ import { CREWSWARM_REPO_ROOT, loadSwarmConfig } from "../runtime/config.mjs";
24
24
  import { resolveCursorLaunchSpec } from "../engines/cursor-launcher.mjs";
25
25
 
26
26
  let _deps = {};
@@ -1087,6 +1087,7 @@ export function createAndStartServer(PORT) {
1087
1087
  bgConsciousnessIntervalMs,
1088
1088
  cursorWavesRef,
1089
1089
  claudeCodeRef,
1090
+ tmuxBridgeRef,
1090
1091
  } = _deps;
1091
1092
 
1092
1093
  // Debug: verify critical deps
@@ -3531,6 +3532,9 @@ export function createAndStartServer(PORT) {
3531
3532
  sessionId: reqSessionId,
3532
3533
  injectHistory,
3533
3534
  model: reqModel,
3535
+ permissionMode: reqPermissionMode,
3536
+ sandbox: reqSandbox,
3537
+ forceL2: reqForceL2,
3534
3538
  } = JSON.parse(body || "{}");
3535
3539
  if (!message) {
3536
3540
  json(res, 400, { ok: false, error: "message required" });
@@ -3761,7 +3765,7 @@ export function createAndStartServer(PORT) {
3761
3765
  "never",
3762
3766
  "exec",
3763
3767
  "--sandbox",
3764
- "danger-full-access",
3768
+ reqSandbox || "danger-full-access",
3765
3769
  "--skip-git-repo-check",
3766
3770
  "--color",
3767
3771
  "never",
@@ -3858,12 +3862,16 @@ export function createAndStartServer(PORT) {
3858
3862
  "-p",
3859
3863
  "--setting-sources",
3860
3864
  "user",
3861
- "--dangerously-skip-permissions",
3862
3865
  "--output-format",
3863
3866
  "stream-json",
3864
3867
  "--verbose",
3865
3868
  "--include-partial-messages",
3866
3869
  ];
3870
+ if (reqPermissionMode) {
3871
+ args.push("--permission-mode", reqPermissionMode);
3872
+ } else {
3873
+ args.push("--dangerously-skip-permissions");
3874
+ }
3867
3875
  if (projectDir) args.push("--add-dir", projectDir);
3868
3876
  if (reqModel) args.push("--model", reqModel);
3869
3877
  // Prefer --resume <session-id> for per-project isolation; fall back to --continue (most recent global)
@@ -3876,7 +3884,10 @@ export function createAndStartServer(PORT) {
3876
3884
  } else if (continueSession) {
3877
3885
  args.push("--continue");
3878
3886
  }
3879
- args.push(finalMessage);
3887
+ // Skip user MCP servers to avoid 30s+ init hangs
3888
+ args.push("--strict-mcp-config", "--mcp-config", path.join(os.homedir(), ".crewswarm", "config", "empty-mcp.json"));
3889
+ // -- separates flags from prompt (--mcp-config is variadic and eats positional args)
3890
+ args.push("--", finalMessage);
3880
3891
  }
3881
3892
 
3882
3893
  send({ type: "start", engine, message: message.slice(0, 80) });
@@ -3900,9 +3911,22 @@ export function createAndStartServer(PORT) {
3900
3911
  const spawnCwd =
3901
3912
  engine === "claude" && projectDir ? "/tmp" : projectDir;
3902
3913
 
3914
+ // Merge crewswarm.json env vars into spawn env (crew-cli L2, model overrides, etc.)
3915
+ const spawnEnv = { ...process.env };
3916
+ try {
3917
+ const _sysCfg = loadSwarmConfig();
3918
+ if (_sysCfg?.env && typeof _sysCfg.env === "object") {
3919
+ for (const [k, v] of Object.entries(_sysCfg.env)) {
3920
+ if (!spawnEnv[k] && v != null) spawnEnv[k] = String(v);
3921
+ }
3922
+ }
3923
+ } catch {}
3924
+ // Force L2 planner for crew-cli when requested (enhance-prompt planner path)
3925
+ if (reqForceL2) spawnEnv.CREW_FORCE_L2 = "true";
3926
+
3903
3927
  const proc = _spawn(bin, args, {
3904
3928
  cwd: spawnCwd,
3905
- env: process.env,
3929
+ env: spawnEnv,
3906
3930
  stdio: ["ignore", "pipe", "pipe"],
3907
3931
  });
3908
3932
  let lineBuffer = "";
@@ -3915,7 +3939,7 @@ export function createAndStartServer(PORT) {
3915
3939
  let passthroughGracefulEnd = false;
3916
3940
 
3917
3941
  const passthroughTimeoutMs = Number(
3918
- process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "240000",
3942
+ process.env.CREWSWARM_PASSTHROUGH_TIMEOUT_MS || "300000",
3919
3943
  );
3920
3944
  const passthroughWatchdog = setTimeout(() => {
3921
3945
  send({
@@ -4206,15 +4230,33 @@ export function createAndStartServer(PORT) {
4206
4230
  // crew-cli special handling: extract JSON from output (skipping logs) and send only response field
4207
4231
  if (engine === "crew-cli" && fullOutput.trim()) {
4208
4232
  try {
4209
- // crew-cli may emit logs before JSON - use regex to find the JSON object
4210
- // Pattern matches: crew chat --json outputs "chat.result"
4211
- const jsonMatch = fullOutput.match(
4212
- /\{[\s\S]*"kind":\s*"chat\.result"[\s\S]*\}/,
4213
- );
4214
- if (!jsonMatch) {
4215
- throw new Error("No chat.result JSON found in output");
4233
+ // crew-cli emits logs before JSON find the JSON by scanning for the last
4234
+ // line that starts with { and contains "chat.result"
4235
+ const lines = fullOutput.split("\n");
4236
+ let jsonStart = -1;
4237
+ for (let i = lines.length - 1; i >= 0; i--) {
4238
+ if (lines[i].trimStart().startsWith("{") && fullOutput.indexOf('"kind"') >= 0) {
4239
+ jsonStart = i;
4240
+ break;
4241
+ }
4242
+ }
4243
+ if (jsonStart < 0) {
4244
+ throw new Error("No JSON object found in crew-cli output");
4245
+ }
4246
+ // Collect from jsonStart to end, parse
4247
+ const jsonCandidate = lines.slice(jsonStart).join("\n");
4248
+ let parsed;
4249
+ try {
4250
+ parsed = JSON.parse(jsonCandidate);
4251
+ } catch {
4252
+ // Try to find just the response field with regex
4253
+ const respMatch = jsonCandidate.match(/"response":\s*"((?:[^"\\]|\\.)*)"/);
4254
+ if (respMatch) {
4255
+ parsed = { response: respMatch[1].replace(/\\n/g, "\n").replace(/\\"/g, '"') };
4256
+ } else {
4257
+ throw new Error("Failed to parse crew-cli JSON output");
4258
+ }
4216
4259
  }
4217
- const parsed = JSON.parse(jsonMatch[0]);
4218
4260
  if (parsed.response) {
4219
4261
  const responseText = typeof parsed.response === 'string'
4220
4262
  ? parsed.response
@@ -4580,6 +4622,46 @@ export function createAndStartServer(PORT) {
4580
4622
  }
4581
4623
  }
4582
4624
 
4625
+ // GET/POST /api/settings/tmux-bridge — toggle tmux-bridge session layer at runtime
4626
+ if (url.pathname === "/api/settings/tmux-bridge") {
4627
+ if (!checkBearer(req)) {
4628
+ json(res, 401, { ok: false, error: "Unauthorized" });
4629
+ return;
4630
+ }
4631
+ if (req.method === "GET") {
4632
+ json(res, 200, { ok: true, enabled: tmuxBridgeRef?.enabled ?? false });
4633
+ return;
4634
+ }
4635
+ if (req.method === "POST") {
4636
+ const body = await readBody(req);
4637
+ const enable =
4638
+ typeof body.enabled === "boolean"
4639
+ ? body.enabled
4640
+ : !(tmuxBridgeRef?.enabled ?? false);
4641
+ if (tmuxBridgeRef) tmuxBridgeRef.enabled = enable;
4642
+ try {
4643
+ const cfgPath = path.join(
4644
+ os.homedir(),
4645
+ ".crewswarm",
4646
+ "crewswarm.json",
4647
+ );
4648
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
4649
+ cfg.tmuxBridge = enable;
4650
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
4651
+ } catch (e) {
4652
+ console.warn(
4653
+ "[crew-lead] Could not persist tmuxBridge:",
4654
+ e.message,
4655
+ );
4656
+ }
4657
+ console.log(
4658
+ `[crew-lead] tmux-bridge ${enable ? "ENABLED" : "DISABLED"} via dashboard`,
4659
+ );
4660
+ json(res, 200, { ok: true, enabled: tmuxBridgeRef?.enabled ?? enable });
4661
+ return;
4662
+ }
4663
+ }
4664
+
4583
4665
  // GET/POST /api/settings/global-fallback — set/get global OpenCode fallback model
4584
4666
  if (url.pathname === "/api/settings/global-fallback") {
4585
4667
  if (!checkBearer(req)) {
@@ -4680,6 +4762,250 @@ export function createAndStartServer(PORT) {
4680
4762
  }
4681
4763
  }
4682
4764
 
4765
+ // ── Missing settings endpoints (bulk implementation) ─────────────────────
4766
+
4767
+ // Helper: read/write crewswarm.json for simple boolean/string settings
4768
+ const cfgPath = path.join(os.homedir(), ".crewswarm", "crewswarm.json");
4769
+ function readCfg() {
4770
+ try { return JSON.parse(fs.readFileSync(cfgPath, "utf8")); } catch { return {}; }
4771
+ }
4772
+ function writeCfg(cfg) {
4773
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
4774
+ }
4775
+ function whichBin(bin) {
4776
+ try {
4777
+ const r = spawn("which", [bin], { stdio: "ignore" });
4778
+ // spawn is async but we can check if the binary exists via fs
4779
+ const paths = (process.env.PATH || "").split(":");
4780
+ return paths.some(p => { try { fs.accessSync(path.join(p, bin), fs.constants.X_OK); return true; } catch { return false; } });
4781
+ } catch { return false; }
4782
+ }
4783
+
4784
+ // GET/POST /api/settings/autonomous-mentions
4785
+ if (url.pathname === "/api/settings/autonomous-mentions") {
4786
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4787
+ if (req.method === "GET") {
4788
+ const cfg = readCfg();
4789
+ const enabled = cfg.settings?.autonomousMentionsEnabled !== false;
4790
+ json(res, 200, { ok: true, enabled });
4791
+ return;
4792
+ }
4793
+ if (req.method === "POST") {
4794
+ const body = await readBody(req);
4795
+ const cfg = readCfg();
4796
+ if (!cfg.settings) cfg.settings = {};
4797
+ cfg.settings.autonomousMentionsEnabled = typeof body.enabled === "boolean" ? body.enabled : true;
4798
+ writeCfg(cfg);
4799
+ console.log(`[crew-lead] Autonomous mentions ${cfg.settings.autonomousMentionsEnabled ? "ENABLED" : "DISABLED"} via dashboard`);
4800
+ json(res, 200, { ok: true, enabled: cfg.settings.autonomousMentionsEnabled });
4801
+ return;
4802
+ }
4803
+ }
4804
+
4805
+ // GET/POST /api/settings/codex
4806
+ if (url.pathname === "/api/settings/codex") {
4807
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4808
+ if (req.method === "GET") {
4809
+ const cfg = readCfg();
4810
+ const enabled = cfg.codexEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_CODEX_ENABLED || ""));
4811
+ json(res, 200, { ok: true, enabled });
4812
+ return;
4813
+ }
4814
+ if (req.method === "POST") {
4815
+ const body = await readBody(req);
4816
+ const cfg = readCfg();
4817
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.codexEnabled;
4818
+ cfg.codexEnabled = enable;
4819
+ writeCfg(cfg);
4820
+ console.log(`[crew-lead] Codex executor ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4821
+ json(res, 200, { ok: true, enabled: enable });
4822
+ return;
4823
+ }
4824
+ }
4825
+
4826
+ // GET/POST /api/settings/gemini-cli
4827
+ if (url.pathname === "/api/settings/gemini-cli") {
4828
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4829
+ if (req.method === "GET") {
4830
+ const cfg = readCfg();
4831
+ const enabled = cfg.geminiCliEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_GEMINI_CLI_ENABLED || ""));
4832
+ const installed = whichBin("gemini");
4833
+ json(res, 200, { ok: true, enabled, installed });
4834
+ return;
4835
+ }
4836
+ if (req.method === "POST") {
4837
+ const body = await readBody(req);
4838
+ const cfg = readCfg();
4839
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.geminiCliEnabled;
4840
+ cfg.geminiCliEnabled = enable;
4841
+ writeCfg(cfg);
4842
+ console.log(`[crew-lead] Gemini CLI ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4843
+ json(res, 200, { ok: true, enabled: enable, installed: whichBin("gemini") });
4844
+ return;
4845
+ }
4846
+ }
4847
+
4848
+ // GET/POST /api/settings/crew-cli
4849
+ if (url.pathname === "/api/settings/crew-cli") {
4850
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4851
+ if (req.method === "GET") {
4852
+ const cfg = readCfg();
4853
+ const enabled = cfg.crewCliEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_CREW_CLI_ENABLED || ""));
4854
+ json(res, 200, { ok: true, enabled });
4855
+ return;
4856
+ }
4857
+ if (req.method === "POST") {
4858
+ const body = await readBody(req);
4859
+ const cfg = readCfg();
4860
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.crewCliEnabled;
4861
+ cfg.crewCliEnabled = enable;
4862
+ writeCfg(cfg);
4863
+ console.log(`[crew-lead] Crew CLI ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4864
+ json(res, 200, { ok: true, enabled: enable });
4865
+ return;
4866
+ }
4867
+ }
4868
+
4869
+ // GET/POST /api/settings/opencode
4870
+ if (url.pathname === "/api/settings/opencode") {
4871
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4872
+ if (req.method === "GET") {
4873
+ const cfg = readCfg();
4874
+ const enabled = cfg.opencodeEnabled === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_OPENCODE_ENABLED || ""));
4875
+ const installed = whichBin("opencode");
4876
+ json(res, 200, { ok: true, enabled, installed });
4877
+ return;
4878
+ }
4879
+ if (req.method === "POST") {
4880
+ const body = await readBody(req);
4881
+ const cfg = readCfg();
4882
+ const enable = typeof body.enabled === "boolean" ? body.enabled : !cfg.opencodeEnabled;
4883
+ cfg.opencodeEnabled = enable;
4884
+ writeCfg(cfg);
4885
+ console.log(`[crew-lead] OpenCode ${enable ? "ENABLED" : "DISABLED"} via dashboard`);
4886
+ json(res, 200, { ok: true, enabled: enable, installed: whichBin("opencode") });
4887
+ return;
4888
+ }
4889
+ }
4890
+
4891
+ // GET/POST /api/settings/global-oc-loop
4892
+ if (url.pathname === "/api/settings/global-oc-loop") {
4893
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4894
+ if (req.method === "GET") {
4895
+ const cfg = readCfg();
4896
+ const enabled = cfg.engineLoop === true || /^1|true|yes$/i.test(String(process.env.CREWSWARM_ENGINE_LOOP || ""));
4897
+ const maxRounds = cfg.engineLoopMaxRounds ?? parseInt(process.env.CREWSWARM_ENGINE_LOOP_MAX_ROUNDS || "10", 10);
4898
+ json(res, 200, { ok: true, enabled, maxRounds });
4899
+ return;
4900
+ }
4901
+ if (req.method === "POST") {
4902
+ const body = await readBody(req);
4903
+ const cfg = readCfg();
4904
+ if (typeof body.enabled === "boolean") cfg.engineLoop = body.enabled;
4905
+ if (body.maxRounds !== undefined) cfg.engineLoopMaxRounds = parseInt(body.maxRounds, 10) || 10;
4906
+ writeCfg(cfg);
4907
+ console.log(`[crew-lead] Engine loop: enabled=${cfg.engineLoop ?? false}, maxRounds=${cfg.engineLoopMaxRounds ?? 10}`);
4908
+ json(res, 200, { ok: true, enabled: cfg.engineLoop ?? false, maxRounds: cfg.engineLoopMaxRounds ?? 10 });
4909
+ return;
4910
+ }
4911
+ }
4912
+
4913
+ // GET/POST /api/settings/passthrough-notify
4914
+ if (url.pathname === "/api/settings/passthrough-notify") {
4915
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4916
+ if (req.method === "GET") {
4917
+ const cfg = readCfg();
4918
+ json(res, 200, { ok: true, value: cfg.passthroughNotify || "both" });
4919
+ return;
4920
+ }
4921
+ if (req.method === "POST") {
4922
+ const body = await readBody(req);
4923
+ const cfg = readCfg();
4924
+ cfg.passthroughNotify = body.value || "both";
4925
+ writeCfg(cfg);
4926
+ console.log(`[crew-lead] Passthrough notify → ${cfg.passthroughNotify}`);
4927
+ json(res, 200, { ok: true, value: cfg.passthroughNotify });
4928
+ return;
4929
+ }
4930
+ }
4931
+
4932
+ // GET/POST /api/settings/loop-brain
4933
+ if (url.pathname === "/api/settings/loop-brain") {
4934
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4935
+ if (req.method === "GET") {
4936
+ const cfg = readCfg();
4937
+ json(res, 200, { ok: true, loopBrain: cfg.loopBrain || "" });
4938
+ return;
4939
+ }
4940
+ if (req.method === "POST") {
4941
+ const body = await readBody(req);
4942
+ const cfg = readCfg();
4943
+ cfg.loopBrain = body.loopBrain || "";
4944
+ writeCfg(cfg);
4945
+ console.log(`[crew-lead] Loop brain → ${cfg.loopBrain || "(cleared)"}`);
4946
+ json(res, 200, { ok: true, loopBrain: cfg.loopBrain });
4947
+ return;
4948
+ }
4949
+ }
4950
+
4951
+ // GET /api/settings/openclaw-status
4952
+ if (url.pathname === "/api/settings/openclaw-status" && req.method === "GET") {
4953
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4954
+ const cfg = readCfg();
4955
+ const installed = !!(cfg.openClaw?.enabled || cfg.openclaw?.gatewayUrl || process.env.OPENCLAW_GATEWAY_URL);
4956
+ json(res, 200, { ok: true, installed });
4957
+ return;
4958
+ }
4959
+
4960
+ // GET/POST /api/settings/rt-token
4961
+ if (url.pathname === "/api/settings/rt-token") {
4962
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4963
+ if (req.method === "GET") {
4964
+ const cfg = readCfg();
4965
+ const token = cfg.rtToken || process.env.CREWSWARM_RT_AUTH_TOKEN || "";
4966
+ json(res, 200, { ok: true, token: token ? true : false });
4967
+ return;
4968
+ }
4969
+ if (req.method === "POST") {
4970
+ const body = await readBody(req);
4971
+ if (!body.token) { json(res, 400, { ok: false, error: "No token provided" }); return; }
4972
+ const cfg = readCfg();
4973
+ cfg.rtToken = body.token;
4974
+ writeCfg(cfg);
4975
+ console.log("[crew-lead] RT token saved via dashboard");
4976
+ json(res, 200, { ok: true, saved: true });
4977
+ return;
4978
+ }
4979
+ }
4980
+
4981
+ // GET /api/config/lock-status, POST /api/config/lock, POST /api/config/unlock
4982
+ if (url.pathname === "/api/config/lock-status" && req.method === "GET") {
4983
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4984
+ const lockFile = path.join(os.homedir(), ".crewswarm", ".config.lock");
4985
+ json(res, 200, { ok: true, locked: fs.existsSync(lockFile) });
4986
+ return;
4987
+ }
4988
+ if (url.pathname === "/api/config/lock" && req.method === "POST") {
4989
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
4990
+ const lockFile = path.join(os.homedir(), ".crewswarm", ".config.lock");
4991
+ try {
4992
+ fs.writeFileSync(lockFile, new Date().toISOString());
4993
+ console.log("[crew-lead] Config LOCKED via dashboard");
4994
+ json(res, 200, { ok: true, locked: true });
4995
+ } catch (e) { json(res, 500, { ok: false, error: e.message }); }
4996
+ return;
4997
+ }
4998
+ if (url.pathname === "/api/config/unlock" && req.method === "POST") {
4999
+ if (!checkBearer(req)) { json(res, 401, { ok: false, error: "Unauthorized" }); return; }
5000
+ const lockFile = path.join(os.homedir(), ".crewswarm", ".config.lock");
5001
+ try {
5002
+ fs.unlinkSync(lockFile);
5003
+ console.log("[crew-lead] Config UNLOCKED via dashboard");
5004
+ } catch {}
5005
+ json(res, 200, { ok: true, locked: false });
5006
+ return;
5007
+ }
5008
+
4683
5009
  // POST /api/spending/reset — reset today's spending counters
4684
5010
  if (url.pathname === "/api/spending/reset" && req.method === "POST") {
4685
5011
  if (!checkBearer(req)) {