hotsheet 0.17.0-beta.23 → 0.17.0-beta.25

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 readFileSync4 } from "fs";
34149
34149
  import { createServer } from "http";
34150
- import { join as join3 } from "path";
34150
+ import { join as join5 } 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,161 @@ function clearAllPermissions() {
34765
34835
  queue.length = 0;
34766
34836
  }
34767
34837
 
34838
+ // src/channelRegistry.ts
34839
+ import { existsSync, mkdirSync, readdirSync, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
34840
+ import { join as join4 } from "path";
34841
+ function registryDir(dataDir2) {
34842
+ return join4(dataDir2, "channel-ports.d");
34843
+ }
34844
+ function entryPath(dataDir2, pid) {
34845
+ return join4(registryDir(dataDir2), `${String(pid)}.json`);
34846
+ }
34847
+ function defaultIsPidAlive(pid) {
34848
+ try {
34849
+ process.kill(pid, 0);
34850
+ return true;
34851
+ } catch {
34852
+ return false;
34853
+ }
34854
+ }
34855
+ function registerSelf(dataDir2, info) {
34856
+ if (info.pid === null) return;
34857
+ mkdirSync(registryDir(dataDir2), { recursive: true });
34858
+ const path = entryPath(dataDir2, info.pid);
34859
+ const body = JSON.stringify({
34860
+ port: info.port,
34861
+ pid: info.pid,
34862
+ slug: info.slug,
34863
+ startedAt: info.startedAt
34864
+ });
34865
+ writeFileSync2(path, body, "utf-8");
34866
+ }
34867
+ function unregisterSelf(dataDir2, pid) {
34868
+ try {
34869
+ unlinkSync2(entryPath(dataDir2, pid));
34870
+ } catch {
34871
+ }
34872
+ }
34873
+ function readEntry(path) {
34874
+ let raw;
34875
+ try {
34876
+ raw = readFileSync3(path, "utf-8");
34877
+ } catch {
34878
+ return null;
34879
+ }
34880
+ try {
34881
+ const parsed = JSON.parse(raw);
34882
+ if (typeof parsed !== "object" || parsed === null) return null;
34883
+ const obj = parsed;
34884
+ if (typeof obj.port !== "number" || !Number.isInteger(obj.port)) return null;
34885
+ if (typeof obj.pid !== "number" || !Number.isInteger(obj.pid)) return null;
34886
+ return {
34887
+ port: obj.port,
34888
+ pid: obj.pid,
34889
+ slug: typeof obj.slug === "string" && obj.slug !== "" ? obj.slug : null,
34890
+ startedAt: typeof obj.startedAt === "string" && obj.startedAt !== "" ? obj.startedAt : null
34891
+ };
34892
+ } catch {
34893
+ return null;
34894
+ }
34895
+ }
34896
+ function listAliveEntries(dataDir2, isPidAlive = defaultIsPidAlive) {
34897
+ const dir = registryDir(dataDir2);
34898
+ if (!existsSync(dir)) return [];
34899
+ let names;
34900
+ try {
34901
+ names = readdirSync(dir);
34902
+ } catch {
34903
+ return [];
34904
+ }
34905
+ const alive = [];
34906
+ for (const name of names) {
34907
+ if (!name.endsWith(".json")) continue;
34908
+ const path = join4(dir, name);
34909
+ const entry = readEntry(path);
34910
+ if (entry === null || entry.pid === null || !isPidAlive(entry.pid)) {
34911
+ try {
34912
+ unlinkSync2(path);
34913
+ } catch {
34914
+ }
34915
+ continue;
34916
+ }
34917
+ alive.push(entry);
34918
+ }
34919
+ alive.sort((a, b) => {
34920
+ if (a.startedAt === null && b.startedAt === null) return a.pid - b.pid;
34921
+ if (a.startedAt === null) return 1;
34922
+ if (b.startedAt === null) return -1;
34923
+ return a.startedAt < b.startedAt ? -1 : a.startedAt > b.startedAt ? 1 : 0;
34924
+ });
34925
+ return alive;
34926
+ }
34927
+ function pickLeader(entries) {
34928
+ return entries.length === 0 ? null : entries[0];
34929
+ }
34930
+
34931
+ // src/channelPortFileWatcher.ts
34932
+ function installPortFileWatcher(opts) {
34933
+ const intervalMs = opts.intervalMs ?? 5e3;
34934
+ const setIntervalFn = opts.setIntervalFn ?? setInterval;
34935
+ const clearIntervalFn = opts.clearIntervalFn ?? ((handle2) => {
34936
+ clearInterval(handle2);
34937
+ });
34938
+ const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
34939
+ let lastRole = "unknown";
34940
+ let lastLeaderPid = null;
34941
+ const tick = () => {
34942
+ if (opts.info.pid !== null) {
34943
+ const allEntries = listAliveEntries(opts.dataDir, isPidAlive);
34944
+ const ourEntry = allEntries.find((e) => e.pid === opts.info.pid);
34945
+ if (ourEntry === void 0) {
34946
+ try {
34947
+ registerSelf(opts.dataDir, opts.info);
34948
+ opts.log?.("port-file-registry-heal", `pid=${String(opts.info.pid)}`);
34949
+ } catch (err) {
34950
+ opts.log?.("port-file-heal-error", `registerSelf: ${String(err)}`);
34951
+ return;
34952
+ }
34953
+ }
34954
+ }
34955
+ const alive = listAliveEntries(opts.dataDir, isPidAlive);
34956
+ const leader = pickLeader(alive);
34957
+ if (leader === null) {
34958
+ return;
34959
+ }
34960
+ const weAreLeader = leader.pid === opts.info.pid;
34961
+ if (weAreLeader) {
34962
+ const current = readChannelInfo(opts.portFile);
34963
+ const matches = current !== null && current.pid === opts.info.pid && current.port === opts.info.port && current.slug === opts.info.slug;
34964
+ if (!matches) {
34965
+ try {
34966
+ writeChannelInfo(opts.portFile, opts.info);
34967
+ const reason = current === null ? "missing" : `previous-leader-pid=${String(current.pid)}`;
34968
+ opts.log?.("port-file-leader-write", reason);
34969
+ opts.notify?.();
34970
+ } catch (err) {
34971
+ opts.log?.("port-file-heal-error", `writeChannelInfo: ${String(err)}`);
34972
+ return;
34973
+ }
34974
+ }
34975
+ if (lastRole !== "leader") {
34976
+ lastRole = "leader";
34977
+ lastLeaderPid = opts.info.pid;
34978
+ }
34979
+ } else {
34980
+ if (lastRole !== "follower" || lastLeaderPid !== leader.pid) {
34981
+ opts.log?.("port-file-follower-defer", `leader-pid=${String(leader.pid)} ours=${String(opts.info.pid)}`);
34982
+ lastRole = "follower";
34983
+ lastLeaderPid = leader.pid;
34984
+ }
34985
+ }
34986
+ };
34987
+ const handle = setIntervalFn(tick, intervalMs);
34988
+ return () => {
34989
+ clearIntervalFn(handle);
34990
+ };
34991
+ }
34992
+
34768
34993
  // src/channelStdioWatcher.ts
34769
34994
  function installStdioDisconnectHandler(opts) {
34770
34995
  const { stdin, stdout, onDisconnect, log } = opts;
@@ -34808,7 +35033,7 @@ function installStdioDisconnectHandler(opts) {
34808
35033
  }
34809
35034
 
34810
35035
  // src/channel.ts
34811
- var CHANNEL_VERSION = 7;
35036
+ var CHANNEL_VERSION = 9;
34812
35037
  var dataDir = ".hotsheet";
34813
35038
  var args = process.argv.slice(2);
34814
35039
  for (let i = 0; i < args.length; i++) {
@@ -34817,10 +35042,11 @@ for (let i = 0; i < args.length; i++) {
34817
35042
  i++;
34818
35043
  }
34819
35044
  }
34820
- var portFile = join3(dataDir, "channel-port");
35045
+ var portFile = join5(dataDir, "channel-port");
34821
35046
  var serverSlug = slugifyDataDir(dataDir);
34822
35047
  var serverName = `hotsheet-channel-${serverSlug}`;
34823
- var channelLog = createChannelLogger(join3(dataDir, "mcp.log"));
35048
+ var processStartedAt = (/* @__PURE__ */ new Date()).toISOString();
35049
+ var channelLog = createChannelLogger(join5(dataDir, "mcp.log"));
34824
35050
  channelLog.log("process-start", `argv=${process.argv.slice(2).join(" ")} dataDir=${dataDir} serverName=${serverName}`);
34825
35051
  var mcp = new Server(
34826
35052
  { name: serverName, version: "0.1.0" },
@@ -34908,7 +35134,13 @@ var httpServer = createServer(async (req, res) => {
34908
35134
  }
34909
35135
  if (req.method === "GET" && req.url === "/health") {
34910
35136
  res.writeHead(200, { "Content-Type": "application/json" });
34911
- res.end(JSON.stringify({ ok: true, version: CHANNEL_VERSION }));
35137
+ res.end(JSON.stringify({
35138
+ ok: true,
35139
+ version: CHANNEL_VERSION,
35140
+ pid: process.pid,
35141
+ slug: serverSlug,
35142
+ startedAt: processStartedAt
35143
+ }));
34912
35144
  return;
34913
35145
  }
34914
35146
  if (req.method === "GET" && req.url === "/permission") {
@@ -35043,8 +35275,8 @@ var httpServer = createServer(async (req, res) => {
35043
35275
  });
35044
35276
  function notifyMainServer(abortSignal) {
35045
35277
  try {
35046
- const settingsPath = join3(dataDir, "settings.json");
35047
- const settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
35278
+ const settingsPath = join5(dataDir, "settings.json");
35279
+ const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
35048
35280
  if (settings.port === void 0 || settings.port === 0) {
35049
35281
  process.stderr.write(`[notify] no port in settings.json
35050
35282
  `);
@@ -35068,14 +35300,44 @@ function notifyMainServer(abortSignal) {
35068
35300
  return Promise.resolve();
35069
35301
  }
35070
35302
  }
35303
+ var myPort = null;
35304
+ var disposePortFileWatcher = null;
35071
35305
  httpServer.listen(0, "127.0.0.1", () => {
35072
35306
  const addr = httpServer.address();
35073
35307
  if (addr !== null && typeof addr !== "string") {
35074
35308
  const port = addr.port;
35309
+ myPort = port;
35310
+ const myInfo = {
35311
+ port,
35312
+ pid: process.pid,
35313
+ slug: serverSlug,
35314
+ startedAt: processStartedAt
35315
+ };
35316
+ try {
35317
+ registerSelf(dataDir, myInfo);
35318
+ } catch (err) {
35319
+ channelLog.log("registry-register-error", String(err));
35320
+ }
35075
35321
  try {
35076
- writeFileSync(portFile, String(port), "utf-8");
35322
+ const alive = listAliveEntries(dataDir);
35323
+ const leader = pickLeader(alive);
35324
+ if (leader === null || leader.pid === process.pid) {
35325
+ writeChannelInfo(portFile, myInfo);
35326
+ } else {
35327
+ channelLog.log("startup-follower", `leader-pid=${String(leader.pid)} aliveCount=${String(alive.length)}`);
35328
+ }
35077
35329
  } catch {
35078
35330
  }
35331
+ disposePortFileWatcher = installPortFileWatcher({
35332
+ portFile,
35333
+ dataDir,
35334
+ info: myInfo,
35335
+ intervalMs: 5e3,
35336
+ log: (event, details) => channelLog.log(event, details),
35337
+ notify: () => {
35338
+ void notifyMainServer();
35339
+ }
35340
+ });
35079
35341
  process.stderr.write(`${serverName} listening on port ${port}
35080
35342
  `);
35081
35343
  channelLog.log("http-listen", `port=${port}`);
@@ -35087,13 +35349,18 @@ var heartbeatInterval = setInterval(() => {
35087
35349
  channelLog.log("heartbeat", `uptime=${process.uptime().toFixed(1)}s rss=${(mem.rss / 1024 / 1024).toFixed(1)}MiB`);
35088
35350
  }, 6e4);
35089
35351
  heartbeatInterval.unref();
35352
+ var cleanupInFlight = false;
35090
35353
  async function cleanup(reason = "unspecified") {
35354
+ if (cleanupInFlight) return;
35355
+ cleanupInFlight = true;
35091
35356
  channelLog.log("cleanup-start", `reason=${reason}`);
35092
35357
  clearInterval(heartbeatInterval);
35093
- try {
35094
- unlinkSync(portFile);
35095
- } catch {
35358
+ if (disposePortFileWatcher !== null) {
35359
+ disposePortFileWatcher();
35360
+ disposePortFileWatcher = null;
35096
35361
  }
35362
+ if (myPort !== null) maybeUnlinkPortFile(portFile, myPort);
35363
+ unregisterSelf(dataDir, process.pid);
35097
35364
  try {
35098
35365
  const controller = new AbortController();
35099
35366
  setTimeout(() => controller.abort(), 1e3);
@@ -35117,10 +35384,7 @@ process.on("SIGHUP", () => {
35117
35384
  });
35118
35385
  process.on("exit", (code) => {
35119
35386
  channelLog.log("exit", `code=${code}`);
35120
- try {
35121
- unlinkSync(portFile);
35122
- } catch {
35123
- }
35387
+ if (myPort !== null) maybeUnlinkPortFile(portFile, myPort);
35124
35388
  });
35125
35389
  export {
35126
35390
  CHANNEL_VERSION