@lifeaitools/clauth 1.19.0 → 1.19.2

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.
@@ -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 as remote and fall through to MCP handling
4096
- req._clauthRemote = true;
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 = "";
@@ -7989,7 +8013,18 @@ function generateSessionId() {
7989
8013
  return crypto.randomUUID();
7990
8014
  }
7991
8015
 
7992
- function startTerminalSession(name, knowledge_tier, context_md, cwd) {
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}\nReady. Await instructions.`;
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
- cwd: cwd || CHITCHAT_FALLBACK_CWD,
8046
+ use_warm,
8047
+ cwd: cwd || defaultTerminalCwd(),
8012
8048
  };
8013
8049
 
8014
- // Try to acquire a warm worker from the agent pool for low-latency turns
8015
- try {
8016
- const pool = getAgentPool();
8017
- if (pool) {
8018
- const worker = pool.acquireForSession(session_id);
8019
- if (worker) {
8020
- session.warmWorker = worker;
8021
- console.log(`[terminal] session ${session_id} acquired warm worker`);
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 fullPrompt = `${session.context}\n\n---\nUser: ${message}`;
8087
-
8088
- const proc = spawnProc(binary, ['-p', fullPrompt, '--dangerously-skip-permissions'], {
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: true,
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 = await resolveChitchatRoot(vault);
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");
@@ -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 (!STYLE_WRITE_EXTENSIONS.has(path.extname(candidate).toLowerCase())) return null;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {