@lifeaitools/clauth 1.5.78 → 1.5.80

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,46 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3545
3687
  return ok(res, { status: tunnelStatus });
3546
3688
  }
3547
3689
 
3690
+ // POST /launch-ccandme — spawn CCandMe (WezTerm 4-pane: Claude + Codex + Me)
3691
+ if (method === "POST" && reqPath === "/launch-ccandme") {
3692
+ if (lockedGuard(res)) return;
3693
+ try {
3694
+ const { spawn } = await import("child_process");
3695
+ const ccandmeBin = path.resolve(process.env.APPDATA || "", "../Local/node_modules/.bin/ccandme.cmd");
3696
+ const fallbacks = [
3697
+ ccandmeBin,
3698
+ "C:/Dev/CCandMe/bin/ccandme.mjs",
3699
+ ];
3700
+ // Find the first ccandme binary that exists
3701
+ const { existsSync } = await import("fs");
3702
+ let cmd = null;
3703
+ let args = [];
3704
+ for (const f of fallbacks) {
3705
+ if (existsSync(f)) {
3706
+ if (f.endsWith(".mjs")) { cmd = process.execPath; args = [f, "start"]; }
3707
+ else { cmd = f; args = ["start"]; }
3708
+ break;
3709
+ }
3710
+ }
3711
+ // Last resort: try the global npm bin
3712
+ if (!cmd) {
3713
+ cmd = process.platform === "win32" ? "cmd.exe" : "ccandme";
3714
+ args = process.platform === "win32" ? ["/c", "ccandme", "start"] : ["start"];
3715
+ }
3716
+ const child = spawn(cmd, args, {
3717
+ detached: true,
3718
+ stdio: "ignore",
3719
+ windowsHide: false,
3720
+ shell: process.platform === "win32" && cmd === "cmd.exe" ? false : true,
3721
+ });
3722
+ child.unref();
3723
+ return ok(res, { ok: true, message: "CCandMe launched" });
3724
+ } catch (err) {
3725
+ res.writeHead(500, { "Content-Type": "application/json", ...CORS });
3726
+ return res.end(JSON.stringify({ error: err.message }));
3727
+ }
3728
+ }
3729
+
3548
3730
  // POST /restart — spawn fresh process then exit (keeps boot.key, vault stays unlocked)
