wokitoki 0.3.1 → 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
@@ -10,12 +10,16 @@ Your Claude Code talks to your friend's Claude Code (or Codex), directly, peer t
10
10
  No server. No account. No tunnel. Free and open source.
11
11
 
12
12
  [![CI](https://github.com/pandeyvishwas51-oss/wokitoki/actions/workflows/ci.yml/badge.svg)](https://github.com/pandeyvishwas51-oss/wokitoki/actions/workflows/ci.yml)
13
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/pandeyvishwas51-oss/wokitoki/badge)](https://scorecard.dev/viewer/?uri=github.com/pandeyvishwas51-oss/wokitoki)
13
14
  [![npm](https://img.shields.io/npm/v/wokitoki?color=FF5A1F)](https://www.npmjs.com/package/wokitoki)
14
15
  [![WokiToki MCP server on Glama](https://glama.ai/mcp/servers/pandeyvishwas51-oss/wokitoki/badges/score.svg)](https://glama.ai/mcp/servers/pandeyvishwas51-oss/wokitoki)
16
+ [![M8ven Score](https://m8ven.ai/badge/mcp/pandeyvishwas51-oss-wokitoki-atcfpk?v=f7bb2fa9b9dcfb420e5aafaf4712ed9e)](https://m8ven.ai/mcp/pandeyvishwas51-oss-wokitoki-atcfpk?s=readme)
15
17
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-FF5A1F)](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/LICENSE)
16
18
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-io.github.pandeyvishwas51--oss%2Fwokitoki-2B1306)](https://registry.modelcontextprotocol.io/v0/servers?search=wokitoki)
17
19
  [![Node 22.18+](https://img.shields.io/badge/node-22.18%2B-2B1306)](https://nodejs.org)
18
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
+
19
23
  **[▶ Watch the 1-minute demo (sound on)](https://pandeyvishwas51-oss.github.io/wokitoki/#demo)**
20
24
 
21
25
  [Website](https://pandeyvishwas51-oss.github.io/wokitoki/) · [Quick start](#quick-start) · [How it works](#how-it-works) · [FAQ](#faq) · [Security](#security-honestly)
@@ -153,6 +157,8 @@ Yes. It is open source under Apache-2.0, and there is no paid tier, hosted servi
153
157
  - Messages are end-to-end encrypted between the two daemons. In peer-to-peer mode, nothing is stored anywhere but the two machines.
154
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.
155
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.
156
162
  - No external security audit has been done. Don't send secrets.
157
163
 
158
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)
@@ -188,9 +194,13 @@ Every command accepts `--json` and `--home <dir>` (default `~/.wokitoki`). The o
188
194
 
189
195
  ```bash
190
196
  pnpm install
191
- pnpm check # strict TypeScript + 64 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
192
198
  ```
193
199
 
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.
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
+
194
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).
195
205
 
196
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
@@ -281,7 +281,11 @@ var init_src2 = __esm({
281
281
  utf8 = new TextEncoder();
282
282
  strictUtf8 = new TextDecoder("utf-8", { fatal: true });
283
283
  toB64 = (u) => Buffer.from(u).toString("base64url");
284
- fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64url"));
284
+ fromB64 = (s) => {
285
+ const bytes = new Uint8Array(Buffer.from(s, "base64url"));
286
+ if (Buffer.from(bytes).toString("base64url") !== s) throw new Error("invalid base64url: not in canonical form");
287
+ return bytes;
288
+ };
285
289
  toHex = (u) => Buffer.from(u).toString("hex");
286
290
  joinTranscript = (pairId, joiner) => utf8.encode(canonicalJson({ t: "agentmesh-pair-join-v1", pairId, joiner }));
287
291
  }
@@ -464,6 +468,14 @@ CREATE TABLE IF NOT EXISTS events (
464
468
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
465
469
  ).run(m.id, m.peer_id, m.pair_id, m.direction, m.text, m.reply_to, m.state, t, t);
466
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
+ }
467
479
  getMessage(id) {
468
480
  return this.db.prepare("SELECT * FROM messages WHERE id = ?").get(id);
469
481
  }
@@ -591,6 +603,10 @@ var init_dht_mailbox = __esm({
591
603
  clearInterval(this.timer);
592
604
  await this.running;
593
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
+ }
594
610
  round() {
595
611
  if (this.stopped || this.running) return;
596
612
  const peers = this.host.livePeers().filter((p) => p.state === "active" && !this.connected(p.device_id));
@@ -690,7 +706,7 @@ var init_dht_mailbox = __esm({
690
706
  import DHT from "hyperdht";
691
707
  import { randomUUID } from "node:crypto";
692
708
  import { StringDecoder } from "node:string_decoder";
693
- var CONNECT_TIMEOUT_MS, REQUEST_TIMEOUT_MS, P2PTransport;
709
+ var CONNECT_TIMEOUT_MS, WAKE_CHECK_MS, WAKE_GAP_MS, REQUEST_TIMEOUT_MS, P2PTransport;
694
710
  var init_p2p_transport = __esm({
695
711
  async "apps/bridge/src/p2p-transport.ts"() {
696
712
  "use strict";
@@ -699,6 +715,8 @@ var init_p2p_transport = __esm({
699
715
  init_transport();
700
716
  await init_dht_mailbox();
701
717
  CONNECT_TIMEOUT_MS = 3e4;
718
+ WAKE_CHECK_MS = 1e4;
719
+ WAKE_GAP_MS = 3e4;
702
720
  REQUEST_TIMEOUT_MS = 2e4;
703
721
  P2PTransport = class {
704
722
  kind = "p2p";
@@ -713,6 +731,7 @@ var init_p2p_transport = __esm({
713
731
  bootstrap;
714
732
  mailboxIntervalMs;
715
733
  mailbox;
734
+ wakeTimer;
716
735
  startup;
717
736
  constructor(host, bootstrap, mailboxIntervalMs = 3e4) {
718
737
  this.host = host;
@@ -741,6 +760,7 @@ var init_p2p_transport = __esm({
741
760
  this.host.audit("p2p_online", null, { firewalled: this.dht.firewalled });
742
761
  this.mailbox = new DhtMailbox(this.dht, this.host, (id) => this.conns.has(id), this.mailboxIntervalMs);
743
762
  this.mailbox.start();
763
+ this.watchForWake();
744
764
  for (const peer of this.host.livePeers()) {
745
765
  this.dial(peer.device_id, this.host.signPubOf(peer)).then(
746
766
  () => this.host.peerOnline(peer.device_id),
@@ -758,10 +778,34 @@ var init_p2p_transport = __esm({
758
778
  }
759
779
  }
760
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
+ }
761
804
  async stop() {
762
805
  this.stopped = true;
763
806
  await this.startup?.catch(() => {
764
807
  });
808
+ clearInterval(this.wakeTimer);
765
809
  await this.mailbox?.stop();
766
810
  for (const c of this.conns.values()) c.stream.destroy();
767
811
  this.conns.clear();
@@ -940,7 +984,7 @@ var init_p2p_transport = __esm({
940
984
  }
941
985
  const r = this.host.handleItem(frame.item, conn.deviceId);
942
986
  if (r.ok) this.send(conn, { t: "ack", id: frame.item.id });
943
- 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 });
944
988
  break;
945
989
  }
946
990
  case "ack":
@@ -1179,6 +1223,9 @@ var init_bridge = __esm({
1179
1223
  keys = /* @__PURE__ */ new Map();
1180
1224
  // ponytail: failed-join counters live in memory; a daemon restart resets them. Invites expire in <=1h anyway.
1181
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();
1182
1229
  stopped = false;
1183
1230
  flushing;
1184
1231
  flushAgain = false;
@@ -1254,6 +1301,7 @@ var init_bridge = __esm({
1254
1301
  remoteApproved: p.remote_ok === 1,
1255
1302
  role: p.role,
1256
1303
  online: this.transport.isPeerConnected(p.device_id),
1304
+ ...this.store.peerActivity(p.device_id),
1257
1305
  createdAt: p.created_at,
1258
1306
  updatedAt: p.updated_at
1259
1307
  };
@@ -1553,6 +1601,10 @@ var init_bridge = __esm({
1553
1601
  this.audit("security.envelope_rejected", env.from, meta);
1554
1602
  return { ok: false, permanent: true, code: "rejected" };
1555
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
+ }
1556
1608
  this.store.tx(() => {
1557
1609
  this.store.updatePeer(peer.device_id, { recv_seq: env.seq });
1558
1610
  if (payload.type === "receipt") {
@@ -1596,6 +1648,27 @@ var init_bridge = __esm({
1596
1648
  }
1597
1649
  });
1598
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
+ }
1599
1672
  // ---------------------------------------------------------------- outbox
1600
1673
  mustPeer(deviceId) {
1601
1674
  const p = this.store.getPeer(deviceId);
@@ -1906,13 +1979,20 @@ async function guard(fn) {
1906
1979
  }
1907
1980
  function createMcpServer(socketPath, version) {
1908
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
+ };
1909
1988
  const server = new McpServer({ name: "wokitoki", version });
1910
1989
  server.registerTool(
1911
1990
  "wokitoki_get_status",
1912
1991
  {
1913
1992
  title: "WokiToki status",
1914
1993
  description: "Show this WokiToki identity, network connection state (peer-to-peer or relay), pending outbox size and peer counts.",
1915
- annotations: { readOnlyHint: true, openWorldHint: false }
1994
+ inputSchema: {},
1995
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1916
1996
  },
1917
1997
  () => guard(async () => ok(await daemon2("GET", "/status")))
1918
1998
  );
@@ -1922,7 +2002,7 @@ function createMcpServer(socketPath, version) {
1922
2002
  title: "List WokiToki peers",
1923
2003
  description: 'List agents this machine is paired with. Only peers in state "active" can receive messages. Pairing and approval are done by the human with the wokitoki CLI, not by this tool.',
1924
2004
  inputSchema: { state: z7.enum(["pending", "active", "rejected", "revoked"]).optional().describe("filter by pairing state") },
1925
- annotations: { readOnlyHint: true, openWorldHint: false }
2005
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1926
2006
  },
1927
2007
  ({ state }) => guard(async () => ok((await daemon2("GET", `/peers${state ? `?state=${state}` : ""}`)).map(peerSummary)))
1928
2008
  );
@@ -1930,7 +2010,7 @@ function createMcpServer(socketPath, version) {
1930
2010
  "wokitoki_send_message",
1931
2011
  {
1932
2012
  title: "Send WokiToki message",
1933
- 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).',
1934
2014
  inputSchema: {
1935
2015
  to: z7.string().min(1).max(128).describe("peer name or device id"),
1936
2016
  text: z7.string().min(1).max(16e3).describe("message text"),
@@ -1938,7 +2018,15 @@ function createMcpServer(socketPath, version) {
1938
2018
  },
1939
2019
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1940
2020
  },
1941
- ({ 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
+ })
1942
2030
  );
1943
2031
  server.registerTool(
1944
2032
  "wokitoki_read_inbox",
@@ -1950,14 +2038,15 @@ function createMcpServer(socketPath, version) {
1950
2038
  limit: z7.number().int().min(1).max(200).optional().describe("max messages (default 50)"),
1951
2039
  markRead: z7.boolean().optional().describe("mark returned messages as read (default true)")
1952
2040
  },
1953
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
2041
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
2042
+ // marks messages read
1954
2043
  },
1955
2044
  ({ peer, limit, markRead }) => guard(async () => {
1956
2045
  const q = new URLSearchParams({ unread: "1", limit: String(limit ?? 50), ...peer ? { peer } : {} });
1957
2046
  const msgs = await daemon2("GET", `/messages?${q}`);
1958
2047
  if (msgs.length && markRead !== false) await daemon2("POST", "/messages/read", { ids: msgs.map((m) => m.id) });
1959
2048
  if (!msgs.length) return ok([], "No unread WokiToki messages.");
1960
- return ok(msgs.map(messageSummary), UNTRUSTED);
2049
+ return ok(msgs.map(messageSummary), await header());
1961
2050
  })
1962
2051
  );
1963
2052
  server.registerTool(
@@ -1969,11 +2058,11 @@ function createMcpServer(socketPath, version) {
1969
2058
  peer: z7.string().max(128).optional().describe("only this peer (name or device id)"),
1970
2059
  limit: z7.number().int().min(1).max(200).optional().describe("max messages (default 20)")
1971
2060
  },
1972
- annotations: { readOnlyHint: true, openWorldHint: false }
2061
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1973
2062
  },
1974
2063
  ({ peer, limit }) => guard(async () => {
1975
2064
  const q = new URLSearchParams({ limit: String(limit ?? 20), ...peer ? { peer } : {} });
1976
- return ok((await daemon2("GET", `/messages?${q}`)).map(messageSummary), UNTRUSTED);
2065
+ return ok((await daemon2("GET", `/messages?${q}`)).map(messageSummary), await header());
1977
2066
  })
1978
2067
  );
1979
2068
  return server;
@@ -1996,6 +2085,8 @@ var init_src3 = __esm({
1996
2085
  name: p.name,
1997
2086
  deviceId: p.deviceId,
1998
2087
  state: p.state,
2088
+ online: p.online,
2089
+ queued: p.queued,
1999
2090
  fingerprint: p.fingerprint
2000
2091
  });
2001
2092
  messageSummary = (m) => ({
@@ -2014,14 +2105,14 @@ var init_src3 = __esm({
2014
2105
 
2015
2106
  // apps/cli/src/main.ts
2016
2107
  import { Command } from "commander";
2017
- import { execFileSync as execFileSync2, spawn, spawnSync } from "node:child_process";
2108
+ import { execFileSync as execFileSync2, spawn as spawn2, spawnSync } from "node:child_process";
2018
2109
  import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
2019
2110
  import { homedir as homedir3 } from "node:os";
2020
2111
  import { fileURLToPath } from "node:url";
2021
2112
  import { dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
2022
2113
 
2023
2114
  // apps/cli/src/service.ts
2024
- import { execFileSync } from "node:child_process";
2115
+ import { execFileSync, spawn } from "node:child_process";
2025
2116
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2026
2117
  import { homedir } from "node:os";
2027
2118
  import { dirname, join } from "node:path";
@@ -2030,6 +2121,15 @@ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&
2030
2121
  var sh = (s) => `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
2031
2122
  var winQuote = (s) => `"${s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
2032
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");
2033
2133
  var appData = () => process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
2034
2134
  function servicePath() {
2035
2135
  if (process.platform === "darwin") return join(homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
@@ -2055,7 +2155,7 @@ function startService() {
2055
2155
  execFileSync("launchctl", ["bootstrap", domain, servicePath()], { stdio: "ignore" });
2056
2156
  }
2057
2157
  } else if (process.platform === "win32") {
2058
- 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();
2059
2159
  } else {
2060
2160
  execFileSync("systemctl", ["--user", "start", "wokitoki.service"], { stdio: "ignore" });
2061
2161
  }
@@ -2091,16 +2191,8 @@ ${argv.map((a) => ` <string>${xml(a)}</string>`).join("\n")}
2091
2191
  }
2092
2192
  execFileSync("launchctl", ["bootstrap", domain, path]);
2093
2193
  } else if (process.platform === "win32") {
2094
- const line = `cmd.exe /d /s /c "${argv.map(winQuote).join(" ")} >> ${winQuote(logPath)} 2>&1"`;
2095
- writeFileSync(path, `\uFEFF' WokiToki daemon: started at login by \`wokitoki daemon install\`\r
2096
- CreateObject("WScript.Shell").Run ${vbs(line)}, 0, False\r
2097
- `, "utf16le");
2098
- try {
2099
- startService();
2100
- } catch (e) {
2101
- rmSync(path, { force: true });
2102
- throw e;
2103
- }
2194
+ writeFileSync(path, windowsLauncher(`cmd.exe /d /s /c "${argv.map(winQuote).join(" ")} >> ${winQuote(logPath)} 2>&1"`), "utf16le");
2195
+ startService();
2104
2196
  } else {
2105
2197
  writeFileSync(
2106
2198
  path,
@@ -2183,7 +2275,8 @@ function parseDuration(s) {
2183
2275
  function peerLine(p) {
2184
2276
  const approvals = p.state === "pending" ? ` you:${p.localApproved ? "approved" : "waiting"} peer:${p.remoteApproved ? "approved" : "waiting"}` : "";
2185
2277
  const online = p.online === null ? "" : p.online ? " [online]" : " [offline]";
2186
- 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 : ""}`;
2187
2280
  }
2188
2281
  var joinText = (x) => [
2189
2282
  "Pairing request sent.",
@@ -2279,7 +2372,7 @@ async function startDaemonInBackground(waitConnectedMs = 3e3) {
2279
2372
  if (serviceInstalledFor(home)) startService();
2280
2373
  else {
2281
2374
  const log = openSync(homePaths(home).log, "a", 384);
2282
- 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();
2283
2376
  }
2284
2377
  for (let i = 0; i < 200 && !await up(); i++) await new Promise((r) => setTimeout(r, 100));
2285
2378
  if (!await up()) throw new Error(`wokitoki daemon failed to start; see ${homePaths(home).log}`);
@@ -2327,7 +2420,12 @@ async function enableAutostart() {
2327
2420
  loadHome(home);
2328
2421
  if (await callDaemon(socket, "GET", "/status").then(() => true, () => false)) await stopDaemon();
2329
2422
  const path = installService([process.execPath, BIN, "--home", home, "daemon", "run"], homePaths(home).log);
2330
- 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
+ }
2331
2429
  return path;
2332
2430
  }
2333
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.1",
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.1",
9
+ "version": "0.4.0",
10
10
  "license": "Apache-2.0",
11
11
  "os": [
12
12
  "darwin",
@@ -790,9 +790,9 @@
790
790
  "license": "MIT"
791
791
  },
792
792
  "node_modules/fast-uri": {
793
- "version": "3.1.8",
794
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
795
- "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
793
+ "version": "3.1.9",
794
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
795
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
796
796
  "funding": [
797
797
  {
798
798
  "type": "github",
@@ -1102,9 +1102,9 @@
1102
1102
  "license": "ISC"
1103
1103
  },
1104
1104
  "node_modules/jose": {
1105
- "version": "6.2.12",
1106
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
1107
- "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
1105
+ "version": "6.2.13",
1106
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz",
1107
+ "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==",
1108
1108
  "license": "MIT",
1109
1109
  "funding": {
1110
1110
  "url": "https://github.com/sponsors/panva"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wokitoki",
3
- "version": "0.3.1",
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",