hatchee 0.1.2 → 0.1.4
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.mjs +143 -48
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11521,6 +11521,24 @@ class Daemon {
|
|
|
11521
11521
|
openPairingWindow(code, ttlMs = 5 * 60000) {
|
|
11522
11522
|
this.pairing = { code, expiresAt: Date.now() + ttlMs };
|
|
11523
11523
|
}
|
|
11524
|
+
pairingPayload(forceCode) {
|
|
11525
|
+
const code = forceCode || newPairingCode();
|
|
11526
|
+
this.openPairingWindow(code);
|
|
11527
|
+
const secret = b64.decode(this.cfg.secretKey);
|
|
11528
|
+
const pk = b64.encode(publicKey(secret));
|
|
11529
|
+
const room = b64url(publicKey(secret));
|
|
11530
|
+
const lan = lanIPv4();
|
|
11531
|
+
const payload = {
|
|
11532
|
+
v: 1,
|
|
11533
|
+
...lan ? { ws: `ws://${lan}:${this.cfg.wsPort}` } : {},
|
|
11534
|
+
code,
|
|
11535
|
+
host: this.cfg.host,
|
|
11536
|
+
pk,
|
|
11537
|
+
relay: this.cfg.relayUrl,
|
|
11538
|
+
room
|
|
11539
|
+
};
|
|
11540
|
+
return { payload, code, lan, room, relay: this.cfg.relayUrl, wsPort: this.cfg.wsPort };
|
|
11541
|
+
}
|
|
11524
11542
|
sealFor(p, msg) {
|
|
11525
11543
|
return p.key ? encode({ t: "enc", b: seal(p.key, encode(msg)) }) : encode(msg);
|
|
11526
11544
|
}
|
|
@@ -11923,7 +11941,23 @@ class Daemon {
|
|
|
11923
11941
|
sock.on("error", () => self.phones.delete(conn));
|
|
11924
11942
|
});
|
|
11925
11943
|
const hooks = createServer((req, res) => {
|
|
11926
|
-
|
|
11944
|
+
const url = req.url ?? "";
|
|
11945
|
+
const jsonPost = req.method === "POST" && String(req.headers["content-type"] ?? "").includes("application/json");
|
|
11946
|
+
if (url === "/pair") {
|
|
11947
|
+
if (!jsonPost) {
|
|
11948
|
+
res.writeHead(415).end("expected application/json");
|
|
11949
|
+
return;
|
|
11950
|
+
}
|
|
11951
|
+
try {
|
|
11952
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
11953
|
+
res.end(JSON.stringify(self.pairingPayload()));
|
|
11954
|
+
} catch (e) {
|
|
11955
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
11956
|
+
res.end(JSON.stringify({ ok: false, error: String(e) }));
|
|
11957
|
+
}
|
|
11958
|
+
return;
|
|
11959
|
+
}
|
|
11960
|
+
if (!jsonPost || url !== "/hook") {
|
|
11927
11961
|
res.writeHead(404).end("not found");
|
|
11928
11962
|
return;
|
|
11929
11963
|
}
|
|
@@ -11951,6 +11985,9 @@ class Daemon {
|
|
|
11951
11985
|
function decision(d, reason) {
|
|
11952
11986
|
return { body: { decision: d, reason } };
|
|
11953
11987
|
}
|
|
11988
|
+
function b64url(bytes) {
|
|
11989
|
+
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
11990
|
+
}
|
|
11954
11991
|
function toolSummary(toolName, input) {
|
|
11955
11992
|
switch (toolName) {
|
|
11956
11993
|
case "Bash":
|
|
@@ -11984,7 +12021,7 @@ class RelayClient {
|
|
|
11984
12021
|
this.secretKeyB64 = secretKeyB64;
|
|
11985
12022
|
}
|
|
11986
12023
|
get room() {
|
|
11987
|
-
return
|
|
12024
|
+
return b64url2(publicKey(b64.decode(this.secretKeyB64)));
|
|
11988
12025
|
}
|
|
11989
12026
|
url() {
|
|
11990
12027
|
return `${this.relayBase.replace(/\/$/, "")}/c/${this.room}?role=daemon`;
|
|
@@ -12065,7 +12102,7 @@ class RelayClient {
|
|
|
12065
12102
|
}
|
|
12066
12103
|
}
|
|
12067
12104
|
}
|
|
12068
|
-
function
|
|
12105
|
+
function b64url2(bytes) {
|
|
12069
12106
|
return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
12070
12107
|
}
|
|
12071
12108
|
|
|
@@ -12108,7 +12145,12 @@ function installHooks(droverBin) {
|
|
|
12108
12145
|
return settingsPath();
|
|
12109
12146
|
}
|
|
12110
12147
|
function isOurs(x) {
|
|
12111
|
-
|
|
12148
|
+
if (typeof x?.command !== "string")
|
|
12149
|
+
return false;
|
|
12150
|
+
const cmd = x.command.trimEnd();
|
|
12151
|
+
if (!/\bhook'?\s*$/.test(cmd))
|
|
12152
|
+
return false;
|
|
12153
|
+
return cmd.includes("hatchee") || cmd.includes("drover") || cmd.includes("daemon/src/cli.ts");
|
|
12112
12154
|
}
|
|
12113
12155
|
function uninstallHooks() {
|
|
12114
12156
|
if (!existsSync2(settingsPath()))
|
|
@@ -12126,12 +12168,22 @@ function uninstallHooks() {
|
|
|
12126
12168
|
function readStdin() {
|
|
12127
12169
|
return new Promise((resolve) => {
|
|
12128
12170
|
let data = "";
|
|
12171
|
+
let done = false;
|
|
12172
|
+
const finish = () => {
|
|
12173
|
+
if (!done) {
|
|
12174
|
+
done = true;
|
|
12175
|
+
resolve(data);
|
|
12176
|
+
}
|
|
12177
|
+
};
|
|
12129
12178
|
process.stdin.setEncoding("utf8");
|
|
12130
12179
|
process.stdin.on("data", (c) => {
|
|
12131
12180
|
data += c;
|
|
12132
12181
|
});
|
|
12133
|
-
process.stdin.on("end",
|
|
12134
|
-
setTimeout(() =>
|
|
12182
|
+
process.stdin.on("end", finish);
|
|
12183
|
+
setTimeout(() => {
|
|
12184
|
+
if (data === "")
|
|
12185
|
+
finish();
|
|
12186
|
+
}, 2000);
|
|
12135
12187
|
});
|
|
12136
12188
|
}
|
|
12137
12189
|
async function runHook(hookPort) {
|
|
@@ -12181,17 +12233,20 @@ var UNIT = join3(HOME, ".config", "systemd", "user", "hatchee.service");
|
|
|
12181
12233
|
function servicePath(node) {
|
|
12182
12234
|
return [
|
|
12183
12235
|
dirname(node),
|
|
12184
|
-
join3(HOME, ".bun/bin"),
|
|
12185
|
-
join3(HOME, ".npm-global/bin"),
|
|
12186
|
-
join3(HOME, ".local/bin"),
|
|
12187
|
-
"/opt/homebrew/bin",
|
|
12188
|
-
"/usr/local/bin",
|
|
12189
12236
|
"/usr/bin",
|
|
12190
12237
|
"/bin",
|
|
12191
12238
|
"/usr/sbin",
|
|
12192
|
-
"/sbin"
|
|
12239
|
+
"/sbin",
|
|
12240
|
+
"/opt/homebrew/bin",
|
|
12241
|
+
"/usr/local/bin",
|
|
12242
|
+
join3(HOME, ".bun/bin"),
|
|
12243
|
+
join3(HOME, ".npm-global/bin"),
|
|
12244
|
+
join3(HOME, ".local/bin")
|
|
12193
12245
|
].join(":");
|
|
12194
12246
|
}
|
|
12247
|
+
function xmlEsc(s) {
|
|
12248
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
12249
|
+
}
|
|
12195
12250
|
function copyStableBin() {
|
|
12196
12251
|
mkdirSync3(BIN_DIR, { recursive: true });
|
|
12197
12252
|
const src = process.argv[1];
|
|
@@ -12201,7 +12256,7 @@ function copyStableBin() {
|
|
|
12201
12256
|
}
|
|
12202
12257
|
}
|
|
12203
12258
|
function plistContent(node, bin) {
|
|
12204
|
-
const args = [node, bin, "up"].map((s) => ` <string>${s}</string>`).join(`
|
|
12259
|
+
const args = [node, bin, "up"].map((s) => ` <string>${xmlEsc(s)}</string>`).join(`
|
|
12205
12260
|
`);
|
|
12206
12261
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12207
12262
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -12215,10 +12270,10 @@ ${args}
|
|
|
12215
12270
|
<key>RunAtLoad</key><true/>
|
|
12216
12271
|
<key>KeepAlive</key><true/>
|
|
12217
12272
|
<key>ThrottleInterval</key><integer>10</integer>
|
|
12218
|
-
<key>StandardOutPath</key><string>${LOG}</string>
|
|
12219
|
-
<key>StandardErrorPath</key><string>${LOG}</string>
|
|
12273
|
+
<key>StandardOutPath</key><string>${xmlEsc(LOG)}</string>
|
|
12274
|
+
<key>StandardErrorPath</key><string>${xmlEsc(LOG)}</string>
|
|
12220
12275
|
<key>EnvironmentVariables</key>
|
|
12221
|
-
<dict><key>PATH</key><string>${servicePath(node)}</string></dict>
|
|
12276
|
+
<dict><key>PATH</key><string>${xmlEsc(servicePath(node))}</string></dict>
|
|
12222
12277
|
</dict>
|
|
12223
12278
|
</plist>
|
|
12224
12279
|
`;
|
|
@@ -12229,10 +12284,10 @@ Description=Hatchee — watch & approve your coding agents from your phone
|
|
|
12229
12284
|
After=network-online.target
|
|
12230
12285
|
|
|
12231
12286
|
[Service]
|
|
12232
|
-
ExecStart
|
|
12287
|
+
ExecStart="${node}" "${bin}" up
|
|
12233
12288
|
Restart=always
|
|
12234
12289
|
RestartSec=10
|
|
12235
|
-
Environment=PATH=${servicePath(node)}
|
|
12290
|
+
Environment="PATH=${servicePath(node)}"
|
|
12236
12291
|
|
|
12237
12292
|
[Install]
|
|
12238
12293
|
WantedBy=default.target
|
|
@@ -12241,12 +12296,22 @@ WantedBy=default.target
|
|
|
12241
12296
|
function run(cmd, args) {
|
|
12242
12297
|
try {
|
|
12243
12298
|
execFileSync(cmd, args, { stdio: "pipe" });
|
|
12299
|
+
return true;
|
|
12244
12300
|
} catch (e) {
|
|
12245
12301
|
console.log(` · ${cmd} ${args.join(" ")} → ${String(e?.stderr || e?.message || e).trim().slice(0, 200)}`);
|
|
12302
|
+
return false;
|
|
12246
12303
|
}
|
|
12247
12304
|
}
|
|
12248
12305
|
function installService(print = false) {
|
|
12249
12306
|
const node = process.execPath;
|
|
12307
|
+
const src = process.argv[1] ?? "";
|
|
12308
|
+
if (!print && src && !/\.(mjs|cjs|js)$/.test(src)) {
|
|
12309
|
+
console.log(`
|
|
12310
|
+
⚠ running from source (${src}); the service needs the built bundle.`);
|
|
12311
|
+
console.log(` use \`npx hatchee install-service\` for a real install.
|
|
12312
|
+
`);
|
|
12313
|
+
return;
|
|
12314
|
+
}
|
|
12250
12315
|
if (process.platform === "darwin") {
|
|
12251
12316
|
const content = plistContent(node, STABLE_BIN);
|
|
12252
12317
|
if (print) {
|
|
@@ -12264,8 +12329,15 @@ ${content}
|
|
|
12264
12329
|
writeFileSync3(PLIST, content);
|
|
12265
12330
|
const uid = String(process.getuid?.() ?? "");
|
|
12266
12331
|
run("launchctl", ["bootout", `gui/${uid}/${LABEL}`]);
|
|
12267
|
-
run("launchctl", ["bootstrap", `gui/${uid}`, PLIST]);
|
|
12332
|
+
const ok = run("launchctl", ["bootstrap", `gui/${uid}`, PLIST]);
|
|
12268
12333
|
run("launchctl", ["enable", `gui/${uid}/${LABEL}`]);
|
|
12334
|
+
if (!ok) {
|
|
12335
|
+
console.log(`
|
|
12336
|
+
⚠ wrote ${PLIST} but launchctl bootstrap failed (see the message above).`);
|
|
12337
|
+
console.log(` try manually: launchctl bootstrap gui/${uid} ${PLIST}
|
|
12338
|
+
`);
|
|
12339
|
+
return;
|
|
12340
|
+
}
|
|
12269
12341
|
console.log(`
|
|
12270
12342
|
✓ Hatchee will now run in the background (launchd: ${LABEL})`);
|
|
12271
12343
|
console.log(` logs ${LOG}`);
|
|
@@ -12292,8 +12364,15 @@ ${content}
|
|
|
12292
12364
|
mkdirSync3(dirname(UNIT), { recursive: true });
|
|
12293
12365
|
writeFileSync3(UNIT, content);
|
|
12294
12366
|
run("systemctl", ["--user", "daemon-reload"]);
|
|
12295
|
-
run("systemctl", ["--user", "enable", "--now", "hatchee.service"]);
|
|
12367
|
+
const ok = run("systemctl", ["--user", "enable", "--now", "hatchee.service"]);
|
|
12296
12368
|
run("loginctl", ["enable-linger", process.env.USER ?? ""]);
|
|
12369
|
+
if (!ok) {
|
|
12370
|
+
console.log(`
|
|
12371
|
+
⚠ wrote ${UNIT} but \`systemctl --user enable --now\` failed (see above).`);
|
|
12372
|
+
console.log(` is the user systemd bus available? try manually: systemctl --user enable --now hatchee.service
|
|
12373
|
+
`);
|
|
12374
|
+
return;
|
|
12375
|
+
}
|
|
12297
12376
|
console.log(`
|
|
12298
12377
|
✓ Hatchee will now run in the background (systemd --user: hatchee.service)`);
|
|
12299
12378
|
console.log(` logs journalctl --user -u hatchee.service -f`);
|
|
@@ -12329,7 +12408,7 @@ function uninstallService() {
|
|
|
12329
12408
|
}
|
|
12330
12409
|
|
|
12331
12410
|
// src/cli.ts
|
|
12332
|
-
var VERSION2 = "0.1.
|
|
12411
|
+
var VERSION2 = "0.1.4";
|
|
12333
12412
|
var cmd = process.argv[2] ?? "help";
|
|
12334
12413
|
switch (cmd) {
|
|
12335
12414
|
case "up": {
|
|
@@ -12360,39 +12439,19 @@ switch (cmd) {
|
|
|
12360
12439
|
}
|
|
12361
12440
|
case "pair": {
|
|
12362
12441
|
const cfg = loadConfig();
|
|
12442
|
+
const remote = await remotePair(cfg.hookPort);
|
|
12443
|
+
if (remote) {
|
|
12444
|
+
printPairing(remote, true);
|
|
12445
|
+
break;
|
|
12446
|
+
}
|
|
12363
12447
|
const daemon = new Daemon(cfg);
|
|
12364
12448
|
if (!safeListen(daemon))
|
|
12365
12449
|
break;
|
|
12366
12450
|
const relay = new RelayClient(daemon, cfg.relayUrl, cfg.secretKey);
|
|
12367
12451
|
relay.start();
|
|
12368
12452
|
installHooks(hatcheeBin());
|
|
12369
|
-
const
|
|
12370
|
-
|
|
12371
|
-
const myPub = b64.encode(publicKey(b64.decode(cfg.secretKey)));
|
|
12372
|
-
const lan = lanIPv4();
|
|
12373
|
-
const payload = JSON.stringify({
|
|
12374
|
-
v: 1,
|
|
12375
|
-
...lan ? { ws: `ws://${lan}:${cfg.wsPort}` } : {},
|
|
12376
|
-
code,
|
|
12377
|
-
host: cfg.host,
|
|
12378
|
-
pk: myPub,
|
|
12379
|
-
relay: cfg.relayUrl,
|
|
12380
|
-
room: relay.room
|
|
12381
|
-
});
|
|
12382
|
-
banner();
|
|
12383
|
-
import_qrcode_terminal.default.generate(payload, { small: true }, (qr) => {
|
|
12384
|
-
console.log(qr);
|
|
12385
|
-
console.log(` scan with the Hatchee iOS app \u2014 or enter manually:`);
|
|
12386
|
-
if (lan)
|
|
12387
|
-
console.log(` LAN address ${lan}:${cfg.wsPort}`);
|
|
12388
|
-
else
|
|
12389
|
-
console.log(` LAN address no LAN address (VPN?) \u2014 phone will use the relay`);
|
|
12390
|
-
console.log(` relay ${cfg.relayUrl}/c/${relay.room}?role=phone`);
|
|
12391
|
-
console.log(` code ${code} (valid 5 minutes, single use)
|
|
12392
|
-
`);
|
|
12393
|
-
console.log(` keeping the daemon running after pairing\u2026 (^C to stop)`);
|
|
12394
|
-
tips();
|
|
12395
|
-
});
|
|
12453
|
+
const info = daemon.pairingPayload(process.env.DROVER_PAIR_CODE);
|
|
12454
|
+
printPairing(info, false);
|
|
12396
12455
|
break;
|
|
12397
12456
|
}
|
|
12398
12457
|
case "hook":
|
|
@@ -12459,6 +12518,42 @@ function banner() {
|
|
|
12459
12518
|
\uD83D\uDC23 hatchee ${VERSION2} \u2014 your coding agents, alive on your lock screen
|
|
12460
12519
|
`);
|
|
12461
12520
|
}
|
|
12521
|
+
async function remotePair(hookPort) {
|
|
12522
|
+
try {
|
|
12523
|
+
const r = await fetch(`http://127.0.0.1:${hookPort}/pair`, {
|
|
12524
|
+
method: "POST",
|
|
12525
|
+
headers: { "content-type": "application/json" },
|
|
12526
|
+
signal: AbortSignal.timeout(2500)
|
|
12527
|
+
});
|
|
12528
|
+
if (!r.ok)
|
|
12529
|
+
return null;
|
|
12530
|
+
const info = await r.json();
|
|
12531
|
+
return info?.payload ? info : null;
|
|
12532
|
+
} catch {
|
|
12533
|
+
return null;
|
|
12534
|
+
}
|
|
12535
|
+
}
|
|
12536
|
+
function printPairing(info, viaService) {
|
|
12537
|
+
banner();
|
|
12538
|
+
import_qrcode_terminal.default.generate(JSON.stringify(info.payload), { small: true }, (qr) => {
|
|
12539
|
+
console.log(qr);
|
|
12540
|
+
console.log(` scan with the Hatchee iOS app \u2014 or enter manually:`);
|
|
12541
|
+
if (info.lan)
|
|
12542
|
+
console.log(` LAN address ${info.lan}:${info.wsPort}`);
|
|
12543
|
+
else
|
|
12544
|
+
console.log(` LAN address no LAN address (VPN?) \u2014 phone will use the relay`);
|
|
12545
|
+
console.log(` relay ${info.relay}/c/${info.room}?role=phone`);
|
|
12546
|
+
console.log(` code ${info.code} (valid 5 minutes, single use)
|
|
12547
|
+
`);
|
|
12548
|
+
if (viaService)
|
|
12549
|
+
console.log(` paired against the Hatchee already running here \u2014 it keeps running.
|
|
12550
|
+
`);
|
|
12551
|
+
else {
|
|
12552
|
+
console.log(` keeping the daemon running after pairing\u2026 (^C to stop)`);
|
|
12553
|
+
tips();
|
|
12554
|
+
}
|
|
12555
|
+
});
|
|
12556
|
+
}
|
|
12462
12557
|
function tips() {
|
|
12463
12558
|
console.log(`
|
|
12464
12559
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
package/package.json
CHANGED