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.
Files changed (2) hide show
  1. package/dist/cli.mjs +143 -48
  2. 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
- if (req.method !== "POST" || (req.url ?? "") !== "/hook") {
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 b64url(publicKey(b64.decode(this.secretKeyB64)));
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 b64url(bytes) {
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
- return typeof x?.command === "string" && /\bhook'?\s*$/.test(x.command.trimEnd()) && (x.command.includes("hatchee") || x.command.includes("cli.ts") || x.command.includes("cli.mjs") || x.command.includes("drover"));
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", () => resolve(data));
12134
- setTimeout(() => resolve(data), 2000);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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=${node} ${bin} up
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.2";
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 code = process.env.DROVER_PAIR_CODE ?? newPairingCode();
12370
- daemon.openPairingWindow(code);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hatchee",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Your coding agents, alive on your iPhone lock screen — Hatchee daemon. Approve Claude Code / Codex from your phone.",
5
5
  "type": "module",
6
6
  "bin": { "hatchee": "dist/cli.mjs" },