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 +7 -1
- package/dist/wokitoki.mjs +114 -22
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ No server. No account. No tunnel. Free and open source.
|
|
|
18
18
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=wokitoki)
|
|
19
19
|
[](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 +
|
|
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
|
|
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:
|
|
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 () =>
|
|
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),
|
|
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),
|
|
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, "&").replace(/</g, "<").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
|
-
|
|
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
|
-
|
|
2101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
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",
|
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",
|