clauder-hub 0.1.4 → 0.1.6

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 (26) hide show
  1. package/package.json +1 -1
  2. package/server/dist/index.js +1019 -798
  3. package/web/dist/assets/{MonacoEditor-7a1aZ6Pf.js → MonacoEditor-CBiXYCP7.js} +3 -3
  4. package/web/dist/assets/{cssMode-D42SG8q2.js → cssMode-CQchaYnQ.js} +1 -1
  5. package/web/dist/assets/{freemarker2-HuLVqAyk.js → freemarker2-BN6MtnD2.js} +1 -1
  6. package/web/dist/assets/{handlebars-CQbtHmDj.js → handlebars-Ciy4VHE8.js} +1 -1
  7. package/web/dist/assets/{html-DCQkaSoP.js → html-BqmI8abz.js} +1 -1
  8. package/web/dist/assets/{htmlMode-Occ0oux7.js → htmlMode-hqhxUX1t.js} +1 -1
  9. package/web/dist/assets/index-CRmMCp9l.css +32 -0
  10. package/web/dist/assets/index-Ch_46Kom.js +594 -0
  11. package/web/dist/assets/{index-DB6lAoRD.js → index-sOL-0RCg.js} +1 -1
  12. package/web/dist/assets/{javascript-DkkoffJP.js → javascript-tSmmqqDu.js} +1 -1
  13. package/web/dist/assets/{jsonMode-CvTmblXx.js → jsonMode-BWRnRKuQ.js} +1 -1
  14. package/web/dist/assets/{liquid-DhsiR38F.js → liquid-UASeAh73.js} +1 -1
  15. package/web/dist/assets/{lspLanguageFeatures-lfpHn74b.js → lspLanguageFeatures-k7agW304.js} +1 -1
  16. package/web/dist/assets/{mdx-LuMf9AxQ.js → mdx-4PNlcmYK.js} +1 -1
  17. package/web/dist/assets/{python-BkVjahJB.js → python-B3oyLMGp.js} +1 -1
  18. package/web/dist/assets/{razor-CoVA4mXW.js → razor-DertI82h.js} +1 -1
  19. package/web/dist/assets/{tsMode-C3jqF6o4.js → tsMode-BJcxrzuV.js} +1 -1
  20. package/web/dist/assets/{typescript-DzEhu0NR.js → typescript-B2dfrWYs.js} +1 -1
  21. package/web/dist/assets/{workers-DO15KvMQ.js → workers-C3liFWwz.js} +1 -1
  22. package/web/dist/assets/{xml-Dnv-hWWk.js → xml-D6-qhmMC.js} +1 -1
  23. package/web/dist/assets/{yaml-hUd7D5AP.js → yaml-CnI-_wqS.js} +1 -1
  24. package/web/dist/index.html +2 -2
  25. package/web/dist/assets/index-382ZKQ6T.js +0 -527
  26. package/web/dist/assets/index-YQ0zZelJ.css +0 -32
