@lifeaitools/clauth 1.5.78 → 1.5.81

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.
@@ -17,10 +17,10 @@ import ora from "ora";
17
17
  import { execSync as execSyncTop } from "child_process";
18
18
  import Conf from "conf";
19
19
  import { getConfOptions } from "../conf-path.js";
20
- import { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
21
- import fg from "fast-glob";
22
- import { rgPath } from "@vscode/ripgrep";
23
- import { createStudioDebugRuntime } from "../studio-debug.js";
20
+ import { readdir, readFile, writeFile, rm, mkdir, stat, rename } from "node:fs/promises";
21
+ import fg from "fast-glob";
22
+ import { rgPath } from "@vscode/ripgrep";
23
+ import { createStudioDebugRuntime } from "../studio-debug.js";
24
24
 
25
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
26
26
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
@@ -564,6 +564,9 @@ function dashboardHtml(port, whitelist, isStaged = false) {
564
564
  .tunnel-url{font-family:'Courier New',monospace;font-size:.78rem;color:#60a5fa;word-break:break-all}
565
565
  .tunnel-url a{color:#60a5fa;text-decoration:none}
566
566
  .tunnel-url a:hover{text-decoration:underline}
567
+ .btn-ccandme{background:linear-gradient(135deg,#1a1a2e,#16213e);color:#a78bfa;border:1px solid #4c1d95;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:600;transition:all .15s;white-space:nowrap}
568
+ .btn-ccandme:hover{background:linear-gradient(135deg,#2d1b69,#1e1b4b);border-color:#7c3aed;color:#c4b5fd;transform:translateY(-1px)}
569
+ .btn-ccandme:disabled{opacity:.5;cursor:not-allowed;transform:none}
567
570
  .btn-claude{background:linear-gradient(135deg,#d97706,#f59e0b);color:#0a0f1a;padding:8px 18px;font-size:.85rem;border-radius:7px;border:none;cursor:pointer;font-weight:700;letter-spacing:.3px;transition:all .15s;white-space:nowrap}
568
571
  .btn-claude:hover{filter:brightness(1.1);transform:translateY(-1px)}
569
572
  .btn-claude:disabled{opacity:.4;cursor:not-allowed;transform:none;filter:none}
@@ -743,6 +746,7 @@ function dashboardHtml(port, whitelist, isStaged = false) {
743
746
  <button class="btn-refresh" onclick="loadServices()">↻ Refresh</button>
744
747
  <button class="btn-add" onclick="toggleAddService()">+ Add Service</button>
745
748
  <button class="btn-check" id="check-btn" onclick="checkAll()">⬤ Check All</button>
749
+ <button class="btn-ccandme" id="ccandme-btn" onclick="launchCCandMe()" title="Launch CCandMe — 4-pane WezTerm: Claude + Codex + Me">⚡ CCandMe</button>
746
750
  <button class="btn-lock" onclick="lockVault()">🔒 Lock</button>
747
751
  <button class="btn-stop" onclick="restartDaemon()" style="background:#1a2e1a;border:1px solid #166534;color:#86efac" title="Restart daemon — keeps vault unlocked">↺ Restart</button>
748
752
  <button class="btn-stop" onclick="stopDaemon()" style="background:#7f1d1d;border:1px solid #991b1b;color:#fca5a5" title="Stop daemon — password required on next start">⏹ Stop</button>
@@ -1115,6 +1119,24 @@ async function makeLive() {
1115
1119
  }
1116
1120
  }
1117
1121
 
1122
+ // ── Launch CCandMe (4-pane WezTerm: Claude + Codex + Me) ──
1123
+ async function launchCCandMe() {
1124
+ const btn = document.getElementById("ccandme-btn");
1125
+ if (btn) { btn.disabled = true; btn.textContent = "⚡ Launching…"; }
1126
+ try {
1127
+ const r = await fetch(BASE + "/launch-ccandme", { method: "POST" }).then(r => r.json()).catch(() => ({}));
1128
+ if (r.ok) {
1129
+ if (btn) { btn.textContent = "⚡ Launched!"; setTimeout(() => { btn.disabled = false; btn.textContent = "⚡ CCandMe"; }, 3000); }
1130
+ } else {
1131
+ alert("CCandMe launch failed: " + (r.error || "unknown error"));
1132
+ if (btn) { btn.disabled = false; btn.textContent = "⚡ CCandMe"; }
1133
+ }
1134
+ } catch (err) {
1135
+ alert("CCandMe launch failed: " + err.message);
1136
+ if (btn) { btn.disabled = false; btn.textContent = "⚡ CCandMe"; }
1137
+ }
1138
+ }
1139
+
1118
1140
  // ── Restart daemon (keeps boot.key — vault stays unlocked) ──
1119
1141
  async function restartDaemon() {
1120
1142
  if (!confirm("Restart the daemon?\\n\\nThe vault will stay unlocked (boot.key kept).")) return;
@@ -2406,7 +2428,9 @@ function readBody(req) {
2406
2428
  }
2407
2429
 
2408
2430
  // ── Server logic (shared by foreground + daemon) ─────────────
2409
- function createServer(initPassword, whitelist, port, tunnelHostnameInit = null, isStaged = false) {
2431
+ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null, isStaged = false) {
2432
+ mcpHttpBaseUrl = `http://127.0.0.1:${port}`;
2433
+
2410
2434
  // tunnelHostname may be updated at runtime (fetched from DB after unlock)
2411
2435
  let tunnelHostname = tunnelHostnameInit;
2412
2436
 
@@ -2436,12 +2460,12 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2436
2460
  // Rotation engine — starts after unlock
2437
2461
  const rotationEngine = createRotationEngine(initPassword, machineHash, LOG_FILE);
2438
2462
 
2439
- const CORS = {
2463
+ const CORS = {
2440
2464
  "Access-Control-Allow-Origin": "*",
2441
2465
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
2442
2466
  "Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version, mcp-session-id",
2443
- };
2444
- const studioDebugRuntime = createStudioDebugRuntime({ port, logFile: LOG_FILE });
2467
+ };
2468
+ const studioDebugRuntime = createStudioDebugRuntime({ port, logFile: LOG_FILE });
2445
2469
 
2446
2470
  // ── MCP SSE session tracking ──────────────────────────────
2447
2471
  const sseSessions = new Map(); // sessionId → { res, initialized }
@@ -2842,14 +2866,14 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2842
2866
  return strike(res, 403, `Rejected non-local address: ${remote}`);
2843
2867
  }
2844
2868
 
2845
- // CORS preflight
2846
- if (req.method === "OPTIONS") {
2847
- res.writeHead(204, CORS);
2848
- return res.end();
2849
- }
2850
-
2851
- const studioDebugHandled = await studioDebugRuntime.handle(req, res, url, CORS);
2852
- if (studioDebugHandled !== false) return;
2869
+ // CORS preflight
2870
+ if (req.method === "OPTIONS") {
2871
+ res.writeHead(204, CORS);
2872
+ return res.end();
2873
+ }
2874
+
2875
+ const studioDebugHandled = await studioDebugRuntime.handle(req, res, url, CORS);
2876
+ if (studioDebugHandled !== false) return;
2853
2877
 
2854
2878
  // ── Hosts that bypass OAuth (fresh domains for claude.ai compatibility) ──
2855
2879
  const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com", "chitchat.regendevcorp.com"];
@@ -3047,13 +3071,14 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3047
3071
  }
3048
3072
 
3049
3073
  // ── MCP path helpers ──
3050
- const MCP_PATHS = ["/mcp", "/gws", "/clauth", "/fs", "/chitchat"];
3074
+ const MCP_PATHS = ["/mcp", "/gws", "/clauth", "/fs", "/chitchat", "/codevelop"];
3051
3075
  const isMcpPath = MCP_PATHS.includes(reqPath);
3052
3076
  function toolsForPath(p) {
3053
3077
  if (p === "/gws") return MCP_TOOLS.filter(t => t.name.startsWith("gws_"));
3054
3078
  if (p === "/clauth") return MCP_TOOLS.filter(t => t.name.startsWith("clauth_") || t.name === "monkey_dispatch" || t.name.startsWith("terminal_") || t.name.startsWith("channel_"));
3055
3079
  if (p === "/fs") return MCP_TOOLS.filter(t => t.name.startsWith("fs_"));
3056
3080
  if (p === "/chitchat") return MCP_TOOLS.filter(t => t.name.startsWith("chitchat_"));
3081
+ if (p === "/codevelop") return MCP_TOOLS.filter(t => t.name.startsWith("codevelop_"));
3057
3082
  return MCP_TOOLS; // /mcp — all tools
3058
3083
  }
3059
3084
  function serverNameForPath(p) {
@@ -3061,6 +3086,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3061
3086
  if (p === "/clauth") return "clauth";
3062
3087
  if (p === "/fs") return "fs";
3063
3088
  if (p === "/chitchat") return "chitchat";
3089
+ if (p === "/codevelop") return "codevelop";
3064
3090
  return "clauth";
3065
3091
  }
3066
3092
 
@@ -3325,6 +3351,122 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3325
3351
  });
3326
3352
  }
3327
3353
 
3354
+ // ── Co-development transport — peer-aware Claude/Codex sessions ─────
3355
+ // This is intentionally separate from /chitchat so existing relay behavior
3356
+ // cannot leak into terminal peer routing.
3357
+ if (method === "POST" && reqPath === "/codevelop/start") {
3358
+ let body;
3359
+ try { body = await readBody(req); } catch {
3360
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3361
+ return res.end(JSON.stringify({ error: "Invalid JSON" }));
3362
+ }
3363
+ const result = startCodevelopSession(body || {});
3364
+ return ok(res, result);
3365
+ }
3366
+
3367
+ if (method === "POST" && reqPath === "/codevelop/join") {
3368
+ let body;
3369
+ try { body = await readBody(req); } catch {
3370
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3371
+ return res.end(JSON.stringify({ error: "Invalid JSON" }));
3372
+ }
3373
+ const result = joinCodevelopSession(body || {});
3374
+ if (result.error) {
3375
+ res.writeHead(result.error === "not_found" ? 404 : 400, { "Content-Type": "application/json", ...CORS });
3376
+ return res.end(JSON.stringify(result));
3377
+ }
3378
+ return ok(res, result);
3379
+ }
3380
+
3381
+ if (method === "POST" && reqPath === "/codevelop/send") {
3382
+ let body;
3383
+ try { body = await readBody(req); } catch {
3384
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3385
+ return res.end(JSON.stringify({ error: "Invalid JSON" }));
3386
+ }
3387
+ const result = sendCodevelopMessage(body || {});
3388
+ if (result.error) {
3389
+ res.writeHead(result.error === "not_found" ? 404 : 400, { "Content-Type": "application/json", ...CORS });
3390
+ return res.end(JSON.stringify(result));
3391
+ }
3392
+ return ok(res, result);
3393
+ }
3394
+
3395
+ if (method === "POST" && reqPath === "/codevelop/poll") {
3396
+ let body;
3397
+ try { body = await readBody(req); } catch {
3398
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3399
+ return res.end(JSON.stringify({ error: "Invalid JSON" }));
3400
+ }
3401
+ const result = pollCodevelopMessages(body || {});
3402
+ if (result.error) {
3403
+ res.writeHead(result.error === "not_found" ? 404 : 400, { "Content-Type": "application/json", ...CORS });
3404
+ return res.end(JSON.stringify(result));
3405
+ }
3406
+ return ok(res, result);
3407
+ }
3408
+
3409
+ if (method === "GET" && reqPath === "/codevelop") {
3410
+ return ok(res, listCodevelopSessions());
3411
+ }
3412
+
3413
+ const codevelopStatusMatch = reqPath.match(/^\/codevelop\/([^/]+)\/status$/);
3414
+ if (method === "GET" && codevelopStatusMatch) {
3415
+ const result = statusCodevelopSession(codevelopStatusMatch[1]);
3416
+ if (result.error) {
3417
+ res.writeHead(404, { "Content-Type": "application/json", ...CORS });
3418
+ return res.end(JSON.stringify(result));
3419
+ }
3420
+ return ok(res, result);
3421
+ }
3422
+
3423
+ const codevelopStreamMatch = reqPath.match(/^\/codevelop\/([^/]+)\/([^/]+)\/stream$/);
3424
+ if (method === "GET" && codevelopStreamMatch) {
3425
+ const session_id = decodeURIComponent(codevelopStreamMatch[1]);
3426
+ const peer_id = decodeURIComponent(codevelopStreamMatch[2]);
3427
+ const result = attachCodevelopStream(session_id, peer_id, res);
3428
+ if (result.error) {
3429
+ res.writeHead(result.error === "not_found" ? 404 : 400, { "Content-Type": "application/json", ...CORS });
3430
+ return res.end(JSON.stringify(result));
3431
+ }
3432
+
3433
+ res.writeHead(200, {
3434
+ "Content-Type": "text/event-stream",
3435
+ "Cache-Control": "no-cache",
3436
+ "Connection": "keep-alive",
3437
+ ...CORS,
3438
+ });
3439
+
3440
+ for (const entry of result.queued) {
3441
+ res.write(`event: message\ndata: ${JSON.stringify(entry)}\n\n`);
3442
+ }
3443
+
3444
+ const keepalive = setInterval(() => {
3445
+ try { res.write(": keepalive\n\n"); } catch {}
3446
+ }, 15_000);
3447
+
3448
+ req.on("close", () => {
3449
+ clearInterval(keepalive);
3450
+ detachCodevelopStream(session_id, peer_id, res);
3451
+ });
3452
+
3453
+ return;
3454
+ }
3455
+
3456
+ if (method === "POST" && reqPath === "/codevelop/stop") {
3457
+ let body;
3458
+ try { body = await readBody(req); } catch {
3459
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3460
+ return res.end(JSON.stringify({ error: "Invalid JSON" }));
3461
+ }
3462
+ const result = stopCodevelopSession(body?.session_id);
3463
+ if (result.error) {
3464
+ res.writeHead(404, { "Content-Type": "application/json", ...CORS });
3465
+ return res.end(JSON.stringify(result));
3466
+ }
3467
+ return ok(res, result);
3468
+ }
3469
+
3328
3470
  // GET /chitchat/<session_id>/stream — SSE push stream for Claude Code (inbox events)
3329
3471
  // Claude Code connects once; daemon pushes an event on every chitchat_send call
3330
3472
  const inboxStreamMatch = reqPath.match(/^\/chitchat\/([^/]+)\/stream$/);
@@ -3545,6 +3687,52 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3545
3687
  return ok(res, { status: tunnelStatus });
3546
3688
  }
3547
3689
 
3690
+ // POST /launch-ccandme — open a new CCandMe WezTerm window without killing existing sessions
3691
+ if (method === "POST" && reqPath === "/launch-ccandme") {
3692
+ if (lockedGuard(res)) return;
3693
+ try {
3694
+ const { spawn } = await import("child_process");
3695
+ const { existsSync, readFileSync, writeFileSync } = await import("fs");
3696
+
3697
+ // Locate WezTerm
3698
+ const wezCandidates = [
3699
+ "C:/Program Files/WezTerm/wezterm.exe",
3700
+ "C:/Program Files (x86)/WezTerm/wezterm.exe",
3701
+ process.env.WEZTERM_EXE,
3702
+ ].filter(Boolean);
3703
+ const wezExe = wezCandidates.find(existsSync) || "wezterm";
3704
+
3705
+ // Render the lua config from the CCandMe template (skip the kill-existing step)
3706
+ const CCANDME_DIR = "C:/Dev/CCandMe";
3707
+ const WORK_DIR = "C:/Dev/regen-root";
3708
+ const templatePath = path.join(CCANDME_DIR, "templates", "wezterm.lua");
3709
+ const luaOutPath = path.join(CCANDME_DIR, ".ccandme-wezterm.lua");
3710
+
3711
+ if (existsSync(templatePath)) {
3712
+ const lua = readFileSync(templatePath, "utf8")
3713
+ .replaceAll("__SUPERVISOR_DIR__", CCANDME_DIR.replace(/\//g, "\\\\"))
3714
+ .replaceAll("__WORK_DIR__", WORK_DIR.replace(/\//g, "\\\\"))
3715
+ .replaceAll("__WORKSPACE__", "ccandme")
3716
+ .replaceAll("__CLAUDE_CMD__", "claude")
3717
+ .replaceAll("__CODEX_CMD__", "codex")
3718
+ .replaceAll("__PACKAGE_ROOT__", CCANDME_DIR.replace(/\//g, "\\\\"));
3719
+ writeFileSync(luaOutPath, lua, "utf8");
3720
+ }
3721
+
3722
+ // Launch WezTerm with the CCandMe config — no kill of existing sessions
3723
+ const luaArg = existsSync(luaOutPath) ? luaOutPath : path.join(CCANDME_DIR, ".ccandme-wezterm.lua");
3724
+ const child = spawn(wezExe, ["--config-file", luaArg, "start"], {
3725
+ detached: true,
3726
+ stdio: "ignore",
3727
+ });
3728
+ child.unref();
3729
+ return ok(res, { ok: true, message: "CCandMe launched" });
3730
+ } catch (err) {
3731
+ res.writeHead(500, { "Content-Type": "application/json", ...CORS });
3732
+ return res.end(JSON.stringify({ error: err.message }));
3733
+ }
3734
+ }
3735
+
3548
3736
  // POST /restart — spawn fresh process then exit (keeps boot.key, vault stays unlocked)
3549
3737
  if (method === "POST" && reqPath === "/restart") {
3550
3738
  ok(res, { ok: true, message: "restarting" });
@@ -4970,6 +5158,10 @@ async function verifyAuth(password) {
4970
5158
  }
4971
5159
 
4972
5160
  async function actionStart(opts) {
5161
+ if (opts.isolated) {
5162
+ return actionForeground(opts);
5163
+ }
5164
+
4973
5165
  const isStaged = !!opts.staged || process.env.__CLAUTH_STAGED === "1";
4974
5166
  const port = isStaged ? STAGED_PORT : parseInt(opts.port || String(LIVE_PORT), 10);
4975
5167
  let password = opts.pw || null;
@@ -5220,12 +5412,18 @@ async function actionRestart(opts) {
5220
5412
 
5221
5413
  async function actionForeground(opts) {
5222
5414
  const port = parseInt(opts.port || "52437", 10);
5223
- const password = opts.pw || null;
5415
+ const isolated = !!opts.isolated;
5416
+ const password = isolated ? null : (opts.pw || null);
5224
5417
  const tunnelHostname = opts.tunnel || null;
5225
5418
  const whitelist = opts.services
5226
5419
  ? opts.services.split(",").map(s => s.trim().toLowerCase())
5227
5420
  : null;
5228
5421
 
5422
+ if (isolated && (port === LIVE_PORT || port === STAGED_PORT)) {
5423
+ console.log(chalk.red(`\n Refusing isolated mode on reserved port ${port}. Use a separate port such as 53137.\n`));
5424
+ process.exit(1);
5425
+ }
5426
+
5229
5427
  if (password) {
5230
5428
  console.log(chalk.gray("\n Verifying vault credentials..."));
5231
5429
  try {
@@ -5235,6 +5433,8 @@ async function actionForeground(opts) {
5235
5433
  process.exit(1);
5236
5434
  }
5237
5435
  console.log(chalk.green(" ✓ Vault auth verified"));
5436
+ } else if (isolated) {
5437
+ console.log(chalk.yellow("\n Starting isolated passwordless server — credential routes remain locked"));
5238
5438
  } else {
5239
5439
  console.log(chalk.yellow("\n Starting in locked state — open browser to unlock"));
5240
5440
  }
@@ -5245,7 +5445,7 @@ async function actionForeground(opts) {
5245
5445
 
5246
5446
  const server = createServer(password, whitelist, port, tunnelHostname);
5247
5447
  server.listen(port, "127.0.0.1", () => {
5248
- writePid(process.pid, port);
5448
+ if (!isolated) writePid(process.pid, port);
5249
5449
  console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
5250
5450
  if (tunnelHostname) {
5251
5451
  console.log(chalk.cyan(` Tunnel: https://${tunnelHostname}/sse`));
@@ -5256,10 +5456,9 @@ async function actionForeground(opts) {
5256
5456
  console.log(chalk.white(` Client Secret: ${server.__oauthClientSecret}`));
5257
5457
  console.log(chalk.gray(" (paste these into Advanced Settings when adding the connector)"));
5258
5458
  }
5259
- if (!password) console.log(chalk.cyan(` 👉 Open http://127.0.0.1:${port} to unlock`));
5459
+ if (!password && !isolated) console.log(chalk.cyan(` 👉 Open http://127.0.0.1:${port} to unlock`));
5260
5460
  console.log(chalk.gray(" Ctrl+C to stop\n"));
5261
- // Auto-open browser
5262
- openBrowser(`http://127.0.0.1:${port}`);
5461
+ if (!isolated) openBrowser(`http://127.0.0.1:${port}`);
5263
5462
  });
5264
5463
 
5265
5464
  server.on("error", err => {
@@ -5273,7 +5472,7 @@ async function actionForeground(opts) {
5273
5472
 
5274
5473
  process.on("SIGINT", () => {
5275
5474
  console.log(chalk.yellow("\n Stopping clauth serve...\n"));
5276
- removePid();
5475
+ if (!isolated) removePid();
5277
5476
  server.close(() => process.exit(0));
5278
5477
  });
5279
5478
  }
@@ -5344,6 +5543,7 @@ function spawnClaudeTask(prompt, jobId, cwd) {
5344
5543
  // /terminal/send spawns a fresh claude -p with [context + message],
5345
5544
  // captures stdout, stores result back in session context.
5346
5545
  const terminalSessions = new Map(); // session_id → SessionState
5546
+ let mcpHttpBaseUrl = "http://127.0.0.1:52437";
5347
5547
 
5348
5548
  // ── Channel webhook event queue ───────────────────────────────────────────────
5349
5549
  const channelEvents = []; // { id, event, resource, status, url, repository, created_at }
@@ -5643,6 +5843,249 @@ function stopChitchatSession(session_id) {
5643
5843
  return { stopped: true, session_id };
5644
5844
  }
5645
5845
 
5846
+ // ── Co-development — peer-aware Claude/Codex terminal relay ───────────────
5847
+ //
5848
+ // This route is intentionally not a chitchat migration path. It is a separate
5849
+ // transport with addressed peer inboxes and a stable JSON envelope.
5850
+
5851
+ function normalizePeerId(peer) {
5852
+ return String(peer || "").trim().toLowerCase();
5853
+ }
5854
+
5855
+ function serializeCodevelopSession(session) {
5856
+ const peers = {};
5857
+ for (const [peer_id, peer] of Object.entries(session.peers || {})) {
5858
+ peers[peer_id] = {
5859
+ peer_id,
5860
+ role: peer.role,
5861
+ target_peer: peer.target_peer || null,
5862
+ joined_at: peer.joined_at,
5863
+ last_seen_at: peer.last_seen_at,
5864
+ queued: peer.inbox.length,
5865
+ streams: peer.streams.length,
5866
+ };
5867
+ }
5868
+ return {
5869
+ session_id: session.session_id,
5870
+ name: session.name,
5871
+ repo: session.repo,
5872
+ status: session.status,
5873
+ started_at: session.started_at,
5874
+ stopped_at: session.stopped_at || null,
5875
+ turn: session.turn || 0,
5876
+ peers,
5877
+ };
5878
+ }
5879
+
5880
+ function startCodevelopSession({ name, repo, metadata } = {}) {
5881
+ const session_id = generateSessionId();
5882
+ const session = {
5883
+ session_id,
5884
+ name: name || `codevelop-${session_id.slice(0, 8)}`,
5885
+ repo: repo || null,
5886
+ status: "active",
5887
+ started_at: new Date().toISOString(),
5888
+ is_codevelop: true,
5889
+ turn: 0,
5890
+ peers: {},
5891
+ history: [],
5892
+ metadata: metadata && typeof metadata === "object" ? metadata : {},
5893
+ };
5894
+ terminalSessions.set(session_id, session);
5895
+ console.log(`[codevelop] started session ${session_id} name=${session.name}`);
5896
+ return serializeCodevelopSession(session);
5897
+ }
5898
+
5899
+ function joinCodevelopSession({ session_id, peer_id, role, target_peer, metadata } = {}) {
5900
+ const session = terminalSessions.get(session_id);
5901
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
5902
+ if (session.status === "stopped") return { error: "stopped", message: "Session is stopped" };
5903
+
5904
+ const normalizedPeer = normalizePeerId(peer_id);
5905
+ if (!normalizedPeer) return { error: "invalid_peer", message: "peer_id is required" };
5906
+
5907
+ const now = new Date().toISOString();
5908
+ const existing = session.peers[normalizedPeer];
5909
+ session.peers[normalizedPeer] = {
5910
+ peer_id: normalizedPeer,
5911
+ role: role || existing?.role || "participant",
5912
+ target_peer: target_peer ? normalizePeerId(target_peer) : existing?.target_peer || null,
5913
+ joined_at: existing?.joined_at || now,
5914
+ last_seen_at: now,
5915
+ inbox: existing?.inbox || [],
5916
+ streams: existing?.streams || [],
5917
+ metadata: metadata && typeof metadata === "object" ? metadata : existing?.metadata || {},
5918
+ };
5919
+
5920
+ console.log(`[codevelop] session ${session_id} peer=${normalizedPeer} joined role=${session.peers[normalizedPeer].role}`);
5921
+ return serializeCodevelopSession(session);
5922
+ }
5923
+
5924
+ function buildCodevelopEnvelope(session, payload) {
5925
+ const from = normalizePeerId(payload.from);
5926
+ const to = normalizePeerId(payload.to);
5927
+ if (!from || !to) return { error: "invalid_route", message: "from and to are required" };
5928
+
5929
+ session.turn = (session.turn || 0) + 1;
5930
+ const turn_id = payload.turn_id || `turn-${String(session.turn).padStart(4, "0")}`;
5931
+ return {
5932
+ session_id: session.session_id,
5933
+ turn_id,
5934
+ from,
5935
+ to,
5936
+ type: payload.type || "message",
5937
+ role: payload.role || null,
5938
+ skill: payload.skill || null,
5939
+ task: payload.task || payload.message || "",
5940
+ message: payload.message || null,
5941
+ context: payload.context && typeof payload.context === "object" ? payload.context : {},
5942
+ expect: payload.expect && typeof payload.expect === "object" ? payload.expect : {},
5943
+ verdict: payload.verdict || null,
5944
+ summary: payload.summary || null,
5945
+ evidence: Array.isArray(payload.evidence) ? payload.evidence : [],
5946
+ files_changed: Array.isArray(payload.files_changed) ? payload.files_changed : [],
5947
+ commits: Array.isArray(payload.commits) ? payload.commits : [],
5948
+ blockers: Array.isArray(payload.blockers) ? payload.blockers : [],
5949
+ next: Array.isArray(payload.next) ? payload.next : (payload.next_action ? [String(payload.next_action)] : []),
5950
+ sent_at: new Date().toISOString(),
5951
+ };
5952
+ }
5953
+
5954
+ function sendCodevelopMessage(payload = {}) {
5955
+ const session = terminalSessions.get(payload.session_id);
5956
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${payload.session_id} not found` };
5957
+ if (session.status === "stopped") return { error: "stopped", message: "Session is stopped" };
5958
+
5959
+ const envelope = buildCodevelopEnvelope(session, payload);
5960
+ if (envelope.error) return envelope;
5961
+
5962
+ if (!session.peers[envelope.from]) {
5963
+ session.peers[envelope.from] = {
5964
+ peer_id: envelope.from,
5965
+ role: "participant",
5966
+ target_peer: envelope.to,
5967
+ joined_at: envelope.sent_at,
5968
+ last_seen_at: envelope.sent_at,
5969
+ inbox: [],
5970
+ streams: [],
5971
+ metadata: {},
5972
+ };
5973
+ }
5974
+ if (!session.peers[envelope.to]) {
5975
+ session.peers[envelope.to] = {
5976
+ peer_id: envelope.to,
5977
+ role: "participant",
5978
+ target_peer: envelope.from,
5979
+ joined_at: envelope.sent_at,
5980
+ last_seen_at: envelope.sent_at,
5981
+ inbox: [],
5982
+ streams: [],
5983
+ metadata: {},
5984
+ };
5985
+ }
5986
+
5987
+ const target = session.peers[envelope.to];
5988
+ target.inbox.push(envelope);
5989
+ session.history.push(envelope);
5990
+ if (session.history.length > 200) session.history = session.history.slice(-200);
5991
+
5992
+ const event = `event: message\ndata: ${JSON.stringify(envelope)}\n\n`;
5993
+ target.streams = target.streams.filter(res => {
5994
+ try { res.write(event); return true; } catch { return false; }
5995
+ });
5996
+
5997
+ console.log(`[codevelop] session ${session.session_id} ${envelope.from}->${envelope.to} ${envelope.turn_id} queued`);
5998
+ return { queued: true, session_id: session.session_id, turn_id: envelope.turn_id, to: envelope.to };
5999
+ }
6000
+
6001
+ function pollCodevelopMessages({ session_id, peer_id } = {}) {
6002
+ const session = terminalSessions.get(session_id);
6003
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6004
+
6005
+ const normalizedPeer = normalizePeerId(peer_id);
6006
+ if (!normalizedPeer) return { error: "invalid_peer", message: "peer_id is required" };
6007
+ const peer = session.peers[normalizedPeer];
6008
+ if (!peer) return { error: "not_joined", message: `Peer ${normalizedPeer} has not joined session ${session_id}` };
6009
+
6010
+ peer.last_seen_at = new Date().toISOString();
6011
+ const messages = peer.inbox.splice(0);
6012
+ return {
6013
+ session_id,
6014
+ peer_id: normalizedPeer,
6015
+ status: messages.length ? "ready" : "idle",
6016
+ count: messages.length,
6017
+ messages,
6018
+ };
6019
+ }
6020
+
6021
+ function attachCodevelopStream(session_id, peer_id, res) {
6022
+ const session = terminalSessions.get(session_id);
6023
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6024
+
6025
+ const normalizedPeer = normalizePeerId(peer_id);
6026
+ const peer = session.peers[normalizedPeer];
6027
+ if (!peer) return { error: "not_joined", message: `Peer ${normalizedPeer} has not joined session ${session_id}` };
6028
+
6029
+ peer.last_seen_at = new Date().toISOString();
6030
+ peer.streams.push(res);
6031
+ console.log(`[codevelop] session ${session_id} peer=${normalizedPeer} stream connected (${peer.streams.length} total)`);
6032
+ return { queued: peer.inbox.slice() };
6033
+ }
6034
+
6035
+ function detachCodevelopStream(session_id, peer_id, res) {
6036
+ const session = terminalSessions.get(session_id);
6037
+ if (!session || !session.is_codevelop) return;
6038
+ const normalizedPeer = normalizePeerId(peer_id);
6039
+ const peer = session.peers[normalizedPeer];
6040
+ if (!peer) return;
6041
+ peer.streams = peer.streams.filter(r => r !== res);
6042
+ console.log(`[codevelop] session ${session_id} peer=${normalizedPeer} stream disconnected`);
6043
+ }
6044
+
6045
+ function statusCodevelopSession(session_id) {
6046
+ const session = terminalSessions.get(session_id);
6047
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6048
+ return serializeCodevelopSession(session);
6049
+ }
6050
+
6051
+ function listCodevelopSessions() {
6052
+ const sessions = [];
6053
+ for (const [, s] of terminalSessions) {
6054
+ if (s.is_codevelop) sessions.push(serializeCodevelopSession(s));
6055
+ }
6056
+ return { sessions, count: sessions.length };
6057
+ }
6058
+
6059
+ function stopCodevelopSession(session_id) {
6060
+ const session = terminalSessions.get(session_id);
6061
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6062
+ session.status = "stopped";
6063
+ session.stopped_at = new Date().toISOString();
6064
+
6065
+ for (const peer of Object.values(session.peers)) {
6066
+ const entry = {
6067
+ session_id,
6068
+ turn_id: `stop-${Date.now()}`,
6069
+ from: "system",
6070
+ to: peer.peer_id,
6071
+ type: "stop",
6072
+ task: "Session stopped.",
6073
+ context: {},
6074
+ expect: {},
6075
+ sent_at: session.stopped_at,
6076
+ };
6077
+ peer.inbox.push(entry);
6078
+ const event = `event: stop\ndata: ${JSON.stringify(entry)}\n\n`;
6079
+ peer.streams = peer.streams.filter(res => {
6080
+ try { res.write(event); return true; } catch { return false; }
6081
+ });
6082
+ }
6083
+
6084
+ terminalSessions.delete(session_id);
6085
+ console.log(`[codevelop] stopped session ${session_id}`);
6086
+ return { stopped: true, session_id };
6087
+ }
6088
+
5646
6089
  const ENV_MAP = {
5647
6090
  "github": "GITHUB_TOKEN",
5648
6091
  "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
@@ -5998,6 +6441,116 @@ const MCP_TOOLS = [
5998
6441
  },
5999
6442
  },
6000
6443
 
6444
+ // ── Co-development — peer-aware Claude/Codex terminal sessions ─────────
6445
+ {
6446
+ name: "codevelop_start",
6447
+ description: "Start a peer-aware co-development session. Separate from chitchat; routes messages by session_id/from/to.",
6448
+ inputSchema: {
6449
+ type: "object",
6450
+ properties: {
6451
+ name: { type: "string", description: "Human-readable session name" },
6452
+ repo: { type: "string", description: "Repository path or slug for the session" },
6453
+ metadata: { type: "object", description: "Optional session metadata" },
6454
+ },
6455
+ additionalProperties: false,
6456
+ },
6457
+ },
6458
+ {
6459
+ name: "codevelop_join",
6460
+ description: "Join a co-development session as a stable peer such as claude or codex.",
6461
+ inputSchema: {
6462
+ type: "object",
6463
+ properties: {
6464
+ session_id: { type: "string", description: "Session ID from codevelop_start" },
6465
+ peer_id: { type: "string", description: "Stable peer ID, e.g. claude or codex" },
6466
+ role: { type: "string", description: "Session role, e.g. supervisor or implementation_partner" },
6467
+ target_peer: { type: "string", description: "Default partner peer ID" },
6468
+ metadata: { type: "object", description: "Optional peer metadata" },
6469
+ },
6470
+ required: ["session_id", "peer_id"],
6471
+ additionalProperties: false,
6472
+ },
6473
+ },
6474
+ {
6475
+ name: "codevelop_send",
6476
+ description: "Send a structured addressed turn to a peer in a co-development session.",
6477
+ inputSchema: {
6478
+ type: "object",
6479
+ properties: {
6480
+ session_id: { type: "string" },
6481
+ turn_id: { type: "string" },
6482
+ from: { type: "string" },
6483
+ to: { type: "string" },
6484
+ type: { type: "string", description: "Message type, e.g. audit_request, reply, blocker" },
6485
+ role: { type: "string", description: "Temporary requested turn role" },
6486
+ skill: { type: "string", description: "Optional requested skill, e.g. rdc:review" },
6487
+ task: { type: "string", description: "Task or request body" },
6488
+ message: { type: "string", description: "Plain fallback message" },
6489
+ context: { type: "object", description: "Repo/work item/branch/files metadata" },
6490
+ expect: { type: "object", description: "Expected response format and evidence requirements" },
6491
+ verdict: { type: "string", description: "Reply verdict, e.g. pass, fail, blocked" },
6492
+ summary: { type: "string", description: "Concise reply summary" },
6493
+ evidence: { type: "array", items: { type: "string" }, description: "Evidence strings or checked commands/files" },
6494
+ files_changed: { type: "array", items: { type: "string" }, description: "Files changed by the sender" },
6495
+ commits: { type: "array", items: { type: "string" }, description: "Commit hashes or messages, if any" },
6496
+ blockers: { type: "array", items: { type: "string" }, description: "Blocking issues, if any" },
6497
+ next: { type: "array", items: { type: "string" }, description: "Next recommended actions" },
6498
+ next_action: { type: "string", description: "Single next action shorthand" },
6499
+ },
6500
+ required: ["session_id", "from", "to"],
6501
+ additionalProperties: false,
6502
+ },
6503
+ },
6504
+ {
6505
+ name: "codevelop_poll",
6506
+ description: "Poll and drain all pending messages for one peer.",
6507
+ inputSchema: {
6508
+ type: "object",
6509
+ properties: {
6510
+ session_id: { type: "string" },
6511
+ peer_id: { type: "string" },
6512
+ },
6513
+ required: ["session_id", "peer_id"],
6514
+ additionalProperties: false,
6515
+ },
6516
+ },
6517
+ {
6518
+ name: "codevelop_stream",
6519
+ description: "Return the local SSE stream URL for a peer. Use HTTP GET on that URL to receive events.",
6520
+ inputSchema: {
6521
+ type: "object",
6522
+ properties: {
6523
+ session_id: { type: "string" },
6524
+ peer_id: { type: "string" },
6525
+ },
6526
+ required: ["session_id", "peer_id"],
6527
+ additionalProperties: false,
6528
+ },
6529
+ },
6530
+ {
6531
+ name: "codevelop_status",
6532
+ description: "Return status, peers, queue counts, and stream counts for a co-development session.",
6533
+ inputSchema: {
6534
+ type: "object",
6535
+ properties: {
6536
+ session_id: { type: "string", description: "Optional session ID. Omit to list all codevelop sessions." },
6537
+ },
6538
+ additionalProperties: false,
6539
+ },
6540
+ },
6541
+ {
6542
+ name: "codevelop_stop",
6543
+ description: "Stop a co-development session and notify joined peers.",
6544
+ inputSchema: {
6545
+ type: "object",
6546
+ properties: {
6547
+ session_id: { type: "string" },
6548
+ },
6549
+ required: ["session_id"],
6550
+ additionalProperties: false,
6551
+ },
6552
+ },
6553
+
6001
6554
  // ── Google Workspace (gws CLI) ──────────────────────────────────────────
6002
6555
  {
6003
6556
  name: "gws_run",
@@ -6838,6 +7391,54 @@ async function handleMcpTool(vault, name, args) {
6838
7391
  return mcpResult(JSON.stringify(result));
6839
7392
  }
6840
7393
 
7394
+ case "codevelop_start": {
7395
+ return mcpResult(JSON.stringify(startCodevelopSession(args || {})));
7396
+ }
7397
+
7398
+ case "codevelop_join": {
7399
+ const result = joinCodevelopSession(args || {});
7400
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7401
+ return mcpResult(JSON.stringify(result));
7402
+ }
7403
+
7404
+ case "codevelop_send": {
7405
+ const result = sendCodevelopMessage(args || {});
7406
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7407
+ return mcpResult(JSON.stringify(result));
7408
+ }
7409
+
7410
+ case "codevelop_poll": {
7411
+ const result = pollCodevelopMessages(args || {});
7412
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7413
+ return mcpResult(JSON.stringify(result));
7414
+ }
7415
+
7416
+ case "codevelop_stream": {
7417
+ const { session_id, peer_id } = args || {};
7418
+ if (!session_id || !peer_id) return mcpError("session_id and peer_id required");
7419
+ const session = terminalSessions.get(session_id);
7420
+ if (!session || !session.is_codevelop) return mcpError(`not_found: Codevelop session ${session_id} not found`);
7421
+ const peer = session.peers[normalizePeerId(peer_id)];
7422
+ if (!peer) return mcpError(`not_joined: Peer ${peer_id} has not joined session ${session_id}`);
7423
+ const stream_url = `${mcpHttpBaseUrl}/codevelop/${encodeURIComponent(session_id)}/${encodeURIComponent(normalizePeerId(peer_id))}/stream`;
7424
+ return mcpResult(JSON.stringify({ session_id, peer_id: normalizePeerId(peer_id), stream_url }));
7425
+ }
7426
+
7427
+ case "codevelop_status": {
7428
+ const { session_id } = args || {};
7429
+ const result = session_id ? statusCodevelopSession(session_id) : listCodevelopSessions();
7430
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7431
+ return mcpResult(JSON.stringify(result));
7432
+ }
7433
+
7434
+ case "codevelop_stop": {
7435
+ const { session_id } = args || {};
7436
+ if (!session_id) return mcpError("session_id required");
7437
+ const result = stopCodevelopSession(session_id);
7438
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7439
+ return mcpResult(JSON.stringify(result));
7440
+ }
7441
+
6841
7442
  default:
6842
7443
  return mcpError(`Unknown tool: ${name}`);
6843
7444
  }