sakuraai 0.0.15 → 0.0.16

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/index.js +116 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4679,13 +4679,14 @@ function startTsnet(port2) {
4679
4679
  const bin = resolveBinary();
4680
4680
  if (!bin) {
4681
4681
  log.warn(
4682
- "embedded Tailscale is configured but the sakura-tsnet binary was not found \u2014 build it with modules/sakura-tailscale/scripts/build-sidecar.sh (LAN access still works)."
4682
+ "embedded Tailscale is configured but the sakura-tsnet binary was not found \u2014 reinstall with `npm i -g sakuraai@latest --foreground-scripts` (LAN access still works)."
4683
4683
  );
4684
4684
  return;
4685
4685
  }
4686
4686
  stopping = false;
4687
4687
  resetStateIfKeyChanged(ts.authKey);
4688
- spawnSidecar(bin, ts.controlUrl, ts.authKey, ts.hostname || cfg.machineName, port2);
4688
+ const fresh = !fs21.existsSync(path19.join(STATE_DIR, "tailscaled.state")) && !fs21.existsSync(JOINED_MARK);
4689
+ spawnSidecar(bin, ts.controlUrl, ts.authKey, ts.hostname || cfg.machineName, port2, fresh);
4689
4690
  }
4690
4691
  function resetStateIfKeyChanged(authKey) {
4691
4692
  const hash = crypto.createHash("sha256").update(authKey).digest("hex");
@@ -4701,7 +4702,7 @@ function resetStateIfKeyChanged(authKey) {
4701
4702
  fs21.mkdirSync(STATE_DIR, { recursive: true });
4702
4703
  fs21.writeFileSync(KEY_MARK, hash);
4703
4704
  }
4704
- function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
4705
+ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
4705
4706
  const env = {
4706
4707
  ...process.env,
4707
4708
  SAKURA_TS_CONTROL_URL: controlUrl,
@@ -4711,18 +4712,30 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
4711
4712
  SAKURA_TS_STATE_FILE: TSNET_PATH,
4712
4713
  SAKURA_TS_PORT: String(port2)
4713
4714
  };
4714
- log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})`);
4715
+ if (forceLogin) env.TSNET_FORCE_LOGIN = "1";
4716
+ log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})${forceLogin ? " with a forced login" : ""}`);
4715
4717
  const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
4716
4718
  child = c;
4717
- const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
4719
+ let joinFailed = false;
4720
+ const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => {
4721
+ log.info(`[tsnet] ${line}`);
4722
+ if (line.includes("failed to join tailnet")) joinFailed = true;
4723
+ if (line.includes("on tailnet as")) {
4724
+ try {
4725
+ fs21.writeFileSync(JOINED_MARK, (/* @__PURE__ */ new Date()).toISOString());
4726
+ } catch {
4727
+ }
4728
+ }
4729
+ });
4718
4730
  c.stdout?.on("data", pipe);
4719
4731
  c.stderr?.on("data", pipe);
4720
4732
  c.on("exit", (code) => {
4721
4733
  child = null;
4722
4734
  if (stopping) return;
4735
+ const force = forceLogin || joinFailed && !fs21.existsSync(JOINED_MARK);
4723
4736
  log.warn(`tsnet sidecar exited (code ${code}); restarting in 5s`);
4724
4737
  restartTimer = setTimeout(() => {
4725
- if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2);
4738
+ if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2, force);
4726
4739
  }, 5e3);
4727
4740
  });
4728
4741
  }
@@ -4744,7 +4757,7 @@ function stopTsnet() {
4744
4757
  } catch {
4745
4758
  }
4746
4759
  }
