sakuraai 0.0.1 → 0.0.2

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 +158 -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,7 +419,7 @@ function messages(sessionId) {
425
419
  const file = findFile(sessionId);
426
420
  return file ? readMessages(file) : [];
427
421
  }
428
- function send(sessionId, message) {
422
+ function send(sessionId, message, onData) {
429
423
  return new Promise((resolve) => {
430
424
  const bin = process.env.CLAUDE_BIN || "claude";
431
425
  const child = spawn(bin, ["--resume", sessionId, "-p", message], {
@@ -433,7 +427,10 @@ function send(sessionId, message) {
433
427
  });
434
428
  let out = "";
435
429
  let err = "";
436
- child.stdout.on("data", (d) => out += d.toString());
430
+ child.stdout.on("data", (d) => {
431
+ out += d.toString();
432
+ onData?.(d.toString());
433
+ });
437
434
  child.stderr.on("data", (d) => err += d.toString());
438
435
  child.on(
439
436
  "error",
@@ -560,7 +557,7 @@ function messages2(sessionId) {
560
557
  const file = findFile2(sessionId);
561
558
  return file ? readMessages2(file) : [];
562
559
  }
563
- function send2(sessionId, message) {
560
+ function send2(sessionId, message, onData) {
564
561
  return new Promise((resolve) => {
565
562
  const child = spawn2(codexBin(), ["exec", "resume", sessionId, message], {
566
563
  stdio: ["ignore", "pipe", "pipe"],
@@ -568,7 +565,10 @@ function send2(sessionId, message) {
568
565
  });
569
566
  let out = "";
570
567
  let err = "";
571
- child.stdout.on("data", (d) => out += d.toString());
568
+ child.stdout.on("data", (d) => {
569
+ out += d.toString();
570
+ onData?.(d.toString());
571
+ });
572
572
  child.stderr.on("data", (d) => err += d.toString());
573
573
  child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
574
574
  child.on(
@@ -701,7 +701,7 @@ async function messages3(sessionId) {
701
701
  db.close();
702
702
  }
703
703
  }
704
- function send3(sessionId, message) {
704
+ function send3(sessionId, message, onData) {
705
705
  return new Promise((resolve) => {
706
706
  const child = spawn3(opencodeBin(), ["run", "-s", sessionId, message], {
707
707
  stdio: ["ignore", "pipe", "pipe"],
@@ -709,7 +709,10 @@ function send3(sessionId, message) {
709
709
  });
710
710
  let out = "";
711
711
  let err = "";
712
- child.stdout.on("data", (d) => out += d.toString());
712
+ child.stdout.on("data", (d) => {
713
+ out += d.toString();
714
+ onData?.(d.toString());
715
+ });
713
716
  child.stderr.on("data", (d) => err += d.toString());
714
717
  child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
715
718
  child.on(
@@ -776,11 +779,11 @@ async function history(sessionId, opts = {}, agent) {
776
779
  if (opts.reverse) msgs = [...msgs].reverse();
777
780
  return msgs;
778
781
  }
779
- async function chat(sessionId, prompt, agent) {
782
+ async function chat(sessionId, prompt, agent, onData) {
780
783
  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);
784
+ if (a === "claude") return send(sessionId, prompt, onData);
785
+ if (a === "codex") return send2(sessionId, prompt, onData);
786
+ if (a === "opencode") return send3(sessionId, prompt, onData);
784
787
  return {
785
788
  ok: false,
786
789
  output: "",
@@ -1134,10 +1137,107 @@ var init_tailscale = __esm({
1134
1137
  }
1135
1138
  });
1136
1139
 
1140
+ // src/daemon/hub.ts
1141
+ function sessionKey(agent, sessionId) {
1142
+ return `${agent}:${sessionId}`;
1143
+ }
1144
+ var Hub, hub;
1145
+ var init_hub = __esm({
1146
+ "src/daemon/hub.ts"() {
1147
+ "use strict";
1148
+ Hub = class {
1149
+ clients = /* @__PURE__ */ new Map();
1150
+ subscribe(key, ws) {
1151
+ let set = this.clients.get(key);
1152
+ if (!set) {
1153
+ set = /* @__PURE__ */ new Set();
1154
+ this.clients.set(key, set);
1155
+ }
1156
+ set.add(ws);
1157
+ }
1158
+ remove(ws) {
1159
+ for (const set of this.clients.values()) set.delete(ws);
1160
+ }
1161
+ broadcast(key, event) {
1162
+ const set = this.clients.get(key);
1163
+ if (!set || set.size === 0) return;
1164
+ const payload = JSON.stringify(event);
1165
+ for (const ws of set) {
1166
+ try {
1167
+ ws.send(payload);
1168
+ } catch {
1169
+ }
1170
+ }
1171
+ }
1172
+ };
1173
+ hub = new Hub();
1174
+ }
1175
+ });
1176
+
1177
+ // src/daemon/stream.ts
1178
+ async function runTurn(sessionId, agent, prompt) {
1179
+ const key = sessionKey(agent, sessionId);
1180
+ const emit = (e) => hub.broadcast(key, e);
1181
+ let lastEmitted = "\0";
1182
+ const emitLive = (text) => {
1183
+ if (text === lastEmitted) return;
1184
+ lastEmitted = text;
1185
+ emit({
1186
+ type: "message",
1187
+ sessionId,
1188
+ agent,
1189
+ data: {
1190
+ id: LIVE_ID,
1191
+ role: "assistant",
1192
+ content: text,
1193
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1194
+ streaming: true
1195
+ }
1196
+ });
1197
+ };
1198
+ emitLive("");
1199
+ const baseIds = /* @__PURE__ */ new Set();
1200
+ try {
1201
+ for (const m of await messages4(sessionId, agent)) baseIds.add(m.id);
1202
+ } catch {
1203
+ }
1204
+ let stdoutAcc = "";
1205
+ const onData = (chunk) => {
1206
+ stdoutAcc += stripAnsi(chunk);
1207
+ const t = stdoutAcc.trim();
1208
+ if (t.length >= lastEmitted.replace("\0", "").length) emitLive(t);
1209
+ };
1210
+ const timer = setInterval(async () => {
1211
+ try {
1212
+ const msgs = await messages4(sessionId, agent);
1213
+ const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
1214
+ if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
1215
+ } catch {
1216
+ }
1217
+ }, POLL_MS);
1218
+ const result = await chat(sessionId, prompt, agent, onData);
1219
+ clearInterval(timer);
1220
+ emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
1221
+ return { ok: result.ok, error: result.error };
1222
+ }
1223
+ var ANSI, stripAnsi, LIVE_ID, POLL_MS;
1224
+ var init_stream = __esm({
1225
+ "src/daemon/stream.ts"() {
1226
+ "use strict";
1227
+ init_sessions();
1228
+ init_hub();
1229
+ ANSI = /\[[0-9;]*[a-zA-Z]/g;
1230
+ stripAnsi = (s) => s.replace(ANSI, "");
1231
+ LIVE_ID = "__live__";
1232
+ POLL_MS = 300;
1233
+ }
1234
+ });
1235
+
1137
1236
  // src/daemon/server.ts
1138
1237
  import http from "http";
1139
1238
  import fs8 from "fs";
1140
1239
  import { URL } from "url";
1240
+ import { WebSocketServer } from "ws";
1141
1241
  function add(method, path6, handler) {
1142
1242
  const keys = [];
1143
1243
  const pattern = new RegExp(
@@ -1190,7 +1290,7 @@ function send4(res, status2, payload) {
1190
1290
  res.end(body);
1191
1291
  }
1192
1292
  function createServer(port2) {
1193
- return http.createServer(async (req, res) => {
1293
+ const server = http.createServer(async (req, res) => {
1194
1294
  const method = req.method ?? "GET";
1195
1295
  const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
1196
1296
  if (method === "OPTIONS") return send4(res, 204, {});
@@ -1219,6 +1319,31 @@ function createServer(port2) {
1219
1319
  send4(res, 500, fail("nonzero", e.message));
1220
1320
  }
1221
1321
  });
1322
+ const wss = new WebSocketServer({ server, path: "/ws" });
1323
+ wss.on("connection", (ws, req) => {
1324
+ const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
1325
+ const token = u.searchParams.get("token");
1326
+ const expected = getToken();
1327
+ if (!expected || token !== expected) {
1328
+ ws.close(1008, "Unauthorized");
1329
+ return;
1330
+ }
1331
+ ws.on("message", (raw) => {
1332
+ try {
1333
+ const msg = JSON.parse(raw.toString());
1334
+ if (msg?.type === "subscribe" && msg.sessionId && msg.agent) {
1335
+ hub.subscribe(sessionKey(msg.agent, msg.sessionId), ws);
1336
+ ws.send(JSON.stringify({ type: "subscribed", sessionId: msg.sessionId, agent: msg.agent }));
1337
+ } else if (msg?.type === "ping") {
1338
+ ws.send(JSON.stringify({ type: "pong" }));
1339
+ }
1340
+ } catch {
1341
+ }
1342
+ });
1343
+ ws.on("close", () => hub.remove(ws));
1344
+ ws.on("error", () => hub.remove(ws));
1345
+ });
1346
+ return server;
1222
1347
  }
1223
1348
  function setVersion(v) {
1224
1349
  VERSION2 = v;
@@ -1234,6 +1359,8 @@ var init_server = __esm({
1234
1359
  init_registry();
1235
1360
  init_tailscale();
1236
1361
  init_registry();
1362
+ init_hub();
1363
+ init_stream();
1237
1364
  routes = [];
1238
1365
  add("GET", "/sessions", async () => {
1239
1366
  const list2 = await list();
@@ -1245,9 +1372,10 @@ var init_server = __esm({
1245
1372
  return { messages: messages5 };
1246
1373
  });
1247
1374
  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 };
1375
+ const agent = body?.agent || await detectAgent(params.id);
1376
+ if (!agent) return { ok: false, error: "unknown agent for session" };
1377
+ const r = await runTurn(params.id, agent, String(body?.message ?? ""));
1378
+ return { ok: r.ok, error: r.error };
1251
1379
  });
1252
1380
  add(
1253
1381
  "GET",
@@ -1327,8 +1455,10 @@ var init_server = __esm({
1327
1455
  return ok(await history(params.id, { all, limit, reverse }));
1328
1456
  });
1329
1457
  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");
1458
+ const agent = body?.agent || await detectAgent(params.id);
1459
+ if (!agent) return fail("nonzero", "unknown agent for session");
1460
+ const r = await runTurn(params.id, agent, String(body?.prompt ?? ""));
1461
+ return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
1332
1462
  });
1333
1463
  add(
1334
1464
  "POST",
@@ -1526,6 +1656,7 @@ var init_manager = __esm({
1526
1656
  });
1527
1657
 
1528
1658
  // src/pairing.ts
1659
+ import os5 from "os";
1529
1660
  import qrcode from "qrcode-terminal";
1530
1661
  async function buildPairing() {
1531
1662
  const token = requireToken();
@@ -1542,15 +1673,11 @@ function renderQr(deepLink) {
1542
1673
  });
1543
1674
  }
1544
1675
  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
- }
1676
+ const nets = os5.networkInterfaces();
1677
+ for (const name of Object.keys(nets)) {
1678
+ for (const net of nets[name] ?? []) {
1679
+ if (net.family === "IPv4" && !net.internal) return net.address;
1552
1680
  }
1553
- } catch {
1554
1681
  }
1555
1682
  return "127.0.0.1";
1556
1683
  }
@@ -1609,7 +1736,7 @@ var init_pair = __esm({
1609
1736
  import { Command } from "commander";
1610
1737
 
1611
1738
  // src/version.ts
1612
- var VERSION = "0.0.1";
1739
+ var VERSION = "0.0.2";
1613
1740
 
1614
1741
  // src/index.ts
1615
1742
  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.2",
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",