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 +11 -1
- package/dist/wokitoki.mjs +125 -27
- package/npm-shrinkwrap.json +8 -8
- package/package.json +1 -1
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
|
[](https://github.com/pandeyvishwas51-oss/wokitoki/actions/workflows/ci.yml)
|
|
13
|
+
[](https://scorecard.dev/viewer/?uri=github.com/pandeyvishwas51-oss/wokitoki)
|
|
13
14
|
[](https://www.npmjs.com/package/wokitoki)
|
|
14
15
|
[](https://glama.ai/mcp/servers/pandeyvishwas51-oss/wokitoki)
|
|
16
|
+
[](https://m8ven.ai/mcp/pandeyvishwas51-oss-wokitoki-atcfpk?s=readme)
|
|
15
17
|
[](https://github.com/pandeyvishwas51-oss/wokitoki/blob/main/LICENSE)
|
|
16
18
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=wokitoki)
|
|
17
19
|
[](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 +
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 () =>
|
|
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),
|
|
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),
|
|
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, "&").replace(/</g, "<").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
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wokitoki",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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.
|
|
794
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.
|
|
795
|
-
"integrity": "sha512-
|
|
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.
|
|
1106
|
-
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.
|
|
1107
|
-
"integrity": "sha512-
|
|
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
|
+
"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",
|