@pipemd-core/pipemd 1.0.0 → 1.1.0

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command14 } from "commander";
5
- import chalk18 from "chalk";
4
+ import { Command as Command15 } from "commander";
5
+ import chalk19 from "chalk";
6
6
 
7
7
  // src/commands/init.ts
8
8
  import { Command } from "commander";
@@ -1257,6 +1257,7 @@ function storePayload(trigger, payload) {
1257
1257
  try {
1258
1258
  const tool = (input && input.tool) || "";
1259
1259
  const args = (output && output.args) || {};
1260
+ if (tool === "read") resolveFifoRead(args);
1260
1261
  const trigger = isEditTool(tool) ? "before-edit" : "before-read";
1261
1262
  const filePath = extractFilePath(args);
1262
1263
  join(); heartbeat();
@@ -1279,12 +1280,14 @@ function storePayload(trigger, payload) {
1279
1280
  try {
1280
1281
  const tool = (input && input.tool) || "";
1281
1282
  const args = (output && output.args) || {};
1283
+ if (tool === "read") resolveFifoRead(args);
1282
1284
  join(); heartbeat();
1283
1285
  pushEvent("before", tool, extractFilePath(args), "ok", 0);
1284
1286
  } catch (e) { logPluginError("tool.execute.before", e); }
1285
1287
  },`;
1286
1288
  const afterHandler = withInjection ? `"tool.execute.after": async (input, output) => {
1287
1289
  try {
1290
+ cleanupFifoTemp();
1288
1291
  const tool = (input && input.tool) || "";
1289
1292
  const isEdit = isEditTool(tool);
1290
1293
  if (isEdit) {
@@ -1308,6 +1311,7 @@ function storePayload(trigger, payload) {
1308
1311
  } catch (e) { logPluginError("tool.execute.after", e); }
1309
1312
  },` : `"tool.execute.after": async (input, output) => {
1310
1313
  try {
1314
+ cleanupFifoTemp();
1311
1315
  const tool = (input && input.tool) || "";
1312
1316
  if (!isEditTool(tool)) return;
1313
1317
  const args = (output && output.args) || (input && input.args) || {};
@@ -1705,7 +1709,9 @@ function generateInjectionYml(config) {
1705
1709
 
1706
1710
  // src/core/paths.ts
1707
1711
  import path10 from "path";
1712
+ import os3 from "os";
1708
1713
  var PIPEMD_DIR = ".pipemd";
1714
+ var HOME_LINK_DIR = path10.join(os3.homedir(), ".pipemd", "link");
1709
1715
  var LIVE_DIR = path10.join(PIPEMD_DIR, "live");
1710
1716
  var PID_FILE = path10.join(PIPEMD_DIR, ".daemon.pid");
1711
1717
  var STATUS_FILE = path10.join(PIPEMD_DIR, ".status.json");
@@ -1828,7 +1834,6 @@ function generateConfigYml(config) {
1828
1834
  return lines.join("\n");
1829
1835
  }
1830
1836
  function generateTemplate(agent, selectedScripts) {
1831
- const agentLabel = agent === "Generic" ? "AI assistant" : agent;
1832
1837
  const sorted = [...selectedScripts].sort((a, b) => a.volatile - b.volatile);
1833
1838
  const stableScripts = sorted.filter((s) => s.volatile <= 2);
1834
1839
  const volatileScripts = sorted.filter((s) => s.volatile > 2);
@@ -1838,7 +1843,7 @@ function generateTemplate(agent, selectedScripts) {
1838
1843
  \`\`\`
1839
1844
  <!-- /pmd -->`;
1840
1845
  const sections = [];
1841
- sections.push(`# \u{1F3F4}\u200D\u2620\uFE0F ${agentLabel} Context \u2014 powered by PipeMD
1846
+ sections.push(`# \u{1F3F4}\u200D\u2620\uFE0F Context \u2014 powered by PipeMD
1842
1847
 
1843
1848
  > **\u{1F916} PipeMD Context File**
1844
1849
  >
@@ -1946,6 +1951,7 @@ function scaffoldProject(ecosystem, selectedIds, profile) {
1946
1951
  mkdirp(path11.join(SCRIPTS_DIR, "api"));
1947
1952
  mkdirp(path11.join(SCRIPTS_DIR, "frontend"));
1948
1953
  mkdirp(path11.join(SCRIPTS_DIR, "devops"));
1954
+ mkdirp(path11.join(SCRIPTS_DIR, "crew"));
1949
1955
  const ecoEnv = `PMD_ECOSYSTEM=${ecosystem.replace(/\//g, "-")}`;
1950
1956
  const profileEnv = `PMD_TOKEN_PROFILE=${profile}`;
1951
1957
  const commands = {};
@@ -3170,7 +3176,7 @@ function listSessions() {
3170
3176
  try {
3171
3177
  files = fs10.readdirSync(CREW_DIR2);
3172
3178
  } catch {
3173
- return [];
3179
+ files = [];
3174
3180
  }
3175
3181
  const out = [];
3176
3182
  for (const f of files) {
@@ -3181,6 +3187,7 @@ function listSessions() {
3181
3187
  } catch {
3182
3188
  }
3183
3189
  }
3190
+ out.push(...remoteSessionsCache);
3184
3191
  return out;
3185
3192
  }
3186
3193
  function deleteSession(id) {
@@ -3189,6 +3196,7 @@ function deleteSession(id) {
3189
3196
  } catch {
3190
3197
  }
3191
3198
  }
3199
+ var remoteSessionsCache = [];
3192
3200
  function isPidAlive(pid) {
3193
3201
  if (!pid || pid <= 0) return false;
3194
3202
  try {
@@ -3240,8 +3248,8 @@ function resolveProcessCwd(pid) {
3240
3248
  if (isWindows()) return void 0;
3241
3249
  try {
3242
3250
  const target = `/proc/${pid}/cwd`;
3243
- const link = fs10.readlinkSync(target);
3244
- return typeof link === "string" ? link : void 0;
3251
+ const link2 = fs10.readlinkSync(target);
3252
+ return typeof link2 === "string" ? link2 : void 0;
3245
3253
  } catch {
3246
3254
  return void 0;
3247
3255
  }
@@ -3507,8 +3515,9 @@ function renderCrewBlock(opts = {}) {
3507
3515
  lines.push("_No active PipeMD crew sessions._");
3508
3516
  }
3509
3517
  for (const coord of coordinators) {
3518
+ const remoteTag = coord._remote && coord._origin ? ` \xB7 remote: ${coord._origin}` : "";
3510
3519
  lines.push("");
3511
- lines.push(`\u25B8 ${coord.harness} (coordinator ${coord.id} \xB7 pid ${coord.pid})`);
3520
+ lines.push(`\u25B8 ${coord.harness} (coordinator ${coord.id} \xB7 pid ${coord.pid}${remoteTag})`);
3512
3521
  if (coord.note) lines.push(` \xB7 note: ${coord.note}`);
3513
3522
  const cc = claimList(coord);
3514
3523
  if (cc) lines.push(` \xB7 claimed: ${cc}`);
@@ -3519,7 +3528,8 @@ function renderCrewBlock(opts = {}) {
3519
3528
  const claimStr = claimed ? `claimed: ${claimed}` : "no claim";
3520
3529
  const noteStr = w.note ? ` "${w.note}"` : "";
3521
3530
  const flag = w.claimedFiles.some((c) => conflictPaths.has(c.path)) ? " \u26A0\uFE0F" : "";
3522
- lines.push(` ${branch} ${w.label || w.id} ${claimStr}${noteStr}${flag}`);
3531
+ const rmt = w._remote && w._origin ? ` \xB7 remote: ${w._origin}` : "";
3532
+ lines.push(` ${branch} ${w.label || w.id} ${claimStr}${noteStr}${flag}${rmt}`);
3523
3533
  });
3524
3534
  }
3525
3535
  const unattached = workers.filter(
@@ -3702,12 +3712,12 @@ function ensureCacheDir() {
3702
3712
  }
3703
3713
  }
3704
3714
  function readCache(key) {
3705
- const path25 = entryPath(key);
3706
- if (!existsSync(path25)) {
3715
+ const path27 = entryPath(key);
3716
+ if (!existsSync(path27)) {
3707
3717
  return null;
3708
3718
  }
3709
3719
  try {
3710
- const raw = readFileSync(path25, "utf-8");
3720
+ const raw = readFileSync(path27, "utf-8");
3711
3721
  const entry = JSON.parse(raw);
3712
3722
  if (Date.now() - entry.timestamp > entry.ttl) {
3713
3723
  return null;
@@ -3728,17 +3738,17 @@ function writeCache(key, data, ttl, metadata) {
3728
3738
  ttl,
3729
3739
  ...metadata ? { metadata } : {}
3730
3740
  };
3731
- const path25 = entryPath(key);
3732
- atomicWrite(path25, JSON.stringify(entry));
3741
+ const path27 = entryPath(key);
3742
+ atomicWrite(path27, JSON.stringify(entry));
3733
3743
  return entry;
3734
3744
  }
3735
3745
  function isFresh(key) {
3736
3746
  return readCache(key) !== null;
3737
3747
  }
3738
3748
  function invalidate(key) {
3739
- const path25 = entryPath(key);
3740
- if (existsSync(path25)) {
3741
- unlinkSync(path25);
3749
+ const path27 = entryPath(key);
3750
+ if (existsSync(path27)) {
3751
+ unlinkSync(path27);
3742
3752
  }
3743
3753
  }
3744
3754
 
@@ -3761,18 +3771,18 @@ function loadSession(sessionId) {
3761
3771
  return {};
3762
3772
  }
3763
3773
  }
3764
- function saveSession(sessionId, store) {
3774
+ function saveSession(sessionId, store2) {
3765
3775
  ensureInjectedDir();
3766
- atomicWrite(sessionPath(sessionId), JSON.stringify(store));
3776
+ atomicWrite(sessionPath(sessionId), JSON.stringify(store2));
3767
3777
  }
3768
3778
  function recordInjection(sessionId, source, content) {
3769
- const store = loadSession(sessionId);
3770
- store[source] = { hash: computePayloadHash(content), timestamp: Date.now() };
3771
- saveSession(sessionId, store);
3779
+ const store2 = loadSession(sessionId);
3780
+ store2[source] = { hash: computePayloadHash(content), timestamp: Date.now() };
3781
+ saveSession(sessionId, store2);
3772
3782
  }
3773
3783
  function checkInjectionStatus(sessionId, source, content) {
3774
- const store = loadSession(sessionId);
3775
- const entry = store[source];
3784
+ const store2 = loadSession(sessionId);
3785
+ const entry = store2[source];
3776
3786
  if (!entry) return "new";
3777
3787
  const hash = computePayloadHash(content);
3778
3788
  return hash === entry.hash ? "unchanged" : "changed";
@@ -3836,6 +3846,115 @@ function tailLog(lines = 20) {
3836
3846
  }
3837
3847
  }
3838
3848
 
3849
+ // src/core/net/daemon-client.ts
3850
+ import http from "http";
3851
+ import os4 from "os";
3852
+
3853
+ // src/core/net/protocol.ts
3854
+ var DEFAULT_PORT = 9741;
3855
+ var POLL_INTERVAL_MS = 5e3;
3856
+ var SESSION_EXPIRY_MS = 15e3;
3857
+
3858
+ // src/core/net/daemon-client.ts
3859
+ var remoteCache = [];
3860
+ var pollTimer = null;
3861
+ function setRemoteSessions(sessions) {
3862
+ remoteCache = sessions;
3863
+ }
3864
+ function clearRemoteSessions() {
3865
+ remoteCache = [];
3866
+ }
3867
+ function relayUrl() {
3868
+ return process.env.PMD_RELAY || null;
3869
+ }
3870
+ function postToRelay(url, body) {
3871
+ return new Promise((resolve2, reject) => {
3872
+ const data = JSON.stringify(body);
3873
+ const req = http.request(
3874
+ {
3875
+ hostname: url.hostname,
3876
+ port: url.port || 9741,
3877
+ path: url.pathname || "/crew",
3878
+ method: "POST",
3879
+ timeout: 5e3,
3880
+ headers: {
3881
+ "Content-Type": "application/json",
3882
+ "Content-Length": Buffer.byteLength(data)
3883
+ }
3884
+ },
3885
+ (res) => {
3886
+ const chunks = [];
3887
+ res.on("data", (c) => chunks.push(c));
3888
+ res.on("end", () => {
3889
+ if (res.statusCode !== 200) {
3890
+ reject(new Error(`relay responded ${res.statusCode}`));
3891
+ return;
3892
+ }
3893
+ try {
3894
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
3895
+ const sessions = (parsed.sessions || []).map((s) => ({
3896
+ ...s,
3897
+ _remote: true,
3898
+ _origin: s._origin || "remote"
3899
+ }));
3900
+ resolve2({ sessions });
3901
+ } catch (e) {
3902
+ reject(e);
3903
+ }
3904
+ });
3905
+ }
3906
+ );
3907
+ req.on("error", reject);
3908
+ req.on("timeout", () => {
3909
+ req.destroy();
3910
+ reject(new Error("relay timeout"));
3911
+ });
3912
+ req.write(data);
3913
+ req.end();
3914
+ });
3915
+ }
3916
+ async function syncWithRelay(group, sessions) {
3917
+ const urlStr = relayUrl();
3918
+ if (!urlStr) return [];
3919
+ try {
3920
+ const url = new URL(urlStr);
3921
+ const msg = {
3922
+ group,
3923
+ hostname: os4.hostname(),
3924
+ sessions
3925
+ };
3926
+ const result = await postToRelay(url, msg);
3927
+ return result.sessions;
3928
+ } catch (e) {
3929
+ const msg = e instanceof Error ? e.message : String(e);
3930
+ if (!msg.includes("ECONNREFUSED") && !msg.includes("timeout")) {
3931
+ log.warn(`Relay client: ${msg}`);
3932
+ }
3933
+ return [];
3934
+ }
3935
+ }
3936
+ function startRelayClient(group, getLocalSessions) {
3937
+ if (pollTimer) return;
3938
+ if (!relayUrl()) return;
3939
+ const poll = async () => {
3940
+ try {
3941
+ const local = getLocalSessions();
3942
+ const remote = await syncWithRelay(group, local);
3943
+ setRemoteSessions(remote);
3944
+ } catch {
3945
+ }
3946
+ };
3947
+ poll();
3948
+ pollTimer = setInterval(poll, POLL_INTERVAL_MS);
3949
+ }
3950
+ function stopRelayClient() {
3951
+ if (pollTimer) {
3952
+ clearInterval(pollTimer);
3953
+ pollTimer = null;
3954
+ }
3955
+ clearRemoteSessions();
3956
+ }
3957
+
3839
3958
  // src/core/daemon.ts
3840
3959
  var WRITE_BUFFER_DEBOUNCE_MS = 1e3;
3841
3960
  var INJECTION_LOG_MAX_AGE_MS = 36e5;
@@ -4474,10 +4593,25 @@ function runDaemon() {
4474
4593
  } else if (legacyModePipes.length === 0) {
4475
4594
  startLegacyWatcher(config);
4476
4595
  }
4477
- process.on("SIGTERM", () => shutdown(allPipePaths));
4478
- process.on("SIGINT", () => shutdown(allPipePaths));
4596
+ process.on("SIGTERM", () => {
4597
+ stopRelayClient();
4598
+ shutdown(allPipePaths);
4599
+ });
4600
+ process.on("SIGINT", () => {
4601
+ stopRelayClient();
4602
+ shutdown(allPipePaths);
4603
+ });
4479
4604
  process.on("SIGHUP", () => {
4480
4605
  });
4606
+ const relayUrl2 = config.link?.relay || process.env.PMD_RELAY;
4607
+ if (relayUrl2) {
4608
+ const groupName = config.link?.group || path15.basename(process.cwd());
4609
+ startRelayClient(groupName, () => {
4610
+ const all = listSessions();
4611
+ return all.filter((s) => !s._remote);
4612
+ });
4613
+ log.info(`Relay client started: ${relayUrl2} (group: ${groupName})`);
4614
+ }
4481
4615
  process.on("uncaughtException", (err) => {
4482
4616
  log.error(`Uncaught exception: ${err.message}`);
4483
4617
  shutdown(allPipePaths);
@@ -6551,8 +6685,561 @@ var trace = new Command13("trace").description("Live resolution tree \u2014 debu
6551
6685
  });
6552
6686
  var traceCommand = trace;
6553
6687
 
6688
+ // src/commands/link.ts
6689
+ import { Command as Command14 } from "commander";
6690
+ import chalk18 from "chalk";
6691
+ import http2 from "http";
6692
+ import crypto3 from "crypto";
6693
+ import fs25 from "fs";
6694
+ import path25 from "path";
6695
+ import os5 from "os";
6696
+ import { spawn as spawn3, execSync as sleepExec } from "child_process";
6697
+ var LINK_DIR = path25.join(os5.homedir(), ".pipemd", "link");
6698
+ var PID_FILE3 = path25.join(LINK_DIR, "relay.pid");
6699
+ var TOKEN_FILE = path25.join(LINK_DIR, "relay.token");
6700
+ var PORT_FILE = path25.join(LINK_DIR, "relay.port");
6701
+ var PEERS_FILE = path25.join(LINK_DIR, "peers.json");
6702
+ function ensureLinkDir() {
6703
+ fs25.mkdirSync(LINK_DIR, { recursive: true });
6704
+ }
6705
+ function readRelayPid() {
6706
+ try {
6707
+ return parseInt(fs25.readFileSync(PID_FILE3, "utf-8").trim(), 10) || null;
6708
+ } catch {
6709
+ return null;
6710
+ }
6711
+ }
6712
+ function isRelayRunning() {
6713
+ const pid = readRelayPid();
6714
+ if (!pid) return false;
6715
+ try {
6716
+ process.kill(pid, 0);
6717
+ return true;
6718
+ } catch {
6719
+ try {
6720
+ fs25.unlinkSync(PID_FILE3);
6721
+ } catch {
6722
+ }
6723
+ return false;
6724
+ }
6725
+ }
6726
+ function readRelayPort() {
6727
+ try {
6728
+ return parseInt(fs25.readFileSync(PORT_FILE, "utf-8").trim(), 10) || DEFAULT_PORT;
6729
+ } catch {
6730
+ return DEFAULT_PORT;
6731
+ }
6732
+ }
6733
+ function readOrGenerateToken() {
6734
+ try {
6735
+ if (fs25.existsSync(TOKEN_FILE)) {
6736
+ return fs25.readFileSync(TOKEN_FILE, "utf-8").trim();
6737
+ }
6738
+ } catch {
6739
+ }
6740
+ const token = crypto3.randomBytes(16).toString("hex");
6741
+ ensureLinkDir();
6742
+ fs25.writeFileSync(TOKEN_FILE, token, "utf-8");
6743
+ return token;
6744
+ }
6745
+ function readPeers() {
6746
+ try {
6747
+ if (!fs25.existsSync(PEERS_FILE)) return [];
6748
+ return JSON.parse(fs25.readFileSync(PEERS_FILE, "utf-8"));
6749
+ } catch {
6750
+ return [];
6751
+ }
6752
+ }
6753
+ function writePeers(peers) {
6754
+ ensureLinkDir();
6755
+ fs25.writeFileSync(PEERS_FILE, JSON.stringify(peers, null, 2), "utf-8");
6756
+ }
6757
+ function startRelayProcess() {
6758
+ const selfPath = process.argv[1];
6759
+ const child = spawn3(process.execPath, [selfPath, "_linkd"], {
6760
+ cwd: process.cwd(),
6761
+ detached: true,
6762
+ stdio: "ignore"
6763
+ });
6764
+ child.unref();
6765
+ return child.pid;
6766
+ }
6767
+ function httpGet(urlStr) {
6768
+ return new Promise((resolve2) => {
6769
+ try {
6770
+ const url = new URL(urlStr);
6771
+ const req = http2.get(
6772
+ { hostname: url.hostname, port: url.port || DEFAULT_PORT, path: url.pathname || "/health", timeout: 3e3 },
6773
+ (res) => {
6774
+ const chunks = [];
6775
+ res.on("data", (c) => chunks.push(c));
6776
+ res.on("end", () => {
6777
+ try {
6778
+ resolve2({ ok: res.statusCode === 200, data: JSON.parse(Buffer.concat(chunks).toString("utf-8")) });
6779
+ } catch {
6780
+ resolve2({ ok: res.statusCode === 200, data: null });
6781
+ }
6782
+ });
6783
+ }
6784
+ );
6785
+ req.on("error", () => resolve2({ ok: false, data: null }));
6786
+ req.on("timeout", () => {
6787
+ req.destroy();
6788
+ resolve2({ ok: false, data: null });
6789
+ });
6790
+ } catch {
6791
+ resolve2({ ok: false, data: null });
6792
+ }
6793
+ });
6794
+ }
6795
+ function httpGetStatus(host, token) {
6796
+ return new Promise((resolve2) => {
6797
+ try {
6798
+ const [h, portStr] = host.split(":");
6799
+ const port = parseInt(portStr || "9741", 10);
6800
+ const headers = {};
6801
+ if (token) headers.Authorization = `Bearer ${token}`;
6802
+ const req = http2.get({ hostname: h, port, path: "/status", timeout: 3e3, headers }, (res) => {
6803
+ const chunks = [];
6804
+ res.on("data", (c) => chunks.push(c));
6805
+ res.on("end", () => {
6806
+ try {
6807
+ resolve2(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
6808
+ } catch {
6809
+ resolve2(null);
6810
+ }
6811
+ });
6812
+ });
6813
+ req.on("error", () => resolve2(null));
6814
+ req.on("timeout", () => {
6815
+ req.destroy();
6816
+ resolve2(null);
6817
+ });
6818
+ } catch {
6819
+ resolve2(null);
6820
+ }
6821
+ });
6822
+ }
6823
+ function doStart() {
6824
+ if (isRelayRunning()) {
6825
+ return chalk18.dim(`Relay already running (PID ${readRelayPid()}, port ${readRelayPort()})`);
6826
+ }
6827
+ ensureLinkDir();
6828
+ const pid = startRelayProcess();
6829
+ for (let i = 0; i < 20; i++) {
6830
+ const check = readRelayPid();
6831
+ if (check && (() => {
6832
+ try {
6833
+ process.kill(check, 0);
6834
+ return true;
6835
+ } catch {
6836
+ return false;
6837
+ }
6838
+ })()) {
6839
+ const port = readRelayPort();
6840
+ return chalk18.green(`\u2714 Relay started (PID ${check}, port ${port})`);
6841
+ }
6842
+ sleepExec("sleep 0.25", { stdio: "ignore" });
6843
+ }
6844
+ return chalk18.yellow("\u26A0 Relay may not have started. Check with `pmd link --list`");
6845
+ }
6846
+ function formatLinkHelp() {
6847
+ const lines = [];
6848
+ const w = (name, desc) => ` ${chalk18.cyan(name.padEnd(22))}${chalk18.dim(desc)}`;
6849
+ lines.push(chalk18.bold("Usage:"));
6850
+ lines.push(w("pmd link", "Start relay and show invite command"));
6851
+ lines.push(w("pmd link <host:port>", "Connect to a remote relay"));
6852
+ lines.push("");
6853
+ lines.push(chalk18.bold("Options:"));
6854
+ lines.push(w("--token <token>", "Auth token for the remote relay"));
6855
+ lines.push(w("--list", "Show relay status and connected peers"));
6856
+ lines.push(w("--disconnect <host>", "Remove a peer connection"));
6857
+ lines.push(w("--stop", "Stop the relay process"));
6858
+ lines.push("");
6859
+ return "\n" + lines.join("\n") + "\n";
6860
+ }
6861
+ var link = new Command14("link").description("Connect PipeMD daemons across machines and Docker containers").configureHelp({ visibleCommands: () => [] }).addHelpText("after", formatLinkHelp());
6862
+ link.command("start", { hidden: true }).action(() => {
6863
+ const msg = doStart();
6864
+ if (msg) console.log(msg);
6865
+ });
6866
+ link.option("--token <token>", "auth token for remote relay").option("--list", "show relay status and connected peers").option("--disconnect <host>", "remove a peer connection").option("--stop", "stop the relay process").argument("[host]", "remote relay address (host:port)").action(async (host, opts) => {
6867
+ if (opts.stop) {
6868
+ const pid = readRelayPid();
6869
+ if (pid) {
6870
+ try {
6871
+ process.kill(pid, "SIGTERM");
6872
+ } catch {
6873
+ }
6874
+ console.log(chalk18.green(`\u2714 Relay stopped (PID ${pid})`));
6875
+ } else {
6876
+ console.log(chalk18.dim("No relay running."));
6877
+ }
6878
+ return;
6879
+ }
6880
+ if (opts.disconnect) {
6881
+ const peers = readPeers().filter((p) => p.host !== opts.disconnect);
6882
+ writePeers(peers);
6883
+ console.log(chalk18.green(`\u2714 Disconnected from ${opts.disconnect}`));
6884
+ return;
6885
+ }
6886
+ if (opts.list) {
6887
+ const running = isRelayRunning();
6888
+ const port2 = readRelayPort();
6889
+ const pid = readRelayPid();
6890
+ console.log(chalk18.bold("Relay:"));
6891
+ if (running) {
6892
+ console.log(chalk18.green(` \u2714 Running (PID ${pid}, port ${port2})`));
6893
+ } else {
6894
+ console.log(chalk18.dim(" Not running"));
6895
+ }
6896
+ const peers = readPeers();
6897
+ if (peers.length > 0) {
6898
+ console.log(chalk18.bold("\nPeers:"));
6899
+ for (const p of peers) {
6900
+ const status = await httpGetStatus(p.host, p.token);
6901
+ if (status && status.ok) {
6902
+ const groupNames = Object.keys(status.groups || {});
6903
+ const totalAgents = Object.values(status.groups || {}).reduce((sum, g) => sum + (g.local || 0) + (g.remote || 0), 0);
6904
+ console.log(chalk18.green(` \u2714 ${p.host}`) + chalk18.dim(` \u2014 ${totalAgents} agents, groups: ${groupNames.join(", ") || "none"}`));
6905
+ } else {
6906
+ console.log(chalk18.red(` \u2716 ${p.host}`) + chalk18.dim(" \u2014 unreachable"));
6907
+ }
6908
+ }
6909
+ } else {
6910
+ console.log(chalk18.dim("\n No peers configured."));
6911
+ }
6912
+ if (running) {
6913
+ const localStatus = await httpGetStatus(`localhost:${port2}`);
6914
+ if (localStatus && localStatus.groups) {
6915
+ const groups = localStatus.groups;
6916
+ const names = Object.keys(groups);
6917
+ if (names.length > 0) {
6918
+ console.log(chalk18.bold("\nGroups:"));
6919
+ for (const [name, info] of Object.entries(groups)) {
6920
+ const g = info;
6921
+ console.log(` ${chalk18.cyan(name)} \u2014 ${g.local} local, ${g.remote} remote`);
6922
+ }
6923
+ }
6924
+ }
6925
+ }
6926
+ return;
6927
+ }
6928
+ if (host) {
6929
+ const { ok } = await httpGet(`http://${host}/health`);
6930
+ if (!ok) {
6931
+ console.log(chalk18.red(`\u2716 Cannot reach ${host}. Is the relay running there?`));
6932
+ process.exit(1);
6933
+ }
6934
+ const peerToken = opts.token || "";
6935
+ const peers = readPeers();
6936
+ if (!peers.find((p) => p.host === host)) {
6937
+ peers.push({ host, token: peerToken });
6938
+ writePeers(peers);
6939
+ }
6940
+ if (!isRelayRunning()) {
6941
+ doStart();
6942
+ }
6943
+ console.log(chalk18.green(`\u2714 Connected to ${host}`));
6944
+ console.log(chalk18.dim(" Crew sessions will sync bidirectionally within 5 seconds."));
6945
+ return;
6946
+ }
6947
+ if (!isRelayRunning()) {
6948
+ doStart();
6949
+ }
6950
+ const token = readOrGenerateToken();
6951
+ const port = readRelayPort();
6952
+ const h = os5.hostname();
6953
+ console.log();
6954
+ console.log(chalk18.green(`\u2714 Token: ${token}`));
6955
+ console.log(chalk18.green(`\u2714 Relay: ${h}:${port}`));
6956
+ console.log();
6957
+ console.log("On the other machine, run:");
6958
+ console.log(chalk18.cyan(` pmd link ${h}:${port} --token ${token}`));
6959
+ console.log();
6960
+ });
6961
+ var linkCommand = link;
6962
+
6963
+ // src/core/net/relay.ts
6964
+ import http3 from "http";
6965
+ import os6 from "os";
6966
+ import fs26 from "fs";
6967
+ import path26 from "path";
6968
+ var store = /* @__PURE__ */ new Map();
6969
+ var peerLastSync = /* @__PURE__ */ new Map();
6970
+ var syncTimer = null;
6971
+ var server = null;
6972
+ function hostname() {
6973
+ return os6.hostname();
6974
+ }
6975
+ function readBody(req) {
6976
+ return new Promise((resolve2, reject) => {
6977
+ const chunks = [];
6978
+ req.on("data", (c) => chunks.push(c));
6979
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
6980
+ req.on("error", reject);
6981
+ });
6982
+ }
6983
+ function jsonResponse(res, code, data) {
6984
+ const body = JSON.stringify(data);
6985
+ res.writeHead(code, {
6986
+ "Content-Type": "application/json",
6987
+ "Content-Length": Buffer.byteLength(body)
6988
+ });
6989
+ res.end(body);
6990
+ }
6991
+ function mergeSessionsFor(group, excludeOrigin) {
6992
+ const origins = store.get(group);
6993
+ if (!origins) return [];
6994
+ const out = [];
6995
+ for (const [origin, entry] of origins) {
6996
+ if (origin === excludeOrigin) continue;
6997
+ out.push(
6998
+ ...entry.sessions.map((s) => ({
6999
+ ...s,
7000
+ _remote: true,
7001
+ _origin: origin
7002
+ }))
7003
+ );
7004
+ }
7005
+ return out;
7006
+ }
7007
+ function expireStaleGroups() {
7008
+ const now = Date.now();
7009
+ for (const [group, origins] of store) {
7010
+ for (const [origin, entry] of origins) {
7011
+ if (now - entry.lastSeen > SESSION_EXPIRY_MS) {
7012
+ origins.delete(origin);
7013
+ log.info(`Relay: expired ${origin}/${group} (stale ${Math.round((now - entry.lastSeen) / 1e3)}s)`);
7014
+ }
7015
+ }
7016
+ if (origins.size === 0) {
7017
+ store.delete(group);
7018
+ }
7019
+ }
7020
+ }
7021
+ function readPeers2() {
7022
+ try {
7023
+ const homeDir = os6.homedir();
7024
+ const peersFile = path26.join(homeDir, ".pipemd", "link", "peers.json");
7025
+ if (!fs26.existsSync(peersFile)) return [];
7026
+ return JSON.parse(fs26.readFileSync(peersFile, "utf-8"));
7027
+ } catch {
7028
+ return [];
7029
+ }
7030
+ }
7031
+ function readToken() {
7032
+ try {
7033
+ const homeDir = os6.homedir();
7034
+ const tokenFile = path26.join(homeDir, ".pipemd", "link", "relay.token");
7035
+ if (fs26.existsSync(tokenFile)) {
7036
+ return fs26.readFileSync(tokenFile, "utf-8").trim();
7037
+ }
7038
+ } catch {
7039
+ }
7040
+ return "";
7041
+ }
7042
+ function syncWithPeers() {
7043
+ expireStaleGroups();
7044
+ const peers = readPeers2();
7045
+ if (peers.length === 0) return;
7046
+ const localToken = readToken();
7047
+ const allGroups = {};
7048
+ for (const [group, origins] of store) {
7049
+ const sessions = [];
7050
+ for (const [, entry] of origins) {
7051
+ sessions.push(...entry.sessions);
7052
+ }
7053
+ allGroups[group] = sessions;
7054
+ }
7055
+ const myHostname = hostname();
7056
+ for (const peer of peers) {
7057
+ const [host, portStr] = peer.host.split(":");
7058
+ const port = parseInt(portStr || "9741", 10);
7059
+ const payload = { hostname: myHostname, groups: allGroups };
7060
+ const body = JSON.stringify(payload);
7061
+ const req = http3.request(
7062
+ { hostname: host, port, path: "/sync", method: "POST", timeout: 5e3, headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), Authorization: `Bearer ${localToken || peer.token}` } },
7063
+ (res) => {
7064
+ const chunks = [];
7065
+ res.on("data", (c) => chunks.push(c));
7066
+ res.on("end", () => {
7067
+ if (res.statusCode !== 200) return;
7068
+ try {
7069
+ const remote = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
7070
+ const now = Date.now();
7071
+ for (const [group, sessions] of Object.entries(remote.groups)) {
7072
+ let origins = store.get(group);
7073
+ if (!origins) {
7074
+ origins = /* @__PURE__ */ new Map();
7075
+ store.set(group, origins);
7076
+ }
7077
+ origins.set(remote.hostname, { sessions, lastSeen: now });
7078
+ }
7079
+ peerLastSync.set(peer.host, now);
7080
+ } catch {
7081
+ log.warn(`Relay: failed to parse sync response from ${peer.host}`);
7082
+ }
7083
+ });
7084
+ }
7085
+ );
7086
+ req.on("error", () => {
7087
+ });
7088
+ req.on("timeout", () => req.destroy());
7089
+ req.write(body);
7090
+ req.end();
7091
+ }
7092
+ }
7093
+ function handleCrew(req, res) {
7094
+ readBody(req).then((raw) => {
7095
+ const msg = JSON.parse(raw);
7096
+ const { group, hostname: origin, sessions } = msg;
7097
+ let origins = store.get(group);
7098
+ if (!origins) {
7099
+ origins = /* @__PURE__ */ new Map();
7100
+ store.set(group, origins);
7101
+ }
7102
+ origins.set(origin, { sessions, lastSeen: Date.now() });
7103
+ log.info(`Relay: received ${sessions.length} session(s) for ${group} from ${origin}`);
7104
+ const remoteSessions = mergeSessionsFor(group, origin);
7105
+ jsonResponse(res, 200, { sessions: remoteSessions });
7106
+ }).catch(() => jsonResponse(res, 400, { error: "invalid body" }));
7107
+ }
7108
+ function handleSync(req, res) {
7109
+ const token = readToken();
7110
+ const auth = req.headers.authorization;
7111
+ if (token && auth !== `Bearer ${token}`) {
7112
+ jsonResponse(res, 403, { error: "unauthorized" });
7113
+ return;
7114
+ }
7115
+ readBody(req).then((raw) => {
7116
+ const msg = JSON.parse(raw);
7117
+ const now = Date.now();
7118
+ for (const [group, sessions] of Object.entries(msg.groups)) {
7119
+ let origins = store.get(group);
7120
+ if (!origins) {
7121
+ origins = /* @__PURE__ */ new Map();
7122
+ store.set(group, origins);
7123
+ }
7124
+ origins.set(msg.hostname, { sessions, lastSeen: now });
7125
+ }
7126
+ const myGroups = {};
7127
+ for (const [group, origins] of store) {
7128
+ const sessions = [];
7129
+ for (const [origin, entry] of origins) {
7130
+ if (origin !== msg.hostname) {
7131
+ sessions.push(...entry.sessions);
7132
+ }
7133
+ }
7134
+ if (sessions.length > 0) myGroups[group] = sessions;
7135
+ }
7136
+ peerLastSync.set(msg.hostname, now);
7137
+ jsonResponse(res, 200, { hostname: hostname(), groups: myGroups });
7138
+ }).catch(() => jsonResponse(res, 400, { error: "invalid body" }));
7139
+ }
7140
+ function handleStatus(_req, res) {
7141
+ const groups = {};
7142
+ const myHost = hostname();
7143
+ for (const [group, origins] of store) {
7144
+ let local = 0;
7145
+ let remote = 0;
7146
+ for (const [origin, entry] of origins) {
7147
+ if (origin === myHost) local += entry.sessions.length;
7148
+ else remote += entry.sessions.length;
7149
+ }
7150
+ groups[group] = { local, remote };
7151
+ }
7152
+ const peers = readPeers2().map((p) => {
7153
+ const ts = peerLastSync.get(p.host);
7154
+ return { host: p.host, lastSync: ts ? new Date(ts).toISOString() : null };
7155
+ });
7156
+ jsonResponse(res, 200, { ok: true, hostname: myHost, groups, peers });
7157
+ }
7158
+ function requestHandler(req, res) {
7159
+ if (req.method === "POST" && req.url === "/crew") {
7160
+ handleCrew(req, res);
7161
+ } else if (req.method === "POST" && req.url === "/sync") {
7162
+ handleSync(req, res);
7163
+ } else if (req.method === "GET" && req.url === "/status") {
7164
+ handleStatus(req, res);
7165
+ } else if (req.method === "GET" && req.url === "/health") {
7166
+ jsonResponse(res, 200, { ok: true, hostname: hostname() });
7167
+ } else {
7168
+ jsonResponse(res, 404, { error: "not found" });
7169
+ }
7170
+ }
7171
+ function startRelay(port = DEFAULT_PORT) {
7172
+ return new Promise((resolve2, reject) => {
7173
+ server = http3.createServer(requestHandler);
7174
+ server.on("error", (err) => {
7175
+ if (err.code === "EADDRINUSE") {
7176
+ server = null;
7177
+ reject(err);
7178
+ } else {
7179
+ log.error(`Relay error: ${err.message}`);
7180
+ }
7181
+ });
7182
+ server.listen(port, () => {
7183
+ const addr = server.address();
7184
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
7185
+ log.info(`Relay listening on port ${actualPort}`);
7186
+ syncTimer = setInterval(syncWithPeers, POLL_INTERVAL_MS);
7187
+ setInterval(expireStaleGroups, POLL_INTERVAL_MS * 3);
7188
+ resolve2(actualPort);
7189
+ });
7190
+ });
7191
+ }
7192
+ function stopRelay() {
7193
+ if (syncTimer) {
7194
+ clearInterval(syncTimer);
7195
+ syncTimer = null;
7196
+ }
7197
+ if (server) {
7198
+ server.close();
7199
+ server = null;
7200
+ }
7201
+ log.info("Relay stopped");
7202
+ }
7203
+ function runRelay() {
7204
+ const homeDir = os6.homedir();
7205
+ const linkDir = path26.join(homeDir, ".pipemd", "link");
7206
+ fs26.mkdirSync(linkDir, { recursive: true });
7207
+ const pidFile = path26.join(linkDir, "relay.pid");
7208
+ fs26.writeFileSync(pidFile, String(process.pid), "utf-8");
7209
+ process.on("SIGTERM", () => {
7210
+ try {
7211
+ fs26.unlinkSync(pidFile);
7212
+ } catch {
7213
+ }
7214
+ stopRelay();
7215
+ process.exit(0);
7216
+ });
7217
+ process.on("SIGINT", () => {
7218
+ try {
7219
+ fs26.unlinkSync(pidFile);
7220
+ } catch {
7221
+ }
7222
+ stopRelay();
7223
+ process.exit(0);
7224
+ });
7225
+ const envPort = parseInt(process.env.PMD_LINK_PORT || "", 10);
7226
+ let port = isNaN(envPort) ? DEFAULT_PORT : envPort;
7227
+ startRelay(port).then((actualPort) => {
7228
+ log.info(`Relay running on port ${actualPort}`);
7229
+ const portFile = path26.join(linkDir, "relay.port");
7230
+ fs26.writeFileSync(portFile, String(actualPort), "utf-8");
7231
+ }).catch((err) => {
7232
+ log.error(`Relay failed to start: ${err.message}`);
7233
+ try {
7234
+ fs26.unlinkSync(pidFile);
7235
+ } catch {
7236
+ }
7237
+ process.exit(1);
7238
+ });
7239
+ }
7240
+
6554
7241
  // src/index.ts
6555
- var program = new Command14();
7242
+ var program = new Command15();
6556
7243
  var GROUPS = [
6557
7244
  {
6558
7245
  title: "Setup",
@@ -6590,6 +7277,10 @@ var GROUPS = [
6590
7277
  {
6591
7278
  name: "trace",
6592
7279
  desc: "Live resolution tree \u2014 debug crew coordination"
7280
+ },
7281
+ {
7282
+ name: "link",
7283
+ desc: "Connect daemons across machines and Docker containers"
6593
7284
  }
6594
7285
  ]
6595
7286
  },
@@ -6609,17 +7300,17 @@ function formatHelp() {
6609
7300
  ...GROUPS.flatMap((g) => g.commands.map((c) => c.name.length))
6610
7301
  );
6611
7302
  for (const group of GROUPS) {
6612
- lines.push(chalk18.bold(group.title + ":"));
7303
+ lines.push(chalk19.bold(group.title + ":"));
6613
7304
  for (const cmd of group.commands) {
6614
7305
  lines.push(
6615
- ` ${chalk18.cyan(padRight(cmd.name, maxName))} ${chalk18.dim(cmd.desc)}`
7306
+ ` ${chalk19.cyan(padRight(cmd.name, maxName))} ${chalk19.dim(cmd.desc)}`
6616
7307
  );
6617
7308
  }
6618
7309
  lines.push("");
6619
7310
  }
6620
7311
  lines.push(
6621
- chalk18.dim(
6622
- `Run ${chalk18.reset("pmd <command> --help")} for usage on any command.`
7312
+ chalk19.dim(
7313
+ `Run ${chalk19.reset("pmd <command> --help")} for usage on any command.`
6623
7314
  )
6624
7315
  );
6625
7316
  lines.push("");
@@ -6627,7 +7318,7 @@ function formatHelp() {
6627
7318
  }
6628
7319
  program.name("pmd").description(
6629
7320
  "PipeMD \u2014 The Dynamic Context Harness for AI Coding Agents"
6630
- ).version("1.0.0").configureHelp({ visibleCommands: () => [] }).addHelpText("after", formatHelp());
7321
+ ).version("1.1.0").configureHelp({ visibleCommands: () => [] }).addHelpText("after", formatHelp());
6631
7322
  program.addCommand(initCommand);
6632
7323
  program.addCommand(startCommand);
6633
7324
  program.addCommand(stopCommand);
@@ -6639,9 +7330,13 @@ program.addCommand(doctorCommand);
6639
7330
  program.addCommand(uninstallCommand);
6640
7331
  program.addCommand(crewCommand);
6641
7332
  program.addCommand(traceCommand);
7333
+ program.addCommand(linkCommand);
6642
7334
  program.addCommand(injectCommand, { hidden: true });
6643
7335
  program.addCommand(statuslineCommand, { hidden: true });
6644
7336
  program.command("_daemon", { hidden: true }).description("(internal) Run the daemon process").action(() => {
6645
7337
  runDaemon();
6646
7338
  });
7339
+ program.command("_linkd", { hidden: true }).description("(internal) Run the link relay process").action(() => {
7340
+ runRelay();
7341
+ });
6647
7342
  program.parse();