@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
|
|
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 =
|
|
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 @@
|
|
|
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"}
|