@lifeaitools/clauth 1.19.0 → 1.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/commands/serve.js +277 -109
- package/cli/studio-debug.js +60 -3
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -747,9 +747,9 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
747
747
|
.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}
|
|
748
748
|
.btn-ccandme:hover{background:linear-gradient(135deg,#2d1b69,#1e1b4b);border-color:#7c3aed;color:#c4b5fd;transform:translateY(-1px)}
|
|
749
749
|
.btn-ccandme:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
750
|
-
.btn-
|
|
751
|
-
.btn-
|
|
752
|
-
.btn-
|
|
750
|
+
.btn-tintin{background:linear-gradient(135deg,#103c2f,#0f2f3f);color:#86efac;border:1px solid #166534;padding:7px 16px;font-size:.85rem;border-radius:7px;cursor:pointer;font-weight:700;transition:all .15s;white-space:nowrap}
|
|
751
|
+
.btn-tintin:hover{background:linear-gradient(135deg,#14532d,#155e75);border-color:#22c55e;color:#bbf7d0;transform:translateY(-1px)}
|
|
752
|
+
.btn-tintin:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
|
753
753
|
.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}
|
|
754
754
|
.btn-claude:hover{filter:brightness(1.1);transform:translateY(-1px)}
|
|
755
755
|
.btn-claude:disabled{opacity:.4;cursor:not-allowed;transform:none;filter:none}
|
|
@@ -766,22 +766,22 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
766
766
|
.build-label{font-size:.85rem;color:#94a3b8;flex:1}
|
|
767
767
|
.build-label strong{color:#e2e8f0}
|
|
768
768
|
.build-meta{font-family:'Courier New',monospace;font-size:.75rem;color:#64748b}
|
|
769
|
-
.
|
|
770
|
-
.
|
|
771
|
-
.
|
|
772
|
-
.
|
|
773
|
-
.
|
|
774
|
-
.
|
|
775
|
-
.
|
|
776
|
-
.
|
|
777
|
-
.
|
|
778
|
-
.
|
|
779
|
-
.
|
|
780
|
-
.
|
|
781
|
-
.
|
|
782
|
-
.
|
|
783
|
-
.
|
|
784
|
-
@media (max-width:720px){.
|
|
769
|
+
.tintin-panel{display:none;position:fixed;top:0;right:0;bottom:0;z-index:80;width:min(420px,calc(100vw - 24px));background:#081c1a;border-left:1px solid #14532d;padding:1rem 1.25rem;box-shadow:-22px 0 60px rgba(0,0,0,.42);overflow:auto}
|
|
770
|
+
.tintin-panel.open{display:block}
|
|
771
|
+
.tintin-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:.8rem}
|
|
772
|
+
.tintin-title{font-size:.95rem;font-weight:700;color:#dcfce7}
|
|
773
|
+
.tintin-sub{font-size:.78rem;color:#6ee7b7;margin-top:2px}
|
|
774
|
+
.tintin-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
|
|
775
|
+
.tintin-field{display:flex;flex-direction:column;gap:4px}
|
|
776
|
+
.tintin-field label{font-size:.72rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;font-weight:700}
|
|
777
|
+
.tintin-input,.tintin-textarea{background:#031312;border:1px solid #14532d;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.85rem;padding:8px 10px;outline:none;transition:border-color .15s}
|
|
778
|
+
.tintin-input:focus,.tintin-textarea:focus{border-color:#22c55e}
|
|
779
|
+
.tintin-textarea{width:100%;min-height:92px;resize:vertical;line-height:1.45;margin-bottom:10px}
|
|
780
|
+
.tintin-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
781
|
+
.tintin-status{font-family:'Courier New',monospace;font-size:.78rem;color:#94a3b8}
|
|
782
|
+
.tintin-result{display:none;margin-top:12px;background:#020b0a;border:1px solid #134e4a;border-radius:8px;padding:10px;white-space:pre-wrap;word-break:break-word;font-family:'Courier New',monospace;font-size:.82rem;color:#bbf7d0;max-height:240px;overflow:auto}
|
|
783
|
+
.tintin-result.open{display:block}
|
|
784
|
+
@media (max-width:720px){.tintin-panel{width:100vw}.tintin-grid{grid-template-columns:1fr}}
|
|
785
785
|
.mcp-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}
|
|
786
786
|
.mcp-label{font-size:.72rem;color:#64748b;min-width:80px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
|
|
787
787
|
.mcp-val{flex:1;font-family:'Courier New',monospace;font-size:.82rem;color:#60a5fa;background:#0a0f1a;border:1px solid #1e3a5f;border-radius:4px;padding:6px 10px;word-break:break-all;user-select:all}
|
|
@@ -950,7 +950,7 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
950
950
|
<button class="btn-refresh" onclick="loadServices()">↻ Refresh</button>
|
|
951
951
|
<button class="btn-add" onclick="toggleAddService()">+ Add Service</button>
|
|
952
952
|
<button class="btn-check" id="check-btn" onclick="checkAll()">⬤ Check All</button>
|
|
953
|
-
<button class="btn-monkey" id="
|
|
953
|
+
<button class="btn-monkey" id="tintin-btn" onclick="toggleTinTinPanel()" title="Open TinTin local CLI dispatch panel">TinTin</button>
|
|
954
954
|
<button class="btn-ccandme" id="ccandme-btn" onclick="launchCCandMe()" title="Launch CCandMe — 4-pane WezTerm: Claude + Codex + Me">⚡ CCandMe</button>
|
|
955
955
|
<button class="btn-lock" id="unlock-writes-btn" onclick="unlockWrites()" title="Enter password to enable saving changes this browser session (needed after a daemon restart when auto-unlocked via --pw)">🔓 Unlock Writes</button>
|
|
956
956
|
<button class="btn-lock" onclick="lockVault()">🔒 Lock</button>
|
|
@@ -1079,31 +1079,31 @@ function dashboardHtml(port, whitelist, isStaged = false, initWriteToken = null)
|
|
|
1079
1079
|
</div>
|
|
1080
1080
|
</div>
|
|
1081
1081
|
|
|
1082
|
-
<div class="
|
|
1083
|
-
<div class="
|
|
1082
|
+
<div class="tintin-panel" id="tintin-panel">
|
|
1083
|
+
<div class="tintin-head">
|
|
1084
1084
|
<div>
|
|
1085
|
-
<div class="
|
|
1086
|
-
<div class="
|
|
1085
|
+
<div class="tintin-title">TinTin — clauth CLI dispatch</div>
|
|
1086
|
+
<div class="tintin-sub">Dispatches a Claude CLI agent with clauth context. Uses sonnet-4-6 model. Routes: /monkey/dispatch → Claude CLI → /monkey/dispatch/:job_id.</div>
|
|
1087
1087
|
</div>
|
|
1088
|
-
<button class="btn-cancel" onclick="
|
|
1088
|
+
<button class="btn-cancel" onclick="toggleTinTinPanel()">Close</button>
|
|
1089
1089
|
</div>
|
|
1090
|
-
<div class="
|
|
1091
|
-
<div class="
|
|
1090
|
+
<div class="tintin-grid">
|
|
1091
|
+
<div class="tintin-field">
|
|
1092
1092
|
<label>App slug</label>
|
|
1093
|
-
<input class="
|
|
1093
|
+
<input class="tintin-input" id="tintin-app" value="clauth-dashboard" spellcheck="false" autocomplete="off">
|
|
1094
1094
|
</div>
|
|
1095
|
-
<div class="
|
|
1095
|
+
<div class="tintin-field">
|
|
1096
1096
|
<label>CWD</label>
|
|
1097
|
-
<input class="
|
|
1097
|
+
<input class="tintin-input" id="tintin-cwd" value="C:\\Dev\\regen-root" spellcheck="false" autocomplete="off">
|
|
1098
1098
|
</div>
|
|
1099
1099
|
</div>
|
|
1100
|
-
<textarea class="
|
|
1101
|
-
<div class="
|
|
1102
|
-
<button class="btn-monkey" id="
|
|
1103
|
-
<button class="btn-check" onclick="
|
|
1104
|
-
<span class="
|
|
1100
|
+
<textarea class="tintin-textarea" id="tintin-prompt" spellcheck="false">Reply exactly: tintin-dashboard-ok</textarea>
|
|
1101
|
+
<div class="tintin-actions">
|
|
1102
|
+
<button class="btn-monkey" id="tintin-send" onclick="sendTinTinMessage()">Dispatch</button>
|
|
1103
|
+
<button class="btn-check" onclick="fillTinTinSmoke()" style="padding:7px 12px">Smoke Prompt</button>
|
|
1104
|
+
<span class="tintin-status" id="tintin-status">idle</span>
|
|
1105
1105
|
</div>
|
|
1106
|
-
<div class="
|
|
1106
|
+
<div class="tintin-result" id="tintin-result"></div>
|
|
1107
1107
|
</div>
|
|
1108
1108
|
|
|
1109
1109
|
|
|
@@ -1505,52 +1505,52 @@ async function launchCCandMe() {
|
|
|
1505
1505
|
}
|
|
1506
1506
|
}
|
|
1507
1507
|
|
|
1508
|
-
// ──
|
|
1509
|
-
let
|
|
1508
|
+
// ── TinTin local CLI dispatch sidebar ──
|
|
1509
|
+
let tintinPollTimer = null;
|
|
1510
1510
|
|
|
1511
|
-
function
|
|
1512
|
-
const panel = document.getElementById("
|
|
1511
|
+
function toggleTinTinPanel() {
|
|
1512
|
+
const panel = document.getElementById("tintin-panel");
|
|
1513
1513
|
if (!panel) return;
|
|
1514
1514
|
panel.classList.toggle("open");
|
|
1515
1515
|
}
|
|
1516
1516
|
|
|
1517
|
-
function
|
|
1518
|
-
const prompt = document.getElementById("
|
|
1519
|
-
const app = document.getElementById("
|
|
1517
|
+
function fillTinTinSmoke() {
|
|
1518
|
+
const prompt = document.getElementById("tintin-prompt");
|
|
1519
|
+
const app = document.getElementById("tintin-app");
|
|
1520
1520
|
if (app) app.value = "clauth-dashboard";
|
|
1521
|
-
if (prompt) prompt.value = "Reply exactly:
|
|
1521
|
+
if (prompt) prompt.value = "Reply exactly: tintin-dashboard-ok";
|
|
1522
1522
|
}
|
|
1523
1523
|
|
|
1524
|
-
function
|
|
1525
|
-
const el = document.getElementById("
|
|
1524
|
+
function setTinTinStatus(text) {
|
|
1525
|
+
const el = document.getElementById("tintin-status");
|
|
1526
1526
|
if (el) el.textContent = text || "";
|
|
1527
1527
|
}
|
|
1528
1528
|
|
|
1529
|
-
function
|
|
1530
|
-
const el = document.getElementById("
|
|
1529
|
+
function setTinTinResult(text, open) {
|
|
1530
|
+
const el = document.getElementById("tintin-result");
|
|
1531
1531
|
if (!el) return;
|
|
1532
1532
|
el.textContent = text || "";
|
|
1533
1533
|
if (open) el.classList.add("open");
|
|
1534
1534
|
else el.classList.remove("open");
|
|
1535
1535
|
}
|
|
1536
1536
|
|
|
1537
|
-
async function
|
|
1538
|
-
const sendBtn = document.getElementById("
|
|
1539
|
-
const promptEl = document.getElementById("
|
|
1540
|
-
const appEl = document.getElementById("
|
|
1541
|
-
const cwdEl = document.getElementById("
|
|
1537
|
+
async function sendTinTinMessage() {
|
|
1538
|
+
const sendBtn = document.getElementById("tintin-send");
|
|
1539
|
+
const promptEl = document.getElementById("tintin-prompt");
|
|
1540
|
+
const appEl = document.getElementById("tintin-app");
|
|
1541
|
+
const cwdEl = document.getElementById("tintin-cwd");
|
|
1542
1542
|
const prompt = (promptEl && promptEl.value || "").trim();
|
|
1543
1543
|
const appSlug = (appEl && appEl.value || "clauth-dashboard").trim() || "clauth-dashboard";
|
|
1544
1544
|
const cwd = (cwdEl && cwdEl.value || "").trim();
|
|
1545
1545
|
if (!prompt) {
|
|
1546
|
-
|
|
1546
|
+
setTinTinStatus("prompt required");
|
|
1547
1547
|
return;
|
|
1548
1548
|
}
|
|
1549
|
-
if (
|
|
1550
|
-
clearTimeout(
|
|
1551
|
-
|
|
1549
|
+
if (tintinPollTimer) {
|
|
1550
|
+
clearTimeout(tintinPollTimer);
|
|
1551
|
+
tintinPollTimer = null;
|
|
1552
1552
|
}
|
|
1553
|
-
const jobId = "
|
|
1553
|
+
const jobId = "tintin-" + Date.now();
|
|
1554
1554
|
const body = {
|
|
1555
1555
|
prompt,
|
|
1556
1556
|
job_id: jobId,
|
|
@@ -1558,14 +1558,14 @@ async function sendMonkeyDashboardMessage() {
|
|
|
1558
1558
|
agent_context: {
|
|
1559
1559
|
app: { slug: appSlug, route: "/clauth-dashboard", origin: window.location.origin },
|
|
1560
1560
|
repo: { root: cwd, cwd },
|
|
1561
|
-
runtime: { agent: "clauth-cli", requested_by: "clauth-dashboard" },
|
|
1561
|
+
runtime: { agent: "clauth-cli", requested_by: "clauth-dashboard", model: "claude-sonnet-4-6" },
|
|
1562
1562
|
task: { intent: "general_chat", thread_id: jobId }
|
|
1563
1563
|
}
|
|
1564
1564
|
};
|
|
1565
1565
|
|
|
1566
1566
|
if (sendBtn) { sendBtn.disabled = true; sendBtn.textContent = "Dispatching..."; }
|
|
1567
|
-
|
|
1568
|
-
|
|
1567
|
+
setTinTinStatus("dispatching " + jobId);
|
|
1568
|
+
setTinTinResult("", false);
|
|
1569
1569
|
try {
|
|
1570
1570
|
const response = await fetch(BASE + "/monkey/dispatch", {
|
|
1571
1571
|
method: "POST",
|
|
@@ -1574,35 +1574,35 @@ async function sendMonkeyDashboardMessage() {
|
|
|
1574
1574
|
});
|
|
1575
1575
|
const result = await response.json();
|
|
1576
1576
|
if (!response.ok || result.error) {
|
|
1577
|
-
|
|
1578
|
-
|
|
1577
|
+
setTinTinStatus("dispatch failed");
|
|
1578
|
+
setTinTinResult(JSON.stringify(result, null, 2), true);
|
|
1579
1579
|
return;
|
|
1580
1580
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1581
|
+
setTinTinStatus("spawned pid " + result.pid + " - polling");
|
|
1582
|
+
pollTinTinJob(jobId, 0);
|
|
1583
1583
|
} catch (err) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1584
|
+
setTinTinStatus("dispatch error");
|
|
1585
|
+
setTinTinResult(err && err.message ? err.message : String(err), true);
|
|
1586
1586
|
} finally {
|
|
1587
|
-
if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = "Dispatch
|
|
1587
|
+
if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = "Dispatch"; }
|
|
1588
1588
|
}
|
|
1589
1589
|
}
|
|
1590
1590
|
|
|
1591
|
-
async function
|
|
1591
|
+
async function pollTinTinJob(jobId, attempt) {
|
|
1592
1592
|
try {
|
|
1593
1593
|
const response = await fetch(BASE + "/monkey/dispatch/" + encodeURIComponent(jobId), { cache: "no-store" });
|
|
1594
1594
|
const job = await response.json();
|
|
1595
1595
|
if (!response.ok || job.error) {
|
|
1596
|
-
|
|
1597
|
-
|
|
1596
|
+
setTinTinStatus("job lookup failed");
|
|
1597
|
+
setTinTinResult(JSON.stringify(job, null, 2), true);
|
|
1598
1598
|
return;
|
|
1599
1599
|
}
|
|
1600
1600
|
if (job.status === "running" && attempt < 60) {
|
|
1601
|
-
|
|
1602
|
-
|
|
1601
|
+
setTinTinStatus("running pid " + (job.pid || "?") + " - " + (attempt + 1));
|
|
1602
|
+
tintinPollTimer = setTimeout(function() { pollTinTinJob(jobId, attempt + 1); }, 1500);
|
|
1603
1603
|
return;
|
|
1604
1604
|
}
|
|
1605
|
-
|
|
1605
|
+
setTinTinStatus(job.status + " exit " + job.exit_code);
|
|
1606
1606
|
const out = [
|
|
1607
1607
|
"status: " + job.status,
|
|
1608
1608
|
"job: " + jobId,
|
|
@@ -1612,10 +1612,10 @@ async function pollMonkeyDashboardJob(jobId, attempt) {
|
|
|
1612
1612
|
job.stdout || "",
|
|
1613
1613
|
job.stderr ? "\\nstderr:\\n" + job.stderr : ""
|
|
1614
1614
|
].join("\\n");
|
|
1615
|
-
|
|
1615
|
+
setTinTinResult(out, true);
|
|
1616
1616
|
} catch (err) {
|
|
1617
|
-
|
|
1618
|
-
|
|
1617
|
+
setTinTinStatus("poll error");
|
|
1618
|
+
setTinTinResult(err && err.message ? err.message : String(err), true);
|
|
1619
1619
|
}
|
|
1620
1620
|
}
|
|
1621
1621
|
|
|
@@ -3857,6 +3857,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3857
3857
|
const NOAUTH_HOSTS = ["fs.regendevcorp.com", "clauth.regendevcorp.com", "chitchat.regendevcorp.com"];
|
|
3858
3858
|
const requestHost = (req.headers.host || "").split(":")[0].toLowerCase();
|
|
3859
3859
|
const noAuthHost = NOAUTH_HOSTS.includes(requestHost);
|
|
3860
|
+
const localTrustedHost = ["127.0.0.1", "localhost", "::1", "[::1]"].includes(requestHost);
|
|
3860
3861
|
|
|
3861
3862
|
// ── OAuth Discovery (RFC 9728 + RFC 8414) ──────────────
|
|
3862
3863
|
// Suppress OAuth discovery on noauth hosts — prevents claude.ai from entering OAuth flow
|
|
@@ -4075,7 +4076,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4075
4076
|
const authHeader = req.headers.authorization;
|
|
4076
4077
|
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
4077
4078
|
|
|
4078
|
-
if (!noAuthHost && (!token || !oauthTokens.has(token))) {
|
|
4079
|
+
if (!noAuthHost && !localTrustedHost && (!token || !oauthTokens.has(token))) {
|
|
4079
4080
|
// No valid Bearer token → return 401 with discovery hint
|
|
4080
4081
|
const base = oauthBase();
|
|
4081
4082
|
const resourcePath = isMcpPath ? reqPath.slice(1) : "sse"; // "mcp", "gws", "clauth", or "sse"
|
|
@@ -4092,8 +4093,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
4092
4093
|
}));
|
|
4093
4094
|
}
|
|
4094
4095
|
|
|
4095
|
-
// Valid token — mark
|
|
4096
|
-
req._clauthRemote =
|
|
4096
|
+
// Valid token or trusted localhost — mark remote status and fall through to MCP handling
|
|
4097
|
+
req._clauthRemote = !localTrustedHost;
|
|
4097
4098
|
}
|
|
4098
4099
|
|
|
4099
4100
|
// ── MCP Streamable HTTP transport ──
|
|
@@ -5337,6 +5338,29 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
5337
5338
|
return;
|
|
5338
5339
|
}
|
|
5339
5340
|
|
|
5341
|
+
// POST /terminal/show — reveal/focus an active terminal session or open a visible inspector
|
|
5342
|
+
if (method === "POST" && reqPath === "/terminal/show") {
|
|
5343
|
+
let body = "";
|
|
5344
|
+
req.on("data", d => body += d);
|
|
5345
|
+
req.on("end", () => {
|
|
5346
|
+
try {
|
|
5347
|
+
const { session_id } = JSON.parse(body);
|
|
5348
|
+
if (!session_id) {
|
|
5349
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
5350
|
+
return res.end(JSON.stringify({ error: "session_id required" }));
|
|
5351
|
+
}
|
|
5352
|
+
const result = showTerminalSession(session_id);
|
|
5353
|
+
const status = result.error === 'not_found' ? 404 : result.error ? 500 : 200;
|
|
5354
|
+
res.writeHead(status, { "Content-Type": "application/json", ...CORS });
|
|
5355
|
+
res.end(JSON.stringify(result));
|
|
5356
|
+
} catch {
|
|
5357
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
5358
|
+
res.end(JSON.stringify({ error: "invalid JSON" }));
|
|
5359
|
+
}
|
|
5360
|
+
});
|
|
5361
|
+
return;
|
|
5362
|
+
}
|
|
5363
|
+
|
|
5340
5364
|
// POST /terminal/stop — stop a terminal session
|
|
5341
5365
|
if (method === "POST" && reqPath === "/terminal/stop") {
|
|
5342
5366
|
let body = "";
|
|
@@ -7868,20 +7892,20 @@ function buildMonkeyDispatchPrompt({ prompt, job_id, agent_context }) {
|
|
|
7868
7892
|
const normalized = normalizeAgentContext(agent_context);
|
|
7869
7893
|
const jobInstruction = job_id
|
|
7870
7894
|
? [
|
|
7871
|
-
`
|
|
7895
|
+
`TinTin job id: ${job_id}`,
|
|
7872
7896
|
"If the prompt does not contain all job details, use clauth at http://127.0.0.1:52437 to fetch exact credentials and query the app database/API directly.",
|
|
7873
7897
|
"Do not assume agent MCP/plugin tools are available to this spawned CLI process.",
|
|
7874
7898
|
].join("\n")
|
|
7875
7899
|
: "";
|
|
7876
7900
|
|
|
7877
7901
|
const body = prompt || [
|
|
7878
|
-
"Process this
|
|
7902
|
+
"Process this TinTin job using the provided agent context.",
|
|
7879
7903
|
jobInstruction,
|
|
7880
7904
|
"Find the job payload from the consuming app's declared data path before making changes.",
|
|
7881
7905
|
].filter(Boolean).join("\n\n");
|
|
7882
7906
|
|
|
7883
7907
|
return [
|
|
7884
|
-
"You are a clauth-spawned
|
|
7908
|
+
"You are a clauth-spawned TinTin CLI agent (model: claude-sonnet-4-6).",
|
|
7885
7909
|
"Agent context setup follows. Treat it as the runtime contract for this task.",
|
|
7886
7910
|
"```json",
|
|
7887
7911
|
JSON.stringify(normalized, null, 2),
|
|
@@ -7989,7 +8013,18 @@ function generateSessionId() {
|
|
|
7989
8013
|
return crypto.randomUUID();
|
|
7990
8014
|
}
|
|
7991
8015
|
|
|
7992
|
-
function
|
|
8016
|
+
function defaultTerminalCwd() {
|
|
8017
|
+
const dir = process.env.CLAUTH_TERMINAL_CWD || path.join(os.tmpdir(), "clauth-tintin-terminal");
|
|
8018
|
+
try {
|
|
8019
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8020
|
+
} catch {
|
|
8021
|
+
// If the neutral directory cannot be created, fall back to the user's home.
|
|
8022
|
+
return os.homedir();
|
|
8023
|
+
}
|
|
8024
|
+
return dir;
|
|
8025
|
+
}
|
|
8026
|
+
|
|
8027
|
+
function startTerminalSession(name, knowledge_tier, context_md, cwd, use_warm = false) {
|
|
7993
8028
|
const binary = findClaudeBinary();
|
|
7994
8029
|
if (!binary) {
|
|
7995
8030
|
return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
|
|
@@ -7998,7 +8033,7 @@ function startTerminalSession(name, knowledge_tier, context_md, cwd) {
|
|
|
7998
8033
|
const preamble = context_md
|
|
7999
8034
|
? `${context_md}\n\n---\n`
|
|
8000
8035
|
: '';
|
|
8001
|
-
const initialContext = `${preamble}Session: ${name}, Tier: ${knowledge_tier}\
|
|
8036
|
+
const initialContext = `${preamble}Session: ${name}, Tier: ${knowledge_tier}\nConversation transcript follows. Respond only to the final User message.`;
|
|
8002
8037
|
const session = {
|
|
8003
8038
|
session_id,
|
|
8004
8039
|
name,
|
|
@@ -8008,26 +8043,31 @@ function startTerminalSession(name, knowledge_tier, context_md, cwd) {
|
|
|
8008
8043
|
context: initialContext,
|
|
8009
8044
|
activeProc: null,
|
|
8010
8045
|
warmWorker: null,
|
|
8011
|
-
|
|
8046
|
+
use_warm,
|
|
8047
|
+
cwd: cwd || defaultTerminalCwd(),
|
|
8012
8048
|
};
|
|
8013
8049
|
|
|
8014
|
-
// Try to acquire a warm worker
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8050
|
+
// Try to acquire a warm worker only when explicitly requested. The warm pool
|
|
8051
|
+
// is optimized for call_agent one-shots; TinTin browser chat needs reliable
|
|
8052
|
+
// repeated turns more than experimental low-latency pinning.
|
|
8053
|
+
if (use_warm) {
|
|
8054
|
+
try {
|
|
8055
|
+
const pool = getAgentPool();
|
|
8056
|
+
if (pool) {
|
|
8057
|
+
const worker = pool.acquireForSession(session_id);
|
|
8058
|
+
if (worker) {
|
|
8059
|
+
session.warmWorker = worker;
|
|
8060
|
+
console.log(`[terminal] session ${session_id} acquired warm worker`);
|
|
8061
|
+
}
|
|
8022
8062
|
}
|
|
8063
|
+
} catch (e) {
|
|
8064
|
+
console.log(`[terminal] session ${session_id} warm pool unavailable, will use cold path: ${e.message}`);
|
|
8023
8065
|
}
|
|
8024
|
-
} catch (e) {
|
|
8025
|
-
console.log(`[terminal] session ${session_id} warm pool unavailable, will use cold path: ${e.message}`);
|
|
8026
8066
|
}
|
|
8027
8067
|
|
|
8028
8068
|
terminalSessions.set(session_id, session);
|
|
8029
8069
|
console.log(`[terminal] started session ${session_id} name=${name} cwd=${session.cwd} warm=${!!session.warmWorker}`);
|
|
8030
|
-
return { session_id, status: 'ready' };
|
|
8070
|
+
return { session_id, status: 'ready', warm: !!session.warmWorker };
|
|
8031
8071
|
}
|
|
8032
8072
|
|
|
8033
8073
|
function sendTerminalMessage(session_id, message) {
|
|
@@ -8036,6 +8076,11 @@ function sendTerminalMessage(session_id, message) {
|
|
|
8036
8076
|
if (session.status === 'stopped') return { error: 'stopped', message: 'Session is stopped' };
|
|
8037
8077
|
if (session.status === 'busy') return { error: 'session_busy', message: 'Session is busy — try again shortly' };
|
|
8038
8078
|
|
|
8079
|
+
const nextTurn = (session.turn || 0) + 1;
|
|
8080
|
+
session.lastResponse = undefined;
|
|
8081
|
+
session.lastResponseAt = null;
|
|
8082
|
+
session.pendingTurn = nextTurn;
|
|
8083
|
+
|
|
8039
8084
|
// ── Warm-pool path: dispatch to the pinned worker (maintains its own context) ──
|
|
8040
8085
|
if (session.warmWorker && !session.warmWorker.isDead()) {
|
|
8041
8086
|
session.status = 'busy';
|
|
@@ -8053,7 +8098,8 @@ function sendTerminalMessage(session_id, message) {
|
|
|
8053
8098
|
s.context = combined.length > 8000 ? combined.slice(combined.length - 8000) : combined;
|
|
8054
8099
|
s.lastResponse = response;
|
|
8055
8100
|
s.lastResponseAt = new Date().toISOString();
|
|
8056
|
-
s.turn = (s.turn || 0) + 1;
|
|
8101
|
+
s.turn = s.pendingTurn || ((s.turn || 0) + 1);
|
|
8102
|
+
s.pendingTurn = null;
|
|
8057
8103
|
s.status = 'ready';
|
|
8058
8104
|
s.activeProc = null;
|
|
8059
8105
|
console.log(`[terminal] session ${session_id} turn=${s.turn} complete (warm) ok=${result.ok}`);
|
|
@@ -8069,13 +8115,14 @@ function sendTerminalMessage(session_id, message) {
|
|
|
8069
8115
|
const s = terminalSessions.get(session_id);
|
|
8070
8116
|
s.lastResponse = `(warm dispatch failed: ${err.message})`;
|
|
8071
8117
|
s.lastResponseAt = new Date().toISOString();
|
|
8072
|
-
s.turn = (s.turn || 0) + 1;
|
|
8118
|
+
s.turn = s.pendingTurn || ((s.turn || 0) + 1);
|
|
8119
|
+
s.pendingTurn = null;
|
|
8073
8120
|
s.status = 'ready';
|
|
8074
8121
|
s.activeProc = null;
|
|
8075
8122
|
s.warmWorker = null; // fall back to cold on next turn
|
|
8076
8123
|
console.log(`[terminal] session ${session_id} warm dispatch threw, falling back to cold: ${err.message}`);
|
|
8077
8124
|
});
|
|
8078
|
-
return { queued: true, session_id, warm: true };
|
|
8125
|
+
return { queued: true, session_id, warm: true, turn: nextTurn };
|
|
8079
8126
|
}
|
|
8080
8127
|
|
|
8081
8128
|
// ── Cold-spawn fallback: original path ──
|
|
@@ -8083,13 +8130,16 @@ function sendTerminalMessage(session_id, message) {
|
|
|
8083
8130
|
if (!binary) return { error: 'binary_not_found', message: 'claude CLI not found in PATH or AppData/npm' };
|
|
8084
8131
|
|
|
8085
8132
|
session.status = 'busy';
|
|
8086
|
-
const
|
|
8087
|
-
|
|
8088
|
-
const
|
|
8133
|
+
const isCmdShim = process.platform === "win32" && /\.cmd$/i.test(binary);
|
|
8134
|
+
const command = isCmdShim ? "cmd" : binary;
|
|
8135
|
+
const args = isCmdShim
|
|
8136
|
+
? ["/d", "/s", "/c", `"${binary}"`, "-p", message, "--system-prompt", session.context, "--setting-sources", "user", "--dangerously-skip-permissions"]
|
|
8137
|
+
: ["-p", message, "--system-prompt", session.context, "--setting-sources", "user", "--dangerously-skip-permissions"];
|
|
8138
|
+
const proc = spawnProc(command, args, {
|
|
8089
8139
|
cwd: session.cwd || CHITCHAT_FALLBACK_CWD,
|
|
8090
8140
|
env: process.env,
|
|
8091
8141
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8092
|
-
shell:
|
|
8142
|
+
shell: false,
|
|
8093
8143
|
windowsHide: true,
|
|
8094
8144
|
});
|
|
8095
8145
|
session.activeProc = proc;
|
|
@@ -8108,14 +8158,15 @@ function sendTerminalMessage(session_id, message) {
|
|
|
8108
8158
|
s.context = combined.length > 8000 ? combined.slice(combined.length - 8000) : combined;
|
|
8109
8159
|
s.lastResponse = response;
|
|
8110
8160
|
s.lastResponseAt = new Date().toISOString();
|
|
8111
|
-
s.turn = (s.turn || 0) + 1;
|
|
8161
|
+
s.turn = s.pendingTurn || ((s.turn || 0) + 1);
|
|
8162
|
+
s.pendingTurn = null;
|
|
8112
8163
|
s.status = 'ready';
|
|
8113
8164
|
s.activeProc = null;
|
|
8114
8165
|
console.log(`[terminal] session ${session_id} turn=${s.turn} complete code=${code}`);
|
|
8115
8166
|
}
|
|
8116
8167
|
});
|
|
8117
8168
|
|
|
8118
|
-
return { queued: true, session_id, warm: false };
|
|
8169
|
+
return { queued: true, session_id, warm: false, turn: nextTurn };
|
|
8119
8170
|
}
|
|
8120
8171
|
|
|
8121
8172
|
function recvTerminalResponse(session_id, timeout_ms = 30000) {
|
|
@@ -8157,6 +8208,101 @@ function listTerminalSessions() {
|
|
|
8157
8208
|
return sessions;
|
|
8158
8209
|
}
|
|
8159
8210
|
|
|
8211
|
+
function showTerminalSession(session_id) {
|
|
8212
|
+
const session = terminalSessions.get(session_id);
|
|
8213
|
+
if (!session) return { error: 'not_found', message: `Session ${session_id} not found` };
|
|
8214
|
+
|
|
8215
|
+
const pid = session.activeProc?.pid || null;
|
|
8216
|
+
if (process.platform === "win32" && pid) {
|
|
8217
|
+
try {
|
|
8218
|
+
const script = [
|
|
8219
|
+
"Add-Type @'",
|
|
8220
|
+
"using System;",
|
|
8221
|
+
"using System.Runtime.InteropServices;",
|
|
8222
|
+
"public class Win32ShowWindow {",
|
|
8223
|
+
" [DllImport(\"user32.dll\")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);",
|
|
8224
|
+
" [DllImport(\"user32.dll\")] public static extern bool SetForegroundWindow(IntPtr hWnd);",
|
|
8225
|
+
"}",
|
|
8226
|
+
"'@",
|
|
8227
|
+
`$p = Get-Process -Id ${Number(pid)} -ErrorAction SilentlyContinue`,
|
|
8228
|
+
"if ($p -and $p.MainWindowHandle -ne 0) {",
|
|
8229
|
+
" [Win32ShowWindow]::ShowWindowAsync($p.MainWindowHandle, 5) | Out-Null",
|
|
8230
|
+
" [Win32ShowWindow]::SetForegroundWindow($p.MainWindowHandle) | Out-Null",
|
|
8231
|
+
" 'shown'",
|
|
8232
|
+
"} else {",
|
|
8233
|
+
" 'no-window'",
|
|
8234
|
+
"}",
|
|
8235
|
+
].join("\n");
|
|
8236
|
+
const result = spawnSync("powershell.exe", [
|
|
8237
|
+
"-NoProfile",
|
|
8238
|
+
"-ExecutionPolicy",
|
|
8239
|
+
"Bypass",
|
|
8240
|
+
"-Command",
|
|
8241
|
+
script,
|
|
8242
|
+
], {
|
|
8243
|
+
encoding: "utf8",
|
|
8244
|
+
windowsHide: true,
|
|
8245
|
+
timeout: 5000,
|
|
8246
|
+
});
|
|
8247
|
+
const stdout = String(result.stdout || "").trim();
|
|
8248
|
+
if (stdout.includes("shown")) {
|
|
8249
|
+
session.lastShownAt = new Date().toISOString();
|
|
8250
|
+
return { shown: true, mode: "active_process", session_id, pid };
|
|
8251
|
+
}
|
|
8252
|
+
} catch (e) {
|
|
8253
|
+
console.log(`[terminal] session ${session_id} active process show failed: ${e.message}`);
|
|
8254
|
+
}
|
|
8255
|
+
}
|
|
8256
|
+
|
|
8257
|
+
if (process.platform === "win32") {
|
|
8258
|
+
try {
|
|
8259
|
+
const title = `TinTin ${session.name || session_id}`;
|
|
8260
|
+
const lines = [
|
|
8261
|
+
`$Host.UI.RawUI.WindowTitle = ${JSON.stringify(title)}`,
|
|
8262
|
+
`Set-Location -LiteralPath ${JSON.stringify(session.cwd || os.homedir())}`,
|
|
8263
|
+
"Clear-Host",
|
|
8264
|
+
`Write-Host ${JSON.stringify(`TinTin session ${session_id}`)} -ForegroundColor Cyan`,
|
|
8265
|
+
`Write-Host ${JSON.stringify(`Name: ${session.name || ""}`)}`,
|
|
8266
|
+
`Write-Host ${JSON.stringify(`Status: ${session.status}`)}`,
|
|
8267
|
+
`Write-Host ${JSON.stringify(`CWD: ${session.cwd || ""}`)}`,
|
|
8268
|
+
`Write-Host ${JSON.stringify(`Turn: ${session.turn || 0}`)}`,
|
|
8269
|
+
`Write-Host ${JSON.stringify("This viewer is attached to clauth session metadata. If a Claude turn is active, terminal_show focuses it; otherwise this is the visible session inspection point.")} -ForegroundColor Yellow`,
|
|
8270
|
+
"Write-Host ''",
|
|
8271
|
+
"Write-Host 'Recent response preview:' -ForegroundColor Green",
|
|
8272
|
+
`Write-Host ${JSON.stringify((session.lastResponse || "").slice(0, 1500))}`,
|
|
8273
|
+
"Write-Host ''",
|
|
8274
|
+
"Write-Host 'Leave this window open or close it when done.' -ForegroundColor DarkGray",
|
|
8275
|
+
].join("; ");
|
|
8276
|
+
const child = spawnProc("powershell.exe", [
|
|
8277
|
+
"-NoExit",
|
|
8278
|
+
"-NoProfile",
|
|
8279
|
+
"-ExecutionPolicy",
|
|
8280
|
+
"Bypass",
|
|
8281
|
+
"-Command",
|
|
8282
|
+
lines,
|
|
8283
|
+
], {
|
|
8284
|
+
cwd: session.cwd || os.homedir(),
|
|
8285
|
+
env: process.env,
|
|
8286
|
+
stdio: "ignore",
|
|
8287
|
+
detached: true,
|
|
8288
|
+
windowsHide: false,
|
|
8289
|
+
});
|
|
8290
|
+
child.unref?.();
|
|
8291
|
+
session.lastShownAt = new Date().toISOString();
|
|
8292
|
+
return { shown: true, mode: "session_inspector", session_id, pid: child.pid || null };
|
|
8293
|
+
} catch (e) {
|
|
8294
|
+
return { error: "show_failed", message: e.message };
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
return {
|
|
8299
|
+
shown: false,
|
|
8300
|
+
mode: "unsupported_platform",
|
|
8301
|
+
session_id,
|
|
8302
|
+
message: "terminal_show currently opens/focuses local Windows sessions only.",
|
|
8303
|
+
};
|
|
8304
|
+
}
|
|
8305
|
+
|
|
8160
8306
|
function stopTerminalSession(session_id) {
|
|
8161
8307
|
const session = terminalSessions.get(session_id);
|
|
8162
8308
|
if (!session) return { error: 'not_found', message: `Session ${session_id} not found` };
|
|
@@ -9317,6 +9463,8 @@ const MCP_TOOLS = [
|
|
|
9317
9463
|
name: { type: "string", description: "Human-readable session name (e.g. 'knowledgedude-prt')" },
|
|
9318
9464
|
knowledge_tier: { type: "string", enum: ["db_only", "corpus", "project"], description: "Knowledge tier for the session. 'corpus' injects context_md as preamble." },
|
|
9319
9465
|
context_md: { type: "string", description: "Optional markdown preamble injected as session context (used for 'corpus' tier)" },
|
|
9466
|
+
cwd: { type: "string", description: "Optional working directory. Defaults to a neutral temp directory so browser chat avoids repo hooks." },
|
|
9467
|
+
use_warm: { type: "boolean", description: "Opt into experimental pinned warm-worker mode. Defaults to false for reliable browser chat turns." },
|
|
9320
9468
|
},
|
|
9321
9469
|
required: ["name"],
|
|
9322
9470
|
additionalProperties: false,
|
|
@@ -9340,6 +9488,18 @@ const MCP_TOOLS = [
|
|
|
9340
9488
|
description: "List all active terminal sessions with their status (ready/busy/stopped), knowledge tier, and start time.",
|
|
9341
9489
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
9342
9490
|
},
|
|
9491
|
+
{
|
|
9492
|
+
name: "terminal_show",
|
|
9493
|
+
description: "Reveal/focus a TinTin terminal session on this Windows machine. If no live Claude process window exists, opens a visible session inspector at the session cwd.",
|
|
9494
|
+
inputSchema: {
|
|
9495
|
+
type: "object",
|
|
9496
|
+
properties: {
|
|
9497
|
+
session_id: { type: "string", description: "Session ID returned by terminal_start" },
|
|
9498
|
+
},
|
|
9499
|
+
required: ["session_id"],
|
|
9500
|
+
additionalProperties: false,
|
|
9501
|
+
},
|
|
9502
|
+
},
|
|
9343
9503
|
{
|
|
9344
9504
|
name: "terminal_recv",
|
|
9345
9505
|
description: "Read the last response from a terminal session. Returns response if ready, or status='busy' if still processing. Poll every 3-5 seconds until status='ready'.",
|
|
@@ -11085,10 +11245,10 @@ async function handleMcpTool(vault, name, args) {
|
|
|
11085
11245
|
}
|
|
11086
11246
|
|
|
11087
11247
|
case "terminal_start": {
|
|
11088
|
-
const { name, knowledge_tier, context_md } = args;
|
|
11248
|
+
const { name, knowledge_tier, context_md, use_warm, cwd: requestedCwd } = args;
|
|
11089
11249
|
if (!name) return mcpError("name required");
|
|
11090
|
-
const cwd =
|
|
11091
|
-
const result = startTerminalSession(name, knowledge_tier || 'db_only', context_md || null, cwd);
|
|
11250
|
+
const cwd = requestedCwd ? path.resolve(String(requestedCwd)) : defaultTerminalCwd();
|
|
11251
|
+
const result = startTerminalSession(name, knowledge_tier || 'db_only', context_md || null, cwd, use_warm === true);
|
|
11092
11252
|
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
11093
11253
|
return mcpResult(JSON.stringify(result));
|
|
11094
11254
|
}
|
|
@@ -11105,6 +11265,14 @@ async function handleMcpTool(vault, name, args) {
|
|
|
11105
11265
|
return mcpResult(JSON.stringify(listTerminalSessions()));
|
|
11106
11266
|
}
|
|
11107
11267
|
|
|
11268
|
+
case "terminal_show": {
|
|
11269
|
+
const { session_id } = args;
|
|
11270
|
+
if (!session_id) return mcpError("session_id required");
|
|
11271
|
+
const result = showTerminalSession(session_id);
|
|
11272
|
+
if (result.error) return mcpError(`${result.error}: ${result.message}`);
|
|
11273
|
+
return mcpResult(JSON.stringify(result));
|
|
11274
|
+
}
|
|
11275
|
+
|
|
11108
11276
|
case "terminal_recv": {
|
|
11109
11277
|
const { session_id } = args;
|
|
11110
11278
|
if (!session_id) return mcpError("session_id required");
|
package/cli/studio-debug.js
CHANGED
|
@@ -9,6 +9,7 @@ const MAX_POLL_TIMEOUT = 600_000;
|
|
|
9
9
|
const MAX_EVENT_QUEUE = 100;
|
|
10
10
|
const MAX_SNIPPET = 2_000;
|
|
11
11
|
const STYLE_WRITE_EXTENSIONS = new Set([".css", ".scss", ".sass", ".less", ".html", ".jsx", ".tsx"]);
|
|
12
|
+
const TEXT_WRITE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".html", ".md", ".txt", ".css", ".scss"]);
|
|
12
13
|
|
|
13
14
|
const APP_TARGETS = {
|
|
14
15
|
studio_test: {
|
|
@@ -99,16 +100,34 @@ function writeJson(res, status, data, cors) {
|
|
|
99
100
|
res.end(JSON.stringify(data));
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
function resolveSourceFile(session, file) {
|
|
103
|
+
function resolveSourceFile(session, file, allowedExtensions = STYLE_WRITE_EXTENSIONS) {
|
|
103
104
|
if (!file || typeof file !== "string") return null;
|
|
104
105
|
const root = path.resolve(session.repoRoot || session.cwd || process.cwd());
|
|
105
106
|
const candidate = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file);
|
|
106
107
|
const rel = path.relative(root, candidate);
|
|
107
108
|
if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
|
|
108
|
-
if (!
|
|
109
|
+
if (!allowedExtensions.has(path.extname(candidate).toLowerCase())) return null;
|
|
109
110
|
return { root, path: candidate, relativePath: rel.replace(/\\/g, "/") };
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function applyTextRangeEdit(sourceText, textEdit) {
|
|
114
|
+
const source = textEdit.source;
|
|
115
|
+
if (!source || !source.range || typeof source.range.start !== "number" || typeof source.range.end !== "number") {
|
|
116
|
+
throw new Error("text-write requires source.range.start and source.range.end.");
|
|
117
|
+
}
|
|
118
|
+
const { start, end } = source.range;
|
|
119
|
+
if (start < 0 || end < start || end > sourceText.length) {
|
|
120
|
+
throw new Error(`text-write source range [${start}, ${end}] is out of bounds (file length: ${sourceText.length}).`);
|
|
121
|
+
}
|
|
122
|
+
if (textEdit.oldText !== undefined) {
|
|
123
|
+
const oldSlice = sourceText.slice(start, end);
|
|
124
|
+
if (oldSlice !== textEdit.oldText) {
|
|
125
|
+
throw new Error(`text-write oldText mismatch at [${start}, ${end}]: expected "${String(textEdit.oldText).slice(0, 80)}", found "${oldSlice.slice(0, 80)}".`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return `${sourceText.slice(0, start)}${textEdit.newText}${sourceText.slice(end)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
112
131
|
function escapeRegExp(value) {
|
|
113
132
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
114
133
|
}
|
|
@@ -451,6 +470,40 @@ export class StudioDebugSessionStore {
|
|
|
451
470
|
return { ok: true, status: session.status, reply };
|
|
452
471
|
}
|
|
453
472
|
|
|
473
|
+
writeText(sessionId, body) {
|
|
474
|
+
const auth = this.authenticate(sessionId, body.token);
|
|
475
|
+
if (auth.error) return auth;
|
|
476
|
+
const session = auth.session;
|
|
477
|
+
const textEdit = body.textEdit && typeof body.textEdit === "object" ? body.textEdit : {};
|
|
478
|
+
const source = textEdit.source && typeof textEdit.source === "object" ? textEdit.source : {};
|
|
479
|
+
const file = source.file || textEdit.file || body.file;
|
|
480
|
+
const fileInfo = resolveSourceFile(session, file, TEXT_WRITE_EXTENSIONS);
|
|
481
|
+
if (!fileInfo) {
|
|
482
|
+
return { error: "invalid_source", message: `text-write requires a source file inside the debug session repo (got: ${file || "(none)"}).` };
|
|
483
|
+
}
|
|
484
|
+
if (typeof textEdit.newText !== "string") {
|
|
485
|
+
return { error: "invalid_body", message: "text-write requires textEdit.newText." };
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const original = fs.readFileSync(fileInfo.path, "utf8");
|
|
489
|
+
const updated = applyTextRangeEdit(original, { ...textEdit, source });
|
|
490
|
+
fs.writeFileSync(fileInfo.path, updated, "utf8");
|
|
491
|
+
const reply = {
|
|
492
|
+
eventId: body.id || `text_${crypto.randomUUID()}`,
|
|
493
|
+
status: "done",
|
|
494
|
+
message: `Direct text write (source_replace [${source.range?.start}, ${source.range?.end}])`,
|
|
495
|
+
filesChanged: [fileInfo.relativePath],
|
|
496
|
+
createdAt: nowIso(),
|
|
497
|
+
};
|
|
498
|
+
session.replies.push(reply);
|
|
499
|
+
session.status = "done";
|
|
500
|
+
session.updatedAt = nowIso();
|
|
501
|
+
return { ok: true, status: "done", eventId: reply.eventId, filesChanged: reply.filesChanged };
|
|
502
|
+
} catch (err) {
|
|
503
|
+
return { error: "text_write_failed", message: err instanceof Error ? err.message : String(err) };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
454
507
|
writeStyle(sessionId, body) {
|
|
455
508
|
const auth = this.authenticate(sessionId, body.token);
|
|
456
509
|
if (auth.error) return auth;
|
|
@@ -551,7 +604,7 @@ export function createStudioDebugRuntime(options) {
|
|
|
551
604
|
}
|
|
552
605
|
}
|
|
553
606
|
|
|
554
|
-
const debugMatch = reqPath.match(/^\/studio\/debug\/([^/]+)\/(events|style-write|status|stop)$/);
|
|
607
|
+
const debugMatch = reqPath.match(/^\/studio\/debug\/([^/]+)\/(events|style-write|text-write|status|stop)$/);
|
|
555
608
|
const claudeMatch = reqPath.match(/^\/studio\/claude\/([^/]+)\/(poll|reply|status)$/);
|
|
556
609
|
if (!debugMatch && !claudeMatch) return false;
|
|
557
610
|
const [, sessionId, action] = debugMatch || claudeMatch;
|
|
@@ -565,6 +618,10 @@ export function createStudioDebugRuntime(options) {
|
|
|
565
618
|
const result = store.writeStyle(sessionId, await readBody(req));
|
|
566
619
|
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 400 : 200, result, cors);
|
|
567
620
|
}
|
|
621
|
+
if (method === "POST" && action === "text-write") {
|
|
622
|
+
const result = store.writeText(sessionId, await readBody(req));
|
|
623
|
+
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 400 : 200, result, cors);
|
|
624
|
+
}
|
|
568
625
|
if (method === "GET" && action === "poll") {
|
|
569
626
|
const result = await store.poll(sessionId, url.searchParams.get("token"), url.searchParams.get("timeout"));
|
|
570
627
|
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
|