@@ -1665,8 +1665,8 @@ var require_node = __commonJS({
1665
1665
  break;
1666
1666
  case "PIPE":
1667
1667
  case "TCP":
1668
- var net = __require("net");
1669
- stream2 = new net.Socket({
1668
+ var net2 = __require("net");
1669
+ stream2 = new net2.Socket({
1670
1670
  fd: fd2,
1671
1671
  readable: false,
1672
1672
  writable: true
@@ -25418,7 +25418,7 @@ var require_websocket = __commonJS({
25418
25418
  var EventEmitter = __require("events");
25419
25419
  var https = __require("https");
25420
25420
  var http = __require("http");
25421
- var net = __require("net");
25421
+ var net2 = __require("net");
25422
25422
  var tls = __require("tls");
25423
25423
  var { randomBytes, createHash } = __require("crypto");
25424
25424
  var { Duplex, Readable } = __require("stream");
@@ -26175,12 +26175,12 @@ var require_websocket = __commonJS({
26175
26175
  }
26176
26176
  function netConnect(options) {
26177
26177
  options.path = options.socketPath;
26178
- return net.connect(options);
26178
+ return net2.connect(options);
26179
26179
  }
26180
26180
  function tlsConnect(options) {
26181
26181
  options.path = void 0;
26182
26182
  if (!options.servername && options.servername !== "") {
26183
- options.servername = net.isIP(options.host) ? "" : options.host;
26183
+ options.servername = net2.isIP(options.host) ? "" : options.host;
26184
26184
  }
26185
26185
  return tls.connect(options);
26186
26186
  }
@@ -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),
@@ -29070,6 +29070,294 @@ var init_categories = __esm({
29070
29070
  }
29071
29071
  });
29072
29072
 
29073
+ // server/src/channels/markdown.ts
29074
+ function pieces(markdown) {
29075
+ const out = [];
29076
+ let prose = [];
29077
+ let code = null;
29078
+ for (const line of markdown.split("\n")) {
29079
+ if (code) {
29080
+ if (/^\s*```/.test(line)) {
29081
+ out.push({ code: true, text: code.join("\n") });
29082
+ code = null;
29083
+ } else code.push(line);
29084
+ } else if (/^\s*```[^`]*$/.test(line)) {
29085
+ if (prose.length) out.push({ code: false, text: prose.join("\n") });
29086
+ prose = [];
29087
+ code = [];
29088
+ } else prose.push(line);
29089
+ }
29090
+ if (code) out.push({ code: true, text: code.join("\n") });
29091
+ if (prose.length) out.push({ code: false, text: prose.join("\n") });
29092
+ return out;
29093
+ }
29094
+ function holdLinks(text, render2, rewrite) {
29095
+ const links = [];
29096
+ const held = text.replace(/\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)/g, (_whole, label, url) => `${links.push(render2(label, url)) - 1}`);
29097
+ return rewrite(held).replace(/\u0002(\d+)\u0002/g, (_whole, index) => links[Number(index)]);
29098
+ }
29099
+ function lines(text, bold) {
29100
+ return text.split("\n").map((line) => {
29101
+ if (/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/.test(line)) return null;
29102
+ if (/^\s*\|.*\|\s*$/.test(line))
29103
+ return line.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim()).filter(Boolean).join(" \xB7 ");
29104
+ if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) return "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
29105
+ const heading = /^\s*#{1,6}\s+(.*)$/.exec(line);
29106
+ if (heading) return bold(heading[1].replace(/\*\*/g, ""));
29107
+ const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
29108
+ if (bullet) return `${bullet[1].length >= 2 ? " \u25E6 " : "\u2022 "}${bullet[2]}`;
29109
+ return line.replace(/^\s*>\s?/, "");
29110
+ }).filter((line) => line !== null).join("\n").replace(/\n{3,}/g, "\n\n");
29111
+ }
29112
+ function inline(text, codeOf, rest) {
29113
+ const codes = [];
29114
+ const held = text.replace(/`{1,3}([^`\n]+)`{1,3}/g, (_whole, code) => `\0${codes.push(code) - 1}\0`);
29115
+ return rest(held).replace(/\u0000(\d+)\u0000/g, (_whole, index) => codeOf(codes[Number(index)]));
29116
+ }
29117
+ function toTelegramHtml(markdown, { inQuote = false } = {}) {
29118
+ return pieces(markdown).map((piece) => {
29119
+ if (piece.code) return inQuote ? `<code>${escapeHtml(piece.text)}</code>` : `<pre>${escapeHtml(piece.text)}</pre>`;
29120
+ return inline(
29121
+ lines(piece.text, (heading) => `**${heading}**`),
29122
+ (code) => `<code>${escapeHtml(code)}</code>`,
29123
+ (text) => holdLinks(
29124
+ escapeHtml(text),
29125
+ (label, url) => `<a href="${url.replace(/"/g, "&quot;")}">${label}</a>`,
29126
+ (prose) => prose.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/(^|[\s(])__([^_\n]+)__(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1<b>$2</b>").replace(/~~([^~\n]+)~~/g, "<s>$1</s>").replace(/(^|[\s(])\*([^*\s][^*\n]*?)\*(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1<i>$2</i>").replace(/(^|[\s(])_([^_\s][^_\n]*?)_(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1<i>$2</i>")
29127
+ )
29128
+ );
29129
+ }).join("\n").replace(/\n{3,}/g, "\n\n").trim();
29130
+ }
29131
+ function toSlackMrkdwn(markdown) {
29132
+ return pieces(markdown).map((piece) => {
29133
+ if (piece.code) return `\`\`\`
29134
+ ${escapeHtml(piece.text)}
29135
+ \`\`\``;
29136
+ return inline(
29137
+ lines(piece.text, (heading) => `**${heading}**`),
29138
+ // Escaped like the rest: "<https://x>" or "<!here>" in code would otherwise become a link or a mention.
29139
+ (code) => `\`${escapeHtml(code)}\``,
29140
+ (text) => holdLinks(
29141
+ escapeHtml(text),
29142
+ (label, url) => `<${url}|${label}>`,
29143
+ (prose) => prose.replace(/(^|[\s(])\*([^*\s][^*\n]*?)\*(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1$2").replace(/\*\*([^*\n]+)\*\*/g, "*$1*").replace(/(^|[\s(])__([^_\n]+)__(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1*$2*").replace(/~~([^~\n]+)~~/g, "~$1~").replace(/\u0001/g, "_")
29144
+ )
29145
+ );
29146
+ }).join("\n").replace(/\n{3,}/g, "\n\n").trim();
29147
+ }
29148
+ function gistOf(markdown, skip = "") {
29149
+ const prose = pieces(markdown).filter((piece) => !piece.code).map((piece) => piece.text).join("\n");
29150
+ const strip = (text) => text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^\s*#+\s+/, "").replace(/`+|\*\*|~~/g, "").replace(/(^|[\s(])(__|_|\*)([^_*\n]+?)\2(?=[\s),:;!?]|\.(?!\w)|$)/g, "$1$3").replace(/\s+/g, " ").trim();
29151
+ const short2 = (text, max) => text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
29152
+ const items = prose.split("\n").map((line) => /^\s{0,1}(?:[-*+]|\d+[.)])\s+(.*)$/.exec(line)?.[1]).filter((item) => Boolean(item)).map(strip).filter(Boolean);
29153
+ if (items.length >= 2) {
29154
+ const shown = items.slice(0, 4).map((item) => `\u2022 ${short2(item, 110)}`);
29155
+ return items.length > 4 ? [...shown, `\u2026and ${items.length - 4} more`] : shown;
29156
+ }
29157
+ const opening = strip(
29158
+ prose.split(/\n\s*\n/).map((part) => part.trim()).find((part) => part && !/^\s*(#|\|)/.test(part)) ?? ""
29159
+ );
29160
+ const sentences = opening.split(/(?<=[.?!])\s+/).slice(0, 2).join(" ");
29161
+ const gist = short2(sentences, 260);
29162
+ return gist && strip(skip) !== gist ? [gist] : [];
29163
+ }
29164
+ var escapeHtml;
29165
+ var init_markdown = __esm({
29166
+ "server/src/channels/markdown.ts"() {
29167
+ "use strict";
29168
+ escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29169
+ }
29170
+ });
29171
+
29172
+ // server/src/presentation.ts
29173
+ function mainQuestion(message) {
29174
+ const paragraphs = plain(message).split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean);
29175
+ const sentencesOf = (part) => part.split(/(?<=[.?!])\s+|\n+/).map((sentence) => sentence.trim()).filter(Boolean);
29176
+ const last = paragraphs[paragraphs.length - 1] ?? "";
29177
+ const sentences = sentencesOf(last);
29178
+ const first = sentences.findIndex((sentence) => sentence.endsWith("?"));
29179
+ if (first !== -1) {
29180
+ const run4 = sentences.slice(first).join(" ");
29181
+ const lastQuestion = [...sentences].reverse().find((sentence) => sentence.endsWith("?"));
29182
+ return { headline: oneLine(run4.length <= 160 ? run4 : lastQuestion), isQuestion: true };
29183
+ }
29184
+ const anyQuestion = [...paragraphs].reverse().flatMap((part) => sentencesOf(part).filter((sentence) => sentence.endsWith("?")).reverse())[0];
29185
+ if (anyQuestion) return { headline: oneLine(anyQuestion), isQuestion: true };
29186
+ return { headline: oneLine(last || "Claude finished its turn."), isQuestion: false };
29187
+ }
29188
+ function target(toolName2, input) {
29189
+ if (!input) return "";
29190
+ const pick = (key) => typeof input[key] === "string" ? input[key] : "";
29191
+ const file = pick("file_path") || pick("notebook_path") || pick("path");
29192
+ if (file) return file.split(/[\\/]/).slice(-2).join("/");
29193
+ const command = pick("command");
29194
+ if (command) return command.split("\n")[0];
29195
+ return pick("url") || pick("pattern") || pick("query") || pick("description") || (toolName2.startsWith("mcp__") ? toolName2.split("__").pop() ?? "" : "");
29196
+ }
29197
+ function present(interaction, sessionTitle2) {
29198
+ const data = interaction.data ?? {};
29199
+ const answerable = interaction.status === "open";
29200
+ const base = { kind: interaction.kind, sessionTitle: sessionTitle2, answerable, quickReplies: [] };
29201
+ switch (interaction.kind) {
29202
+ case "permission": {
29203
+ const tool = String(data.toolName ?? "a tool");
29204
+ const what = target(tool, data.toolInput);
29205
+ const shortTool = tool.startsWith("mcp__") ? tool.replace(/^mcp__/, "").split("__")[0] : tool;
29206
+ return {
29207
+ ...base,
29208
+ headline: oneLine(`${shortTool}${what ? ` \u2192 ${what}` : ""}`, 120),
29209
+ question: data.agent ? `The ${data.agent} agent asks to use ${tool}.` : `Claude asks to use ${tool}.`,
29210
+ choices: [
29211
+ { ref: "o:0", label: "Allow", tone: "primary" },
29212
+ { ref: "o:1", label: "Allow for this session" },
29213
+ { ref: "o:2", label: "Deny", tone: "danger" }
29214
+ ],
29215
+ freeText: "Deny with a reason for Claude...",
29216
+ details: { label: "Full request", text: interaction.body, format: "code" }
29217
+ };
29218
+ }
29219
+ case "question": {
29220
+ const questions = (Array.isArray(data.questions) ? data.questions : []).map((item) => ({
29221
+ header: item?.header ? String(item.header) : void 0,
29222
+ question: String(item?.question ?? ""),
29223
+ options: (Array.isArray(item?.options) ? item.options : []).map((option) => ({
29224
+ label: String(option?.label ?? option),
29225
+ description: option?.description ? String(option.description) : void 0
29226
+ })),
29227
+ multiSelect: Boolean(item?.multiSelect)
29228
+ }));
29229
+ const first = questions[0];
29230
+ const single = questions.length === 1;
29231
+ return {
29232
+ ...base,
29233
+ headline: oneLine(first?.question || interaction.body || interaction.title),
29234
+ question: first?.header,
29235
+ questions,
29236
+ choices: single ? first.options.map((option, index) => ({ ref: `o:${index}`, label: option.label, description: option.description, tone: index === 0 ? "primary" : void 0 })) : [],
29237
+ freeText: "Or answer in your own words..."
29238
+ };
29239
+ }
29240
+ case "turn": {
29241
+ const { headline, isQuestion } = mainQuestion(interaction.body);
29242
+ const replies = isQuestion ? ["Yes", "No", "Continue"] : ["Continue"];
29243
+ return {
29244
+ ...base,
29245
+ headline,
29246
+ choices: [],
29247
+ quickReplies: replies.map((label, index) => ({ ref: `q:${index}`, label })),
29248
+ freeText: "Reply to the session...",
29249
+ details: interaction.body && plain(interaction.body) !== headline ? { label: "Full message", text: interaction.body, format: "markdown" } : void 0,
29250
+ gist: interaction.body ? gistOf(interaction.body, headline) : void 0
29251
+ };
29252
+ }
29253
+ case "mcp":
29254
+ return {
29255
+ ...base,
29256
+ headline: oneLine(`Needs the MCP server '${data.name ?? ""}'`),
29257
+ question: data.reason ? oneLine(String(data.reason), 240) : void 0,
29258
+ choices: [
29259
+ { ref: "o:0", label: "Install & resume", tone: "primary" },
29260
+ { ref: "o:1", label: "Skip" }
29261
+ ],
29262
+ freeText: "Or tell Claude what to do instead...",
29263
+ details: data.command ? { label: "Install command", text: String(data.command), format: "code" } : void 0
29264
+ };
29265
+ case "done": {
29266
+ const deliverables = Object.entries(data.deliverables ?? {}).map(([key, value]) => `- **${key}**: ${typeof value === "string" ? value : JSON.stringify(value)}`).join("\n");
29267
+ return {
29268
+ ...base,
29269
+ answerable: false,
29270
+ headline: oneLine(String(data.summary ?? interaction.body ?? "Task done."), 200),
29271
+ question: data.category ? `Category: ${data.category}` : void 0,
29272
+ choices: [],
29273
+ freeText: null,
29274
+ details: deliverables ? { label: "Deliverables", text: deliverables, format: "markdown" } : void 0
29275
+ };
29276
+ }
29277
+ }
29278
+ }
29279
+ function resolveChoice(interaction, ref) {
29280
+ const [type, raw] = ref.split(":");
29281
+ const index = Number(raw);
29282
+ if (!Number.isInteger(index)) return null;
29283
+ if (type === "q") return present(interaction, "").quickReplies[index]?.label ?? null;
29284
+ if (type !== "o") return null;
29285
+ switch (interaction.kind) {
29286
+ case "permission":
29287
+ return ["1", "2", "3"][index] ?? null;
29288
+ case "question":
29289
+ return String(index + 1);
29290
+ case "mcp":
29291
+ return ["install", "skip"][index] ?? null;
29292
+ default:
29293
+ return null;
29294
+ }
29295
+ }
29296
+ var oneLine, plain;
29297
+ var init_presentation = __esm({
29298
+ "server/src/presentation.ts"() {
29299
+ "use strict";
29300
+ init_markdown();
29301
+ oneLine = (text, max = 160) => {
29302
+ const flat2 = text.replace(/\s+/g, " ").trim();
29303
+ return flat2.length > max ? `${flat2.slice(0, max - 1).trimEnd()}...` : flat2;
29304
+ };
29305
+ plain = (text) => text.replace(/`{3}[\s\S]*?`{3}/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^\s*(#+|>)\s?/gm, "").replace(/`+|\*\*|~~/g, "").replace(/(^|[\s(])(__|_|\*)([^_*\n]+?)\2(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1$3").trim();
29306
+ }
29307
+ });
29308
+
29309
+ // server/src/smallModel.ts
29310
+ import { spawn as spawn2 } from "node:child_process";
29311
+ import fs5 from "node:fs";
29312
+ import path6 from "node:path";
29313
+ function askSmallModel(prompt, timeoutMs = 45e3) {
29314
+ const claude2 = findClaudeCode();
29315
+ if (!claude2 || claudeStatus()?.problem) return Promise.resolve(null);
29316
+ return new Promise((resolve) => {
29317
+ const cwd = path6.join(hubHome, "global");
29318
+ fs5.mkdirSync(cwd, { recursive: true });
29319
+ const child = spawn2(claude2, ["-p", "--model", "haiku", "--tools", "", "--no-session-persistence", "--strict-mcp-config", "--output-format", "text"], {
29320
+ cwd,
29321
+ windowsHide: true,
29322
+ stdio: ["pipe", "pipe", "ignore"]
29323
+ });
29324
+ let out = "";
29325
+ const timer = setTimeout(() => child.kill(), timeoutMs);
29326
+ child.stdout.on("data", (chunk) => out += chunk);
29327
+ child.on("error", () => {
29328
+ clearTimeout(timer);
29329
+ resolve(null);
29330
+ });
29331
+ child.on("close", (code) => {
29332
+ clearTimeout(timer);
29333
+ resolve(code === 0 && out.trim() ? out : null);
29334
+ });
29335
+ child.stdin.end(prompt);
29336
+ });
29337
+ }
29338
+ function jsonIn(answer, open) {
29339
+ if (!answer) return null;
29340
+ const start = answer.indexOf(open);
29341
+ const end = answer.lastIndexOf(open === "[" ? "]" : "}");
29342
+ if (start === -1 || end <= start) return null;
29343
+ try {
29344
+ return JSON.parse(answer.slice(start, end + 1));
29345
+ } catch {
29346
+ return null;
29347
+ }
29348
+ }
29349
+ async function pickNumbers(prompt) {
29350
+ const picked = jsonIn(await askSmallModel(prompt), "[");
29351
+ return picked ? picked.map(Number).filter((number) => Number.isInteger(number) && number > 0) : null;
29352
+ }
29353
+ var init_smallModel = __esm({
29354
+ "server/src/smallModel.ts"() {
29355
+ "use strict";
29356
+ init_claudeCode();
29357
+ init_paths();
29358
+ }
29359
+ });
29360
+
29073
29361
  // server/src/terminals.ts
29074
29362
  import * as pty from "@lydell/node-pty";
29075
29363
  function onTerminalExit(listener) {
@@ -29102,6 +29390,7 @@ function spawnTerminal(input) {
29102
29390
  clients: /* @__PURE__ */ new Set(),
29103
29391
  backlog: [],
29104
29392
  backlogSize: 0,
29393
+ lastOutputAt: Date.now(),
29105
29394
  cols,
29106
29395
  rows
29107
29396
  };
@@ -29109,6 +29398,7 @@ function spawnTerminal(input) {
29109
29398
  child.onData(bindProject((data) => {
29110
29399
  terminal.backlog.push(data);
29111
29400
  terminal.backlogSize += data.length;
29401
+ terminal.lastOutputAt = Date.now();
29112
29402
  while (terminal.backlogSize > BACKLOG_LIMIT && terminal.backlog.length > 1) {
29113
29403
  terminal.backlogSize -= terminal.backlog.shift().length;
29114
29404
  }
@@ -29195,11 +29485,11 @@ function killTerminal(terminal) {
29195
29485
  function waitForExit(terminal, timeoutMs = 1e4) {
29196
29486
  return new Promise((resolve) => {
29197
29487
  const started = Date.now();
29198
- const check2 = () => {
29488
+ const check3 = () => {
29199
29489
  if (!terminals.has(terminal.id) || Date.now() - started > timeoutMs) return resolve();
29200
- setTimeout(check2, 100);
29490
+ setTimeout(check3, 100);
29201
29491
  };
29202
- check2();
29492
+ check3();
29203
29493
  });
29204
29494
  }
29205
29495
  function killAll() {
@@ -29238,232 +29528,6 @@ var init_terminals = __esm({
29238
29528
  }
29239
29529
  });
29240
29530
 
29241
- // server/src/inventory.ts
29242
- import fs5 from "node:fs";
29243
- import os2 from "node:os";
29244
- import path6 from "node:path";
29245
- function readJson(file) {
29246
- try {
29247
- return JSON.parse(fs5.readFileSync(file, "utf8"));
29248
- } catch {
29249
- return null;
29250
- }
29251
- }
29252
- function frontMatter(text) {
29253
- const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
29254
- if (!match) return {};
29255
- const fields = {};
29256
- let lastKey = "";
29257
- for (const line of match[1].split(/\r?\n/)) {
29258
- const pair = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
29259
- if (pair) {
29260
- lastKey = pair[1];
29261
- fields[lastKey] = pair[2].replace(/^["']|["']$/g, "").replace(/^[>|]-?$/, "");
29262
- } else if (lastKey && /^\s+\S/.test(line)) {
29263
- fields[lastKey] = `${fields[lastKey]} ${line.trim()}`.trim();
29264
- }
29265
- }
29266
- return fields;
29267
- }
29268
- function definition(file, fallbackName, scope, plugin) {
29269
- let text;
29270
- try {
29271
- text = fs5.readFileSync(file, "utf8");
29272
- } catch {
29273
- return null;
29274
- }
29275
- const meta = frontMatter(text);
29276
- const name = meta.name || fallbackName;
29277
- return {
29278
- name: plugin ? `${plugin.split("@")[0]}:${name}` : name,
29279
- description: meta.description ?? "",
29280
- scope,
29281
- plugin,
29282
- path: file,
29283
- model: meta.model || void 0
29284
- };
29285
- }
29286
- function markdownFiles(dir, depth = 3) {
29287
- const found = [];
29288
- const walk = (current, left) => {
29289
- let entries;
29290
- try {
29291
- entries = fs5.readdirSync(current, { withFileTypes: true });
29292
- } catch {
29293
- return;
29294
- }
29295
- for (const entry of entries) {
29296
- const full = path6.join(current, entry.name);
29297
- if (entry.isDirectory() && left > 0) walk(full, left - 1);
29298
- else if (entry.isFile() && entry.name.endsWith(".md")) found.push(full);
29299
- }
29300
- };
29301
- walk(dir, depth);
29302
- return found;
29303
- }
29304
- function agentsIn(dir, scope, plugin) {
29305
- return markdownFiles(dir, 1).map((file) => definition(file, path6.basename(file, ".md"), scope, plugin)).filter((item) => item !== null);
29306
- }
29307
- function skillsIn(dir, scope, plugin) {
29308
- let entries;
29309
- try {
29310
- entries = fs5.readdirSync(dir, { withFileTypes: true });
29311
- } catch {
29312
- return [];
29313
- }
29314
- return entries.filter((entry) => entry.isDirectory()).map((entry) => definition(path6.join(dir, entry.name, "SKILL.md"), entry.name, scope, plugin)).filter((item) => item !== null);
29315
- }
29316
- function commandsIn(dir, scope, plugin) {
29317
- return markdownFiles(dir, 2).map((file) => definition(file, path6.relative(dir, file).replace(/\.md$/, "").replace(/[\\/]/g, ":"), scope, plugin)).filter((item) => item !== null);
29318
- }
29319
- function mcpFrom(servers, scope, source, plugin) {
29320
- if (!servers || typeof servers !== "object") return [];
29321
- return Object.entries(servers).map(([name, config]) => ({
29322
- name: plugin ? `plugin:${plugin.split("@")[0]}:${name}` : name,
29323
- scope,
29324
- plugin,
29325
- transport: String(config?.type ?? (config?.url ? "http" : "stdio")),
29326
- target: config?.url ? String(config.url) : [config?.command, ...Array.isArray(config?.args) ? config.args : []].filter(Boolean).join(" "),
29327
- source
29328
- }));
29329
- }
29330
- function hooksFrom(settings, scope, source, plugin) {
29331
- const hooks = settings?.hooks;
29332
- if (!hooks || typeof hooks !== "object") return [];
29333
- const items = [];
29334
- for (const [event, groups] of Object.entries(hooks)) {
29335
- if (!Array.isArray(groups)) continue;
29336
- for (const group of groups) {
29337
- for (const hook of Array.isArray(group?.hooks) ? group.hooks : []) {
29338
- items.push({
29339
- event,
29340
- matcher: String(group?.matcher ?? ""),
29341
- type: String(hook?.type ?? "command"),
29342
- scope,
29343
- plugin,
29344
- source,
29345
- summary: String(hook?.command ?? hook?.url ?? hook?.prompt ?? hook?.tool ?? "").slice(0, 200)
29346
- });
29347
- }
29348
- }
29349
- }
29350
- return items;
29351
- }
29352
- function localProjectConfig(global3) {
29353
- const projects = global3?.projects ?? {};
29354
- const wanted = [projectRoot(), projectRoot().replace(/\\/g, "/")];
29355
- for (const [key, value] of Object.entries(projects)) {
29356
- if (wanted.some((candidate) => candidate.toLowerCase() === key.toLowerCase())) return value;
29357
- }
29358
- return null;
29359
- }
29360
- function readInventory() {
29361
- const userSettingsFile = path6.join(claudeHome, "settings.json");
29362
- const projectSettingsFile = path6.join(projectRoot(), ".claude", "settings.json");
29363
- const localSettingsFile = path6.join(projectRoot(), ".claude", "settings.local.json");
29364
- const userSettings = readJson(userSettingsFile);
29365
- const projectSettings = readJson(projectSettingsFile);
29366
- const localSettings = readJson(localSettingsFile);
29367
- const globalConfigFile = path6.join(os2.homedir(), ".claude.json");
29368
- const globalConfig = readJson(globalConfigFile);
29369
- const local = localProjectConfig(globalConfig);
29370
- const enabled = {
29371
- ...userSettings?.enabledPlugins ?? {},
29372
- ...projectSettings?.enabledPlugins ?? {},
29373
- ...localSettings?.enabledPlugins ?? {}
29374
- };
29375
- const plugins = [];
29376
- const installed = readJson(path6.join(claudeHome, "plugins", "installed_plugins.json"));
29377
- for (const [id, installs] of Object.entries(installed?.plugins ?? {})) {
29378
- const install = Array.isArray(installs) ? installs[installs.length - 1] : null;
29379
- if (!install?.installPath) continue;
29380
- plugins.push({
29381
- id,
29382
- version: String(install.version ?? ""),
29383
- scope: String(install.scope ?? "user"),
29384
- // Installed plugins are on unless a settings file turns them off.
29385
- enabled: enabled[id] !== false,
29386
- path: install.installPath
29387
- });
29388
- }
29389
- const agents = [
29390
- ...agentsIn(path6.join(claudeHome, "agents"), "user"),
29391
- ...agentsIn(path6.join(projectRoot(), ".claude", "agents"), "project"),
29392
- // No file behind these, so no path: they can be turned off, not edited.
29393
- ...BUILT_IN_AGENTS.map(([name, description]) => ({ name, description, scope: "built-in", path: "" }))
29394
- ];
29395
- const skills = [...skillsIn(path6.join(claudeHome, "skills"), "user"), ...skillsIn(path6.join(projectRoot(), ".claude", "skills"), "project")];
29396
- const commands = [
29397
- ...commandsIn(path6.join(claudeHome, "commands"), "user"),
29398
- ...commandsIn(path6.join(projectRoot(), ".claude", "commands"), "project")
29399
- ];
29400
- const disabledJson = new Set(local?.disabledMcpjsonServers ?? []);
29401
- const mcpServers = [
29402
- ...mcpFrom(globalConfig?.mcpServers, "user", globalConfigFile),
29403
- ...mcpFrom(local?.mcpServers, "local", globalConfigFile),
29404
- ...mcpFrom(readJson(path6.join(projectRoot(), ".mcp.json"))?.mcpServers, "project", path6.join(projectRoot(), ".mcp.json")).map(
29405
- (server2) => ({ ...server2, disabled: disabledJson.has(server2.name) })
29406
- )
29407
- ];
29408
- const hooks = [
29409
- ...hooksFrom(userSettings, "user", userSettingsFile),
29410
- ...hooksFrom(projectSettings, "project", projectSettingsFile),
29411
- ...hooksFrom(localSettings, "local", localSettingsFile)
29412
- ];
29413
- for (const plugin of plugins.filter((item) => item.enabled)) {
29414
- agents.push(...agentsIn(path6.join(plugin.path, "agents"), "plugin", plugin.id));
29415
- skills.push(...skillsIn(path6.join(plugin.path, "skills"), "plugin", plugin.id));
29416
- commands.push(...commandsIn(path6.join(plugin.path, "commands"), "plugin", plugin.id));
29417
- const pluginMcp = readJson(path6.join(plugin.path, ".mcp.json"));
29418
- mcpServers.push(...mcpFrom(pluginMcp?.mcpServers ?? pluginMcp, "plugin", path6.join(plugin.path, ".mcp.json"), plugin.id));
29419
- hooks.push(...hooksFrom(readJson(path6.join(plugin.path, "hooks", "hooks.json")), "plugin", path6.join(plugin.path, "hooks", "hooks.json"), plugin.id));
29420
- }
29421
- return { agents, skills, commands, mcpServers, hooks, plugins };
29422
- }
29423
- function memoryFiles() {
29424
- const files = [];
29425
- const add = (file, label, group, scope, always = false) => {
29426
- const exists = fs5.existsSync(file);
29427
- if (exists || always) files.push({ path: file, label, group, scope, exists });
29428
- };
29429
- add(path6.join(claudeHome, "CLAUDE.md"), "~/.claude/CLAUDE.md", "memory", "user", true);
29430
- add(path6.join(projectRoot(), "CLAUDE.md"), "CLAUDE.md", "memory", "project", true);
29431
- add(path6.join(projectRoot(), ".claude", "CLAUDE.md"), ".claude/CLAUDE.md", "memory", "project");
29432
- add(path6.join(projectRoot(), "CLAUDE.local.md"), "CLAUDE.local.md", "memory", "local");
29433
- for (const file of markdownFiles(path6.join(claudeHome, "rules"))) add(file, `~/.claude/rules/${path6.relative(path6.join(claudeHome, "rules"), file)}`, "rules", "user");
29434
- for (const file of markdownFiles(path6.join(projectRoot(), ".claude", "rules"))) add(file, `.claude/rules/${path6.relative(path6.join(projectRoot(), ".claude", "rules"), file)}`, "rules", "project");
29435
- for (const file of markdownFiles(path6.join(transcriptsDir(), "memory"), 1)) add(file, `memory/${path6.basename(file)}`, "auto-memory", "local");
29436
- const inventory = readInventory();
29437
- for (const agent of inventory.agents.filter((item) => item.scope !== "plugin" && item.path)) add(agent.path, `agent: ${agent.name}`, "agent", agent.scope);
29438
- for (const skill of inventory.skills.filter((item) => item.scope !== "plugin")) add(skill.path, `skill: ${skill.name}`, "skill", skill.scope);
29439
- for (const command of inventory.commands.filter((item) => item.scope !== "plugin")) add(command.path, `command: /${command.name}`, "command", command.scope);
29440
- return files.map((file) => ({ ...file, label: file.label.replace(/\\/g, "/") }));
29441
- }
29442
- function isMemoryFile(file) {
29443
- const wanted = path6.resolve(file).toLowerCase();
29444
- return memoryFiles().some((item) => path6.resolve(item.path).toLowerCase() === wanted);
29445
- }
29446
- function isDefinitionFile(file) {
29447
- const wanted = path6.resolve(file).toLowerCase();
29448
- const inventory = readInventory();
29449
- return [...inventory.agents, ...inventory.skills, ...inventory.commands].some((item) => item.path && path6.resolve(item.path).toLowerCase() === wanted);
29450
- }
29451
- var BUILT_IN_AGENTS;
29452
- var init_inventory = __esm({
29453
- "server/src/inventory.ts"() {
29454
- "use strict";
29455
- init_paths();
29456
- BUILT_IN_AGENTS = [
29457
- ["Explore", "Fast read-only search across the codebase."],
29458
- ["Plan", "Designs an implementation plan."],
29459
- ["general-purpose", "Multi-step research, code search and tasks."],
29460
- ["claude-code-guide", "Answers questions about Claude Code, the Agent SDK and the API."],
29461
- ["statusline-setup", "Configures the status line."],
29462
- ["web-fetch", "Fetches and reads web pages."]
29463
- ];
29464
- }
29465
- });
29466
-
29467
29531
  // server/src/transcripts.ts
29468
29532
  import fs6 from "node:fs";
29469
29533
  import path7 from "node:path";
@@ -29847,414 +29911,6 @@ var init_transcripts = __esm({
29847
29911
  }
29848
29912
  });
29849
29913
 
29850
- // server/src/toolset.ts
29851
- function normaliseToolset(input) {
29852
- const value = input ?? {};
29853
- const list = (items) => [...new Set((Array.isArray(items) ? items : []).map(String).filter(Boolean))].sort();
29854
- return { agents: list(value.agents), skills: list(value.skills), mcp: list(value.mcp), plugins: list(value.plugins) };
29855
- }
29856
- function isEmptyToolset(toolset) {
29857
- return !toolset.agents.length && !toolset.skills.length && !toolset.mcp.length && !toolset.plugins.length;
29858
- }
29859
- function mcpPrefix(server2) {
29860
- if (server2.scope !== "plugin") return `mcp__${toolName(server2.name)}`;
29861
- const [, plugin = "", name = ""] = server2.name.split(":");
29862
- return `mcp__plugin_${toolName(plugin)}_${toolName(name)}`;
29863
- }
29864
- function connectorsSeen() {
29865
- const prefixes = /* @__PURE__ */ new Set();
29866
- for (const summary of listTranscripts()) {
29867
- for (const name of Object.keys(transcriptDetail(summary.id)?.totals.tools ?? {})) {
29868
- const match = /^(mcp__claude_ai_[A-Za-z0-9_-]+?)__/.exec(name);
29869
- if (match) prefixes.add(match[1]);
29870
- }
29871
- }
29872
- return [...prefixes].sort();
29873
- }
29874
- function toolsetCatalog() {
29875
- const inventory = readInventory();
29876
- const items = [];
29877
- for (const plugin of inventory.plugins.filter((item) => item.enabled)) {
29878
- items.push({ kind: "plugins", id: plugin.id, label: plugin.id, description: `v${plugin.version}`, scope: plugin.scope === "user" ? "user" : "project", leavesContext: true });
29879
- }
29880
- for (const agent of inventory.agents) {
29881
- items.push({ kind: "agents", id: agent.name, label: agent.name, description: agent.description, scope: agent.scope, plugin: agent.plugin, leavesContext: true });
29882
- }
29883
- for (const skill of inventory.skills) {
29884
- items.push({ kind: "skills", id: skill.name, label: skill.name, description: skill.description, scope: skill.scope, plugin: skill.plugin, leavesContext: skill.scope !== "plugin" });
29885
- }
29886
- for (const server2 of inventory.mcpServers) {
29887
- items.push({ kind: "mcp", id: mcpPrefix(server2), label: server2.name, description: server2.target, scope: server2.scope, plugin: server2.plugin, leavesContext: false });
29888
- }
29889
- for (const prefix of connectorsSeen()) {
29890
- items.push({ kind: "mcp", id: prefix, label: prefix.replace(/^mcp__claude_ai_/, "claude.ai ").replace(/_/g, " "), description: "claude.ai connector", scope: "connector", leavesContext: false });
29891
- }
29892
- const seen = /* @__PURE__ */ new Set();
29893
- return items.filter((item) => {
29894
- const key = `${item.kind}:${item.id}`;
29895
- if (seen.has(key)) return false;
29896
- seen.add(key);
29897
- return true;
29898
- });
29899
- }
29900
- function toolsetSettings(toolset) {
29901
- if (isEmptyToolset(toolset)) return {};
29902
- const pluginSkills = new Set(readInventory().skills.filter((skill) => skill.scope === "plugin").map((skill) => skill.name));
29903
- const deny = [
29904
- ...toolset.agents.map((name) => `Agent(${name})`),
29905
- ...toolset.skills.filter((name) => pluginSkills.has(name)).map((name) => `Skill(${name})`),
29906
- ...toolset.mcp
29907
- ];
29908
- const overrides = Object.fromEntries(toolset.skills.filter((name) => !pluginSkills.has(name)).map((name) => [name, "off"]));
29909
- const settings = {};
29910
- if (deny.length) settings.permissions = { deny };
29911
- if (Object.keys(overrides).length) settings.skillOverrides = overrides;
29912
- if (toolset.plugins.length) settings.enabledPlugins = Object.fromEntries(toolset.plugins.map((id) => [id, false]));
29913
- return settings;
29914
- }
29915
- function listPresets() {
29916
- return db.prepare("SELECT * FROM presets ORDER BY name COLLATE NOCASE").all().map(toPreset);
29917
- }
29918
- function getPreset(id) {
29919
- const row = db.prepare("SELECT * FROM presets WHERE id = ?").get(id);
29920
- return row ? toPreset(row) : null;
29921
- }
29922
- function defaultToolset() {
29923
- const row = db.prepare("SELECT * FROM presets WHERE is_default = 1 LIMIT 1").get();
29924
- return row ? toPreset(row).toolset : EMPTY_TOOLSET;
29925
- }
29926
- function savePreset(input) {
29927
- const name = String(input.name ?? "").trim();
29928
- if (!name) throw new Error("A preset needs a name.");
29929
- const id = input.id ?? nanoid(10);
29930
- db.transaction(() => {
29931
- if (input.isDefault) db.prepare("UPDATE presets SET is_default = 0").run();
29932
- db.prepare(
29933
- `INSERT INTO presets (id, name, toolset, is_default, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)
29934
- ON CONFLICT(id) DO UPDATE SET name = excluded.name, toolset = excluded.toolset, is_default = excluded.is_default, updated_at = excluded.updated_at`
29935
- ).run(id, name, JSON.stringify(normaliseToolset(input.toolset)), input.isDefault ? 1 : 0, now(), now());
29936
- })();
29937
- broadcast("jobs", {});
29938
- return getPreset(id);
29939
- }
29940
- function setDefaultItem(input) {
29941
- const kind = String(input.kind ?? "");
29942
- const id = String(input.id ?? "").trim();
29943
- if (!(kind in EMPTY_TOOLSET) || !id) throw new Error("Send { kind, id, enabled }.");
29944
- const row = db.prepare("SELECT * FROM presets WHERE is_default = 1 LIMIT 1").get();
29945
- const preset = row ? toPreset(row) : { id: void 0, name: "Default", toolset: EMPTY_TOOLSET };
29946
- const off = preset.toolset[kind].filter((item) => item !== id);
29947
- if (input.enabled !== true) off.push(id);
29948
- return savePreset({ id: preset.id, name: preset.name, toolset: { ...preset.toolset, [kind]: off }, isDefault: true });
29949
- }
29950
- function deletePreset(id) {
29951
- db.prepare("DELETE FROM presets WHERE id = ?").run(id);
29952
- db.prepare("UPDATE jobs SET preset_id = NULL WHERE preset_id = ?").run(id);
29953
- broadcast("jobs", {});
29954
- }
29955
- var EMPTY_TOOLSET, toolName, toPreset;
29956
- var init_toolset = __esm({
29957
- "server/src/toolset.ts"() {
29958
- "use strict";
29959
- init_nanoid();
29960
- init_db();
29961
- init_events();
29962
- init_inventory();
29963
- init_transcripts();
29964
- EMPTY_TOOLSET = { agents: [], skills: [], mcp: [], plugins: [] };
29965
- toolName = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_");
29966
- toPreset = (row) => ({ id: row.id, name: row.name, toolset: normaliseToolset(parseJson(row.toolset, {})), isDefault: Boolean(row.is_default) });
29967
- }
29968
- });
29969
-
29970
- // server/src/channels/markdown.ts
29971
- function pieces(markdown) {
29972
- const out = [];
29973
- let prose = [];
29974
- let code = null;
29975
- for (const line of markdown.split("\n")) {
29976
- if (code) {
29977
- if (/^\s*```/.test(line)) {
29978
- out.push({ code: true, text: code.join("\n") });
29979
- code = null;
29980
- } else code.push(line);
29981
- } else if (/^\s*```[^`]*$/.test(line)) {
29982
- if (prose.length) out.push({ code: false, text: prose.join("\n") });
29983
- prose = [];
29984
- code = [];
29985
- } else prose.push(line);
29986
- }
29987
- if (code) out.push({ code: true, text: code.join("\n") });
29988
- if (prose.length) out.push({ code: false, text: prose.join("\n") });
29989
- return out;
29990
- }
29991
- function holdLinks(text, render2, rewrite) {
29992
- const links = [];
29993
- const held = text.replace(/\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)/g, (_whole, label, url) => `${links.push(render2(label, url)) - 1}`);
29994
- return rewrite(held).replace(/\u0002(\d+)\u0002/g, (_whole, index) => links[Number(index)]);
29995
- }
29996
- function lines(text, bold) {
29997
- return text.split("\n").map((line) => {
29998
- if (/^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/.test(line)) return null;
29999
- if (/^\s*\|.*\|\s*$/.test(line))
30000
- return line.trim().replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim()).filter(Boolean).join(" \xB7 ");
30001
- if (/^\s*(-{3,}|\*{3,}|_{3,})\s*$/.test(line)) return "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
30002
- const heading = /^\s*#{1,6}\s+(.*)$/.exec(line);
30003
- if (heading) return bold(heading[1].replace(/\*\*/g, ""));
30004
- const bullet = /^(\s*)[-*+]\s+(.*)$/.exec(line);
30005
- if (bullet) return `${bullet[1].length >= 2 ? " \u25E6 " : "\u2022 "}${bullet[2]}`;
30006
- return line.replace(/^\s*>\s?/, "");
30007
- }).filter((line) => line !== null).join("\n").replace(/\n{3,}/g, "\n\n");
30008
- }
30009
- function inline(text, codeOf, rest) {
30010
- const codes = [];
30011
- const held = text.replace(/`{1,3}([^`\n]+)`{1,3}/g, (_whole, code) => `\0${codes.push(code) - 1}\0`);
30012
- return rest(held).replace(/\u0000(\d+)\u0000/g, (_whole, index) => codeOf(codes[Number(index)]));
30013
- }
30014
- function toTelegramHtml(markdown, { inQuote = false } = {}) {
30015
- return pieces(markdown).map((piece) => {
30016
- if (piece.code) return inQuote ? `<code>${escapeHtml(piece.text)}</code>` : `<pre>${escapeHtml(piece.text)}</pre>`;
30017
- return inline(
30018
- lines(piece.text, (heading) => `**${heading}**`),
30019
- (code) => `<code>${escapeHtml(code)}</code>`,
30020
- (text) => holdLinks(
30021
- escapeHtml(text),
30022
- (label, url) => `<a href="${url.replace(/"/g, "&quot;")}">${label}</a>`,
30023
- (prose) => prose.replace(/\*\*([^*\n]+)\*\*/g, "<b>$1</b>").replace(/(^|[\s(])__([^_\n]+)__(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1<b>$2</b>").replace(/~~([^~\n]+)~~/g, "<s>$1</s>").replace(/(^|[\s(])\*([^*\s][^*\n]*?)\*(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1<i>$2</i>").replace(/(^|[\s(])_([^_\s][^_\n]*?)_(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1<i>$2</i>")
30024
- )
30025
- );
30026
- }).join("\n").replace(/\n{3,}/g, "\n\n").trim();
30027
- }
30028
- function toSlackMrkdwn(markdown) {
30029
- return pieces(markdown).map((piece) => {
30030
- if (piece.code) return `\`\`\`
30031
- ${escapeHtml(piece.text)}
30032
- \`\`\``;
30033
- return inline(
30034
- lines(piece.text, (heading) => `**${heading}**`),
30035
- // Escaped like the rest: "<https://x>" or "<!here>" in code would otherwise become a link or a mention.
30036
- (code) => `\`${escapeHtml(code)}\``,
30037
- (text) => holdLinks(
30038
- escapeHtml(text),
30039
- (label, url) => `<${url}|${label}>`,
30040
- (prose) => prose.replace(/(^|[\s(])\*([^*\s][^*\n]*?)\*(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1$2").replace(/\*\*([^*\n]+)\*\*/g, "*$1*").replace(/(^|[\s(])__([^_\n]+)__(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1*$2*").replace(/~~([^~\n]+)~~/g, "~$1~").replace(/\u0001/g, "_")
30041
- )
30042
- );
30043
- }).join("\n").replace(/\n{3,}/g, "\n\n").trim();
30044
- }
30045
- function gistOf(markdown, skip = "") {
30046
- const prose = pieces(markdown).filter((piece) => !piece.code).map((piece) => piece.text).join("\n");
30047
- const strip = (text) => text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^\s*#+\s+/, "").replace(/`+|\*\*|~~/g, "").replace(/(^|[\s(])(__|_|\*)([^_*\n]+?)\2(?=[\s),:;!?]|\.(?!\w)|$)/g, "$1$3").replace(/\s+/g, " ").trim();
30048
- const short2 = (text, max) => text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
30049
- const items = prose.split("\n").map((line) => /^\s{0,1}(?:[-*+]|\d+[.)])\s+(.*)$/.exec(line)?.[1]).filter((item) => Boolean(item)).map(strip).filter(Boolean);
30050
- if (items.length >= 2) {
30051
- const shown = items.slice(0, 4).map((item) => `\u2022 ${short2(item, 110)}`);
30052
- return items.length > 4 ? [...shown, `\u2026and ${items.length - 4} more`] : shown;
30053
- }
30054
- const opening = strip(
30055
- prose.split(/\n\s*\n/).map((part) => part.trim()).find((part) => part && !/^\s*(#|\|)/.test(part)) ?? ""
30056
- );
30057
- const sentences = opening.split(/(?<=[.?!])\s+/).slice(0, 2).join(" ");
30058
- const gist = short2(sentences, 260);
30059
- return gist && strip(skip) !== gist ? [gist] : [];
30060
- }
30061
- var escapeHtml;
30062
- var init_markdown = __esm({
30063
- "server/src/channels/markdown.ts"() {
30064
- "use strict";
30065
- escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
30066
- }
30067
- });
30068
-
30069
- // server/src/presentation.ts
30070
- function mainQuestion(message) {
30071
- const paragraphs = plain(message).split(/\n\s*\n/).map((part) => part.trim()).filter(Boolean);
30072
- const sentencesOf = (part) => part.split(/(?<=[.?!])\s+|\n+/).map((sentence) => sentence.trim()).filter(Boolean);
30073
- const last = paragraphs[paragraphs.length - 1] ?? "";
30074
- const sentences = sentencesOf(last);
30075
- const first = sentences.findIndex((sentence) => sentence.endsWith("?"));
30076
- if (first !== -1) {
30077
- const run4 = sentences.slice(first).join(" ");
30078
- const lastQuestion = [...sentences].reverse().find((sentence) => sentence.endsWith("?"));
30079
- return { headline: oneLine(run4.length <= 160 ? run4 : lastQuestion), isQuestion: true };
30080
- }
30081
- const anyQuestion = [...paragraphs].reverse().flatMap((part) => sentencesOf(part).filter((sentence) => sentence.endsWith("?")).reverse())[0];
30082
- if (anyQuestion) return { headline: oneLine(anyQuestion), isQuestion: true };
30083
- return { headline: oneLine(last || "Claude finished its turn."), isQuestion: false };
30084
- }
30085
- function target(toolName2, input) {
30086
- if (!input) return "";
30087
- const pick = (key) => typeof input[key] === "string" ? input[key] : "";
30088
- const file = pick("file_path") || pick("notebook_path") || pick("path");
30089
- if (file) return file.split(/[\\/]/).slice(-2).join("/");
30090
- const command = pick("command");
30091
- if (command) return command.split("\n")[0];
30092
- return pick("url") || pick("pattern") || pick("query") || pick("description") || (toolName2.startsWith("mcp__") ? toolName2.split("__").pop() ?? "" : "");
30093
- }
30094
- function present(interaction, sessionTitle2) {
30095
- const data = interaction.data ?? {};
30096
- const answerable = interaction.status === "open";
30097
- const base = { kind: interaction.kind, sessionTitle: sessionTitle2, answerable, quickReplies: [] };
30098
- switch (interaction.kind) {
30099
- case "permission": {
30100
- const tool = String(data.toolName ?? "a tool");
30101
- const what = target(tool, data.toolInput);
30102
- const shortTool = tool.startsWith("mcp__") ? tool.replace(/^mcp__/, "").split("__")[0] : tool;
30103
- return {
30104
- ...base,
30105
- headline: oneLine(`${shortTool}${what ? ` \u2192 ${what}` : ""}`, 120),
30106
- question: data.agent ? `The ${data.agent} agent asks to use ${tool}.` : `Claude asks to use ${tool}.`,
30107
- choices: [
30108
- { ref: "o:0", label: "Allow", tone: "primary" },
30109
- { ref: "o:1", label: "Allow for this session" },
30110
- { ref: "o:2", label: "Deny", tone: "danger" }
30111
- ],
30112
- freeText: "Deny with a reason for Claude...",
30113
- details: { label: "Full request", text: interaction.body, format: "code" }
30114
- };
30115
- }
30116
- case "question": {
30117
- const questions = (Array.isArray(data.questions) ? data.questions : []).map((item) => ({
30118
- header: item?.header ? String(item.header) : void 0,
30119
- question: String(item?.question ?? ""),
30120
- options: (Array.isArray(item?.options) ? item.options : []).map((option) => ({
30121
- label: String(option?.label ?? option),
30122
- description: option?.description ? String(option.description) : void 0
30123
- })),
30124
- multiSelect: Boolean(item?.multiSelect)
30125
- }));
30126
- const first = questions[0];
30127
- const single = questions.length === 1;
30128
- return {
30129
- ...base,
30130
- headline: oneLine(first?.question || interaction.body || interaction.title),
30131
- question: first?.header,
30132
- questions,
30133
- choices: single ? first.options.map((option, index) => ({ ref: `o:${index}`, label: option.label, description: option.description, tone: index === 0 ? "primary" : void 0 })) : [],
30134
- freeText: "Or answer in your own words..."
30135
- };
30136
- }
30137
- case "turn": {
30138
- const { headline, isQuestion } = mainQuestion(interaction.body);
30139
- const replies = isQuestion ? ["Yes", "No", "Continue"] : ["Continue"];
30140
- return {
30141
- ...base,
30142
- headline,
30143
- choices: [],
30144
- quickReplies: replies.map((label, index) => ({ ref: `q:${index}`, label })),
30145
- freeText: "Reply to the session...",
30146
- details: interaction.body && plain(interaction.body) !== headline ? { label: "Full message", text: interaction.body, format: "markdown" } : void 0,
30147
- gist: interaction.body ? gistOf(interaction.body, headline) : void 0
30148
- };
30149
- }
30150
- case "mcp":
30151
- return {
30152
- ...base,
30153
- headline: oneLine(`Needs the MCP server '${data.name ?? ""}'`),
30154
- question: data.reason ? oneLine(String(data.reason), 240) : void 0,
30155
- choices: [
30156
- { ref: "o:0", label: "Install & resume", tone: "primary" },
30157
- { ref: "o:1", label: "Skip" }
30158
- ],
30159
- freeText: "Or tell Claude what to do instead...",
30160
- details: data.command ? { label: "Install command", text: String(data.command), format: "code" } : void 0
30161
- };
30162
- case "done": {
30163
- const deliverables = Object.entries(data.deliverables ?? {}).map(([key, value]) => `- **${key}**: ${typeof value === "string" ? value : JSON.stringify(value)}`).join("\n");
30164
- return {
30165
- ...base,
30166
- answerable: false,
30167
- headline: oneLine(String(data.summary ?? interaction.body ?? "Task done."), 200),
30168
- question: data.category ? `Category: ${data.category}` : void 0,
30169
- choices: [],
30170
- freeText: null,
30171
- details: deliverables ? { label: "Deliverables", text: deliverables, format: "markdown" } : void 0
30172
- };
30173
- }
30174
- }
30175
- }
30176
- function resolveChoice(interaction, ref) {
30177
- const [type, raw] = ref.split(":");
30178
- const index = Number(raw);
30179
- if (!Number.isInteger(index)) return null;
30180
- if (type === "q") return present(interaction, "").quickReplies[index]?.label ?? null;
30181
- if (type !== "o") return null;
30182
- switch (interaction.kind) {
30183
- case "permission":
30184
- return ["1", "2", "3"][index] ?? null;
30185
- case "question":
30186
- return String(index + 1);
30187
- case "mcp":
30188
- return ["install", "skip"][index] ?? null;
30189
- default:
30190
- return null;
30191
- }
30192
- }
30193
- var oneLine, plain;
30194
- var init_presentation = __esm({
30195
- "server/src/presentation.ts"() {
30196
- "use strict";
30197
- init_markdown();
30198
- oneLine = (text, max = 160) => {
30199
- const flat2 = text.replace(/\s+/g, " ").trim();
30200
- return flat2.length > max ? `${flat2.slice(0, max - 1).trimEnd()}...` : flat2;
30201
- };
30202
- plain = (text) => text.replace(/`{3}[\s\S]*?`{3}/g, " ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/^\s*(#+|>)\s?/gm, "").replace(/`+|\*\*|~~/g, "").replace(/(^|[\s(])(__|_|\*)([^_*\n]+?)\2(?=[\s),:;!?]|\.(?!\w)|$)/gm, "$1$3").trim();
30203
- }
30204
- });
30205
-
30206
- // server/src/smallModel.ts
30207
- import { spawn as spawn3 } from "node:child_process";
30208
- import fs7 from "node:fs";
30209
- import path8 from "node:path";
30210
- function askSmallModel(prompt, timeoutMs = 45e3) {
30211
- const claude2 = findClaudeCode();
30212
- if (!claude2 || claudeStatus()?.problem) return Promise.resolve(null);
30213
- return new Promise((resolve) => {
30214
- const cwd = path8.join(hubHome, "global");
30215
- fs7.mkdirSync(cwd, { recursive: true });
30216
- const child = spawn3(claude2, ["-p", "--model", "haiku", "--tools", "", "--no-session-persistence", "--strict-mcp-config", "--output-format", "text"], {
30217
- cwd,
30218
- windowsHide: true,
30219
- stdio: ["pipe", "pipe", "ignore"]
30220
- });
30221
- let out = "";
30222
- const timer = setTimeout(() => child.kill(), timeoutMs);
30223
- child.stdout.on("data", (chunk) => out += chunk);
30224
- child.on("error", () => {
30225
- clearTimeout(timer);
30226
- resolve(null);
30227
- });
30228
- child.on("close", (code) => {
30229
- clearTimeout(timer);
30230
- resolve(code === 0 && out.trim() ? out : null);
30231
- });
30232
- child.stdin.end(prompt);
30233
- });
30234
- }
30235
- function jsonIn(answer, open) {
30236
- if (!answer) return null;
30237
- const start = answer.indexOf(open);
30238
- const end = answer.lastIndexOf(open === "[" ? "]" : "}");
30239
- if (start === -1 || end <= start) return null;
30240
- try {
30241
- return JSON.parse(answer.slice(start, end + 1));
30242
- } catch {
30243
- return null;
30244
- }
30245
- }
30246
- async function pickNumbers(prompt) {
30247
- const picked = jsonIn(await askSmallModel(prompt), "[");
30248
- return picked ? picked.map(Number).filter((number) => Number.isInteger(number) && number > 0) : null;
30249
- }
30250
- var init_smallModel = __esm({
30251
- "server/src/smallModel.ts"() {
30252
- "use strict";
30253
- init_claudeCode();
30254
- init_paths();
30255
- }
30256
- });
30257
-
30258
29914
  // server/src/channels/commands.ts
30259
29915
  var commands_exports = {};
30260
29916
  __export(commands_exports, {
@@ -31276,6 +30932,113 @@ var init_telegram = __esm({
31276
30932
  }
31277
30933
  });
31278
30934
 
30935
+ // server/src/watchdog.ts
30936
+ var watchdog_exports = {};
30937
+ __export(watchdog_exports, {
30938
+ answerTrust: () => answerTrust,
30939
+ startWatchdog: () => startWatchdog
30940
+ });
30941
+ function screenTail(terminal) {
30942
+ let text = "";
30943
+ for (let index = terminal.backlog.length - 1; index >= 0 && text.length < SCREEN_TAIL * 2; index -= 1) text = terminal.backlog[index] + text;
30944
+ return text.slice(-SCREEN_TAIL * 2).replace(ANSI, "").slice(-SCREEN_TAIL);
30945
+ }
30946
+ async function answerTrust(terminal, trust) {
30947
+ if (!trust) return pressKeys(terminal, "\x1B");
30948
+ for (let step = 0; step < 4; step += 1) {
30949
+ const flat2 = squeeze(screenTail(terminal).slice(-1500));
30950
+ const cursor = flat2.lastIndexOf("\u276F");
30951
+ const after = cursor >= 0 ? flat2.slice(cursor + 1) : "";
30952
+ if (after.startsWith("yes")) return pressKeys(terminal, "\r");
30953
+ const yesAt = flat2.lastIndexOf("yes,itrust");
30954
+ await pressKeys(terminal, yesAt > cursor ? "\x1B[B" : "\x1B[A");
30955
+ await sleep2(300);
30956
+ }
30957
+ throw new Error("Could not find the trust option on the screen.");
30958
+ }
30959
+ function lastLines(screen) {
30960
+ const lines2 = screen.split(/\r?\n/).map((line) => line.replace(/\s+/g, " ").trim()).filter(Boolean);
30961
+ return lines2.slice(-14).join("\n").slice(-1200);
30962
+ }
30963
+ function check2() {
30964
+ for (const terminal of listTerminals()) {
30965
+ if (terminal.finished || terminal.restarting) continue;
30966
+ const open = listInteractions({ sessionId: terminal.sessionId, status: "open" });
30967
+ const screen = screenTail(terminal);
30968
+ const trustOpen = open.find((item) => item.data?.trust);
30969
+ if (showsTrustDialog(screen)) {
30970
+ if (!trustOpen) {
30971
+ createInteraction({
30972
+ sessionId: terminal.sessionId,
30973
+ kind: "question",
30974
+ title: "Trust this folder?",
30975
+ body: "Claude Code asks whether to trust the files in this project's folder before it starts. Nothing runs until it is answered.",
30976
+ options: [
30977
+ { key: "1", label: "Yes, trust this folder" },
30978
+ { key: "2", label: "No, exit" }
30979
+ ],
30980
+ data: { trust: true, questions: [{ question: "Trust this folder?", options: ["Yes, trust this folder", "No, exit"] }] }
30981
+ });
30982
+ }
30983
+ continue;
30984
+ }
30985
+ if (trustOpen) closeOpen(terminal.sessionId, "terminal", ["question"]);
30986
+ const stalled = open.find((item) => item.data?.stalled);
30987
+ if (stalled && terminal.lastOutputAt > Date.parse(stalled.createdAt) + 5e3) {
30988
+ closeOpen(terminal.sessionId, "terminal", ["turn"]);
30989
+ setSessionStatus(terminal.sessionId, "running");
30990
+ continue;
30991
+ }
30992
+ const quietFor = Date.now() - terminal.lastOutputAt;
30993
+ if (quietFor < STALL_MS || open.length || reported.get(terminal.id) === terminal.lastOutputAt) continue;
30994
+ if (!getSessionRow(terminal.sessionId)?.run_id) continue;
30995
+ reported.set(terminal.id, terminal.lastOutputAt);
30996
+ createInteraction({
30997
+ sessionId: terminal.sessionId,
30998
+ kind: "turn",
30999
+ title: "This run looks stuck",
31000
+ 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.
31001
+
31002
+ ${lastLines(screen)}`,
31003
+ data: { stalled: true }
31004
+ });
31005
+ }
31006
+ for (const id of reported.keys()) if (!allTerminals().some((terminal) => terminal.id === id)) reported.delete(id);
31007
+ }
31008
+ function startWatchdog(intervalMs = 3e3) {
31009
+ const timer = setInterval(bindProject(() => {
31010
+ try {
31011
+ check2();
31012
+ } catch (error) {
31013
+ console.error("[watchdog]", error.message);
31014
+ }
31015
+ }), intervalMs);
31016
+ return () => clearInterval(timer);
31017
+ }
31018
+ var TRUST_MARKERS, STALL_MS, SCREEN_TAIL, ANSI, squeeze, showsTrustDialog, sleep2, reported;
31019
+ var init_watchdog = __esm({
31020
+ "server/src/watchdog.ts"() {
31021
+ "use strict";
31022
+ init_context();
31023
+ init_interactions();
31024
+ init_sessions();
31025
+ init_terminals();
31026
+ TRUST_MARKERS = ["doyoutrustthefilesinthisfolder", "isthisaprojectyoucreatedoroneyoutrust", "yes,itrustthisfolder"];
31027
+ STALL_MS = Number(process.env.CLAUDER_HUB_STALL_MS) || 8 * 6e4;
31028
+ SCREEN_TAIL = 8e3;
31029
+ ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>78]/g;
31030
+ squeeze = (text) => text.toLowerCase().replace(/\s+/g, "");
31031
+ showsTrustDialog = (screen) => {
31032
+ const flat2 = squeeze(screen.slice(-3e3));
31033
+ if (!TRUST_MARKERS.some((marker) => flat2.includes(marker))) return false;
31034
+ const end = Math.max(flat2.lastIndexOf("entertoconfirm"), flat2.lastIndexOf("itrustthisfolder"), flat2.lastIndexOf("no,exit"));
31035
+ return end >= 0 && flat2.length - end < 120;
31036
+ };
31037
+ sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31038
+ reported = /* @__PURE__ */ new Map();
31039
+ }
31040
+ });
31041
+
31279
31042
  // server/src/mcpSuggestions.ts
31280
31043
  var mcpSuggestions_exports = {};
31281
31044
  __export(mcpSuggestions_exports, {
@@ -31430,6 +31193,7 @@ __export(interactions_exports, {
31430
31193
  chooseInteraction: () => chooseInteraction,
31431
31194
  closeOpen: () => closeOpen,
31432
31195
  createInteraction: () => createInteraction,
31196
+ dismissInteraction: () => dismissInteraction,
31433
31197
  getInteraction: () => getInteraction,
31434
31198
  listInteractions: () => listInteractions,
31435
31199
  routeReply: () => routeReply,
@@ -31498,8 +31262,22 @@ function closeOpen(sessionId, by, kinds) {
31498
31262
  }
31499
31263
  broadcast("interactions", { sessionId });
31500
31264
  }
31265
+ function dismissInteraction(id) {
31266
+ const interaction = getInteraction(id);
31267
+ if (!interaction) throw new AnswerError("No such interaction.");
31268
+ if (interaction.status !== "open") return interaction;
31269
+ db.prepare("UPDATE interactions SET status = 'closed', answered_by = 'dismissed', updated_at = ? WHERE id = ?").run(now(), id);
31270
+ void acknowledgeOnChannels(interaction, "Dismissed on the dashboard.");
31271
+ broadcast("interactions", { sessionId: interaction.sessionId });
31272
+ return getInteraction(id);
31273
+ }
31501
31274
  async function answerQuestion(interaction, answer) {
31502
31275
  const terminal = terminalForSession(interaction.sessionId);
31276
+ if (interaction.data.trust) {
31277
+ const { answerTrust: answerTrust2 } = await Promise.resolve().then(() => (init_watchdog(), watchdog_exports));
31278
+ await answerTrust2(terminal, answer.trim() !== "2");
31279
+ return;
31280
+ }
31503
31281
  const questions = interaction.data.questions ?? [];
31504
31282
  const split = answer.includes("\n") ? answer.split("\n") : answer.split(/[;,]/);
31505
31283
  const parts = questions.length > 1 ? split.map((part) => part.trim()) : [answer.trim()];
@@ -31671,8 +31449,8 @@ __export(projects_exports, {
31671
31449
  touchProject: () => touchProject
31672
31450
  });
31673
31451
  import Database2 from "better-sqlite3";
31674
- import fs8 from "node:fs";
31675
- import path9 from "node:path";
31452
+ import fs7 from "node:fs";
31453
+ import path8 from "node:path";
31676
31454
  function listProjects() {
31677
31455
  return globalDb.prepare("SELECT * FROM projects ORDER BY opened_at DESC").all().map(toInfo);
31678
31456
  }
@@ -31689,19 +31467,19 @@ function globalProject() {
31689
31467
  }
31690
31468
  function bootGlobal() {
31691
31469
  if (global2) return global2.project;
31692
- const root2 = path9.join(hubHome, GLOBAL_ID);
31693
- fs8.mkdirSync(root2, { recursive: true });
31470
+ const root2 = path8.join(hubHome, GLOBAL_ID);
31471
+ fs7.mkdirSync(root2, { recursive: true });
31694
31472
  const project = {
31695
31473
  id: GLOBAL_ID,
31696
31474
  name: "Global",
31697
31475
  root: root2,
31698
- transcriptsDir: path9.join(claudeHome, "projects", projectSlug(root2)),
31476
+ transcriptsDir: path8.join(claudeHome, "projects", projectSlug(root2)),
31699
31477
  dataDir: root2,
31700
- db: openProjectDb(path9.join(root2, "hub.db"))
31478
+ db: openProjectDb(path8.join(root2, "hub.db"))
31701
31479
  };
31702
31480
  const stops = withProject(project, () => {
31703
31481
  recoverRuns();
31704
- const list = [startTranscriptWatcher(), startInteractionSweeper(), stopChannels];
31482
+ const list = [startTranscriptWatcher(), startInteractionSweeper(), startWatchdog(), stopChannels];
31705
31483
  startChannels({ device: false });
31706
31484
  return list;
31707
31485
  });
@@ -31714,7 +31492,7 @@ function freeId(name, root2) {
31714
31492
  const taken = new Set(globalDb.prepare("SELECT id FROM projects").all().map((row) => row.id));
31715
31493
  const free = (id) => {
31716
31494
  if (taken.has(id) || RESERVED.has(id)) return false;
31717
- if (!fs8.existsSync(path9.join(projectsDir, id))) return true;
31495
+ if (!fs7.existsSync(path8.join(projectsDir, id))) return true;
31718
31496
  const owner = ownerOf(id);
31719
31497
  return owner !== null && samePath(owner, root2);
31720
31498
  };
@@ -31722,8 +31500,8 @@ function freeId(name, root2) {
31722
31500
  for (let n2 = 2; ; n2++) if (free(`${base}-${n2}`)) return `${base}-${n2}`;
31723
31501
  }
31724
31502
  function importOldDatabase(root2, file) {
31725
- const old = path9.join(root2, ".clauder-hub", "data", "hub.db");
31726
- if (!fs8.existsSync(old) || fs8.existsSync(file)) return;
31503
+ const old = path8.join(root2, ".clauder-hub", "data", "hub.db");
31504
+ if (!fs7.existsSync(old) || fs7.existsSync(file)) return;
31727
31505
  const source = new Database2(old, { readonly: true, fileMustExist: true });
31728
31506
  try {
31729
31507
  source.prepare("VACUUM INTO ?").run(file);
@@ -31732,7 +31510,7 @@ function importOldDatabase(root2, file) {
31732
31510
  for (const row of overrides) insert.run(row.key, row.text, row.updated_at);
31733
31511
  console.log(` imported : ${old}`);
31734
31512
  } catch (error) {
31735
- fs8.rmSync(file, { force: true });
31513
+ fs7.rmSync(file, { force: true });
31736
31514
  console.error(`[projects] could not import ${old}:`, error.message);
31737
31515
  } finally {
31738
31516
  source.close();
@@ -31741,16 +31519,16 @@ function importOldDatabase(root2, file) {
31741
31519
  function boot(row) {
31742
31520
  const running = live.get(row.id);
31743
31521
  if (running) return running.project;
31744
- const dataDir = path9.join(projectsDir, row.id);
31745
- fs8.mkdirSync(dataDir, { recursive: true });
31746
- if (ownerOf(row.id) === null) fs8.writeFileSync(path9.join(dataDir, OWNER_FILE), row.root, "utf8");
31747
- const file = path9.join(dataDir, "hub.db");
31522
+ const dataDir = path8.join(projectsDir, row.id);
31523
+ fs7.mkdirSync(dataDir, { recursive: true });
31524
+ if (ownerOf(row.id) === null) fs7.writeFileSync(path8.join(dataDir, OWNER_FILE), row.root, "utf8");
31525
+ const file = path8.join(dataDir, "hub.db");
31748
31526
  importOldDatabase(row.root, file);
31749
31527
  const project = {
31750
31528
  id: row.id,
31751
31529
  name: row.name,
31752
31530
  root: row.root,
31753
- transcriptsDir: path9.join(claudeHome, "projects", projectSlug(row.root)),
31531
+ transcriptsDir: path8.join(claudeHome, "projects", projectSlug(row.root)),
31754
31532
  dataDir,
31755
31533
  db: openProjectDb(file)
31756
31534
  };
@@ -31759,7 +31537,7 @@ function boot(row) {
31759
31537
  withProject(project, () => {
31760
31538
  const stranded = recoverRuns();
31761
31539
  if (stranded) console.log(` note : ${row.name}: ${stranded} queued run(s) were running when the hub last stopped`);
31762
- stops.push(startTranscriptWatcher(), startInteractionSweeper());
31540
+ stops.push(startTranscriptWatcher(), startInteractionSweeper(), startWatchdog());
31763
31541
  stops.push(stopChannels);
31764
31542
  startChannels();
31765
31543
  stops.push(stopScheduler);
@@ -31778,7 +31556,7 @@ function boot(row) {
31778
31556
  }
31779
31557
  function bootProjects() {
31780
31558
  for (const row of globalDb.prepare("SELECT * FROM projects").all()) {
31781
- if (!fs8.existsSync(row.root)) continue;
31559
+ if (!fs7.existsSync(row.root)) continue;
31782
31560
  try {
31783
31561
  boot(row);
31784
31562
  } catch (error) {
@@ -31789,13 +31567,13 @@ function bootProjects() {
31789
31567
  }
31790
31568
  function addProject(input) {
31791
31569
  if (typeof input !== "string" || !input.trim()) throw new Error("Send the folder's full path.");
31792
- const typed = path9.resolve(input.trim());
31793
- if (!fs8.existsSync(typed) || !fs8.statSync(typed).isDirectory()) throw new Error(`${typed} is not a folder.`);
31794
- const root2 = fs8.realpathSync.native(typed);
31570
+ const typed = path8.resolve(input.trim());
31571
+ if (!fs7.existsSync(typed) || !fs7.statSync(typed).isDirectory()) throw new Error(`${typed} is not a folder.`);
31572
+ const root2 = fs7.realpathSync.native(typed);
31795
31573
  const rows = globalDb.prepare("SELECT * FROM projects").all();
31796
31574
  let row = rows.find((item) => samePath(item.root, root2));
31797
31575
  if (!row) {
31798
- const name = path9.basename(root2) || root2;
31576
+ const name = path8.basename(root2) || root2;
31799
31577
  row = { id: freeId(name, root2), root: root2, name, added_at: now(), opened_at: now() };
31800
31578
  boot(row);
31801
31579
  globalDb.prepare("INSERT INTO projects (id, root, name, added_at, opened_at) VALUES (?, ?, ?, ?, ?)").run(row.id, row.root, row.name, row.added_at, row.opened_at);
@@ -31840,18 +31618,19 @@ var init_projects = __esm({
31840
31618
  init_db();
31841
31619
  init_events();
31842
31620
  init_interactions();
31621
+ init_watchdog();
31843
31622
  init_jobs();
31844
31623
  init_paths();
31845
31624
  init_terminals();
31846
31625
  init_transcripts();
31847
31626
  live = /* @__PURE__ */ new Map();
31848
- toInfo = (row) => ({ id: row.id, name: row.name, root: row.root, addedAt: row.added_at, openedAt: row.opened_at, exists: fs8.existsSync(row.root) });
31627
+ toInfo = (row) => ({ id: row.id, name: row.name, root: row.root, addedAt: row.added_at, openedAt: row.opened_at, exists: fs7.existsSync(row.root) });
31849
31628
  global2 = null;
31850
31629
  samePath = (a, b2) => process.platform === "linux" ? a === b2 : a.toLowerCase() === b2.toLowerCase();
31851
31630
  OWNER_FILE = "root.txt";
31852
31631
  ownerOf = (id) => {
31853
31632
  try {
31854
- return fs8.readFileSync(path9.join(projectsDir, id, OWNER_FILE), "utf8").trim();
31633
+ return fs7.readFileSync(path8.join(projectsDir, id, OWNER_FILE), "utf8").trim();
31855
31634
  } catch {
31856
31635
  return null;
31857
31636
  }
@@ -31865,29 +31644,29 @@ var folderFinder_exports = {};
31865
31644
  __export(folderFinder_exports, {
31866
31645
  findFolders: () => findFolders
31867
31646
  });
31868
- import fs9 from "node:fs";
31869
- import os3 from "node:os";
31870
- import path10 from "node:path";
31647
+ import fs8 from "node:fs";
31648
+ import os2 from "node:os";
31649
+ import path9 from "node:path";
31871
31650
  function isProject(dir, entries) {
31872
31651
  return entries.some((entry) => MARKERS.includes(entry.name) || entry.isFile() && PROJECT_FILE.test(entry.name));
31873
31652
  }
31874
31653
  function roots() {
31875
- const home = os3.homedir();
31654
+ const home = os2.homedir();
31876
31655
  const found = /* @__PURE__ */ new Set();
31877
31656
  for (const project of allContexts()) {
31878
31657
  if (isGlobal(project)) continue;
31879
- found.add(path10.dirname(project.root));
31880
- found.add(path10.dirname(path10.dirname(project.root)));
31658
+ found.add(path9.dirname(project.root));
31659
+ found.add(path9.dirname(path9.dirname(project.root)));
31881
31660
  }
31882
- for (const dir of CODE_DIRS) found.add(path10.join(home, dir));
31661
+ for (const dir of CODE_DIRS) found.add(path9.join(home, dir));
31883
31662
  if (process.platform === "win32") {
31884
31663
  for (const letter of "CDEFG") {
31885
31664
  const drive = `${letter}:\\`;
31886
- if (!fs9.existsSync(drive)) continue;
31887
- for (const dir of CODE_DIRS) found.add(path10.join(drive, dir));
31665
+ if (!fs8.existsSync(drive)) continue;
31666
+ for (const dir of CODE_DIRS) found.add(path9.join(drive, dir));
31888
31667
  }
31889
31668
  }
31890
- const real = [...found].filter((dir) => dir !== path10.parse(dir).root && fs9.existsSync(dir)).map((dir) => fs9.realpathSync.native(dir));
31669
+ const real = [...found].filter((dir) => dir !== path9.parse(dir).root && fs8.existsSync(dir)).map((dir) => fs8.realpathSync.native(dir));
31891
31670
  return [...new Set(real)];
31892
31671
  }
31893
31672
  function scan() {
@@ -31899,22 +31678,22 @@ function scan() {
31899
31678
  visited += 1;
31900
31679
  let entries;
31901
31680
  try {
31902
- entries = fs9.readdirSync(dir, { withFileTypes: true });
31681
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
31903
31682
  } catch {
31904
31683
  return;
31905
31684
  }
31906
31685
  if (depth > 0 && isProject(dir, entries)) {
31907
31686
  let mtimeMs = 0;
31908
31687
  try {
31909
- mtimeMs = fs9.statSync(dir).mtimeMs;
31688
+ mtimeMs = fs8.statSync(dir).mtimeMs;
31910
31689
  } catch {
31911
31690
  }
31912
- folders.set(dir.toLowerCase(), { path: dir, name: path10.basename(dir), mtimeMs });
31691
+ folders.set(dir.toLowerCase(), { path: dir, name: path9.basename(dir), mtimeMs });
31913
31692
  return;
31914
31693
  }
31915
31694
  for (const entry of entries) {
31916
31695
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIP.has(entry.name.toLowerCase())) continue;
31917
- walk(path10.join(dir, entry.name), depth + 1);
31696
+ walk(path9.join(dir, entry.name), depth + 1);
31918
31697
  }
31919
31698
  };
31920
31699
  for (const root2 of roots()) walk(root2, 0);
@@ -31964,7 +31743,7 @@ var init_folderFinder = __esm({
31964
31743
  MARKERS = [".git", "package.json", "pyproject.toml", "requirements.txt", "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "composer.json", "Gemfile", "CLAUDE.md", ".claude"];
31965
31744
  PROJECT_FILE = /\.(sln|csproj)$/i;
31966
31745
  SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "bin", "obj", "target", "vendor", "venv", ".venv", "__pycache__", "appdata", "library", "application data", "$recycle.bin", "program files", "program files (x86)", "windows", "programdata"]);
31967
- CODE_DIRS = ["source", "src", "repos", "projects", "code", "dev", "work", "git", "github", "workspace", "documents", "desktop", path10.join("source", "repos"), "source code"];
31746
+ CODE_DIRS = ["source", "src", "repos", "projects", "code", "dev", "work", "git", "github", "workspace", "documents", "desktop", path9.join("source", "repos"), "source code"];
31968
31747
  cache2 = null;
31969
31748
  CACHE_MS = 5 * 6e4;
31970
31749
  }
@@ -32055,6 +31834,47 @@ function stopChannels() {
32055
31834
  adapters.delete(key);
32056
31835
  }
32057
31836
  }
31837
+ function readSetting(key, fallback) {
31838
+ const row = db.prepare("SELECT value FROM settings WHERE key = ?").get(key);
31839
+ return parseJson(row?.value, fallback);
31840
+ }
31841
+ function writeSetting(key, value) {
31842
+ db.prepare("INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at").run(
31843
+ key,
31844
+ JSON.stringify(value),
31845
+ now()
31846
+ );
31847
+ }
31848
+ function notificationsPaused() {
31849
+ return readSetting("notificationsPaused", false) === true;
31850
+ }
31851
+ function setNotificationsPaused(paused) {
31852
+ writeSetting("notificationsPaused", paused);
31853
+ broadcast("channels", {});
31854
+ return { paused };
31855
+ }
31856
+ function notifyKinds() {
31857
+ const saved = readSetting("notifyKinds", null);
31858
+ if (saved) return saved.filter((kind) => INTERACTION_KINDS.includes(kind));
31859
+ const channels = allChannels();
31860
+ return channels.length ? INTERACTION_KINDS.filter((kind) => channels.some((channel) => channel.kinds.includes(kind))) : [...INTERACTION_KINDS];
31861
+ }
31862
+ function setNotifyKinds(kinds) {
31863
+ writeSetting("notifyKinds", kinds.filter((kind) => INTERACTION_KINDS.includes(kind)));
31864
+ broadcast("channels", {});
31865
+ return notifyKinds();
31866
+ }
31867
+ function quietSessions() {
31868
+ return readSetting("quietSessions", []);
31869
+ }
31870
+ function setSessionQuiet(sessionId, quiet) {
31871
+ const next = new Set(quietSessions());
31872
+ if (quiet) next.add(sessionId);
31873
+ else next.delete(sessionId);
31874
+ writeSetting("quietSessions", [...next]);
31875
+ broadcast("sessions", { sessionId });
31876
+ return { quiet };
31877
+ }
32058
31878
  function mutedProjects() {
32059
31879
  const global3 = globalContext();
32060
31880
  if (!global3) return [];
@@ -32107,8 +31927,9 @@ function messageText(interaction, session) {
32107
31927
  async function deliver(interaction, sessionTitle2, projectId) {
32108
31928
  const text = messageText(interaction, sessionTitle2);
32109
31929
  const presented = present(interaction, sessionTitle2);
31930
+ if (!notifyKinds().includes(interaction.kind)) return;
32110
31931
  for (const channel of allChannels()) {
32111
- if (!channel.enabled || !channel.kinds.includes(interaction.kind)) continue;
31932
+ if (!channel.enabled) continue;
32112
31933
  const adapter = adapters.get(adapterKey(channel.id));
32113
31934
  if (!adapter) continue;
32114
31935
  const thread = db.prepare("SELECT thread FROM channel_threads WHERE channel_id = ? AND session_id = ?").get(channel.id, interaction.sessionId)?.thread ?? null;
@@ -32149,12 +31970,14 @@ async function deliver(interaction, sessionTitle2, projectId) {
32149
31970
  }
32150
31971
  }
32151
31972
  async function notifyChannels(interaction, sessionTitle2) {
31973
+ if (notificationsPaused() || quietSessions().includes(interaction.sessionId)) return;
32152
31974
  await deliver(interaction, sessionTitle2, null);
32153
31975
  broadcast("channels", {});
32154
31976
  const project = currentProject();
32155
31977
  const global3 = globalContext();
32156
31978
  if (!global3 || isGlobal(project) || mutedProjects().includes(project.id)) return;
32157
31979
  await withProject(global3, async () => {
31980
+ if (notificationsPaused()) return;
32158
31981
  await deliver(interaction, `${project.name} \xB7 ${sessionTitle2}`, project.id);
32159
31982
  broadcast("channels", {});
32160
31983
  });
@@ -32464,6 +32287,461 @@ var init_channels = __esm({
32464
32287
  }
32465
32288
  });
32466
32289
 
32290
+ // server/src/sessionFiles.ts
32291
+ import fs9 from "node:fs";
32292
+ import os3 from "node:os";
32293
+ import path10 from "node:path";
32294
+ function checkId(id) {
32295
+ if (!UUID.test(id)) throw new Error("Not a session id.");
32296
+ }
32297
+ function transcriptFile(id) {
32298
+ return path10.join(transcriptsDir(), `${id}.jsonl`);
32299
+ }
32300
+ function setSessionTitle(id, title) {
32301
+ checkId(id);
32302
+ const name = title.replace(/\s+/g, " ").trim();
32303
+ if (!name) throw new Error("The name is empty.");
32304
+ if (name.length > 200) throw new Error("Keep the name under 200 characters.");
32305
+ const file = transcriptFile(id);
32306
+ if (!fs9.existsSync(file)) throw new Error("This session has no transcript yet; give it a moment to start.");
32307
+ const size = fs9.statSync(file).size;
32308
+ let lead = "";
32309
+ if (size > 0) {
32310
+ const fd = fs9.openSync(file, "r");
32311
+ const last = Buffer.alloc(1);
32312
+ fs9.readSync(fd, last, 0, 1, size - 1);
32313
+ fs9.closeSync(fd);
32314
+ if (last.toString() !== "\n") lead = "\n";
32315
+ }
32316
+ fs9.appendFileSync(file, `${lead}${JSON.stringify({ type: "custom-title", customTitle: name, sessionId: id })}
32317
+ `, "utf8");
32318
+ broadcast("sessions", { sessionId: id });
32319
+ }
32320
+ function sizeOf(target2) {
32321
+ const stat = fs9.statSync(target2);
32322
+ if (!stat.isDirectory()) return stat.size;
32323
+ return fs9.readdirSync(target2).reduce((total, name) => total + sizeOf(path10.join(target2, name)), 0);
32324
+ }
32325
+ function tempDirOf(id) {
32326
+ const base = process.env.CLAUDE_CODE_TMPDIR ?? os3.tmpdir();
32327
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
32328
+ const roots2 = [path10.join(base, "claude"), ...uid !== null ? [path10.join(base, `claude-${uid}`)] : []];
32329
+ const candidates2 = roots2.map((root2) => path10.join(root2, projectSlug(projectRoot()), id));
32330
+ return candidates2.find((candidate) => fs9.existsSync(candidate)) ?? candidates2[0];
32331
+ }
32332
+ function sessionDiskBytes(id) {
32333
+ if (!UUID.test(id)) return null;
32334
+ const key = `${projectRoot()}\0${id}`;
32335
+ const hit = diskCache.get(key);
32336
+ if (hit && Date.now() - hit.at < 6e4) return hit.bytes;
32337
+ let bytes = null;
32338
+ try {
32339
+ const plan = deletionPlan(id);
32340
+ bytes = plan.totalBytes + (plan.temp?.bytes ?? 0);
32341
+ } catch {
32342
+ }
32343
+ if (bytes !== null) diskCache.set(key, { bytes, at: Date.now() });
32344
+ return bytes ?? hit?.bytes ?? null;
32345
+ }
32346
+ function deletionPlan(id) {
32347
+ checkId(id);
32348
+ const candidates2 = [
32349
+ [transcriptFile(id), "transcript"],
32350
+ [path10.join(transcriptsDir(), id), "subagent transcripts and tool output"],
32351
+ [path10.join(claudeHome, "file-history", id), "file snapshots used to rewind"],
32352
+ [path10.join(claudeHome, "session-env", id), "session environment"]
32353
+ ];
32354
+ const files = candidates2.filter(([target2]) => fs9.existsSync(target2)).map(([target2, what]) => ({ path: target2, bytes: sizeOf(target2), what }));
32355
+ const temp = tempDirOf(id);
32356
+ return {
32357
+ sessionId: id,
32358
+ files,
32359
+ totalBytes: files.reduce((total, item) => total + item.bytes, 0),
32360
+ temp: fs9.existsSync(temp) ? { path: temp, bytes: sizeOf(temp) } : null
32361
+ };
32362
+ }
32363
+ async function deleteSession(id, options = {}) {
32364
+ checkId(id);
32365
+ const terminal = terminalForSession(id);
32366
+ if (terminal) {
32367
+ killTerminal(terminal);
32368
+ await waitForExit(terminal);
32369
+ if (terminalForSession(id)) throw new Error("The session did not stop in time; nothing was deleted. Try again.");
32370
+ }
32371
+ const plan = deletionPlan(id);
32372
+ for (const item of plan.files) fs9.rmSync(item.path, { recursive: true, force: true });
32373
+ if (options.clearTemp !== false && plan.temp) fs9.rmSync(plan.temp.path, { recursive: true, force: true });
32374
+ db.transaction(() => {
32375
+ for (const table of ["interactions", "mcp_suggestions", "channel_refs", "channel_threads", "sessions"]) {
32376
+ db.prepare(`DELETE FROM ${table} WHERE ${table === "sessions" ? "id" : "session_id"} = ?`).run(id);
32377
+ }
32378
+ })();
32379
+ forgetTranscript(id);
32380
+ broadcast("sessions", {});
32381
+ broadcast("interactions", {});
32382
+ broadcast("mcp", {});
32383
+ return plan;
32384
+ }
32385
+ var UUID, diskCache;
32386
+ var init_sessionFiles = __esm({
32387
+ "server/src/sessionFiles.ts"() {
32388
+ "use strict";
32389
+ init_db();
32390
+ init_events();
32391
+ init_paths();
32392
+ init_terminals();
32393
+ init_transcripts();
32394
+ UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
32395
+ diskCache = /* @__PURE__ */ new Map();
32396
+ }
32397
+ });
32398
+
32399
+ // server/src/inventory.ts
32400
+ import fs10 from "node:fs";
32401
+ import os4 from "node:os";
32402
+ import path11 from "node:path";
32403
+ function readJson(file) {
32404
+ try {
32405
+ return JSON.parse(fs10.readFileSync(file, "utf8"));
32406
+ } catch {
32407
+ return null;
32408
+ }
32409
+ }
32410
+ function frontMatter(text) {
32411
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
32412
+ if (!match) return {};
32413
+ const fields = {};
32414
+ let lastKey = "";
32415
+ for (const line of match[1].split(/\r?\n/)) {
32416
+ const pair = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
32417
+ if (pair) {
32418
+ lastKey = pair[1];
32419
+ fields[lastKey] = pair[2].replace(/^["']|["']$/g, "").replace(/^[>|]-?$/, "");
32420
+ } else if (lastKey && /^\s+\S/.test(line)) {
32421
+ fields[lastKey] = `${fields[lastKey]} ${line.trim()}`.trim();
32422
+ }
32423
+ }
32424
+ return fields;
32425
+ }
32426
+ function definition(file, fallbackName, scope, plugin) {
32427
+ let text;
32428
+ try {
32429
+ text = fs10.readFileSync(file, "utf8");
32430
+ } catch {
32431
+ return null;
32432
+ }
32433
+ const meta = frontMatter(text);
32434
+ const name = meta.name || fallbackName;
32435
+ return {
32436
+ name: plugin ? `${plugin.split("@")[0]}:${name}` : name,
32437
+ description: meta.description ?? "",
32438
+ scope,
32439
+ plugin,
32440
+ path: file,
32441
+ model: meta.model || void 0
32442
+ };
32443
+ }
32444
+ function markdownFiles(dir, depth = 3) {
32445
+ const found = [];
32446
+ const walk = (current, left) => {
32447
+ let entries;
32448
+ try {
32449
+ entries = fs10.readdirSync(current, { withFileTypes: true });
32450
+ } catch {
32451
+ return;
32452
+ }
32453
+ for (const entry of entries) {
32454
+ const full = path11.join(current, entry.name);
32455
+ if (entry.isDirectory() && left > 0) walk(full, left - 1);
32456
+ else if (entry.isFile() && entry.name.endsWith(".md")) found.push(full);
32457
+ }
32458
+ };
32459
+ walk(dir, depth);
32460
+ return found;
32461
+ }
32462
+ function agentsIn(dir, scope, plugin) {
32463
+ return markdownFiles(dir, 1).map((file) => definition(file, path11.basename(file, ".md"), scope, plugin)).filter((item) => item !== null);
32464
+ }
32465
+ function skillsIn(dir, scope, plugin) {
32466
+ let entries;
32467
+ try {
32468
+ entries = fs10.readdirSync(dir, { withFileTypes: true });
32469
+ } catch {
32470
+ return [];
32471
+ }
32472
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => definition(path11.join(dir, entry.name, "SKILL.md"), entry.name, scope, plugin)).filter((item) => item !== null);
32473
+ }
32474
+ function commandsIn(dir, scope, plugin) {
32475
+ return markdownFiles(dir, 2).map((file) => definition(file, path11.relative(dir, file).replace(/\.md$/, "").replace(/[\\/]/g, ":"), scope, plugin)).filter((item) => item !== null);
32476
+ }
32477
+ function mcpFrom(servers, scope, source, plugin) {
32478
+ if (!servers || typeof servers !== "object") return [];
32479
+ return Object.entries(servers).map(([name, config]) => ({
32480
+ name: plugin ? `plugin:${plugin.split("@")[0]}:${name}` : name,
32481
+ scope,
32482
+ plugin,
32483
+ transport: String(config?.type ?? (config?.url ? "http" : "stdio")),
32484
+ target: config?.url ? String(config.url) : [config?.command, ...Array.isArray(config?.args) ? config.args : []].filter(Boolean).join(" "),
32485
+ source
32486
+ }));
32487
+ }
32488
+ function hooksFrom(settings, scope, source, plugin) {
32489
+ const hooks = settings?.hooks;
32490
+ if (!hooks || typeof hooks !== "object") return [];
32491
+ const items = [];
32492
+ for (const [event, groups] of Object.entries(hooks)) {
32493
+ if (!Array.isArray(groups)) continue;
32494
+ for (const group of groups) {
32495
+ for (const hook of Array.isArray(group?.hooks) ? group.hooks : []) {
32496
+ items.push({
32497
+ event,
32498
+ matcher: String(group?.matcher ?? ""),
32499
+ type: String(hook?.type ?? "command"),
32500
+ scope,
32501
+ plugin,
32502
+ source,
32503
+ summary: String(hook?.command ?? hook?.url ?? hook?.prompt ?? hook?.tool ?? "").slice(0, 200)
32504
+ });
32505
+ }
32506
+ }
32507
+ }
32508
+ return items;
32509
+ }
32510
+ function localProjectConfig(global3) {
32511
+ const projects = global3?.projects ?? {};
32512
+ const wanted = [projectRoot(), projectRoot().replace(/\\/g, "/")];
32513
+ for (const [key, value] of Object.entries(projects)) {
32514
+ if (wanted.some((candidate) => candidate.toLowerCase() === key.toLowerCase())) return value;
32515
+ }
32516
+ return null;
32517
+ }
32518
+ function readInventory() {
32519
+ const userSettingsFile = path11.join(claudeHome, "settings.json");
32520
+ const projectSettingsFile = path11.join(projectRoot(), ".claude", "settings.json");
32521
+ const localSettingsFile = path11.join(projectRoot(), ".claude", "settings.local.json");
32522
+ const userSettings = readJson(userSettingsFile);
32523
+ const projectSettings = readJson(projectSettingsFile);
32524
+ const localSettings = readJson(localSettingsFile);
32525
+ const globalConfigFile = path11.join(os4.homedir(), ".claude.json");
32526
+ const globalConfig = readJson(globalConfigFile);
32527
+ const local = localProjectConfig(globalConfig);
32528
+ const enabled = {
32529
+ ...userSettings?.enabledPlugins ?? {},
32530
+ ...projectSettings?.enabledPlugins ?? {},
32531
+ ...localSettings?.enabledPlugins ?? {}
32532
+ };
32533
+ const plugins = [];
32534
+ const installed = readJson(path11.join(claudeHome, "plugins", "installed_plugins.json"));
32535
+ for (const [id, installs] of Object.entries(installed?.plugins ?? {})) {
32536
+ const install = Array.isArray(installs) ? installs[installs.length - 1] : null;
32537
+ if (!install?.installPath) continue;
32538
+ plugins.push({
32539
+ id,
32540
+ version: String(install.version ?? ""),
32541
+ scope: String(install.scope ?? "user"),
32542
+ // Installed plugins are on unless a settings file turns them off.
32543
+ enabled: enabled[id] !== false,
32544
+ path: install.installPath
32545
+ });
32546
+ }
32547
+ const agents = [
32548
+ ...agentsIn(path11.join(claudeHome, "agents"), "user"),
32549
+ ...agentsIn(path11.join(projectRoot(), ".claude", "agents"), "project"),
32550
+ // No file behind these, so no path: they can be turned off, not edited.
32551
+ ...BUILT_IN_AGENTS.map(([name, description]) => ({ name, description, scope: "built-in", path: "" }))
32552
+ ];
32553
+ const skills = [...skillsIn(path11.join(claudeHome, "skills"), "user"), ...skillsIn(path11.join(projectRoot(), ".claude", "skills"), "project")];
32554
+ const commands = [
32555
+ ...commandsIn(path11.join(claudeHome, "commands"), "user"),
32556
+ ...commandsIn(path11.join(projectRoot(), ".claude", "commands"), "project")
32557
+ ];
32558
+ const disabledJson = new Set(local?.disabledMcpjsonServers ?? []);
32559
+ const mcpServers = [
32560
+ ...mcpFrom(globalConfig?.mcpServers, "user", globalConfigFile),
32561
+ ...mcpFrom(local?.mcpServers, "local", globalConfigFile),
32562
+ ...mcpFrom(readJson(path11.join(projectRoot(), ".mcp.json"))?.mcpServers, "project", path11.join(projectRoot(), ".mcp.json")).map(
32563
+ (server2) => ({ ...server2, disabled: disabledJson.has(server2.name) })
32564
+ )
32565
+ ];
32566
+ const hooks = [
32567
+ ...hooksFrom(userSettings, "user", userSettingsFile),
32568
+ ...hooksFrom(projectSettings, "project", projectSettingsFile),
32569
+ ...hooksFrom(localSettings, "local", localSettingsFile)
32570
+ ];
32571
+ for (const plugin of plugins.filter((item) => item.enabled)) {
32572
+ agents.push(...agentsIn(path11.join(plugin.path, "agents"), "plugin", plugin.id));
32573
+ skills.push(...skillsIn(path11.join(plugin.path, "skills"), "plugin", plugin.id));
32574
+ commands.push(...commandsIn(path11.join(plugin.path, "commands"), "plugin", plugin.id));
32575
+ const pluginMcp = readJson(path11.join(plugin.path, ".mcp.json"));
32576
+ mcpServers.push(...mcpFrom(pluginMcp?.mcpServers ?? pluginMcp, "plugin", path11.join(plugin.path, ".mcp.json"), plugin.id));
32577
+ hooks.push(...hooksFrom(readJson(path11.join(plugin.path, "hooks", "hooks.json")), "plugin", path11.join(plugin.path, "hooks", "hooks.json"), plugin.id));
32578
+ }
32579
+ return { agents, skills, commands, mcpServers, hooks, plugins };
32580
+ }
32581
+ function memoryFiles() {
32582
+ const files = [];
32583
+ const add = (file, label, group, scope, always = false) => {
32584
+ const exists = fs10.existsSync(file);
32585
+ if (exists || always) files.push({ path: file, label, group, scope, exists });
32586
+ };
32587
+ add(path11.join(claudeHome, "CLAUDE.md"), "~/.claude/CLAUDE.md", "memory", "user", true);
32588
+ add(path11.join(projectRoot(), "CLAUDE.md"), "CLAUDE.md", "memory", "project", true);
32589
+ add(path11.join(projectRoot(), ".claude", "CLAUDE.md"), ".claude/CLAUDE.md", "memory", "project");
32590
+ add(path11.join(projectRoot(), "CLAUDE.local.md"), "CLAUDE.local.md", "memory", "local");
32591
+ for (const file of markdownFiles(path11.join(claudeHome, "rules"))) add(file, `~/.claude/rules/${path11.relative(path11.join(claudeHome, "rules"), file)}`, "rules", "user");
32592
+ for (const file of markdownFiles(path11.join(projectRoot(), ".claude", "rules"))) add(file, `.claude/rules/${path11.relative(path11.join(projectRoot(), ".claude", "rules"), file)}`, "rules", "project");
32593
+ for (const file of markdownFiles(path11.join(transcriptsDir(), "memory"), 1)) add(file, `memory/${path11.basename(file)}`, "auto-memory", "local");
32594
+ const inventory = readInventory();
32595
+ for (const agent of inventory.agents.filter((item) => item.scope !== "plugin" && item.path)) add(agent.path, `agent: ${agent.name}`, "agent", agent.scope);
32596
+ for (const skill of inventory.skills.filter((item) => item.scope !== "plugin")) add(skill.path, `skill: ${skill.name}`, "skill", skill.scope);
32597
+ for (const command of inventory.commands.filter((item) => item.scope !== "plugin")) add(command.path, `command: /${command.name}`, "command", command.scope);
32598
+ return files.map((file) => ({ ...file, label: file.label.replace(/\\/g, "/") }));
32599
+ }
32600
+ function isMemoryFile(file) {
32601
+ const wanted = path11.resolve(file).toLowerCase();
32602
+ return memoryFiles().some((item) => path11.resolve(item.path).toLowerCase() === wanted);
32603
+ }
32604
+ function isDefinitionFile(file) {
32605
+ const wanted = path11.resolve(file).toLowerCase();
32606
+ const inventory = readInventory();
32607
+ return [...inventory.agents, ...inventory.skills, ...inventory.commands].some((item) => item.path && path11.resolve(item.path).toLowerCase() === wanted);
32608
+ }
32609
+ var BUILT_IN_AGENTS;
32610
+ var init_inventory = __esm({
32611
+ "server/src/inventory.ts"() {
32612
+ "use strict";
32613
+ init_paths();
32614
+ BUILT_IN_AGENTS = [
32615
+ ["Explore", "Fast read-only search across the codebase."],
32616
+ ["Plan", "Designs an implementation plan."],
32617
+ ["general-purpose", "Multi-step research, code search and tasks."],
32618
+ ["claude-code-guide", "Answers questions about Claude Code, the Agent SDK and the API."],
32619
+ ["statusline-setup", "Configures the status line."],
32620
+ ["web-fetch", "Fetches and reads web pages."]
32621
+ ];
32622
+ }
32623
+ });
32624
+
32625
+ // server/src/toolset.ts
32626
+ function normaliseToolset(input) {
32627
+ const value = input ?? {};
32628
+ const list = (items) => [...new Set((Array.isArray(items) ? items : []).map(String).filter(Boolean))].sort();
32629
+ return { agents: list(value.agents), skills: list(value.skills), mcp: list(value.mcp), plugins: list(value.plugins) };
32630
+ }
32631
+ function isEmptyToolset(toolset) {
32632
+ return !toolset.agents.length && !toolset.skills.length && !toolset.mcp.length && !toolset.plugins.length;
32633
+ }
32634
+ function mcpPrefix(server2) {
32635
+ if (server2.scope !== "plugin") return `mcp__${toolName(server2.name)}`;
32636
+ const [, plugin = "", name = ""] = server2.name.split(":");
32637
+ return `mcp__plugin_${toolName(plugin)}_${toolName(name)}`;
32638
+ }
32639
+ function connectorsSeen() {
32640
+ const prefixes = /* @__PURE__ */ new Set();
32641
+ for (const summary of listTranscripts()) {
32642
+ for (const name of Object.keys(transcriptDetail(summary.id)?.totals.tools ?? {})) {
32643
+ const match = /^(mcp__claude_ai_[A-Za-z0-9_-]+?)__/.exec(name);
32644
+ if (match) prefixes.add(match[1]);
32645
+ }
32646
+ }
32647
+ return [...prefixes].sort();
32648
+ }
32649
+ function toolsetCatalog() {
32650
+ const inventory = readInventory();
32651
+ const items = [];
32652
+ for (const plugin of inventory.plugins.filter((item) => item.enabled)) {
32653
+ items.push({ kind: "plugins", id: plugin.id, label: plugin.id, description: `v${plugin.version}`, scope: plugin.scope === "user" ? "user" : "project", leavesContext: true });
32654
+ }
32655
+ for (const agent of inventory.agents) {
32656
+ items.push({ kind: "agents", id: agent.name, label: agent.name, description: agent.description, scope: agent.scope, plugin: agent.plugin, leavesContext: true });
32657
+ }
32658
+ for (const skill of inventory.skills) {
32659
+ items.push({ kind: "skills", id: skill.name, label: skill.name, description: skill.description, scope: skill.scope, plugin: skill.plugin, leavesContext: skill.scope !== "plugin" });
32660
+ }
32661
+ for (const server2 of inventory.mcpServers) {
32662
+ items.push({ kind: "mcp", id: mcpPrefix(server2), label: server2.name, description: server2.target, scope: server2.scope, plugin: server2.plugin, leavesContext: false });
32663
+ }
32664
+ for (const prefix of connectorsSeen()) {
32665
+ items.push({ kind: "mcp", id: prefix, label: prefix.replace(/^mcp__claude_ai_/, "claude.ai ").replace(/_/g, " "), description: "claude.ai connector", scope: "connector", leavesContext: false });
32666
+ }
32667
+ const seen = /* @__PURE__ */ new Set();
32668
+ return items.filter((item) => {
32669
+ const key = `${item.kind}:${item.id}`;
32670
+ if (seen.has(key)) return false;
32671
+ seen.add(key);
32672
+ return true;
32673
+ });
32674
+ }
32675
+ function toolsetSettings(toolset) {
32676
+ if (isEmptyToolset(toolset)) return {};
32677
+ const pluginSkills = new Set(readInventory().skills.filter((skill) => skill.scope === "plugin").map((skill) => skill.name));
32678
+ const deny = [
32679
+ ...toolset.agents.map((name) => `Agent(${name})`),
32680
+ ...toolset.skills.filter((name) => pluginSkills.has(name)).map((name) => `Skill(${name})`),
32681
+ ...toolset.mcp
32682
+ ];
32683
+ const overrides = Object.fromEntries(toolset.skills.filter((name) => !pluginSkills.has(name)).map((name) => [name, "off"]));
32684
+ const settings = {};
32685
+ if (deny.length) settings.permissions = { deny };
32686
+ if (Object.keys(overrides).length) settings.skillOverrides = overrides;
32687
+ if (toolset.plugins.length) settings.enabledPlugins = Object.fromEntries(toolset.plugins.map((id) => [id, false]));
32688
+ return settings;
32689
+ }
32690
+ function listPresets() {
32691
+ return db.prepare("SELECT * FROM presets ORDER BY name COLLATE NOCASE").all().map(toPreset);
32692
+ }
32693
+ function getPreset(id) {
32694
+ const row = db.prepare("SELECT * FROM presets WHERE id = ?").get(id);
32695
+ return row ? toPreset(row) : null;
32696
+ }
32697
+ function defaultToolset() {
32698
+ const row = db.prepare("SELECT * FROM presets WHERE is_default = 1 LIMIT 1").get();
32699
+ return row ? toPreset(row).toolset : EMPTY_TOOLSET;
32700
+ }
32701
+ function savePreset(input) {
32702
+ const name = String(input.name ?? "").trim();
32703
+ if (!name) throw new Error("A preset needs a name.");
32704
+ const id = input.id ?? nanoid(10);
32705
+ db.transaction(() => {
32706
+ if (input.isDefault) db.prepare("UPDATE presets SET is_default = 0").run();
32707
+ db.prepare(
32708
+ `INSERT INTO presets (id, name, toolset, is_default, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)
32709
+ ON CONFLICT(id) DO UPDATE SET name = excluded.name, toolset = excluded.toolset, is_default = excluded.is_default, updated_at = excluded.updated_at`
32710
+ ).run(id, name, JSON.stringify(normaliseToolset(input.toolset)), input.isDefault ? 1 : 0, now(), now());
32711
+ })();
32712
+ broadcast("jobs", {});
32713
+ return getPreset(id);
32714
+ }
32715
+ function setDefaultItem(input) {
32716
+ const kind = String(input.kind ?? "");
32717
+ const id = String(input.id ?? "").trim();
32718
+ if (!(kind in EMPTY_TOOLSET) || !id) throw new Error("Send { kind, id, enabled }.");
32719
+ const row = db.prepare("SELECT * FROM presets WHERE is_default = 1 LIMIT 1").get();
32720
+ const preset = row ? toPreset(row) : { id: void 0, name: "Default", toolset: EMPTY_TOOLSET };
32721
+ const off = preset.toolset[kind].filter((item) => item !== id);
32722
+ if (input.enabled !== true) off.push(id);
32723
+ return savePreset({ id: preset.id, name: preset.name, toolset: { ...preset.toolset, [kind]: off }, isDefault: true });
32724
+ }
32725
+ function deletePreset(id) {
32726
+ db.prepare("DELETE FROM presets WHERE id = ?").run(id);
32727
+ db.prepare("UPDATE jobs SET preset_id = NULL WHERE preset_id = ?").run(id);
32728
+ broadcast("jobs", {});
32729
+ }
32730
+ var EMPTY_TOOLSET, toolName, toPreset;
32731
+ var init_toolset = __esm({
32732
+ "server/src/toolset.ts"() {
32733
+ "use strict";
32734
+ init_nanoid();
32735
+ init_db();
32736
+ init_events();
32737
+ init_inventory();
32738
+ init_transcripts();
32739
+ EMPTY_TOOLSET = { agents: [], skills: [], mcp: [], plugins: [] };
32740
+ toolName = (value) => value.replace(/[^A-Za-z0-9_-]/g, "_");
32741
+ toPreset = (row) => ({ id: row.id, name: row.name, toolset: normaliseToolset(parseJson(row.toolset, {})), isDefault: Boolean(row.is_default) });
32742
+ }
32743
+ });
32744
+
32467
32745
  // server/src/mcpCatalog.ts
32468
32746
  function catalogEntry(id) {
32469
32747
  return MCP_CATALOG.find((entry) => entry.id === id) ?? null;
@@ -32694,7 +32972,6 @@ function channelParams() {
32694
32972
  { name: "kind", type: `"${CHANNEL_KINDS.map((kind) => kind.kind).join('" | "')}"`, required: true, description: "The platform. Cannot change after creation.", example: "slack" },
32695
32973
  { name: "name", type: "string", required: true, description: "Shown in the dashboard.", example: "Team Slack" },
32696
32974
  { name: "enabled", type: "boolean", required: false, description: "Default true.", example: "true" },
32697
- { name: "kinds", type: "string[]", required: false, description: `What it is sent; any of ${INTERACTION_KINDS.join(", ")}. Default all.`, example: '["permission", "question", "done"]' },
32698
32975
  { name: "config", type: "object", required: true, description: `Per platform. ${fields}`, example: '{ "botToken": "xoxb-...", "appToken": "xapp-...", "channel": "C0123456789" }' }
32699
32976
  ];
32700
32977
  }
@@ -32778,8 +33055,8 @@ function definitionOf(purpose) {
32778
33055
  summary: isGlobal(currentProject()) ? "A global channel serves every project in the hub: their permission requests, questions, ends of turns, MCP suggestions and finished tasks all arrive on it, each named after its project, and replies go back into the right session. A bot used here cannot also be a project's own channel." : "A channel is where sessions reach the person: permission requests, questions, ends of turns, MCP suggestions and finished tasks. Replies there go back into the session.",
32779
33056
  params: channelParams(),
32780
33057
  samples: [
32781
- { kind: "slack", name: "Team Slack", enabled: true, kinds: ["permission", "question", "turn", "mcp", "done"], config: { botToken: "xoxb-...", appToken: "xapp-...", channel: "C0123456789" } },
32782
- { kind: "telegram", name: "My phone", enabled: true, kinds: ["permission", "question", "done"], config: { botToken: "123456:ABC-...", chatId: "123456789" } }
33058
+ { kind: "slack", name: "Team Slack", enabled: true, config: { botToken: "xoxb-...", appToken: "xapp-...", channel: "C0123456789" } },
33059
+ { kind: "telegram", name: "My phone", enabled: true, config: { botToken: "123456:ABC-...", chatId: "123456789" } }
32783
33060
  ],
32784
33061
  api: [
32785
33062
  "GET /api/channels (kinds and fields, existing channels with secrets masked as ********)",
@@ -32807,7 +33084,7 @@ function currentValue(purpose) {
32807
33084
  }
32808
33085
  case "edit-channel": {
32809
33086
  const channel = listChannels().find((item) => item.id === purpose.targetId);
32810
- return channel && { kind: channel.kind, name: channel.name, enabled: channel.enabled, kinds: channel.kinds, config: channel.config };
33087
+ return channel && { kind: channel.kind, name: channel.name, enabled: channel.enabled, config: channel.config };
32811
33088
  }
32812
33089
  default:
32813
33090
  return void 0;
@@ -32937,8 +33214,8 @@ JSON`;
32937
33214
 
32938
33215
  // server/src/sessions.ts
32939
33216
  import { randomUUID } from "node:crypto";
32940
- import fs10 from "node:fs";
32941
- import path11 from "node:path";
33217
+ import fs11 from "node:fs";
33218
+ import path12 from "node:path";
32942
33219
  function getSessionRow(id) {
32943
33220
  return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id) ?? null;
32944
33221
  }
@@ -33034,11 +33311,11 @@ function startSession(options) {
33034
33311
  const category = options.category !== void 0 ? options.category || null : existing?.category ?? null;
33035
33312
  const token = nanoid(24);
33036
33313
  const toolset = options.toolset ? normaliseToolset(options.toolset) : existing?.toolset ? normaliseToolset(JSON.parse(existing.toolset)) : defaultToolset();
33037
- const settingsFile = path11.join(runDir, `${token}.settings.json`);
33038
- const promptFile = path11.join(runDir, `${token}.prompt.md`);
33039
- fs10.writeFileSync(settingsFile, JSON.stringify({ ...toolsetSettings(toolset), ...hookSettings(token) }, null, 2), "utf8");
33314
+ const settingsFile = path12.join(runDir, `${token}.settings.json`);
33315
+ const promptFile = path12.join(runDir, `${token}.prompt.md`);
33316
+ fs11.writeFileSync(settingsFile, JSON.stringify({ ...toolsetSettings(toolset), ...hookSettings(token) }, null, 2), "utf8");
33040
33317
  const purpose = options.purpose ?? (existing?.purpose ? JSON.parse(existing.purpose) : null);
33041
- fs10.writeFileSync(promptFile, systemPromptFor(category, purpose, existing ? sessionSetup(sessionId) : null), "utf8");
33318
+ fs11.writeFileSync(promptFile, systemPromptFor(category, purpose, existing ? sessionSetup(sessionId) : null), "utf8");
33042
33319
  const args = ["--settings", settingsFile, "--append-system-prompt-file", promptFile];
33043
33320
  args.push(...options.resumeId ? ["--resume", options.resumeId] : ["--session-id", sessionId]);
33044
33321
  if (options.name) args.push("--name", options.name);
@@ -33087,7 +33364,7 @@ function terminalByToken(token) {
33087
33364
  return allTerminals().find((terminal) => terminal.token === token);
33088
33365
  }
33089
33366
  function cleanRunFiles(token) {
33090
- for (const suffix of [".settings.json", ".prompt.md"]) fs10.rmSync(path11.join(runDir, `${token}${suffix}`), { force: true });
33367
+ for (const suffix of [".settings.json", ".prompt.md"]) fs11.rmSync(path12.join(runDir, `${token}${suffix}`), { force: true });
33091
33368
  }
33092
33369
  function purposeOf(row) {
33093
33370
  const purpose = parseJson(row?.purpose, null);
@@ -33101,6 +33378,7 @@ function listSessions() {
33101
33378
  )
33102
33379
  );
33103
33380
  const terminals2 = new Map(listTerminals().map((terminal) => [terminal.sessionId, terminal]));
33381
+ const quiet = new Set(quietSessions());
33104
33382
  const views = [];
33105
33383
  const seen = /* @__PURE__ */ new Set();
33106
33384
  const view = (id, summary) => {
@@ -33121,7 +33399,9 @@ function listSessions() {
33121
33399
  runId: row?.run_id ?? null,
33122
33400
  openInteractions: open.get(id) ?? 0,
33123
33401
  done: parseJson(row?.done_json, null),
33124
- purpose
33402
+ purpose,
33403
+ diskBytes: sessionDiskBytes(id),
33404
+ quiet: quiet.has(id)
33125
33405
  };
33126
33406
  };
33127
33407
  for (const summary of listTranscripts()) {
@@ -33144,7 +33424,9 @@ var init_sessions = __esm({
33144
33424
  init_db();
33145
33425
  init_events();
33146
33426
  init_paths();
33427
+ init_channels();
33147
33428
  init_prompts();
33429
+ init_sessionFiles();
33148
33430
  init_terminals();
33149
33431
  init_toolset();
33150
33432
  init_purposes();
@@ -33421,6 +33703,7 @@ init_claudeCode();
33421
33703
  init_context();
33422
33704
  init_db();
33423
33705
  import fs14 from "node:fs";
33706
+ import net from "node:net";
33424
33707
  import path15 from "node:path";
33425
33708
  import { fileURLToPath as fileURLToPath2 } from "node:url";
33426
33709
 
@@ -33865,16 +34148,16 @@ import os5 from "node:os";
33865
34148
  import path14 from "node:path";
33866
34149
 
33867
34150
  // server/src/fileIcons.ts
33868
- import fs11 from "node:fs";
34151
+ import fs12 from "node:fs";
33869
34152
  import nodeModule from "node:module";
33870
- import path12 from "node:path";
34153
+ import path13 from "node:path";
33871
34154
  var resolveFrom = nodeModule.createRequire(import.meta.url);
33872
- var root = path12.dirname(resolveFrom.resolve("material-icon-theme/package.json"));
33873
- var iconsDir = path12.join(root, "icons");
34155
+ var root = path13.dirname(resolveFrom.resolve("material-icon-theme/package.json"));
34156
+ var iconsDir = path13.join(root, "icons");
33874
34157
  var manifest = null;
33875
34158
  function iconManifest() {
33876
34159
  if (!manifest) {
33877
- const full = JSON.parse(fs11.readFileSync(path12.join(root, "dist", "material-icons.json"), "utf8"));
34160
+ const full = JSON.parse(fs12.readFileSync(path13.join(root, "dist", "material-icons.json"), "utf8"));
33878
34161
  manifest = {
33879
34162
  file: full.file,
33880
34163
  folder: full.folder,
@@ -33889,8 +34172,8 @@ function iconManifest() {
33889
34172
  }
33890
34173
  function iconFile(name) {
33891
34174
  if (!/^[\w.-]+$/.test(name)) return null;
33892
- const file = path12.join(iconsDir, `${name}.svg`);
33893
- return fs11.existsSync(file) ? file : null;
34175
+ const file = path13.join(iconsDir, `${name}.svg`);
34176
+ return fs12.existsSync(file) ? file : null;
33894
34177
  }
33895
34178
 
33896
34179
  // server/src/routes.ts
@@ -34039,96 +34322,7 @@ init_prompts();
34039
34322
  init_sessions();
34040
34323
  init_terminals();
34041
34324
  init_purposes();
34042
-
34043
- // server/src/sessionFiles.ts
34044
- init_db();
34045
- init_events();
34046
- init_paths();
34047
- init_terminals();
34048
- init_transcripts();
34049
- import fs12 from "node:fs";
34050
- import os4 from "node:os";
34051
- import path13 from "node:path";
34052
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
34053
- function checkId(id) {
34054
- if (!UUID.test(id)) throw new Error("Not a session id.");
34055
- }
34056
- function transcriptFile(id) {
34057
- return path13.join(transcriptsDir(), `${id}.jsonl`);
34058
- }
34059
- function setSessionTitle(id, title) {
34060
- checkId(id);
34061
- const name = title.replace(/\s+/g, " ").trim();
34062
- if (!name) throw new Error("The name is empty.");
34063
- if (name.length > 200) throw new Error("Keep the name under 200 characters.");
34064
- const file = transcriptFile(id);
34065
- if (!fs12.existsSync(file)) throw new Error("This session has no transcript yet; give it a moment to start.");
34066
- const size = fs12.statSync(file).size;
34067
- let lead = "";
34068
- if (size > 0) {
34069
- const fd = fs12.openSync(file, "r");
34070
- const last = Buffer.alloc(1);
34071
- fs12.readSync(fd, last, 0, 1, size - 1);
34072
- fs12.closeSync(fd);
34073
- if (last.toString() !== "\n") lead = "\n";
34074
- }
34075
- fs12.appendFileSync(file, `${lead}${JSON.stringify({ type: "custom-title", customTitle: name, sessionId: id })}
34076
- `, "utf8");
34077
- broadcast("sessions", { sessionId: id });
34078
- }
34079
- function sizeOf(target2) {
34080
- const stat = fs12.statSync(target2);
34081
- if (!stat.isDirectory()) return stat.size;
34082
- return fs12.readdirSync(target2).reduce((total, name) => total + sizeOf(path13.join(target2, name)), 0);
34083
- }
34084
- function tempDirOf(id) {
34085
- const base = process.env.CLAUDE_CODE_TMPDIR ?? os4.tmpdir();
34086
- const uid = typeof process.getuid === "function" ? process.getuid() : null;
34087
- const roots2 = [path13.join(base, "claude"), ...uid !== null ? [path13.join(base, `claude-${uid}`)] : []];
34088
- const candidates2 = roots2.map((root2) => path13.join(root2, projectSlug(projectRoot()), id));
34089
- return candidates2.find((candidate) => fs12.existsSync(candidate)) ?? candidates2[0];
34090
- }
34091
- function deletionPlan(id) {
34092
- checkId(id);
34093
- const candidates2 = [
34094
- [transcriptFile(id), "transcript"],
34095
- [path13.join(transcriptsDir(), id), "subagent transcripts and tool output"],
34096
- [path13.join(claudeHome, "file-history", id), "file snapshots used to rewind"],
34097
- [path13.join(claudeHome, "session-env", id), "session environment"]
34098
- ];
34099
- const files = candidates2.filter(([target2]) => fs12.existsSync(target2)).map(([target2, what]) => ({ path: target2, bytes: sizeOf(target2), what }));
34100
- const temp = tempDirOf(id);
34101
- return {
34102
- sessionId: id,
34103
- files,
34104
- totalBytes: files.reduce((total, item) => total + item.bytes, 0),
34105
- temp: fs12.existsSync(temp) ? { path: temp, bytes: sizeOf(temp) } : null
34106
- };
34107
- }
34108
- async function deleteSession(id, options = {}) {
34109
- checkId(id);
34110
- const terminal = terminalForSession(id);
34111
- if (terminal) {
34112
- killTerminal(terminal);
34113
- await waitForExit(terminal);
34114
- if (terminalForSession(id)) throw new Error("The session did not stop in time; nothing was deleted. Try again.");
34115
- }
34116
- const plan = deletionPlan(id);
34117
- for (const item of plan.files) fs12.rmSync(item.path, { recursive: true, force: true });
34118
- if (options.clearTemp !== false && plan.temp) fs12.rmSync(plan.temp.path, { recursive: true, force: true });
34119
- db.transaction(() => {
34120
- for (const table of ["interactions", "mcp_suggestions", "channel_refs", "channel_threads", "sessions"]) {
34121
- db.prepare(`DELETE FROM ${table} WHERE ${table === "sessions" ? "id" : "session_id"} = ?`).run(id);
34122
- }
34123
- })();
34124
- forgetTranscript(id);
34125
- broadcast("sessions", {});
34126
- broadcast("interactions", {});
34127
- broadcast("mcp", {});
34128
- return plan;
34129
- }
34130
-
34131
- // server/src/routes.ts
34325
+ init_sessionFiles();
34132
34326
  init_toolset();
34133
34327
  init_transcripts();
34134
34328
  var api = import_express2.default.Router();
@@ -34364,6 +34558,16 @@ api.post(
34364
34558
  "/interactions/:id/answer",
34365
34559
  wrap2((req) => answerInteraction(req.params.id, String(req.body?.answer ?? ""), "dashboard"))
34366
34560
  );
34561
+ api.post("/interactions/:id/dismiss", wrap2((req) => dismissInteraction(req.params.id)));
34562
+ api.post(
34563
+ "/sessions/:id/dismiss-waiting",
34564
+ wrap2((req) => {
34565
+ const open = listInteractions({ sessionId: req.params.id, status: "open" });
34566
+ for (const item of open) dismissInteraction(item.id);
34567
+ return { dismissed: open.length };
34568
+ })
34569
+ );
34570
+ api.put("/sessions/:id/quiet", wrap2((req) => setSessionQuiet(req.params.id, Boolean(req.body?.quiet))));
34367
34571
  api.get("/inventory", wrap2(() => readInventory()));
34368
34572
  api.get(
34369
34573
  "/stats",
@@ -34437,7 +34641,9 @@ api.put(
34437
34641
  return { path: file, content };
34438
34642
  })
34439
34643
  );
34440
- api.get("/channels", wrap2(() => ({ kinds: CHANNEL_KINDS, interactionKinds: INTERACTION_KINDS, channels: listChannels() })));
34644
+ api.get("/channels", wrap2(() => ({ kinds: CHANNEL_KINDS, interactionKinds: INTERACTION_KINDS, channels: listChannels(), paused: notificationsPaused(), notifyKinds: notifyKinds() })));
34645
+ api.put("/notifications/kinds", wrap2((req) => ({ notifyKinds: setNotifyKinds(Array.isArray(req.body?.kinds) ? req.body.kinds.map(String) : []) })));
34646
+ api.put("/notifications/paused", wrap2((req) => setNotificationsPaused(Boolean(req.body?.paused))));
34441
34647
  api.post("/channels", wrap2((req) => saveChannel(req.body ?? {})));
34442
34648
  api.put("/channels/:id", wrap2((req) => saveChannel({ ...req.body ?? {}, id: req.params.id })));
34443
34649
  api.delete("/channels/:id", wrap2((req) => deleteChannel(req.params.id)));
@@ -34566,6 +34772,21 @@ async function otherHubPort() {
34566
34772
  }
34567
34773
  return null;
34568
34774
  }
34775
+ var listening = (port2) => new Promise((resolve) => {
34776
+ const socket = net.connect(port2, "127.0.0.1");
34777
+ const done = (answer) => {
34778
+ socket.destroy();
34779
+ resolve(answer);
34780
+ };
34781
+ socket.setTimeout(2e3);
34782
+ socket.once("connect", () => done(true));
34783
+ socket.once("timeout", () => done(true));
34784
+ socket.once("error", () => done(false));
34785
+ });
34786
+ var leftover = readLock();
34787
+ if (leftover?.port && alive(leftover.pid) && !await listening(leftover.port) && readLock()?.pid === leftover.pid) {
34788
+ fs14.rmSync(lockFile, { force: true });
34789
+ }
34569
34790
  if (!takeLock() && !await otherHubPort().then((found) => found === null && takeLock())) {
34570
34791
  const otherPort = await otherHubPort();
34571
34792
  if (!otherPort) {