wokitoki 0.3.2 → 0.4.0

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 CHANGED
@@ -18,6 +18,8 @@ No server. No account. No tunnel. Free and open source.
18
18
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.pandeyvishwas51--oss%2Fwokitoki-2B1306)](https://registry.modelcontextprotocol.io/v0/servers?search=wokitoki)
19
19
  [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-2B1306)](https://nodejs.org)
20
20
 
21
+ <img src="https://raw.githubusercontent.com/pandeyvishwas51-oss/wokitoki/main/assets/demo.gif" width="600" alt="Demo: two developers pair WokiToki with a 6-digit code, then one Claude Code asks the other developer's Codex whether the orders API returns paise and gets the answer back, end to end encrypted">
22
+
21
23
  **[▶ Watch the 1-minute demo (sound on)](https://pandeyvishwas51-oss.github.io/wokitoki/#demo)**
22
24
 
23
25
  [Website](https://pandeyvishwas51-oss.github.io/wokitoki/) · [Quick start](#quick-start) · [How it works](#how-it-works) · [FAQ](#faq) · [Security](#security-honestly)
@@ -155,6 +157,8 @@ Yes. It is open source under Apache-2.0, and there is no paid tier, hosted servi
155
157
  - Messages are end-to-end encrypted between the two daemons. In peer-to-peer mode, nothing is stored anywhere but the two machines.
156
158
  - Pairing needs a one-time invite **and** both people approving the same verify code. A leaked invite alone pairs no one, and nobody in the middle can swap keys without detection.
157
159
  - Message-layer keys are static per pairing (no ratchet yet). Keys live in `~/.wokitoki/identity.json` (mode 0600), and local history is stored unencrypted on disk.
160
+ - A paired peer can't flood your agent: incoming messages are rate-limited per peer and capped at 500 unread; the rest wait in the sender's outbox.
161
+ - Every parser that reads network input is fuzz-tested, and releases are built only by CI with signed provenance and a human approval step.
158
162
  - No external security audit has been done. Don't send secrets.
159
163
 
160
164
  Details: [threat model](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/docs/THREAT_MODEL.md) · [architecture](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/docs/ARCHITECTURE.md) · [protocol](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/docs/PROTOCOL.md) · [security policy](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/SECURITY.md) · [verification log](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/VERIFICATION.md)
@@ -190,11 +194,13 @@ Every command accepts `--json` and `--home <dir>` (default `~/.wokitoki`). The o
190
194
 
191
195
  ```bash
192
196
  pnpm install
193
- pnpm check # strict TypeScript + 65 tests: unit, adversarial, P2P on a local DHT testnet, multi-process CLI e2e, real MCP calls
197
+ pnpm check # strict TypeScript + 73 tests: unit, adversarial, P2P on a local DHT testnet, multi-process CLI e2e, real MCP calls
194
198
  ```
195
199
 
196
200
  Releases come only from CI: bump the version, tag `vX.Y.Z` on `main` and push the tag. [release.yml](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/.github/workflows/release.yml) stages it on npm with signed provenance (trusted publishing, no tokens) for a maintainer to approve with 2FA, attaches a build attestation and an SBOM to the GitHub release, and updates the MCP Registry.
197
201
 
202
+ Contributions welcome: see [CONTRIBUTING.md](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/CONTRIBUTING.md) and the [good first issues](https://github.com/pandeyvishwas51-oss/wokitoki/labels/good%20first%20issue).
203
+
198
204
  [Roadmap](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/ROADMAP.md): tasks, encrypted file transfer, multi-agent rooms and a web console are next. License: [Apache-2.0](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/LICENSE).
199
205
 
200
206
  <p align="center"><img src="https://raw.githubusercontent.com/pandeyvishwas51-oss/wokitoki/main/assets/logo.svg" width="72" alt="WokiToki mascot"></p>
package/dist/wokitoki.mjs CHANGED
@@ -468,6 +468,14 @@ CREATE TABLE IF NOT EXISTS events (
468
468
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
469
469
  ).run(m.id, m.peer_id, m.pair_id, m.direction, m.text, m.reply_to, m.state, t, t);
470
470
  }
471
+ countUnread(peerId) {
472
+ return this.db.prepare(`SELECT COUNT(*) AS n FROM messages WHERE peer_id = ? AND direction = 'in' AND read_at IS NULL`).get(peerId).n;
473
+ }
474
+ peerActivity(peerId) {
475
+ const queued = this.db.prepare("SELECT COUNT(*) AS n FROM outbox WHERE peer_id = ?").get(peerId).n;
476
+ const last = this.db.prepare("SELECT MAX(created_at) AS t FROM messages WHERE peer_id = ?").get(peerId).t;
477
+ return { queued, lastMessageAt: last };
478
+ }
471
479
  getMessage(id) {
472
480
  return this.db.prepare("SELECT * FROM messages WHERE id = ?").get(id);
473
481
  }
@@ -595,6 +603,10 @@ var init_dht_mailbox = __esm({
595
603
  clearInterval(this.timer);
596
604
  await this.running;
597
605
  }
606
+ /** Sync now instead of waiting for the next interval (e.g. after the machine wakes from sleep). */
607
+ poke() {
608
+ this.round();
609
+ }
598
610
  round() {
599
611
  if (this.stopped || this.running) return;
600
612
  const peers = this.host.livePeers().filter((p) => p.state === "active" && !this.connected(p.device_id));
@@ -694,7 +706,7 @@ var init_dht_mailbox = __esm({
694
706
  import DHT from "hyperdht";
695
707
  import { randomUUID } from "node:crypto";
696
708
  import { StringDecoder } from "node:string_decoder";
697
- var CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, P2PTransport;
709
+ var CONNECT_TIMEOUT_MS, WAKE_CHECK_MS, WAKE_GAP_MS, REQUEST_TIMEOUT_MS, P2PTransport;
698
710
  var init_p2p_transport = __esm({
699
711
  async "apps/bridge/src/p2p-transport.ts"() {
700
712
  "use strict";
@@ -703,6 +715,8 @@ var init_p2p_transport = __esm({
703
715
  init_transport();
704
716
  await init_dht_mailbox();
705
717
  CONNECT_TIMEOUT_MS = 3e4;
718
+ WAKE_CHECK_MS = 1e4;
719
+ WAKE_GAP_MS = 3e4;
706
720
  REQUEST_TIMEOUT_MS = 2e4;
707
721
  P2PTransport = class {
708
722
  kind = "p2p";
@@ -717,6 +731,7 @@ var init_p2p_transport = __esm({
717
731
  bootstrap;
718
732
  mailboxIntervalMs;
719
733
  mailbox;
734
+ wakeTimer;
720
735
  startup;
721
736
  constructor(host, bootstrap, mailboxIntervalMs = 3e4) {
722
737
  this.host = host;
@@ -745,6 +760,7 @@ var init_p2p_transport = __esm({
745
760
  this.host.audit("p2p_online", null, { firewalled: this.dht.firewalled });
746
761
  this.mailbox = new DhtMailbox(this.dht, this.host, (id) => this.conns.has(id), this.mailboxIntervalMs);
747
762
  this.mailbox.start();
763
+ this.watchForWake();
748
764
  for (const peer of this.host.livePeers()) {
749
765
  this.dial(peer.device_id, this.host.signPubOf(peer)).then(
750
766
  () => this.host.peerOnline(peer.device_id),
@@ -762,10 +778,34 @@ var init_p2p_transport = __esm({
762
778
  }
763
779
  }
764
780
  }
781
+ /** After sleep, connections are stale and queues wait on long backoffs: reconnect and flush right away. */
782
+ watchForWake() {
783
+ let last = Date.now();
784
+ this.wakeTimer = setInterval(() => {
785
+ const now = Date.now();
786
+ if (now - last > WAKE_GAP_MS) {
787
+ this.host.log(`woke after ${Math.round((now - last) / 1e3)} s; reconnecting to peers`);
788
+ this.host.audit("p2p_wake", null, { gapMs: now - last });
789
+ for (const peer of this.host.livePeers()) {
790
+ this.disconnect(peer.device_id);
791
+ this.dial(peer.device_id, this.host.signPubOf(peer)).then(
792
+ () => this.host.peerOnline(peer.device_id),
793
+ () => {
794
+ }
795
+ );
796
+ }
797
+ this.host.transportReady();
798
+ this.mailbox?.poke();
799
+ }
800
+ last = now;
801
+ }, WAKE_CHECK_MS);
802
+ this.wakeTimer.unref();
803
+ }
765
804
  async stop() {
766
805
  this.stopped = true;
767
806
  await this.startup?.catch(() => {
768
807
  });
808
+ clearInterval(this.wakeTimer);
769
809
  await this.mailbox?.stop();
770
810
  for (const c of this.conns.values()) c.stream.destroy();
771
811
  this.conns.clear();
@@ -944,7 +984,7 @@ var init_p2p_transport = __esm({
944
984
  }
945
985
  const r = this.host.handleItem(frame.item, conn.deviceId);
946
986
  if (r.ok) this.send(conn, { t: "ack", id: frame.item.id });
947
- else if (r.permanent) this.send(conn, { t: "nack", id: frame.item.id, code: r.code, message: r.code, permanent: true });
987
+ else this.send(conn, { t: "nack", id: frame.item.id, code: r.code, message: r.code, permanent: r.permanent });
948
988
  break;
949
989
  }
950
990
  case "ack":
@@ -1183,6 +1223,9 @@ var init_bridge = __esm({
1183
1223
  keys = /* @__PURE__ */ new Map();
1184
1224
  // ponytail: failed-join counters live in memory; a daemon restart resets them. Invites expire in <=1h anyway.
1185
1225
  joinFailures = /* @__PURE__ */ new Map();
1226
+ // ponytail: per-peer inbound buckets live in memory; a restart refills them, which is harmless.
1227
+ buckets = /* @__PURE__ */ new Map();
1228
+ throttleAudited = /* @__PURE__ */ new Map();
1186
1229
  stopped = false;
1187
1230
  flushing;
1188
1231
  flushAgain = false;
@@ -1258,6 +1301,7 @@ var init_bridge = __esm({
1258
1301
  remoteApproved: p.remote_ok === 1,
1259
1302
  role: p.role,
1260
1303
  online: this.transport.isPeerConnected(p.device_id),
1304
+ ...this.store.peerActivity(p.device_id),
1261
1305
  createdAt: p.created_at,
1262
1306
  updatedAt: p.updated_at
1263
1307
  };
@@ -1557,6 +1601,10 @@ var init_bridge = __esm({
1557
1601
  this.audit("security.envelope_rejected", env.from, meta);
1558
1602
  return { ok: false, permanent: true, code: "rejected" };
1559
1603
  }
1604
+ if (payload.type === "text" && !this.store.getMessage(env.id)) {
1605
+ const refusal = this.inboundRefusal(peer.device_id);
1606
+ if (refusal) return { ok: false, permanent: false, code: refusal };
1607
+ }
1560
1608
  this.store.tx(() => {
1561
1609
  this.store.updatePeer(peer.device_id, { recv_seq: env.seq });
1562
1610
  if (payload.type === "receipt") {
@@ -1600,6 +1648,27 @@ var init_bridge = __esm({
1600
1648
  }
1601
1649
  });
1602
1650
  }
1651
+ /** A paired peer cannot flood this agent: a token bucket per peer, and a cap on unread messages. */
1652
+ inboundRefusal(peerId) {
1653
+ const burst = this.opts.inboundBurst ?? 60;
1654
+ const perMs = (this.opts.inboundPerMinute ?? 60) / 6e4;
1655
+ let refusal = null;
1656
+ if (this.store.countUnread(peerId) >= (this.opts.maxUnread ?? 500)) refusal = "inbox_full";
1657
+ else {
1658
+ const now = Date.now();
1659
+ const b = this.buckets.get(peerId) ?? { tokens: burst, at: now };
1660
+ b.tokens = Math.min(burst, b.tokens + (now - b.at) * perMs);
1661
+ b.at = now;
1662
+ this.buckets.set(peerId, b);
1663
+ if (b.tokens < 1) refusal = "rate_limited";
1664
+ else b.tokens -= 1;
1665
+ }
1666
+ if (refusal && Date.now() - (this.throttleAudited.get(peerId) ?? 0) > 6e4) {
1667
+ this.throttleAudited.set(peerId, Date.now());
1668
+ this.audit("security.inbound_throttled", peerId, { reason: refusal });
1669
+ }
1670
+ return refusal;
1671
+ }
1603
1672
  // ---------------------------------------------------------------- outbox
1604
1673
  mustPeer(deviceId) {
1605
1674
  const p = this.store.getPeer(deviceId);
@@ -1910,6 +1979,12 @@ async function guard(fn) {
1910
1979
  }
1911
1980
  function createMcpServer(socketPath, version) {
1912
1981
  const daemon2 = (method, path, body) => callDaemon(socketPath, method, path, body);
1982
+ let myName;
1983
+ const header = async () => {
1984
+ myName ??= (await daemon2("GET", "/identity")).name;
1985
+ return `You are ${JSON.stringify(myName)} on WokiToki; received messages were sent to you.
1986
+ ${UNTRUSTED}`;
1987
+ };
1913
1988
  const server = new McpServer({ name: "wokitoki", version });
1914
1989
  server.registerTool(
1915
1990
  "wokitoki_get_status",
@@ -1935,7 +2010,7 @@ function createMcpServer(socketPath, version) {
1935
2010
  "wokitoki_send_message",
1936
2011
  {
1937
2012
  title: "Send WokiToki message",
1938
- description: `Send an end-to-end encrypted text message to an already-paired, active peer agent (by name or device id). The text leaves this machine: never include secrets, credentials or private code your user has not approved sharing. Returns the message id; state becomes "delivered" once the peer's bridge confirms receipt (check with wokitoki_list_messages).`,
2013
+ description: 'Send an end-to-end encrypted text message to an already-paired, active peer agent (by name or device id). The text leaves this machine: never include secrets, credentials or private code your user has not approved sharing. Waits up to 3 seconds for the peer to confirm, then returns the message id and its state: "relayed" or "delivered" = the peer has it; "queued" = the peer is offline right now, and it is delivered automatically later (check with wokitoki_list_messages).',
1939
2014
  inputSchema: {
1940
2015
  to: z7.string().min(1).max(128).describe("peer name or device id"),
1941
2016
  text: z7.string().min(1).max(16e3).describe("message text"),
@@ -1943,7 +2018,15 @@ function createMcpServer(socketPath, version) {
1943
2018
  },
1944
2019
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1945
2020
  },
1946
- ({ to, text, replyTo }) => guard(async () => ok(messageSummary(await daemon2("POST", "/messages", { to, text, replyTo }))))
2021
+ ({ to, text, replyTo }) => guard(async () => {
2022
+ let m = await daemon2("POST", "/messages", { to, text, replyTo });
2023
+ for (let i = 0; i < 30 && m.state === "queued"; i++) {
2024
+ await new Promise((r) => setTimeout(r, 100));
2025
+ m = (await daemon2("GET", `/messages?peer=${encodeURIComponent(m.peer)}&limit=50`)).find((x) => x.id === m.id) ?? m;
2026
+ }
2027
+ const note = m.state === "queued" ? "The peer is offline right now; this is delivered automatically when it can be." : void 0;
2028
+ return ok({ ...messageSummary(m), ...note ? { note } : {} });
2029
+ })
1947
2030
  );
1948
2031
  server.registerTool(
1949
2032
  "wokitoki_read_inbox",
@@ -1963,7 +2046,7 @@ function createMcpServer(socketPath, version) {
1963
2046
  const msgs = await daemon2("GET", `/messages?${q}`);
1964
2047
  if (msgs.length && markRead !== false) await daemon2("POST", "/messages/read", { ids: msgs.map((m) => m.id) });
1965
2048
  if (!msgs.length) return ok([], "No unread WokiToki messages.");
1966
- return ok(msgs.map(messageSummary), UNTRUSTED);
2049
+ return ok(msgs.map(messageSummary), await header());
1967
2050
  })
1968
2051
  );
1969
2052
  server.registerTool(
@@ -1979,7 +2062,7 @@ function createMcpServer(socketPath, version) {
1979
2062
  },
1980
2063
  ({ peer, limit }) => guard(async () => {
1981
2064
  const q = new URLSearchParams({ limit: String(limit ?? 20), ...peer ? { peer } : {} });
1982
- return ok((await daemon2("GET", `/messages?${q}`)).map(messageSummary), UNTRUSTED);
2065
+ return ok((await daemon2("GET", `/messages?${q}`)).map(messageSummary), await header());
1983
2066
  })
1984
2067
  );
1985
2068
  return server;
@@ -2002,6 +2085,8 @@ var init_src3 = __esm({
2002
2085
  name: p.name,
2003
2086
  deviceId: p.deviceId,
2004
2087
  state: p.state,
2088
+ online: p.online,
2089
+ queued: p.queued,
2005
2090
  fingerprint: p.fingerprint
2006
2091
  });
2007
2092
  messageSummary = (m) => ({
@@ -2020,14 +2105,14 @@ var init_src3 = __esm({
2020
2105
 
2021
2106
  // apps/cli/src/main.ts
2022
2107
  import { Command } from "commander";
2023
- import { execFileSync as execFileSync2, spawn, spawnSync } from "node:child_process";
2108
+ import { execFileSync as execFileSync2, spawn as spawn2, spawnSync } from "node:child_process";
2024
2109
  import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
2025
2110
  import { homedir as homedir3 } from "node:os";
2026
2111
  import { fileURLToPath } from "node:url";
2027
2112
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
2028
2113
 
2029
2114
  // apps/cli/src/service.ts
2030
- import { execFileSync } from "node:child_process";
2115
+ import { execFileSync, spawn } from "node:child_process";
2031
2116
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2032
2117
  import { homedir } from "node:os";
2033
2118
  import { dirname, join } from "node:path";
@@ -2036,6 +2121,15 @@ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&
2036
2121
  var sh = (s) => `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
2037
2122
  var winQuote = (s) => `"${s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
2038
2123
  var vbs = (s) => `"${s.replace(/"/g, '""')}"`;
2124
+ var windowsLauncher = (line) => [
2125
+ "\uFEFF' WokiToki daemon: started at login by `wokitoki daemon install`; restarted 5 s after a crash",
2126
+ 'Set shell = CreateObject("WScript.Shell")',
2127
+ "Do",
2128
+ ` If shell.Run(${vbs(line)}, 0, True) = 0 Then Exit Do`,
2129
+ " WScript.Sleep 5000",
2130
+ "Loop",
2131
+ ""
2132
+ ].join("\r\n");
2039
2133
  var appData = () => process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
2040
2134
  function servicePath() {
2041
2135
  if (process.platform === "darwin") return join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
@@ -2061,7 +2155,7 @@ function startService() {
2061
2155
  execFileSync("launchctl", ["bootstrap", domain, servicePath()], { stdio: "ignore" });
2062
2156
  }
2063
2157
  } else if (process.platform === "win32") {
2064
- execFileSync("wscript.exe", ["//B", "//Nologo", servicePath()], { stdio: "ignore", cwd: homedir() });
2158
+ spawn("wscript.exe", ["//B", "//Nologo", servicePath()], { stdio: "ignore", cwd: homedir(), detached: true, windowsHide: true }).unref();
2065
2159
  } else {
2066
2160
  execFileSync("systemctl", ["--user", "start", "wokitoki.service"], { stdio: "ignore" });
2067
2161
  }
@@ -2097,16 +2191,8 @@ ${argv.map((a) => ` <string>${xml(a)}</string>`).join("\n")}
2097
2191
  }
2098
2192
  execFileSync("launchctl", ["bootstrap", domain, path]);
2099
2193
  } else if (process.platform === "win32") {
2100
- const line = `cmd.exe /d /s /c "${argv.map(winQuote).join(" ")} >> ${winQuote(logPath)} 2>&1"`;
2101
- writeFileSync(path, `\uFEFF' WokiToki daemon: started at login by \`wokitoki daemon install\`\r
2102
- CreateObject("WScript.Shell").Run ${vbs(line)}, 0, False\r
2103
- `, "utf16le");
2104
- try {
2105
- startService();
2106
- } catch (e) {
2107
- rmSync(path, { force: true });
2108
- throw e;
2109
- }
2194
+ writeFileSync(path, windowsLauncher(`cmd.exe /d /s /c "${argv.map(winQuote).join(" ")} >> ${winQuote(logPath)} 2>&1"`), "utf16le");
2195
+ startService();
2110
2196
  } else {
2111
2197
  writeFileSync(
2112
2198
  path,
@@ -2189,7 +2275,8 @@ function parseDuration(s) {
2189
2275
  function peerLine(p) {
2190
2276
  const approvals = p.state === "pending" ? ` you:${p.localApproved ? "approved" : "waiting"} peer:${p.remoteApproved ? "approved" : "waiting"}` : "";
2191
2277
  const online = p.online === null ? "" : p.online ? " [online]" : " [offline]";
2192
- return `${p.deviceId} ${safe(p.name).padEnd(20)} ${p.state.padEnd(8)}${approvals}${p.state === "active" ? online : ""}`;
2278
+ const queued = p.queued ? ` ${p.queued} queued` : "";
2279
+ return `${p.deviceId} ${safe(p.name).padEnd(20)} ${p.state.padEnd(8)}${approvals}${p.state === "active" ? online + queued : ""}`;
2193
2280
  }
2194
2281
  var joinText = (x) => [
2195
2282
  "Pairing request sent.",
@@ -2285,7 +2372,7 @@ async function startDaemonInBackground(waitConnectedMs = 3e3) {
2285
2372
  if (serviceInstalledFor(home)) startService();
2286
2373
  else {
2287
2374
  const log = openSync(homePaths(home).log, "a", 384);
2288
- spawn(process.execPath, [SELF, "--home", home, "daemon", "run"], { cwd: home, detached: true, stdio: ["ignore", log, log], windowsHide: true }).unref();
2375
+ spawn2(process.execPath, [SELF, "--home", home, "daemon", "run"], { cwd: home, detached: true, stdio: ["ignore", log, log], windowsHide: true }).unref();
2289
2376
  }
2290
2377
  for (let i = 0; i < 200 && !await up(); i++) await new Promise((r) => setTimeout(r, 100));
2291
2378
  if (!await up()) throw new Error(`wokitoki daemon failed to start; see ${homePaths(home).log}`);
@@ -2333,7 +2420,12 @@ async function enableAutostart() {
2333
2420
  loadHome(home);
2334
2421
  if (await callDaemon(socket, "GET", "/status").then(() => true, () => false)) await stopDaemon();
2335
2422
  const path = installService([process.execPath, BIN, "--home", home, "daemon", "run"], homePaths(home).log);
2336
- for (let i = 0; i < 100 && !await callDaemon(socket, "GET", "/status").then(() => true, () => false); i++) await new Promise((r) => setTimeout(r, 100));
2423
+ const up = () => callDaemon(socket, "GET", "/status").then(() => true, () => false);
2424
+ for (let i = 0; i < 100 && !await up(); i++) await new Promise((r) => setTimeout(r, 100));
2425
+ if (!await up()) {
2426
+ uninstallService();
2427
+ throw new Error("the login service did not start the daemon");
2428
+ }
2337
2429
  return path;
2338
2430
  }
2339
2431
  program.command("status").description("show daemon and network connection status").action(showStatus);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "wokitoki",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "wokitoki",
9
- "version": "0.3.2",
9
+ "version": "0.4.0",
10
10
  "license": "Apache-2.0",
11
11
  "os": [
12
12
  "darwin",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wokitoki",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Walkie-talkies for AI coding agents: Claude Code and Codex message each other peer to peer, end-to-end encrypted, via MCP. No server, no account.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Vishwas Pandey",