hotsheet 0.17.0-beta.22 → 0.17.0-beta.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/channel.js CHANGED
@@ -34145,9 +34145,9 @@ var StdioServerTransport = class {
34145
34145
 
34146
34146
  // src/channel.ts
34147
34147
  init_zod();
34148
- import { readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
34148
+ import { readFileSync as readFileSync3 } from "fs";
34149
34149
  import { createServer } from "http";
34150
- import { join as join3 } from "path";
34150
+ import { join as join4 } from "path";
34151
34151
 
34152
34152
  // src/channel.tools.ts
34153
34153
  init_zod();
@@ -34685,9 +34685,113 @@ async function callTool(name, args2, dataDir2, fetchFn = globalThis.fetch) {
34685
34685
  }
34686
34686
 
34687
34687
  // src/channel-config.ts
34688
- import { basename as basename2, dirname, join as join2, resolve } from "path";
34688
+ import { basename as basename2, dirname, join as join3, resolve } from "path";
34689
34689
  init_zod();
34690
34690
 
34691
+ // src/channelLog.ts
34692
+ import { appendFileSync, renameSync, statSync } from "fs";
34693
+ import { join as join2 } from "path";
34694
+ var CHANNEL_LOG_MAX_BYTES = 1048576;
34695
+ function createChannelLogger(logPath, options = {}) {
34696
+ let injectedBlankLine = false;
34697
+ const pidSuffix = options.pidLabel !== void 0 && options.pidLabel !== "" ? ` ${options.pidLabel}` : "";
34698
+ return {
34699
+ log(event, details) {
34700
+ try {
34701
+ const size = safeStatSize(logPath);
34702
+ if (size >= CHANNEL_LOG_MAX_BYTES) {
34703
+ try {
34704
+ renameSync(logPath, `${logPath}.old`);
34705
+ } catch {
34706
+ }
34707
+ }
34708
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
34709
+ const detailsText = details === void 0 || details === "" ? "" : ` ${details}`;
34710
+ const prefix = !injectedBlankLine && size > 0 ? "\n" : "";
34711
+ injectedBlankLine = true;
34712
+ appendFileSync(logPath, `${prefix}[${ts}] [pid ${process.pid}${pidSuffix}] ${event}:${detailsText}
34713
+ `, "utf-8");
34714
+ } catch {
34715
+ }
34716
+ }
34717
+ };
34718
+ }
34719
+ function safeStatSize(path) {
34720
+ try {
34721
+ return statSync(path).size;
34722
+ } catch {
34723
+ return 0;
34724
+ }
34725
+ }
34726
+
34727
+ // src/channelPortFile.ts
34728
+ import { closeSync, openSync, readFileSync as readFileSync2, renameSync as renameSync2, unlinkSync, writeFileSync } from "fs";
34729
+ function readChannelInfo(portFile2) {
34730
+ let raw;
34731
+ try {
34732
+ raw = readFileSync2(portFile2, "utf-8").trim();
34733
+ } catch {
34734
+ return null;
34735
+ }
34736
+ if (raw === "") return null;
34737
+ if (raw.startsWith("{")) {
34738
+ try {
34739
+ const parsed = JSON.parse(raw);
34740
+ if (typeof parsed !== "object" || parsed === null) return null;
34741
+ const obj = parsed;
34742
+ if (typeof obj.port !== "number" || !Number.isInteger(obj.port)) return null;
34743
+ return {
34744
+ port: obj.port,
34745
+ pid: typeof obj.pid === "number" && Number.isInteger(obj.pid) ? obj.pid : null,
34746
+ slug: typeof obj.slug === "string" && obj.slug !== "" ? obj.slug : null,
34747
+ startedAt: typeof obj.startedAt === "string" && obj.startedAt !== "" ? obj.startedAt : null
34748
+ };
34749
+ } catch {
34750
+ return null;
34751
+ }
34752
+ }
34753
+ const legacyPort = parseInt(raw, 10);
34754
+ if (isNaN(legacyPort)) return null;
34755
+ return { port: legacyPort, pid: null, slug: null, startedAt: null };
34756
+ }
34757
+ function writeChannelInfo(portFile2, info) {
34758
+ const tmp = `${portFile2}.tmp.${process.pid.toString(36)}`;
34759
+ const body = JSON.stringify({
34760
+ port: info.port,
34761
+ pid: info.pid,
34762
+ slug: info.slug,
34763
+ startedAt: info.startedAt
34764
+ });
34765
+ writeFileSync(tmp, body, "utf-8");
34766
+ renameSync2(tmp, portFile2);
34767
+ try {
34768
+ const slash = portFile2.lastIndexOf("/");
34769
+ if (slash > 0) {
34770
+ const fd = openSync(portFile2.slice(0, slash), "r");
34771
+ try {
34772
+ closeSync(fd);
34773
+ } catch {
34774
+ }
34775
+ }
34776
+ } catch {
34777
+ }
34778
+ }
34779
+ function maybeUnlinkPortFile(portFile2, myPort2, myPid = process.pid) {
34780
+ const info = readChannelInfo(portFile2);
34781
+ if (info === null) return false;
34782
+ if (info.pid !== null) {
34783
+ if (info.pid !== myPid) return false;
34784
+ } else {
34785
+ if (info.port !== myPort2) return false;
34786
+ }
34787
+ try {
34788
+ unlinkSync(portFile2);
34789
+ return true;
34790
+ } catch {
34791
+ return false;
34792
+ }
34793
+ }
34794
+
34691
34795
  // src/claude-allow-rule.ts
34692
34796
  init_zod();
34693
34797
  init_file_settings();
@@ -34731,6 +34835,48 @@ function clearAllPermissions() {
34731
34835
  queue.length = 0;
34732
34836
  }
34733
34837
 
34838
+ // src/channelPortFileWatcher.ts
34839
+ var REWRITE_BURST_MAX = 5;
34840
+ var REWRITE_BURST_WINDOW_MS = 6e4;
34841
+ function installPortFileWatcher(opts) {
34842
+ const intervalMs = opts.intervalMs ?? 5e3;
34843
+ const setIntervalFn = opts.setIntervalFn ?? setInterval;
34844
+ const clearIntervalFn = opts.clearIntervalFn ?? ((handle2) => {
34845
+ clearInterval(handle2);
34846
+ });
34847
+ const rewriteTimestamps = [];
34848
+ let livelocked = false;
34849
+ const tick = () => {
34850
+ if (livelocked) return;
34851
+ const current = readChannelInfo(opts.portFile);
34852
+ if (current !== null && current.pid === opts.info.pid && current.port === opts.info.port && current.slug === opts.info.slug) {
34853
+ return;
34854
+ }
34855
+ const now = Date.now();
34856
+ while (rewriteTimestamps.length > 0 && now - rewriteTimestamps[0] > REWRITE_BURST_WINDOW_MS) {
34857
+ rewriteTimestamps.shift();
34858
+ }
34859
+ if (rewriteTimestamps.length >= REWRITE_BURST_MAX) {
34860
+ livelocked = true;
34861
+ opts.log?.("port-file-heal-livelock", `${String(REWRITE_BURST_MAX)} rewrites in ${String(REWRITE_BURST_WINDOW_MS)}ms \u2014 giving up to avoid hot-spin`);
34862
+ return;
34863
+ }
34864
+ rewriteTimestamps.push(now);
34865
+ const reason = current === null ? "vanished" : `pid-mismatch on-disk=${String(current.pid)} ours=${String(opts.info.pid)}`;
34866
+ try {
34867
+ writeChannelInfo(opts.portFile, opts.info);
34868
+ opts.log?.("port-file-heal-rewrite", reason);
34869
+ opts.notify?.();
34870
+ } catch (err) {
34871
+ opts.log?.("port-file-heal-error", String(err));
34872
+ }
34873
+ };
34874
+ const handle = setIntervalFn(tick, intervalMs);
34875
+ return () => {
34876
+ clearIntervalFn(handle);
34877
+ };
34878
+ }
34879
+
34734
34880
  // src/channelStdioWatcher.ts
34735
34881
  function installStdioDisconnectHandler(opts) {
34736
34882
  const { stdin, stdout, onDisconnect, log } = opts;
@@ -34774,7 +34920,7 @@ function installStdioDisconnectHandler(opts) {
34774
34920
  }
34775
34921
 
34776
34922
  // src/channel.ts
34777
- var CHANNEL_VERSION = 7;
34923
+ var CHANNEL_VERSION = 8;
34778
34924
  var dataDir = ".hotsheet";
34779
34925
  var args = process.argv.slice(2);
34780
34926
  for (let i = 0; i < args.length; i++) {
@@ -34783,9 +34929,12 @@ for (let i = 0; i < args.length; i++) {
34783
34929
  i++;
34784
34930
  }
34785
34931
  }
34786
- var portFile = join3(dataDir, "channel-port");
34932
+ var portFile = join4(dataDir, "channel-port");
34787
34933
  var serverSlug = slugifyDataDir(dataDir);
34788
34934
  var serverName = `hotsheet-channel-${serverSlug}`;
34935
+ var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
34936
+ var channelLog = createChannelLogger(join4(dataDir, "mcp.log"));
34937
+ channelLog.log("process-start", `argv=${process.argv.slice(2).join(" ")} dataDir=${dataDir} serverName=${serverName}`);
34789
34938
  var mcp = new Server(
34790
34939
  { name: serverName, version: "0.1.0" },
34791
34940
  {
@@ -34816,17 +34965,23 @@ mcp.setRequestHandler(ListToolsRequestSchema, () => {
34816
34965
  });
34817
34966
  mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
34818
34967
  const { name, arguments: args2 } = request.params;
34968
+ channelLog.log("tools/call", `name=${name}`);
34819
34969
  const result = await callTool(name, args2 ?? {}, dataDir);
34820
34970
  return result;
34821
34971
  });
34822
34972
  await mcp.connect(new StdioServerTransport());
34973
+ channelLog.log("mcp-connect", "StdioServerTransport ready");
34823
34974
  installStdioDisconnectHandler({
34824
34975
  stdin: process.stdin,
34825
34976
  stdout: process.stdout,
34826
- log: (msg) => process.stderr.write(`${serverName}: ${msg}
34827
- `),
34828
- onDisconnect: () => {
34829
- void cleanup();
34977
+ log: (msg) => {
34978
+ process.stderr.write(`${serverName}: ${msg}
34979
+ `);
34980
+ channelLog.log("stdio-watcher", msg);
34981
+ },
34982
+ onDisconnect: (reason) => {
34983
+ channelLog.log("disconnect", `reason=${reason} \u2014 invoking cleanup`);
34984
+ void cleanup(`stdio-${reason}`);
34830
34985
  }
34831
34986
  });
34832
34987
  var PermissionRequestSchema = external_exports3.object({
@@ -34866,7 +35021,13 @@ var httpServer = createServer(async (req, res) => {
34866
35021
  }
34867
35022
  if (req.method === "GET" && req.url === "/health") {
34868
35023
  res.writeHead(200, { "Content-Type": "application/json" });
34869
- res.end(JSON.stringify({ ok: true, version: CHANNEL_VERSION }));
35024
+ res.end(JSON.stringify({
35025
+ ok: true,
35026
+ version: CHANNEL_VERSION,
35027
+ pid: process.pid,
35028
+ slug: serverSlug,
35029
+ startedAt: processStartedAt
35030
+ }));
34870
35031
  return;
34871
35032
  }
34872
35033
  if (req.method === "GET" && req.url === "/permission") {
@@ -34972,6 +35133,7 @@ var httpServer = createServer(async (req, res) => {
34972
35133
  }
34973
35134
  body += String(chunk);
34974
35135
  }
35136
+ channelLog.log("trigger", `bodyBytes=${body.length}`);
34975
35137
  try {
34976
35138
  await mcp.notification({
34977
35139
  method: "notifications/claude/channel",
@@ -34983,6 +35145,7 @@ var httpServer = createServer(async (req, res) => {
34983
35145
  res.writeHead(200, { "Content-Type": "application/json" });
34984
35146
  res.end(JSON.stringify({ ok: true }));
34985
35147
  } catch (err) {
35148
+ channelLog.log("trigger-error", String(err));
34986
35149
  res.writeHead(500, { "Content-Type": "application/json" });
34987
35150
  res.end(JSON.stringify({ error: String(err) }));
34988
35151
  }
@@ -34999,8 +35162,8 @@ var httpServer = createServer(async (req, res) => {
34999
35162
  });
35000
35163
  function notifyMainServer(abortSignal) {
35001
35164
  try {
35002
- const settingsPath = join3(dataDir, "settings.json");
35003
- const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
35165
+ const settingsPath = join4(dataDir, "settings.json");
35166
+ const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
35004
35167
  if (settings.port === void 0 || settings.port === 0) {
35005
35168
  process.stderr.write(`[notify] no port in settings.json
35006
35169
  `);
@@ -35024,40 +35187,78 @@ function notifyMainServer(abortSignal) {
35024
35187
  return Promise.resolve();
35025
35188
  }
35026
35189
  }
35190
+ var myPort = null;
35191
+ var disposePortFileWatcher = null;
35027
35192
  httpServer.listen(0, "127.0.0.1", () => {
35028
35193
  const addr = httpServer.address();
35029
35194
  if (addr !== null && typeof addr !== "string") {
35030
35195
  const port = addr.port;
35196
+ myPort = port;
35197
+ const myInfo = {
35198
+ port,
35199
+ pid: process.pid,
35200
+ slug: serverSlug,
35201
+ startedAt: processStartedAt
35202
+ };
35031
35203
  try {
35032
- writeFileSync(portFile, String(port), "utf-8");
35204
+ writeChannelInfo(portFile, myInfo);
35033
35205
  } catch {
35034
35206
  }
35207
+ disposePortFileWatcher = installPortFileWatcher({
35208
+ portFile,
35209
+ info: myInfo,
35210
+ intervalMs: 5e3,
35211
+ log: (event, details) => channelLog.log(event, details),
35212
+ notify: () => {
35213
+ void notifyMainServer();
35214
+ }
35215
+ });
35035
35216
  process.stderr.write(`${serverName} listening on port ${port}
35036
35217
  `);
35218
+ channelLog.log("http-listen", `port=${port}`);
35037
35219
  void notifyMainServer();
35038
35220
  }
35039
35221
  });
35040
- async function cleanup() {
35041
- try {
35042
- unlinkSync(portFile);
35043
- } catch {
35044
- }
35222
+ var heartbeatInterval = setInterval(() => {
35223
+ const mem = process.memoryUsage();
35224
+ channelLog.log("heartbeat", `uptime=${process.uptime().toFixed(1)}s rss=${(mem.rss / 1024 / 1024).toFixed(1)}MiB`);
35225
+ }, 6e4);
35226
+ heartbeatInterval.unref();
35227
+ var cleanupInFlight = false;
35228
+ async function cleanup(reason = "unspecified") {
35229
+ if (cleanupInFlight) return;
35230
+ cleanupInFlight = true;
35231
+ channelLog.log("cleanup-start", `reason=${reason}`);
35232
+ clearInterval(heartbeatInterval);
35233
+ if (disposePortFileWatcher !== null) {
35234
+ disposePortFileWatcher();
35235
+ disposePortFileWatcher = null;
35236
+ }
35237
+ if (myPort !== null) maybeUnlinkPortFile(portFile, myPort);
35045
35238
  try {
35046
35239
  const controller = new AbortController();
35047
35240
  setTimeout(() => controller.abort(), 1e3);
35048
35241
  await notifyMainServer(controller.signal);
35049
35242
  } catch {
35050
35243
  }
35244
+ channelLog.log("cleanup-end", `reason=${reason} \u2014 exiting`);
35051
35245
  process.exit(0);
35052
35246
  }
35053
- process.on("SIGTERM", () => void cleanup());
35054
- process.on("SIGINT", () => void cleanup());
35055
- process.on("SIGHUP", () => void cleanup());
35056
- process.on("exit", () => {
35057
- try {
35058
- unlinkSync(portFile);
35059
- } catch {
35060
- }
35247
+ process.on("SIGTERM", () => {
35248
+ channelLog.log("signal", "SIGTERM");
35249
+ void cleanup("SIGTERM");
35250
+ });
35251
+ process.on("SIGINT", () => {
35252
+ channelLog.log("signal", "SIGINT");
35253
+ void cleanup("SIGINT");
35254
+ });
35255
+ process.on("SIGHUP", () => {
35256
+ channelLog.log("signal", "SIGHUP");
35257
+ void cleanup("SIGHUP");
35258
+ });
35259
+ process.on("exit", (code) => {
35260
+ channelLog.log("exit", `code=${code}`);
35261
+ if (myPort !== null) maybeUnlinkPortFile(portFile, myPort);
35061
35262
  });
35062
35263
  export {
35063
35264
  CHANNEL_VERSION