crewswarm 0.9.4 → 1.0.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 (64) hide show
  1. package/.env.example +8 -1
  2. package/README.md +58 -9
  3. package/apps/dashboard/README.md +49 -0
  4. package/apps/dashboard/dist/assets/{index-D-sRshvg.css → index-C5-vlIwl.css} +1 -1
  5. package/apps/dashboard/dist/assets/index-CSooN9fi.js +2 -0
  6. package/apps/dashboard/dist/assets/index-CSooN9fi.js.br +0 -0
  7. package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js +1 -0
  8. package/apps/dashboard/dist/assets/tab-spending-tab-DcXD5TQY.js.br +0 -0
  9. package/apps/dashboard/dist/assets/tab-testing-tab-Ea5K-rsb.js +1 -0
  10. package/apps/dashboard/dist/index.html +85 -7
  11. package/apps/dashboard/dist/index.html.br +0 -0
  12. package/contrib/openclaw-plugin/index.ts +20 -11
  13. package/install.sh +2 -2
  14. package/lib/autoharness/index.mjs +151 -1
  15. package/lib/chat/history.mjs +1 -1
  16. package/lib/contacts/identity-linker.mjs +24 -3
  17. package/lib/contacts/index.mjs +2 -1
  18. package/lib/crew-lead/chat-handler.mjs +56 -33
  19. package/lib/crew-lead/llm-caller.mjs +71 -14
  20. package/lib/crew-lead/prompts.mjs +4 -2
  21. package/lib/crew-lead/wave-dispatcher.mjs +53 -3
  22. package/lib/crew-lead/worktree.mjs +258 -0
  23. package/lib/crew-lead/ws-router.mjs +43 -0
  24. package/lib/engines/rt-envelope.mjs +4 -1
  25. package/lib/memory/relevance-scorer.mjs +199 -0
  26. package/lib/memory/shared-adapter.mjs +85 -19
  27. package/package.json +10 -3
  28. package/scripts/dashboard.mjs +398 -28
  29. package/scripts/health-check.mjs +70 -28
  30. package/scripts/install-docker.sh +1 -1
  31. package/scripts/restart-all-from-repo.sh +25 -21
  32. package/scripts/start.mjs +81 -26
  33. package/apps/dashboard/dist/assets/chat-core-uXb_C0GM.js.br +0 -0
  34. package/apps/dashboard/dist/assets/cli-process-CNZ_UBCt.js.br +0 -0
  35. package/apps/dashboard/dist/assets/components-BS9fQjE_.js.br +0 -0
  36. package/apps/dashboard/dist/assets/core-utils-CmOkXgzi.js.br +0 -0
  37. package/apps/dashboard/dist/assets/index-BeVllEj_.js +0 -2
  38. package/apps/dashboard/dist/assets/index-BeVllEj_.js.br +0 -0
  39. package/apps/dashboard/dist/assets/index-D-sRshvg.css.br +0 -0
  40. package/apps/dashboard/dist/assets/orchestration-Ca2DLWN-.js.br +0 -0
  41. package/apps/dashboard/dist/assets/setup-wizard-CA0Or47w.js.br +0 -0
  42. package/apps/dashboard/dist/assets/tab-agents-tab-BgpIsjkw.js.br +0 -0
  43. package/apps/dashboard/dist/assets/tab-benchmarks-tab-BHjKCPm3.js.br +0 -0
  44. package/apps/dashboard/dist/assets/tab-comms-tab-kguqTIzD.js.br +0 -0
  45. package/apps/dashboard/dist/assets/tab-contacts-tab-DiOyMYth.js.br +0 -0
  46. package/apps/dashboard/dist/assets/tab-engines-tab-BsdZVvU0.js.br +0 -0
  47. package/apps/dashboard/dist/assets/tab-memory-tab-Cu6u13EQ.js.br +0 -0
  48. package/apps/dashboard/dist/assets/tab-models-tab-dNRgsTOO.js.br +0 -0
  49. package/apps/dashboard/dist/assets/tab-pm-loop-tab-DiAPTJXu.js.br +0 -0
  50. package/apps/dashboard/dist/assets/tab-projects-tab-SFH4E--a.js.br +0 -0
  51. package/apps/dashboard/dist/assets/tab-prompts-tab-DVkUNaJd.js.br +0 -0
  52. package/apps/dashboard/dist/assets/tab-services-tab-DU_LH3uG.js.br +0 -0
  53. package/apps/dashboard/dist/assets/tab-settings-tab-CuvH_Fj_.js.br +0 -0
  54. package/apps/dashboard/dist/assets/tab-skills-tab-DR7PJ7NB.js.br +0 -0
  55. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js +0 -1
  56. package/apps/dashboard/dist/assets/tab-spending-tab-DEccQHnt.js.br +0 -0
  57. package/apps/dashboard/dist/assets/tab-swarm-chat-tab-BNrd88-r.js.br +0 -0
  58. package/apps/dashboard/dist/assets/tab-swarm-tab-B1AcjL1W.js.br +0 -0
  59. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js +0 -1
  60. package/apps/dashboard/dist/assets/tab-testing-tab-CezZOZcJ.js.br +0 -0
  61. package/apps/dashboard/dist/assets/tab-usage-tab-BIOOnB-Y.js.br +0 -0
  62. package/apps/dashboard/dist/assets/tab-waves-tab-SaJDkb4x.js.br +0 -0
  63. package/apps/dashboard/dist/assets/tab-workflows-tab-B-soSy1k.js.br +0 -0
  64. package/apps/dashboard/dist/index.html.gz +0 -0
