clauder-hub 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server/dist/index.js +126 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauder-hub",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A local dashboard that runs, watches and automates the Claude Code sessions of every project on the machine",
5
5
  "license": "MIT",
6
6
  "author": "goldyndevs <goldyndevs@gmail.com>",
@@ -28560,14 +28560,14 @@ function claudeSettings() {
28560
28560
  }
28561
28561
  function saveClaudeSettings(input) {
28562
28562
  const next = { ...claudeSettings(), ...input };
28563
- const check2 = (ok, message) => {
28563
+ const check3 = (ok, message) => {
28564
28564
  if (!ok) throw new Error(message);
28565
28565
  };
28566
- check2(!next.permissionMode || PERMISSION_MODES.includes(next.permissionMode), `Unknown permission mode '${next.permissionMode}'.`);
28567
- check2(!next.runPermissionMode || PERMISSION_MODES.includes(next.runPermissionMode), `Unknown permission mode '${next.runPermissionMode}'.`);
28568
- check2(!next.effort || EFFORTS.includes(next.effort), `Effort is one of ${EFFORTS.join(", ")}.`);
28569
- check2(word.test(next.model) && word.test(next.fallbackModel), "A model is one word, like opus or claude-sonnet-5-5.");
28570
- check2(["", "on", "off"].includes(next.chrome), "Claude in Chrome is on, off or your own setting.");
28566
+ check3(!next.permissionMode || PERMISSION_MODES.includes(next.permissionMode), `Unknown permission mode '${next.permissionMode}'.`);
28567
+ check3(!next.runPermissionMode || PERMISSION_MODES.includes(next.runPermissionMode), `Unknown permission mode '${next.runPermissionMode}'.`);
28568
+ check3(!next.effort || EFFORTS.includes(next.effort), `Effort is one of ${EFFORTS.join(", ")}.`);
28569
+ check3(word.test(next.model) && word.test(next.fallbackModel), "A model is one word, like opus or claude-sonnet-5-5.");
28570
+ check3(["", "on", "off"].includes(next.chrome), "Claude in Chrome is on, off or your own setting.");
28571
28571
  next.addDirs = (Array.isArray(next.addDirs) ? next.addDirs : []).map((dir) => String(dir).trim()).filter(Boolean);
28572
28572
  db.prepare("INSERT INTO settings (key, value, updated_at) VALUES ('claude', ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at").run(
28573
28573
  JSON.stringify(next),
@@ -29102,6 +29102,7 @@ function spawnTerminal(input) {
29102
29102
  clients: /* @__PURE__ */ new Set(),
29103
29103
  backlog: [],
29104
29104
  backlogSize: 0,
29105
+ lastOutputAt: Date.now(),
29105
29106
  cols,
29106
29107
  rows
29107
29108
  };
@@ -29109,6 +29110,7 @@ function spawnTerminal(input) {
29109
29110
  child.onData(bindProject((data) => {
29110
29111
  terminal.backlog.push(data);
29111
29112
  terminal.backlogSize += data.length;
29113
+ terminal.lastOutputAt = Date.now();
29112
29114
  while (terminal.backlogSize > BACKLOG_LIMIT && terminal.backlog.length > 1) {
29113
29115
  terminal.backlogSize -= terminal.backlog.shift().length;
29114
29116
  }
@@ -29195,11 +29197,11 @@ function killTerminal(terminal) {
29195
29197
  function waitForExit(terminal, timeoutMs = 1e4) {
29196
29198
  return new Promise((resolve) => {
29197
29199
  const started = Date.now();
29198
- const check2 = () => {
29200
+ const check3 = () => {
29199
29201
  if (!terminals.has(terminal.id) || Date.now() - started > timeoutMs) return resolve();
29200
- setTimeout(check2, 100);
29202
+ setTimeout(check3, 100);
29201
29203
  };
29202
- check2();
29204
+ check3();
29203
29205
  });
29204
29206
  }
29205
29207
  function killAll() {
@@ -31276,6 +31278,113 @@ var init_telegram = __esm({
31276
31278
  }
31277
31279
  });
31278
31280
 
31281
+ // server/src/watchdog.ts
31282
+ var watchdog_exports = {};
31283
+ __export(watchdog_exports, {
31284
+ answerTrust: () => answerTrust,
31285
+ startWatchdog: () => startWatchdog
31286
+ });
31287
+ function screenTail(terminal) {
31288
+ let text = "";
31289
+ for (let index = terminal.backlog.length - 1; index >= 0 && text.length < SCREEN_TAIL * 2; index -= 1) text = terminal.backlog[index] + text;
31290
+ return text.slice(-SCREEN_TAIL * 2).replace(ANSI, "").slice(-SCREEN_TAIL);
31291
+ }
31292
+ async function answerTrust(terminal, trust) {
31293
+ if (!trust) return pressKeys(terminal, "\x1B");
31294
+ for (let step = 0; step < 4; step += 1) {
31295
+ const flat2 = squeeze(screenTail(terminal).slice(-1500));
31296
+ const cursor = flat2.lastIndexOf("\u276F");
31297
+ const after = cursor >= 0 ? flat2.slice(cursor + 1) : "";
31298
+ if (after.startsWith("yes")) return pressKeys(terminal, "\r");
31299
+ const yesAt = flat2.lastIndexOf("yes,itrust");
31300
+ await pressKeys(terminal, yesAt > cursor ? "\x1B[B" : "\x1B[A");
31301
+ await sleep2(300);
31302
+ }
31303
+ throw new Error("Could not find the trust option on the screen.");
31304
+ }
31305
+ function lastLines(screen) {
31306
+ const lines2 = screen.split(/\r?\n/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
31307
+ return lines2.slice(-14).join("\n").slice(-1200);
31308
+ }
31309
+ function check2() {
31310
+ for (const terminal of listTerminals()) {
31311
+ if (terminal.finished || terminal.restarting) continue;
31312
+ const open = listInteractions({ sessionId: terminal.sessionId, status: "open" });
31313
+ const screen = screenTail(terminal);
31314
+ const trustOpen = open.find((item) => item.data?.trust);
31315
+ if (showsTrustDialog(screen)) {
31316
+ if (!trustOpen) {
31317
+ createInteraction({
31318
+ sessionId: terminal.sessionId,
31319
+ kind: "question",
31320
+ title: "Trust this folder?",
31321
+ body: "Claude Code asks whether to trust the files in this project's folder before it starts. Nothing runs until it is answered.",
31322
+ options: [
31323
+ { key: "1", label: "Yes, trust this folder" },
31324
+ { key: "2", label: "No, exit" }
31325
+ ],
31326
+ data: { trust: true, questions: [{ question: "Trust this folder?", options: ["Yes, trust this folder", "No, exit"] }] }
31327
+ });
31328
+ }
31329
+ continue;
31330
+ }
31331
+ if (trustOpen) closeOpen(terminal.sessionId, "terminal", ["question"]);
31332
+ const stalled = open.find((item) => item.data?.stalled);
31333
+ if (stalled && terminal.lastOutputAt > Date.parse(stalled.createdAt) + 5e3) {
31334
+ closeOpen(terminal.sessionId, "terminal", ["turn"]);
31335
+ setSessionStatus(terminal.sessionId, "running");
31336
+ continue;
31337
+ }
31338
+ const quietFor = Date.now() - terminal.lastOutputAt;
31339
+ if (quietFor < STALL_MS || open.length || reported.get(terminal.id) === terminal.lastOutputAt) continue;
31340
+ if (!getSessionRow(terminal.sessionId)?.run_id) continue;
31341
+ reported.set(terminal.id, terminal.lastOutputAt);
31342
+ createInteraction({
31343
+ sessionId: terminal.sessionId,
31344
+ kind: "turn",
31345
+ title: "This run looks stuck",
31346
+ body: `Nothing has changed on its screen for ${Math.round(quietFor / 6e4)} minutes. It may be waiting on a dialog, or it stopped without reporting done. Reply to type into it, or stop it.
31347
+
31348
+ ${lastLines(screen)}`,
31349
+ data: { stalled: true }
31350
+ });
31351
+ }
31352
+ for (const id of reported.keys()) if (!allTerminals().some((terminal) => terminal.id === id)) reported.delete(id);
31353
+ }
31354
+ function startWatchdog(intervalMs = 3e3) {
31355
+ const timer = setInterval(bindProject(() => {
31356
+ try {
31357
+ check2();
31358
+ } catch (error) {
31359
+ console.error("[watchdog]", error.message);
31360
+ }
31361
+ }), intervalMs);
31362
+ return () => clearInterval(timer);
31363
+ }
31364
+ var TRUST_MARKERS, STALL_MS, SCREEN_TAIL, ANSI, squeeze, showsTrustDialog, sleep2, reported;
31365
+ var init_watchdog = __esm({
31366
+ "server/src/watchdog.ts"() {
31367
+ "use strict";
31368
+ init_context();
31369
+ init_interactions();
31370
+ init_sessions();
31371
+ init_terminals();
31372
+ TRUST_MARKERS = ["doyoutrustthefilesinthisfolder", "isthisaprojectyoucreatedoroneyoutrust", "yes,itrustthisfolder"];
31373
+ STALL_MS = Number(process.env.CLAUDER_HUB_STALL_MS) || 8 * 6e4;
31374
+ SCREEN_TAIL = 8e3;
31375
+ ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>78]/g;
31376
+ squeeze = (text) => text.toLowerCase().replace(/\s+/g, "");
31377
+ showsTrustDialog = (screen) => {
31378
+ const flat2 = squeeze(screen.slice(-3e3));
31379
+ if (!TRUST_MARKERS.some((marker) => flat2.includes(marker))) return false;
31380
+ const end = Math.max(flat2.lastIndexOf("entertoconfirm"), flat2.lastIndexOf("itrustthisfolder"), flat2.lastIndexOf("no,exit"));
31381
+ return end >= 0 && flat2.length - end < 120;
31382
+ };
31383
+ sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31384
+ reported = /* @__PURE__ */ new Map();
31385
+ }
31386
+ });
31387
+
31279
31388
  // server/src/mcpSuggestions.ts
31280
31389
  var mcpSuggestions_exports = {};
31281
31390
  __export(mcpSuggestions_exports, {
@@ -31500,6 +31609,11 @@ function closeOpen(sessionId, by, kinds) {
31500
31609
  }
31501
31610
  async function answerQuestion(interaction, answer) {
31502
31611
  const terminal = terminalForSession(interaction.sessionId);
31612
+ if (interaction.data.trust) {
31613
+ const { answerTrust: answerTrust2 } = await Promise.resolve().then(() => (init_watchdog(), watchdog_exports));
31614
+ await answerTrust2(terminal, answer.trim() !== "2");
31615
+ return;
31616
+ }
31503
31617
  const questions = interaction.data.questions ?? [];
31504
31618
  const split = answer.includes("\n") ? answer.split("\n") : answer.split(/[;,]/);
31505
31619
  const parts = questions.length > 1 ? split.map((part) => part.trim()) : [answer.trim()];
@@ -31701,7 +31815,7 @@ function bootGlobal() {
31701
31815
  };
31702
31816
  const stops = withProject(project, () => {
31703
31817
  recoverRuns();
31704
- const list = [startTranscriptWatcher(), startInteractionSweeper(), stopChannels];
31818
+ const list = [startTranscriptWatcher(), startInteractionSweeper(), startWatchdog(), stopChannels];
31705
31819
  startChannels({ device: false });
31706
31820
  return list;
31707
31821
  });
@@ -31759,7 +31873,7 @@ function boot(row) {
31759
31873
  withProject(project, () => {
31760
31874
  const stranded = recoverRuns();
31761
31875
  if (stranded) console.log(` note : ${row.name}: ${stranded} queued run(s) were running when the hub last stopped`);
31762
- stops.push(startTranscriptWatcher(), startInteractionSweeper());
31876
+ stops.push(startTranscriptWatcher(), startInteractionSweeper(), startWatchdog());
31763
31877
  stops.push(stopChannels);
31764
31878
  startChannels();
31765
31879
  stops.push(stopScheduler);
@@ -31840,6 +31954,7 @@ var init_projects = __esm({
31840
31954
  init_db();
31841
31955
  init_events();
31842
31956
  init_interactions();
31957
+ init_watchdog();
31843
31958
  init_jobs();
31844
31959
  init_paths();
31845
31960
  init_terminals();