sakuraai 0.0.12 → 0.0.13
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/README.md +15 -2
- package/dist/index.js +221 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# sakuraai
|
|
2
2
|
|
|
3
3
|
CLI + local runtime for the [Sakura](https://sakura.mom) app. Manage your local
|
|
4
|
-
AI coding sessions (Claude Code, Codex, OpenCode) and reach them
|
|
5
|
-
over Tailscale.
|
|
4
|
+
AI coding sessions (Claude Code, Codex, OpenCode, ZCode, DeepSeek) and reach them
|
|
5
|
+
from your phone over Tailscale.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm i -g sakuraai
|
|
@@ -12,4 +12,17 @@ sakuraai daemon start # start the runtime
|
|
|
12
12
|
sakuraai session list # list sessions across all agents
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
### Menu bar app (optional, macOS)
|
|
16
|
+
|
|
17
|
+
Prefer a GUI? `sakuraai app install` adds **Sakura** to your menu bar: daemon
|
|
18
|
+
status, a pairing QR for your phone, and **Stay awake** sessions (timer, until a
|
|
19
|
+
time, or unlimited) — including **Closed lid** mode, so you can keep driving your
|
|
20
|
+
agents from your phone with the MacBook shut. Closed lid asks for your password,
|
|
21
|
+
needs the charger by default, and puts sleep back when the session ends.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
sakuraai app install # download Sakura.app into /Applications and open it
|
|
25
|
+
sakuraai app open
|
|
26
|
+
```
|
|
27
|
+
|
|
15
28
|
Run `sakuraai --help` for all commands. More at **https://sakura.mom**.
|
package/dist/index.js
CHANGED
|
@@ -103,8 +103,8 @@ var init_config = __esm({
|
|
|
103
103
|
function apiBase() {
|
|
104
104
|
return (loadAuth().serverUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
105
105
|
}
|
|
106
|
-
async function postJson(base,
|
|
107
|
-
const res = await fetch(`${base}${
|
|
106
|
+
async function postJson(base, path18, body, token) {
|
|
107
|
+
const res = await fetch(`${base}${path18}`, {
|
|
108
108
|
method: "POST",
|
|
109
109
|
headers: {
|
|
110
110
|
"Content-Type": "application/json",
|
|
@@ -114,7 +114,7 @@ async function postJson(base, path16, body, token) {
|
|
|
114
114
|
});
|
|
115
115
|
if (!res.ok) {
|
|
116
116
|
const text = await res.text().catch(() => "");
|
|
117
|
-
throw new Error(`${
|
|
117
|
+
throw new Error(`${path18} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
|
|
118
118
|
}
|
|
119
119
|
return await res.json();
|
|
120
120
|
}
|
|
@@ -2925,8 +2925,13 @@ var init_hub = __esm({
|
|
|
2925
2925
|
});
|
|
2926
2926
|
|
|
2927
2927
|
// src/daemon/stream.ts
|
|
2928
|
+
function turnState(agent, sessionId) {
|
|
2929
|
+
return turns.get(sessionKey(agent, sessionId)) ?? null;
|
|
2930
|
+
}
|
|
2928
2931
|
async function runTurn(sessionId, agent, prompt, images, model) {
|
|
2929
2932
|
const key = sessionKey(agent, sessionId);
|
|
2933
|
+
const turn = { status: "running", startedAt: Date.now() };
|
|
2934
|
+
turns.set(key, turn);
|
|
2930
2935
|
const emit = (e) => hub.broadcast(key, e);
|
|
2931
2936
|
let lastEmitted = "\0";
|
|
2932
2937
|
const emitLive = (text) => {
|
|
@@ -2987,11 +2992,22 @@ async function runTurn(sessionId, agent, prompt, images, model) {
|
|
|
2987
2992
|
}, POLL_MS);
|
|
2988
2993
|
const result = await chat(sessionId, prompt, agent, onData, images, model);
|
|
2989
2994
|
clearInterval(timer);
|
|
2990
|
-
|
|
2995
|
+
turn.status = result.ok ? "done" : "error";
|
|
2996
|
+
turn.finishedAt = Date.now();
|
|
2997
|
+
turn.error = result.error;
|
|
2998
|
+
emit({
|
|
2999
|
+
type: "done",
|
|
3000
|
+
sessionId,
|
|
3001
|
+
agent,
|
|
3002
|
+
ok: result.ok,
|
|
3003
|
+
error: result.error,
|
|
3004
|
+
startedAt: turn.startedAt,
|
|
3005
|
+
finishedAt: turn.finishedAt
|
|
3006
|
+
});
|
|
2991
3007
|
notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
|
|
2992
3008
|
return { ok: result.ok, error: result.error };
|
|
2993
3009
|
}
|
|
2994
|
-
var ANSI, stripAnsi, LIVE_ID, POLL_MS;
|
|
3010
|
+
var ANSI, stripAnsi, LIVE_ID, POLL_MS, turns;
|
|
2995
3011
|
var init_stream = __esm({
|
|
2996
3012
|
"src/daemon/stream.ts"() {
|
|
2997
3013
|
"use strict";
|
|
@@ -3002,6 +3018,77 @@ var init_stream = __esm({
|
|
|
3002
3018
|
stripAnsi = (s) => s.replace(ANSI, "");
|
|
3003
3019
|
LIVE_ID = "__live__";
|
|
3004
3020
|
POLL_MS = 300;
|
|
3021
|
+
turns = /* @__PURE__ */ new Map();
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
// src/runtime/presence.ts
|
|
3026
|
+
import fs15 from "fs";
|
|
3027
|
+
import path13 from "path";
|
|
3028
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3029
|
+
function alive(pid) {
|
|
3030
|
+
try {
|
|
3031
|
+
process.kill(pid, 0);
|
|
3032
|
+
return true;
|
|
3033
|
+
} catch (e) {
|
|
3034
|
+
return e?.code === "EPERM";
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function claudePresence(sessionId) {
|
|
3038
|
+
const dir = path13.join(HOME, ".claude", "sessions");
|
|
3039
|
+
let files = [];
|
|
3040
|
+
try {
|
|
3041
|
+
files = fs15.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
3042
|
+
} catch {
|
|
3043
|
+
return { open: false };
|
|
3044
|
+
}
|
|
3045
|
+
for (const f of files) {
|
|
3046
|
+
try {
|
|
3047
|
+
const rec = JSON.parse(fs15.readFileSync(path13.join(dir, f), "utf8"));
|
|
3048
|
+
if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
|
|
3049
|
+
if (rec.kind && rec.kind !== "interactive") continue;
|
|
3050
|
+
if (!alive(rec.pid)) continue;
|
|
3051
|
+
const where = rec.entrypoint === "cli" ? "Claude Code in a terminal" : "Claude Code";
|
|
3052
|
+
return { open: true, where, pid: rec.pid };
|
|
3053
|
+
} catch {
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return { open: false };
|
|
3057
|
+
}
|
|
3058
|
+
function processPresence(agent, sessionId) {
|
|
3059
|
+
const program = AGENT_PROGRAM[agent];
|
|
3060
|
+
if (!program) return { open: false };
|
|
3061
|
+
let out = "";
|
|
3062
|
+
try {
|
|
3063
|
+
out = execFileSync2("ps", ["-axo", "pid=,command="], { encoding: "utf8", timeout: 2e3 });
|
|
3064
|
+
} catch {
|
|
3065
|
+
return { open: false };
|
|
3066
|
+
}
|
|
3067
|
+
for (const line of out.split("\n")) {
|
|
3068
|
+
if (!line.includes(sessionId)) continue;
|
|
3069
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
3070
|
+
if (!m) continue;
|
|
3071
|
+
const pid = Number(m[1]);
|
|
3072
|
+
if (pid === process.pid || !program.test(m[2])) continue;
|
|
3073
|
+
return { open: true, where: agent, pid };
|
|
3074
|
+
}
|
|
3075
|
+
return { open: false };
|
|
3076
|
+
}
|
|
3077
|
+
function presence(agent, sessionId) {
|
|
3078
|
+
if (!sessionId) return { open: false };
|
|
3079
|
+
return agent === "claude" ? claudePresence(sessionId) : processPresence(agent, sessionId);
|
|
3080
|
+
}
|
|
3081
|
+
var AGENT_PROGRAM;
|
|
3082
|
+
var init_presence = __esm({
|
|
3083
|
+
"src/runtime/presence.ts"() {
|
|
3084
|
+
"use strict";
|
|
3085
|
+
init_util();
|
|
3086
|
+
AGENT_PROGRAM = {
|
|
3087
|
+
codex: /(^|\/)codex(\s|$)/,
|
|
3088
|
+
opencode: /(^|\/)opencode(-cli)?(\s|$)/,
|
|
3089
|
+
zcode: /(^|\/)zcode(\.cjs)?(\s|$)/,
|
|
3090
|
+
deepseek: /(^|\/)dsh(\s|$)|dsh-desktop-host/
|
|
3091
|
+
};
|
|
3005
3092
|
}
|
|
3006
3093
|
});
|
|
3007
3094
|
|
|
@@ -3093,17 +3180,17 @@ var init_terminal = __esm({
|
|
|
3093
3180
|
|
|
3094
3181
|
// src/daemon/server.ts
|
|
3095
3182
|
import http from "http";
|
|
3096
|
-
import
|
|
3183
|
+
import fs16 from "fs";
|
|
3097
3184
|
import os8 from "os";
|
|
3098
3185
|
import zlib2 from "zlib";
|
|
3099
|
-
import
|
|
3186
|
+
import path14 from "path";
|
|
3100
3187
|
import { URL } from "url";
|
|
3101
|
-
import { execFileSync as
|
|
3188
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3102
3189
|
import { WebSocketServer } from "ws";
|
|
3103
|
-
function add2(method,
|
|
3190
|
+
function add2(method, path18, handler) {
|
|
3104
3191
|
const keys = [];
|
|
3105
3192
|
const pattern = new RegExp(
|
|
3106
|
-
"^" +
|
|
3193
|
+
"^" + path18.replace(/:[^/]+/g, (m) => {
|
|
3107
3194
|
keys.push(m.slice(1));
|
|
3108
3195
|
return "([^/]+)";
|
|
3109
3196
|
}) + "/?$"
|
|
@@ -3126,8 +3213,8 @@ function sniffExt(buf) {
|
|
|
3126
3213
|
return null;
|
|
3127
3214
|
}
|
|
3128
3215
|
function saveImages(sessionId, images) {
|
|
3129
|
-
const dir =
|
|
3130
|
-
|
|
3216
|
+
const dir = path14.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
3217
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
3131
3218
|
const paths = [];
|
|
3132
3219
|
images.forEach((img, i) => {
|
|
3133
3220
|
const raw = String(img?.data ?? "");
|
|
@@ -3138,15 +3225,15 @@ function saveImages(sessionId, images) {
|
|
|
3138
3225
|
}
|
|
3139
3226
|
const bytes = Buffer.from(m[2], "base64");
|
|
3140
3227
|
const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
|
|
3141
|
-
const base = img.name ?
|
|
3228
|
+
const base = img.name ? path14.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
|
|
3142
3229
|
const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
|
|
3143
|
-
let file =
|
|
3144
|
-
|
|
3230
|
+
let file = path14.join(dir, `${stem}.${ext}`);
|
|
3231
|
+
fs16.writeFileSync(file, bytes);
|
|
3145
3232
|
if (ext === "heic") {
|
|
3146
|
-
const jpg =
|
|
3233
|
+
const jpg = path14.join(dir, `${stem}.jpg`);
|
|
3147
3234
|
try {
|
|
3148
|
-
|
|
3149
|
-
|
|
3235
|
+
execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
|
|
3236
|
+
fs16.rmSync(file, { force: true });
|
|
3150
3237
|
file = jpg;
|
|
3151
3238
|
} catch {
|
|
3152
3239
|
log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
|
|
@@ -3321,6 +3408,7 @@ var init_server = __esm({
|
|
|
3321
3408
|
init_zcode();
|
|
3322
3409
|
init_hub();
|
|
3323
3410
|
init_stream();
|
|
3411
|
+
init_presence();
|
|
3324
3412
|
init_terminal();
|
|
3325
3413
|
routes = [];
|
|
3326
3414
|
EXT_BY_MIME = {
|
|
@@ -3340,13 +3428,13 @@ var init_server = __esm({
|
|
|
3340
3428
|
const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
|
|
3341
3429
|
sendBytes(res, reader ? await reader(params.session, params.part) : null);
|
|
3342
3430
|
});
|
|
3343
|
-
UPLOADS_DIR =
|
|
3431
|
+
UPLOADS_DIR = path14.join(os8.tmpdir(), "sakura-uploads");
|
|
3344
3432
|
add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
|
|
3345
|
-
const file =
|
|
3346
|
-
const ext =
|
|
3433
|
+
const file = path14.resolve(UPLOADS_DIR, params.dir, params.file);
|
|
3434
|
+
const ext = path14.extname(file).slice(1).toLowerCase();
|
|
3347
3435
|
const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
|
|
3348
|
-
const ok2 = file.startsWith(UPLOADS_DIR +
|
|
3349
|
-
sendBytes(res, ok2 ? { mediaType: mime, data:
|
|
3436
|
+
const ok2 = file.startsWith(UPLOADS_DIR + path14.sep) && mime && fs16.existsSync(file);
|
|
3437
|
+
sendBytes(res, ok2 ? { mediaType: mime, data: fs16.readFileSync(file) } : null);
|
|
3350
3438
|
});
|
|
3351
3439
|
add2("GET", "/sessions", async ({ url }) => {
|
|
3352
3440
|
const archived = url.searchParams.get("archived") === "true";
|
|
@@ -3371,6 +3459,13 @@ var init_server = __esm({
|
|
|
3371
3459
|
const r = await runTurn(params.id, agent, message, imagePaths, model);
|
|
3372
3460
|
return { ok: r.ok, error: r.error };
|
|
3373
3461
|
});
|
|
3462
|
+
add2("GET", "/sessions/:id/status", async ({ params, url }) => {
|
|
3463
|
+
const agent = url.searchParams.get("agent") || await detectAgent(params.id);
|
|
3464
|
+
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
3465
|
+
const turn = turnState(agent, params.id);
|
|
3466
|
+
const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
|
|
3467
|
+
return { ok: true, agent, turn, openOnMac: elsewhere };
|
|
3468
|
+
});
|
|
3374
3469
|
add2("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
3375
3470
|
const title = String(body?.title ?? "").trim();
|
|
3376
3471
|
if (!title) return { ok: false, error: "title is required" };
|
|
@@ -3407,7 +3502,7 @@ var init_server = __esm({
|
|
|
3407
3502
|
case "logs": {
|
|
3408
3503
|
let logs2 = "";
|
|
3409
3504
|
try {
|
|
3410
|
-
const lines2 =
|
|
3505
|
+
const lines2 = fs16.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3411
3506
|
logs2 = lines2.slice(-200).join("\n");
|
|
3412
3507
|
} catch {
|
|
3413
3508
|
}
|
|
@@ -3459,7 +3554,14 @@ var init_server = __esm({
|
|
|
3459
3554
|
return ok(listModels(agent));
|
|
3460
3555
|
});
|
|
3461
3556
|
add2("POST", "/sakura/push-token", ({ body }) => {
|
|
3462
|
-
|
|
3557
|
+
let b = body;
|
|
3558
|
+
if (typeof b === "string") {
|
|
3559
|
+
try {
|
|
3560
|
+
b = JSON.parse(b);
|
|
3561
|
+
} catch {
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
const token = String(b?.token ?? "");
|
|
3463
3565
|
if (!token) return fail("bad_request", "token required");
|
|
3464
3566
|
addPushToken(token);
|
|
3465
3567
|
return ok({ registered: true });
|
|
@@ -3613,24 +3715,24 @@ var init_server = __esm({
|
|
|
3613
3715
|
});
|
|
3614
3716
|
|
|
3615
3717
|
// src/daemon/tsnet.ts
|
|
3616
|
-
import
|
|
3617
|
-
import
|
|
3718
|
+
import fs17 from "fs";
|
|
3719
|
+
import path15 from "path";
|
|
3618
3720
|
import { fileURLToPath } from "url";
|
|
3619
3721
|
import { spawn as spawn10 } from "child_process";
|
|
3620
3722
|
function resolveBinary() {
|
|
3621
3723
|
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
3622
|
-
if (envBin &&
|
|
3623
|
-
const here =
|
|
3724
|
+
if (envBin && fs17.existsSync(envBin)) return envBin;
|
|
3725
|
+
const here = path15.dirname(fileURLToPath(import.meta.url));
|
|
3624
3726
|
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
3625
3727
|
const candidates = [
|
|
3626
|
-
|
|
3728
|
+
path15.join(here, "..", "bin", name),
|
|
3627
3729
|
// package/bin (postinstall download / build-sidecar.sh)
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3730
|
+
path15.join(here, "bin", name),
|
|
3731
|
+
path15.join(process.cwd(), "bin", name),
|
|
3732
|
+
path15.join(SAKURA_DIR, name)
|
|
3631
3733
|
// user-dropped
|
|
3632
3734
|
];
|
|
3633
|
-
return candidates.find((p) =>
|
|
3735
|
+
return candidates.find((p) => fs17.existsSync(p)) ?? null;
|
|
3634
3736
|
}
|
|
3635
3737
|
function startTsnet(port2) {
|
|
3636
3738
|
const cfg = loadConfig();
|
|
@@ -3653,11 +3755,11 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
|
3653
3755
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
3654
3756
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
3655
3757
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
3656
|
-
SAKURA_TS_STATE_DIR:
|
|
3758
|
+
SAKURA_TS_STATE_DIR: path15.join(SAKURA_DIR, "tsnet-state"),
|
|
3657
3759
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
3658
3760
|
SAKURA_TS_PORT: String(port2)
|
|
3659
3761
|
};
|
|
3660
|
-
log.info(`starting embedded Tailscale sidecar (${
|
|
3762
|
+
log.info(`starting embedded Tailscale sidecar (${path15.basename(bin)})`);
|
|
3661
3763
|
const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
3662
3764
|
child = c;
|
|
3663
3765
|
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
@@ -3686,7 +3788,7 @@ function stopTsnet() {
|
|
|
3686
3788
|
child = null;
|
|
3687
3789
|
}
|
|
3688
3790
|
try {
|
|
3689
|
-
|
|
3791
|
+
fs17.rmSync(TSNET_PATH, { force: true });
|
|
3690
3792
|
} catch {
|
|
3691
3793
|
}
|
|
3692
3794
|
}
|
|
@@ -3703,7 +3805,7 @@ var init_tsnet = __esm({
|
|
|
3703
3805
|
});
|
|
3704
3806
|
|
|
3705
3807
|
// src/daemon/manager.ts
|
|
3706
|
-
import
|
|
3808
|
+
import fs18 from "fs";
|
|
3707
3809
|
import { spawn as spawn11 } from "child_process";
|
|
3708
3810
|
async function ensureTailnetProvisioned() {
|
|
3709
3811
|
const serverToken = getServerToken();
|
|
@@ -3726,7 +3828,7 @@ async function ensureTailnetProvisioned() {
|
|
|
3726
3828
|
}
|
|
3727
3829
|
function readDaemonInfo() {
|
|
3728
3830
|
try {
|
|
3729
|
-
return JSON.parse(
|
|
3831
|
+
return JSON.parse(fs18.readFileSync(DAEMON_PATH, "utf8"));
|
|
3730
3832
|
} catch {
|
|
3731
3833
|
return null;
|
|
3732
3834
|
}
|
|
@@ -3743,8 +3845,8 @@ function running() {
|
|
|
3743
3845
|
const info2 = readDaemonInfo();
|
|
3744
3846
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
3745
3847
|
if (info2) {
|
|
3746
|
-
|
|
3747
|
-
|
|
3848
|
+
fs18.rmSync(DAEMON_PATH, { force: true });
|
|
3849
|
+
fs18.rmSync(PID_PATH, { force: true });
|
|
3748
3850
|
}
|
|
3749
3851
|
return null;
|
|
3750
3852
|
}
|
|
@@ -3767,8 +3869,8 @@ async function runServer(version) {
|
|
|
3767
3869
|
url: `http://${host}:${port2}`,
|
|
3768
3870
|
startedAt: Date.now()
|
|
3769
3871
|
};
|
|
3770
|
-
|
|
3771
|
-
|
|
3872
|
+
fs18.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
3873
|
+
fs18.writeFileSync(PID_PATH, String(process.pid));
|
|
3772
3874
|
await ensureTailnetProvisioned();
|
|
3773
3875
|
startTsnet(port2);
|
|
3774
3876
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
@@ -3779,8 +3881,8 @@ async function runServer(version) {
|
|
|
3779
3881
|
log.info("shutting down");
|
|
3780
3882
|
stopTsnet();
|
|
3781
3883
|
server.close();
|
|
3782
|
-
|
|
3783
|
-
|
|
3884
|
+
fs18.rmSync(DAEMON_PATH, { force: true });
|
|
3885
|
+
fs18.rmSync(PID_PATH, { force: true });
|
|
3784
3886
|
process.exit(0);
|
|
3785
3887
|
};
|
|
3786
3888
|
process.on("SIGINT", shutdown);
|
|
@@ -3791,8 +3893,8 @@ function start() {
|
|
|
3791
3893
|
if (existing) return existing;
|
|
3792
3894
|
ensureSakuraDirs();
|
|
3793
3895
|
const entry = process.argv[1] ?? "";
|
|
3794
|
-
const out =
|
|
3795
|
-
const err =
|
|
3896
|
+
const out = fs18.openSync(LOG_PATH, "a");
|
|
3897
|
+
const err = fs18.openSync(LOG_PATH, "a");
|
|
3796
3898
|
const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
|
|
3797
3899
|
detached: true,
|
|
3798
3900
|
stdio: ["ignore", out, err],
|
|
@@ -3814,8 +3916,8 @@ function stop() {
|
|
|
3814
3916
|
process.kill(info2.pid, "SIGTERM");
|
|
3815
3917
|
} catch {
|
|
3816
3918
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3919
|
+
fs18.rmSync(DAEMON_PATH, { force: true });
|
|
3920
|
+
fs18.rmSync(PID_PATH, { force: true });
|
|
3819
3921
|
return true;
|
|
3820
3922
|
}
|
|
3821
3923
|
function restart() {
|
|
@@ -3825,7 +3927,7 @@ function restart() {
|
|
|
3825
3927
|
}
|
|
3826
3928
|
function logs(lines2 = 50) {
|
|
3827
3929
|
try {
|
|
3828
|
-
const all2 =
|
|
3930
|
+
const all2 = fs18.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3829
3931
|
return all2.slice(-lines2).join("\n");
|
|
3830
3932
|
} catch {
|
|
3831
3933
|
return "";
|
|
@@ -3947,7 +4049,7 @@ var init_pair = __esm({
|
|
|
3947
4049
|
import { Command } from "commander";
|
|
3948
4050
|
|
|
3949
4051
|
// src/version.ts
|
|
3950
|
-
var VERSION = "0.0.
|
|
4052
|
+
var VERSION = "0.0.13";
|
|
3951
4053
|
|
|
3952
4054
|
// src/index.ts
|
|
3953
4055
|
init_config();
|
|
@@ -4076,7 +4178,7 @@ function registerRuntime(program, version) {
|
|
|
4076
4178
|
// src/commands/session.ts
|
|
4077
4179
|
init_sessions();
|
|
4078
4180
|
init_output();
|
|
4079
|
-
import
|
|
4181
|
+
import fs19 from "fs";
|
|
4080
4182
|
function resolveSessionId(arg) {
|
|
4081
4183
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
4082
4184
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -4085,7 +4187,7 @@ function resolveSessionId(arg) {
|
|
|
4085
4187
|
async function resolvePrompt(positional, opts) {
|
|
4086
4188
|
if (positional) return positional;
|
|
4087
4189
|
if (opts.prompt) return opts.prompt;
|
|
4088
|
-
if (opts.promptFile) return
|
|
4190
|
+
if (opts.promptFile) return fs19.readFileSync(opts.promptFile, "utf8");
|
|
4089
4191
|
if (!process.stdin.isTTY) {
|
|
4090
4192
|
const chunks = [];
|
|
4091
4193
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -4400,25 +4502,25 @@ init_pair();
|
|
|
4400
4502
|
|
|
4401
4503
|
// src/commands/welcome.ts
|
|
4402
4504
|
init_config();
|
|
4403
|
-
import
|
|
4404
|
-
import
|
|
4505
|
+
import fs20 from "fs";
|
|
4506
|
+
import path16 from "path";
|
|
4405
4507
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4406
4508
|
import { spawn as spawn12 } from "child_process";
|
|
4407
|
-
var WELCOMED_MARKER =
|
|
4509
|
+
var WELCOMED_MARKER = path16.join(SAKURA_DIR, ".welcomed");
|
|
4408
4510
|
function resolveBinary2() {
|
|
4409
4511
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
4410
|
-
if (envBin &&
|
|
4411
|
-
const here =
|
|
4512
|
+
if (envBin && fs20.existsSync(envBin)) return envBin;
|
|
4513
|
+
const here = path16.dirname(fileURLToPath2(import.meta.url));
|
|
4412
4514
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
4413
4515
|
const candidates = [
|
|
4414
|
-
|
|
4516
|
+
path16.join(here, "..", "bin", name),
|
|
4415
4517
|
// package/bin (postinstall download)
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4518
|
+
path16.join(here, "bin", name),
|
|
4519
|
+
path16.join(process.cwd(), "bin", name),
|
|
4520
|
+
path16.join(SAKURA_DIR, name)
|
|
4419
4521
|
// user-dropped
|
|
4420
4522
|
];
|
|
4421
|
-
return candidates.find((p) =>
|
|
4523
|
+
return candidates.find((p) => fs20.existsSync(p)) ?? null;
|
|
4422
4524
|
}
|
|
4423
4525
|
function showWelcome() {
|
|
4424
4526
|
return new Promise((resolve) => {
|
|
@@ -4436,10 +4538,10 @@ function showWelcome() {
|
|
|
4436
4538
|
});
|
|
4437
4539
|
}
|
|
4438
4540
|
async function maybeFirstRunWelcome() {
|
|
4439
|
-
if (
|
|
4541
|
+
if (fs20.existsSync(WELCOMED_MARKER)) return;
|
|
4440
4542
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
4441
4543
|
try {
|
|
4442
|
-
|
|
4544
|
+
fs20.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
4443
4545
|
} catch {
|
|
4444
4546
|
}
|
|
4445
4547
|
await showWelcome();
|
|
@@ -4476,6 +4578,59 @@ function registerWelcome(program) {
|
|
|
4476
4578
|
});
|
|
4477
4579
|
}
|
|
4478
4580
|
|
|
4581
|
+
// src/commands/app.ts
|
|
4582
|
+
import fs21 from "fs";
|
|
4583
|
+
import os10 from "os";
|
|
4584
|
+
import path17 from "path";
|
|
4585
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4586
|
+
init_output();
|
|
4587
|
+
var APP_PATH = "/Applications/Sakura.app";
|
|
4588
|
+
function dmgUrl() {
|
|
4589
|
+
const base = process.env.SAKURA_APP_BASE_URL || `https://github.com/Nishu0/sakura-tsnet/releases/download/v${VERSION}`;
|
|
4590
|
+
return `${base}/Sakura.dmg`;
|
|
4591
|
+
}
|
|
4592
|
+
async function download(url, dest) {
|
|
4593
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
4594
|
+
if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
4595
|
+
fs21.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
4596
|
+
}
|
|
4597
|
+
async function install() {
|
|
4598
|
+
if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
|
|
4599
|
+
const tmp = fs21.mkdtempSync(path17.join(os10.tmpdir(), "sakura-app-"));
|
|
4600
|
+
const dmg = path17.join(tmp, "Sakura.dmg");
|
|
4601
|
+
const mount = path17.join(tmp, "mnt");
|
|
4602
|
+
info(`Downloading Sakura.app ${VERSION}\u2026`);
|
|
4603
|
+
try {
|
|
4604
|
+
await download(dmgUrl(), dmg);
|
|
4605
|
+
} catch (e) {
|
|
4606
|
+
die(`${e.message}
|
|
4607
|
+
Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
|
|
4608
|
+
}
|
|
4609
|
+
fs21.mkdirSync(mount);
|
|
4610
|
+
execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
|
|
4611
|
+
try {
|
|
4612
|
+
try {
|
|
4613
|
+
execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
|
|
4614
|
+
} catch {
|
|
4615
|
+
}
|
|
4616
|
+
fs21.rmSync(APP_PATH, { recursive: true, force: true });
|
|
4617
|
+
execFileSync4("ditto", [path17.join(mount, "Sakura.app"), APP_PATH]);
|
|
4618
|
+
} finally {
|
|
4619
|
+
execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
4620
|
+
fs21.rmSync(tmp, { recursive: true, force: true });
|
|
4621
|
+
}
|
|
4622
|
+
execFileSync4("open", [APP_PATH]);
|
|
4623
|
+
success("Sakura is in your menu bar (look for the blossom).");
|
|
4624
|
+
}
|
|
4625
|
+
function registerApp(program) {
|
|
4626
|
+
const app = program.command("app").description("The optional Sakura menu bar app for macOS");
|
|
4627
|
+
app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
|
|
4628
|
+
app.command("open").description("Open the Sakura menu bar app").action(() => {
|
|
4629
|
+
if (!fs21.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
|
|
4630
|
+
execFileSync4("open", [APP_PATH]);
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4479
4634
|
// src/index.ts
|
|
4480
4635
|
init_manager();
|
|
4481
4636
|
async function main() {
|
|
@@ -4499,6 +4654,7 @@ async function main() {
|
|
|
4499
4654
|
registerConnectivity(program);
|
|
4500
4655
|
registerPair(program);
|
|
4501
4656
|
registerWelcome(program);
|
|
4657
|
+
registerApp(program);
|
|
4502
4658
|
program.showHelpAfterError("(add --help for usage)");
|
|
4503
4659
|
await program.parseAsync(process.argv);
|
|
4504
4660
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sakuraai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
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",
|