@hasna/machines 0.0.4 → 0.0.5

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/cli/index.js CHANGED
@@ -17959,6 +17959,28 @@ function readHistory(historyPath) {
17959
17959
  return [];
17960
17960
  }
17961
17961
  }
17962
+ function writeHistory(entries, historyPath) {
17963
+ const path = resolveHistoryPath(historyPath);
17964
+ ensureParentDir(path);
17965
+ writeFileSync5(path, `${JSON.stringify(entries, null, 2)}
17966
+ `, "utf8");
17967
+ }
17968
+ function computeHash(content) {
17969
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
17970
+ }
17971
+ function shouldSkipContent(content, skipPatterns) {
17972
+ const lower = content.toLowerCase();
17973
+ return skipPatterns.some((pattern) => lower.includes(pattern.toLowerCase()));
17974
+ }
17975
+ function sanitizeClipboardForRead(content, maxSizeBytes, skipPatterns) {
17976
+ if (Buffer.byteLength(content, "utf8") > maxSizeBytes) {
17977
+ return { ok: false, reason: "content exceeds size limit" };
17978
+ }
17979
+ if (shouldSkipContent(content, skipPatterns)) {
17980
+ return { ok: false, reason: "content matches skip pattern" };
17981
+ }
17982
+ return { ok: true };
17983
+ }
17962
17984
  function getOrCreateClipboardKey() {
17963
17985
  const keyPath = getClipboardKeyPath();
17964
17986
  if (existsSync8(keyPath)) {
@@ -17985,6 +18007,20 @@ function writeClipboardConfig(config, configPath) {
17985
18007
  function readClipboardHistory(historyPath) {
17986
18008
  return readHistory(historyPath);
17987
18009
  }
18010
+ function addClipboardEntry(entry, historyPath) {
18011
+ const entries = readHistory(historyPath);
18012
+ const existing = entries.find((e) => e.hash === entry.hash);
18013
+ if (existing) {
18014
+ existing.timestamp = entry.timestamp;
18015
+ } else {
18016
+ entries.unshift(entry);
18017
+ }
18018
+ const config = readConfig();
18019
+ if (entries.length > config.maxHistory) {
18020
+ entries.length = config.maxHistory;
18021
+ }
18022
+ writeHistory(entries, historyPath);
18023
+ }
17988
18024
  function clearClipboardHistory(historyPath) {
17989
18025
  const path = resolveHistoryPath(historyPath);
17990
18026
  if (existsSync8(path)) {
@@ -18001,6 +18037,292 @@ function getClipboardStatus(historyPath) {
18001
18037
  };
18002
18038
  }
18003
18039
 
18040
+ // src/commands/clipboard-daemon.ts
18041
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "fs";
18042
+ import { join as join10 } from "path";
18043
+ import { createHash as createHash3 } from "crypto";
18044
+
18045
+ // src/commands/clipboard-server.ts
18046
+ import { createServer } from "http";
18047
+ import { createHash as createHash2 } from "crypto";
18048
+ import { readFileSync as readFileSync8 } from "fs";
18049
+ function readLocalClipboardSync() {
18050
+ const platform4 = process.platform;
18051
+ if (platform4 === "darwin") {
18052
+ const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
18053
+ return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
18054
+ }
18055
+ if (platform4 === "linux") {
18056
+ if (hasCommand2("wl-paste")) {
18057
+ const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
18058
+ return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
18059
+ }
18060
+ if (hasCommand2("xclip")) {
18061
+ const result = Bun.spawnSync(["xclip", "-selection", "clipboard", "-o"], { stdout: "pipe", stderr: "pipe" });
18062
+ return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
18063
+ }
18064
+ return "";
18065
+ }
18066
+ return "";
18067
+ }
18068
+ function writeLocalClipboardSync(content) {
18069
+ const platform4 = process.platform;
18070
+ if (platform4 === "darwin") {
18071
+ const result = Bun.spawnSync(["pbcopy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
18072
+ return result.exitCode === 0;
18073
+ }
18074
+ if (platform4 === "linux") {
18075
+ if (hasCommand2("wl-copy")) {
18076
+ const result = Bun.spawnSync(["wl-copy"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
18077
+ return result.exitCode === 0;
18078
+ }
18079
+ if (hasCommand2("xclip")) {
18080
+ const result = Bun.spawnSync(["xclip", "-selection", "clipboard"], { stdin: new TextEncoder().encode(content), stdout: "ignore", stderr: "ignore" });
18081
+ return result.exitCode === 0;
18082
+ }
18083
+ return false;
18084
+ }
18085
+ return false;
18086
+ }
18087
+ function hasCommand2(binary) {
18088
+ const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], { stdout: "ignore", stderr: "ignore", env: process.env });
18089
+ return result.exitCode === 0;
18090
+ }
18091
+ function loadSharedSecret() {
18092
+ const keyPath = getClipboardKeyPath();
18093
+ try {
18094
+ return readFileSync8(keyPath, "utf8").trim();
18095
+ } catch {
18096
+ return "";
18097
+ }
18098
+ }
18099
+ function authenticate(request) {
18100
+ const authHeader = request.headers["authorization"];
18101
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
18102
+ return false;
18103
+ }
18104
+ const token = authHeader.slice(7);
18105
+ const secret = loadSharedSecret();
18106
+ if (!secret)
18107
+ return false;
18108
+ return createHash2("sha256").update(token).digest("hex") === createHash2("sha256").update(secret).digest("hex");
18109
+ }
18110
+ function jsonResponse(response, status, data) {
18111
+ response.writeHead(status, { "content-type": "application/json" });
18112
+ response.end(JSON.stringify(data));
18113
+ }
18114
+ var currentContentHash = null;
18115
+ function startClipboardServer(options = {}) {
18116
+ const config = options.config || readClipboardConfig();
18117
+ const port = options.port || config.port;
18118
+ const server = createServer(async (request, response) => {
18119
+ if (!authenticate(request)) {
18120
+ return jsonResponse(response, 401, { error: "unauthorized" });
18121
+ }
18122
+ const url = new URL(request.url || "/", `http://${request.headers.host || "localhost"}`);
18123
+ if (url.pathname === "/clipboard" && request.method === "POST") {
18124
+ return handleReceiveClipboard(request, response, config);
18125
+ }
18126
+ if (url.pathname === "/clipboard" && request.method === "GET") {
18127
+ return handleGetClipboard(response, config);
18128
+ }
18129
+ if (url.pathname === "/health" && request.method === "GET") {
18130
+ return jsonResponse(response, 200, { ok: true, machineId: process.env["HASNA_MACHINES_MACHINE_ID"] || "unknown" });
18131
+ }
18132
+ jsonResponse(response, 404, { error: "not found" });
18133
+ });
18134
+ server.listen(port, "0.0.0.0", () => {});
18135
+ server.on("error", (error) => {
18136
+ console.error(`clipboard server error: ${error.message}`);
18137
+ });
18138
+ return {
18139
+ server,
18140
+ port,
18141
+ close: async () => {
18142
+ await new Promise((resolve2) => server.close(() => resolve2()));
18143
+ }
18144
+ };
18145
+ }
18146
+ function handleReceiveClipboard(request, response, config) {
18147
+ let body = "";
18148
+ request.on("data", (chunk) => {
18149
+ body += chunk;
18150
+ });
18151
+ request.on("end", () => {
18152
+ try {
18153
+ const parsed = JSON.parse(body);
18154
+ const content = parsed.content || "";
18155
+ const contentType = parsed.contentType || "text";
18156
+ const sourceMachine = parsed.sourceMachine || "unknown";
18157
+ if (!content) {
18158
+ return jsonResponse(response, 400, { error: "empty content" });
18159
+ }
18160
+ const hash = computeHash(content);
18161
+ if (hash === currentContentHash) {
18162
+ return jsonResponse(response, 200, { received: false, reason: "loop detected" });
18163
+ }
18164
+ const check2 = sanitizeClipboardForRead(content, config.maxSizeBytes, config.skipPatterns);
18165
+ if (!check2.ok) {
18166
+ return jsonResponse(response, 200, { received: false, reason: check2.reason });
18167
+ }
18168
+ writeLocalClipboardSync(content);
18169
+ currentContentHash = hash;
18170
+ addClipboardEntry({
18171
+ hash,
18172
+ content,
18173
+ contentType,
18174
+ sourceMachine,
18175
+ timestamp: new Date().toISOString()
18176
+ });
18177
+ return jsonResponse(response, 200, { received: true, hash });
18178
+ } catch {
18179
+ return jsonResponse(response, 400, { error: "invalid JSON" });
18180
+ }
18181
+ });
18182
+ }
18183
+ function handleGetClipboard(response, config) {
18184
+ const content = readLocalClipboardSync();
18185
+ if (!content) {
18186
+ return jsonResponse(response, 200, { content: "", hash: null });
18187
+ }
18188
+ const hash = computeHash(content);
18189
+ return jsonResponse(response, 200, { content, hash, contentType: "text" });
18190
+ }
18191
+
18192
+ // src/commands/clipboard-daemon.ts
18193
+ var DAEMON_PID_PATH = join10(getDataDir(), "clipboard-daemon.pid");
18194
+ function readLocalClipboardSync2() {
18195
+ const platform4 = process.platform;
18196
+ if (platform4 === "darwin") {
18197
+ const result = Bun.spawnSync(["pbpaste"], { stdout: "pipe", stderr: "pipe" });
18198
+ return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
18199
+ }
18200
+ if (platform4 === "linux") {
18201
+ if (hasCommand3("wl-paste")) {
18202
+ const result = Bun.spawnSync(["wl-paste"], { stdout: "pipe", stderr: "pipe" });
18203
+ return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
18204
+ }
18205
+ if (hasCommand3("xclip")) {
18206
+ const result = Bun.spawnSync(["xclip", "-selection", "clipboard", "-o"], { stdout: "pipe", stderr: "pipe" });
18207
+ return result.exitCode === 0 ? result.stdout.toString("utf8").trim() : "";
18208
+ }
18209
+ return "";
18210
+ }
18211
+ return "";
18212
+ }
18213
+ function hasCommand3(binary) {
18214
+ const result = Bun.spawnSync(["bash", "-lc", `command -v ${binary} >/dev/null 2>&1`], { stdout: "ignore", stderr: "ignore", env: process.env });
18215
+ return result.exitCode === 0;
18216
+ }
18217
+ function computeHash2(content) {
18218
+ return createHash3("sha256").update(content).digest("hex").slice(0, 16);
18219
+ }
18220
+ function loadSharedSecret2() {
18221
+ try {
18222
+ return readFileSync9(getClipboardKeyPath(), "utf8").trim();
18223
+ } catch {
18224
+ return "";
18225
+ }
18226
+ }
18227
+ function writePid(pid) {
18228
+ writeFileSync6(DAEMON_PID_PATH, `${pid}
18229
+ `);
18230
+ }
18231
+ function readPid() {
18232
+ try {
18233
+ const pid = Number.parseInt(readFileSync9(DAEMON_PID_PATH, "utf8").trim());
18234
+ return Number.isFinite(pid) ? pid : null;
18235
+ } catch {
18236
+ return null;
18237
+ }
18238
+ }
18239
+ function isProcessRunning(pid) {
18240
+ try {
18241
+ process.kill(pid, 0);
18242
+ return true;
18243
+ } catch {
18244
+ return false;
18245
+ }
18246
+ }
18247
+ function stopClipboardDaemon() {
18248
+ const pid = readPid();
18249
+ if (pid && isProcessRunning(pid)) {
18250
+ process.kill(pid, "SIGTERM");
18251
+ return { stopped: true, pid };
18252
+ }
18253
+ return { stopped: false, pid };
18254
+ }
18255
+ function startClipboardDaemon(port) {
18256
+ const config = readClipboardConfig();
18257
+ const daemonPort = port || config.port;
18258
+ const { server, close } = startClipboardServer({ port: daemonPort });
18259
+ server.on("listening", () => {
18260
+ console.log(`clipboard daemon started on port ${daemonPort} (pid ${process.pid})`);
18261
+ writePid(process.pid);
18262
+ });
18263
+ let lastHash = "";
18264
+ const secret = loadSharedSecret2();
18265
+ const machineId = process.env["HASNA_MACHINES_MACHINE_ID"] || "unknown";
18266
+ setInterval(async () => {
18267
+ const content = readLocalClipboardSync2();
18268
+ if (!content)
18269
+ return;
18270
+ const hash = computeHash2(content);
18271
+ if (hash === lastHash)
18272
+ return;
18273
+ lastHash = hash;
18274
+ const peers = await discoverPeers();
18275
+ for (const peer of peers) {
18276
+ try {
18277
+ const res = await fetch(`http://${peer.host}:${peer.port}/clipboard`, {
18278
+ method: "POST",
18279
+ headers: {
18280
+ "content-type": "application/json",
18281
+ authorization: `Bearer ${secret}`
18282
+ },
18283
+ body: JSON.stringify({
18284
+ content,
18285
+ contentType: "text",
18286
+ sourceMachine: machineId
18287
+ }),
18288
+ signal: AbortSignal.timeout(2000)
18289
+ });
18290
+ if (res.ok) {
18291
+ const data = await res.json();
18292
+ if (data["received"] === true) {
18293
+ console.log(`clipboard sent to ${peer.host}`);
18294
+ }
18295
+ }
18296
+ } catch {}
18297
+ }
18298
+ }, 500);
18299
+ }
18300
+ async function discoverPeers() {
18301
+ const config = readClipboardConfig();
18302
+ const peers = [];
18303
+ try {
18304
+ const result = Bun.spawnSync(["tailscale", "status", "--json"], { stdout: "pipe", stderr: "pipe", env: process.env });
18305
+ if (result.exitCode === 0) {
18306
+ const status = JSON.parse(result.stdout.toString("utf8"));
18307
+ const peers_map = status["Peer"] || {};
18308
+ for (const [, peerInfo] of Object.entries(peers_map)) {
18309
+ for (const ip of peerInfo.TailscaleIPs) {
18310
+ if (ip.includes(".") && !ip.endsWith(".1")) {
18311
+ peers.push({ host: ip, port: config.port });
18312
+ }
18313
+ }
18314
+ }
18315
+ }
18316
+ } catch {}
18317
+ const knownPeers = ["100.82.44.120", "100.100.226.69", "100.71.123.34", "100.85.234.92"];
18318
+ for (const ip of knownPeers) {
18319
+ if (!peers.some((p) => p.host === ip)) {
18320
+ peers.push({ host: ip, port: config.port });
18321
+ }
18322
+ }
18323
+ return peers;
18324
+ }
18325
+
18004
18326
  // src/cli-utils.ts
18005
18327
  function parseIntegerOption(value, label, constraints = {}) {
18006
18328
  const parsed = Number.parseInt(value, 10);
@@ -18032,7 +18354,7 @@ ${items.map((item) => `- ${item}`).join(`
18032
18354
 
18033
18355
  // src/cli/index.ts
18034
18356
  import { rmSync as rmSync2 } from "fs";
18035
- import { readFileSync as readFileSync8 } from "fs";
18357
+ import { readFileSync as readFileSync10 } from "fs";
18036
18358
  var program2 = new Command;
18037
18359
  function printJsonOrText(data, text, json = false) {
18038
18360
  if (json || program2.opts().quiet) {
@@ -18179,7 +18501,7 @@ manifestCommand.command("add").description("Add or replace a machine in the flee
18179
18501
  console.error("error: --from-stdin requires piped input");
18180
18502
  process.exit(1);
18181
18503
  }
18182
- const input = readFileSync8(0, "utf8");
18504
+ const input = readFileSync10(0, "utf8");
18183
18505
  const machine2 = JSON.parse(input);
18184
18506
  console.log(JSON.stringify(manifestAdd(machine2), null, 2));
18185
18507
  return;
@@ -18358,6 +18680,14 @@ clipboardCommand.command("key").description("Show or rotate the shared secret ke
18358
18680
  const key = getOrCreateClipboardKey();
18359
18681
  printJsonOrText({ key }, key, options.json);
18360
18682
  });
18683
+ clipboardCommand.command("start").description("Start clipboard sync daemon").option("--port <port>", "Port to listen on").action((options) => {
18684
+ const port = options.port ? Number(options.port) : undefined;
18685
+ startClipboardDaemon(port);
18686
+ });
18687
+ clipboardCommand.command("stop").description("Stop clipboard sync daemon").action(() => {
18688
+ const result = stopClipboardDaemon();
18689
+ console.log(result.stopped ? `daemon stopped (pid ${result.pid})` : "daemon not running");
18690
+ });
18361
18691
  installClaudeCommand.command("status").description("Check installed state for Claude, Codex, and Gemini CLIs").option("--machine <id>", "Machine identifier").option("--tool <name...>", "CLI tools to inspect (claude, codex, gemini)").option("-j, --json", "Print JSON output", false).action((options) => {
18362
18692
  const result = getClaudeCliStatus(options.machine, options.tool);
18363
18693
  printJsonOrText(result, renderClaudeStatusResult(result), options.json);
@@ -0,0 +1,6 @@
1
+ export declare function stopClipboardDaemon(): {
2
+ stopped: boolean;
3
+ pid: number | null;
4
+ };
5
+ export declare function startClipboardDaemon(port?: number): void;
6
+ //# sourceMappingURL=clipboard-daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipboard-daemon.d.ts","sourceRoot":"","sources":["../../src/commands/clipboard-daemon.ts"],"names":[],"mappings":"AAwFA,wBAAgB,mBAAmB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAO9E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAqDxD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/machines",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Machine fleet management CLI + MCP for developers",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",