hotsheet 0.17.0-beta.23 → 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,34 +34685,16 @@ 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/claude-allow-rule.ts
34692
- init_zod();
34693
- init_file_settings();
34694
- var ClaudeSettingsSchema = external_exports3.object({
34695
- permissions: external_exports3.object({
34696
- allow: external_exports3.array(external_exports3.string()).default([])
34697
- }).loose().default(() => ({ allow: [] }))
34698
- }).loose();
34699
-
34700
- // src/channel-config.ts
34701
- var McpConfigSchema = external_exports3.object({
34702
- mcpServers: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional()
34703
- }).loose();
34704
- function slugifyDataDir(dataDir2) {
34705
- const root = dataDir2.replace(/[\\/]\.hotsheet[\\/]?$/, "");
34706
- const base = basename2(root) || "project";
34707
- const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
34708
- return slug !== "" ? slug : "project";
34709
- }
34710
-
34711
34691
  // src/channelLog.ts
34712
34692
  import { appendFileSync, renameSync, statSync } from "fs";
34693
+ import { join as join2 } from "path";
34713
34694
  var CHANNEL_LOG_MAX_BYTES = 1048576;
34714
- function createChannelLogger(logPath) {
34695
+ function createChannelLogger(logPath, options = {}) {
34715
34696
  let injectedBlankLine = false;
34697
+ const pidSuffix = options.pidLabel !== void 0 && options.pidLabel !== "" ? ` ${options.pidLabel}` : "";
34716
34698
  return {
34717
34699
  log(event, details) {
34718
34700
  try {
@@ -34727,7 +34709,7 @@ function createChannelLogger(logPath) {
34727
34709
  const detailsText = details === void 0 || details === "" ? "" : ` ${details}`;
34728
34710
  const prefix = !injectedBlankLine && size > 0 ? "\n" : "";
34729
34711
  injectedBlankLine = true;
34730
- appendFileSync(logPath, `${prefix}[${ts}] [pid ${process.pid}] ${event}:${detailsText}
34712
+ appendFileSync(logPath, `${prefix}[${ts}] [pid ${process.pid}${pidSuffix}] ${event}:${detailsText}
34731
34713
  `, "utf-8");
34732
34714
  } catch {
34733
34715
  }
@@ -34742,6 +34724,94 @@ function safeStatSize(path) {
34742
34724
  }
34743
34725
  }
34744
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
+
34795
+ // src/claude-allow-rule.ts
34796
+ init_zod();
34797
+ init_file_settings();
34798
+ var ClaudeSettingsSchema = external_exports3.object({
34799
+ permissions: external_exports3.object({
34800
+ allow: external_exports3.array(external_exports3.string()).default([])
34801
+ }).loose().default(() => ({ allow: [] }))
34802
+ }).loose();
34803
+
34804
+ // src/channel-config.ts
34805
+ var McpConfigSchema = external_exports3.object({
34806
+ mcpServers: external_exports3.record(external_exports3.string(), external_exports3.unknown()).optional()
34807
+ }).loose();
34808
+ function slugifyDataDir(dataDir2) {
34809
+ const root = dataDir2.replace(/[\\/]\.hotsheet[\\/]?$/, "");
34810
+ const base = basename2(root) || "project";
34811
+ const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
34812
+ return slug !== "" ? slug : "project";
34813
+ }
34814
+
34745
34815
  // src/channelPermissions.ts
34746
34816
  var PERMISSION_TTL_MS = 12e4;
34747
34817
  var queue = [];
@@ -34765,6 +34835,48 @@ function clearAllPermissions() {
34765
34835
  queue.length = 0;
34766
34836
  }
34767
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
+
34768
34880
  // src/channelStdioWatcher.ts
34769
34881
  function installStdioDisconnectHandler(opts) {
34770
34882
  const { stdin, stdout, onDisconnect, log } = opts;
@@ -34808,7 +34920,7 @@ function installStdioDisconnectHandler(opts) {
34808
34920
  }
34809
34921
 
34810
34922
  // src/channel.ts
34811
- var CHANNEL_VERSION = 7;
34923
+ var CHANNEL_VERSION = 8;
34812
34924
  var dataDir = ".hotsheet";
34813
34925
  var args = process.argv.slice(2);
34814
34926
  for (let i = 0; i < args.length; i++) {
@@ -34817,10 +34929,11 @@ for (let i = 0; i < args.length; i++) {
34817
34929
  i++;
34818
34930
  }
34819
34931
  }
34820
- var portFile = join3(dataDir, "channel-port");
34932
+ var portFile = join4(dataDir, "channel-port");
34821
34933
  var serverSlug = slugifyDataDir(dataDir);
34822
34934
  var serverName = `hotsheet-channel-${serverSlug}`;
34823
- var channelLog = createChannelLogger(join3(dataDir, "mcp.log"));
34935
+ var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
34936
+ var channelLog = createChannelLogger(join4(dataDir, "mcp.log"));
34824
34937
  channelLog.log("process-start", `argv=${process.argv.slice(2).join(" ")} dataDir=${dataDir} serverName=${serverName}`);
34825
34938
  var mcp = new Server(
34826
34939
  { name: serverName, version: "0.1.0" },
@@ -34908,7 +35021,13 @@ var httpServer = createServer(async (req, res) => {
34908
35021
  }
34909
35022
  if (req.method === "GET" && req.url === "/health") {
34910
35023
  res.writeHead(200, { "Content-Type": "application/json" });
34911
- 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
+ }));
34912
35031
  return;
34913
35032
  }
34914
35033
  if (req.method === "GET" && req.url === "/permission") {
@@ -35043,8 +35162,8 @@ var httpServer = createServer(async (req, res) => {
35043
35162
  });
35044
35163
  function notifyMainServer(abortSignal) {
35045
35164
  try {
35046
- const settingsPath = join3(dataDir, "settings.json");
35047
- const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
35165
+ const settingsPath = join4(dataDir, "settings.json");
35166
+ const settings = JSON.parse(readFileSync3(settingsPath, "utf-8"));
35048
35167
  if (settings.port === void 0 || settings.port === 0) {
35049
35168
  process.stderr.write(`[notify] no port in settings.json
35050
35169
  `);
@@ -35068,14 +35187,32 @@ function notifyMainServer(abortSignal) {
35068
35187
  return Promise.resolve();
35069
35188
  }
35070
35189
  }
35190
+ var myPort = null;
35191
+ var disposePortFileWatcher = null;
35071
35192
  httpServer.listen(0, "127.0.0.1", () => {
35072
35193
  const addr = httpServer.address();
35073
35194
  if (addr !== null && typeof addr !== "string") {
35074
35195
  const port = addr.port;
35196
+ myPort = port;
35197
+ const myInfo = {
35198
+ port,
35199
+ pid: process.pid,
35200
+ slug: serverSlug,
35201
+ startedAt: processStartedAt
35202
+ };
35075
35203
  try {
35076
- writeFileSync(portFile, String(port), "utf-8");
35204
+ writeChannelInfo(portFile, myInfo);
35077
35205
  } catch {
35078
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
+ });
35079
35216
  process.stderr.write(`${serverName} listening on port ${port}
35080
35217
  `);
35081
35218
  channelLog.log("http-listen", `port=${port}`);
@@ -35087,13 +35224,17 @@ var heartbeatInterval = setInterval(() => {
35087
35224
  channelLog.log("heartbeat", `uptime=${process.uptime().toFixed(1)}s rss=${(mem.rss / 1024 / 1024).toFixed(1)}MiB`);
35088
35225
  }, 6e4);
35089
35226
  heartbeatInterval.unref();
35227
+ var cleanupInFlight = false;
35090
35228
  async function cleanup(reason = "unspecified") {
35229
+ if (cleanupInFlight) return;
35230
+ cleanupInFlight = true;
35091
35231
  channelLog.log("cleanup-start", `reason=${reason}`);
35092
35232
  clearInterval(heartbeatInterval);
35093
- try {
35094
- unlinkSync(portFile);
35095
- } catch {
35233
+ if (disposePortFileWatcher !== null) {
35234
+ disposePortFileWatcher();
35235
+ disposePortFileWatcher = null;
35096
35236
  }
35237
+ if (myPort !== null) maybeUnlinkPortFile(portFile, myPort);
35097
35238
  try {
35098
35239
  const controller = new AbortController();
35099
35240
  setTimeout(() => controller.abort(), 1e3);
@@ -35117,10 +35258,7 @@ process.on("SIGHUP", () => {
35117
35258
  });
35118
35259
  process.on("exit", (code) => {
35119
35260
  channelLog.log("exit", `code=${code}`);
35120
- try {
35121
- unlinkSync(portFile);
35122
- } catch {
35123
- }
35261
+ if (myPort !== null) maybeUnlinkPortFile(portFile, myPort);
35124
35262
  });
35125
35263
  export {
35126
35264
  CHANNEL_VERSION