@vdoninja/ninja-p2p 0.1.1 → 0.1.2
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 +142 -0
- package/.agents/skills/ninja-p2p/agents/openai.yaml +5 -0
- package/.claude/skills/ninja-p2p/SKILL.md +130 -0
- package/.codex/skills/ninja-p2p/SKILL.md +142 -0
- package/.codex/skills/ninja-p2p/agents/openai.yaml +5 -0
- package/README.md +628 -53
- package/dashboard.html +595 -56
- package/dist/agent-state.d.ts +167 -0
- package/dist/agent-state.js +304 -0
- package/dist/cli-lib.d.ts +132 -0
- package/dist/cli-lib.js +602 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +1337 -0
- package/dist/file-transfer.d.ts +54 -0
- package/dist/file-transfer.js +241 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/message-bus.d.ts +1 -0
- package/dist/message-bus.js +10 -3
- package/dist/peer-registry.d.ts +2 -1
- package/dist/peer-registry.js +4 -0
- package/dist/protocol.d.ts +57 -1
- package/dist/shared-folders.d.ts +31 -0
- package/dist/shared-folders.js +137 -0
- package/dist/vdo-bridge.d.ts +13 -1
- package/dist/vdo-bridge.js +55 -9
- package/package.json +25 -2
- package/skills/ninja-p2p/SKILL.md +0 -70
package/dist/cli-lib.js
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createMessageId, generateRoomName } from "./protocol.js";
|
|
3
|
+
import { parseSharedFolderSpecs, toSharedFolderSummaries } from "./shared-folders.js";
|
|
4
|
+
export function helpText() {
|
|
5
|
+
return [
|
|
6
|
+
"ninja-p2p",
|
|
7
|
+
"",
|
|
8
|
+
"Start here:",
|
|
9
|
+
" ninja-p2p menu",
|
|
10
|
+
" ninja-p2p start --id claude",
|
|
11
|
+
" ninja-p2p start --id codex",
|
|
12
|
+
"",
|
|
13
|
+
"Install:",
|
|
14
|
+
" npm install -g @vdoninja/ninja-p2p @roamhq/wrtc",
|
|
15
|
+
" ninja-p2p install-skill codex",
|
|
16
|
+
" ninja-p2p install-skill claude",
|
|
17
|
+
"",
|
|
18
|
+
"Agent mode:",
|
|
19
|
+
" ninja-p2p start --room ai-room --id codex",
|
|
20
|
+
" ninja-p2p room --id codex",
|
|
21
|
+
" ninja-p2p status --id codex",
|
|
22
|
+
" ninja-p2p notify --id codex",
|
|
23
|
+
" ninja-p2p read --id codex --take 10",
|
|
24
|
+
" ninja-p2p start --room ai-room --id codex --share docs=./docs",
|
|
25
|
+
" ninja-p2p shares --id codex worker",
|
|
26
|
+
" ninja-p2p list-files --id codex worker docs",
|
|
27
|
+
" ninja-p2p get-file --id codex worker docs guide.md",
|
|
28
|
+
" ninja-p2p send-file --id codex worker .\\notes.txt",
|
|
29
|
+
" ninja-p2p send-image --id codex worker .\\diagram.png",
|
|
30
|
+
" ninja-p2p plan --id codex planner \"Propose a safe rollout plan\"",
|
|
31
|
+
" ninja-p2p dm --id codex human \"working on it\"",
|
|
32
|
+
" ninja-p2p task --id codex worker \"Implement the patch\"",
|
|
33
|
+
" ninja-p2p review --id codex reviewer \"Review the change\"",
|
|
34
|
+
" ninja-p2p approve --id codex reviewer \"Approve this plan before I continue\"",
|
|
35
|
+
" ninja-p2p respond --id codex planner <requestId> '{\"approved\":true}'",
|
|
36
|
+
" ninja-p2p event --id codex builds build_failed '{\"job\":\"api\"}'",
|
|
37
|
+
" ninja-p2p command --id codex claude capabilities",
|
|
38
|
+
" ninja-p2p stop --id codex",
|
|
39
|
+
"",
|
|
40
|
+
"Use:",
|
|
41
|
+
" ninja-p2p connect --room my-room --name Claude",
|
|
42
|
+
" ninja-p2p chat --room my-room --name Steve \"hello\"",
|
|
43
|
+
" ninja-p2p dm --room my-room --name Steve worker_bot \"hello\"",
|
|
44
|
+
" ninja-p2p shares --room my-room --name Steve worker_bot",
|
|
45
|
+
" ninja-p2p list-files --room my-room --name Steve worker_bot docs",
|
|
46
|
+
" ninja-p2p get-file --room my-room --name Steve worker_bot docs guide.md",
|
|
47
|
+
" ninja-p2p send-file --room my-room --name Steve worker_bot ./notes.txt",
|
|
48
|
+
" ninja-p2p plan --room my-room --name Planner worker_bot \"Suggest a plan\"",
|
|
49
|
+
" ninja-p2p task --room my-room --name Planner worker_bot \"Implement this\"",
|
|
50
|
+
" ninja-p2p command --room my-room --name Steve worker_bot status",
|
|
51
|
+
"",
|
|
52
|
+
"Connect mode commands:",
|
|
53
|
+
" hello world send chat to the room",
|
|
54
|
+
" /dm worker_bot hello send direct chat",
|
|
55
|
+
" /shares worker_bot list shared folders",
|
|
56
|
+
" /ls worker_bot docs list a shared folder",
|
|
57
|
+
" /get worker_bot docs x request one shared file",
|
|
58
|
+
" /file worker_bot a.txt send a file",
|
|
59
|
+
" /image worker_bot a.png send an image",
|
|
60
|
+
" /cmd worker_bot status send command",
|
|
61
|
+
" /plan planner fix x ask for a plan",
|
|
62
|
+
" /task worker_bot fix x send a task request",
|
|
63
|
+
" /review reviewer diff send a review request",
|
|
64
|
+
" /approve reviewer plan ask for approval",
|
|
65
|
+
" /respond worker <id> {} send a command response",
|
|
66
|
+
" /event events ping publish event",
|
|
67
|
+
" /status busy writing update local status",
|
|
68
|
+
" /peers list peers",
|
|
69
|
+
" /quit disconnect",
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
export function parseCliArgs(argv, env = process.env) {
|
|
73
|
+
const args = [...argv];
|
|
74
|
+
const kind = (args.shift() ?? "help").toLowerCase();
|
|
75
|
+
if (kind === "help" || kind === "--help" || kind === "-h") {
|
|
76
|
+
return { kind: "help" };
|
|
77
|
+
}
|
|
78
|
+
if (kind === "menu") {
|
|
79
|
+
const parsed = parseOptions(args, env);
|
|
80
|
+
const options = buildCommonOptions(parsed.values, env, { requireRoom: false, requireStateDir: false, allowGeneratedRoom: false });
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
options: {
|
|
84
|
+
...options,
|
|
85
|
+
stateDir: resolveStateDir(parsed.values, env, options.streamId),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (kind === "install-skill") {
|
|
90
|
+
const runtime = (args.shift() ?? "").toLowerCase();
|
|
91
|
+
if (runtime !== "codex" && runtime !== "claude") {
|
|
92
|
+
throw new Error("install-skill requires 'codex' or 'claude'");
|
|
93
|
+
}
|
|
94
|
+
return { kind, runtime };
|
|
95
|
+
}
|
|
96
|
+
const parsed = parseOptions(args, env);
|
|
97
|
+
const positional = parsed.positional;
|
|
98
|
+
switch (kind) {
|
|
99
|
+
case "start":
|
|
100
|
+
{
|
|
101
|
+
const options = buildCommonOptions(parsed.values, env, { requireRoom: false, requireStateDir: false, allowGeneratedRoom: true });
|
|
102
|
+
return {
|
|
103
|
+
kind,
|
|
104
|
+
options: {
|
|
105
|
+
...options,
|
|
106
|
+
stateDir: resolveStateDir(parsed.values, env, options.streamId),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
case "stop":
|
|
111
|
+
return { kind, stateDir: resolveStateDir(parsed.values, env) };
|
|
112
|
+
case "status":
|
|
113
|
+
return { kind, stateDir: resolveStateDir(parsed.values, env) };
|
|
114
|
+
case "room":
|
|
115
|
+
return { kind, stateDir: resolveStateDir(parsed.values, env) };
|
|
116
|
+
case "agent":
|
|
117
|
+
{
|
|
118
|
+
const options = buildCommonOptions(parsed.values, env, { requireRoom: false, requireStateDir: false, allowGeneratedRoom: true });
|
|
119
|
+
return {
|
|
120
|
+
kind,
|
|
121
|
+
options: {
|
|
122
|
+
...options,
|
|
123
|
+
stateDir: resolveStateDir(parsed.values, env, options.streamId),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
case "notify":
|
|
128
|
+
return { kind, stateDir: resolveStateDir(parsed.values, env) };
|
|
129
|
+
case "read":
|
|
130
|
+
return {
|
|
131
|
+
kind,
|
|
132
|
+
stateDir: resolveStateDir(parsed.values, env),
|
|
133
|
+
take: parsePositiveInt(getSingleValue(parsed.values, "take"), 20),
|
|
134
|
+
peek: getSingleValue(parsed.values, "peek") === "true",
|
|
135
|
+
};
|
|
136
|
+
case "connect":
|
|
137
|
+
return { kind, options: buildCommonOptions(parsed.values, env, { requireRoom: false, requireStateDir: false, allowGeneratedRoom: true }) };
|
|
138
|
+
case "chat":
|
|
139
|
+
if (positional.length < 1) {
|
|
140
|
+
throw new Error("chat requires text");
|
|
141
|
+
}
|
|
142
|
+
{
|
|
143
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
144
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
145
|
+
requireRoom: !useStateMode,
|
|
146
|
+
requireStateDir: false,
|
|
147
|
+
});
|
|
148
|
+
if (useStateMode) {
|
|
149
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
kind,
|
|
153
|
+
options,
|
|
154
|
+
text: positional.join(" "),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
case "dm":
|
|
158
|
+
if (positional.length < 2) {
|
|
159
|
+
throw new Error("dm requires a target and text");
|
|
160
|
+
}
|
|
161
|
+
{
|
|
162
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
163
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
164
|
+
requireRoom: !useStateMode,
|
|
165
|
+
requireStateDir: false,
|
|
166
|
+
});
|
|
167
|
+
if (useStateMode) {
|
|
168
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
kind,
|
|
172
|
+
options,
|
|
173
|
+
target: positional[0],
|
|
174
|
+
text: positional.slice(1).join(" "),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
case "send-file":
|
|
178
|
+
case "send-image":
|
|
179
|
+
if (positional.length < 2) {
|
|
180
|
+
throw new Error(`${kind} requires a target and file path`);
|
|
181
|
+
}
|
|
182
|
+
{
|
|
183
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
184
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
185
|
+
requireRoom: !useStateMode,
|
|
186
|
+
requireStateDir: false,
|
|
187
|
+
});
|
|
188
|
+
if (useStateMode) {
|
|
189
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
kind,
|
|
193
|
+
options,
|
|
194
|
+
target: positional[0],
|
|
195
|
+
filePath: positional.slice(1).join(" "),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
case "shares":
|
|
199
|
+
if (positional.length < 1) {
|
|
200
|
+
throw new Error("shares requires a target");
|
|
201
|
+
}
|
|
202
|
+
{
|
|
203
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
204
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
205
|
+
requireRoom: !useStateMode,
|
|
206
|
+
requireStateDir: false,
|
|
207
|
+
});
|
|
208
|
+
if (useStateMode) {
|
|
209
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
kind,
|
|
213
|
+
options,
|
|
214
|
+
target: positional[0],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
case "list-files":
|
|
218
|
+
if (positional.length < 2) {
|
|
219
|
+
throw new Error("list-files requires a target and share name");
|
|
220
|
+
}
|
|
221
|
+
{
|
|
222
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
223
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
224
|
+
requireRoom: !useStateMode,
|
|
225
|
+
requireStateDir: false,
|
|
226
|
+
});
|
|
227
|
+
if (useStateMode) {
|
|
228
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
kind,
|
|
232
|
+
options,
|
|
233
|
+
target: positional[0],
|
|
234
|
+
share: positional[1],
|
|
235
|
+
folderPath: positional.slice(2).join(" "),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
case "get-file":
|
|
239
|
+
if (positional.length < 3) {
|
|
240
|
+
throw new Error("get-file requires a target, share name, and file path");
|
|
241
|
+
}
|
|
242
|
+
{
|
|
243
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
244
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
245
|
+
requireRoom: !useStateMode,
|
|
246
|
+
requireStateDir: false,
|
|
247
|
+
});
|
|
248
|
+
if (useStateMode) {
|
|
249
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
kind,
|
|
253
|
+
options,
|
|
254
|
+
target: positional[0],
|
|
255
|
+
share: positional[1],
|
|
256
|
+
filePath: positional.slice(2).join(" "),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
case "command":
|
|
260
|
+
if (positional.length < 2) {
|
|
261
|
+
throw new Error("command requires a target and command name");
|
|
262
|
+
}
|
|
263
|
+
{
|
|
264
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
265
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
266
|
+
requireRoom: !useStateMode,
|
|
267
|
+
requireStateDir: false,
|
|
268
|
+
});
|
|
269
|
+
if (useStateMode) {
|
|
270
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
kind,
|
|
274
|
+
options,
|
|
275
|
+
target: positional[0],
|
|
276
|
+
command: positional[1],
|
|
277
|
+
args: positional[2] ? parseJsonMaybe(positional.slice(2).join(" ")) : undefined,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
case "respond":
|
|
281
|
+
if (positional.length < 2) {
|
|
282
|
+
throw new Error("respond requires a target and request id");
|
|
283
|
+
}
|
|
284
|
+
{
|
|
285
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
286
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
287
|
+
requireRoom: !useStateMode,
|
|
288
|
+
requireStateDir: false,
|
|
289
|
+
});
|
|
290
|
+
if (useStateMode) {
|
|
291
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
kind,
|
|
295
|
+
options,
|
|
296
|
+
target: positional[0],
|
|
297
|
+
requestId: positional[1],
|
|
298
|
+
result: positional[2] ? parseJsonMaybe(positional.slice(2).join(" ")) : undefined,
|
|
299
|
+
error: getSingleValue(parsed.values, "error"),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
case "event":
|
|
303
|
+
if (positional.length < 2) {
|
|
304
|
+
throw new Error("event requires a topic and event name");
|
|
305
|
+
}
|
|
306
|
+
{
|
|
307
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
308
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
309
|
+
requireRoom: !useStateMode,
|
|
310
|
+
requireStateDir: false,
|
|
311
|
+
});
|
|
312
|
+
if (useStateMode) {
|
|
313
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
kind,
|
|
317
|
+
options,
|
|
318
|
+
topic: positional[0],
|
|
319
|
+
eventKind: positional[1],
|
|
320
|
+
data: positional[2] ? parseJsonMaybe(positional.slice(2).join(" ")) : undefined,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
case "task":
|
|
324
|
+
case "review":
|
|
325
|
+
case "plan":
|
|
326
|
+
case "approve":
|
|
327
|
+
if (positional.length < 2) {
|
|
328
|
+
throw new Error(`${kind} requires a target and request text`);
|
|
329
|
+
}
|
|
330
|
+
{
|
|
331
|
+
const useStateMode = shouldUseStateMode(parsed.values, env);
|
|
332
|
+
const options = buildCommonOptions(parsed.values, env, {
|
|
333
|
+
requireRoom: !useStateMode,
|
|
334
|
+
requireStateDir: false,
|
|
335
|
+
});
|
|
336
|
+
if (useStateMode) {
|
|
337
|
+
options.stateDir = resolveStateDir(parsed.values, env);
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
kind,
|
|
341
|
+
options,
|
|
342
|
+
target: positional[0],
|
|
343
|
+
request: positional.slice(1).join(" "),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
default:
|
|
347
|
+
throw new Error(`unknown command: ${kind}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
export function defaultStreamId(name) {
|
|
351
|
+
const base = name
|
|
352
|
+
.trim()
|
|
353
|
+
.toLowerCase()
|
|
354
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
355
|
+
.replace(/^_+|_+$/g, "") || "agent";
|
|
356
|
+
return `${base}_${createMessageId().slice(0, 6)}`;
|
|
357
|
+
}
|
|
358
|
+
export function defaultNameForRuntime(runtime) {
|
|
359
|
+
const value = (runtime || "").trim().toLowerCase();
|
|
360
|
+
if (value.includes("claude"))
|
|
361
|
+
return "Claude";
|
|
362
|
+
if (value.includes("codex"))
|
|
363
|
+
return "Codex";
|
|
364
|
+
return "Agent";
|
|
365
|
+
}
|
|
366
|
+
export function defaultStreamIdForRuntime(name, runtime) {
|
|
367
|
+
const runtimeValue = (runtime || "").trim().toLowerCase();
|
|
368
|
+
const normalizedName = name.trim().toLowerCase();
|
|
369
|
+
if (runtimeValue.includes("claude") || normalizedName === "claude")
|
|
370
|
+
return "claude";
|
|
371
|
+
if (runtimeValue.includes("codex") || normalizedName === "codex")
|
|
372
|
+
return "codex";
|
|
373
|
+
return defaultStreamId(name);
|
|
374
|
+
}
|
|
375
|
+
export function parseJsonMaybe(value) {
|
|
376
|
+
try {
|
|
377
|
+
return JSON.parse(value);
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
export function getSkillInstallTarget(runtime, env = process.env) {
|
|
384
|
+
const home = env.USERPROFILE || env.HOME;
|
|
385
|
+
if (!home) {
|
|
386
|
+
throw new Error("cannot determine home directory");
|
|
387
|
+
}
|
|
388
|
+
if (runtime === "codex") {
|
|
389
|
+
return path.join(home, ".codex", "skills", "ninja-p2p");
|
|
390
|
+
}
|
|
391
|
+
return path.join(home, ".claude", "skills", "ninja-p2p");
|
|
392
|
+
}
|
|
393
|
+
export function getSkillInstallTargets(runtime, env = process.env) {
|
|
394
|
+
const primary = getSkillInstallTarget(runtime, env);
|
|
395
|
+
const home = env.USERPROFILE || env.HOME;
|
|
396
|
+
if (!home) {
|
|
397
|
+
throw new Error("cannot determine home directory");
|
|
398
|
+
}
|
|
399
|
+
if (runtime === "codex") {
|
|
400
|
+
return [primary, path.join(home, ".agents", "skills", "ninja-p2p")];
|
|
401
|
+
}
|
|
402
|
+
return [primary];
|
|
403
|
+
}
|
|
404
|
+
export function getDefaultStateDir(streamId, env = process.env) {
|
|
405
|
+
const home = env.USERPROFILE || env.HOME;
|
|
406
|
+
if (!home) {
|
|
407
|
+
throw new Error("cannot determine home directory");
|
|
408
|
+
}
|
|
409
|
+
return path.join(home, ".ninja-p2p", streamId);
|
|
410
|
+
}
|
|
411
|
+
function parseOptions(argv, env) {
|
|
412
|
+
const values = {};
|
|
413
|
+
const positional = [];
|
|
414
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
415
|
+
const arg = argv[i];
|
|
416
|
+
if (!arg.startsWith("--")) {
|
|
417
|
+
positional.push(arg);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const key = arg.slice(2);
|
|
421
|
+
const next = argv[i + 1];
|
|
422
|
+
if (!next || next.startsWith("--")) {
|
|
423
|
+
throw new Error(`missing value for --${key}`);
|
|
424
|
+
}
|
|
425
|
+
const existing = values[key];
|
|
426
|
+
if (existing === undefined) {
|
|
427
|
+
values[key] = next;
|
|
428
|
+
}
|
|
429
|
+
else if (Array.isArray(existing)) {
|
|
430
|
+
existing.push(next);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
values[key] = [existing, next];
|
|
434
|
+
}
|
|
435
|
+
i += 1;
|
|
436
|
+
}
|
|
437
|
+
if (!values.room && env.NINJA_ROOM)
|
|
438
|
+
values.room = env.NINJA_ROOM;
|
|
439
|
+
if (!values.name && env.NINJA_NAME)
|
|
440
|
+
values.name = env.NINJA_NAME;
|
|
441
|
+
if (!values.id && env.NINJA_ID)
|
|
442
|
+
values.id = env.NINJA_ID;
|
|
443
|
+
if (!values.role && env.NINJA_ROLE)
|
|
444
|
+
values.role = env.NINJA_ROLE;
|
|
445
|
+
if (!values.password && env.NINJA_PASSWORD)
|
|
446
|
+
values.password = env.NINJA_PASSWORD;
|
|
447
|
+
if (!values["state-dir"] && env.NINJA_STATE_DIR)
|
|
448
|
+
values["state-dir"] = env.NINJA_STATE_DIR;
|
|
449
|
+
if (!values["wait-ms"] && env.NINJA_WAIT_MS)
|
|
450
|
+
values["wait-ms"] = env.NINJA_WAIT_MS;
|
|
451
|
+
if (!values.runtime && env.NINJA_RUNTIME)
|
|
452
|
+
values.runtime = env.NINJA_RUNTIME;
|
|
453
|
+
if (!values.provider && env.NINJA_PROVIDER)
|
|
454
|
+
values.provider = env.NINJA_PROVIDER;
|
|
455
|
+
if (!values.model && env.NINJA_MODEL)
|
|
456
|
+
values.model = env.NINJA_MODEL;
|
|
457
|
+
if (!values.summary && env.NINJA_SUMMARY)
|
|
458
|
+
values.summary = env.NINJA_SUMMARY;
|
|
459
|
+
if (!values.workspace && env.NINJA_WORKSPACE)
|
|
460
|
+
values.workspace = env.NINJA_WORKSPACE;
|
|
461
|
+
if (!values.can && env.NINJA_CAN)
|
|
462
|
+
values.can = env.NINJA_CAN;
|
|
463
|
+
if (!values.ask && env.NINJA_ASKS)
|
|
464
|
+
values.ask = env.NINJA_ASKS.split(";").map((item) => item.trim()).filter(Boolean);
|
|
465
|
+
if (!values.share && env.NINJA_SHARE)
|
|
466
|
+
values.share = env.NINJA_SHARE.split(";").map((item) => item.trim()).filter(Boolean);
|
|
467
|
+
return { values, positional };
|
|
468
|
+
}
|
|
469
|
+
function buildCommonOptions(values, env, options) {
|
|
470
|
+
const requireRoom = options?.requireRoom ?? true;
|
|
471
|
+
const room = getSingleValue(values, "room") || env.NINJA_ROOM || (options?.allowGeneratedRoom ? generateRoomName() : "");
|
|
472
|
+
if (requireRoom && !room) {
|
|
473
|
+
throw new Error("missing room; use --room my-room");
|
|
474
|
+
}
|
|
475
|
+
const runtime = getSingleValue(values, "runtime") || env.NINJA_RUNTIME;
|
|
476
|
+
const name = getSingleValue(values, "name") || env.NINJA_NAME || defaultNameForRuntime(runtime);
|
|
477
|
+
const streamId = getSingleValue(values, "id") || env.NINJA_ID || defaultStreamIdForRuntime(name, runtime);
|
|
478
|
+
const role = getSingleValue(values, "role") || env.NINJA_ROLE || "agent";
|
|
479
|
+
const passwordValue = getSingleValue(values, "password") ?? env.NINJA_PASSWORD;
|
|
480
|
+
const password = passwordValue === "false" ? false : (passwordValue || false);
|
|
481
|
+
const waitMsRaw = getSingleValue(values, "wait-ms") ?? env.NINJA_WAIT_MS ?? "1500";
|
|
482
|
+
const waitMs = Number.parseInt(waitMsRaw, 10);
|
|
483
|
+
const stateDir = getSingleValue(values, "state-dir") || env.NINJA_STATE_DIR || null;
|
|
484
|
+
const sharedFolders = parseSharedFolderSpecs([
|
|
485
|
+
...getListValue(values, "share"),
|
|
486
|
+
...(env.NINJA_SHARE ? env.NINJA_SHARE.split(";") : []),
|
|
487
|
+
]);
|
|
488
|
+
if (options?.requireStateDir && !stateDir) {
|
|
489
|
+
throw new Error("missing state dir; use --state-dir or --id");
|
|
490
|
+
}
|
|
491
|
+
const agentProfile = buildAgentProfile(values, env, sharedFolders);
|
|
492
|
+
return {
|
|
493
|
+
room,
|
|
494
|
+
streamId,
|
|
495
|
+
name,
|
|
496
|
+
role,
|
|
497
|
+
password,
|
|
498
|
+
waitMs: Number.isFinite(waitMs) ? waitMs : 1500,
|
|
499
|
+
stateDir,
|
|
500
|
+
agentProfile,
|
|
501
|
+
sharedFolders,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function resolveStateDir(values, env, fallbackStreamId) {
|
|
505
|
+
const explicit = getSingleValue(values, "state-dir") || env.NINJA_STATE_DIR;
|
|
506
|
+
if (explicit)
|
|
507
|
+
return explicit;
|
|
508
|
+
const streamId = getSingleValue(values, "id") || env.NINJA_ID || fallbackStreamId;
|
|
509
|
+
if (!streamId) {
|
|
510
|
+
throw new Error("missing state dir; use --state-dir or --id");
|
|
511
|
+
}
|
|
512
|
+
return getDefaultStateDir(streamId, env);
|
|
513
|
+
}
|
|
514
|
+
function parsePositiveInt(raw, fallback) {
|
|
515
|
+
if (!raw)
|
|
516
|
+
return fallback;
|
|
517
|
+
const value = Number.parseInt(raw, 10);
|
|
518
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
519
|
+
return fallback;
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
function shouldUseStateMode(values, env) {
|
|
523
|
+
if (getSingleValue(values, "state-dir") || env.NINJA_STATE_DIR)
|
|
524
|
+
return true;
|
|
525
|
+
return !getSingleValue(values, "room") && Boolean(getSingleValue(values, "id") || env.NINJA_ID);
|
|
526
|
+
}
|
|
527
|
+
function buildAgentProfile(values, env, sharedFolders) {
|
|
528
|
+
const runtime = getSingleValue(values, "runtime") || env.NINJA_RUNTIME;
|
|
529
|
+
const provider = getSingleValue(values, "provider") || env.NINJA_PROVIDER;
|
|
530
|
+
const model = getSingleValue(values, "model") || env.NINJA_MODEL;
|
|
531
|
+
const summary = getSingleValue(values, "summary") || env.NINJA_SUMMARY;
|
|
532
|
+
const workspace = getSingleValue(values, "workspace") || env.NINJA_WORKSPACE;
|
|
533
|
+
const can = splitList(getListValue(values, "can"), ",", env.NINJA_CAN);
|
|
534
|
+
const asks = parseAgentAsks([
|
|
535
|
+
...getListValue(values, "ask"),
|
|
536
|
+
...(env.NINJA_ASKS ? env.NINJA_ASKS.split(";") : []),
|
|
537
|
+
]);
|
|
538
|
+
if (!runtime && !provider && !model && !summary && !workspace && can.length === 0 && asks.length === 0 && sharedFolders.length === 0) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
const profile = {};
|
|
542
|
+
if (runtime)
|
|
543
|
+
profile.runtime = runtime;
|
|
544
|
+
if (provider)
|
|
545
|
+
profile.provider = provider;
|
|
546
|
+
if (model)
|
|
547
|
+
profile.model = model;
|
|
548
|
+
if (summary)
|
|
549
|
+
profile.summary = summary;
|
|
550
|
+
if (workspace)
|
|
551
|
+
profile.workspace = workspace;
|
|
552
|
+
if (can.length > 0)
|
|
553
|
+
profile.can = can;
|
|
554
|
+
if (asks.length > 0)
|
|
555
|
+
profile.asks = asks;
|
|
556
|
+
if (sharedFolders.length > 0)
|
|
557
|
+
profile.shares = toSharedFolderSummaries(sharedFolders);
|
|
558
|
+
return profile;
|
|
559
|
+
}
|
|
560
|
+
function getSingleValue(values, key) {
|
|
561
|
+
const value = values[key];
|
|
562
|
+
if (Array.isArray(value)) {
|
|
563
|
+
return value[value.length - 1];
|
|
564
|
+
}
|
|
565
|
+
return value;
|
|
566
|
+
}
|
|
567
|
+
function getListValue(values, key) {
|
|
568
|
+
const value = values[key];
|
|
569
|
+
if (value === undefined)
|
|
570
|
+
return [];
|
|
571
|
+
return Array.isArray(value) ? value : [value];
|
|
572
|
+
}
|
|
573
|
+
function splitList(rawValues, delimiter, fallback) {
|
|
574
|
+
const values = [...rawValues];
|
|
575
|
+
if (values.length === 0 && fallback) {
|
|
576
|
+
values.push(fallback);
|
|
577
|
+
}
|
|
578
|
+
return [...new Set(values
|
|
579
|
+
.flatMap((value) => value.split(delimiter))
|
|
580
|
+
.map((value) => value.trim())
|
|
581
|
+
.filter(Boolean))];
|
|
582
|
+
}
|
|
583
|
+
function parseAgentAsks(values) {
|
|
584
|
+
const asks = [];
|
|
585
|
+
const seen = new Set();
|
|
586
|
+
for (const raw of values.map((value) => value.trim()).filter(Boolean)) {
|
|
587
|
+
const separatorIndex = raw.indexOf("=") >= 0 ? raw.indexOf("=") : raw.indexOf(":");
|
|
588
|
+
const name = (separatorIndex >= 0 ? raw.slice(0, separatorIndex) : raw).trim();
|
|
589
|
+
const description = (separatorIndex >= 0 ? raw.slice(separatorIndex + 1) : raw).trim();
|
|
590
|
+
if (!name)
|
|
591
|
+
continue;
|
|
592
|
+
const key = name.toLowerCase();
|
|
593
|
+
if (seen.has(key))
|
|
594
|
+
continue;
|
|
595
|
+
seen.add(key);
|
|
596
|
+
asks.push({
|
|
597
|
+
name,
|
|
598
|
+
description: description || `${name} command`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return asks;
|
|
602
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { VDOBridge } from "./vdo-bridge.js";
|
|
3
|
+
import { type AgentAsk, type AgentProfile, type MessageEnvelope, type PeerIdentity } from "./protocol.js";
|
|
4
|
+
import { type CliCommonOptions } from "./cli-lib.js";
|
|
5
|
+
import { type SharedFolderConfig } from "./shared-folders.js";
|
|
6
|
+
export declare const SIDECAR_DISCOVERY_ASKS: AgentAsk[];
|
|
7
|
+
export declare const SIDECAR_SHARE_ASKS: AgentAsk[];
|
|
8
|
+
export declare function buildMenuText(options: CliCommonOptions): string;
|
|
9
|
+
export declare function buildRoomText(stateDir: string): string;
|
|
10
|
+
export declare function getAgentStatus(stateDir: string): Record<string, unknown>;
|
|
11
|
+
export declare function createFileTransferEventEnvelope(from: PeerIdentity, kind: "file_offered" | "file_received" | "file_delivered" | "file_delivery_failed" | "file_receive_failed" | "file_send_failed", details: Record<string, unknown>): MessageEnvelope;
|
|
12
|
+
export declare function composeSidecarAgentProfile(profile?: AgentProfile): AgentProfile;
|
|
13
|
+
export declare function buildSidecarCommandResponse(bridge: VDOBridge, stateDir: string, commandName: string, commandArgs?: unknown, sharedFolders?: SharedFolderConfig[], requesterStreamId?: string): {
|
|
14
|
+
handled: boolean;
|
|
15
|
+
result?: unknown;
|
|
16
|
+
error?: string;
|
|
17
|
+
transfer?: {
|
|
18
|
+
targetStreamId: string;
|
|
19
|
+
filePath: string;
|
|
20
|
+
transferKind: "file" | "image";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare function maybeHandleSidecarCommand(bridge: VDOBridge, stateDir: string, envelope: MessageEnvelope): boolean;
|
|
24
|
+
export declare function createPeerNoticeEnvelope(peerIdentity: PeerIdentity, kind: "peer_discovered" | "peer_updated" | "peer_left", details: Record<string, unknown>): MessageEnvelope;
|
|
25
|
+
export declare function recordPeerNotice(fingerprints: Map<string, string>, peerIdentity: PeerIdentity, kind: "peer_discovered" | "peer_updated", details: Record<string, unknown>): MessageEnvelope | null;
|