@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.
@@ -0,0 +1,167 @@
1
+ import { type AgentAsk, type AgentProfile, type FileTransferKind, type MessageEnvelope, type MessageType, type SharedFolderSummary } from "./protocol.js";
2
+ import type { SharedFolderConfig } from "./shared-folders.js";
3
+ export type AgentSessionState = {
4
+ room: string;
5
+ streamId: string;
6
+ name: string;
7
+ role: string;
8
+ stateDir: string;
9
+ pid: number;
10
+ connected: boolean;
11
+ skills: string[];
12
+ topics: string[];
13
+ agentProfile: AgentProfile | null;
14
+ sharedFolders: SharedFolderConfig[];
15
+ startedAt: number;
16
+ updatedAt: number;
17
+ };
18
+ export type QueuedAgentAction = {
19
+ id: string;
20
+ createdAt: number;
21
+ kind: "chat";
22
+ text: string;
23
+ } | {
24
+ id: string;
25
+ createdAt: number;
26
+ kind: "dm";
27
+ target: string;
28
+ text: string;
29
+ } | {
30
+ id: string;
31
+ createdAt: number;
32
+ kind: "send_file";
33
+ target: string;
34
+ filePath: string;
35
+ transferKind: FileTransferKind;
36
+ } | {
37
+ id: string;
38
+ createdAt: number;
39
+ kind: "command";
40
+ target: string;
41
+ command: string;
42
+ args?: unknown;
43
+ } | {
44
+ id: string;
45
+ createdAt: number;
46
+ kind: "response";
47
+ target: string;
48
+ requestId: string;
49
+ result?: unknown;
50
+ error?: string;
51
+ } | {
52
+ id: string;
53
+ createdAt: number;
54
+ kind: "event";
55
+ topic: string;
56
+ eventKind: string;
57
+ data?: unknown;
58
+ } | {
59
+ id: string;
60
+ createdAt: number;
61
+ kind: "status";
62
+ status: string;
63
+ detail?: string;
64
+ };
65
+ export type QueuedAgentActionInput = {
66
+ kind: "chat";
67
+ text: string;
68
+ } | {
69
+ kind: "dm";
70
+ target: string;
71
+ text: string;
72
+ } | {
73
+ kind: "send_file";
74
+ target: string;
75
+ filePath: string;
76
+ transferKind: FileTransferKind;
77
+ } | {
78
+ kind: "command";
79
+ target: string;
80
+ command: string;
81
+ args?: unknown;
82
+ } | {
83
+ kind: "response";
84
+ target: string;
85
+ requestId: string;
86
+ result?: unknown;
87
+ error?: string;
88
+ } | {
89
+ kind: "event";
90
+ topic: string;
91
+ eventKind: string;
92
+ data?: unknown;
93
+ } | {
94
+ kind: "status";
95
+ status: string;
96
+ detail?: string;
97
+ };
98
+ export type InboxMessage = {
99
+ envelope: MessageEnvelope;
100
+ receivedAt: number;
101
+ path: string;
102
+ };
103
+ export type PeerInboxSummary = {
104
+ streamId: string;
105
+ name: string;
106
+ role: string;
107
+ connected: boolean;
108
+ status: string;
109
+ statusDetail: string;
110
+ summary: string | null;
111
+ can: string[];
112
+ asks: AgentAsk[];
113
+ shares: SharedFolderSummary[];
114
+ };
115
+ export type InboxSummary = {
116
+ connected: boolean;
117
+ pending: number;
118
+ queued: number;
119
+ room: string | null;
120
+ streamId: string | null;
121
+ stale: boolean;
122
+ peersKnown: number;
123
+ peersConnected: number;
124
+ senders: Array<{
125
+ streamId: string;
126
+ name: string;
127
+ count: number;
128
+ }>;
129
+ types: Array<{
130
+ type: string;
131
+ count: number;
132
+ }>;
133
+ commands: Array<{
134
+ name: string;
135
+ count: number;
136
+ }>;
137
+ events: Array<{
138
+ kind: string;
139
+ count: number;
140
+ }>;
141
+ peers: PeerInboxSummary[];
142
+ };
143
+ type StatePaths = {
144
+ stateDir: string;
145
+ inboxDir: string;
146
+ archiveDir: string;
147
+ outboxDir: string;
148
+ sessionFile: string;
149
+ peersFile: string;
150
+ };
151
+ export declare function ensureAgentState(stateDir: string): StatePaths;
152
+ export declare function writeAgentSession(stateDir: string, session: AgentSessionState): void;
153
+ export declare function readAgentSession(stateDir: string): AgentSessionState | null;
154
+ export declare function writePeersSnapshot(stateDir: string, peers: unknown): void;
155
+ export declare function readPeersSnapshot(stateDir: string): unknown;
156
+ export declare function queueAgentAction(stateDir: string, action: QueuedAgentActionInput): QueuedAgentAction;
157
+ export declare function listQueuedAgentActions(stateDir: string): Array<{
158
+ action: QueuedAgentAction;
159
+ path: string;
160
+ }>;
161
+ export declare function removeQueuedAgentAction(filePath: string): void;
162
+ export declare function storeInboxMessage(stateDir: string, envelope: MessageEnvelope): InboxMessage;
163
+ export declare function listInboxMessages(stateDir: string): InboxMessage[];
164
+ export declare function takeInboxMessages(stateDir: string, count?: number, peek?: boolean): InboxMessage[];
165
+ export declare function getInboxSummary(stateDir: string): InboxSummary;
166
+ export declare function isInboxWorthy(type: MessageType): boolean;
167
+ export {};
@@ -0,0 +1,304 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import path from "node:path";
3
+ import { createMessageId, } from "./protocol.js";
4
+ const SESSION_FILE = "session.json";
5
+ const PEERS_FILE = "peers.json";
6
+ export function ensureAgentState(stateDir) {
7
+ const resolved = path.resolve(stateDir);
8
+ const paths = {
9
+ stateDir: resolved,
10
+ inboxDir: path.join(resolved, "inbox"),
11
+ archiveDir: path.join(resolved, "archive"),
12
+ outboxDir: path.join(resolved, "outbox"),
13
+ sessionFile: path.join(resolved, SESSION_FILE),
14
+ peersFile: path.join(resolved, PEERS_FILE),
15
+ };
16
+ mkdirSync(paths.stateDir, { recursive: true });
17
+ mkdirSync(paths.inboxDir, { recursive: true });
18
+ mkdirSync(paths.archiveDir, { recursive: true });
19
+ mkdirSync(paths.outboxDir, { recursive: true });
20
+ return paths;
21
+ }
22
+ export function writeAgentSession(stateDir, session) {
23
+ const paths = ensureAgentState(stateDir);
24
+ writeJsonAtomic(paths.sessionFile, session);
25
+ }
26
+ export function readAgentSession(stateDir) {
27
+ const paths = ensureAgentState(stateDir);
28
+ return readJsonFile(paths.sessionFile);
29
+ }
30
+ export function writePeersSnapshot(stateDir, peers) {
31
+ const paths = ensureAgentState(stateDir);
32
+ writeJsonAtomic(paths.peersFile, peers);
33
+ }
34
+ export function readPeersSnapshot(stateDir) {
35
+ const paths = ensureAgentState(stateDir);
36
+ return readJsonFile(paths.peersFile) ?? [];
37
+ }
38
+ export function queueAgentAction(stateDir, action) {
39
+ const paths = ensureAgentState(stateDir);
40
+ const queued = {
41
+ ...action,
42
+ id: createMessageId(),
43
+ createdAt: Date.now(),
44
+ };
45
+ const filename = `${queued.createdAt}_${queued.id}.json`;
46
+ writeJsonAtomic(path.join(paths.outboxDir, filename), queued);
47
+ return queued;
48
+ }
49
+ export function listQueuedAgentActions(stateDir) {
50
+ const paths = ensureAgentState(stateDir);
51
+ const entries = listJsonFiles(paths.outboxDir);
52
+ return entries
53
+ .map((file) => {
54
+ const action = readJsonFile(file);
55
+ return action ? { action, path: file } : null;
56
+ })
57
+ .filter((item) => item !== null)
58
+ .sort((a, b) => a.action.createdAt - b.action.createdAt);
59
+ }
60
+ export function removeQueuedAgentAction(filePath) {
61
+ if (existsSync(filePath)) {
62
+ rmSync(filePath, { force: true });
63
+ }
64
+ }
65
+ export function storeInboxMessage(stateDir, envelope) {
66
+ const paths = ensureAgentState(stateDir);
67
+ const filename = `${envelope.ts}_${envelope.id}.json`;
68
+ const filePath = path.join(paths.inboxDir, filename);
69
+ const record = {
70
+ envelope,
71
+ receivedAt: Date.now(),
72
+ };
73
+ if (!existsSync(filePath)) {
74
+ writeJsonAtomic(filePath, record);
75
+ }
76
+ return {
77
+ envelope,
78
+ receivedAt: record.receivedAt,
79
+ path: filePath,
80
+ };
81
+ }
82
+ export function listInboxMessages(stateDir) {
83
+ const paths = ensureAgentState(stateDir);
84
+ return listJsonFiles(paths.inboxDir)
85
+ .map((file) => {
86
+ const item = readJsonFile(file);
87
+ return item ? { ...item, path: file } : null;
88
+ })
89
+ .filter((item) => item !== null)
90
+ .sort((a, b) => a.envelope.ts - b.envelope.ts);
91
+ }
92
+ export function takeInboxMessages(stateDir, count = 20, peek = false) {
93
+ const paths = ensureAgentState(stateDir);
94
+ const items = listInboxMessages(stateDir).slice(0, count);
95
+ if (!peek) {
96
+ for (const item of items) {
97
+ const target = path.join(paths.archiveDir, path.basename(item.path));
98
+ renameSync(item.path, target);
99
+ item.path = target;
100
+ }
101
+ }
102
+ return items;
103
+ }
104
+ export function getInboxSummary(stateDir) {
105
+ const paths = ensureAgentState(stateDir);
106
+ const session = readJsonFile(paths.sessionFile);
107
+ const messages = listInboxMessages(stateDir);
108
+ const queued = listQueuedAgentActions(stateDir).length;
109
+ const peers = normalizePeers(readJsonFile(paths.peersFile) ?? []);
110
+ const senders = new Map();
111
+ const types = new Map();
112
+ const commands = new Map();
113
+ const events = new Map();
114
+ for (const item of messages) {
115
+ const key = item.envelope.from.streamId;
116
+ const sender = senders.get(key) ?? {
117
+ streamId: item.envelope.from.streamId,
118
+ name: item.envelope.from.name,
119
+ count: 0,
120
+ };
121
+ sender.count += 1;
122
+ senders.set(key, sender);
123
+ types.set(item.envelope.type, (types.get(item.envelope.type) ?? 0) + 1);
124
+ if (item.envelope.type === "command") {
125
+ const commandName = getStringField(item.envelope.payload, "command");
126
+ if (commandName) {
127
+ commands.set(commandName, (commands.get(commandName) ?? 0) + 1);
128
+ }
129
+ }
130
+ if (item.envelope.type === "event") {
131
+ const eventKind = getStringField(item.envelope.payload, "kind");
132
+ if (eventKind) {
133
+ events.set(eventKind, (events.get(eventKind) ?? 0) + 1);
134
+ }
135
+ }
136
+ }
137
+ const updatedAt = session?.updatedAt ?? 0;
138
+ const stale = updatedAt > 0 ? Date.now() - updatedAt > 20_000 : true;
139
+ return {
140
+ connected: session?.connected === true && !stale,
141
+ pending: messages.length,
142
+ queued,
143
+ room: session?.room ?? null,
144
+ streamId: session?.streamId ?? null,
145
+ stale,
146
+ peersKnown: peers.length,
147
+ peersConnected: peers.filter((peer) => peer.connected).length,
148
+ senders: [...senders.values()].sort((a, b) => b.count - a.count),
149
+ types: sortTypeCounts(types),
150
+ commands: sortCommandCounts(commands),
151
+ events: sortEventCounts(events),
152
+ peers,
153
+ };
154
+ }
155
+ export function isInboxWorthy(type) {
156
+ return ![
157
+ "announce",
158
+ "skill_update",
159
+ "ping",
160
+ "pong",
161
+ "history_request",
162
+ "file_offer",
163
+ "file_chunk",
164
+ "file_complete",
165
+ "file_ack",
166
+ ].includes(type);
167
+ }
168
+ function listJsonFiles(dir) {
169
+ if (!existsSync(dir))
170
+ return [];
171
+ return readdirSync(dir)
172
+ .filter((name) => name.endsWith(".json"))
173
+ .map((name) => path.join(dir, name))
174
+ .sort();
175
+ }
176
+ function readJsonFile(filePath) {
177
+ try {
178
+ const raw = readFileSync(filePath, "utf8");
179
+ return JSON.parse(raw);
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ function writeJsonAtomic(filePath, data) {
186
+ const tmp = `${filePath}.${createMessageId()}.tmp`;
187
+ writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, "utf8");
188
+ renameSync(tmp, filePath);
189
+ }
190
+ function getStringField(payload, key) {
191
+ if (typeof payload !== "object" || payload === null)
192
+ return null;
193
+ const value = payload[key];
194
+ return typeof value === "string" && value.trim() ? value : null;
195
+ }
196
+ function sortTypeCounts(map) {
197
+ return [...map.entries()]
198
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
199
+ .map(([type, count]) => ({ type, count }));
200
+ }
201
+ function sortCommandCounts(map) {
202
+ return [...map.entries()]
203
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
204
+ .map(([name, count]) => ({ name, count }));
205
+ }
206
+ function sortEventCounts(map) {
207
+ return [...map.entries()]
208
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
209
+ .map(([kind, count]) => ({ kind, count }));
210
+ }
211
+ function normalizePeers(peers) {
212
+ return peers
213
+ .map((peer) => normalizePeer(peer))
214
+ .filter((peer) => peer !== null)
215
+ .sort((a, b) => Number(b.connected) - Number(a.connected) || a.streamId.localeCompare(b.streamId));
216
+ }
217
+ function normalizePeer(peer) {
218
+ if (typeof peer !== "object" || peer === null)
219
+ return null;
220
+ const data = peer;
221
+ const streamId = getStringLike(data.streamId);
222
+ if (!streamId)
223
+ return null;
224
+ const agentProfile = (typeof data.agentProfile === "object" && data.agentProfile !== null)
225
+ ? data.agentProfile
226
+ : null;
227
+ return {
228
+ streamId,
229
+ name: getStringLike(data.name) ?? streamId,
230
+ role: getStringLike(data.role) ?? "unknown",
231
+ connected: data.connected === true,
232
+ status: getStringLike(data.status) ?? "unknown",
233
+ statusDetail: getStringLike(data.statusDetail) ?? "",
234
+ summary: getStringLike(agentProfile?.summary) ?? null,
235
+ can: normalizeStringArray(agentProfile?.can),
236
+ asks: normalizeAgentAsks(agentProfile?.asks),
237
+ shares: normalizeSharedFolders(agentProfile?.shares),
238
+ };
239
+ }
240
+ function normalizeAgentAsks(value) {
241
+ if (!Array.isArray(value))
242
+ return [];
243
+ const asks = [];
244
+ const seen = new Set();
245
+ for (const item of value) {
246
+ if (typeof item !== "object" || item === null)
247
+ continue;
248
+ const ask = item;
249
+ const name = getStringLike(ask.name);
250
+ if (!name)
251
+ continue;
252
+ const key = name.toLowerCase();
253
+ if (seen.has(key))
254
+ continue;
255
+ seen.add(key);
256
+ const normalized = {
257
+ name,
258
+ description: getStringLike(ask.description) ?? `${name} command`,
259
+ };
260
+ const via = getStringLike(ask.via);
261
+ if (via === "command" || via === "chat" || via === "event") {
262
+ normalized.via = via;
263
+ }
264
+ const example = getStringLike(ask.example);
265
+ if (example)
266
+ normalized.example = example;
267
+ asks.push(normalized);
268
+ }
269
+ return asks;
270
+ }
271
+ function normalizeStringArray(value) {
272
+ if (!Array.isArray(value))
273
+ return [];
274
+ return [...new Set(value
275
+ .map((item) => getStringLike(item))
276
+ .filter((item) => Boolean(item)))];
277
+ }
278
+ function normalizeSharedFolders(value) {
279
+ if (!Array.isArray(value))
280
+ return [];
281
+ const shares = [];
282
+ const seen = new Set();
283
+ for (const item of value) {
284
+ if (typeof item !== "object" || item === null)
285
+ continue;
286
+ const share = item;
287
+ const name = getStringLike(share.name);
288
+ if (!name)
289
+ continue;
290
+ const key = name.toLowerCase();
291
+ if (seen.has(key))
292
+ continue;
293
+ seen.add(key);
294
+ const normalized = { name };
295
+ const description = getStringLike(share.description);
296
+ if (description)
297
+ normalized.description = description;
298
+ shares.push(normalized);
299
+ }
300
+ return shares;
301
+ }
302
+ function getStringLike(value) {
303
+ return typeof value === "string" && value.trim() ? value.trim() : null;
304
+ }
@@ -0,0 +1,132 @@
1
+ import { type AgentProfile } from "./protocol.js";
2
+ import { type SharedFolderConfig } from "./shared-folders.js";
3
+ export type CliCommonOptions = {
4
+ room: string;
5
+ streamId: string;
6
+ name: string;
7
+ role: string;
8
+ password: string | false;
9
+ waitMs: number;
10
+ stateDir: string | null;
11
+ agentProfile?: AgentProfile;
12
+ sharedFolders: SharedFolderConfig[];
13
+ };
14
+ export type SkillRuntime = "claude" | "codex";
15
+ export type CliCommand = {
16
+ kind: "help";
17
+ } | {
18
+ kind: "menu";
19
+ options: CliCommonOptions;
20
+ } | {
21
+ kind: "install-skill";
22
+ runtime: SkillRuntime;
23
+ } | {
24
+ kind: "start";
25
+ options: CliCommonOptions;
26
+ } | {
27
+ kind: "stop";
28
+ stateDir: string;
29
+ } | {
30
+ kind: "status";
31
+ stateDir: string;
32
+ } | {
33
+ kind: "room";
34
+ stateDir: string;
35
+ } | {
36
+ kind: "agent";
37
+ options: CliCommonOptions;
38
+ } | {
39
+ kind: "notify";
40
+ stateDir: string;
41
+ } | {
42
+ kind: "read";
43
+ stateDir: string;
44
+ take: number;
45
+ peek: boolean;
46
+ } | {
47
+ kind: "connect";
48
+ options: CliCommonOptions;
49
+ } | {
50
+ kind: "chat";
51
+ options: CliCommonOptions;
52
+ text: string;
53
+ } | {
54
+ kind: "dm";
55
+ options: CliCommonOptions;
56
+ target: string;
57
+ text: string;
58
+ } | {
59
+ kind: "send-file";
60
+ options: CliCommonOptions;
61
+ target: string;
62
+ filePath: string;
63
+ } | {
64
+ kind: "send-image";
65
+ options: CliCommonOptions;
66
+ target: string;
67
+ filePath: string;
68
+ } | {
69
+ kind: "shares";
70
+ options: CliCommonOptions;
71
+ target: string;
72
+ } | {
73
+ kind: "list-files";
74
+ options: CliCommonOptions;
75
+ target: string;
76
+ share: string;
77
+ folderPath: string;
78
+ } | {
79
+ kind: "get-file";
80
+ options: CliCommonOptions;
81
+ target: string;
82
+ share: string;
83
+ filePath: string;
84
+ } | {
85
+ kind: "command";
86
+ options: CliCommonOptions;
87
+ target: string;
88
+ command: string;
89
+ args: unknown;
90
+ } | {
91
+ kind: "respond";
92
+ options: CliCommonOptions;
93
+ target: string;
94
+ requestId: string;
95
+ result: unknown;
96
+ error?: string;
97
+ } | {
98
+ kind: "event";
99
+ options: CliCommonOptions;
100
+ topic: string;
101
+ eventKind: string;
102
+ data: unknown;
103
+ } | {
104
+ kind: "task";
105
+ options: CliCommonOptions;
106
+ target: string;
107
+ request: string;
108
+ } | {
109
+ kind: "review";
110
+ options: CliCommonOptions;
111
+ target: string;
112
+ request: string;
113
+ } | {
114
+ kind: "plan";
115
+ options: CliCommonOptions;
116
+ target: string;
117
+ request: string;
118
+ } | {
119
+ kind: "approve";
120
+ options: CliCommonOptions;
121
+ target: string;
122
+ request: string;
123
+ };
124
+ export declare function helpText(): string;
125
+ export declare function parseCliArgs(argv: string[], env?: NodeJS.ProcessEnv): CliCommand;
126
+ export declare function defaultStreamId(name: string): string;
127
+ export declare function defaultNameForRuntime(runtime: string | undefined): string;
128
+ export declare function defaultStreamIdForRuntime(name: string, runtime: string | undefined): string;
129
+ export declare function parseJsonMaybe(value: string): unknown;
130
+ export declare function getSkillInstallTarget(runtime: SkillRuntime, env?: NodeJS.ProcessEnv): string;
131
+ export declare function getSkillInstallTargets(runtime: SkillRuntime, env?: NodeJS.ProcessEnv): string[];
132
+ export declare function getDefaultStateDir(streamId: string, env?: NodeJS.ProcessEnv): string;