3549
3731
  if (method === "POST" && reqPath === "/restart") {
3550
3732
  ok(res, { ok: true, message: "restarting" });
@@ -4970,6 +5152,10 @@ async function verifyAuth(password) {
4970
5152
  }
4971
5153
 
4972
5154
  async function actionStart(opts) {
5155
+ if (opts.isolated) {
5156
+ return actionForeground(opts);
5157
+ }
5158
+
4973
5159
  const isStaged = !!opts.staged || process.env.__CLAUTH_STAGED === "1";
4974
5160
  const port = isStaged ? STAGED_PORT : parseInt(opts.port || String(LIVE_PORT), 10);
4975
5161
  let password = opts.pw || null;
@@ -5220,12 +5406,18 @@ async function actionRestart(opts) {
5220
5406
 
5221
5407
  async function actionForeground(opts) {
5222
5408
  const port = parseInt(opts.port || "52437", 10);
5223
- const password = opts.pw || null;
5409
+ const isolated = !!opts.isolated;
5410
+ const password = isolated ? null : (opts.pw || null);
5224
5411
  const tunnelHostname = opts.tunnel || null;
5225
5412
  const whitelist = opts.services
5226
5413
  ? opts.services.split(",").map(s => s.trim().toLowerCase())
5227
5414
  : null;
5228
5415
 
5416
+ if (isolated && (port === LIVE_PORT || port === STAGED_PORT)) {
5417
+ console.log(chalk.red(`\n Refusing isolated mode on reserved port ${port}. Use a separate port such as 53137.\n`));
5418
+ process.exit(1);
5419
+ }
5420
+
5229
5421
  if (password) {
5230
5422
  console.log(chalk.gray("\n Verifying vault credentials..."));
5231
5423
  try {
@@ -5235,6 +5427,8 @@ async function actionForeground(opts) {
5235
5427
  process.exit(1);
5236
5428
  }
5237
5429
  console.log(chalk.green(" ✓ Vault auth verified"));
5430
+ } else if (isolated) {
5431
+ console.log(chalk.yellow("\n Starting isolated passwordless server — credential routes remain locked"));
5238
5432
  } else {
5239
5433
  console.log(chalk.yellow("\n Starting in locked state — open browser to unlock"));
5240
5434
  }
@@ -5245,7 +5439,7 @@ async function actionForeground(opts) {
5245
5439
 
5246
5440
  const server = createServer(password, whitelist, port, tunnelHostname);
5247
5441
  server.listen(port, "127.0.0.1", () => {
5248
- writePid(process.pid, port);
5442
+ if (!isolated) writePid(process.pid, port);
5249
5443
  console.log(chalk.green(` clauth serve → http://127.0.0.1:${port}`));
5250
5444
  if (tunnelHostname) {
5251
5445
  console.log(chalk.cyan(` Tunnel: https://${tunnelHostname}/sse`));
@@ -5256,10 +5450,9 @@ async function actionForeground(opts) {
5256
5450
  console.log(chalk.white(` Client Secret: ${server.__oauthClientSecret}`));
5257
5451
  console.log(chalk.gray(" (paste these into Advanced Settings when adding the connector)"));
5258
5452
  }
5259
- if (!password) console.log(chalk.cyan(` 👉 Open http://127.0.0.1:${port} to unlock`));
5453
+ if (!password && !isolated) console.log(chalk.cyan(` 👉 Open http://127.0.0.1:${port} to unlock`));
5260
5454
  console.log(chalk.gray(" Ctrl+C to stop\n"));
5261
- // Auto-open browser
5262
- openBrowser(`http://127.0.0.1:${port}`);
5455
+ if (!isolated) openBrowser(`http://127.0.0.1:${port}`);
5263
5456
  });
5264
5457
 
5265
5458
  server.on("error", err => {
@@ -5273,7 +5466,7 @@ async function actionForeground(opts) {
5273
5466
 
5274
5467
  process.on("SIGINT", () => {
5275
5468
  console.log(chalk.yellow("\n Stopping clauth serve...\n"));
5276
- removePid();
5469
+ if (!isolated) removePid();
5277
5470
  server.close(() => process.exit(0));
5278
5471
  });
5279
5472
  }
@@ -5344,6 +5537,7 @@ function spawnClaudeTask(prompt, jobId, cwd) {
5344
5537
  // /terminal/send spawns a fresh claude -p with [context + message],
5345
5538
  // captures stdout, stores result back in session context.
5346
5539
  const terminalSessions = new Map(); // session_id → SessionState
5540
+ let mcpHttpBaseUrl = "http://127.0.0.1:52437";
5347
5541
 
5348
5542
  // ── Channel webhook event queue ───────────────────────────────────────────────
5349
5543
  const channelEvents = []; // { id, event, resource, status, url, repository, created_at }
@@ -5643,6 +5837,249 @@ function stopChitchatSession(session_id) {
5643
5837
  return { stopped: true, session_id };
5644
5838
  }
5645
5839
 
5840
+ // ── Co-development — peer-aware Claude/Codex terminal relay ───────────────
5841
+ //
5842
+ // This route is intentionally not a chitchat migration path. It is a separate
5843
+ // transport with addressed peer inboxes and a stable JSON envelope.
5844
+
5845
+ function normalizePeerId(peer) {
5846
+ return String(peer || "").trim().toLowerCase();
5847
+ }
5848
+
5849
+ function serializeCodevelopSession(session) {
5850
+ const peers = {};
5851
+ for (const [peer_id, peer] of Object.entries(session.peers || {})) {
5852
+ peers[peer_id] = {
5853
+ peer_id,
5854
+ role: peer.role,
5855
+ target_peer: peer.target_peer || null,
5856
+ joined_at: peer.joined_at,
5857
+ last_seen_at: peer.last_seen_at,
5858
+ queued: peer.inbox.length,
5859
+ streams: peer.streams.length,
5860
+ };
5861
+ }
5862
+ return {
5863
+ session_id: session.session_id,
5864
+ name: session.name,
5865
+ repo: session.repo,
5866
+ status: session.status,
5867
+ started_at: session.started_at,
5868
+ stopped_at: session.stopped_at || null,
5869
+ turn: session.turn || 0,
5870
+ peers,
5871
+ };
5872
+ }
5873
+
5874
+ function startCodevelopSession({ name, repo, metadata } = {}) {
5875
+ const session_id = generateSessionId();
5876
+ const session = {
5877
+ session_id,
5878
+ name: name || `codevelop-${session_id.slice(0, 8)}`,
5879
+ repo: repo || null,
5880
+ status: "active",
5881
+ started_at: new Date().toISOString(),
5882
+ is_codevelop: true,
5883
+ turn: 0,
5884
+ peers: {},
5885
+ history: [],
5886
+ metadata: metadata && typeof metadata === "object" ? metadata : {},
5887
+ };
5888
+ terminalSessions.set(session_id, session);
5889
+ console.log(`[codevelop] started session ${session_id} name=${session.name}`);
5890
+ return serializeCodevelopSession(session);
5891
+ }
5892
+
5893
+ function joinCodevelopSession({ session_id, peer_id, role, target_peer, metadata } = {}) {
5894
+ const session = terminalSessions.get(session_id);
5895
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
5896
+ if (session.status === "stopped") return { error: "stopped", message: "Session is stopped" };
5897
+
5898
+ const normalizedPeer = normalizePeerId(peer_id);
5899
+ if (!normalizedPeer) return { error: "invalid_peer", message: "peer_id is required" };
5900
+
5901
+ const now = new Date().toISOString();
5902
+ const existing = session.peers[normalizedPeer];
5903
+ session.peers[normalizedPeer] = {
5904
+ peer_id: normalizedPeer,
5905
+ role: role || existing?.role || "participant",
5906
+ target_peer: target_peer ? normalizePeerId(target_peer) : existing?.target_peer || null,
5907
+ joined_at: existing?.joined_at || now,
5908
+ last_seen_at: now,
5909
+ inbox: existing?.inbox || [],
5910
+ streams: existing?.streams || [],
5911
+ metadata: metadata && typeof metadata === "object" ? metadata : existing?.metadata || {},
5912
+ };
5913
+
5914
+ console.log(`[codevelop] session ${session_id} peer=${normalizedPeer} joined role=${session.peers[normalizedPeer].role}`);
5915
+ return serializeCodevelopSession(session);
5916
+ }
5917
+
5918
+ function buildCodevelopEnvelope(session, payload) {
5919
+ const from = normalizePeerId(payload.from);
5920
+ const to = normalizePeerId(payload.to);
5921
+ if (!from || !to) return { error: "invalid_route", message: "from and to are required" };
5922
+
5923
+ session.turn = (session.turn || 0) + 1;
5924
+ const turn_id = payload.turn_id || `turn-${String(session.turn).padStart(4, "0")}`;
5925
+ return {
5926
+ session_id: session.session_id,
5927
+ turn_id,
5928
+ from,
5929
+ to,
5930
+ type: payload.type || "message",
5931
+ role: payload.role || null,
5932
+ skill: payload.skill || null,
5933
+ task: payload.task || payload.message || "",
5934
+ message: payload.message || null,
5935
+ context: payload.context && typeof payload.context === "object" ? payload.context : {},
5936
+ expect: payload.expect && typeof payload.expect === "object" ? payload.expect : {},
5937
+ verdict: payload.verdict || null,
5938
+ summary: payload.summary || null,
5939
+ evidence: Array.isArray(payload.evidence) ? payload.evidence : [],
5940
+ files_changed: Array.isArray(payload.files_changed) ? payload.files_changed : [],
5941
+ commits: Array.isArray(payload.commits) ? payload.commits : [],
5942
+ blockers: Array.isArray(payload.blockers) ? payload.blockers : [],
5943
+ next: Array.isArray(payload.next) ? payload.next : (payload.next_action ? [String(payload.next_action)] : []),
5944
+ sent_at: new Date().toISOString(),
5945
+ };
5946
+ }
5947
+
5948
+ function sendCodevelopMessage(payload = {}) {
5949
+ const session = terminalSessions.get(payload.session_id);
5950
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${payload.session_id} not found` };
5951
+ if (session.status === "stopped") return { error: "stopped", message: "Session is stopped" };
5952
+
5953
+ const envelope = buildCodevelopEnvelope(session, payload);
5954
+ if (envelope.error) return envelope;
5955
+
5956
+ if (!session.peers[envelope.from]) {
5957
+ session.peers[envelope.from] = {
5958
+ peer_id: envelope.from,
5959
+ role: "participant",
5960
+ target_peer: envelope.to,
5961
+ joined_at: envelope.sent_at,
5962
+ last_seen_at: envelope.sent_at,
5963
+ inbox: [],
5964
+ streams: [],
5965
+ metadata: {},
5966
+ };
5967
+ }
5968
+ if (!session.peers[envelope.to]) {
5969
+ session.peers[envelope.to] = {
5970
+ peer_id: envelope.to,
5971
+ role: "participant",
5972
+ target_peer: envelope.from,
5973
+ joined_at: envelope.sent_at,
5974
+ last_seen_at: envelope.sent_at,
5975
+ inbox: [],
5976
+ streams: [],
5977
+ metadata: {},
5978
+ };
5979
+ }
5980
+
5981
+ const target = session.peers[envelope.to];
5982
+ target.inbox.push(envelope);
5983
+ session.history.push(envelope);
5984
+ if (session.history.length > 200) session.history = session.history.slice(-200);
5985
+
5986
+ const event = `event: message\ndata: ${JSON.stringify(envelope)}\n\n`;
5987
+ target.streams = target.streams.filter(res => {
5988
+ try { res.write(event); return true; } catch { return false; }
5989
+ });
5990
+
5991
+ console.log(`[codevelop] session ${session.session_id} ${envelope.from}->${envelope.to} ${envelope.turn_id} queued`);
5992
+ return { queued: true, session_id: session.session_id, turn_id: envelope.turn_id, to: envelope.to };
5993
+ }
5994
+
5995
+ function pollCodevelopMessages({ session_id, peer_id } = {}) {
5996
+ const session = terminalSessions.get(session_id);
5997
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
5998
+
5999
+ const normalizedPeer = normalizePeerId(peer_id);
6000
+ if (!normalizedPeer) return { error: "invalid_peer", message: "peer_id is required" };
6001
+ const peer = session.peers[normalizedPeer];
6002
+ if (!peer) return { error: "not_joined", message: `Peer ${normalizedPeer} has not joined session ${session_id}` };
6003
+
6004
+ peer.last_seen_at = new Date().toISOString();
6005
+ const messages = peer.inbox.splice(0);
6006
+ return {
6007
+ session_id,
6008
+ peer_id: normalizedPeer,
6009
+ status: messages.length ? "ready" : "idle",
6010
+ count: messages.length,
6011
+ messages,
6012
+ };
6013
+ }
6014
+
6015
+ function attachCodevelopStream(session_id, peer_id, res) {
6016
+ const session = terminalSessions.get(session_id);
6017
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6018
+
6019
+ const normalizedPeer = normalizePeerId(peer_id);
6020
+ const peer = session.peers[normalizedPeer];
6021
+ if (!peer) return { error: "not_joined", message: `Peer ${normalizedPeer} has not joined session ${session_id}` };
6022
+
6023
+ peer.last_seen_at = new Date().toISOString();
6024
+ peer.streams.push(res);
6025
+ console.log(`[codevelop] session ${session_id} peer=${normalizedPeer} stream connected (${peer.streams.length} total)`);
6026
+ return { queued: peer.inbox.slice() };
6027
+ }
6028
+
6029
+ function detachCodevelopStream(session_id, peer_id, res) {
6030
+ const session = terminalSessions.get(session_id);
6031
+ if (!session || !session.is_codevelop) return;
6032
+ const normalizedPeer = normalizePeerId(peer_id);
6033
+ const peer = session.peers[normalizedPeer];
6034
+ if (!peer) return;
6035
+ peer.streams = peer.streams.filter(r => r !== res);
6036
+ console.log(`[codevelop] session ${session_id} peer=${normalizedPeer} stream disconnected`);
6037
+ }
6038
+
6039
+ function statusCodevelopSession(session_id) {
6040
+ const session = terminalSessions.get(session_id);
6041
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6042
+ return serializeCodevelopSession(session);
6043
+ }
6044
+
6045
+ function listCodevelopSessions() {
6046
+ const sessions = [];
6047
+ for (const [, s] of terminalSessions) {
6048
+ if (s.is_codevelop) sessions.push(serializeCodevelopSession(s));
6049
+ }
6050
+ return { sessions, count: sessions.length };
6051
+ }
6052
+
6053
+ function stopCodevelopSession(session_id) {
6054
+ const session = terminalSessions.get(session_id);
6055
+ if (!session || !session.is_codevelop) return { error: "not_found", message: `Codevelop session ${session_id} not found` };
6056
+ session.status = "stopped";
6057
+ session.stopped_at = new Date().toISOString();
6058
+
6059
+ for (const peer of Object.values(session.peers)) {
6060
+ const entry = {
6061
+ session_id,
6062
+ turn_id: `stop-${Date.now()}`,
6063
+ from: "system",
6064
+ to: peer.peer_id,
6065
+ type: "stop",
6066
+ task: "Session stopped.",
6067
+ context: {},
6068
+ expect: {},
6069
+ sent_at: session.stopped_at,
6070
+ };
6071
+ peer.inbox.push(entry);
6072
+ const event = `event: stop\ndata: ${JSON.stringify(entry)}\n\n`;
6073
+ peer.streams = peer.streams.filter(res => {
6074
+ try { res.write(event); return true; } catch { return false; }
6075
+ });
6076
+ }
6077
+
6078
+ terminalSessions.delete(session_id);
6079
+ console.log(`[codevelop] stopped session ${session_id}`);
6080
+ return { stopped: true, session_id };
6081
+ }
6082
+
5646
6083
  const ENV_MAP = {
5647
6084
  "github": "GITHUB_TOKEN",
5648
6085
  "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
@@ -5998,6 +6435,116 @@ const MCP_TOOLS = [
5998
6435
  },
5999
6436
  },
6000
6437
 
6438
+ // ── Co-development — peer-aware Claude/Codex terminal sessions ─────────
6439
+ {
6440
+ name: "codevelop_start",
6441
+ description: "Start a peer-aware co-development session. Separate from chitchat; routes messages by session_id/from/to.",
6442
+ inputSchema: {
6443
+ type: "object",
6444
+ properties: {
6445
+ name: { type: "string", description: "Human-readable session name" },
6446
+ repo: { type: "string", description: "Repository path or slug for the session" },
6447
+ metadata: { type: "object", description: "Optional session metadata" },
6448
+ },
6449
+ additionalProperties: false,
6450
+ },
6451
+ },
6452
+ {
6453
+ name: "codevelop_join",
6454
+ description: "Join a co-development session as a stable peer such as claude or codex.",
6455
+ inputSchema: {
6456
+ type: "object",
6457
+ properties: {
6458
+ session_id: { type: "string", description: "Session ID from codevelop_start" },
6459
+ peer_id: { type: "string", description: "Stable peer ID, e.g. claude or codex" },
6460
+ role: { type: "string", description: "Session role, e.g. supervisor or implementation_partner" },
6461
+ target_peer: { type: "string", description: "Default partner peer ID" },
6462
+ metadata: { type: "object", description: "Optional peer metadata" },
6463
+ },
6464
+ required: ["session_id", "peer_id"],
6465
+ additionalProperties: false,
6466
+ },
6467
+ },
6468
+ {
6469
+ name: "codevelop_send",
6470
+ description: "Send a structured addressed turn to a peer in a co-development session.",
6471
+ inputSchema: {
6472
+ type: "object",
6473
+ properties: {
6474
+ session_id: { type: "string" },
6475
+ turn_id: { type: "string" },
6476
+ from: { type: "string" },
6477
+ to: { type: "string" },
6478
+ type: { type: "string", description: "Message type, e.g. audit_request, reply, blocker" },
6479
+ role: { type: "string", description: "Temporary requested turn role" },
6480
+ skill: { type: "string", description: "Optional requested skill, e.g. rdc:review" },
6481
+ task: { type: "string", description: "Task or request body" },
6482
+ message: { type: "string", description: "Plain fallback message" },
6483
+ context: { type: "object", description: "Repo/work item/branch/files metadata" },
6484
+ expect: { type: "object", description: "Expected response format and evidence requirements" },
6485
+ verdict: { type: "string", description: "Reply verdict, e.g. pass, fail, blocked" },
6486
+ summary: { type: "string", description: "Concise reply summary" },
6487
+ evidence: { type: "array", items: { type: "string" }, description: "Evidence strings or checked commands/files" },
6488
+ files_changed: { type: "array", items: { type: "string" }, description: "Files changed by the sender" },
6489
+ commits: { type: "array", items: { type: "string" }, description: "Commit hashes or messages, if any" },
6490
+ blockers: { type: "array", items: { type: "string" }, description: "Blocking issues, if any" },
6491
+ next: { type: "array", items: { type: "string" }, description: "Next recommended actions" },
6492
+ next_action: { type: "string", description: "Single next action shorthand" },
6493
+ },
6494
+ required: ["session_id", "from", "to"],
6495
+ additionalProperties: false,
6496
+ },
6497
+ },
6498
+ {
6499
+ name: "codevelop_poll",
6500
+ description: "Poll and drain all pending messages for one peer.",
6501
+ inputSchema: {
6502
+ type: "object",
6503
+ properties: {
6504
+ session_id: { type: "string" },
6505
+ peer_id: { type: "string" },
6506
+ },
6507
+ required: ["session_id", "peer_id"],
6508
+ additionalProperties: false,
6509
+ },
6510
+ },
6511
+ {
6512
+ name: "codevelop_stream",
6513
+ description: "Return the local SSE stream URL for a peer. Use HTTP GET on that URL to receive events.",
6514
+ inputSchema: {
6515
+ type: "object",
6516
+ properties: {
6517
+ session_id: { type: "string" },
6518
+ peer_id: { type: "string" },
6519
+ },
6520
+ required: ["session_id", "peer_id"],
6521
+ additionalProperties: false,
6522
+ },
6523
+ },
6524
+ {
6525
+ name: "codevelop_status",
6526
+ description: "Return status, peers, queue counts, and stream counts for a co-development session.",
6527
+ inputSchema: {
6528
+ type: "object",
6529
+ properties: {
6530
+ session_id: { type: "string", description: "Optional session ID. Omit to list all codevelop sessions." },
6531
+ },
6532
+ additionalProperties: false,
6533
+ },
6534
+ },
6535
+ {
6536
+ name: "codevelop_stop",
6537
+ description: "Stop a co-development session and notify joined peers.",
6538
+ inputSchema: {
6539
+ type: "object",
6540
+ properties: {
6541
+ session_id: { type: "string" },
6542
+ },
6543
+ required: ["session_id"],
6544
+ additionalProperties: false,
6545
+ },
6546
+ },
6547
+
6001
6548
  // ── Google Workspace (gws CLI) ──────────────────────────────────────────
6002
6549
  {
6003
6550
  name: "gws_run",
@@ -6838,6 +7385,54 @@ async function handleMcpTool(vault, name, args) {
6838
7385
  return mcpResult(JSON.stringify(result));
6839
7386
  }
6840
7387
 
7388
+ case "codevelop_start": {
7389
+ return mcpResult(JSON.stringify(startCodevelopSession(args || {})));
7390
+ }
7391
+
7392
+ case "codevelop_join": {
7393
+ const result = joinCodevelopSession(args || {});
7394
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7395
+ return mcpResult(JSON.stringify(result));
7396
+ }
7397
+
7398
+ case "codevelop_send": {
7399
+ const result = sendCodevelopMessage(args || {});
7400
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7401
+ return mcpResult(JSON.stringify(result));
7402
+ }
7403
+
7404
+ case "codevelop_poll": {
7405
+ const result = pollCodevelopMessages(args || {});
7406
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7407
+ return mcpResult(JSON.stringify(result));
7408
+ }
7409
+
7410
+ case "codevelop_stream": {
7411
+ const { session_id, peer_id } = args || {};
7412
+ if (!session_id || !peer_id) return mcpError("session_id and peer_id required");
7413
+ const session = terminalSessions.get(session_id);
7414
+ if (!session || !session.is_codevelop) return mcpError(`not_found: Codevelop session ${session_id} not found`);
7415
+ const peer = session.peers[normalizePeerId(peer_id)];
7416
+ if (!peer) return mcpError(`not_joined: Peer ${peer_id} has not joined session ${session_id}`);
7417
+ const stream_url = `${mcpHttpBaseUrl}/codevelop/${encodeURIComponent(session_id)}/${encodeURIComponent(normalizePeerId(peer_id))}/stream`;
7418
+ return mcpResult(JSON.stringify({ session_id, peer_id: normalizePeerId(peer_id), stream_url }));
7419
+ }
7420
+
7421
+ case "codevelop_status": {
7422
+ const { session_id } = args || {};
7423
+ const result = session_id ? statusCodevelopSession(session_id) : listCodevelopSessions();
7424
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7425
+ return mcpResult(JSON.stringify(result));
7426
+ }
7427
+
7428
+ case "codevelop_stop": {
7429
+ const { session_id } = args || {};
7430
+ if (!session_id) return mcpError("session_id required");
7431
+ const result = stopCodevelopSession(session_id);
7432
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
7433
+ return mcpResult(JSON.stringify(result));
7434
+ }
7435
+
6841
7436
  default:
6842
7437
  return mcpError(`Unknown tool: ${name}`);
6843
7438
  }