@@ -16,6 +16,8 @@ import fs from "node:fs";
16
16
  import path from "node:path";
17
17
  import os from "node:os";
18
18
  import { execSync } from "node:child_process";
19
+ import http from "node:http";
20
+ import https from "node:https";
19
21
 
20
22
  const JSON_MODE = process.argv.includes("--json");
21
23
  const QUIET_MODE = process.argv.includes("--quiet");
@@ -51,9 +53,31 @@ function authHeaders() {
51
53
  return { "content-type": "application/json", ...(t ? { authorization: `Bearer ${t}` } : {}) };
52
54
  }
53
55
 
56
+ function request(url, opts = {}) {
57
+ return new Promise((resolve, reject) => {
58
+ const parsed = new URL(url);
59
+ const transport = parsed.protocol === "https:" ? https : http;
60
+ const req = transport.request(parsed, {
61
+ method: opts.method || "GET",
62
+ headers: opts.headers || {},
63
+ }, (res) => {
64
+ let body = "";
65
+ res.setEncoding("utf8");
66
+ res.on("data", chunk => { body += chunk; });
67
+ res.on("end", () => resolve({ status: res.statusCode || 0, body, headers: res.headers }));
68
+ });
69
+
70
+ req.on("error", reject);
71
+ req.setTimeout(opts.timeout || 10000, () => req.destroy(new Error("timeout")));
72
+
73
+ if (opts.body) req.write(opts.body);
74
+ req.end();
75
+ });
76
+ }
77
+
54
78
  async function ping(url, label, opts = {}) {
55
79
  try {
56
- const res = await fetch(url, { signal: AbortSignal.timeout(opts.timeout || 10000), headers: authHeaders() });
80
+ const res = await request(url, { timeout: opts.timeout || 10000, headers: authHeaders() });
57
81
  return { ok: res.ok || res.status < 500, status: res.status };
58
82
  } catch (e) {
59
83
  return { ok: false, error: e.message };
@@ -84,7 +108,7 @@ async function run() {
84
108
  check("~/.crewswarm/crewswarm.json", hasSwarm ? "pass" : "fail", hasSwarm ? "" : "run: bash install.sh");
85
109
 
86
110
  const token = getToken();
87
- check("Auth token", token ? "pass" : "fail", token ? `${token.slice(0,8)}…` : "missing rt.authToken");
111
+ check("Auth token", token ? "pass" : "warn", token ? `${token.slice(0,8)}…` : "no rt.authToken (optional for local dev)");
88
112
 
89
113
  // ── 2. API keys ──────────────────────────────────────────────────────────────
90
114
  section("API Keys");
@@ -127,19 +151,32 @@ async function run() {
127
151
  if (!NO_SERVICES) {
128
152
  section("Agents");
129
153
  try {
130
- const res = await fetch(`${CREW_LEAD}/api/agents`, { headers: authHeaders(), signal: AbortSignal.timeout(5000) });
131
- const d = await res.json();
132
- const agents = d.agents || [];
133
- // crew-lead /api/agents uses liveness field; dashboard /api/agents uses online/alive/liveness
134
- const online = agents.filter(a => a.online || a.alive || a.liveness === "alive");
135
- const coreAgents = ["crew-coder","crew-qa","crew-pm","crew-main","crew-fixer"];
136
- check(`Agents online (${online.length}/${agents.length})`,
137
- online.length > 0 ? "pass" : "warn",
138
- online.length === 0 ? "bridges not started — run: npm run start-crew" : online.map(a => a.id?.replace("crew-","")).join(", ").slice(0,80));
139
- for (const core of coreAgents) {
140
- const a = agents.find(x => x.id === core);
141
- const isOnline = a?.online || a?.alive || a?.liveness === "alive";
142
- check(` ${core}`, isOnline ? "pass" : "warn", isOnline ? "" : "bridge not running");
154
+ // Try with auth first, fall back to no-auth for local dev
155
+ let res = await request(`${CREW_LEAD}/api/agents`, { headers: authHeaders(), timeout: 5000 });
156
+ if (res.status === 401) {
157
+ // No token or wrong token — try the dashboard proxy which may not require auth
158
+ try {
159
+ res = await request(`${DASHBOARD}/api/agents`, { timeout: 5000 });
160
+ } catch { /* dashboard proxy also failed, use original 401 response */ }
161
+ }
162
+ const d = JSON.parse(res.body || "{}");
163
+ // Dashboard returns array directly, crew-lead returns { agents: [...] }
164
+ const agents = Array.isArray(d) ? d : (d.agents || []);
165
+ if (agents.length === 0 && d.error) {
166
+ // Auth required but no valid token report agent count from bridge process list
167
+ const bridgeCount = (() => { try { return execSync("pgrep -f 'gateway-bridge.mjs' | wc -l", { encoding: "utf8", timeout: 2000 }).trim(); } catch { return "0"; } })();
168
+ check(`Agents`, bridgeCount > 0 ? "pass" : "warn", `${bridgeCount} bridge processes running (auth required for detailed status)`);
169
+ } else {
170
+ const online = agents.filter(a => a.online || a.alive || a.liveness === "online" || a.liveness === "alive");
171
+ const coreAgents = ["crew-coder","crew-qa","crew-pm","crew-main","crew-fixer"];
172
+ check(`Agents online (${online.length}/${agents.length})`,
173
+ online.length > 0 ? "pass" : "warn",
174
+ online.length === 0 ? "bridges not started — run: npm run start-crew" : online.map(a => a.id?.replace("crew-","")).join(", ").slice(0,80));
175
+ for (const core of coreAgents) {
176
+ const a = agents.find(x => x.id === core);
177
+ const isOnline = a?.online || a?.alive || a?.liveness === "online" || a?.liveness === "alive";
178
+ check(` ${core}`, isOnline ? "pass" : "warn", isOnline ? "" : "bridge not running");
179
+ }
143
180
  }
144
181
  } catch (e) {
145
182
  check("Agents", "fail", `could not reach crew-lead: ${e.message}`);
@@ -150,7 +187,7 @@ async function run() {
150
187
  section("CLI Tools");
151
188
  const [cursorCli, claudeCli, opencodeCli, nodeCli] = await Promise.all([
152
189
  Promise.resolve(cliCheck("cursor --version 2>/dev/null || cursor-cli --version 2>/dev/null", "cursor")),
153
- Promise.resolve(cliCheck("echo '' | timeout 3 claude --version 2>/dev/null || echo 'not found'", "claude")),
190
+ Promise.resolve(cliCheck("which claude 2>/dev/null && claude --version 2>/dev/null || echo 'not found'", "claude")),
154
191
  Promise.resolve(cliCheck("opencode --version 2>/dev/null || echo 'not found'", "opencode")),
155
192
  Promise.resolve(cliCheck("node --version", "node")),
156
193
  ]);
@@ -168,23 +205,23 @@ async function run() {
168
205
  if (!NO_SERVICES && mcpServer.ok) {
169
206
  section("MCP Protocol");
170
207
  try {
171
- const initRes = await fetch(`${MCP_URL}/mcp`, {
208
+ const initRes = await request(`${MCP_URL}/mcp`, {
172
209
  method: "POST",
173
210
  headers: { "content-type": "application/json" },
174
211
  body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", clientInfo: { name: "health-check", version: "1.0" } } }),
175
- signal: AbortSignal.timeout(4000),
212
+ timeout: 4000,
176
213
  });
177
- const initData = await initRes.json();
214
+ const initData = JSON.parse(initRes.body || "{}");
178
215
  check("MCP initialize", initData?.result?.serverInfo ? "pass" : "fail",
179
216
  initData?.result?.serverInfo?.name || JSON.stringify(initData).slice(0,60));
180
217
 
181
- const toolsRes = await fetch(`${MCP_URL}/mcp`, {
218
+ const toolsRes = await request(`${MCP_URL}/mcp`, {
182
219
  method: "POST",
183
220
  headers: { "content-type": "application/json" },
184
221
  body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
185
- signal: AbortSignal.timeout(4000),
222
+ timeout: 4000,
186
223
  });
187
- const toolsData = await toolsRes.json();
224
+ const toolsData = JSON.parse(toolsRes.body || "{}");
188
225
  const toolCount = toolsData?.result?.tools?.length || 0;
189
226
  check(`MCP tools/list (${toolCount} tools)`, toolCount >= 5 ? "pass" : "warn",
190
227
  toolsData?.result?.tools?.map(t => t.name).join(", ").slice(0, 80));
@@ -200,17 +237,22 @@ async function run() {
200
237
  } else {
201
238
  try {
202
239
  const start = Date.now();
203
- const res = await fetch(`${CREW_LEAD}/chat`, {
240
+ const res = await request(`${CREW_LEAD}/chat`, {
204
241
  method: "POST",
205
242
  headers: authHeaders(),
206
243
  body: JSON.stringify({ message: "say: HEALTH_OK", sessionId: "health-check" }),
207
- signal: AbortSignal.timeout(15000),
244
+ timeout: 15000,
208
245
  });
209
- const d = await res.json();
246
+ const d = JSON.parse(res.body || "{}");
210
247
  const elapsed = Date.now() - start;
211
- const reply = d.reply || d.message || "";
212
- check("crew-lead responds", reply.length > 0 ? "pass" : "fail",
213
- `${Math.round(elapsed/100)/10}s "${reply.slice(0,60)}"`);
248
+ if (res.status === 401) {
249
+ // Auth required — crew-lead is up but we can't chat without token
250
+ check("crew-lead responds", "pass", `up (auth required for chat test)`);
251
+ } else {
252
+ const reply = d.reply || d.message || "";
253
+ check("crew-lead responds", reply.length > 0 ? "pass" : "warn",
254
+ `${Math.round(elapsed/100)/10}s — "${reply.slice(0,60) || "(empty reply)"}"`);
255
+ }
214
256
  } catch (e) {
215
257
  check("crew-lead chat", "fail", e.message);
216
258
  }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # crewswarm Docker Installer — one-line setup for cloud VMs and dedicated servers
3
- # Usage: curl -fsSL https://raw.githubusercontent.com/your-org/crewswarm/main/scripts/install-docker.sh | bash
3
+ # Usage: curl -fsSL https://raw.githubusercontent.com/crewswarm/crewswarm/main/scripts/install-docker.sh | bash
4
4
 
5
5
  set -e
6
6
 
@@ -21,8 +21,9 @@ YELLOW='\033[0;33m'
21
21
  BOLD='\033[1m'
22
22
  RESET='\033[0m'
23
23
 
24
- # ── Service status tracking ──────────────────────────────────────────────────
25
- declare -A SVC_STATUS # "up" or "down"
24
+ # ── Service status tracking (bash 3.2 compatible — no associative arrays) ────
25
+ svc_set() { eval "SVC_$(echo "$1" | tr '-' '_')=$2"; }
26
+ svc_get() { eval "echo \${SVC_$(echo "$1" | tr '-' '_'):-unknown}"; }
26
27
 
27
28
  # ── Graceful kill: SIGTERM first, SIGKILL after 5s ────────────────────────────
28
29
  graceful_kill_pattern() {
@@ -71,14 +72,14 @@ wait_for_health() {
71
72
  while [ $attempt -lt $max_attempts ]; do
72
73
  if curl -s --max-time 2 "$url" > /dev/null 2>&1; then
73
74
  echo -e " ${GREEN}✓${RESET} $name is up"
74
- SVC_STATUS[$name]="up"
75
+ svc_set "$name" "up"
75
76
  return 0
76
77
  fi
77
78
  attempt=$((attempt + 1))
78
79
  sleep 1
79
80
  done
80
81
  echo -e " ${RED}✗${RESET} $name failed to start (tried ${max_attempts}s)"
81
- SVC_STATUS[$name]="down"
82
+ svc_set "$name" "down"
82
83
  return 1
83
84
  }
84
85
 
@@ -112,6 +113,8 @@ graceful_kill_pattern "telegram-bridge.mjs"
112
113
  graceful_kill_pattern "whatsapp-bridge.mjs"
113
114
  graceful_kill_pattern "opencode serve"
114
115
  graceful_kill_pattern "pm-loop.mjs"
116
+ graceful_kill_pattern "node --test" # orphaned test runners from dashboard
117
+ graceful_kill_pattern "npx playwright" # orphaned playwright runners
115
118
  graceful_kill_pattern "apps/vibe/server.mjs"
116
119
  graceful_kill_pattern "watch-server.mjs"
117
120
  graceful_kill_pattern "vite.*vibe"
@@ -151,11 +154,11 @@ OPENCODE_BIN="$(command -v opencode 2>/dev/null)" || OPENCODE_BIN="/usr/local/bi
151
154
  if [[ -x "$OPENCODE_BIN" ]]; then
152
155
  nohup "$OPENCODE_BIN" serve --port 4096 --hostname 127.0.0.1 >> /tmp/opencode.log 2>&1 &
153
156
  # Non-critical — don't block on health
154
- SVC_STATUS["opencode"]="up"
157
+ svc_set "opencode" "up"
155
158
  echo " Started (PID $!)"
156
159
  else
157
160
  echo " (opencode not found; skip)"
158
- SVC_STATUS["opencode"]="skip"
161
+ svc_set "opencode" "skip"
159
162
  fi
160
163
 
161
164
  # ── 2. RT daemon (port 18889) ────────────────────────────────────────────────
@@ -181,7 +184,7 @@ fi
181
184
  echo ""
182
185
  echo "Starting gateway bridges (crew-main, crew-pm, crew-coder, etc.)..."
183
186
  "$NODE" scripts/start-crew.mjs --force
184
- SVC_STATUS["agents"]="up"
187
+ svc_set "agents" "up"
185
188
  sleep 1
186
189
 
187
190
  # ── 4. crew-lead (port 5010) — CRITICAL ─────────────────────────────────────
@@ -206,27 +209,27 @@ if [[ "$START_DASH" -eq 1 ]]; then
206
209
  fi
207
210
  wait_for_health "http://127.0.0.1:4319/" "dashboard" 30 || true
208
211
  else
209
- SVC_STATUS["dashboard"]="skip"
212
+ svc_set "dashboard" "skip"
210
213
  fi
211
214
 
212
215
  # ── 6. Messaging bridges (Telegram + WhatsApp) ──────────────────────────────
213
216
  if [[ "$START_BRIDGES" -eq 1 ]]; then
214
217
  echo ""
215
218
  # Verify RT bus is still up before starting bridges
216
- if [[ "${SVC_STATUS[rt-bus]:-down}" == "up" ]]; then
219
+ if [[ "$(svc_get rt-bus)" == "up" ]]; then
217
220
  echo "Starting Telegram bridge..."
218
221
  nohup "$NODE" "$REPO_DIR/telegram-bridge.mjs" >> /tmp/telegram-bridge.log 2>&1 &
219
- SVC_STATUS["telegram"]="up"
222
+ svc_set "telegram" "up"
220
223
  echo " PID: $!"
221
224
 
222
225
  echo "Starting WhatsApp bridge..."
223
226
  nohup "$NODE" "$REPO_DIR/whatsapp-bridge.mjs" >> /tmp/whatsapp-bridge.log 2>&1 &
224
- SVC_STATUS["whatsapp"]="up"
227
+ svc_set "whatsapp" "up"
225
228
  echo " PID: $!"
226
229
  else
227
230
  echo -e " ${YELLOW}Skipping messaging bridges — RT bus is not up${RESET}"
228
- SVC_STATUS["telegram"]="skip"
229
- SVC_STATUS["whatsapp"]="skip"
231
+ svc_set "telegram" "skip"
232
+ svc_set "whatsapp" "skip"
230
233
  fi
231
234
  fi
232
235
 
@@ -238,7 +241,7 @@ if ! lsof -ti :5020 >/dev/null 2>&1; then
238
241
  wait_for_health "http://127.0.0.1:5020/health" "mcp-server" 15 || true
239
242
  else
240
243
  echo " (already running on :5020)"
241
- SVC_STATUS["mcp-server"]="up"
244
+ svc_set "mcp-server" "up"
242
245
  fi
243
246
 
244
247
  # ── 8. Vibe + file watcher (ports 3333, 3334) ───────────────────────────────
@@ -257,18 +260,18 @@ if [[ "$START_STUDIO" -eq 1 ]]; then
257
260
  for i in $(seq 1 10); do
258
261
  if lsof -ti :3334 >/dev/null 2>&1; then
259
262
  echo -e " ${GREEN}✓${RESET} file watcher is up"
260
- SVC_STATUS["file-watcher"]="up"
263
+ svc_set "file-watcher" "up"
261
264
  break
262
265
  fi
263
266
  if [[ "$i" -eq 10 ]]; then
264
267
  echo -e " ${YELLOW}!${RESET} file watcher not detected on :3334"
265
- SVC_STATUS["file-watcher"]="down"
268
+ svc_set "file-watcher" "down"
266
269
  fi
267
270
  sleep 1
268
271
  done
269
272
  else
270
- SVC_STATUS["vibe-studio"]="skip"
271
- SVC_STATUS["file-watcher"]="skip"
273
+ svc_set "vibe-studio" "skip"
274
+ svc_set "file-watcher" "skip"
272
275
  fi
273
276
 
274
277
  # ══════════════════════════════════════════════════════════════════════════════
@@ -280,7 +283,7 @@ echo -e "${BOLD}━━━ Service Summary ━━━${RESET}"
280
283
  print_status() {
281
284
  local name="$1"
282
285
  local port="$2"
283
- local status="${SVC_STATUS[$name]:-unknown}"
286
+ local status="$(svc_get "$name")"
284
287
  case "$status" in
285
288
  up) echo -e " ${GREEN}✓${RESET} $name :$port" ;;
286
289
  down) echo -e " ${RED}✗${RESET} $name :$port" ;;
@@ -309,12 +312,13 @@ echo "Logs: /tmp/opencode.log /tmp/opencrew-rt-daemon.log /tmp/crew-lead.log /tm
309
312
  if [[ -f "$REPO_DIR/scripts/health-check.mjs" ]]; then
310
313
  echo ""
311
314
  echo -e "${BOLD}Running health check...${RESET}"
315
+ sleep 2 # let services fully warm up
312
316
  "$NODE" "$REPO_DIR/scripts/health-check.mjs" --quiet 2>/dev/null || true
313
317
  fi
314
318
 
315
319
  # ── Exit code: 0 if critical services are up, 1 otherwise ────────────────────
316
- CREW_LEAD_OK="${SVC_STATUS[crew-lead]:-down}"
317
- DASHBOARD_OK="${SVC_STATUS[dashboard]:-down}"
320
+ CREW_LEAD_OK="$(svc_get crew-lead)"
321
+ DASHBOARD_OK="$(svc_get dashboard)"
318
322
 
319
323
  if [[ "$CREW_LEAD_OK" == "up" ]] && { [[ "$DASHBOARD_OK" == "up" ]] || [[ "$START_DASH" -eq 0 ]]; }; then
320
324
  echo ""
package/scripts/start.mjs CHANGED
@@ -2,24 +2,29 @@
2
2
  /**
3
3
  * scripts/start.mjs — CrewSwarm first-run entry point
4
4
  *
5
- * This is the script behind `npm start`. It validates the environment before
6
- * handing off to the real stack so that a brand-new user who clones the repo
7
- * and types `npm start` gets clear, actionable guidance rather than a stack
8
- * trace.
5
+ * This is the script behind `npm start` / `npx crewswarm`. It validates the
6
+ * environment before handing off to the real stack so that a brand-new user
7
+ * who clones the repo and types `npm start` gets clear, actionable guidance
8
+ * rather than a stack trace.
9
9
  *
10
10
  * Checks performed (in order):
11
11
  * 1. Node.js version >= 20
12
12
  * 2. ~/.crewswarm/crewswarm.json exists (created by install.sh)
13
- * 3. ~/.crewswarm/crewswarm.json exists (created by install.sh)
13
+ * 3. ~/.crewswarm/config.json exists (created by install.sh)
14
14
  * 4. At least one provider with an apiKey is configured
15
15
  *
16
- * On success, delegates to `npm run dashboard` (the standard start target).
16
+ * On success:
17
+ * 1. Spawns start-crew.mjs in the background (RT bus + crew-lead + bridges)
18
+ * 2. Waits 3 seconds for services to come up
19
+ * 3. Starts dashboard in the foreground (the process the user sees)
20
+ *
21
+ * NOTE: `npm run dashboard` still starts only the dashboard (unchanged).
17
22
  */
18
23
 
19
24
  import fs from "node:fs";
20
25
  import path from "node:path";
21
26
  import os from "node:os";
22
- import { spawnSync } from "node:child_process";
27
+ import { spawn, spawnSync } from "node:child_process";
23
28
  import { fileURLToPath } from "node:url";
24
29
 
25
30
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -63,43 +68,53 @@ success(`Node.js v${process.versions.node}`);
63
68
  // ── 2. Config directory ───────────────────────────────────────────────────────
64
69
  const CREWSWARM_DIR = path.join(os.homedir(), ".crewswarm");
65
70
  const SWARM_CFG = path.join(CREWSWARM_DIR, "crewswarm.json");
66
- const SYS_CFG = path.join(CREWSWARM_DIR, "crewswarm.json");
71
+ const SYS_CFG = path.join(CREWSWARM_DIR, "config.json");
67
72
  const INSTALL_SH = path.join(ROOT, "install.sh");
68
73
 
69
74
  function tryReadJSON(p) {
70
75
  try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return null; }
71
76
  }
72
77
 
78
+ // Bootstrap config directory + minimal config if missing — the dashboard's
79
+ // setup wizard will guide the user through API key entry on first visit.
80
+ let firstRun = false;
73
81
  if (!fs.existsSync(CREWSWARM_DIR)) {
74
- fatal(
75
- `Config directory not found: ${CREWSWARM_DIR}`,
76
- `Run the installer first:\n\n bash ${INSTALL_SH}\n\n` +
77
- `Or, for a one-liner from the web:\n\n` +
78
- ` bash <(curl -fsSL https://raw.githubusercontent.com/crewswarm/crewswarm/main/install.sh)`
79
- );
82
+ fs.mkdirSync(CREWSWARM_DIR, { recursive: true });
83
+ info(`Created config directory: ${CREWSWARM_DIR}`);
84
+ firstRun = true;
80
85
  }
81
86
 
82
87
  // ── 3. crewswarm.json ─────────────────────────────────────────────────────────
83
88
  if (!fs.existsSync(SWARM_CFG)) {
84
- fatal(
85
- `Agent config not found: ${SWARM_CFG}`,
86
- `Run the installer to create it:\n\n bash ${INSTALL_SH}`
87
- );
89
+ const defaultConfig = {
90
+ agents: [
91
+ { id: "crew-lead", model: "groq/llama-3.3-70b-versatile" },
92
+ { id: "crew-main", model: "groq/llama-3.3-70b-versatile" },
93
+ { id: "crew-coder", model: "groq/llama-3.3-70b-versatile" },
94
+ { id: "crew-qa", model: "groq/llama-3.3-70b-versatile" },
95
+ { id: "crew-fixer", model: "groq/llama-3.3-70b-versatile" },
96
+ { id: "crew-pm", model: "groq/llama-3.3-70b-versatile" }
97
+ ],
98
+ providers: {}
99
+ };
100
+ fs.writeFileSync(SWARM_CFG, JSON.stringify(defaultConfig, null, 2));
101
+ info(`Created default ${SWARM_CFG} — add API keys in the dashboard setup wizard`);
102
+ firstRun = true;
88
103
  }
89
- const swarm = tryReadJSON(SWARM_CFG);
104
+ let swarm = tryReadJSON(SWARM_CFG);
90
105
  if (!swarm) {
91
106
  fatal(
92
107
  `Cannot parse ${SWARM_CFG} — file may be corrupt.`,
93
108
  `Check it is valid JSON, or re-run:\n\n bash ${INSTALL_SH}`
94
109
  );
95
110
  }
96
- success(`crewswarm.json found`);
111
+ success(`crewswarm.json found${firstRun ? " (first run — setup wizard will launch)" : ""}`);
97
112
 
98
113
  // ── 4. config.json ───────────────────────────────────────────────────────────
99
114
  if (!fs.existsSync(SYS_CFG)) {
100
- warn(`System config not found: ${SYS_CFG}`);
101
- warn(`Some features (RT bus auth, background consciousness) will be disabled.`);
102
- warn(`To configure them, run: bash ${INSTALL_SH}`);
115
+ const defaultSysCfg = { rt: { authToken: `local-${Date.now().toString(36)}` } };
116
+ fs.writeFileSync(SYS_CFG, JSON.stringify(defaultSysCfg, null, 2));
117
+ info(`Created default ${SYS_CFG}`);
103
118
  } else {
104
119
  const sys = tryReadJSON(SYS_CFG);
105
120
  if (!sys) {
@@ -109,6 +124,16 @@ if (!fs.existsSync(SYS_CFG)) {
109
124
  }
110
125
  }
111
126
 
127
+ // ── Bootstrap supporting files if missing ────────────────────────────────────
128
+ const CMD_ALLOWLIST = path.join(CREWSWARM_DIR, "cmd-allowlist.json");
129
+ const AGENT_PROMPTS = path.join(CREWSWARM_DIR, "agent-prompts.json");
130
+ if (!fs.existsSync(CMD_ALLOWLIST)) {
131
+ fs.writeFileSync(CMD_ALLOWLIST, '["npm *","node *","npx *","git *"]');
132
+ }
133
+ if (!fs.existsSync(AGENT_PROMPTS)) {
134
+ fs.writeFileSync(AGENT_PROMPTS, "{}");
135
+ }
136
+
112
137
  // ── 5. Provider check ────────────────────────────────────────────────────────
113
138
  const providers = swarm.providers || {};
114
139
  const configured = Object.entries(providers).filter(([, v]) => v?.apiKey && String(v.apiKey).trim().length > 0 && !String(v.apiKey).startsWith("your-"));
@@ -130,14 +155,44 @@ if (agents.length === 0) {
130
155
  }
131
156
  info(`${agents.length} agent(s) defined`);
132
157
 
133
- // ── 7. Hand off to dashboard ──────────────────────────────────────────────────
158
+ // ── 7. Start full stack ───────────────────────────────────────────────────────
159
+ console.log("");
160
+ divider();
161
+ info("All checks passed — starting full CrewSwarm stack");
162
+ divider();
163
+ console.log("");
164
+
165
+ // Step 1: Spawn start-crew.mjs in the background (RT bus + crew-lead + bridges)
166
+ info("Launching RT bus, crew-lead, and gateway bridges…");
167
+ const crewScript = path.join(ROOT, "scripts", "start-crew.mjs");
168
+ const crewProc = spawn(process.execPath, [crewScript], {
169
+ cwd: ROOT,
170
+ stdio: "inherit",
171
+ detached: false,
172
+ env: process.env,
173
+ });
174
+
175
+ // Wait for start-crew.mjs to finish its synchronous setup (it exits after spawning daemons)
176
+ await new Promise((resolve) => {
177
+ crewProc.on("exit", resolve);
178
+ crewProc.on("error", (err) => {
179
+ warn(`start-crew.mjs exited with error: ${err.message}`);
180
+ resolve();
181
+ });
182
+ });
183
+
184
+ // Step 2: Give the background daemons a moment to bind their ports
185
+ info("Waiting 3 s for services to come up…");
186
+ await new Promise((resolve) => setTimeout(resolve, 3000));
187
+
188
+ // Step 3: Start dashboard in the foreground (what the user sees)
134
189
  console.log("");
135
190
  divider();
136
- info("All checks passed launching dashboard on http://127.0.0.1:4319");
191
+ console.log(bold(" All services started. Dashboard http://127.0.0.1:4319"));
137
192
  divider();
138
193
  console.log("");
139
194
 
140
- const result = spawnSync("node", [path.join(ROOT, "scripts", "dashboard.mjs")], {
195
+ const result = spawnSync(process.execPath, [path.join(ROOT, "scripts", "dashboard.mjs")], {
141
196
  cwd: ROOT,
142
197
  stdio: "inherit",
143
198
  env: process.env,