4747
- var child, stopping, restartTimer, STATE_DIR, KEY_MARK;
4760
+ var child, stopping, restartTimer, STATE_DIR, KEY_MARK, JOINED_MARK;
4748
4761
  var init_tsnet = __esm({
4749
4762
  "src/daemon/tsnet.ts"() {
4750
4763
  "use strict";
@@ -4755,6 +4768,7 @@ var init_tsnet = __esm({
4755
4768
  restartTimer = null;
4756
4769
  STATE_DIR = path19.join(SAKURA_DIR, "tsnet-state");
4757
4770
  KEY_MARK = path19.join(STATE_DIR, "joined-with");
4771
+ JOINED_MARK = path19.join(STATE_DIR, "joined");
4758
4772
  }
4759
4773
  });
4760
4774
 
@@ -4903,27 +4917,80 @@ var init_manager = __esm({
4903
4917
  // src/pairing.ts
4904
4918
  import os10 from "os";
4905
4919
  import qrcode from "qrcode-terminal";
4920
+ import fs23 from "fs";
4921
+ function recentTsnetLog(max = 6) {
4922
+ let text = "";
4923
+ try {
4924
+ const fd = fs23.openSync(LOG_PATH, "r");
4925
+ const size = fs23.fstatSync(fd).size;
4926
+ const len = Math.min(size, 64 * 1024);
4927
+ const buf = Buffer.alloc(len);
4928
+ fs23.readSync(fd, buf, 0, len, size - len);
4929
+ fs23.closeSync(fd);
4930
+ text = buf.toString("utf8");
4931
+ } catch {
4932
+ return [];
4933
+ }
4934
+ const lines2 = text.split("\n").filter((l) => /tsnet|sidecar|tailnet/i.test(l));
4935
+ const distinct = lines2.filter((l, i) => l !== lines2[i - 1]);
4936
+ return distinct.slice(-max);
4937
+ }
4938
+ function diagnoseTailnet(tsnet) {
4939
+ if (!resolveBinary()) {
4940
+ return {
4941
+ reason: "the sakura-tsnet binary is missing. It is downloaded when sakuraai installs, and that download failed or was skipped (bun/pnpm, or --ignore-scripts).",
4942
+ fix: "npm i -g sakuraai@latest --foreground-scripts && sakuraai daemon restart",
4943
+ log: []
4944
+ };
4945
+ }
4946
+ if (!tsnet) {
4947
+ return {
4948
+ reason: "the tailnet sidecar has not reported in. It may still be starting, or it failed to start.",
4949
+ fix: "sakuraai daemon restart, wait ~15s, then run sakuraai pair again",
4950
+ log: recentTsnetLog()
4951
+ };
4952
+ }
4953
+ return {
4954
+ reason: `the tailnet sidecar is stuck in state "${tsnet.backendState}" instead of Running.`,
4955
+ fix: "sakuraai daemon restart, wait ~15s, then run sakuraai pair again. If it stays stuck, sakuraai logout && sakuraai login for a fresh key",
4956
+ log: recentTsnetLog()
4957
+ };
4958
+ }
4906
4959
  async function buildPairing() {
4907
4960
  const token = requireToken();
4908
4961
  const cfg = loadConfig();
4909
4962
  const port2 = Number(process.env.SAKURA_PORT || cfg.daemon.port);
4910
4963
  const tsnet = readTsnetState();
4911
4964
  const tsnetUrl = tsnet?.tailscaleIp && tsnet.backendState === "Running" ? `http://${tsnet.tailscaleIp}:${port2}` : null;
4912
- const cliTailnet = tsnetUrl ? null : await discoverBaseUrl(port2).catch(() => null);
4965
+ const ts = cfg.tailscale;
4966
+ const configured = !!(ts?.controlUrl && ts?.authKey);
4967
+ const cliTailnet = tsnetUrl || configured ? null : await discoverBaseUrl(port2).catch(() => null);
4913
4968
  const url = tsnetUrl ?? cliTailnet ?? `http://${localIp()}:${port2}`;
4914
4969
  const params = /* @__PURE__ */ new Map([
4915
4970
  ["url", url],
4916
4971
  ["token", token]
4917
4972
  ]);
4918
- const ts = cfg.tailscale;
4919
- const embeddedTailscale = !!(ts?.controlUrl && ts?.authKey);
4973
+ const embeddedTailscale = configured && !!tsnetUrl;
4920
4974
  if (embeddedTailscale) {
4921
4975
  params.set("cn", ts.controlUrl);
4922
4976
  params.set("tskey", ts.authKey);
4923
4977
  }
4924
4978
  const query = [...params].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
4925
4979
  const deepLink = `sakura://pair?${query}`;
4926
- return { url, token, deepLink, embeddedTailscale };
4980
+ const tailnetProblem = configured && !tsnetUrl ? diagnoseTailnet(tsnet) : void 0;
4981
+ return { url, token, deepLink, embeddedTailscale, tailnetProblem };
4982
+ }
4983
+ function tunnelUp() {
4984
+ const ts = readTsnetState();
4985
+ return !!(ts?.tailscaleIp && ts.backendState === "Running");
4986
+ }
4987
+ async function waitForTunnel(timeoutMs) {
4988
+ const deadline = Date.now() + timeoutMs;
4989
+ while (Date.now() < deadline) {
4990
+ if (tunnelUp()) return true;
4991
+ await new Promise((r) => setTimeout(r, 500));
4992
+ }
4993
+ return tunnelUp();
4927
4994
  }
4928
4995
  function renderQr(deepLink) {
4929
4996
  return new Promise((resolve) => {
@@ -4945,6 +5012,7 @@ var init_pairing = __esm({
4945
5012
  init_config();
4946
5013
  init_auth();
4947
5014
  init_tailscale();
5015
+ init_tsnet();
4948
5016
  }
4949
5017
  });
4950
5018
 
@@ -4954,10 +5022,22 @@ __export(pair_exports, {
4954
5022
  registerPair: () => registerPair,
4955
5023
  showPairing: () => showPairing
4956
5024
  });
5025
+ import chalk2 from "chalk";
4957
5026
  async function showPairing(opts = {}) {
4958
5027
  const pairing = await buildPairing();
4959
5028
  if (opts.json) return printJson(pairing);
4960
5029
  const qr = await renderQr(pairing.deepLink);
5030
+ const problem = pairing.tailnetProblem;
5031
+ if (problem) {
5032
+ info("");
5033
+ warn(chalk2.bold("This Mac is not on Sakura\u2019s private tailnet, so this QR only works on the same Wi\u2011Fi."));
5034
+ info(` Why: ${problem.reason}`);
5035
+ info(` Fix: ${chalk2.cyan(problem.fix)}`);
5036
+ if (problem.log.length) {
5037
+ info(" Recent tailnet log:");
5038
+ problem.log.forEach((l) => info(chalk2.dim(` ${l}`)));
5039
+ }
5040
+ }
4961
5041
  info("");
4962
5042
  info("Scan this with your phone to pair the Sakura app:");
4963
5043
  info("");
@@ -4984,6 +5064,9 @@ function registerPair(program) {
4984
5064
  }
4985
5065
  if (!running()) {
4986
5066
  warn("The daemon is not running \u2014 start it with `sakuraai daemon start` so the app can connect.");
5067
+ } else if (loadConfig().tailscale?.authKey && !tunnelUp()) {
5068
+ info("Waiting for Sakura\u2019s private tunnel (up to 90s)\u2026");
5069
+ await waitForTunnel(9e4);
4987
5070
  }
4988
5071
  await showPairing(opts);
4989
5072
  success("Waiting for the app to connect\u2026");
@@ -4993,6 +5076,7 @@ var init_pair = __esm({
4993
5076
  "src/commands/pair.ts"() {
4994
5077
  "use strict";
4995
5078
  init_pairing();
5079
+ init_config();
4996
5080
  init_auth();
4997
5081
  init_manager();
4998
5082
  init_output();
@@ -5003,7 +5087,7 @@ var init_pair = __esm({
5003
5087
  import { Command } from "commander";
5004
5088
 
5005
5089
  // src/version.ts
5006
- var VERSION = "0.0.15";
5090
+ var VERSION = "0.0.16";
5007
5091
 
5008
5092
  // src/index.ts
5009
5093
  init_config();
@@ -5066,23 +5150,12 @@ init_tailscale();
5066
5150
  init_auth();
5067
5151
  init_config();
5068
5152
  init_output();
5069
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
5153
+ init_pairing();
5070
5154
  async function tunnelUrl(port2) {
5071
5155
  const ts = readTsnetState();
5072
5156
  if (ts?.tailscaleIp && ts.backendState === "Running") return `http://${ts.tailscaleIp}:${port2}`;
5073
5157
  return await discoverBaseUrl(port2).catch(() => null);
5074
5158
  }
5075
- async function waitForTunnel(timeoutMs) {
5076
- if (!getServerToken()) return false;
5077
- const deadline = Date.now() + timeoutMs;
5078
- while (Date.now() < deadline) {
5079
- const cfg = loadConfig();
5080
- const ts = readTsnetState();
5081
- if (cfg.tailscale?.controlUrl && ts?.tailscaleIp && ts.backendState === "Running") return true;
5082
- await sleep2(500);
5083
- }
5084
- return false;
5085
- }
5086
5159
  function registerRuntime(program, version) {
5087
5160
  program.command("start").description("Run the Sakura runtime in this terminal (foreground)").option("--auth <token>", "sign in with a CLI token before starting").action(async (opts) => {
5088
5161
  if (opts.auth) {
@@ -5099,11 +5172,11 @@ function registerRuntime(program, version) {
5099
5172
  if (!isLoggedIn()) die("Not signed in. Run `sakuraai login` first.");
5100
5173
  const info0 = start();
5101
5174
  success(`Daemon running (pid ${info0.pid}) on ${info0.url}`);
5102
- const ready = await waitForTunnel(2e4);
5103
- if (ready) {
5104
- info("Embedded Tailscale tunnel up \u2014 the phone can connect from any network, no Tailscale app needed.");
5105
- } else if (getServerToken()) {
5106
- warn("Tunnel not up yet \u2014 pairing over LAN for now. Re-run `sakuraai pair` in a moment for the private URL.");
5175
+ if (getServerToken() && loadConfig().tailscale?.controlUrl) {
5176
+ info("Waiting for Sakura\u2019s private tunnel (up to 90s)\u2026");
5177
+ if (await waitForTunnel(9e4)) {
5178
+ info("Embedded Tailscale tunnel up \u2014 the phone can connect from any network, no Tailscale app needed.");
5179
+ }
5107
5180
  }
5108
5181
  const { showPairing: showPairing2 } = await Promise.resolve().then(() => (init_pair(), pair_exports));
5109
5182
  await showPairing2();
@@ -5132,7 +5205,7 @@ function registerRuntime(program, version) {
5132
5205
  // src/commands/session.ts
5133
5206
  init_sessions();
5134
5207
  init_output();
5135
- import fs23 from "fs";
5208
+ import fs24 from "fs";
5136
5209
  function resolveSessionId(arg) {
5137
5210
  const id = arg || process.env.SAKURA_SESSION_ID;
5138
5211
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -5141,7 +5214,7 @@ function resolveSessionId(arg) {
5141
5214
  async function resolvePrompt(positional, opts) {
5142
5215
  if (positional) return positional;
5143
5216
  if (opts.prompt) return opts.prompt;
5144
- if (opts.promptFile) return fs23.readFileSync(opts.promptFile, "utf8");
5217
+ if (opts.promptFile) return fs24.readFileSync(opts.promptFile, "utf8");
5145
5218
  if (!process.stdin.isTTY) {
5146
5219
  const chunks = [];
5147
5220
  for await (const c of process.stdin) chunks.push(c);
@@ -5456,14 +5529,14 @@ init_pair();
5456
5529
 
5457
5530
  // src/commands/welcome.ts
5458
5531
  init_config();
5459
- import fs24 from "fs";
5532
+ import fs25 from "fs";
5460
5533
  import path20 from "path";
5461
5534
  import { fileURLToPath as fileURLToPath2 } from "url";
5462
5535
  import { spawn as spawn13 } from "child_process";
5463
5536
  var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
5464
5537
  function resolveBinary2() {
5465
5538
  const envBin = process.env.SAKURA_ONBOARD_BIN;
5466
- if (envBin && fs24.existsSync(envBin)) return envBin;
5539
+ if (envBin && fs25.existsSync(envBin)) return envBin;
5467
5540
  const here = path20.dirname(fileURLToPath2(import.meta.url));
5468
5541
  const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
5469
5542
  const candidates = [
@@ -5474,7 +5547,7 @@ function resolveBinary2() {
5474
5547
  path20.join(SAKURA_DIR, name)
5475
5548
  // user-dropped
5476
5549
  ];
5477
- return candidates.find((p) => fs24.existsSync(p)) ?? null;
5550
+ return candidates.find((p) => fs25.existsSync(p)) ?? null;
5478
5551
  }
5479
5552
  function showWelcome() {
5480
5553
  return new Promise((resolve) => {
@@ -5492,10 +5565,10 @@ function showWelcome() {
5492
5565
  });
5493
5566
  }
5494
5567
  async function maybeFirstRunWelcome() {
5495
- if (fs24.existsSync(WELCOMED_MARKER)) return;
5568
+ if (fs25.existsSync(WELCOMED_MARKER)) return;
5496
5569
  if (!process.stdout.isTTY || !process.stdin.isTTY) return;
5497
5570
  try {
5498
- fs24.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
5571
+ fs25.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
5499
5572
  } catch {
5500
5573
  }
5501
5574
  await showWelcome();
@@ -5533,7 +5606,7 @@ function registerWelcome(program) {
5533
5606
  }
5534
5607
 
5535
5608
  // src/commands/app.ts
5536
- import fs25 from "fs";
5609
+ import fs26 from "fs";
5537
5610
  import os11 from "os";
5538
5611
  import path21 from "path";
5539
5612
  import { execFileSync as execFileSync4 } from "child_process";
@@ -5546,11 +5619,11 @@ function dmgUrl() {
5546
5619
  async function download(url, dest) {
5547
5620
  const res = await fetch(url, { redirect: "follow" });
5548
5621
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
5549
- fs25.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
5622
+ fs26.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
5550
5623
  }
5551
5624
  async function install() {
5552
5625
  if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
5553
- const tmp = fs25.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
5626
+ const tmp = fs26.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
5554
5627
  const dmg = path21.join(tmp, "Sakura.dmg");
5555
5628
  const mount = path21.join(tmp, "mnt");
5556
5629
  info(`Downloading Sakura.app ${VERSION}\u2026`);
@@ -5560,18 +5633,18 @@ async function install() {
5560
5633
  die(`${e.message}
5561
5634
  Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
5562
5635
  }
5563
- fs25.mkdirSync(mount);
5636
+ fs26.mkdirSync(mount);
5564
5637
  execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
5565
5638
  try {
5566
5639
  try {
5567
5640
  execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
5568
5641
  } catch {
5569
5642
  }
5570
- fs25.rmSync(APP_PATH, { recursive: true, force: true });
5643
+ fs26.rmSync(APP_PATH, { recursive: true, force: true });
5571
5644
  execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
5572
5645
  } finally {
5573
5646
  execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
5574
- fs25.rmSync(tmp, { recursive: true, force: true });
5647
+ fs26.rmSync(tmp, { recursive: true, force: true });
5575
5648
  }
5576
5649
  execFileSync4("open", [APP_PATH]);
5577
5650
  success("Sakura is in your menu bar (look for the blossom).");
@@ -5580,7 +5653,7 @@ function registerApp(program) {
5580
5653
  const app = program.command("app").description("The optional Sakura menu bar app for macOS");
5581
5654
  app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
5582
5655
  app.command("open").description("Open the Sakura menu bar app").action(() => {
5583
- if (!fs25.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
5656
+ if (!fs26.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
5584
5657
  execFileSync4("open", [APP_PATH]);
5585
5658
  });
5586
5659
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sakuraai",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "description": "Sakura Agent CLI + local runtime for managing AI coding sessions (Claude Code, Codex, OpenCode, ZCode, DeepSeek) and reaching them privately from the Sakura mobile app over Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",