@agentsquared/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,397 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ import { createLibp2p } from 'libp2p'
5
+ import { tcp } from '@libp2p/tcp'
6
+ import { noise } from '@libp2p/noise'
7
+ import { yamux } from '@chainsafe/libp2p-yamux'
8
+ import { identify } from '@libp2p/identify'
9
+ import { autoNAT } from '@libp2p/autonat'
10
+ import { dcutr } from '@libp2p/dcutr'
11
+ import { circuitRelayTransport } from '@libp2p/circuit-relay-v2'
12
+ import { generateKeyPair, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys'
13
+ import { peerIdFromString } from '@libp2p/peer-id'
14
+ import { multiaddr } from '@multiformats/multiaddr'
15
+
16
+ export const DEFAULT_LISTEN_ADDRS = ['/ip6/::/tcp/0', '/ip4/0.0.0.0/tcp/0']
17
+ const DEFAULT_DIRECT_UPGRADE_TIMEOUT_MS = 12000
18
+ const DEFAULT_TRANSPORT_READY_TIMEOUT_MS = 20000
19
+ const streamReaders = new WeakMap()
20
+
21
+ function unique(values = []) {
22
+ return [...new Set(values.map((value) => `${value}`.trim()).filter(Boolean))]
23
+ }
24
+
25
+ function addrPriority(value) {
26
+ const text = `${value}`.trim().toLowerCase()
27
+ if (text.includes('/dns6/')) return 0
28
+ if (text.includes('/ip6/')) return 1
29
+ if (text.includes('/dnsaddr/')) return 2
30
+ if (text.includes('/dns4/')) return 3
31
+ if (text.includes('/ip4/')) return 4
32
+ return 5
33
+ }
34
+
35
+ function prioritizeAddrs(values = []) {
36
+ return unique(values)
37
+ .map((value, index) => ({ value, index, priority: addrPriority(value) }))
38
+ .sort((left, right) => {
39
+ if (left.priority !== right.priority) {
40
+ return left.priority - right.priority
41
+ }
42
+ return left.index - right.index
43
+ })
44
+ .map((item) => item.value)
45
+ }
46
+
47
+ function ensureParentDir(filePath) {
48
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
49
+ }
50
+
51
+ async function loadOrCreatePeerPrivateKey(peerKeyFile) {
52
+ if (!peerKeyFile) {
53
+ throw new Error('peerKeyFile is required for gateway node identity')
54
+ }
55
+ const cleaned = path.resolve(peerKeyFile)
56
+ if (fs.existsSync(cleaned)) {
57
+ return privateKeyFromProtobuf(fs.readFileSync(cleaned))
58
+ }
59
+ ensureParentDir(cleaned)
60
+ const privateKey = await generateKeyPair('Ed25519')
61
+ fs.writeFileSync(cleaned, Buffer.from(privateKeyToProtobuf(privateKey)), { mode: 0o600 })
62
+ fs.chmodSync(cleaned, 0o600)
63
+ return privateKey
64
+ }
65
+
66
+ export function buildRelayListenAddrs(relayMultiaddrs = []) {
67
+ return prioritizeAddrs(relayMultiaddrs.map((value) => `${value}`.trim()).filter(Boolean).map((value) => `${value}/p2p-circuit`))
68
+ }
69
+
70
+ function parseFailedListenAddrs(error) {
71
+ const message = `${error?.message ?? ''}`
72
+ if (!message.includes('Some configured addresses failed to be listened on')) {
73
+ return []
74
+ }
75
+ return message
76
+ .split('\n')
77
+ .map((line) => line.trim())
78
+ .filter((line) => line.startsWith('/'))
79
+ .map((line) => line.split(':', 1)[0].trim())
80
+ .filter(Boolean)
81
+ }
82
+
83
+ export async function createNode({
84
+ listenAddrs = DEFAULT_LISTEN_ADDRS,
85
+ relayListenAddrs = [],
86
+ peerKeyFile
87
+ } = {}) {
88
+ const privateKey = await loadOrCreatePeerPrivateKey(peerKeyFile)
89
+ let activeListenAddrs = unique([...listenAddrs, ...relayListenAddrs])
90
+ let lastError = null
91
+
92
+ for (let attempt = 0; attempt < 3; attempt += 1) {
93
+ try {
94
+ return await createLibp2p({
95
+ privateKey,
96
+ addresses: {
97
+ listen: activeListenAddrs
98
+ },
99
+ transports: [
100
+ tcp(),
101
+ circuitRelayTransport()
102
+ ],
103
+ connectionEncrypters: [noise()],
104
+ streamMuxers: [yamux()],
105
+ services: {
106
+ identify: identify(),
107
+ autoNAT: autoNAT(),
108
+ dcutr: dcutr()
109
+ },
110
+ start: true
111
+ })
112
+ } catch (error) {
113
+ lastError = error
114
+ const failedListenAddrs = parseFailedListenAddrs(error)
115
+ if (failedListenAddrs.length === 0) {
116
+ throw error
117
+ }
118
+ const failedSet = new Set(failedListenAddrs)
119
+ const nextListenAddrs = activeListenAddrs.filter((value) => !failedSet.has(value))
120
+ const removedAny = nextListenAddrs.length < activeListenAddrs.length
121
+ if (!removedAny || nextListenAddrs.length === 0) {
122
+ throw error
123
+ }
124
+ activeListenAddrs = prioritizeAddrs(nextListenAddrs)
125
+ }
126
+ }
127
+
128
+ throw lastError ?? new Error('gateway node failed to start')
129
+ }
130
+
131
+ export function advertisedAddrs(node) {
132
+ return prioritizeAddrs(node.getMultiaddrs().map((addr) => addr.toString()))
133
+ }
134
+
135
+ export function relayReservationAddrs(node) {
136
+ return prioritizeAddrs(advertisedAddrs(node).filter((addr) => addr.includes('/p2p-circuit')))
137
+ }
138
+
139
+ export function directListenAddrs(node) {
140
+ return prioritizeAddrs(advertisedAddrs(node).filter((addr) => !addr.includes('/p2p-circuit')))
141
+ }
142
+
143
+ export function requireListeningTransport(node, binding) {
144
+ const peerId = node?.peerId?.toString?.() ?? ''
145
+ const listenAddrs = directListenAddrs(node)
146
+ const relayAddrs = relayReservationAddrs(node)
147
+ const supportedBindings = binding?.binding ? [binding.binding] : []
148
+ const streamProtocol = `${binding?.streamProtocol ?? ''}`.trim()
149
+ const a2aProtocolVersion = `${binding?.a2aProtocolVersion ?? ''}`.trim()
150
+
151
+ if (!peerId) {
152
+ throw new Error('local gateway is not ready: peerId is unavailable')
153
+ }
154
+ if (listenAddrs.length === 0 && relayAddrs.length === 0) {
155
+ throw new Error('local gateway is not ready: no direct or relay-backed addresses were published')
156
+ }
157
+ if (!streamProtocol) {
158
+ throw new Error('local gateway is not ready: streamProtocol is unavailable')
159
+ }
160
+ if (supportedBindings.length === 0) {
161
+ throw new Error('local gateway is not ready: supportedBindings are unavailable')
162
+ }
163
+
164
+ return {
165
+ peerId,
166
+ dialAddrs: prioritizeAddrs(relayAddrs.length > 0 ? relayAddrs : listenAddrs),
167
+ listenAddrs,
168
+ relayAddrs,
169
+ supportedBindings,
170
+ streamProtocol,
171
+ a2aProtocolVersion
172
+ }
173
+ }
174
+
175
+ export async function waitForPublishedTransport(node, binding, {
176
+ requireRelayReservation = false,
177
+ timeoutMs = DEFAULT_TRANSPORT_READY_TIMEOUT_MS
178
+ } = {}) {
179
+ const startedAt = Date.now()
180
+ let lastError = null
181
+
182
+ while (Date.now() - startedAt < timeoutMs) {
183
+ try {
184
+ const transport = requireListeningTransport(node, binding)
185
+ if (requireRelayReservation && transport.relayAddrs.length === 0) {
186
+ throw new Error('waiting for relay reservation-backed transport')
187
+ }
188
+ return transport
189
+ } catch (error) {
190
+ lastError = error
191
+ await new Promise((resolve) => setTimeout(resolve, 250))
192
+ }
193
+ }
194
+
195
+ throw lastError ?? new Error('gateway transport did not become ready before timeout')
196
+ }
197
+
198
+ function isDirectConnection(connection) {
199
+ const remoteAddr = connection?.remoteAddr?.toString?.() ?? ''
200
+ return !remoteAddr.includes('/p2p-circuit') && connection?.limits == null
201
+ }
202
+
203
+ function newStreamOptions(connection) {
204
+ if (connection?.limits != null) {
205
+ return { runOnLimitedConnection: true }
206
+ }
207
+ const remoteAddr = connection?.remoteAddr?.toString?.() ?? ''
208
+ if (remoteAddr.includes('/p2p-circuit')) {
209
+ return { runOnLimitedConnection: true }
210
+ }
211
+ return undefined
212
+ }
213
+
214
+ export function currentPeerConnection(node, peerId) {
215
+ if (!peerId?.trim?.()) return null
216
+ const remotePeer = peerIdFromString(peerId)
217
+ const connections = node.getConnections(remotePeer)
218
+ return connections.find(isDirectConnection) ?? connections[0] ?? null
219
+ }
220
+
221
+ function chooseDialAddrs(transport) {
222
+ return prioritizeAddrs(
223
+ transport?.dialAddrs?.length
224
+ ? transport.dialAddrs
225
+ : transport?.relayAddrs?.length
226
+ ? transport.relayAddrs
227
+ : transport?.listenAddrs ?? []
228
+ )
229
+ }
230
+
231
+ async function waitForDirectConnection(node, peerId, timeoutMs = DEFAULT_DIRECT_UPGRADE_TIMEOUT_MS) {
232
+ const remotePeer = peerIdFromString(peerId)
233
+ const startedAt = Date.now()
234
+ let relayedConnection = null
235
+
236
+ while (Date.now() - startedAt < timeoutMs) {
237
+ const connections = node.getConnections(remotePeer)
238
+ const directConnection = connections.find(isDirectConnection)
239
+ if (directConnection) {
240
+ return directConnection
241
+ }
242
+ relayedConnection = connections.find((connection) => connection?.remoteAddr?.toString?.().includes('/p2p-circuit'))
243
+ await new Promise((resolve) => setTimeout(resolve, 300))
244
+ }
245
+
246
+ if (relayedConnection) {
247
+ try {
248
+ await relayedConnection.close()
249
+ } catch {
250
+ // best-effort cleanup only
251
+ }
252
+ }
253
+ throw new Error(`direct P2P upgrade did not complete for ${peerId}`)
254
+ }
255
+
256
+ export async function dialProtocol(node, transport, {
257
+ requireDirect = false,
258
+ timeoutMs = DEFAULT_DIRECT_UPGRADE_TIMEOUT_MS
259
+ } = {}) {
260
+ if (!transport?.streamProtocol) {
261
+ throw new Error('target transport is missing streamProtocol')
262
+ }
263
+ if (!transport?.peerId?.trim()) {
264
+ throw new Error('target transport is missing peerId')
265
+ }
266
+
267
+ const dialAddrs = chooseDialAddrs(transport)
268
+ if (dialAddrs.length === 0) {
269
+ throw new Error('target transport is missing dialAddrs')
270
+ }
271
+
272
+ let lastError = null
273
+ for (const value of dialAddrs) {
274
+ try {
275
+ await node.dial(multiaddr(value))
276
+ break
277
+ } catch (error) {
278
+ lastError = error
279
+ }
280
+ }
281
+
282
+ if (lastError && node.getConnections(peerIdFromString(transport.peerId)).length === 0) {
283
+ throw lastError
284
+ }
285
+
286
+ const connections = node.getConnections(peerIdFromString(transport.peerId))
287
+ const connection = requireDirect
288
+ ? await waitForDirectConnection(node, transport.peerId, timeoutMs)
289
+ : connections.find(isDirectConnection) ?? connections[0]
290
+
291
+ if (!connection) {
292
+ throw new Error(`no connection was available for ${transport.peerId}`)
293
+ }
294
+
295
+ return connection.newStream([transport.streamProtocol], newStreamOptions(connection))
296
+ }
297
+
298
+ export async function openStreamOnExistingConnection(node, transport) {
299
+ if (!transport?.streamProtocol) {
300
+ throw new Error('target transport is missing streamProtocol')
301
+ }
302
+ if (!transport?.peerId?.trim()) {
303
+ throw new Error('target transport is missing peerId')
304
+ }
305
+ const connection = currentPeerConnection(node, transport.peerId)
306
+ if (!connection) {
307
+ throw new Error(`no existing peer connection is available for ${transport.peerId}`)
308
+ }
309
+ return connection.newStream([transport.streamProtocol], newStreamOptions(connection))
310
+ }
311
+
312
+ export async function writeLine(stream, line) {
313
+ const payload = Buffer.from(`${line}\n`, 'utf8')
314
+ const accepted = stream.send(payload)
315
+ if (accepted) return
316
+ await new Promise((resolve, reject) => {
317
+ const cleanup = () => {
318
+ stream.removeEventListener('drain', onDrain)
319
+ stream.removeEventListener('close', onClose)
320
+ }
321
+ const onDrain = () => {
322
+ cleanup()
323
+ resolve()
324
+ }
325
+ const onClose = (event) => {
326
+ cleanup()
327
+ reject(event?.error ?? new Error('stream closed before drain'))
328
+ }
329
+ stream.addEventListener('drain', onDrain, { once: true })
330
+ stream.addEventListener('close', onClose, { once: true })
331
+ })
332
+ }
333
+
334
+ export async function readSingleLine(stream) {
335
+ const chunks = []
336
+ for await (const chunk of stream) {
337
+ chunks.push(Buffer.from(chunk.subarray ? chunk.subarray(0) : chunk.slice()))
338
+ const buffer = Buffer.concat(chunks)
339
+ const newline = buffer.indexOf(0x0a)
340
+ if (newline >= 0) {
341
+ return buffer.subarray(0, newline).toString('utf8')
342
+ }
343
+ }
344
+ return Buffer.concat(chunks).toString('utf8')
345
+ }
346
+
347
+ function isUnexpectedEndJson(error) {
348
+ return error instanceof SyntaxError && /Unexpected end of JSON input/i.test(`${error.message ?? ''}`)
349
+ }
350
+
351
+ export async function readJsonMessage(stream) {
352
+ if (!stream) {
353
+ throw new Error('stream is required')
354
+ }
355
+ let reader = streamReaders.get(stream)
356
+ if (!reader) {
357
+ reader = {
358
+ iterator: stream[Symbol.asyncIterator](),
359
+ buffer: ''
360
+ }
361
+ streamReaders.set(stream, reader)
362
+ }
363
+
364
+ while (true) {
365
+ while (true) {
366
+ const newlineIndex = reader.buffer.indexOf('\n')
367
+ if (newlineIndex < 0) {
368
+ break
369
+ }
370
+ const candidate = reader.buffer.slice(0, newlineIndex).trim()
371
+ reader.buffer = reader.buffer.slice(newlineIndex + 1)
372
+ if (!candidate) {
373
+ continue
374
+ }
375
+ try {
376
+ return JSON.parse(candidate)
377
+ } catch (error) {
378
+ if (candidate.startsWith('{') || candidate.startsWith('[')) {
379
+ reader.buffer = `${candidate}\n${reader.buffer}`
380
+ break
381
+ }
382
+ throw error
383
+ }
384
+ }
385
+
386
+ const next = await reader.iterator.next()
387
+ if (next.done) {
388
+ const finalText = reader.buffer.trim()
389
+ streamReaders.delete(stream)
390
+ if (!finalText) {
391
+ throw new Error('empty JSON message')
392
+ }
393
+ return JSON.parse(finalText)
394
+ }
395
+ reader.buffer += Buffer.from(next.value?.subarray ? next.value.subarray(0) : next.value.slice()).toString('utf8')
396
+ }
397
+ }