sakuraai 0.0.1 → 0.0.3

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 +168 -31
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,12 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
6
- }) : x)(function(x) {
7
- if (typeof require !== "undefined") return require.apply(this, arguments);
8
- throw Error('Dynamic require of "' + x + '" is not supported');
9
- });
10
4
  var __esm = (fn, res) => function __init() {
11
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
6
  };
@@ -425,15 +419,28 @@ function messages(sessionId) {
425
419
  const file = findFile(sessionId);
426
420
  return file ? readMessages(file) : [];
427
421
  }
428
- function send(sessionId, message) {
422
+ function sessionCwd(sessionId) {
423
+ const file = findFile(sessionId);
424
+ if (!file) return void 0;
425
+ for (const entry of jsonlEntries(file)) {
426
+ if (typeof entry?.cwd === "string" && entry.cwd) return entry.cwd;
427
+ }
428
+ return void 0;
429
+ }
430
+ function send(sessionId, message, onData) {
429
431
  return new Promise((resolve) => {
430
432
  const bin = process.env.CLAUDE_BIN || "claude";
433
+ const cwd = sessionCwd(sessionId);
431
434
  const child = spawn(bin, ["--resume", sessionId, "-p", message], {
435
+ cwd: cwd && fs3.existsSync(cwd) ? cwd : void 0,
432
436
  stdio: ["ignore", "pipe", "pipe"]
433
437
  });
434
438
  let out = "";
435
439
  let err = "";
436
- child.stdout.on("data", (d) => out += d.toString());
440
+ child.stdout.on("data", (d) => {
441
+ out += d.toString();
442
+ onData?.(d.toString());
443
+ });
437
444
  child.stderr.on("data", (d) => err += d.toString());
438
445
  child.on(
439
446
  "error",
@@ -560,7 +567,7 @@ function messages2(sessionId) {
560
567
  const file = findFile2(sessionId);
561
568
  return file ? readMessages2(file) : [];
562
569
  }
563
- function send2(sessionId, message) {
570
+ function send2(sessionId, message, onData) {
564
571
  return new Promise((resolve) => {
565
572
  const child = spawn2(codexBin(), ["exec", "resume", sessionId, message], {
566
573
  stdio: ["ignore", "pipe", "pipe"],
@@ -568,7 +575,10 @@ function send2(sessionId, message) {
568
575
  });
569
576
  let out = "";
570
577
  let err = "";
571
- child.stdout.on("data", (d) => out += d.toString());
578
+ child.stdout.on("data", (d) => {
579
+ out += d.toString();
580
+ onData?.(d.toString());
581
+ });
572
582
  child.stderr.on("data", (d) => err += d.toString());
573
583
  child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
574
584
  child.on(
@@ -701,7 +711,7 @@ async function messages3(sessionId) {
701
711
  db.close();
702
712
  }
703
713
  }
704
- function send3(sessionId, message) {
714
+ function send3(sessionId, message, onData) {
705
715
  return new Promise((resolve) => {
706
716
  const child = spawn3(opencodeBin(), ["run", "-s", sessionId, message], {
707
717
  stdio: ["ignore", "pipe", "pipe"],
@@ -709,7 +719,10 @@ function send3(sessionId, message) {
709
719
  });
710
720
  let out = "";
711
721
  let err = "";
712
- child.stdout.on("data", (d) => out += d.toString());
722
+ child.stdout.on("data", (d) => {
723
+ out += d.toString();
724
+ onData?.(d.toString());
725
+ });
713
726
  child.stderr.on("data", (d) => err += d.toString());
714
727
  child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
715
728
  child.on(
@@ -776,11 +789,11 @@ async function history(sessionId, opts = {}, agent) {
776
789
  if (opts.reverse) msgs = [...msgs].reverse();
777
790
  return msgs;
778
791
  }
779
- async function chat(sessionId, prompt, agent) {
792
+ async function chat(sessionId, prompt, agent, onData) {
780
793
  const a = agent ?? await detectAgent(sessionId);
781
- if (a === "claude") return send(sessionId, prompt);
782
- if (a === "codex") return send2(sessionId, prompt);
783
- if (a === "opencode") return send3(sessionId, prompt);
794
+ if (a === "claude") return send(sessionId, prompt, onData);
795
+ if (a === "codex") return send2(sessionId, prompt, onData);
796
+ if (a === "opencode") return send3(sessionId, prompt, onData);
784
797
  return {
785
798
  ok: false,
786
799
  output: "",
@@ -1134,10 +1147,107 @@ var init_tailscale = __esm({
1134
1147
  }
1135
1148
  });
1136
1149
 
1150
+ // src/daemon/hub.ts
1151
+ function sessionKey(agent, sessionId) {
1152
+ return `${agent}:${sessionId}`;
1153
+ }
1154
+ var Hub, hub;
1155
+ var init_hub = __esm({
1156
+ "src/daemon/hub.ts"() {
1157
+ "use strict";
1158
+ Hub = class {
1159
+ clients = /* @__PURE__ */ new Map();
1160
+ subscribe(key, ws) {
1161
+ let set = this.clients.get(key);
1162
+ if (!set) {
1163
+ set = /* @__PURE__ */ new Set();
1164
+ this.clients.set(key, set);
1165
+ }
1166
+ set.add(ws);
1167
+ }
1168
+ remove(ws) {
1169
+ for (const set of this.clients.values()) set.delete(ws);
1170
+ }
1171
+ broadcast(key, event) {
1172
+ const set = this.clients.get(key);
1173
+ if (!set || set.size === 0) return;
1174
+ const payload = JSON.stringify(event);
1175
+ for (const ws of set) {
1176
+ try {
1177
+ ws.send(payload);
1178
+ } catch {
1179
+ }
1180
+ }
1181
+ }
1182
+ };
1183
+ hub = new Hub();
1184
+ }
1185
+ });
1186
+
1187
+ // src/daemon/stream.ts
1188
+ async function runTurn(sessionId, agent, prompt) {
1189
+ const key = sessionKey(agent, sessionId);
1190
+ const emit = (e) => hub.broadcast(key, e);
1191
+ let lastEmitted = "\0";
1192
+ const emitLive = (text) => {
1193
+ if (text === lastEmitted) return;
1194
+ lastEmitted = text;
1195
+ emit({
1196
+ type: "message",
1197
+ sessionId,
1198
+ agent,
1199
+ data: {
1200
+ id: LIVE_ID,
1201
+ role: "assistant",
1202
+ content: text,
1203
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1204
+ streaming: true
1205
+ }
1206
+ });
1207
+ };
1208
+ emitLive("");
1209
+ const baseIds = /* @__PURE__ */ new Set();
1210
+ try {
1211
+ for (const m of await messages4(sessionId, agent)) baseIds.add(m.id);
1212
+ } catch {
1213
+ }
1214
+ let stdoutAcc = "";
1215
+ const onData = (chunk) => {
1216
+ stdoutAcc += stripAnsi(chunk);
1217
+ const t = stdoutAcc.trim();
1218
+ if (t.length >= lastEmitted.replace("\0", "").length) emitLive(t);
1219
+ };
1220
+ const timer = setInterval(async () => {
1221
+ try {
1222
+ const msgs = await messages4(sessionId, agent);
1223
+ const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
1224
+ if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
1225
+ } catch {
1226
+ }
1227
+ }, POLL_MS);
1228
+ const result = await chat(sessionId, prompt, agent, onData);
1229
+ clearInterval(timer);
1230
+ emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
1231
+ return { ok: result.ok, error: result.error };
1232
+ }
1233
+ var ANSI, stripAnsi, LIVE_ID, POLL_MS;
1234
+ var init_stream = __esm({
1235
+ "src/daemon/stream.ts"() {
1236
+ "use strict";
1237
+ init_sessions();
1238
+ init_hub();
1239
+ ANSI = /\[[0-9;]*[a-zA-Z]/g;
1240
+ stripAnsi = (s) => s.replace(ANSI, "");
1241
+ LIVE_ID = "__live__";
1242
+ POLL_MS = 300;
1243
+ }
1244
+ });
1245
+
1137
1246
  // src/daemon/server.ts
1138
1247
  import http from "http";
1139
1248
  import fs8 from "fs";
1140
1249
  import { URL } from "url";
1250
+ import { WebSocketServer } from "ws";
1141
1251
  function add(method, path6, handler) {
1142
1252
  const keys = [];
1143
1253
  const pattern = new RegExp(
@@ -1190,7 +1300,7 @@ function send4(res, status2, payload) {
1190
1300
  res.end(body);
1191
1301
  }
1192
1302
  function createServer(port2) {
1193
- return http.createServer(async (req, res) => {
1303
+ const server = http.createServer(async (req, res) => {
1194
1304
  const method = req.method ?? "GET";
1195
1305
  const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
1196
1306
  if (method === "OPTIONS") return send4(res, 204, {});
@@ -1219,6 +1329,31 @@ function createServer(port2) {
1219
1329
  send4(res, 500, fail("nonzero", e.message));
1220
1330
  }
1221
1331
  });
1332
+ const wss = new WebSocketServer({ server, path: "/ws" });
1333
+ wss.on("connection", (ws, req) => {
1334
+ const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
1335
+ const token = u.searchParams.get("token");
1336
+ const expected = getToken();
1337
+ if (!expected || token !== expected) {
1338
+ ws.close(1008, "Unauthorized");
1339
+ return;
1340
+ }
1341
+ ws.on("message", (raw) => {
1342
+ try {
1343
+ const msg = JSON.parse(raw.toString());
1344
+ if (msg?.type === "subscribe" && msg.sessionId && msg.agent) {
1345
+ hub.subscribe(sessionKey(msg.agent, msg.sessionId), ws);
1346
+ ws.send(JSON.stringify({ type: "subscribed", sessionId: msg.sessionId, agent: msg.agent }));
1347
+ } else if (msg?.type === "ping") {
1348
+ ws.send(JSON.stringify({ type: "pong" }));
1349
+ }
1350
+ } catch {
1351
+ }
1352
+ });
1353
+ ws.on("close", () => hub.remove(ws));
1354
+ ws.on("error", () => hub.remove(ws));
1355
+ });
1356
+ return server;
1222
1357
  }
1223
1358
  function setVersion(v) {
1224
1359
  VERSION2 = v;
@@ -1234,6 +1369,8 @@ var init_server = __esm({
1234
1369
  init_registry();
1235
1370
  init_tailscale();
1236
1371
  init_registry();
1372
+ init_hub();
1373
+ init_stream();
1237
1374
  routes = [];
1238
1375
  add("GET", "/sessions", async () => {
1239
1376
  const list2 = await list();
@@ -1245,9 +1382,10 @@ var init_server = __esm({
1245
1382
  return { messages: messages5 };
1246
1383
  });
1247
1384
  add("POST", "/sessions/:id/send", async ({ params, body }) => {
1248
- const agent = body?.agent || void 0;
1249
- const r = await chat(params.id, String(body?.message ?? ""), agent);
1250
- return { ok: r.ok, output: r.output, error: r.error };
1385
+ const agent = body?.agent || await detectAgent(params.id);
1386
+ if (!agent) return { ok: false, error: "unknown agent for session" };
1387
+ const r = await runTurn(params.id, agent, String(body?.message ?? ""));
1388
+ return { ok: r.ok, error: r.error };
1251
1389
  });
1252
1390
  add(
1253
1391
  "GET",
@@ -1327,8 +1465,10 @@ var init_server = __esm({
1327
1465
  return ok(await history(params.id, { all, limit, reverse }));
1328
1466
  });
1329
1467
  add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
1330
- const r = await chat(params.id, String(body?.prompt ?? ""), body?.agent);
1331
- return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "chat failed");
1468
+ const agent = body?.agent || await detectAgent(params.id);
1469
+ if (!agent) return fail("nonzero", "unknown agent for session");
1470
+ const r = await runTurn(params.id, agent, String(body?.prompt ?? ""));
1471
+ return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
1332
1472
  });
1333
1473
  add(
1334
1474
  "POST",
@@ -1526,6 +1666,7 @@ var init_manager = __esm({
1526
1666
  });
1527
1667
 
1528
1668
  // src/pairing.ts
1669
+ import os5 from "os";
1529
1670
  import qrcode from "qrcode-terminal";
1530
1671
  async function buildPairing() {
1531
1672
  const token = requireToken();
@@ -1542,15 +1683,11 @@ function renderQr(deepLink) {
1542
1683
  });
1543
1684
  }
1544
1685
  function localIp() {
1545
- try {
1546
- const os5 = __require("os");
1547
- const nets = os5.networkInterfaces();
1548
- for (const name of Object.keys(nets)) {
1549
- for (const net of nets[name] ?? []) {
1550
- if (net.family === "IPv4" && !net.internal) return net.address;
1551
- }
1686
+ const nets = os5.networkInterfaces();
1687
+ for (const name of Object.keys(nets)) {
1688
+ for (const net of nets[name] ?? []) {
1689
+ if (net.family === "IPv4" && !net.internal) return net.address;
1552
1690
  }
1553
- } catch {
1554
1691
  }
1555
1692
  return "127.0.0.1";
1556
1693
  }
@@ -1609,7 +1746,7 @@ var init_pair = __esm({
1609
1746
  import { Command } from "commander";
1610
1747
 
1611
1748
  // src/version.ts
1612
- var VERSION = "0.0.1";
1749
+ var VERSION = "0.0.3";
1613
1750
 
1614
1751
  // src/index.ts
1615
1752
  init_config();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sakuraai",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Sakura Agent CLI + local runtime for managing AI coding sessions (Claude Code, Codex, OpenCode) and reaching them privately from the Sakura mobile app over Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -42,7 +42,8 @@
42
42
  "cli-table3": "^0.6.5",
43
43
  "commander": "^12.1.0",
44
44
  "ora": "^8.1.0",
45
- "qrcode-terminal": "^0.12.0"
45
+ "qrcode-terminal": "^0.12.0",
46
+ "ws": "^8.21.0"
46
47
  },
47
48
  "optionalDependencies": {
48
49
  "better-sqlite3": "^11.3.0"
@@ -51,6 +52,7 @@
51
52
  "@types/better-sqlite3": "^7.6.11",
52
53
  "@types/node": "^22.7.0",
53
54
  "@types/qrcode-terminal": "^0.12.2",
55
+ "@types/ws": "^8.18.1",
54
56
  "rimraf": "^6.0.1",
55
57
  "tsup": "^8.3.0",
56
58
  "tsx": "^4.19.0",