@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,54 @@
1
+ import { type FileAckPayload, type FileChunkPayload, type FileCompletePayload, type FileOfferPayload, type FileTransferKind, type PeerIdentity } from "./protocol.js";
2
+ import type { VDOBridge } from "./vdo-bridge.js";
3
+ export declare const DEFAULT_TRANSFER_CHUNK_SIZE = 12000;
4
+ export type PreparedFileTransfer = {
5
+ name: string;
6
+ filePath: string;
7
+ mimeType: string;
8
+ kind: FileTransferKind;
9
+ bytes: Uint8Array;
10
+ size: number;
11
+ sha256: string;
12
+ };
13
+ export type IncomingTransferManifest = {
14
+ transferId: string;
15
+ from: PeerIdentity;
16
+ name: string;
17
+ safeName: string;
18
+ mimeType: string;
19
+ kind: FileTransferKind;
20
+ size: number;
21
+ sha256: string;
22
+ chunkSize: number;
23
+ totalChunks: number;
24
+ receivedChunks: number;
25
+ receivedBytes: number;
26
+ tempPath: string;
27
+ savedPath: string;
28
+ createdAt: number;
29
+ updatedAt: number;
30
+ completedAt?: number;
31
+ };
32
+ export type CompletedTransferResult = {
33
+ transferId: string;
34
+ name: string;
35
+ mimeType: string;
36
+ kind: FileTransferKind;
37
+ size: number;
38
+ sha256: string;
39
+ savedPath: string;
40
+ };
41
+ export declare function prepareFileTransferFromPath(filePath: string, kind?: FileTransferKind): PreparedFileTransfer;
42
+ export declare function sendPreparedFileTransfer(bridge: VDOBridge, targetStreamId: string, prepared: PreparedFileTransfer, chunkSize?: number): FileOfferPayload;
43
+ export declare function sendFileFromPath(bridge: VDOBridge, targetStreamId: string, filePath: string, kind?: FileTransferKind, chunkSize?: number): FileOfferPayload;
44
+ export declare function beginIncomingTransfer(stateDir: string, from: PeerIdentity, offer: FileOfferPayload): IncomingTransferManifest;
45
+ export declare function appendIncomingTransferChunk(stateDir: string, payload: FileChunkPayload): IncomingTransferManifest;
46
+ export declare function completeIncomingTransfer(stateDir: string, payload: FileCompletePayload): CompletedTransferResult;
47
+ export declare function createFileAckPayload(result: CompletedTransferResult): FileAckPayload;
48
+ export declare function createFailedFileAckPayload(transferId: string, error: unknown): FileAckPayload;
49
+ export declare function readTransferManifest(stateDir: string, transferId: string): IncomingTransferManifest | null;
50
+ export declare function sha256Hex(bytes: Uint8Array): string;
51
+ export declare function bytesToBase64(bytes: Uint8Array): string;
52
+ export declare function base64ToBytes(value: string): Uint8Array;
53
+ export declare function readSavedTransferBytes(savedPath: string): Uint8Array;
54
+ export declare function getSavedTransferSize(savedPath: string): number;
@@ -0,0 +1,241 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { createMessageId, } from "./protocol.js";
5
+ export const DEFAULT_TRANSFER_CHUNK_SIZE = 12_000;
6
+ export function prepareFileTransferFromPath(filePath, kind = "file") {
7
+ const resolved = path.resolve(filePath);
8
+ const bytes = new Uint8Array(readFileSync(resolved));
9
+ const name = path.basename(resolved);
10
+ return {
11
+ name,
12
+ filePath: resolved,
13
+ mimeType: guessMimeType(name, kind),
14
+ kind,
15
+ bytes,
16
+ size: bytes.byteLength,
17
+ sha256: sha256Hex(bytes),
18
+ };
19
+ }
20
+ export function sendPreparedFileTransfer(bridge, targetStreamId, prepared, chunkSize = DEFAULT_TRANSFER_CHUNK_SIZE) {
21
+ if (!bridge.isConnected()) {
22
+ throw new Error("bridge is not connected");
23
+ }
24
+ if (!bridge.peers.isConnected(targetStreamId)) {
25
+ throw new Error(`peer is not connected: ${targetStreamId}`);
26
+ }
27
+ const normalizedChunkSize = Math.max(1_024, chunkSize);
28
+ const totalChunks = prepared.size === 0 ? 0 : Math.ceil(prepared.size / normalizedChunkSize);
29
+ const transferId = createMessageId();
30
+ const offer = {
31
+ transferId,
32
+ name: prepared.name,
33
+ mimeType: prepared.mimeType,
34
+ kind: prepared.kind,
35
+ size: prepared.size,
36
+ sha256: prepared.sha256,
37
+ chunkSize: normalizedChunkSize,
38
+ totalChunks,
39
+ };
40
+ bridge.bus.send(targetStreamId, "file_offer", offer);
41
+ for (let index = 0; index < totalChunks; index += 1) {
42
+ const start = index * normalizedChunkSize;
43
+ const end = Math.min(start + normalizedChunkSize, prepared.size);
44
+ const chunk = prepared.bytes.slice(start, end);
45
+ const payload = {
46
+ transferId,
47
+ index,
48
+ totalChunks,
49
+ data: bytesToBase64(chunk),
50
+ };
51
+ bridge.bus.send(targetStreamId, "file_chunk", payload);
52
+ }
53
+ bridge.bus.send(targetStreamId, "file_complete", {
54
+ transferId,
55
+ totalChunks,
56
+ size: prepared.size,
57
+ sha256: prepared.sha256,
58
+ });
59
+ return offer;
60
+ }
61
+ export function sendFileFromPath(bridge, targetStreamId, filePath, kind = "file", chunkSize = DEFAULT_TRANSFER_CHUNK_SIZE) {
62
+ return sendPreparedFileTransfer(bridge, targetStreamId, prepareFileTransferFromPath(filePath, kind), chunkSize);
63
+ }
64
+ export function beginIncomingTransfer(stateDir, from, offer) {
65
+ const paths = getTransferPaths(stateDir, offer.transferId);
66
+ mkdirSync(paths.transfersDir, { recursive: true });
67
+ mkdirSync(paths.downloadsDir, { recursive: true });
68
+ const existing = readTransferManifest(stateDir, offer.transferId);
69
+ if (existing) {
70
+ return existing;
71
+ }
72
+ writeFileSync(paths.tempPath, new Uint8Array(0));
73
+ const safeName = sanitizeFileName(offer.name);
74
+ const manifest = {
75
+ transferId: offer.transferId,
76
+ from,
77
+ name: offer.name,
78
+ safeName,
79
+ mimeType: offer.mimeType,
80
+ kind: offer.kind,
81
+ size: offer.size,
82
+ sha256: offer.sha256,
83
+ chunkSize: offer.chunkSize,
84
+ totalChunks: offer.totalChunks,
85
+ receivedChunks: 0,
86
+ receivedBytes: 0,
87
+ tempPath: paths.tempPath,
88
+ savedPath: chooseSavedPath(paths.downloadsDir, safeName, offer.transferId),
89
+ createdAt: Date.now(),
90
+ updatedAt: Date.now(),
91
+ };
92
+ writeManifest(paths.manifestPath, manifest);
93
+ return manifest;
94
+ }
95
+ export function appendIncomingTransferChunk(stateDir, payload) {
96
+ const manifest = mustReadTransferManifest(stateDir, payload.transferId);
97
+ if (manifest.completedAt) {
98
+ return manifest;
99
+ }
100
+ if (payload.index !== manifest.receivedChunks) {
101
+ throw new Error(`unexpected chunk index ${payload.index}; expected ${manifest.receivedChunks}`);
102
+ }
103
+ if (payload.totalChunks !== manifest.totalChunks) {
104
+ throw new Error(`unexpected totalChunks ${payload.totalChunks}; expected ${manifest.totalChunks}`);
105
+ }
106
+ const bytes = base64ToBytes(payload.data);
107
+ appendFileSync(manifest.tempPath, bytes);
108
+ manifest.receivedChunks += 1;
109
+ manifest.receivedBytes += bytes.byteLength;
110
+ manifest.updatedAt = Date.now();
111
+ writeManifest(getTransferPaths(stateDir, payload.transferId).manifestPath, manifest);
112
+ return manifest;
113
+ }
114
+ export function completeIncomingTransfer(stateDir, payload) {
115
+ const manifest = mustReadTransferManifest(stateDir, payload.transferId);
116
+ if (!manifest.completedAt) {
117
+ if (payload.totalChunks !== manifest.totalChunks) {
118
+ throw new Error(`unexpected totalChunks ${payload.totalChunks}; expected ${manifest.totalChunks}`);
119
+ }
120
+ if (manifest.receivedChunks !== manifest.totalChunks) {
121
+ throw new Error(`transfer incomplete: received ${manifest.receivedChunks}/${manifest.totalChunks} chunks`);
122
+ }
123
+ if (manifest.receivedBytes !== manifest.size || payload.size !== manifest.size) {
124
+ throw new Error(`transfer size mismatch: received ${manifest.receivedBytes}, expected ${manifest.size}`);
125
+ }
126
+ const bytes = new Uint8Array(readFileSync(manifest.tempPath));
127
+ const sha256 = sha256Hex(bytes);
128
+ if (sha256 !== manifest.sha256 || payload.sha256 !== manifest.sha256) {
129
+ throw new Error("transfer sha256 mismatch");
130
+ }
131
+ renameSync(manifest.tempPath, manifest.savedPath);
132
+ manifest.completedAt = Date.now();
133
+ manifest.updatedAt = manifest.completedAt;
134
+ writeManifest(getTransferPaths(stateDir, payload.transferId).manifestPath, manifest);
135
+ }
136
+ return {
137
+ transferId: manifest.transferId,
138
+ name: manifest.name,
139
+ mimeType: manifest.mimeType,
140
+ kind: manifest.kind,
141
+ size: manifest.size,
142
+ sha256: manifest.sha256,
143
+ savedPath: manifest.savedPath,
144
+ };
145
+ }
146
+ export function createFileAckPayload(result) {
147
+ return {
148
+ transferId: result.transferId,
149
+ ok: true,
150
+ name: result.name,
151
+ mimeType: result.mimeType,
152
+ kind: result.kind,
153
+ size: result.size,
154
+ sha256: result.sha256,
155
+ savedPath: result.savedPath,
156
+ };
157
+ }
158
+ export function createFailedFileAckPayload(transferId, error) {
159
+ return {
160
+ transferId,
161
+ ok: false,
162
+ error: error instanceof Error ? error.message : String(error),
163
+ };
164
+ }
165
+ export function readTransferManifest(stateDir, transferId) {
166
+ const manifestPath = getTransferPaths(stateDir, transferId).manifestPath;
167
+ if (!existsSync(manifestPath))
168
+ return null;
169
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
170
+ }
171
+ function mustReadTransferManifest(stateDir, transferId) {
172
+ const manifest = readTransferManifest(stateDir, transferId);
173
+ if (!manifest) {
174
+ throw new Error(`unknown transfer: ${transferId}`);
175
+ }
176
+ return manifest;
177
+ }
178
+ function getTransferPaths(stateDir, transferId) {
179
+ const transfersDir = path.join(path.resolve(stateDir), "transfers");
180
+ const downloadsDir = path.join(path.resolve(stateDir), "downloads");
181
+ return {
182
+ transfersDir,
183
+ downloadsDir,
184
+ manifestPath: path.join(transfersDir, `${transferId}.json`),
185
+ tempPath: path.join(transfersDir, `${transferId}.part`),
186
+ };
187
+ }
188
+ function writeManifest(manifestPath, manifest) {
189
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
190
+ }
191
+ function chooseSavedPath(downloadsDir, safeName, transferId) {
192
+ const parsed = path.parse(safeName);
193
+ const base = parsed.name || "download";
194
+ const ext = parsed.ext || "";
195
+ const direct = path.join(downloadsDir, safeName || `download_${transferId}`);
196
+ if (!existsSync(direct)) {
197
+ return direct;
198
+ }
199
+ return path.join(downloadsDir, `${base}_${transferId}${ext}`);
200
+ }
201
+ function sanitizeFileName(name) {
202
+ const base = path.basename(name || "download");
203
+ return base.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_") || "download";
204
+ }
205
+ function guessMimeType(name, kind) {
206
+ const ext = path.extname(name).toLowerCase();
207
+ const known = {
208
+ ".png": "image/png",
209
+ ".jpg": "image/jpeg",
210
+ ".jpeg": "image/jpeg",
211
+ ".gif": "image/gif",
212
+ ".webp": "image/webp",
213
+ ".svg": "image/svg+xml",
214
+ ".txt": "text/plain",
215
+ ".md": "text/markdown",
216
+ ".json": "application/json",
217
+ ".pdf": "application/pdf",
218
+ ".zip": "application/zip",
219
+ ".gz": "application/gzip",
220
+ ".tar": "application/x-tar",
221
+ ".wav": "audio/wav",
222
+ ".mp3": "audio/mpeg",
223
+ ".mp4": "video/mp4",
224
+ };
225
+ return known[ext] ?? (kind === "image" ? "image/*" : "application/octet-stream");
226
+ }
227
+ export function sha256Hex(bytes) {
228
+ return createHash("sha256").update(bytes).digest("hex");
229
+ }
230
+ export function bytesToBase64(bytes) {
231
+ return Buffer.from(bytes).toString("base64");
232
+ }
233
+ export function base64ToBytes(value) {
234
+ return new Uint8Array(Buffer.from(value, "base64"));
235
+ }
236
+ export function readSavedTransferBytes(savedPath) {
237
+ return new Uint8Array(readFileSync(savedPath));
238
+ }
239
+ export function getSavedTransferSize(savedPath) {
240
+ return statSync(savedPath).size;
241
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export { MessageBus, type KeywordTrigger, type MessageBusOptions, type SendDataFn } from "./message-bus.js";
2
2
  export { PeerRegistry, type PeerRecord, type PeerRegistryEvents } from "./peer-registry.js";
3
- export { createEnvelope, createInstanceId, createMessageId, envelopeToWire, generateRoomName, isValidEnvelope, parseEnvelope, type AnnouncePayload, type MessageEnvelope, type MessageType, type PeerIdentity, type SkillUpdatePayload, } from "./protocol.js";
3
+ export { ensureAgentState, getInboxSummary, isInboxWorthy, listInboxMessages, listQueuedAgentActions, queueAgentAction, readAgentSession, readPeersSnapshot, removeQueuedAgentAction, storeInboxMessage, takeInboxMessages, writeAgentSession, writePeersSnapshot, type AgentSessionState, type InboxMessage, type InboxSummary, type QueuedAgentAction, type QueuedAgentActionInput, } from "./agent-state.js";
4
+ export { DEFAULT_TRANSFER_CHUNK_SIZE, appendIncomingTransferChunk, beginIncomingTransfer, bytesToBase64, completeIncomingTransfer, createFailedFileAckPayload, createFileAckPayload, prepareFileTransferFromPath, readSavedTransferBytes, readTransferManifest, sendFileFromPath, sendPreparedFileTransfer, sha256Hex, type CompletedTransferResult, type IncomingTransferManifest, type PreparedFileTransfer, } from "./file-transfer.js";
5
+ export { createEnvelope, createInstanceId, createMessageId, envelopeToWire, type FileAckPayload, type FileChunkPayload, type FileCompletePayload, type FileOfferPayload, type FileTransferKind, generateRoomName, isValidEnvelope, parseEnvelope, type AgentAsk, type AgentProfile, type AnnouncePayload, type MessageEnvelope, type MessageType, type PeerIdentity, type SharedFolderSummary, type SkillUpdatePayload, } from "./protocol.js";
6
+ export { inferTransferKind, listSharedFolderEntries, parseSharedFolderSpecs, resolveSharedFile, toSharedFolderSummaries, type ResolvedSharedFile, type SharedFolderConfig, type SharedFolderEntry, type SharedFolderListing, } from "./shared-folders.js";
4
7
  export { VDOBridge, type VDOBridgeOptions } from "./vdo-bridge.js";
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { MessageBus } from "./message-bus.js";
2
2
  export { PeerRegistry } from "./peer-registry.js";
3
+ export { ensureAgentState, getInboxSummary, isInboxWorthy, listInboxMessages, listQueuedAgentActions, queueAgentAction, readAgentSession, readPeersSnapshot, removeQueuedAgentAction, storeInboxMessage, takeInboxMessages, writeAgentSession, writePeersSnapshot, } from "./agent-state.js";
4
+ export { DEFAULT_TRANSFER_CHUNK_SIZE, appendIncomingTransferChunk, beginIncomingTransfer, bytesToBase64, completeIncomingTransfer, createFailedFileAckPayload, createFileAckPayload, prepareFileTransferFromPath, readSavedTransferBytes, readTransferManifest, sendFileFromPath, sendPreparedFileTransfer, sha256Hex, } from "./file-transfer.js";
3
5
  export { createEnvelope, createInstanceId, createMessageId, envelopeToWire, generateRoomName, isValidEnvelope, parseEnvelope, } from "./protocol.js";
6
+ export { inferTransferKind, listSharedFolderEntries, parseSharedFolderSpecs, resolveSharedFile, toSharedFolderSummaries, } from "./shared-folders.js";
4
7
  export { VDOBridge } from "./vdo-bridge.js";
@@ -40,6 +40,7 @@ export declare class MessageBus extends EventEmitter {
40
40
  constructor(identity: PeerIdentity, peers: PeerRegistry, options?: MessageBusOptions);
41
41
  /** Set the low-level send function (provided by VDOBridge after SDK connects). */
42
42
  setSendDataFn(fn: SendDataFn): void;
43
+ private emitBusError;
43
44
  subscribe(topic: string): void;
44
45
  unsubscribe(topic: string): void;
45
46
  isSubscribed(topic: string): boolean;
@@ -37,6 +37,13 @@ export class MessageBus extends EventEmitter {
37
37
  setSendDataFn(fn) {
38
38
  this.sendDataFn = fn;
39
39
  }
40
+ emitBusError(err) {
41
+ if (this.listenerCount("error") > 0) {
42
+ this.emit("error", err);
43
+ return;
44
+ }
45
+ console.error("[P2P] Message bus error:", err);
46
+ }
40
47
  // ── Subscriptions ────────────────────────────────────────────────────────
41
48
  subscribe(topic) {
42
49
  this.subscriptions.add(topic);
@@ -186,8 +193,8 @@ export class MessageBus extends EventEmitter {
186
193
  }
187
194
  }
188
195
  addToHistory(envelope) {
189
- // Don't store pings/pongs in history
190
- if (envelope.type === "ping" || envelope.type === "pong")
196
+ // Don't store heartbeat or file chunk traffic in history.
197
+ if (envelope.type === "ping" || envelope.type === "pong" || envelope.type === "file_chunk")
191
198
  return;
192
199
  this.history.push(envelope);
193
200
  while (this.history.length > this.historySize) {
@@ -219,7 +226,7 @@ export class MessageBus extends EventEmitter {
219
226
  trigger.handler(envelope);
220
227
  }
221
228
  catch (err) {
222
- this.emit("error", err);
229
+ this.emitBusError(err);
223
230
  }
224
231
  }
225
232
  }
@@ -8,7 +8,7 @@
8
8
  * Zero dependencies on stevesbot internals.
9
9
  */
10
10
  import { EventEmitter } from "node:events";
11
- import type { AnnouncePayload, PeerIdentity, SkillUpdatePayload } from "./protocol.js";
11
+ import type { AgentProfile, AnnouncePayload, PeerIdentity, SkillUpdatePayload } from "./protocol.js";
12
12
  export type PeerRecord = {
13
13
  streamId: string;
14
14
  uuid: string;
@@ -18,6 +18,7 @@ export type PeerRecord = {
18
18
  statusDetail: string;
19
19
  topics: string[];
20
20
  version: string;
21
+ agentProfile: AgentProfile | null;
21
22
  connectedAt: number;
22
23
  lastSeenAt: number;
23
24
  connected: boolean;
@@ -44,6 +44,7 @@ export class PeerRegistry extends EventEmitter {
44
44
  statusDetail: "",
45
45
  topics: [],
46
46
  version: "",
47
+ agentProfile: null,
47
48
  connectedAt: Date.now(),
48
49
  lastSeenAt: Date.now(),
49
50
  connected: true,
@@ -104,6 +105,7 @@ export class PeerRegistry extends EventEmitter {
104
105
  peer.statusDetail = announce.statusDetail ?? "";
105
106
  peer.topics = announce.topics ?? [];
106
107
  peer.version = announce.version ?? "";
108
+ peer.agentProfile = announce.agent ?? peer.agentProfile;
107
109
  peer.lastSeenAt = Date.now();
108
110
  this.emit("peer:update", peer, "announce");
109
111
  return peer;
@@ -116,6 +118,7 @@ export class PeerRegistry extends EventEmitter {
116
118
  peer.skills = update.skills ?? peer.skills;
117
119
  peer.status = update.status ?? peer.status;
118
120
  peer.statusDetail = update.statusDetail ?? peer.statusDetail;
121
+ peer.agentProfile = update.agent ?? peer.agentProfile;
119
122
  peer.lastSeenAt = Date.now();
120
123
  this.emit("peer:update", peer, "skill_update");
121
124
  return peer;
@@ -192,6 +195,7 @@ export class PeerRegistry extends EventEmitter {
192
195
  skills: p.skills,
193
196
  status: p.status,
194
197
  statusDetail: p.statusDetail,
198
+ agentProfile: p.agentProfile,
195
199
  connected: p.connected,
196
200
  connectedAt: p.connectedAt,
197
201
  lastSeenAt: p.lastSeenAt,
@@ -5,13 +5,67 @@
5
5
  * WebRTC data channels. This module has zero dependencies on stevesbot
6
6
  * internals — it can be extracted and reused by any bot.
7
7
  */
8
- export type MessageType = "chat" | "announce" | "skill_update" | "command" | "command_response" | "event" | "ping" | "pong" | "ack" | "history_replay" | "history_request";
8
+ export type MessageType = "chat" | "announce" | "skill_update" | "command" | "command_response" | "file_offer" | "file_chunk" | "file_complete" | "file_ack" | "event" | "ping" | "pong" | "ack" | "history_replay" | "history_request";
9
9
  export type PeerIdentity = {
10
10
  streamId: string;
11
11
  role: string;
12
12
  name: string;
13
13
  instanceId: string;
14
14
  };
15
+ export type AgentAsk = {
16
+ name: string;
17
+ description: string;
18
+ via?: "command" | "chat" | "event";
19
+ example?: string;
20
+ };
21
+ export type SharedFolderSummary = {
22
+ name: string;
23
+ description?: string;
24
+ };
25
+ export type AgentProfile = {
26
+ runtime?: string;
27
+ provider?: string;
28
+ model?: string;
29
+ summary?: string;
30
+ workspace?: string;
31
+ can?: string[];
32
+ asks?: AgentAsk[];
33
+ shares?: SharedFolderSummary[];
34
+ };
35
+ export type FileTransferKind = "file" | "image";
36
+ export type FileOfferPayload = {
37
+ transferId: string;
38
+ name: string;
39
+ mimeType: string;
40
+ kind: FileTransferKind;
41
+ size: number;
42
+ sha256: string;
43
+ chunkSize: number;
44
+ totalChunks: number;
45
+ };
46
+ export type FileChunkPayload = {
47
+ transferId: string;
48
+ index: number;
49
+ totalChunks: number;
50
+ data: string;
51
+ };
52
+ export type FileCompletePayload = {
53
+ transferId: string;
54
+ totalChunks: number;
55
+ size: number;
56
+ sha256: string;
57
+ };
58
+ export type FileAckPayload = {
59
+ transferId: string;
60
+ ok: boolean;
61
+ name?: string;
62
+ mimeType?: string;
63
+ kind?: FileTransferKind;
64
+ size?: number;
65
+ sha256?: string;
66
+ savedPath?: string;
67
+ error?: string;
68
+ };
15
69
  export type MessageEnvelope = {
16
70
  v: 1;
17
71
  id: string;
@@ -28,11 +82,13 @@ export type AnnouncePayload = {
28
82
  statusDetail?: string;
29
83
  version: string;
30
84
  topics: string[];
85
+ agent?: AgentProfile;
31
86
  };
32
87
  export type SkillUpdatePayload = {
33
88
  skills: string[];
34
89
  status: string;
35
90
  statusDetail?: string;
91
+ agent?: AgentProfile;
36
92
  };
37
93
  /** Generate a short unique message ID. */
38
94
  export declare function createMessageId(): string;
@@ -0,0 +1,31 @@
1
+ import type { FileTransferKind, SharedFolderSummary } from "./protocol.js";
2
+ export type SharedFolderConfig = {
3
+ name: string;
4
+ path: string;
5
+ description?: string;
6
+ };
7
+ export type SharedFolderEntry = {
8
+ name: string;
9
+ path: string;
10
+ type: "dir" | "file";
11
+ size: number | null;
12
+ };
13
+ export type SharedFolderListing = {
14
+ share: string;
15
+ path: string;
16
+ entries: SharedFolderEntry[];
17
+ truncated: boolean;
18
+ };
19
+ export type ResolvedSharedFile = {
20
+ share: string;
21
+ path: string;
22
+ filePath: string;
23
+ name: string;
24
+ kind: FileTransferKind;
25
+ size: number;
26
+ };
27
+ export declare function parseSharedFolderSpecs(rawValues: string[], cwd?: string): SharedFolderConfig[];
28
+ export declare function toSharedFolderSummaries(sharedFolders: SharedFolderConfig[]): SharedFolderSummary[];
29
+ export declare function listSharedFolderEntries(sharedFolders: SharedFolderConfig[], shareName: string, relativePath?: string, maxEntries?: number): SharedFolderListing;
30
+ export declare function resolveSharedFile(sharedFolders: SharedFolderConfig[], shareName: string, relativePath: string): ResolvedSharedFile;
31
+ export declare function inferTransferKind(filePath: string): FileTransferKind;
@@ -0,0 +1,137 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ export function parseSharedFolderSpecs(rawValues, cwd = process.cwd()) {
4
+ const shares = [];
5
+ const seen = new Set();
6
+ for (const raw of rawValues.map((value) => value.trim()).filter(Boolean)) {
7
+ const separatorIndex = raw.indexOf("=");
8
+ if (separatorIndex <= 0 || separatorIndex === raw.length - 1) {
9
+ throw new Error(`invalid share spec: ${raw}; use --share name=path`);
10
+ }
11
+ const name = raw.slice(0, separatorIndex).trim();
12
+ const folderPath = raw.slice(separatorIndex + 1).trim();
13
+ if (!/^[A-Za-z0-9._-]+$/.test(name)) {
14
+ throw new Error(`invalid share name: ${name}`);
15
+ }
16
+ const key = name.toLowerCase();
17
+ if (seen.has(key)) {
18
+ throw new Error(`duplicate share name: ${name}`);
19
+ }
20
+ const resolved = path.resolve(cwd, folderPath);
21
+ if (!existsSync(resolved)) {
22
+ throw new Error(`shared folder does not exist: ${resolved}`);
23
+ }
24
+ const stats = statSync(resolved);
25
+ if (!stats.isDirectory()) {
26
+ throw new Error(`shared folder is not a directory: ${resolved}`);
27
+ }
28
+ seen.add(key);
29
+ shares.push({
30
+ name,
31
+ path: resolved,
32
+ });
33
+ }
34
+ return shares;
35
+ }
36
+ export function toSharedFolderSummaries(sharedFolders) {
37
+ return sharedFolders.map((share) => {
38
+ const summary = { name: share.name };
39
+ if (share.description)
40
+ summary.description = share.description;
41
+ return summary;
42
+ });
43
+ }
44
+ export function listSharedFolderEntries(sharedFolders, shareName, relativePath = "", maxEntries = 200) {
45
+ const share = requireSharedFolder(sharedFolders, shareName);
46
+ const resolved = resolveSharedPath(share, relativePath);
47
+ const stats = statSync(resolved.filePath);
48
+ if (!stats.isDirectory()) {
49
+ throw new Error(`shared path is not a directory: ${resolved.path || "."}`);
50
+ }
51
+ const entries = readdirSync(resolved.filePath, { withFileTypes: true })
52
+ .map((entry) => {
53
+ const childPath = resolved.path ? `${resolved.path}/${entry.name}` : entry.name;
54
+ const absolutePath = path.join(resolved.filePath, entry.name);
55
+ if (entry.isDirectory()) {
56
+ return {
57
+ name: entry.name,
58
+ path: childPath,
59
+ type: "dir",
60
+ size: null,
61
+ };
62
+ }
63
+ return {
64
+ name: entry.name,
65
+ path: childPath,
66
+ type: "file",
67
+ size: statSync(absolutePath).size,
68
+ };
69
+ })
70
+ .sort((a, b) => Number(a.type === "file") - Number(b.type === "file") || a.name.localeCompare(b.name));
71
+ return {
72
+ share: share.name,
73
+ path: resolved.path,
74
+ entries: entries.slice(0, Math.max(1, maxEntries)),
75
+ truncated: entries.length > maxEntries,
76
+ };
77
+ }
78
+ export function resolveSharedFile(sharedFolders, shareName, relativePath) {
79
+ const share = requireSharedFolder(sharedFolders, shareName);
80
+ const resolved = resolveSharedPath(share, relativePath);
81
+ const stats = statSync(resolved.filePath);
82
+ if (!stats.isFile()) {
83
+ throw new Error(`shared path is not a file: ${resolved.path || "."}`);
84
+ }
85
+ return {
86
+ share: share.name,
87
+ path: resolved.path,
88
+ filePath: resolved.filePath,
89
+ name: path.basename(resolved.filePath),
90
+ kind: inferTransferKind(resolved.filePath),
91
+ size: stats.size,
92
+ };
93
+ }
94
+ export function inferTransferKind(filePath) {
95
+ const ext = path.extname(filePath).toLowerCase();
96
+ if ([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp"].includes(ext)) {
97
+ return "image";
98
+ }
99
+ return "file";
100
+ }
101
+ function requireSharedFolder(sharedFolders, shareName) {
102
+ const share = sharedFolders.find((item) => item.name.toLowerCase() === shareName.trim().toLowerCase());
103
+ if (!share) {
104
+ throw new Error(`unknown share: ${shareName}`);
105
+ }
106
+ return share;
107
+ }
108
+ function resolveSharedPath(share, relativePath) {
109
+ const normalized = normalizeRelativeSharePath(relativePath);
110
+ const target = path.resolve(share.path, normalized || ".");
111
+ if (target !== share.path && !target.startsWith(`${share.path}${path.sep}`)) {
112
+ throw new Error("shared path escapes the declared folder");
113
+ }
114
+ if (!existsSync(target)) {
115
+ throw new Error(`shared path does not exist: ${normalized || "."}`);
116
+ }
117
+ return {
118
+ filePath: target,
119
+ path: normalized,
120
+ };
121
+ }
122
+ function normalizeRelativeSharePath(relativePath) {
123
+ const raw = relativePath.replace(/\\/g, "/").trim();
124
+ if (!raw)
125
+ return "";
126
+ if (raw.startsWith("/")) {
127
+ throw new Error("shared path must be relative");
128
+ }
129
+ const normalized = path.posix.normalize(raw);
130
+ if (normalized === ".." || normalized.startsWith("../")) {
131
+ throw new Error("shared path escapes the declared folder");
132
+ }
133
+ if (normalized.includes("/../")) {
134
+ throw new Error("shared path escapes the declared folder");
135
+ }
136
+ return normalized === "." ? "" : normalized;
137
+ }