@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.js
ADDED
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { closeSync, cpSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { appendIncomingTransferChunk, beginIncomingTransfer, completeIncomingTransfer, createFailedFileAckPayload, createFileAckPayload, sendFileFromPath, } from "./file-transfer.js";
|
|
10
|
+
import { VDOBridge } from "./vdo-bridge.js";
|
|
11
|
+
import { createEnvelope, envelopeToWire, } from "./protocol.js";
|
|
12
|
+
import { ensureAgentState, getInboxSummary, isInboxWorthy, listQueuedAgentActions, queueAgentAction, readAgentSession, readPeersSnapshot, removeQueuedAgentAction, storeInboxMessage, takeInboxMessages, writeAgentSession, writePeersSnapshot, } from "./agent-state.js";
|
|
13
|
+
import { getDefaultStateDir, getSkillInstallTarget, getSkillInstallTargets, helpText, parseCliArgs, parseJsonMaybe, } from "./cli-lib.js";
|
|
14
|
+
import { listSharedFolderEntries, resolveSharedFile, } from "./shared-folders.js";
|
|
15
|
+
const SIDECAR_SKILLS = ["cli", "chat", "command", "sidecar", "discovery"];
|
|
16
|
+
export const SIDECAR_DISCOVERY_ASKS = [
|
|
17
|
+
{ name: "help", description: "Summarize this agent sidecar and its built-in commands.", via: "command", example: "ninja-p2p command --id <you> <peer> help" },
|
|
18
|
+
{ name: "profile", description: "Return identity and the advertised agent profile.", via: "command", example: "ninja-p2p command --id <you> <peer> profile" },
|
|
19
|
+
{ name: "whoami", description: "Alias for profile.", via: "command", example: "ninja-p2p command --id <you> <peer> whoami" },
|
|
20
|
+
{ name: "capabilities", description: "Return skills, capabilities, and asks.", via: "command", example: "ninja-p2p command --id <you> <peer> capabilities" },
|
|
21
|
+
{ name: "status", description: "Return connection state, inbox counts, and peer counts.", via: "command", example: "ninja-p2p command --id <you> <peer> status" },
|
|
22
|
+
{ name: "peers", description: "Return the last known peers in the room.", via: "command", example: "ninja-p2p command --id <you> <peer> peers" },
|
|
23
|
+
{ name: "inbox", description: "Return pending inbox summary without consuming messages.", via: "command", example: "ninja-p2p command --id <you> <peer> inbox" },
|
|
24
|
+
{ name: "pending", description: "Alias for inbox.", via: "command", example: "ninja-p2p command --id <you> <peer> pending" },
|
|
25
|
+
];
|
|
26
|
+
export const SIDECAR_SHARE_ASKS = [
|
|
27
|
+
{ name: "shares", description: "List the shared folders this agent exposes.", via: "command", example: "ninja-p2p command --id <you> <peer> shares" },
|
|
28
|
+
{ name: "list-files", description: "List the files and directories in a shared folder.", via: "command", example: "ninja-p2p command --id <you> <peer> list-files '{\"share\":\"docs\"}'" },
|
|
29
|
+
{ name: "get-file", description: "Request one file from a shared folder.", via: "command", example: "ninja-p2p command --id <you> <peer> get-file '{\"share\":\"docs\",\"path\":\"guide.md\"}'" },
|
|
30
|
+
];
|
|
31
|
+
const DERIVED_CAPABILITY_ASKS = {
|
|
32
|
+
plan: [{ name: "plan", description: "Ask for a plan, breakdown, or next-step proposal.", via: "command" }],
|
|
33
|
+
approve: [{ name: "approve", description: "Ask for approval or a go/no-go decision before proceeding.", via: "command" }],
|
|
34
|
+
review: [{ name: "review", description: "Ask for a review, critique, or second opinion.", via: "command" }],
|
|
35
|
+
tests: [{ name: "test", description: "Ask for test ideas, coverage gaps, or validation steps.", via: "command" }],
|
|
36
|
+
edit: [{ name: "implement", description: "Ask for a scoped implementation or code edit.", via: "command" }],
|
|
37
|
+
code: [{ name: "implement", description: "Ask for a scoped implementation or code edit.", via: "command" }],
|
|
38
|
+
debug: [{ name: "debug", description: "Ask for debugging help or root-cause analysis.", via: "command" }],
|
|
39
|
+
docs: [{ name: "docs", description: "Ask for documentation or rewrite help.", via: "command" }],
|
|
40
|
+
research: [{ name: "research", description: "Ask for investigation or background research.", via: "command" }],
|
|
41
|
+
};
|
|
42
|
+
async function main(argv) {
|
|
43
|
+
const parsed = parseCliArgs(argv);
|
|
44
|
+
switch (parsed.kind) {
|
|
45
|
+
case "help":
|
|
46
|
+
console.log(helpText());
|
|
47
|
+
return;
|
|
48
|
+
case "menu":
|
|
49
|
+
console.log(buildMenuText(parsed.options));
|
|
50
|
+
return;
|
|
51
|
+
case "install-skill":
|
|
52
|
+
installSkill(parsed.runtime);
|
|
53
|
+
return;
|
|
54
|
+
case "start":
|
|
55
|
+
startAgent(parsed.options);
|
|
56
|
+
return;
|
|
57
|
+
case "stop":
|
|
58
|
+
stopAgent(parsed.stateDir);
|
|
59
|
+
return;
|
|
60
|
+
case "status":
|
|
61
|
+
console.log(JSON.stringify(getAgentStatus(parsed.stateDir), null, 2));
|
|
62
|
+
return;
|
|
63
|
+
case "room":
|
|
64
|
+
console.log(buildRoomText(parsed.stateDir));
|
|
65
|
+
return;
|
|
66
|
+
case "agent":
|
|
67
|
+
await runAgent(parsed.options);
|
|
68
|
+
return;
|
|
69
|
+
case "notify":
|
|
70
|
+
console.log(JSON.stringify(getInboxSummary(parsed.stateDir), null, 2));
|
|
71
|
+
return;
|
|
72
|
+
case "read":
|
|
73
|
+
console.log(JSON.stringify({
|
|
74
|
+
stateDir: parsed.stateDir,
|
|
75
|
+
messages: takeInboxMessages(parsed.stateDir, parsed.take, parsed.peek).map((item) => item.envelope),
|
|
76
|
+
}, null, 2));
|
|
77
|
+
return;
|
|
78
|
+
case "connect":
|
|
79
|
+
await runConnect(parsed.options);
|
|
80
|
+
return;
|
|
81
|
+
case "chat":
|
|
82
|
+
if (parsed.options.stateDir) {
|
|
83
|
+
const queued = queueAgentAction(parsed.options.stateDir, { kind: "chat", text: parsed.text });
|
|
84
|
+
console.log(`queued chat ${queued.id} in ${parsed.options.stateDir}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
88
|
+
bridge.chat(parsed.text);
|
|
89
|
+
console.log(`sent chat to room ${parsed.options.room}`);
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
case "dm":
|
|
93
|
+
if (parsed.options.stateDir) {
|
|
94
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
95
|
+
kind: "dm",
|
|
96
|
+
target: parsed.target,
|
|
97
|
+
text: parsed.text,
|
|
98
|
+
});
|
|
99
|
+
console.log(`queued direct message ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
103
|
+
const envelope = createEnvelope(bridge.identity, "chat", { text: parsed.text }, { to: parsed.target });
|
|
104
|
+
bridge.sendRaw(envelopeToWire(envelope), parsed.target);
|
|
105
|
+
console.log(`sent direct chat to ${parsed.target}`);
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
case "send-file":
|
|
109
|
+
case "send-image": {
|
|
110
|
+
const filePath = path.resolve(parsed.filePath);
|
|
111
|
+
const transferKind = parsed.kind === "send-image" ? "image" : "file";
|
|
112
|
+
if (parsed.options.stateDir) {
|
|
113
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
114
|
+
kind: "send_file",
|
|
115
|
+
target: parsed.target,
|
|
116
|
+
filePath,
|
|
117
|
+
transferKind,
|
|
118
|
+
});
|
|
119
|
+
console.log(`queued ${transferKind} transfer ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
123
|
+
const offer = sendFileFromPath(bridge, parsed.target, filePath, transferKind);
|
|
124
|
+
console.log(`sent ${transferKind} ${offer.name} to ${parsed.target}`);
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
case "shares":
|
|
129
|
+
if (parsed.options.stateDir) {
|
|
130
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
131
|
+
kind: "command",
|
|
132
|
+
target: parsed.target,
|
|
133
|
+
command: "shares",
|
|
134
|
+
});
|
|
135
|
+
console.log(`queued shares request ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
139
|
+
bridge.command(parsed.target, "shares");
|
|
140
|
+
console.log(`requested shares from ${parsed.target}`);
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
case "list-files":
|
|
144
|
+
if (parsed.options.stateDir) {
|
|
145
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
146
|
+
kind: "command",
|
|
147
|
+
target: parsed.target,
|
|
148
|
+
command: "list-files",
|
|
149
|
+
args: {
|
|
150
|
+
share: parsed.share,
|
|
151
|
+
path: parsed.folderPath || "",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
console.log(`queued list-files request ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
158
|
+
bridge.command(parsed.target, "list-files", {
|
|
159
|
+
share: parsed.share,
|
|
160
|
+
path: parsed.folderPath || "",
|
|
161
|
+
});
|
|
162
|
+
console.log(`requested ${parsed.share}${parsed.folderPath ? `/${parsed.folderPath}` : ""} from ${parsed.target}`);
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
case "get-file":
|
|
166
|
+
if (parsed.options.stateDir) {
|
|
167
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
168
|
+
kind: "command",
|
|
169
|
+
target: parsed.target,
|
|
170
|
+
command: "get-file",
|
|
171
|
+
args: {
|
|
172
|
+
share: parsed.share,
|
|
173
|
+
path: parsed.filePath,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
console.log(`queued get-file request ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
180
|
+
bridge.command(parsed.target, "get-file", {
|
|
181
|
+
share: parsed.share,
|
|
182
|
+
path: parsed.filePath,
|
|
183
|
+
});
|
|
184
|
+
console.log(`requested file ${parsed.share}/${parsed.filePath} from ${parsed.target}`);
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
case "command":
|
|
188
|
+
if (parsed.options.stateDir) {
|
|
189
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
190
|
+
kind: "command",
|
|
191
|
+
target: parsed.target,
|
|
192
|
+
command: parsed.command,
|
|
193
|
+
args: parsed.args,
|
|
194
|
+
});
|
|
195
|
+
console.log(`queued command ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
199
|
+
const envelope = createEnvelope(bridge.identity, "command", {
|
|
200
|
+
command: parsed.command,
|
|
201
|
+
args: parsed.args,
|
|
202
|
+
}, { to: parsed.target });
|
|
203
|
+
bridge.sendRaw(envelopeToWire(envelope), parsed.target);
|
|
204
|
+
console.log(`sent command ${parsed.command} to ${parsed.target}`);
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
case "respond":
|
|
208
|
+
if (parsed.options.stateDir) {
|
|
209
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
210
|
+
kind: "response",
|
|
211
|
+
target: parsed.target,
|
|
212
|
+
requestId: parsed.requestId,
|
|
213
|
+
result: parsed.result,
|
|
214
|
+
error: parsed.error,
|
|
215
|
+
});
|
|
216
|
+
console.log(`queued response ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
220
|
+
sendCommandResponse(bridge, parsed.target, parsed.requestId, parsed.result, parsed.error);
|
|
221
|
+
console.log(`sent response to ${parsed.target} for ${parsed.requestId}`);
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
case "event":
|
|
225
|
+
if (parsed.options.stateDir) {
|
|
226
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
227
|
+
kind: "event",
|
|
228
|
+
topic: parsed.topic,
|
|
229
|
+
eventKind: parsed.eventKind,
|
|
230
|
+
data: parsed.data,
|
|
231
|
+
});
|
|
232
|
+
console.log(`queued event ${queued.id} ${parsed.topic}/${parsed.eventKind} in ${parsed.options.stateDir}`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
236
|
+
bridge.publishEvent(parsed.topic, parsed.eventKind, parsed.data);
|
|
237
|
+
console.log(`published event ${parsed.topic}/${parsed.eventKind}`);
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
case "task":
|
|
241
|
+
if (parsed.options.stateDir) {
|
|
242
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
243
|
+
kind: "command",
|
|
244
|
+
target: parsed.target,
|
|
245
|
+
command: "task",
|
|
246
|
+
args: { request: parsed.request },
|
|
247
|
+
});
|
|
248
|
+
console.log(`queued task ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
252
|
+
bridge.command(parsed.target, "task", { request: parsed.request });
|
|
253
|
+
console.log(`sent task request to ${parsed.target}`);
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
case "review":
|
|
257
|
+
if (parsed.options.stateDir) {
|
|
258
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
259
|
+
kind: "command",
|
|
260
|
+
target: parsed.target,
|
|
261
|
+
command: "review",
|
|
262
|
+
args: { request: parsed.request },
|
|
263
|
+
});
|
|
264
|
+
console.log(`queued review ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
268
|
+
bridge.command(parsed.target, "review", { request: parsed.request });
|
|
269
|
+
console.log(`sent review request to ${parsed.target}`);
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
case "plan":
|
|
273
|
+
if (parsed.options.stateDir) {
|
|
274
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
275
|
+
kind: "command",
|
|
276
|
+
target: parsed.target,
|
|
277
|
+
command: "plan",
|
|
278
|
+
args: { request: parsed.request },
|
|
279
|
+
});
|
|
280
|
+
console.log(`queued plan ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
284
|
+
bridge.command(parsed.target, "plan", { request: parsed.request });
|
|
285
|
+
console.log(`sent plan request to ${parsed.target}`);
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
case "approve":
|
|
289
|
+
if (parsed.options.stateDir) {
|
|
290
|
+
const queued = queueAgentAction(parsed.options.stateDir, {
|
|
291
|
+
kind: "command",
|
|
292
|
+
target: parsed.target,
|
|
293
|
+
command: "approve",
|
|
294
|
+
args: { request: parsed.request },
|
|
295
|
+
});
|
|
296
|
+
console.log(`queued approval request ${queued.id} to ${parsed.target} in ${parsed.options.stateDir}`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
await runOneShot(parsed.options, (bridge) => {
|
|
300
|
+
bridge.command(parsed.target, "approve", { request: parsed.request });
|
|
301
|
+
console.log(`sent approval request to ${parsed.target}`);
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
export function buildMenuText(options) {
|
|
307
|
+
const prefix = getCommandPrefix(options);
|
|
308
|
+
const stateDir = options.stateDir ?? getDefaultStateDir(options.streamId);
|
|
309
|
+
const status = existsSync(path.join(stateDir, "session.json")) ? getAgentStatus(stateDir) : null;
|
|
310
|
+
const room = typeof status?.room === "string" && status.room ? status.room : options.room;
|
|
311
|
+
const running = Boolean(status?.running);
|
|
312
|
+
const lines = [
|
|
313
|
+
"ninja-p2p",
|
|
314
|
+
"",
|
|
315
|
+
`id: ${options.streamId}`,
|
|
316
|
+
`room: ${room || "(generated on start)"}`,
|
|
317
|
+
running ? "status: running" : "status: not started",
|
|
318
|
+
"",
|
|
319
|
+
];
|
|
320
|
+
if (!running) {
|
|
321
|
+
lines.push("Start from Claude or Codex:");
|
|
322
|
+
lines.push(` ${prefix} start`);
|
|
323
|
+
lines.push(` ${prefix} start --room my-room`);
|
|
324
|
+
lines.push("");
|
|
325
|
+
lines.push("If you do not pass --room, ninja-p2p generates one for you.");
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
lines.push("Useful commands:");
|
|
329
|
+
lines.push(` ${prefix} room`);
|
|
330
|
+
lines.push(` ${prefix} status`);
|
|
331
|
+
lines.push(` ${prefix} notify`);
|
|
332
|
+
lines.push(` ${prefix} read`);
|
|
333
|
+
lines.push(` ${prefix} dm <peer> "hello"`);
|
|
334
|
+
lines.push(` ${prefix} shares <peer>`);
|
|
335
|
+
lines.push(` ${prefix} stop`);
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push(`Another agent joins with: --room ${room}`);
|
|
338
|
+
}
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
export function buildRoomText(stateDir) {
|
|
342
|
+
const status = getAgentStatus(stateDir);
|
|
343
|
+
const room = typeof status.room === "string" ? status.room : "";
|
|
344
|
+
const streamId = typeof status.streamId === "string" ? status.streamId : "";
|
|
345
|
+
if (!room) {
|
|
346
|
+
return [
|
|
347
|
+
"room: not set",
|
|
348
|
+
"start the sidecar first, or start it with --room my-room",
|
|
349
|
+
].join("\n");
|
|
350
|
+
}
|
|
351
|
+
return [
|
|
352
|
+
`room: ${room}`,
|
|
353
|
+
`id: ${streamId || "unknown"}`,
|
|
354
|
+
"",
|
|
355
|
+
"Join this room from another agent:",
|
|
356
|
+
` Claude: /ninja-p2p start --room ${room}`,
|
|
357
|
+
` Codex: ninja-p2p start --room ${room} --id codex`,
|
|
358
|
+
` Generic CLI: ninja-p2p start --room ${room} --id worker`,
|
|
359
|
+
].join("\n");
|
|
360
|
+
}
|
|
361
|
+
function installSkill(runtime) {
|
|
362
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
363
|
+
const sourceCandidates = runtime === "codex"
|
|
364
|
+
? [
|
|
365
|
+
path.join(packageRoot, ".codex", "skills", "ninja-p2p"),
|
|
366
|
+
path.join(packageRoot, ".agents", "skills", "ninja-p2p"),
|
|
367
|
+
]
|
|
368
|
+
: [path.join(packageRoot, ".claude", "skills", "ninja-p2p")];
|
|
369
|
+
const source = sourceCandidates.find((candidate) => existsSync(candidate));
|
|
370
|
+
const targets = getSkillInstallTargets(runtime);
|
|
371
|
+
const primaryTarget = getSkillInstallTarget(runtime);
|
|
372
|
+
if (!source) {
|
|
373
|
+
throw new Error(`bundled ${runtime} skill not found`);
|
|
374
|
+
}
|
|
375
|
+
for (const target of targets) {
|
|
376
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
377
|
+
cpSync(source, target, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
console.log(`installed ${runtime} skill at ${primaryTarget}`);
|
|
380
|
+
if (runtime === "codex" && targets.length > 1) {
|
|
381
|
+
console.log(`installed Codex compatibility copy at ${targets[1]}`);
|
|
382
|
+
}
|
|
383
|
+
console.log(`restart ${runtime === "codex" ? "Codex" : "Claude Code"} if it does not appear immediately`);
|
|
384
|
+
}
|
|
385
|
+
function createBridge(options, mode = "oneshot") {
|
|
386
|
+
const agentProfile = mode === "sidecar"
|
|
387
|
+
? composeSidecarAgentProfile(options.agentProfile)
|
|
388
|
+
: options.agentProfile;
|
|
389
|
+
return new VDOBridge({
|
|
390
|
+
room: options.room,
|
|
391
|
+
streamId: options.streamId,
|
|
392
|
+
identity: {
|
|
393
|
+
streamId: options.streamId,
|
|
394
|
+
role: options.role,
|
|
395
|
+
name: options.name,
|
|
396
|
+
},
|
|
397
|
+
password: options.password,
|
|
398
|
+
skills: mode === "sidecar" ? SIDECAR_SKILLS : ["cli", "chat", "command"],
|
|
399
|
+
topics: ["events"],
|
|
400
|
+
agentProfile,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function startAgent(options) {
|
|
404
|
+
if (!options.stateDir) {
|
|
405
|
+
throw new Error("missing state dir; use --state-dir or --id");
|
|
406
|
+
}
|
|
407
|
+
const current = getAgentStatus(options.stateDir);
|
|
408
|
+
if (current.running) {
|
|
409
|
+
console.log(`agent already running: pid=${current.pid} state=${options.stateDir}`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const paths = ensureAgentState(options.stateDir);
|
|
413
|
+
const logFile = path.join(paths.stateDir, "agent.log");
|
|
414
|
+
const stdout = openSync(logFile, "a");
|
|
415
|
+
const stderr = openSync(logFile, "a");
|
|
416
|
+
const child = spawn(process.execPath, [resolveCliEntryPath(), "agent", ...buildAgentArgs(options)], {
|
|
417
|
+
detached: true,
|
|
418
|
+
stdio: ["ignore", stdout, stderr],
|
|
419
|
+
windowsHide: true,
|
|
420
|
+
});
|
|
421
|
+
closeSync(stdout);
|
|
422
|
+
closeSync(stderr);
|
|
423
|
+
child.unref();
|
|
424
|
+
console.log(`started agent pid=${child.pid} state=${options.stateDir}`);
|
|
425
|
+
console.log(`room: ${options.room}`);
|
|
426
|
+
console.log(`id: ${options.streamId}`);
|
|
427
|
+
console.log(`log: ${logFile}`);
|
|
428
|
+
console.log(`join from Claude: /ninja-p2p start --room ${options.room}`);
|
|
429
|
+
console.log(`join from Codex: ninja-p2p start --room ${options.room} --id codex`);
|
|
430
|
+
const prefix = getCommandPrefix(options);
|
|
431
|
+
if (prefix === "/ninja-p2p") {
|
|
432
|
+
console.log(`next: ${prefix} room`);
|
|
433
|
+
console.log(`next: ${prefix} notify`);
|
|
434
|
+
console.log(`next: ${prefix} read --take 10`);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
console.log(`next: ${prefix} room --id ${options.streamId}`);
|
|
438
|
+
console.log(`next: ${prefix} notify --id ${options.streamId}`);
|
|
439
|
+
console.log(`next: ${prefix} read --id ${options.streamId} --take 10`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function stopAgent(stateDir) {
|
|
443
|
+
const session = readAgentSession(stateDir);
|
|
444
|
+
if (!session?.pid) {
|
|
445
|
+
console.log(`agent not running: ${stateDir}`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (isPidRunning(session.pid)) {
|
|
449
|
+
process.kill(session.pid);
|
|
450
|
+
writeAgentSession(stateDir, {
|
|
451
|
+
...session,
|
|
452
|
+
connected: false,
|
|
453
|
+
updatedAt: Date.now(),
|
|
454
|
+
});
|
|
455
|
+
console.log(`stopped agent pid=${session.pid} state=${stateDir}`);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
writeAgentSession(stateDir, {
|
|
459
|
+
...session,
|
|
460
|
+
connected: false,
|
|
461
|
+
updatedAt: Date.now(),
|
|
462
|
+
});
|
|
463
|
+
console.log(`agent already stopped: ${stateDir}`);
|
|
464
|
+
}
|
|
465
|
+
export function getAgentStatus(stateDir) {
|
|
466
|
+
const session = readAgentSession(stateDir);
|
|
467
|
+
const inbox = getInboxSummary(stateDir);
|
|
468
|
+
const peers = readPeersSnapshot(stateDir);
|
|
469
|
+
const pid = session?.pid ?? null;
|
|
470
|
+
const running = pid !== null && isPidRunning(pid);
|
|
471
|
+
return {
|
|
472
|
+
stateDir,
|
|
473
|
+
pid,
|
|
474
|
+
running,
|
|
475
|
+
room: session?.room ?? inbox.room,
|
|
476
|
+
streamId: session?.streamId ?? inbox.streamId,
|
|
477
|
+
connected: running && inbox.connected,
|
|
478
|
+
stale: inbox.stale,
|
|
479
|
+
pending: inbox.pending,
|
|
480
|
+
queued: inbox.queued,
|
|
481
|
+
peersKnown: inbox.peersKnown,
|
|
482
|
+
peersConnected: inbox.peersConnected,
|
|
483
|
+
senders: inbox.senders,
|
|
484
|
+
types: inbox.types,
|
|
485
|
+
commands: inbox.commands,
|
|
486
|
+
events: inbox.events,
|
|
487
|
+
skills: session?.skills ?? [],
|
|
488
|
+
topics: session?.topics ?? [],
|
|
489
|
+
agentProfile: session?.agentProfile ?? null,
|
|
490
|
+
sharedFolders: session?.sharedFolders ?? [],
|
|
491
|
+
peers,
|
|
492
|
+
logFile: path.join(stateDir, "agent.log"),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function runAgent(options) {
|
|
496
|
+
if (!options.stateDir) {
|
|
497
|
+
throw new Error("missing state dir; use --state-dir or --id");
|
|
498
|
+
}
|
|
499
|
+
const bridge = createBridge(options, "sidecar");
|
|
500
|
+
const advertisedProfile = composeSidecarAgentProfile(options.agentProfile);
|
|
501
|
+
const peerFingerprints = new Map();
|
|
502
|
+
let shuttingDown = false;
|
|
503
|
+
const syncSession = (connected = bridge.isConnected()) => {
|
|
504
|
+
writeAgentSession(options.stateDir, {
|
|
505
|
+
room: options.room,
|
|
506
|
+
streamId: options.streamId,
|
|
507
|
+
name: options.name,
|
|
508
|
+
role: options.role,
|
|
509
|
+
stateDir: options.stateDir,
|
|
510
|
+
pid: process.pid,
|
|
511
|
+
connected,
|
|
512
|
+
skills: SIDECAR_SKILLS,
|
|
513
|
+
topics: ["events"],
|
|
514
|
+
agentProfile: advertisedProfile,
|
|
515
|
+
sharedFolders: options.sharedFolders,
|
|
516
|
+
startedAt: readAgentSession(options.stateDir)?.startedAt ?? Date.now(),
|
|
517
|
+
updatedAt: Date.now(),
|
|
518
|
+
});
|
|
519
|
+
writePeersSnapshot(options.stateDir, bridge.peers.toJSON());
|
|
520
|
+
};
|
|
521
|
+
const flushOutbox = () => {
|
|
522
|
+
for (const item of listQueuedAgentActions(options.stateDir)) {
|
|
523
|
+
try {
|
|
524
|
+
switch (item.action.kind) {
|
|
525
|
+
case "chat":
|
|
526
|
+
bridge.chat(item.action.text);
|
|
527
|
+
break;
|
|
528
|
+
case "dm":
|
|
529
|
+
bridge.chat(item.action.text, item.action.target);
|
|
530
|
+
break;
|
|
531
|
+
case "send_file":
|
|
532
|
+
sendFileFromPath(bridge, item.action.target, item.action.filePath, item.action.transferKind);
|
|
533
|
+
break;
|
|
534
|
+
case "command":
|
|
535
|
+
bridge.command(item.action.target, item.action.command, item.action.args);
|
|
536
|
+
break;
|
|
537
|
+
case "response":
|
|
538
|
+
sendCommandResponse(bridge, item.action.target, item.action.requestId, item.action.result, item.action.error);
|
|
539
|
+
break;
|
|
540
|
+
case "event":
|
|
541
|
+
bridge.publishEvent(item.action.topic, item.action.eventKind, item.action.data);
|
|
542
|
+
break;
|
|
543
|
+
case "status":
|
|
544
|
+
bridge.updateStatus(item.action.status, item.action.detail);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (item.action.kind === "send_file" && error instanceof Error && /peer is not connected/i.test(error.message)) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
storeInboxMessage(options.stateDir, createFileTransferEventEnvelope(bridge.identity, "file_send_failed", {
|
|
553
|
+
target: "target" in item.action ? item.action.target : null,
|
|
554
|
+
filePath: "filePath" in item.action ? item.action.filePath : null,
|
|
555
|
+
error: error instanceof Error ? error.message : String(error),
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
removeQueuedAgentAction(item.path);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
writeAgentSession(options.stateDir, {
|
|
562
|
+
room: options.room,
|
|
563
|
+
streamId: options.streamId,
|
|
564
|
+
name: options.name,
|
|
565
|
+
role: options.role,
|
|
566
|
+
stateDir: options.stateDir,
|
|
567
|
+
pid: process.pid,
|
|
568
|
+
connected: false,
|
|
569
|
+
skills: SIDECAR_SKILLS,
|
|
570
|
+
topics: ["events"],
|
|
571
|
+
agentProfile: advertisedProfile,
|
|
572
|
+
sharedFolders: options.sharedFolders,
|
|
573
|
+
startedAt: Date.now(),
|
|
574
|
+
updatedAt: Date.now(),
|
|
575
|
+
});
|
|
576
|
+
await bridge.connect();
|
|
577
|
+
syncSession(true);
|
|
578
|
+
console.log(`agent ready: ${options.streamId} -> ${options.stateDir}`);
|
|
579
|
+
bridge.on("peer:connected", () => {
|
|
580
|
+
syncSession(true);
|
|
581
|
+
});
|
|
582
|
+
bridge.on("peer:announce", ({ streamId, identity, announce }) => {
|
|
583
|
+
const notice = recordPeerNotice(peerFingerprints, identity, "peer_discovered", {
|
|
584
|
+
streamId,
|
|
585
|
+
skills: announce.skills,
|
|
586
|
+
status: announce.status,
|
|
587
|
+
statusDetail: announce.statusDetail,
|
|
588
|
+
topics: announce.topics,
|
|
589
|
+
version: announce.version,
|
|
590
|
+
profile: announce.agent ?? null,
|
|
591
|
+
requestExamples: buildRequestExamples(streamId, announce.agent?.asks ?? SIDECAR_DISCOVERY_ASKS),
|
|
592
|
+
});
|
|
593
|
+
if (notice) {
|
|
594
|
+
storeInboxMessage(options.stateDir, notice);
|
|
595
|
+
console.log(`[peer] discovered ${streamId}`);
|
|
596
|
+
}
|
|
597
|
+
syncSession(true);
|
|
598
|
+
});
|
|
599
|
+
bridge.on("peer:disconnected", () => {
|
|
600
|
+
syncSession(true);
|
|
601
|
+
});
|
|
602
|
+
bridge.on("peer:disconnected", ({ streamId }) => {
|
|
603
|
+
const peer = bridge.peers.getPeer(streamId);
|
|
604
|
+
const identity = peer?.identity ?? {
|
|
605
|
+
streamId,
|
|
606
|
+
role: "agent",
|
|
607
|
+
name: streamId,
|
|
608
|
+
instanceId: "unknown",
|
|
609
|
+
};
|
|
610
|
+
if (peerFingerprints.delete(streamId)) {
|
|
611
|
+
storeInboxMessage(options.stateDir, createPeerNoticeEnvelope(identity, "peer_left", {
|
|
612
|
+
streamId,
|
|
613
|
+
status: "offline",
|
|
614
|
+
profile: peer?.agentProfile ?? null,
|
|
615
|
+
}));
|
|
616
|
+
console.log(`[peer] left ${streamId}`);
|
|
617
|
+
}
|
|
618
|
+
syncSession(true);
|
|
619
|
+
});
|
|
620
|
+
bridge.bus.on("message:skill_update", (envelope) => {
|
|
621
|
+
const payload = envelope.payload;
|
|
622
|
+
const notice = recordPeerNotice(peerFingerprints, envelope.from, "peer_updated", {
|
|
623
|
+
streamId: envelope.from.streamId,
|
|
624
|
+
skills: payload.skills ?? [],
|
|
625
|
+
status: payload.status ?? "online",
|
|
626
|
+
statusDetail: payload.statusDetail ?? "",
|
|
627
|
+
profile: payload.agent ?? null,
|
|
628
|
+
requestExamples: buildRequestExamples(envelope.from.streamId, payload.agent?.asks ?? SIDECAR_DISCOVERY_ASKS),
|
|
629
|
+
});
|
|
630
|
+
if (notice) {
|
|
631
|
+
storeInboxMessage(options.stateDir, notice);
|
|
632
|
+
console.log(`[peer] updated ${envelope.from.streamId}`);
|
|
633
|
+
}
|
|
634
|
+
syncSession(true);
|
|
635
|
+
});
|
|
636
|
+
bridge.bus.on("message", (envelope) => {
|
|
637
|
+
const transferHandled = maybeHandleSidecarFileTransfer(bridge, options.stateDir, envelope);
|
|
638
|
+
const autoHandled = transferHandled || maybeHandleSidecarCommand(bridge, options.stateDir, envelope);
|
|
639
|
+
if (!autoHandled && isInboxWorthy(envelope.type)) {
|
|
640
|
+
storeInboxMessage(options.stateDir, envelope);
|
|
641
|
+
console.log(`[inbox] ${envelope.type} from ${envelope.from.streamId}`);
|
|
642
|
+
}
|
|
643
|
+
else if (transferHandled) {
|
|
644
|
+
console.log(`[transfer] ${envelope.type} from ${envelope.from.streamId}`);
|
|
645
|
+
}
|
|
646
|
+
else if (autoHandled) {
|
|
647
|
+
console.log(`[auto] ${envelope.type} from ${envelope.from.streamId}`);
|
|
648
|
+
}
|
|
649
|
+
syncSession(true);
|
|
650
|
+
});
|
|
651
|
+
const heartbeatTimer = setInterval(() => {
|
|
652
|
+
flushOutbox();
|
|
653
|
+
syncSession(true);
|
|
654
|
+
}, 500);
|
|
655
|
+
if (heartbeatTimer.unref)
|
|
656
|
+
heartbeatTimer.unref();
|
|
657
|
+
const shutdown = async () => {
|
|
658
|
+
if (shuttingDown)
|
|
659
|
+
return;
|
|
660
|
+
shuttingDown = true;
|
|
661
|
+
clearInterval(heartbeatTimer);
|
|
662
|
+
syncSession(false);
|
|
663
|
+
await bridge.disconnect();
|
|
664
|
+
syncSession(false);
|
|
665
|
+
};
|
|
666
|
+
process.on("SIGINT", () => {
|
|
667
|
+
void shutdown();
|
|
668
|
+
});
|
|
669
|
+
process.on("SIGTERM", () => {
|
|
670
|
+
void shutdown();
|
|
671
|
+
});
|
|
672
|
+
await new Promise((resolve) => {
|
|
673
|
+
bridge.once("disconnected", () => resolve());
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
async function runOneShot(options, send) {
|
|
677
|
+
const bridge = createBridge(options, "oneshot");
|
|
678
|
+
await bridge.connect();
|
|
679
|
+
await delay(options.waitMs);
|
|
680
|
+
send(bridge);
|
|
681
|
+
await delay(500);
|
|
682
|
+
await bridge.disconnect();
|
|
683
|
+
}
|
|
684
|
+
async function runConnect(options) {
|
|
685
|
+
const bridge = createBridge(options, "interactive");
|
|
686
|
+
await bridge.connect();
|
|
687
|
+
console.log(`connected: room=${options.room} id=${options.streamId}`);
|
|
688
|
+
console.log("type text to chat, or /help for commands");
|
|
689
|
+
bridge.on("peer:connected", ({ streamId }) => {
|
|
690
|
+
console.log(`[peer] connected ${streamId}`);
|
|
691
|
+
});
|
|
692
|
+
bridge.on("peer:disconnected", ({ streamId }) => {
|
|
693
|
+
console.log(`[peer] disconnected ${streamId}`);
|
|
694
|
+
});
|
|
695
|
+
bridge.bus.on("message:chat", (envelope) => {
|
|
696
|
+
const text = payloadText(envelope);
|
|
697
|
+
console.log(`[chat] ${envelope.from.name}: ${text}`);
|
|
698
|
+
});
|
|
699
|
+
bridge.bus.on("message:command", (envelope) => {
|
|
700
|
+
console.log(`[command] ${envelope.from.name}: ${JSON.stringify(envelope.payload)}`);
|
|
701
|
+
});
|
|
702
|
+
bridge.bus.on("message:command_response", (envelope) => {
|
|
703
|
+
console.log(`[response] ${envelope.from.name}: ${JSON.stringify(envelope.payload)}`);
|
|
704
|
+
});
|
|
705
|
+
bridge.bus.on("message:event", (envelope) => {
|
|
706
|
+
console.log(`[event] ${envelope.from.name}: ${JSON.stringify(envelope.payload)}`);
|
|
707
|
+
});
|
|
708
|
+
const rl = readline.createInterface({
|
|
709
|
+
input: process.stdin,
|
|
710
|
+
output: process.stdout,
|
|
711
|
+
terminal: true,
|
|
712
|
+
});
|
|
713
|
+
let shuttingDown = false;
|
|
714
|
+
const shutdown = async () => {
|
|
715
|
+
if (shuttingDown)
|
|
716
|
+
return;
|
|
717
|
+
shuttingDown = true;
|
|
718
|
+
rl.close();
|
|
719
|
+
await bridge.disconnect();
|
|
720
|
+
};
|
|
721
|
+
process.on("SIGINT", () => {
|
|
722
|
+
void shutdown();
|
|
723
|
+
});
|
|
724
|
+
process.on("SIGTERM", () => {
|
|
725
|
+
void shutdown();
|
|
726
|
+
});
|
|
727
|
+
rl.on("line", (line) => {
|
|
728
|
+
void handleInteractiveLine(bridge, line, shutdown);
|
|
729
|
+
});
|
|
730
|
+
await new Promise((resolve) => {
|
|
731
|
+
rl.on("close", () => resolve());
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
async function handleInteractiveLine(bridge, line, shutdown) {
|
|
735
|
+
const input = line.trim();
|
|
736
|
+
if (!input)
|
|
737
|
+
return;
|
|
738
|
+
if (!input.startsWith("/")) {
|
|
739
|
+
bridge.chat(input);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const [command, ...rest] = input.slice(1).split(" ");
|
|
743
|
+
switch (command) {
|
|
744
|
+
case "help":
|
|
745
|
+
console.log(helpText());
|
|
746
|
+
return;
|
|
747
|
+
case "quit":
|
|
748
|
+
case "exit":
|
|
749
|
+
await shutdown();
|
|
750
|
+
return;
|
|
751
|
+
case "peers":
|
|
752
|
+
console.log(JSON.stringify(bridge.peers.toJSON(), null, 2));
|
|
753
|
+
return;
|
|
754
|
+
case "status":
|
|
755
|
+
bridge.updateStatus(rest[0] || "online", rest.slice(1).join(" "));
|
|
756
|
+
console.log("status updated");
|
|
757
|
+
return;
|
|
758
|
+
case "dm":
|
|
759
|
+
if (rest.length < 2) {
|
|
760
|
+
console.log("usage: /dm <peer> <message>");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
bridge.chat(rest.slice(1).join(" "), rest[0]);
|
|
764
|
+
return;
|
|
765
|
+
case "shares":
|
|
766
|
+
if (rest.length < 1) {
|
|
767
|
+
console.log("usage: /shares <peer>");
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
bridge.command(rest[0], "shares");
|
|
771
|
+
return;
|
|
772
|
+
case "ls":
|
|
773
|
+
if (rest.length < 2) {
|
|
774
|
+
console.log("usage: /ls <peer> <share> [path]");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
bridge.command(rest[0], "list-files", {
|
|
778
|
+
share: rest[1],
|
|
779
|
+
path: rest.slice(2).join(" "),
|
|
780
|
+
});
|
|
781
|
+
return;
|
|
782
|
+
case "get":
|
|
783
|
+
if (rest.length < 3) {
|
|
784
|
+
console.log("usage: /get <peer> <share> <path>");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
bridge.command(rest[0], "get-file", {
|
|
788
|
+
share: rest[1],
|
|
789
|
+
path: rest.slice(2).join(" "),
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
case "file":
|
|
793
|
+
case "image":
|
|
794
|
+
if (rest.length < 2) {
|
|
795
|
+
console.log(`usage: /${command} <peer> <path>`);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
sendFileFromPath(bridge, rest[0], path.resolve(rest.slice(1).join(" ")), command === "image" ? "image" : "file");
|
|
799
|
+
return;
|
|
800
|
+
case "cmd":
|
|
801
|
+
if (rest.length < 2) {
|
|
802
|
+
console.log("usage: /cmd <peer> <command> [json]");
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
bridge.command(rest[0], rest[1], rest[2] ? parseJsonMaybe(rest.slice(2).join(" ")) : undefined);
|
|
806
|
+
return;
|
|
807
|
+
case "respond":
|
|
808
|
+
if (rest.length < 2) {
|
|
809
|
+
console.log("usage: /respond <peer> <requestId> [json]");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
sendCommandResponse(bridge, rest[0], rest[1], rest[2] ? parseJsonMaybe(rest.slice(2).join(" ")) : undefined);
|
|
813
|
+
return;
|
|
814
|
+
case "plan":
|
|
815
|
+
if (rest.length < 2) {
|
|
816
|
+
console.log("usage: /plan <peer> <request>");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
bridge.command(rest[0], "plan", { request: rest.slice(1).join(" ") });
|
|
820
|
+
return;
|
|
821
|
+
case "task":
|
|
822
|
+
if (rest.length < 2) {
|
|
823
|
+
console.log("usage: /task <peer> <request>");
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
bridge.command(rest[0], "task", { request: rest.slice(1).join(" ") });
|
|
827
|
+
return;
|
|
828
|
+
case "review":
|
|
829
|
+
if (rest.length < 2) {
|
|
830
|
+
console.log("usage: /review <peer> <request>");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
bridge.command(rest[0], "review", { request: rest.slice(1).join(" ") });
|
|
834
|
+
return;
|
|
835
|
+
case "approve":
|
|
836
|
+
if (rest.length < 2) {
|
|
837
|
+
console.log("usage: /approve <peer> <request>");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
bridge.command(rest[0], "approve", { request: rest.slice(1).join(" ") });
|
|
841
|
+
return;
|
|
842
|
+
case "event":
|
|
843
|
+
if (rest.length < 2) {
|
|
844
|
+
console.log("usage: /event <topic> <kind> [json]");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
bridge.publishEvent(rest[0], rest[1], rest[2] ? parseJsonMaybe(rest.slice(2).join(" ")) : undefined);
|
|
848
|
+
return;
|
|
849
|
+
default:
|
|
850
|
+
console.log("unknown command; try /help");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function payloadText(envelope) {
|
|
854
|
+
if (typeof envelope.payload === "string")
|
|
855
|
+
return envelope.payload;
|
|
856
|
+
if (typeof envelope.payload === "object" && envelope.payload !== null) {
|
|
857
|
+
const payload = envelope.payload;
|
|
858
|
+
if (typeof payload.text === "string")
|
|
859
|
+
return payload.text;
|
|
860
|
+
if (typeof payload.message === "string")
|
|
861
|
+
return payload.message;
|
|
862
|
+
}
|
|
863
|
+
return JSON.stringify(envelope.payload);
|
|
864
|
+
}
|
|
865
|
+
function getCommandArgString(args, key) {
|
|
866
|
+
if (typeof args !== "object" || args === null)
|
|
867
|
+
return null;
|
|
868
|
+
const value = args[key];
|
|
869
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
870
|
+
}
|
|
871
|
+
function sendCommandResponse(bridge, targetStreamId, requestId, result, error) {
|
|
872
|
+
if (error) {
|
|
873
|
+
return bridge.bus.send(targetStreamId, "command_response", {
|
|
874
|
+
requestId,
|
|
875
|
+
ok: false,
|
|
876
|
+
error,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
return bridge.bus.send(targetStreamId, "command_response", {
|
|
880
|
+
requestId,
|
|
881
|
+
ok: true,
|
|
882
|
+
result: result ?? null,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
function maybeHandleSidecarFileTransfer(bridge, stateDir, envelope) {
|
|
886
|
+
switch (envelope.type) {
|
|
887
|
+
case "file_offer":
|
|
888
|
+
return handleIncomingFileOffer(bridge, stateDir, envelope);
|
|
889
|
+
case "file_chunk":
|
|
890
|
+
return handleIncomingFileChunk(bridge, stateDir, envelope);
|
|
891
|
+
case "file_complete":
|
|
892
|
+
return handleIncomingFileComplete(bridge, stateDir, envelope);
|
|
893
|
+
case "file_ack":
|
|
894
|
+
return handleIncomingFileAck(stateDir, envelope);
|
|
895
|
+
default:
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function handleIncomingFileOffer(bridge, stateDir, envelope) {
|
|
900
|
+
const payload = envelope.payload;
|
|
901
|
+
try {
|
|
902
|
+
beginIncomingTransfer(stateDir, envelope.from, payload);
|
|
903
|
+
storeInboxMessage(stateDir, createFileTransferEventEnvelope(envelope.from, "file_offered", {
|
|
904
|
+
transferId: payload.transferId,
|
|
905
|
+
name: payload.name,
|
|
906
|
+
mimeType: payload.mimeType,
|
|
907
|
+
transferKind: payload.kind,
|
|
908
|
+
size: payload.size,
|
|
909
|
+
totalChunks: payload.totalChunks,
|
|
910
|
+
}));
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
bridge.bus.send(envelope.from.streamId, "file_ack", createFailedFileAckPayload(payload.transferId, error));
|
|
914
|
+
storeInboxMessage(stateDir, createFileTransferEventEnvelope(envelope.from, "file_receive_failed", {
|
|
915
|
+
transferId: payload.transferId,
|
|
916
|
+
error: error instanceof Error ? error.message : String(error),
|
|
917
|
+
}));
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
function handleIncomingFileChunk(bridge, stateDir, envelope) {
|
|
922
|
+
const payload = envelope.payload;
|
|
923
|
+
try {
|
|
924
|
+
appendIncomingTransferChunk(stateDir, payload);
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
bridge.bus.send(envelope.from.streamId, "file_ack", createFailedFileAckPayload(payload.transferId, error));
|
|
928
|
+
storeInboxMessage(stateDir, createFileTransferEventEnvelope(envelope.from, "file_receive_failed", {
|
|
929
|
+
transferId: payload.transferId,
|
|
930
|
+
error: error instanceof Error ? error.message : String(error),
|
|
931
|
+
}));
|
|
932
|
+
}
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
function handleIncomingFileComplete(bridge, stateDir, envelope) {
|
|
936
|
+
const payload = envelope.payload;
|
|
937
|
+
try {
|
|
938
|
+
const completed = completeIncomingTransfer(stateDir, payload);
|
|
939
|
+
bridge.bus.send(envelope.from.streamId, "file_ack", createFileAckPayload(completed));
|
|
940
|
+
storeInboxMessage(stateDir, createReceivedFileEventEnvelope(envelope.from, completed));
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
bridge.bus.send(envelope.from.streamId, "file_ack", createFailedFileAckPayload(payload.transferId, error));
|
|
944
|
+
storeInboxMessage(stateDir, createFileTransferEventEnvelope(envelope.from, "file_receive_failed", {
|
|
945
|
+
transferId: payload.transferId,
|
|
946
|
+
error: error instanceof Error ? error.message : String(error),
|
|
947
|
+
}));
|
|
948
|
+
}
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
function handleIncomingFileAck(stateDir, envelope) {
|
|
952
|
+
const payload = envelope.payload;
|
|
953
|
+
storeInboxMessage(stateDir, createFileTransferEventEnvelope(envelope.from, payload.ok ? "file_delivered" : "file_delivery_failed", {
|
|
954
|
+
transferId: payload.transferId,
|
|
955
|
+
name: payload.name ?? null,
|
|
956
|
+
mimeType: payload.mimeType ?? null,
|
|
957
|
+
transferKind: payload.kind ?? null,
|
|
958
|
+
size: payload.size ?? null,
|
|
959
|
+
sha256: payload.sha256 ?? null,
|
|
960
|
+
savedPath: payload.savedPath ?? null,
|
|
961
|
+
error: payload.error ?? null,
|
|
962
|
+
}));
|
|
963
|
+
return true;
|
|
964
|
+
}
|
|
965
|
+
function createReceivedFileEventEnvelope(from, completed) {
|
|
966
|
+
return createFileTransferEventEnvelope(from, "file_received", {
|
|
967
|
+
transferId: completed.transferId,
|
|
968
|
+
name: completed.name,
|
|
969
|
+
mimeType: completed.mimeType,
|
|
970
|
+
transferKind: completed.kind,
|
|
971
|
+
size: completed.size,
|
|
972
|
+
sha256: completed.sha256,
|
|
973
|
+
savedPath: completed.savedPath,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
export function createFileTransferEventEnvelope(from, kind, details) {
|
|
977
|
+
return createEnvelope(from, "event", {
|
|
978
|
+
...details,
|
|
979
|
+
kind,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
export function composeSidecarAgentProfile(profile) {
|
|
983
|
+
const shares = profile?.shares?.map((share) => ({ ...share }));
|
|
984
|
+
const shareAsks = shares && shares.length > 0 ? SIDECAR_SHARE_ASKS : [];
|
|
985
|
+
const asks = mergeAgentAsks(profile?.asks ?? [], deriveAgentAsksFromCapabilities(profile?.can ?? []), shareAsks, SIDECAR_DISCOVERY_ASKS);
|
|
986
|
+
const can = profile?.can ? [...new Set(profile.can.map((value) => value.trim()).filter(Boolean))] : undefined;
|
|
987
|
+
const composed = {
|
|
988
|
+
asks,
|
|
989
|
+
};
|
|
990
|
+
if (profile?.runtime)
|
|
991
|
+
composed.runtime = profile.runtime;
|
|
992
|
+
if (profile?.provider)
|
|
993
|
+
composed.provider = profile.provider;
|
|
994
|
+
if (profile?.model)
|
|
995
|
+
composed.model = profile.model;
|
|
996
|
+
if (profile?.summary)
|
|
997
|
+
composed.summary = profile.summary;
|
|
998
|
+
if (profile?.workspace)
|
|
999
|
+
composed.workspace = profile.workspace;
|
|
1000
|
+
if (can && can.length > 0)
|
|
1001
|
+
composed.can = can;
|
|
1002
|
+
if (shares && shares.length > 0)
|
|
1003
|
+
composed.shares = shares;
|
|
1004
|
+
return composed;
|
|
1005
|
+
}
|
|
1006
|
+
export function buildSidecarCommandResponse(bridge, stateDir, commandName, commandArgs, sharedFolders = [], requesterStreamId) {
|
|
1007
|
+
const announce = bridge.getAnnouncePayload();
|
|
1008
|
+
const inbox = getInboxSummary(stateDir);
|
|
1009
|
+
const profile = announce.agent ?? null;
|
|
1010
|
+
const identity = bridge.identity;
|
|
1011
|
+
const requestExamples = buildRequestExamples(identity.streamId, profile?.asks ?? SIDECAR_DISCOVERY_ASKS);
|
|
1012
|
+
switch (commandName.trim().toLowerCase()) {
|
|
1013
|
+
case "help":
|
|
1014
|
+
return {
|
|
1015
|
+
handled: true,
|
|
1016
|
+
result: {
|
|
1017
|
+
kind: "ninja-p2p-sidecar",
|
|
1018
|
+
room: inbox.room,
|
|
1019
|
+
identity,
|
|
1020
|
+
connected: bridge.isConnected(),
|
|
1021
|
+
status: announce.status,
|
|
1022
|
+
statusDetail: announce.statusDetail ?? "",
|
|
1023
|
+
profile,
|
|
1024
|
+
commands: profile?.asks ?? SIDECAR_DISCOVERY_ASKS,
|
|
1025
|
+
requestExamples,
|
|
1026
|
+
recommendedFlow: [
|
|
1027
|
+
"Ask for profile or capabilities first.",
|
|
1028
|
+
"Use a command name from the peer's asks list when delegating work.",
|
|
1029
|
+
"If you need sign-off, send approve and wait for a command_response before proceeding.",
|
|
1030
|
+
"If the peer exposes shares, use shares, list-files, and get-file instead of assuming file paths.",
|
|
1031
|
+
"Use chat or DM for open-ended discussion and planning.",
|
|
1032
|
+
"Use events for room-wide updates such as build_failed or task_done.",
|
|
1033
|
+
],
|
|
1034
|
+
},
|
|
1035
|
+
};
|
|
1036
|
+
case "profile":
|
|
1037
|
+
case "whoami":
|
|
1038
|
+
return {
|
|
1039
|
+
handled: true,
|
|
1040
|
+
result: {
|
|
1041
|
+
room: inbox.room,
|
|
1042
|
+
identity,
|
|
1043
|
+
profile,
|
|
1044
|
+
},
|
|
1045
|
+
};
|
|
1046
|
+
case "capabilities":
|
|
1047
|
+
return {
|
|
1048
|
+
handled: true,
|
|
1049
|
+
result: {
|
|
1050
|
+
room: inbox.room,
|
|
1051
|
+
identity,
|
|
1052
|
+
skills: announce.skills,
|
|
1053
|
+
topics: announce.topics,
|
|
1054
|
+
can: profile?.can ?? [],
|
|
1055
|
+
asks: profile?.asks ?? SIDECAR_DISCOVERY_ASKS,
|
|
1056
|
+
shares: profile?.shares ?? [],
|
|
1057
|
+
profile,
|
|
1058
|
+
requestExamples,
|
|
1059
|
+
},
|
|
1060
|
+
};
|
|
1061
|
+
case "status":
|
|
1062
|
+
return {
|
|
1063
|
+
handled: true,
|
|
1064
|
+
result: {
|
|
1065
|
+
room: inbox.room,
|
|
1066
|
+
identity,
|
|
1067
|
+
connected: bridge.isConnected(),
|
|
1068
|
+
status: announce.status,
|
|
1069
|
+
statusDetail: announce.statusDetail ?? "",
|
|
1070
|
+
pending: inbox.pending,
|
|
1071
|
+
queued: inbox.queued,
|
|
1072
|
+
stale: inbox.stale,
|
|
1073
|
+
peersKnown: inbox.peersKnown,
|
|
1074
|
+
peersConnected: inbox.peersConnected,
|
|
1075
|
+
senders: inbox.senders,
|
|
1076
|
+
types: inbox.types,
|
|
1077
|
+
commands: inbox.commands,
|
|
1078
|
+
events: inbox.events,
|
|
1079
|
+
peers: inbox.peers,
|
|
1080
|
+
profile,
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
case "peers":
|
|
1084
|
+
return {
|
|
1085
|
+
handled: true,
|
|
1086
|
+
result: {
|
|
1087
|
+
room: inbox.room,
|
|
1088
|
+
peers: bridge.peers.toJSON(),
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
case "shares":
|
|
1092
|
+
return {
|
|
1093
|
+
handled: true,
|
|
1094
|
+
result: {
|
|
1095
|
+
room: inbox.room,
|
|
1096
|
+
identity,
|
|
1097
|
+
shares: profile?.shares ?? [],
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
1100
|
+
case "list-files":
|
|
1101
|
+
try {
|
|
1102
|
+
const share = getCommandArgString(commandArgs, "share");
|
|
1103
|
+
const folderPath = getCommandArgString(commandArgs, "path") ?? "";
|
|
1104
|
+
if (!share) {
|
|
1105
|
+
return { handled: true, error: "list-files requires args.share" };
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
handled: true,
|
|
1109
|
+
result: listSharedFolderEntries(sharedFolders, share, folderPath),
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
return {
|
|
1114
|
+
handled: true,
|
|
1115
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
case "get-file":
|
|
1119
|
+
try {
|
|
1120
|
+
const share = getCommandArgString(commandArgs, "share");
|
|
1121
|
+
const filePath = getCommandArgString(commandArgs, "path");
|
|
1122
|
+
if (!share || !filePath) {
|
|
1123
|
+
return { handled: true, error: "get-file requires args.share and args.path" };
|
|
1124
|
+
}
|
|
1125
|
+
if (!requesterStreamId) {
|
|
1126
|
+
return { handled: true, error: "missing requester stream id" };
|
|
1127
|
+
}
|
|
1128
|
+
const file = resolveSharedFile(sharedFolders, share, filePath);
|
|
1129
|
+
return {
|
|
1130
|
+
handled: true,
|
|
1131
|
+
result: {
|
|
1132
|
+
share: file.share,
|
|
1133
|
+
path: file.path,
|
|
1134
|
+
name: file.name,
|
|
1135
|
+
size: file.size,
|
|
1136
|
+
transferKind: file.kind,
|
|
1137
|
+
},
|
|
1138
|
+
transfer: {
|
|
1139
|
+
targetStreamId: requesterStreamId,
|
|
1140
|
+
filePath: file.filePath,
|
|
1141
|
+
transferKind: file.kind,
|
|
1142
|
+
},
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
catch (error) {
|
|
1146
|
+
return {
|
|
1147
|
+
handled: true,
|
|
1148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
case "inbox":
|
|
1152
|
+
case "pending":
|
|
1153
|
+
return {
|
|
1154
|
+
handled: true,
|
|
1155
|
+
result: inbox,
|
|
1156
|
+
};
|
|
1157
|
+
default:
|
|
1158
|
+
return { handled: false };
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
export function maybeHandleSidecarCommand(bridge, stateDir, envelope) {
|
|
1162
|
+
if (envelope.type !== "command" || typeof envelope.payload !== "object" || envelope.payload === null) {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
const payload = envelope.payload;
|
|
1166
|
+
if (typeof payload.command !== "string") {
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1169
|
+
const session = readAgentSession(stateDir);
|
|
1170
|
+
const response = buildSidecarCommandResponse(bridge, stateDir, payload.command, payload.args, session?.sharedFolders ?? [], envelope.from.streamId);
|
|
1171
|
+
if (!response.handled) {
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
if (response.transfer) {
|
|
1175
|
+
try {
|
|
1176
|
+
const offer = sendFileFromPath(bridge, response.transfer.targetStreamId, response.transfer.filePath, response.transfer.transferKind);
|
|
1177
|
+
response.result = {
|
|
1178
|
+
...(typeof response.result === "object" && response.result !== null ? response.result : {}),
|
|
1179
|
+
transferId: offer.transferId,
|
|
1180
|
+
mimeType: offer.mimeType,
|
|
1181
|
+
totalChunks: offer.totalChunks,
|
|
1182
|
+
sha256: offer.sha256,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
catch (error) {
|
|
1186
|
+
response.error = error instanceof Error ? error.message : String(error);
|
|
1187
|
+
response.result = undefined;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
bridge.commandResponse(envelope, response.result ?? null, response.error);
|
|
1191
|
+
return true;
|
|
1192
|
+
}
|
|
1193
|
+
export function createPeerNoticeEnvelope(peerIdentity, kind, details) {
|
|
1194
|
+
return createEnvelope(peerIdentity, "event", {
|
|
1195
|
+
kind,
|
|
1196
|
+
...details,
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
export function recordPeerNotice(fingerprints, peerIdentity, kind, details) {
|
|
1200
|
+
const fingerprint = JSON.stringify(details);
|
|
1201
|
+
const previous = fingerprints.get(peerIdentity.streamId);
|
|
1202
|
+
if (previous === fingerprint) {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
const noticeKind = previous ? "peer_updated" : kind;
|
|
1206
|
+
fingerprints.set(peerIdentity.streamId, fingerprint);
|
|
1207
|
+
return createPeerNoticeEnvelope(peerIdentity, noticeKind, details);
|
|
1208
|
+
}
|
|
1209
|
+
function mergeAgentAsks(...groups) {
|
|
1210
|
+
const merged = [];
|
|
1211
|
+
const seen = new Set();
|
|
1212
|
+
for (const ask of groups.flat()) {
|
|
1213
|
+
const name = ask.name.trim();
|
|
1214
|
+
if (!name)
|
|
1215
|
+
continue;
|
|
1216
|
+
const key = name.toLowerCase();
|
|
1217
|
+
if (seen.has(key))
|
|
1218
|
+
continue;
|
|
1219
|
+
seen.add(key);
|
|
1220
|
+
const mergedAsk = {
|
|
1221
|
+
name,
|
|
1222
|
+
description: ask.description.trim() || `${name} command`,
|
|
1223
|
+
};
|
|
1224
|
+
if (ask.via)
|
|
1225
|
+
mergedAsk.via = ask.via;
|
|
1226
|
+
if (ask.example)
|
|
1227
|
+
mergedAsk.example = ask.example;
|
|
1228
|
+
merged.push(mergedAsk);
|
|
1229
|
+
}
|
|
1230
|
+
return merged;
|
|
1231
|
+
}
|
|
1232
|
+
function deriveAgentAsksFromCapabilities(capabilities) {
|
|
1233
|
+
const derived = [];
|
|
1234
|
+
for (const capability of capabilities.map((value) => value.trim().toLowerCase()).filter(Boolean)) {
|
|
1235
|
+
derived.push(...(DERIVED_CAPABILITY_ASKS[capability] ?? []));
|
|
1236
|
+
}
|
|
1237
|
+
return derived;
|
|
1238
|
+
}
|
|
1239
|
+
function buildRequestExamples(peerStreamId, asks) {
|
|
1240
|
+
return asks.map((ask) => {
|
|
1241
|
+
const via = ask.via ?? "command";
|
|
1242
|
+
if (via === "chat") {
|
|
1243
|
+
return {
|
|
1244
|
+
ask: ask.name,
|
|
1245
|
+
via,
|
|
1246
|
+
example: ask.example ?? `ninja-p2p dm --id <you> ${peerStreamId} "Can you help with ${ask.name}?"`,
|
|
1247
|
+
text: `Can you help with ${ask.name}?`,
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
if (via === "event") {
|
|
1251
|
+
return {
|
|
1252
|
+
ask: ask.name,
|
|
1253
|
+
via,
|
|
1254
|
+
example: ask.example ?? `ninja-p2p event --id <you> events ${ask.name} '{"request":"..."}'`,
|
|
1255
|
+
topic: "events",
|
|
1256
|
+
eventKind: ask.name,
|
|
1257
|
+
data: { request: `Please help with ${ask.name}` },
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
ask: ask.name,
|
|
1262
|
+
via: "command",
|
|
1263
|
+
example: ask.example ?? `ninja-p2p command --id <you> ${peerStreamId} ${ask.name} '{"request":"..."}'`,
|
|
1264
|
+
command: ask.name,
|
|
1265
|
+
args: { request: `Please help with ${ask.name}` },
|
|
1266
|
+
};
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
function resolveCliEntryPath() {
|
|
1270
|
+
const current = fileURLToPath(import.meta.url);
|
|
1271
|
+
if (current.endsWith(".ts")) {
|
|
1272
|
+
const built = path.resolve(path.dirname(current), "..", "dist", "cli.js");
|
|
1273
|
+
if (existsSync(built)) {
|
|
1274
|
+
return built;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return current;
|
|
1278
|
+
}
|
|
1279
|
+
function getCommandPrefix(options) {
|
|
1280
|
+
return options.agentProfile?.runtime?.includes("claude") ? "/ninja-p2p" : "ninja-p2p";
|
|
1281
|
+
}
|
|
1282
|
+
function buildAgentArgs(options) {
|
|
1283
|
+
const args = [
|
|
1284
|
+
"--room", options.room,
|
|
1285
|
+
"--name", options.name,
|
|
1286
|
+
"--id", options.streamId,
|
|
1287
|
+
"--role", options.role,
|
|
1288
|
+
"--state-dir", options.stateDir ?? "",
|
|
1289
|
+
];
|
|
1290
|
+
if (options.password !== false) {
|
|
1291
|
+
args.push("--password", String(options.password));
|
|
1292
|
+
}
|
|
1293
|
+
if (options.waitMs !== 1500) {
|
|
1294
|
+
args.push("--wait-ms", String(options.waitMs));
|
|
1295
|
+
}
|
|
1296
|
+
if (options.agentProfile?.runtime) {
|
|
1297
|
+
args.push("--runtime", options.agentProfile.runtime);
|
|
1298
|
+
}
|
|
1299
|
+
if (options.agentProfile?.provider) {
|
|
1300
|
+
args.push("--provider", options.agentProfile.provider);
|
|
1301
|
+
}
|
|
1302
|
+
if (options.agentProfile?.model) {
|
|
1303
|
+
args.push("--model", options.agentProfile.model);
|
|
1304
|
+
}
|
|
1305
|
+
if (options.agentProfile?.summary) {
|
|
1306
|
+
args.push("--summary", options.agentProfile.summary);
|
|
1307
|
+
}
|
|
1308
|
+
if (options.agentProfile?.workspace) {
|
|
1309
|
+
args.push("--workspace", options.agentProfile.workspace);
|
|
1310
|
+
}
|
|
1311
|
+
for (const capability of options.agentProfile?.can ?? []) {
|
|
1312
|
+
args.push("--can", capability);
|
|
1313
|
+
}
|
|
1314
|
+
for (const ask of options.agentProfile?.asks ?? []) {
|
|
1315
|
+
args.push("--ask", `${ask.name}:${ask.description}`);
|
|
1316
|
+
}
|
|
1317
|
+
for (const share of options.sharedFolders) {
|
|
1318
|
+
args.push("--share", `${share.name}=${share.path}`);
|
|
1319
|
+
}
|
|
1320
|
+
return args;
|
|
1321
|
+
}
|
|
1322
|
+
function isPidRunning(pid) {
|
|
1323
|
+
try {
|
|
1324
|
+
process.kill(pid, 0);
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const entry = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false;
|
|
1332
|
+
if (entry) {
|
|
1333
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
1334
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
});
|
|
1337
|
+
}
|