@vdoninja/ninja-p2p 0.1.4 → 0.2.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/.agents/skills/ninja-p2p/SKILL.md +102 -2
- package/.claude/skills/ninja-p2p/SKILL.md +209 -130
- package/.codex/skills/ninja-p2p/SKILL.md +102 -2
- package/CHANGELOG.md +113 -0
- package/README.md +1077 -775
- package/dashboard.html +370 -50
- package/dist/agent-state.js +9 -0
- package/dist/cli-lib.d.ts +40 -0
- package/dist/cli-lib.js +165 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +501 -10
- package/dist/demo.d.ts +37 -0
- package/dist/demo.js +186 -0
- package/dist/doctor.d.ts +52 -0
- package/dist/doctor.js +219 -0
- package/dist/file-transfer.d.ts +15 -2
- package/dist/file-transfer.js +249 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/message-bus.d.ts +15 -0
- package/dist/message-bus.js +32 -3
- package/dist/peer-registry.js +7 -0
- package/dist/protocol.d.ts +78 -1
- package/dist/protocol.js +42 -6
- package/dist/shared-folders.js +20 -1
- package/dist/social-stream.d.ts +100 -0
- package/dist/social-stream.js +254 -0
- package/dist/swarm-manager.d.ts +164 -0
- package/dist/swarm-manager.js +957 -0
- package/dist/swarm-session.d.ts +197 -0
- package/dist/swarm-session.js +465 -0
- package/dist/swarm-wire.d.ts +48 -0
- package/dist/swarm-wire.js +86 -0
- package/dist/swarm.d.ts +258 -0
- package/dist/swarm.js +694 -0
- package/dist/vdo-bridge.d.ts +62 -1
- package/dist/vdo-bridge.js +273 -23
- package/dist/vdoninja-sdk-types.d.ts +75 -0
- package/dist/vdoninja-sdk-types.js +10 -0
- package/dist/wake.d.ts +83 -0
- package/dist/wake.js +206 -0
- package/docs/protocol-and-reliability.md +231 -38
- package/docs/sdk-wishlist.md +236 -0
- package/docs/security.md +197 -0
- package/docs/social-stream-bridge.md +390 -0
- package/package.json +125 -116
package/dist/cli-lib.js
CHANGED
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createMessageId, generateRoomName } from "./protocol.js";
|
|
3
3
|
import { parseSharedFolderSpecs, toSharedFolderSummaries } from "./shared-folders.js";
|
|
4
|
+
import { DEFAULT_WAKE_DEBOUNCE_MS, DEFAULT_WAKE_LIMIT_PER_MINUTE, } from "./wake.js";
|
|
5
|
+
import { DEFAULT_SSN_HOST, DEFAULT_SSN_IN_CHANNEL, DEFAULT_SSN_OUT_CHANNEL, DEFAULT_SSN_TOPIC, } from "./social-stream.js";
|
|
6
|
+
import { DEFAULT_SWARM_CHUNK_SIZE } from "./swarm.js";
|
|
4
7
|
export function helpText() {
|
|
5
8
|
return [
|
|
6
9
|
"ninja-p2p",
|
|
10
|
+
"A shared room and handoff layer that lets separate AI tools work as a team.",
|
|
7
11
|
"",
|
|
8
12
|
"Start here:",
|
|
9
13
|
" ninja-p2p menu",
|
|
14
|
+
" ninja-p2p demo",
|
|
10
15
|
" ninja-p2p start --id claude",
|
|
11
16
|
" ninja-p2p start --id codex",
|
|
12
17
|
"",
|
|
18
|
+
"Troubleshooting:",
|
|
19
|
+
" ninja-p2p doctor",
|
|
20
|
+
"",
|
|
21
|
+
"Swarm file transfer (every downloader becomes a source):",
|
|
22
|
+
" ninja-p2p seed ./big-file.zip --room my-room",
|
|
23
|
+
" ninja-p2p fetch big-file.zip --room my-room --out ./downloads",
|
|
24
|
+
" ninja-p2p fetch <file-id> --room my-room --seed",
|
|
25
|
+
"",
|
|
26
|
+
" --seed keep serving after the download finishes",
|
|
27
|
+
" --out <dir> where finished files land",
|
|
28
|
+
" --chunk-size <n> bytes per chunk (default 64000)",
|
|
29
|
+
"",
|
|
30
|
+
"Live chat bridge (Social Stream Ninja):",
|
|
31
|
+
" ninja-p2p ssn --session <id> --room ai-room --read-only",
|
|
32
|
+
" ninja-p2p ssn --session <ssn-session-id> --room ai-room",
|
|
33
|
+
" ninja-p2p ssn --session <id> --room ai-room --topic social --read-only --echo",
|
|
34
|
+
"",
|
|
35
|
+
" Publishes Twitch/YouTube/Kick chat into the room as shared team input.",
|
|
36
|
+
" Start read-only. Without it, an agent can reply to every platform at once:",
|
|
37
|
+
" ninja-p2p command --id claude social say '{\"text\":\"hi chat\"}'",
|
|
38
|
+
"",
|
|
13
39
|
"Install:",
|
|
14
40
|
" npm install -g @vdoninja/ninja-p2p @roamhq/wrtc",
|
|
15
41
|
" ninja-p2p install-skill codex",
|
|
@@ -37,6 +63,20 @@ export function helpText() {
|
|
|
37
63
|
" ninja-p2p command --id codex claude capabilities",
|
|
38
64
|
" ninja-p2p stop --id codex",
|
|
39
65
|
"",
|
|
66
|
+
"Wake on incoming messages (agents act without a human turn):",
|
|
67
|
+
" ninja-p2p start --id claude --on-message \"claude -p 'Check your ninja-p2p inbox'\"",
|
|
68
|
+
" ninja-p2p start --id codex --on-message \"codex exec 'Check your ninja-p2p inbox'\"",
|
|
69
|
+
" ninja-p2p wait --id codex && echo \"mail arrived\"",
|
|
70
|
+
" while ninja-p2p wait --id codex; do codex exec 'handle inbox'; done",
|
|
71
|
+
"",
|
|
72
|
+
" --on-message <cmd> shell command to run when messages arrive",
|
|
73
|
+
" --wake-debounce <ms> batch window before firing (default 750)",
|
|
74
|
+
" --wake-limit <n> max wakes per minute, 0 = unlimited (default 30)",
|
|
75
|
+
" --timeout <ms> wait: give up after this long, 0 = forever",
|
|
76
|
+
"",
|
|
77
|
+
" The wake command receives NINJA_ID, NINJA_STATE_DIR, NINJA_WAKE_COUNT,",
|
|
78
|
+
" NINJA_WAKE_FROM, NINJA_WAKE_TYPES, and NINJA_WAKE_TEXT.",
|
|
79
|
+
"",
|
|
40
80
|
"Use:",
|
|
41
81
|
" ninja-p2p connect --room my-room --name Claude",
|
|
42
82
|
" ninja-p2p chat --room my-room --name Steve \"hello\"",
|
|
@@ -86,6 +126,24 @@ export function parseCliArgs(argv, env = process.env) {
|
|
|
86
126
|
},
|
|
87
127
|
};
|
|
88
128
|
}
|
|
129
|
+
if (kind === "demo") {
|
|
130
|
+
const parsedDemo = parseOptions(args, env);
|
|
131
|
+
const passwordValue = getSingleValue(parsedDemo.values, "password") ?? env.NINJA_PASSWORD;
|
|
132
|
+
return {
|
|
133
|
+
kind,
|
|
134
|
+
room: getSingleValue(parsedDemo.values, "room") || null,
|
|
135
|
+
password: passwordValue === "false" ? false : (passwordValue || false),
|
|
136
|
+
timeoutMs: parsePositiveInt(getSingleValue(parsedDemo.values, "timeout"), 20_000),
|
|
137
|
+
keep: parsedDemo.flags.has("keep"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (kind === "doctor") {
|
|
141
|
+
const parsedDoctor = parseOptions(args, env);
|
|
142
|
+
return {
|
|
143
|
+
kind,
|
|
144
|
+
stateRoot: getSingleValue(parsedDoctor.values, "state-root") || getStateRoot(env),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
89
147
|
if (kind === "install-skill") {
|
|
90
148
|
const runtime = (args.shift() ?? "").toLowerCase();
|
|
91
149
|
if (runtime !== "codex" && runtime !== "claude") {
|
|
@@ -133,6 +191,77 @@ export function parseCliArgs(argv, env = process.env) {
|
|
|
133
191
|
take: parsePositiveInt(getSingleValue(parsed.values, "take"), 20),
|
|
134
192
|
peek: getSingleValue(parsed.values, "peek") === "true",
|
|
135
193
|
};
|
|
194
|
+
case "wait":
|
|
195
|
+
return {
|
|
196
|
+
kind,
|
|
197
|
+
stateDir: resolveStateDir(parsed.values, env),
|
|
198
|
+
// 0 means wait indefinitely, which is what a `while` loop wants.
|
|
199
|
+
timeoutMs: parseNonNegativeInt(getSingleValue(parsed.values, "timeout"), 0),
|
|
200
|
+
intervalMs: parsePositiveInt(getSingleValue(parsed.values, "interval"), 500),
|
|
201
|
+
};
|
|
202
|
+
case "seed":
|
|
203
|
+
{
|
|
204
|
+
if (positional.length < 1) {
|
|
205
|
+
throw new Error("seed requires a file path");
|
|
206
|
+
}
|
|
207
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
208
|
+
requireRoom: false,
|
|
209
|
+
requireStateDir: false,
|
|
210
|
+
allowGeneratedRoom: true,
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
kind,
|
|
214
|
+
options,
|
|
215
|
+
filePath: positional.join(" "),
|
|
216
|
+
chunkSize: parsePositiveInt(getSingleValue(parsed.values, "chunk-size"), DEFAULT_SWARM_CHUNK_SIZE),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
case "fetch":
|
|
220
|
+
{
|
|
221
|
+
if (positional.length < 1) {
|
|
222
|
+
throw new Error("fetch requires a file id or name");
|
|
223
|
+
}
|
|
224
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
225
|
+
requireRoom: true,
|
|
226
|
+
requireStateDir: false,
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
kind,
|
|
230
|
+
options,
|
|
231
|
+
query: positional.join(" "),
|
|
232
|
+
outDir: getSingleValue(parsed.values, "out") ?? null,
|
|
233
|
+
keepSeeding: parsed.flags.has("seed"),
|
|
234
|
+
timeoutMs: parseNonNegativeInt(getSingleValue(parsed.values, "timeout"), 300_000),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
case "ssn":
|
|
238
|
+
{
|
|
239
|
+
const session = getSingleValue(parsed.values, "session") || env.SSN_SESSION || positional[0];
|
|
240
|
+
if (!session) {
|
|
241
|
+
throw new Error("ssn requires a Social Stream Ninja session id; use --session <id>");
|
|
242
|
+
}
|
|
243
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
244
|
+
requireRoom: false,
|
|
245
|
+
requireStateDir: false,
|
|
246
|
+
allowGeneratedRoom: true,
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
kind,
|
|
250
|
+
options: {
|
|
251
|
+
...options,
|
|
252
|
+
name: getSingleValue(parsed.values, "name") || env.NINJA_NAME || "Social Stream",
|
|
253
|
+
streamId: getSingleValue(parsed.values, "id") || env.NINJA_ID || "social",
|
|
254
|
+
role: getSingleValue(parsed.values, "role") || env.NINJA_ROLE || "bridge",
|
|
255
|
+
},
|
|
256
|
+
session,
|
|
257
|
+
topic: getSingleValue(parsed.values, "topic") || DEFAULT_SSN_TOPIC,
|
|
258
|
+
host: getSingleValue(parsed.values, "ssn-host") || env.SSN_HOST || DEFAULT_SSN_HOST,
|
|
259
|
+
inChannel: parseNonNegativeInt(getSingleValue(parsed.values, "in-channel"), DEFAULT_SSN_IN_CHANNEL),
|
|
260
|
+
outChannel: parseNonNegativeInt(getSingleValue(parsed.values, "out-channel"), DEFAULT_SSN_OUT_CHANNEL),
|
|
261
|
+
echo: parsed.flags.has("echo"),
|
|
262
|
+
readOnly: parsed.flags.has("read-only"),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
136
265
|
case "connect":
|
|
137
266
|
return { kind, options: buildCommonOptions(parsed.values, env, { requireRoom: false, requireStateDir: false, allowGeneratedRoom: true }) };
|
|
138
267
|
case "chat":
|
|
@@ -402,15 +531,22 @@ export function getSkillInstallTargets(runtime, env = process.env) {
|
|
|
402
531
|
return [primary];
|
|
403
532
|
}
|
|
404
533
|
export function getDefaultStateDir(streamId, env = process.env) {
|
|
534
|
+
return path.join(getStateRoot(env), streamId);
|
|
535
|
+
}
|
|
536
|
+
/** Parent folder holding every sidecar's state, used by `doctor`. */
|
|
537
|
+
export function getStateRoot(env = process.env) {
|
|
405
538
|
const home = env.USERPROFILE || env.HOME;
|
|
406
539
|
if (!home) {
|
|
407
540
|
throw new Error("cannot determine home directory");
|
|
408
541
|
}
|
|
409
|
-
return path.join(home, ".ninja-p2p"
|
|
542
|
+
return path.join(home, ".ninja-p2p");
|
|
410
543
|
}
|
|
544
|
+
/** Flags that stand alone. Everything else still requires an explicit value. */
|
|
545
|
+
const BOOLEAN_FLAGS = new Set(["keep", "echo", "read-only", "seed"]);
|
|
411
546
|
function parseOptions(argv, env) {
|
|
412
547
|
const values = {};
|
|
413
548
|
const positional = [];
|
|
549
|
+
const flags = new Set();
|
|
414
550
|
for (let i = 0; i < argv.length; i += 1) {
|
|
415
551
|
const arg = argv[i];
|
|
416
552
|
if (!arg.startsWith("--")) {
|
|
@@ -418,6 +554,10 @@ function parseOptions(argv, env) {
|
|
|
418
554
|
continue;
|
|
419
555
|
}
|
|
420
556
|
const key = arg.slice(2);
|
|
557
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
558
|
+
flags.add(key);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
421
561
|
const next = argv[i + 1];
|
|
422
562
|
if (!next || next.startsWith("--")) {
|
|
423
563
|
throw new Error(`missing value for --${key}`);
|
|
@@ -448,6 +588,8 @@ function parseOptions(argv, env) {
|
|
|
448
588
|
values["state-dir"] = env.NINJA_STATE_DIR;
|
|
449
589
|
if (!values["wait-ms"] && env.NINJA_WAIT_MS)
|
|
450
590
|
values["wait-ms"] = env.NINJA_WAIT_MS;
|
|
591
|
+
if (!values["on-message"] && env.NINJA_ON_MESSAGE)
|
|
592
|
+
values["on-message"] = env.NINJA_ON_MESSAGE;
|
|
451
593
|
if (!values.runtime && env.NINJA_RUNTIME)
|
|
452
594
|
values.runtime = env.NINJA_RUNTIME;
|
|
453
595
|
if (!values.provider && env.NINJA_PROVIDER)
|
|
@@ -464,7 +606,7 @@ function parseOptions(argv, env) {
|
|
|
464
606
|
values.ask = env.NINJA_ASKS.split(";").map((item) => item.trim()).filter(Boolean);
|
|
465
607
|
if (!values.share && env.NINJA_SHARE)
|
|
466
608
|
values.share = env.NINJA_SHARE.split(";").map((item) => item.trim()).filter(Boolean);
|
|
467
|
-
return { values, positional };
|
|
609
|
+
return { values, positional, flags };
|
|
468
610
|
}
|
|
469
611
|
function buildCommonOptions(values, env, options) {
|
|
470
612
|
const requireRoom = options?.requireRoom ?? true;
|
|
@@ -499,6 +641,7 @@ function buildCommonOptions(values, env, options) {
|
|
|
499
641
|
stateDir,
|
|
500
642
|
agentProfile,
|
|
501
643
|
sharedFolders,
|
|
644
|
+
wake: buildWakeConfig(values, env),
|
|
502
645
|
};
|
|
503
646
|
}
|
|
504
647
|
function resolveStateDir(values, env, fallbackStreamId) {
|
|
@@ -519,6 +662,26 @@ function parsePositiveInt(raw, fallback) {
|
|
|
519
662
|
return fallback;
|
|
520
663
|
return value;
|
|
521
664
|
}
|
|
665
|
+
function parseNonNegativeInt(raw, fallback) {
|
|
666
|
+
if (!raw)
|
|
667
|
+
return fallback;
|
|
668
|
+
const value = Number.parseInt(raw, 10);
|
|
669
|
+
if (!Number.isFinite(value) || value < 0)
|
|
670
|
+
return fallback;
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
function buildWakeConfig(values, env) {
|
|
674
|
+
const command = (getSingleValue(values, "on-message") || env.NINJA_ON_MESSAGE || "").trim();
|
|
675
|
+
if (!command)
|
|
676
|
+
return null;
|
|
677
|
+
return {
|
|
678
|
+
command,
|
|
679
|
+
debounceMs: parseNonNegativeInt(getSingleValue(values, "wake-debounce") ?? env.NINJA_WAKE_DEBOUNCE, DEFAULT_WAKE_DEBOUNCE_MS),
|
|
680
|
+
// 0 disables the rate limit. Keep a default ceiling so a pair of agents
|
|
681
|
+
// that reply to each other cannot spin unattended forever.
|
|
682
|
+
limitPerMinute: parseNonNegativeInt(getSingleValue(values, "wake-limit") ?? env.NINJA_WAKE_LIMIT, DEFAULT_WAKE_LIMIT_PER_MINUTE),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
522
685
|
function shouldUseStateMode(values, env) {
|
|
523
686
|
if (getSingleValue(values, "state-dir") || env.NINJA_STATE_DIR)
|
|
524
687
|
return true;
|
package/dist/cli.d.ts
CHANGED
|
@@ -23,3 +23,5 @@ export declare function buildSidecarCommandResponse(bridge: VDOBridge, stateDir:
|
|
|
23
23
|
export declare function maybeHandleSidecarCommand(bridge: VDOBridge, stateDir: string, envelope: MessageEnvelope): boolean;
|
|
24
24
|
export declare function createPeerNoticeEnvelope(peerIdentity: PeerIdentity, kind: "peer_discovered" | "peer_updated" | "peer_left", details: Record<string, unknown>): MessageEnvelope;
|
|
25
25
|
export declare function recordPeerNotice(fingerprints: Map<string, string>, peerIdentity: PeerIdentity, kind: "peer_discovered" | "peer_updated", details: Record<string, unknown>): MessageEnvelope | null;
|
|
26
|
+
export declare const DASHBOARD_URL = "https://steveseguin.github.io/ninja-p2p/dashboard.html";
|
|
27
|
+
export declare function buildDashboardUrl(room: string, password: string | false): string;
|