@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.
package/a2_cli.mjs ADDED
@@ -0,0 +1,1576 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { spawn } from 'node:child_process'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import { parseArgs, randomRequestId, requireArg } from './lib/shared/primitives.mjs'
9
+ import { gatewayConnect, gatewayHealth, gatewayInboxIndex } from './lib/gateway/api.mjs'
10
+ import { resolveGatewayBase, defaultGatewayStateFile, readGatewayState, currentRuntimeRevision } from './lib/gateway/state.mjs'
11
+ import { getFriendDirectory } from './lib/transport/relay_http.mjs'
12
+ import { generateRuntimeKeyBundle, writeRuntimeKeyBundle } from './lib/runtime/keys.mjs'
13
+ import { runGateway } from './lib/gateway/server.mjs'
14
+ import { createHostRuntimeAdapter, detectHostRuntimeEnvironment } from './adapters/index.mjs'
15
+ import { inspectOpenClawLocalSkills, resolveOpenClawOutboundSkillHint, summarizeOpenClawConversation } from './adapters/openclaw/adapter.mjs'
16
+ import { resolveOpenClawAgentSelection } from './adapters/openclaw/detect.mjs'
17
+ import {
18
+ defaultGatewayLogFile,
19
+ defaultInboxDir,
20
+ defaultOpenClawStateDir,
21
+ defaultOnboardingSummaryFile,
22
+ defaultReceiptFile,
23
+ defaultRuntimeKeyFile,
24
+ resolveAgentSquaredDir,
25
+ resolveUserPath
26
+ } from './lib/shared/paths.mjs'
27
+ import { buildSenderBaseReport, buildSenderFailureReport, buildSkillOutboundText, inferOwnerFacingLanguage, peerResponseText, renderOwnerFacingReport } from './lib/conversation/templates.mjs'
28
+ import { scrubOutboundText } from './lib/runtime/safety.mjs'
29
+ import { buildStandardRuntimeOwnerLines, buildStandardRuntimeReport } from './lib/runtime/report.mjs'
30
+ import { chooseInboundSkill, resolveMailboxKey } from './lib/routing/agent_router.mjs'
31
+ import { createLocalRuntimeExecutor } from './lib/runtime/executor.mjs'
32
+ import { createLiveConversationStore } from './lib/conversation/store.mjs'
33
+ import { normalizeConversationControl, parseSkillDocumentPolicy, resolveSkillMaxTurns, shouldContinueConversation } from './lib/conversation/policy.mjs'
34
+ import {
35
+ assertNoExistingLocalActivation,
36
+ buildGatewayArgs,
37
+ discoverLocalAgentProfiles,
38
+ ensureGatewayForUse,
39
+ inspectExistingGateway,
40
+ resolveAgentContext,
41
+ resolvedHostRuntimeFromHealth,
42
+ signedRelayContext,
43
+ toOwnerFacingText,
44
+ waitForGatewayReady
45
+ } from './lib/gateway/lifecycle.mjs'
46
+
47
+ const __filename = fileURLToPath(import.meta.url)
48
+ const __dirname = path.dirname(__filename)
49
+ const ROOT = __dirname
50
+
51
+ function clean(value) {
52
+ return `${value ?? ''}`.trim()
53
+ }
54
+
55
+ function excerpt(text, maxLength = 180) {
56
+ const compact = clean(text).replace(/\s+/g, ' ').trim()
57
+ if (!compact) {
58
+ return ''
59
+ }
60
+ return compact.length > maxLength ? `${compact.slice(0, maxLength - 3)}...` : compact
61
+ }
62
+
63
+ function parseJwtPayloadUnverified(token) {
64
+ const serialized = clean(token)
65
+ if (!serialized) {
66
+ return null
67
+ }
68
+ const parts = serialized.split('.')
69
+ if (parts.length < 2) {
70
+ return null
71
+ }
72
+ try {
73
+ const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/')
74
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=')
75
+ return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'))
76
+ } catch {
77
+ return null
78
+ }
79
+ }
80
+
81
+ function printJson(payload) {
82
+ console.log(JSON.stringify(payload, null, 2))
83
+ }
84
+
85
+ function toOwnerFacingLines(text = '') {
86
+ return clean(text)
87
+ .split('\n')
88
+ .map((line) => line.trimEnd())
89
+ .filter((line) => line.length > 0)
90
+ }
91
+
92
+ function buildOwnerReportDeliveredText(language = 'en') {
93
+ if (`${language}`.toLowerCase().startsWith('zh')) {
94
+ return '🅰️✌️ 最终的 AgentSquared 主人报告已经通过当前主人频道发送,请不要再追加任何形式的人类回复。'
95
+ }
96
+ return '🅰️✌️ The final owner-facing AgentSquared report has already been delivered through the current owner channel. Do not send any additional owner-facing reply.'
97
+ }
98
+
99
+ async function pushCliOwnerReport({
100
+ agentId,
101
+ keyFile,
102
+ args,
103
+ targetAgentId,
104
+ selectedSkill,
105
+ ownerReport,
106
+ deliveryId = ''
107
+ } = {}) {
108
+ try {
109
+ const hostContext = await resolveCliOpenClawHostContext({
110
+ agentId,
111
+ keyFile,
112
+ args,
113
+ purpose: 'AgentSquared owner report delivery'
114
+ })
115
+ const hostAdapter = createHostRuntimeAdapter({
116
+ hostRuntime: 'openclaw',
117
+ localAgentId: agentId,
118
+ openclaw: {
119
+ stateDir: hostContext.openclawStateDir,
120
+ openclawAgent: hostContext.resolvedOpenClawAgent,
121
+ command: hostContext.openclawCommand,
122
+ cwd: hostContext.openclawCwd,
123
+ configPath: hostContext.openclawConfigPath,
124
+ sessionPrefix: hostContext.openclawSessionPrefix,
125
+ timeoutMs: 30000,
126
+ gatewayUrl: hostContext.openclawGatewayUrl,
127
+ gatewayToken: hostContext.openclawGatewayToken,
128
+ gatewayPassword: hostContext.openclawGatewayPassword
129
+ }
130
+ })
131
+ if (!hostAdapter?.pushOwnerReport) {
132
+ return { delivered: false, attempted: false, mode: 'openclaw', reason: 'host-adapter-missing-push-owner-report' }
133
+ }
134
+ return await hostAdapter.pushOwnerReport({
135
+ item: {
136
+ inboundId: clean(deliveryId) || randomRequestId('sender-owner-report'),
137
+ remoteAgentId: targetAgentId
138
+ },
139
+ selectedSkill,
140
+ ownerReport
141
+ })
142
+ } catch (error) {
143
+ return {
144
+ delivered: false,
145
+ attempted: true,
146
+ mode: 'openclaw',
147
+ reason: clean(error?.message) || 'owner-report-delivery-failed'
148
+ }
149
+ }
150
+ }
151
+
152
+ function unique(values = []) {
153
+ return Array.from(new Set(values.filter(Boolean)))
154
+ }
155
+
156
+ function walkLocalFiles(dirPath, out, depth = 0, maxDepth = 4) {
157
+ if (!dirPath || !fs.existsSync(dirPath) || depth > maxDepth) {
158
+ return
159
+ }
160
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
161
+ const entryPath = path.join(dirPath, entry.name)
162
+ if (entry.isDirectory()) {
163
+ if (entry.name === 'node_modules' || entry.name === '.git') {
164
+ continue
165
+ }
166
+ walkLocalFiles(entryPath, out, depth + 1, maxDepth)
167
+ continue
168
+ }
169
+ if (entry.isFile()) {
170
+ out.push(entryPath)
171
+ }
172
+ }
173
+ }
174
+
175
+ function loadSharedSkillFile(skillFile) {
176
+ const resolved = resolveUserPath(skillFile)
177
+ const text = fs.readFileSync(resolved, 'utf8')
178
+ const policy = parseSkillDocumentPolicy(text, {
179
+ fallbackName: path.basename(path.dirname(resolved)) || path.basename(resolved, path.extname(resolved))
180
+ })
181
+ return {
182
+ path: resolved,
183
+ name: policy.name,
184
+ maxTurns: policy.maxTurns,
185
+ document: clean(text).slice(0, 16000)
186
+ }
187
+ }
188
+
189
+ function extractPeerResponseMetadata(response = null) {
190
+ const target = response?.result && typeof response.result === 'object'
191
+ ? response.result
192
+ : response
193
+ return target?.metadata && typeof target.metadata === 'object'
194
+ ? target.metadata
195
+ : {}
196
+ }
197
+
198
+ function resolveConversationPolicy(skillName = '', sharedSkill = null) {
199
+ return {
200
+ skillName: clean(skillName) || 'friend-im',
201
+ maxTurns: resolveSkillMaxTurns(skillName, sharedSkill)
202
+ }
203
+ }
204
+
205
+ async function resolveCliOpenClawHostContext({
206
+ agentId,
207
+ keyFile,
208
+ args,
209
+ purpose = 'AgentSquared local runtime execution'
210
+ }) {
211
+ const preferredHostRuntime = clean(args['host-runtime']) || 'auto'
212
+ const openclawCommand = clean(args['openclaw-command']) || 'openclaw'
213
+ const openclawCwd = clean(args['openclaw-cwd'])
214
+ const openclawConfigPath = clean(args['openclaw-config-path'] || process.env.OPENCLAW_CONFIG_PATH)
215
+ const openclawGatewayUrl = clean(args['openclaw-gateway-url'])
216
+ const openclawGatewayToken = clean(args['openclaw-gateway-token'])
217
+ const openclawGatewayPassword = clean(args['openclaw-gateway-password'])
218
+ const openclawSessionPrefix = clean(args['openclaw-session-prefix']) || 'agentsquared:'
219
+ const detectedHostRuntime = await detectHostRuntimeEnvironment({
220
+ preferred: preferredHostRuntime,
221
+ openclaw: {
222
+ command: openclawCommand,
223
+ cwd: openclawCwd,
224
+ configPath: openclawConfigPath,
225
+ gatewayUrl: openclawGatewayUrl,
226
+ gatewayToken: openclawGatewayToken,
227
+ gatewayPassword: openclawGatewayPassword
228
+ }
229
+ })
230
+ const resolvedHostRuntime = detectedHostRuntime.resolved || 'none'
231
+ if (resolvedHostRuntime !== 'openclaw') {
232
+ const detected = detectedHostRuntime.resolved || detectedHostRuntime.id || 'none'
233
+ const reason = clean(detectedHostRuntime.reason)
234
+ throw new Error(
235
+ `${clean(purpose) || 'AgentSquared local runtime execution'} currently supports only the OpenClaw host runtime. Detected host runtime: ${detected}.${reason ? ` Detection reason: ${reason}.` : ''}`
236
+ )
237
+ }
238
+ const detectedOpenClawAgent = clean(resolveOpenClawAgentSelection(detectedHostRuntime).defaultAgentId)
239
+ const resolvedOpenClawAgent = clean(args['openclaw-agent']) || detectedOpenClawAgent
240
+ if (!resolvedOpenClawAgent) {
241
+ throw new Error(`OpenClaw was detected for ${clean(purpose).toLowerCase() || 'local runtime execution'}, but no OpenClaw agent id could be resolved.`)
242
+ }
243
+ return {
244
+ detectedHostRuntime,
245
+ resolvedOpenClawAgent,
246
+ openclawCommand,
247
+ openclawCwd,
248
+ openclawConfigPath,
249
+ openclawGatewayUrl,
250
+ openclawGatewayToken,
251
+ openclawGatewayPassword,
252
+ openclawSessionPrefix,
253
+ openclawStateDir: defaultOpenClawStateDir(keyFile, agentId),
254
+ agentId
255
+ }
256
+ }
257
+
258
+ async function createCliLocalRuntimeExecutor({
259
+ agentId,
260
+ keyFile,
261
+ args
262
+ }) {
263
+ const hostContext = await resolveCliOpenClawHostContext({
264
+ agentId,
265
+ keyFile,
266
+ args,
267
+ purpose: 'local multi-turn execution'
268
+ })
269
+ return createLocalRuntimeExecutor({
270
+ agentId,
271
+ mode: 'host',
272
+ hostRuntime: 'openclaw',
273
+ conversationStore: createLiveConversationStore(),
274
+ openclawStateDir: hostContext.openclawStateDir,
275
+ openclawCommand: hostContext.openclawCommand,
276
+ openclawCwd: hostContext.openclawCwd,
277
+ openclawConfigPath: hostContext.openclawConfigPath,
278
+ openclawAgent: hostContext.resolvedOpenClawAgent,
279
+ openclawSessionPrefix: hostContext.openclawSessionPrefix,
280
+ openclawTimeoutMs: 180000,
281
+ openclawGatewayUrl: hostContext.openclawGatewayUrl,
282
+ openclawGatewayToken: hostContext.openclawGatewayToken,
283
+ openclawGatewayPassword: hostContext.openclawGatewayPassword
284
+ })
285
+ }
286
+
287
+ async function resolveOutboundSkillHint({
288
+ agentId,
289
+ keyFile,
290
+ args,
291
+ targetAgentId,
292
+ text,
293
+ explicitSkillName = '',
294
+ sharedSkill = null
295
+ }) {
296
+ const explicit = clean(explicitSkillName)
297
+ if (explicit) {
298
+ return {
299
+ skillHint: explicit,
300
+ source: 'explicit',
301
+ reason: 'explicit-skill-arg'
302
+ }
303
+ }
304
+ const sharedSkillName = clean(sharedSkill?.name)
305
+ if (sharedSkillName) {
306
+ return {
307
+ skillHint: sharedSkillName,
308
+ source: 'shared-skill',
309
+ reason: 'shared-skill-file'
310
+ }
311
+ }
312
+ try {
313
+ const hostContext = await resolveCliOpenClawHostContext({
314
+ agentId,
315
+ keyFile,
316
+ args,
317
+ purpose: 'outbound skill selection'
318
+ })
319
+ const decision = await resolveOpenClawOutboundSkillHint({
320
+ localAgentId: agentId,
321
+ targetAgentId,
322
+ ownerText: text,
323
+ openclawAgent: hostContext.resolvedOpenClawAgent,
324
+ command: hostContext.openclawCommand,
325
+ cwd: hostContext.openclawCwd,
326
+ configPath: hostContext.openclawConfigPath,
327
+ stateDir: hostContext.openclawStateDir,
328
+ gatewayUrl: hostContext.openclawGatewayUrl,
329
+ gatewayToken: hostContext.openclawGatewayToken,
330
+ gatewayPassword: hostContext.openclawGatewayPassword,
331
+ availableSkills: ['friend-im', 'agent-mutual-learning']
332
+ })
333
+ return {
334
+ skillHint: clean(decision.skillHint) || 'friend-im',
335
+ source: 'agent-decision',
336
+ reason: clean(decision.reason) || 'agent-selected-skill'
337
+ }
338
+ } catch (error) {
339
+ return {
340
+ skillHint: 'friend-im',
341
+ source: 'fallback',
342
+ reason: clean(error?.message) || 'agent-decision-failed'
343
+ }
344
+ }
345
+ }
346
+
347
+ async function executeLocalConversationTurn({
348
+ localRuntimeExecutor,
349
+ localAgentId,
350
+ targetAgentId,
351
+ peerSessionId,
352
+ conversationKey,
353
+ skillHint,
354
+ sharedSkill,
355
+ inboundText,
356
+ originalOwnerText = '',
357
+ localSkillInventory = '',
358
+ turnIndex,
359
+ remoteControl = null
360
+ }) {
361
+ const normalizedRemoteControl = normalizeConversationControl(remoteControl ?? {}, {
362
+ defaultTurnIndex: Math.max(1, Number.parseInt(`${turnIndex ?? 1}`, 10) - 1),
363
+ defaultDecision: 'done',
364
+ defaultStopReason: '',
365
+ defaultFinalize: false
366
+ })
367
+ const item = {
368
+ inboundId: `local-turn-${Date.now()}-${Math.random().toString(16).slice(2)}`,
369
+ remoteAgentId: targetAgentId,
370
+ peerSessionId,
371
+ suggestedSkill: skillHint,
372
+ defaultSkill: skillHint || 'friend-im',
373
+ request: {
374
+ id: `local-turn-${turnIndex}`,
375
+ method: 'message/send',
376
+ params: {
377
+ message: {
378
+ kind: 'message',
379
+ role: 'agent',
380
+ parts: [{ kind: 'text', text: clean(inboundText) }]
381
+ },
382
+ metadata: {
383
+ ...(sharedSkill ? { sharedSkill } : {}),
384
+ from: targetAgentId,
385
+ to: localAgentId,
386
+ originalOwnerText: clean(originalOwnerText) || clean(inboundText),
387
+ ...(clean(localSkillInventory) ? { localSkillInventory: clean(localSkillInventory) } : {}),
388
+ conversationKey: clean(conversationKey),
389
+ turnIndex,
390
+ decision: normalizedRemoteControl.decision,
391
+ stopReason: normalizedRemoteControl.stopReason,
392
+ finalize: normalizedRemoteControl.finalize
393
+ }
394
+ }
395
+ }
396
+ }
397
+ const selectedSkill = chooseInboundSkill(item, {
398
+ defaultSkill: skillHint || 'friend-im'
399
+ })
400
+ return localRuntimeExecutor({
401
+ item,
402
+ selectedSkill,
403
+ mailboxKey: resolveMailboxKey(item)
404
+ })
405
+ }
406
+
407
+ function receiptFileFor(keyFile, fullName) {
408
+ return defaultReceiptFile(keyFile, fullName)
409
+ }
410
+
411
+ function onboardingSummaryFileFor(keyFile, fullName) {
412
+ return defaultOnboardingSummaryFile(keyFile, fullName)
413
+ }
414
+
415
+ function gatewayLogFileFor(keyFile, fullName) {
416
+ return defaultGatewayLogFile(keyFile, fullName)
417
+ }
418
+
419
+ function writeJson(filePath, payload) {
420
+ const resolved = resolveUserPath(filePath)
421
+ fs.mkdirSync(path.dirname(resolved), { recursive: true })
422
+ fs.writeFileSync(resolved, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 })
423
+ return resolved
424
+ }
425
+
426
+ function readJson(filePath) {
427
+ return JSON.parse(fs.readFileSync(resolveUserPath(filePath), 'utf8'))
428
+ }
429
+
430
+ function archiveGatewayStateFile(gatewayStateFile, reason = 'stale') {
431
+ const resolved = clean(gatewayStateFile) ? resolveUserPath(gatewayStateFile) : ''
432
+ if (!resolved || !fs.existsSync(resolved)) {
433
+ return ''
434
+ }
435
+ const archived = `${resolved}.${clean(reason) || 'archived'}.${Date.now()}.bak`
436
+ fs.renameSync(resolved, archived)
437
+ return archived
438
+ }
439
+
440
+ function pidExists(pid) {
441
+ const numeric = Number.parseInt(`${pid ?? ''}`, 10)
442
+ if (!Number.isFinite(numeric) || numeric <= 0) {
443
+ return false
444
+ }
445
+ try {
446
+ process.kill(numeric, 0)
447
+ return true
448
+ } catch (error) {
449
+ return error?.code !== 'ESRCH'
450
+ }
451
+ }
452
+
453
+ function parsePid(value) {
454
+ const numeric = Number.parseInt(`${value ?? ''}`, 10)
455
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : null
456
+ }
457
+
458
+ function boolFlag(value, fallback = false) {
459
+ const normalized = clean(value).toLowerCase()
460
+ if (!normalized) {
461
+ return fallback
462
+ }
463
+ if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) {
464
+ return true
465
+ }
466
+ if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) {
467
+ return false
468
+ }
469
+ return fallback
470
+ }
471
+
472
+ function classifyGatewayFailure(error = '', hostRuntime = null) {
473
+ const message = clean(error)
474
+ const lower = message.toLowerCase()
475
+ if (!message) {
476
+ return {
477
+ code: 'gateway-startup-failed',
478
+ retryable: true,
479
+ guidance: [
480
+ 'Retry after updating @agentsquared/cli and restarting the local AgentSquared gateway.',
481
+ 'If the relay or host runtime still looks unstable, retry later.',
482
+ 'If the problem persists, open an issue in the official AgentSquared CLI repository.'
483
+ ]
484
+ }
485
+ }
486
+ if (lower.includes('host runtime preflight failed') || lower.includes('openclaw') || lower.includes('pairing') || lower.includes('loopback')) {
487
+ return {
488
+ code: 'adapter-startup-failed',
489
+ retryable: true,
490
+ guidance: [
491
+ `The ${clean(hostRuntime?.resolved) || 'host'} adapter could not be reached during gateway startup.`,
492
+ 'Update @agentsquared/cli and restart the local AgentSquared gateway.',
493
+ 'If the local host runtime is unstable, retry later.',
494
+ 'If the adapter still fails, report it to the official AgentSquared CLI issue tracker.'
495
+ ]
496
+ }
497
+ }
498
+ if (lower.includes('relay') || lower.includes('reservation') || lower.includes('presence') || lower.includes('too many requests') || lower.includes('429')) {
499
+ return {
500
+ code: 'relay-startup-failed',
501
+ retryable: true,
502
+ guidance: [
503
+ 'The AgentSquared relay path was not healthy enough during gateway startup.',
504
+ 'Retry after updating @agentsquared/cli or wait and retry later if the remote service looks unstable.',
505
+ 'If relay startup keeps failing, report it to the official AgentSquared CLI issue tracker.'
506
+ ]
507
+ }
508
+ }
509
+ return {
510
+ code: 'gateway-startup-failed',
511
+ retryable: true,
512
+ guidance: [
513
+ 'Retry after updating @agentsquared/cli and restarting the local AgentSquared gateway.',
514
+ 'If the problem persists, retry later or report it to the official AgentSquared CLI issue tracker.'
515
+ ]
516
+ }
517
+ }
518
+
519
+ function describeDetectedHostRuntime(detectedHostRuntime = null) {
520
+ const resolved = clean(detectedHostRuntime?.resolved)
521
+ if (resolved && resolved !== 'none') {
522
+ return resolved
523
+ }
524
+ const requested = clean(detectedHostRuntime?.requested)
525
+ if (requested && requested !== 'auto') {
526
+ return requested
527
+ }
528
+ return clean(detectedHostRuntime?.id) || 'unknown'
529
+ }
530
+
531
+ function assertSupportedActivationHostRuntime(detectedHostRuntime = null) {
532
+ if (clean(detectedHostRuntime?.resolved) === 'openclaw') {
533
+ return
534
+ }
535
+ const detected = describeDetectedHostRuntime(detectedHostRuntime)
536
+ const reason = clean(detectedHostRuntime?.reason)
537
+ const suggested = clean(detectedHostRuntime?.suggested) || 'openclaw'
538
+ const detail = reason ? ` Detection reason: ${reason}.` : ''
539
+ throw new Error(
540
+ `AgentSquared activation currently supports only the OpenClaw host runtime. Detected host runtime: ${detected}.${detail} Finish installing/configuring OpenClaw first, then retry onboarding. Other host runtimes are not adapted yet, so activation stops before registration. Suggested host runtime: ${suggested}.`
541
+ )
542
+ }
543
+
544
+ function isFlagToken(value) {
545
+ return clean(value).startsWith('-')
546
+ }
547
+
548
+ function sleep(ms) {
549
+ return new Promise((resolve) => setTimeout(resolve, ms))
550
+ }
551
+
552
+ function classifyOutboundFailure(error = '', targetAgentId = '') {
553
+ const failureKind = clean(typeof error === 'object' && error != null ? error.a2FailureKind : '')
554
+ const message = clean(typeof error === 'object' && error != null ? error.message : error)
555
+ const lower = message.toLowerCase()
556
+ if (failureKind === 'post-dispatch-empty-response') {
557
+ return {
558
+ code: 'post-dispatch-empty-response',
559
+ deliveryStatus: 'unknown',
560
+ failureStage: 'post-dispatch / final-response-empty',
561
+ confirmationLevel: 'remote may have received and processed the turn',
562
+ reason: `${clean(targetAgentId) || 'The target agent'} may have received this AgentSquared turn, but the final response stream ended with no JSON payload after dispatch.`,
563
+ nextStep: 'Do not automatically retry this same message. Tell the owner the remote side may have processed the turn, but the final response came back empty. Ask whether they want to check for a later reply or retry later.'
564
+ }
565
+ }
566
+ if (failureKind === 'post-dispatch-stream-closed') {
567
+ return {
568
+ code: 'post-dispatch-stream-closed',
569
+ deliveryStatus: 'unknown',
570
+ failureStage: 'post-dispatch / response-stream-closed',
571
+ confirmationLevel: 'remote may have received and processed the turn',
572
+ reason: `${clean(targetAgentId) || 'The target agent'} may have received this AgentSquared turn, but the response stream closed before the final reply could be confirmed locally.`,
573
+ nextStep: 'Do not automatically retry this same message. Tell the owner the remote side may have processed the turn, but the connection closed during response confirmation. Ask whether they want to check for a later reply or retry later.'
574
+ }
575
+ }
576
+ if (failureKind === 'post-dispatch-response-timeout') {
577
+ return {
578
+ code: 'post-dispatch-response-timeout',
579
+ deliveryStatus: 'unknown',
580
+ failureStage: 'post-dispatch / final-response-timeout',
581
+ confirmationLevel: 'remote accepted the turn but did not finish in time',
582
+ reason: `${clean(targetAgentId) || 'The target agent'} accepted this AgentSquared turn, but the final response timed out after dispatch.`,
583
+ nextStep: 'Do not automatically resend the same turn. Tell the owner the remote side accepted the turn but did not finish responding in time, then ask whether they want to wait for a later reply or retry later.'
584
+ }
585
+ }
586
+ if (lower.includes('request receipt timed out after')) {
587
+ return {
588
+ code: 'turn-receipt-timeout',
589
+ deliveryStatus: 'unconfirmed',
590
+ failureStage: 'awaiting-request-receipt',
591
+ confirmationLevel: 'receipt was never confirmed',
592
+ reason: `${clean(targetAgentId) || 'The target agent'} did not confirm receipt of this AgentSquared turn within 20 seconds, so delivery for this turn could not be confirmed.`,
593
+ nextStep: 'Do not continue the conversation automatically. Tell the owner this specific turn did not receive a delivery receipt in time, then ask whether they want to retry later.'
594
+ }
595
+ }
596
+ if (lower.includes('turn response timed out after')) {
597
+ return {
598
+ code: 'turn-response-timeout',
599
+ deliveryStatus: 'unknown',
600
+ failureStage: 'post-receipt / final-response-timeout',
601
+ confirmationLevel: 'remote acknowledged the turn but final response timed out',
602
+ reason: `${clean(targetAgentId) || 'The target agent'} accepted this AgentSquared turn, but did not return a final response before the per-turn response timeout.`,
603
+ nextStep: 'Do not automatically resend the same turn. Tell the owner the remote side acknowledged the turn but did not finish responding in time, then ask whether they want to wait for a later reply or retry later.'
604
+ }
605
+ }
606
+ if (lower.includes('delivery status is unknown after the request was dispatched')) {
607
+ return {
608
+ code: 'delivery-status-unknown',
609
+ deliveryStatus: 'unknown',
610
+ failureStage: 'post-dispatch / response-unconfirmed',
611
+ confirmationLevel: 'remote may already have processed the message',
612
+ reason: `${clean(targetAgentId) || 'The target agent'} may already have received and processed this AgentSquared message, but the response could not be confirmed locally.`,
613
+ nextStep: 'Do not automatically retry this same message. First tell the owner that delivery status is unknown and ask whether they want to check for a reply or retry later.'
614
+ }
615
+ }
616
+ if (lower.includes('no local agent runtime adapter is configured')) {
617
+ return {
618
+ code: 'target-runtime-unavailable',
619
+ deliveryStatus: 'failed',
620
+ failureStage: 'remote-runtime-unavailable',
621
+ confirmationLevel: 'target gateway was reachable but had no usable runtime',
622
+ reason: `${clean(targetAgentId) || 'The target agent'} is online in AgentSquared, but its local host runtime is not attached correctly right now. The target gateway appears to be running without a supported inbound runtime adapter.`,
623
+ nextStep: 'Do not switch to another target automatically. Stop here and tell the owner the target Agent must restart its AgentSquared gateway after fixing or re-detecting the supported host runtime.'
624
+ }
625
+ }
626
+ if (lower.includes('peer identity') || lower.includes('not visible in friend directory')) {
627
+ return {
628
+ code: 'target-unreachable',
629
+ deliveryStatus: 'failed',
630
+ failureStage: 'pre-dispatch / target-unreachable',
631
+ confirmationLevel: 'relay could not provide a usable live target',
632
+ reason: `${clean(targetAgentId) || 'The target agent'} is not currently reachable through AgentSquared. Relay did not provide a usable live peer identity for this target.`,
633
+ nextStep: 'Do not switch to another target automatically. Stop here and tell the owner this exact target is offline or unavailable. The owner can retry this same target later.'
634
+ }
635
+ }
636
+ if (lower.includes('missing dialaddrs')) {
637
+ return {
638
+ code: 'target-unreachable',
639
+ deliveryStatus: 'failed',
640
+ failureStage: 'pre-dispatch / target-address-missing',
641
+ confirmationLevel: 'target did not expose usable dial addresses',
642
+ reason: `${clean(targetAgentId) || 'The target agent'} does not currently expose any dialable AgentSquared transport addresses. The target may be offline, reconnecting, or missing fresh relay-backed transport publication.`,
643
+ nextStep: 'Do not switch to another target automatically. Stop here and tell the owner this exact target is not currently reachable. The owner can retry the same target later.'
644
+ }
645
+ }
646
+ if (lower.includes('gateway transport is unavailable') || lower.includes('recovering') || lower.includes('429') || lower.includes('too many requests') || lower.includes('relay')) {
647
+ return {
648
+ code: 'relay-or-gateway-unavailable',
649
+ deliveryStatus: 'failed',
650
+ failureStage: 'pre-dispatch / local-or-relay-path-unavailable',
651
+ confirmationLevel: 'delivery path was unstable before confirmation',
652
+ reason: message || 'The local AgentSquared gateway or relay path was not healthy enough to deliver this message.',
653
+ nextStep: 'Do not switch to another target automatically. Stop here and tell the owner this delivery failed because the current AgentSquared path is unstable. The owner can retry the same target later.'
654
+ }
655
+ }
656
+ return {
657
+ code: 'delivery-failed',
658
+ deliveryStatus: 'failed',
659
+ failureStage: 'unknown',
660
+ confirmationLevel: 'delivery could not be completed or confirmed',
661
+ reason: message || 'The AgentSquared message could not be delivered.',
662
+ nextStep: 'Do not switch to another target automatically. Stop here and ask the owner whether they want to retry this same target later.'
663
+ }
664
+ }
665
+
666
+ function extractFailureDetail(error = null) {
667
+ const raw = clean(typeof error === 'object' && error != null ? error.message : error)
668
+ if (!raw) {
669
+ return ''
670
+ }
671
+ return raw.replace(/^delivery status is unknown after the request was dispatched:\s*/i, '').trim()
672
+ }
673
+
674
+
675
+ async function registerAgent(args) {
676
+ const apiBase = clean(args['api-base']) || 'https://api.agentsquared.net'
677
+ const authorizationToken = requireArg(args['authorization-token'], '--authorization-token is required')
678
+ const agentName = requireArg(args['agent-name'], '--agent-name is required')
679
+ const keyTypeName = clean(args['key-type']) || 'ed25519'
680
+ const displayName = clean(args['display-name']) || agentName
681
+ const detectedHostRuntime = args.__detectedHostRuntime ?? null
682
+ const keyFile = resolveUserPath(args['key-file'] || defaultRuntimeKeyFile(agentName, args, detectedHostRuntime))
683
+ const keyBundle = generateRuntimeKeyBundle(keyTypeName)
684
+ writeRuntimeKeyBundle(keyFile, keyBundle)
685
+
686
+ const response = await fetch(`${apiBase.replace(/\/$/, '')}/api/onboard/register`, {
687
+ method: 'POST',
688
+ headers: {
689
+ 'Content-Type': 'application/json'
690
+ },
691
+ body: JSON.stringify({
692
+ authorizationToken,
693
+ agentName,
694
+ keyType: keyBundle.keyType,
695
+ publicKey: keyBundle.publicKey,
696
+ displayName
697
+ })
698
+ })
699
+ const payload = await response.json()
700
+ if (!response.ok) {
701
+ throw new Error(payload?.message || payload?.error || `Agent registration failed with status ${response.status}`)
702
+ }
703
+ const result = payload?.value ?? payload
704
+ const receiptFile = receiptFileFor(keyFile, result.fullName || `${agentName}@unknown`)
705
+ writeJson(receiptFile, result)
706
+ return {
707
+ apiBase,
708
+ keyFile,
709
+ keyBundle,
710
+ receiptFile,
711
+ result
712
+ }
713
+ }
714
+
715
+ async function commandOnboard(args) {
716
+ const authorizationToken = clean(args['authorization-token'])
717
+ assertNoExistingLocalActivation(authorizationToken)
718
+ const detectedHostRuntime = await detectHostRuntimeEnvironment({
719
+ preferred: clean(args['host-runtime']) || 'auto',
720
+ openclaw: {
721
+ command: clean(args['openclaw-command']) || 'openclaw',
722
+ cwd: clean(args['openclaw-cwd']),
723
+ openclawAgent: clean(args['openclaw-agent']),
724
+ gatewayUrl: clean(args['openclaw-gateway-url']),
725
+ gatewayToken: clean(args['openclaw-gateway-token']),
726
+ gatewayPassword: clean(args['openclaw-gateway-password'])
727
+ }
728
+ })
729
+ assertSupportedActivationHostRuntime(detectedHostRuntime)
730
+ if (!authorizationToken) {
731
+ throw new Error('--authorization-token is required for first-time onboarding.')
732
+ }
733
+ const registration = await registerAgent({
734
+ ...args,
735
+ __detectedHostRuntime: detectedHostRuntime
736
+ })
737
+ const fullName = registration.result.fullName
738
+ const gatewayStateFile = clean(args['gateway-state-file']) || defaultGatewayStateFile(registration.keyFile, fullName)
739
+ const previousGatewayState = readGatewayState(gatewayStateFile)
740
+ const shouldStartGateway = boolFlag(args['start-gateway'], true)
741
+ let gateway = {
742
+ started: false,
743
+ launchRequested: false,
744
+ pending: false,
745
+ gatewayBase: '',
746
+ health: null,
747
+ error: '',
748
+ logFile: '',
749
+ pid: null
750
+ }
751
+
752
+ if (shouldStartGateway) {
753
+ gateway.launchRequested = true
754
+ const gatewayArgs = buildGatewayArgs(args, fullName, registration.keyFile, detectedHostRuntime)
755
+ const existingGateway = await inspectExistingGateway({
756
+ keyFile: registration.keyFile,
757
+ agentId: fullName,
758
+ gatewayStateFile: clean(args['gateway-state-file'])
759
+ })
760
+ if (existingGateway.running && !existingGateway.revisionMatches) {
761
+ gateway = {
762
+ started: false,
763
+ launchRequested: true,
764
+ pending: false,
765
+ gatewayBase: existingGateway.gatewayBase,
766
+ health: existingGateway.health,
767
+ error: 'An existing AgentSquared gateway process is running from an older @agentsquared/cli revision. Use `a2-cli gateway restart ...` before onboarding tries to reuse it.',
768
+ logFile: gatewayLogFileFor(registration.keyFile, fullName),
769
+ pid: existingGateway.pid
770
+ }
771
+ } else if (existingGateway.running && existingGateway.healthy) {
772
+ gateway = {
773
+ started: true,
774
+ launchRequested: true,
775
+ pending: false,
776
+ gatewayBase: existingGateway.gatewayBase,
777
+ health: existingGateway.health,
778
+ error: '',
779
+ logFile: gatewayLogFileFor(registration.keyFile, fullName),
780
+ pid: existingGateway.pid
781
+ }
782
+ } else if (existingGateway.running) {
783
+ gateway = {
784
+ started: false,
785
+ launchRequested: true,
786
+ pending: true,
787
+ gatewayBase: existingGateway.gatewayBase,
788
+ health: existingGateway.health,
789
+ error: 'An existing AgentSquared gateway process is already running but is not healthy yet. Use `a2-cli gateway restart ...` instead of starting another one.',
790
+ logFile: gatewayLogFileFor(registration.keyFile, fullName),
791
+ pid: existingGateway.pid
792
+ }
793
+ } else {
794
+ let archivedGatewayStateFile = ''
795
+ if (existingGateway.stateFile && existingGateway.state) {
796
+ const staleState = !existingGateway.revisionMatches || !clean(existingGateway.state?.gatewayBase)
797
+ if (staleState) {
798
+ archivedGatewayStateFile = archiveGatewayStateFile(existingGateway.stateFile, 'restart-required')
799
+ }
800
+ }
801
+ const gatewayLogFile = gatewayLogFileFor(registration.keyFile, fullName)
802
+ fs.mkdirSync(path.dirname(gatewayLogFile), { recursive: true })
803
+ const stdoutFd = fs.openSync(gatewayLogFile, 'a')
804
+ const stderrFd = fs.openSync(gatewayLogFile, 'a')
805
+ const child = spawn(process.execPath, [path.join(ROOT, 'a2_cli.mjs'), 'gateway', ...gatewayArgs], {
806
+ detached: true,
807
+ cwd: ROOT,
808
+ stdio: ['ignore', stdoutFd, stderrFd]
809
+ })
810
+ fs.closeSync(stdoutFd)
811
+ fs.closeSync(stderrFd)
812
+ child.unref()
813
+ gateway.logFile = gatewayLogFile
814
+ gateway.pid = child.pid ?? null
815
+ try {
816
+ const ready = await waitForGatewayReady({
817
+ keyFile: registration.keyFile,
818
+ agentId: fullName,
819
+ gatewayStateFile: clean(args['gateway-state-file']),
820
+ timeoutMs: Number.parseInt(args['gateway-wait-ms'] ?? '90000', 10) || 90000
821
+ })
822
+ gateway = {
823
+ started: true,
824
+ launchRequested: true,
825
+ pending: false,
826
+ gatewayBase: ready.gatewayBase,
827
+ health: ready.health,
828
+ error: '',
829
+ logFile: gatewayLogFile,
830
+ pid: child.pid ?? null,
831
+ archivedGatewayStateFile
832
+ }
833
+ } catch (error) {
834
+ const gatewayState = readGatewayState(gatewayStateFile)
835
+ const discoveredPid = gatewayState?.gatewayPid ?? child.pid ?? null
836
+ const discoveredBase = clean(gatewayState?.gatewayBase)
837
+ const failure = classifyGatewayFailure(error.message, detectedHostRuntime)
838
+ gateway.pending = pidExists(discoveredPid)
839
+ gateway.gatewayBase = discoveredBase
840
+ gateway.pid = parsePid(discoveredPid)
841
+ gateway.error = error.message
842
+ gateway.failure = failure
843
+ gateway.archivedGatewayStateFile = archivedGatewayStateFile
844
+ }
845
+ }
846
+ }
847
+
848
+ const agentsquaredDir = path.dirname(resolveUserPath(registration.keyFile))
849
+ const inboxDir = defaultInboxDir(registration.keyFile, fullName)
850
+ const onboardingSummaryFile = onboardingSummaryFileFor(registration.keyFile, fullName)
851
+ const standardReport = buildStandardRuntimeReport({
852
+ apiBase: registration.apiBase,
853
+ agentId: fullName,
854
+ keyFile: registration.keyFile,
855
+ detectedHostRuntime,
856
+ registration: registration.result,
857
+ gateway,
858
+ gatewayHealth: gateway.health,
859
+ previousState: previousGatewayState
860
+ })
861
+ const summary = {
862
+ setupComplete: true,
863
+ apiBase: registration.apiBase,
864
+ agentsquaredDir,
865
+ hostRuntime: detectedHostRuntime,
866
+ receiptFile: registration.receiptFile,
867
+ keyFile: registration.keyFile,
868
+ inboxDir,
869
+ memoryReminder: {
870
+ required: true,
871
+ instruction: 'Store the AgentSquared platform introduction, this local AgentSquared directory, the runtime key path, and the common a2-cli commands in the host runtime memory system.'
872
+ },
873
+ registration: registration.result,
874
+ gateway,
875
+ standardReport,
876
+ ownerFacingLines: [
877
+ 'AgentSquared setup is complete.',
878
+ `Agent: ${registration.result.fullName}`,
879
+ `AgentSquared directory: ${agentsquaredDir}.`,
880
+ `Host runtime: ${detectedHostRuntime.resolved !== 'none' ? detectedHostRuntime.resolved : `not bound (${detectedHostRuntime.suggested || 'openclaw'} suggested)`}.`,
881
+ gateway.started
882
+ ? `Gateway was auto-started and is running at ${gateway.gatewayBase}.`
883
+ : gateway.pending
884
+ ? `Gateway launch was requested and the background process is still running${gateway.gatewayBase ? ` at ${gateway.gatewayBase}` : ''}, but health was not confirmed before timeout.`
885
+ : `Gateway auto-start is not confirmed${gateway.error ? `: ${gateway.error}` : '.'}`,
886
+ gateway.logFile
887
+ ? `Gateway log file: ${gateway.logFile}.`
888
+ : 'Gateway log file: unavailable.',
889
+ ...(gateway.failure?.guidance ?? []),
890
+ 'Store the AgentSquared platform intro, this AgentSquared directory, the runtime key path, and the common a2-cli commands in the host runtime memory system.',
891
+ `Inbox audit path: ${inboxDir}.`,
892
+ 'AgentSquared, A², and A2 all mean the same platform.',
893
+ 'Use live official reads for exact current friends, agent cards, and relay facts.',
894
+ ...buildStandardRuntimeOwnerLines(standardReport)
895
+ ]
896
+ }
897
+ summary.ownerFacingText = toOwnerFacingText(summary.ownerFacingLines)
898
+ writeJson(onboardingSummaryFile, summary)
899
+ printJson(summary)
900
+ }
901
+
902
+ async function commandGateway(args, rawArgs) {
903
+ const existingGateway = await inspectExistingGateway({
904
+ gatewayBase: args['gateway-base'],
905
+ keyFile: args['key-file'],
906
+ agentId: args['agent-id'],
907
+ gatewayStateFile: args['gateway-state-file']
908
+ })
909
+ if (existingGateway.running && !existingGateway.revisionMatches) {
910
+ throw new Error('An AgentSquared gateway process is already running from an older @agentsquared/cli revision. Use `a2-cli gateway restart --agent-id <fullName> --key-file <runtime-key-file>` instead of reusing it.')
911
+ }
912
+ if (existingGateway.running && existingGateway.healthy) {
913
+ const standardReport = buildStandardRuntimeReport({
914
+ apiBase: clean(args['api-base']) || 'https://api.agentsquared.net',
915
+ agentId: clean(existingGateway.state?.agentId) || clean(args['agent-id']),
916
+ keyFile: clean(existingGateway.state?.keyFile) || clean(args['key-file']),
917
+ detectedHostRuntime: existingGateway.health?.hostRuntime ?? { resolved: resolvedHostRuntimeFromHealth(existingGateway.health) },
918
+ gateway: {
919
+ started: true,
920
+ gatewayBase: existingGateway.gatewayBase,
921
+ health: existingGateway.health
922
+ },
923
+ gatewayHealth: existingGateway.health,
924
+ previousState: existingGateway.state
925
+ })
926
+ printJson({
927
+ alreadyRunning: true,
928
+ gatewayBase: existingGateway.gatewayBase,
929
+ pid: existingGateway.pid,
930
+ health: existingGateway.health,
931
+ standardReport,
932
+ ownerFacingLines: buildStandardRuntimeOwnerLines(standardReport),
933
+ ownerFacingText: toOwnerFacingText(buildStandardRuntimeOwnerLines(standardReport))
934
+ })
935
+ return
936
+ }
937
+ if (existingGateway.running) {
938
+ throw new Error('An AgentSquared gateway process is already running but is not healthy. Use `a2-cli gateway restart --agent-id <fullName> --key-file <runtime-key-file>` instead of starting another instance.')
939
+ }
940
+ await runGateway(rawArgs)
941
+ }
942
+
943
+ async function commandGatewayRestart(args, rawArgs) {
944
+ const context = resolveAgentContext(args)
945
+ const agentId = context.agentId
946
+ const keyFile = context.keyFile
947
+ const gatewayStateFile = clean(args['gateway-state-file']) || context.gatewayStateFile
948
+ const priorState = readGatewayState(gatewayStateFile)
949
+ const priorPid = parsePid(priorState?.gatewayPid)
950
+ let archivedGatewayStateFile = ''
951
+ const gatewayArgs = buildGatewayArgs(args, agentId, keyFile, null)
952
+ const gatewayLogFile = gatewayLogFileFor(keyFile, agentId)
953
+
954
+ if (priorPid) {
955
+ try {
956
+ process.kill(priorPid, 'SIGTERM')
957
+ } catch (error) {
958
+ if (error?.code !== 'ESRCH') {
959
+ throw error
960
+ }
961
+ }
962
+ const deadline = Date.now() + 8000
963
+ while (Date.now() < deadline) {
964
+ try {
965
+ process.kill(priorPid, 0)
966
+ await sleep(250)
967
+ } catch (error) {
968
+ if (error?.code === 'ESRCH') {
969
+ break
970
+ }
971
+ throw error
972
+ }
973
+ }
974
+ }
975
+
976
+ const priorStateRevision = clean(priorState?.runtimeRevision)
977
+ const stalePriorState = priorState && (!priorStateRevision || priorStateRevision !== currentRuntimeRevision() || !clean(priorState?.gatewayBase))
978
+ if (stalePriorState && !pidExists(priorPid)) {
979
+ archivedGatewayStateFile = archiveGatewayStateFile(gatewayStateFile, 'restart-required')
980
+ }
981
+
982
+ fs.mkdirSync(path.dirname(gatewayLogFile), { recursive: true })
983
+ const stdoutFd = fs.openSync(gatewayLogFile, 'a')
984
+ const stderrFd = fs.openSync(gatewayLogFile, 'a')
985
+ const child = spawn(process.execPath, [path.join(ROOT, 'a2_cli.mjs'), 'gateway', ...gatewayArgs], {
986
+ detached: true,
987
+ cwd: ROOT,
988
+ stdio: ['ignore', stdoutFd, stderrFd]
989
+ })
990
+ fs.closeSync(stdoutFd)
991
+ fs.closeSync(stderrFd)
992
+ child.unref()
993
+
994
+ let ready
995
+ try {
996
+ ready = await waitForGatewayReady({
997
+ keyFile,
998
+ agentId,
999
+ gatewayStateFile,
1000
+ timeoutMs: Number.parseInt(args['gateway-wait-ms'] ?? '30000', 10) || 30000
1001
+ })
1002
+ } catch (error) {
1003
+ throw new Error(`${error.message} Check the gateway log at ${gatewayLogFile}.`)
1004
+ }
1005
+
1006
+ const standardReport = buildStandardRuntimeReport({
1007
+ apiBase: clean(args['api-base']) || 'https://api.agentsquared.net',
1008
+ agentId,
1009
+ keyFile,
1010
+ detectedHostRuntime: ready.health?.hostRuntime ?? { resolved: resolvedHostRuntimeFromHealth(ready.health) },
1011
+ gateway: {
1012
+ started: true,
1013
+ gatewayBase: ready.gatewayBase,
1014
+ health: ready.health
1015
+ },
1016
+ gatewayHealth: ready.health,
1017
+ previousState: priorState
1018
+ })
1019
+
1020
+ const ownerFacingLines = buildStandardRuntimeOwnerLines(standardReport)
1021
+ printJson({
1022
+ restarted: true,
1023
+ previousGatewayPid: priorPid,
1024
+ gatewayPid: child.pid ?? null,
1025
+ gatewayBase: ready.gatewayBase,
1026
+ health: ready.health,
1027
+ gatewayLogFile,
1028
+ archivedGatewayStateFile,
1029
+ agentsquaredDir: path.dirname(resolveUserPath(keyFile)),
1030
+ standardReport,
1031
+ ownerFacingLines,
1032
+ ownerFacingText: toOwnerFacingText(ownerFacingLines),
1033
+ memoryReminder: {
1034
+ required: true,
1035
+ instruction: 'Keep the AgentSquared platform introduction, this local AgentSquared directory, the runtime key path, and the common a2-cli commands in the host runtime memory system.'
1036
+ }
1037
+ })
1038
+ }
1039
+
1040
+ async function commandGatewayHealth(args) {
1041
+ const context = resolveAgentContext(args)
1042
+ const gatewayBase = resolveGatewayBase({
1043
+ gatewayBase: args['gateway-base'],
1044
+ keyFile: context.keyFile,
1045
+ agentId: context.agentId,
1046
+ gatewayStateFile: clean(args['gateway-state-file']) || context.gatewayStateFile
1047
+ })
1048
+ printJson(await gatewayHealth(gatewayBase))
1049
+ }
1050
+
1051
+ async function commandFriendList(args) {
1052
+ const ctx = await signedRelayContext(args)
1053
+ const directory = await getFriendDirectory(ctx.apiBase, ctx.agentId, ctx.bundle, ctx.transport)
1054
+ printJson({
1055
+ source: 'relay-friend-directory',
1056
+ apiBase: ctx.apiBase,
1057
+ agentId: ctx.agentId,
1058
+ gatewayBase: ctx.gatewayBase,
1059
+ usedGatewayTransport: Boolean(ctx.transport),
1060
+ directory
1061
+ })
1062
+ }
1063
+
1064
+ async function commandFriendMessage(args) {
1065
+ const gateway = await ensureGatewayForUse(args)
1066
+ const context = {
1067
+ agentId: gateway.agentId,
1068
+ keyFile: gateway.keyFile,
1069
+ gatewayStateFile: gateway.gatewayStateFile
1070
+ }
1071
+ const gatewayBase = gateway.gatewayBase
1072
+ const targetAgentId = requireArg(args['target-agent'], '--target-agent is required')
1073
+ const text = requireArg(args.text, '--text is required')
1074
+ const ownerLanguage = inferOwnerFacingLanguage(text)
1075
+ const ownerTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
1076
+ let skillHint = 'friend-im'
1077
+ const skillFile = clean(args['skill-file'])
1078
+ const sharedSkill = skillFile ? loadSharedSkillFile(skillFile) : null
1079
+ const explicitSkillName = clean(args['skill-name'] || args.skill)
1080
+ const skillDecision = await resolveOutboundSkillHint({
1081
+ agentId: context.agentId,
1082
+ keyFile: context.keyFile,
1083
+ args,
1084
+ targetAgentId,
1085
+ text,
1086
+ explicitSkillName,
1087
+ sharedSkill
1088
+ })
1089
+ skillHint = clean(skillDecision.skillHint) || 'friend-im'
1090
+ const conversationPolicy = resolveConversationPolicy(skillHint, sharedSkill)
1091
+ const conversationKey = randomRequestId('conversation')
1092
+ const sentAt = new Date().toISOString()
1093
+ let localSkillInventorySnapshot = ''
1094
+ if (skillHint === 'agent-mutual-learning') {
1095
+ try {
1096
+ const hostContext = await resolveCliOpenClawHostContext({
1097
+ agentId: context.agentId,
1098
+ keyFile: context.keyFile,
1099
+ args,
1100
+ purpose: 'mutual-learning local skill inventory'
1101
+ })
1102
+ const inspectedLocalSkills = await inspectOpenClawLocalSkills({
1103
+ localAgentId: context.agentId,
1104
+ openclawAgent: hostContext.resolvedOpenClawAgent,
1105
+ command: hostContext.openclawCommand,
1106
+ cwd: hostContext.openclawCwd,
1107
+ configPath: hostContext.openclawConfigPath,
1108
+ stateDir: hostContext.openclawStateDir,
1109
+ timeoutMs: 60000,
1110
+ gatewayUrl: hostContext.openclawGatewayUrl,
1111
+ gatewayToken: hostContext.openclawGatewayToken,
1112
+ gatewayPassword: hostContext.openclawGatewayPassword,
1113
+ purpose: `sender-${conversationKey}`
1114
+ })
1115
+ localSkillInventorySnapshot = clean(inspectedLocalSkills.inventoryPromptText)
1116
+ } catch {
1117
+ localSkillInventorySnapshot = ''
1118
+ }
1119
+ }
1120
+ const outboundText = buildSkillOutboundText({
1121
+ localAgentId: context.agentId,
1122
+ targetAgentId,
1123
+ skillName: skillHint,
1124
+ originalText: text,
1125
+ sentAt,
1126
+ localSkillInventory: localSkillInventorySnapshot
1127
+ })
1128
+ let result
1129
+ const turnLog = []
1130
+ let localRuntimeExecutor = null
1131
+ let currentOutboundText = outboundText
1132
+ let currentOutboundControl = normalizeConversationControl({
1133
+ turnIndex: 1,
1134
+ decision: conversationPolicy.maxTurns <= 1 ? 'done' : 'continue',
1135
+ stopReason: conversationPolicy.maxTurns <= 1 ? 'single-turn' : '',
1136
+ finalize: conversationPolicy.maxTurns <= 1
1137
+ })
1138
+ let turnIndex = 1
1139
+ let localStopReason = ''
1140
+ let continuationError = ''
1141
+ try {
1142
+ while (true) {
1143
+ result = await gatewayConnect(gatewayBase, {
1144
+ targetAgentId,
1145
+ skillHint,
1146
+ method: 'message/send',
1147
+ message: {
1148
+ kind: 'message',
1149
+ role: 'user',
1150
+ parts: [{ kind: 'text', text: currentOutboundText }]
1151
+ },
1152
+ metadata: {
1153
+ ...(sharedSkill ? { sharedSkill } : {}),
1154
+ originalOwnerText: turnIndex === 1 ? text : currentOutboundText,
1155
+ ...(turnIndex === 1 && localSkillInventorySnapshot ? { localSkillInventory: localSkillInventorySnapshot } : {}),
1156
+ conversationKey,
1157
+ sentAt,
1158
+ turnIndex: currentOutboundControl.turnIndex,
1159
+ decision: currentOutboundControl.decision,
1160
+ stopReason: currentOutboundControl.stopReason,
1161
+ finalize: currentOutboundControl.finalize
1162
+ },
1163
+ activitySummary: turnIndex === 1
1164
+ ? 'Preparing an AgentSquared peer conversation.'
1165
+ : `Continuing AgentSquared peer conversation turn ${turnIndex}.`,
1166
+ report: {
1167
+ taskId: skillHint,
1168
+ summary: `Delivered AgentSquared conversation turn ${turnIndex} to ${targetAgentId}.`,
1169
+ publicSummary: ''
1170
+ }
1171
+ })
1172
+
1173
+ const replyText = peerResponseText(result.response)
1174
+ const remoteControl = normalizeConversationControl(extractPeerResponseMetadata(result.response), {
1175
+ defaultTurnIndex: turnIndex,
1176
+ defaultDecision: 'done',
1177
+ defaultStopReason: turnIndex >= conversationPolicy.maxTurns ? 'single-turn' : '',
1178
+ defaultFinalize: turnIndex >= conversationPolicy.maxTurns
1179
+ })
1180
+ turnLog.push({
1181
+ turnIndex,
1182
+ outboundText: currentOutboundText,
1183
+ replyText,
1184
+ localDecision: currentOutboundControl.decision,
1185
+ localStopReason: currentOutboundControl.stopReason,
1186
+ localFinalize: currentOutboundControl.finalize,
1187
+ remoteDecision: remoteControl.decision,
1188
+ remoteStopReason: remoteControl.stopReason,
1189
+ remoteFinalize: remoteControl.finalize
1190
+ })
1191
+
1192
+ if (currentOutboundControl.finalize || !shouldContinueConversation(remoteControl)) {
1193
+ break
1194
+ }
1195
+
1196
+ const nextTurnIndex = turnIndex + 1
1197
+ if (nextTurnIndex > conversationPolicy.maxTurns) {
1198
+ localStopReason = 'max-turns-reached'
1199
+ break
1200
+ }
1201
+
1202
+ if (!localRuntimeExecutor) {
1203
+ localRuntimeExecutor = await createCliLocalRuntimeExecutor({
1204
+ agentId: context.agentId,
1205
+ keyFile: context.keyFile,
1206
+ args
1207
+ })
1208
+ }
1209
+
1210
+ let localExecution
1211
+ try {
1212
+ localExecution = await executeLocalConversationTurn({
1213
+ localRuntimeExecutor,
1214
+ localAgentId: context.agentId,
1215
+ targetAgentId,
1216
+ peerSessionId: result.peerSessionId,
1217
+ conversationKey,
1218
+ skillHint,
1219
+ sharedSkill,
1220
+ inboundText: replyText,
1221
+ originalOwnerText: text,
1222
+ localSkillInventory: localSkillInventorySnapshot,
1223
+ turnIndex: nextTurnIndex,
1224
+ remoteControl
1225
+ })
1226
+ } catch (error) {
1227
+ continuationError = clean(error?.message) || 'local runtime execution failed'
1228
+ localStopReason = 'receiver-runtime-unavailable'
1229
+ break
1230
+ }
1231
+ if (localExecution?.reject) {
1232
+ continuationError = clean(localExecution.reject.message) || 'local runtime rejected the inbound request'
1233
+ localStopReason = 'receiver-runtime-unavailable'
1234
+ break
1235
+ }
1236
+ const localControl = normalizeConversationControl(localExecution?.peerResponse?.metadata ?? {}, {
1237
+ defaultTurnIndex: nextTurnIndex,
1238
+ defaultDecision: nextTurnIndex >= conversationPolicy.maxTurns ? 'done' : 'continue',
1239
+ defaultStopReason: nextTurnIndex >= conversationPolicy.maxTurns ? 'max-turns-reached' : '',
1240
+ defaultFinalize: nextTurnIndex >= conversationPolicy.maxTurns
1241
+ })
1242
+ currentOutboundText = scrubOutboundText(peerResponseText(localExecution.peerResponse))
1243
+ if (!currentOutboundText) {
1244
+ localStopReason = 'goal-satisfied'
1245
+ break
1246
+ }
1247
+ turnIndex = nextTurnIndex
1248
+ currentOutboundControl = localControl
1249
+ if (localControl.finalize && clean(localControl.stopReason)) {
1250
+ localStopReason = localControl.stopReason
1251
+ }
1252
+ }
1253
+ } catch (error) {
1254
+ const failure = classifyOutboundFailure(error, targetAgentId)
1255
+ const senderReport = buildSenderFailureReport({
1256
+ localAgentId: context.agentId,
1257
+ targetAgentId,
1258
+ selectedSkill: skillHint,
1259
+ sentAt,
1260
+ originalText: text,
1261
+ conversationKey,
1262
+ deliveryStatus: failure.deliveryStatus,
1263
+ failureStage: failure.failureStage,
1264
+ confirmationLevel: failure.confirmationLevel,
1265
+ failureCode: failure.code,
1266
+ failureReason: failure.reason,
1267
+ failureDetail: extractFailureDetail(error),
1268
+ nextStep: failure.nextStep,
1269
+ language: ownerLanguage,
1270
+ timeZone: ownerTimeZone,
1271
+ localTime: true
1272
+ })
1273
+ const ownerDelivery = await pushCliOwnerReport({
1274
+ agentId: context.agentId,
1275
+ keyFile: context.keyFile,
1276
+ args,
1277
+ targetAgentId,
1278
+ selectedSkill: skillHint,
1279
+ ownerReport: senderReport,
1280
+ deliveryId: `sender-failure-${conversationKey || randomRequestId('conversation')}`
1281
+ })
1282
+ const deliveredToOwner = Boolean(ownerDelivery.delivered)
1283
+ const ownerFacingText = deliveredToOwner
1284
+ ? ''
1285
+ : renderOwnerFacingReport(senderReport)
1286
+ const payload = {
1287
+ ok: false,
1288
+ targetAgentId,
1289
+ skillHint,
1290
+ skillHintSource: skillDecision.source,
1291
+ skillHintReason: skillDecision.reason,
1292
+ conversationKey,
1293
+ error: {
1294
+ code: failure.code,
1295
+ message: failure.reason,
1296
+ detail: clean(error?.message)
1297
+ },
1298
+ ownerDelivery,
1299
+ ownerReplyPolicy: deliveredToOwner ? 'suppress' : 'report',
1300
+ ownerFacingMode: deliveredToOwner ? 'suppress' : 'verbatim',
1301
+ ownerFacingInstruction: deliveredToOwner
1302
+ ? 'The full owner-facing AgentSquared report has already been delivered through the current owner channel. Do not add any extra owner-facing reply, summary, or recap.'
1303
+ : 'Use ownerFacingText verbatim as the owner-facing update for the human owner.',
1304
+ ownerFacingText,
1305
+ ownerFacingLines: toOwnerFacingLines(ownerFacingText),
1306
+ stdoutNoticeCode: deliveredToOwner ? 'OWNER_REPORT_ALREADY_DELIVERED' : '',
1307
+ stdoutLines: []
1308
+ }
1309
+ if (!deliveredToOwner) {
1310
+ payload.ownerReport = senderReport
1311
+ payload.senderReport = senderReport
1312
+ payload.turnCount = turnLog.length || turnIndex
1313
+ }
1314
+ printJson(payload)
1315
+ process.exitCode = 1
1316
+ return
1317
+ }
1318
+ const replyText = peerResponseText(result.response)
1319
+ const finalRemoteControl = normalizeConversationControl(extractPeerResponseMetadata(result.response), {
1320
+ defaultTurnIndex: turnIndex,
1321
+ defaultDecision: 'done',
1322
+ defaultStopReason: localStopReason || '',
1323
+ defaultFinalize: true
1324
+ })
1325
+ let summarizedOverall = ''
1326
+ let summarizedDetailedConversation = []
1327
+ let summarizedDifferentiatedSkills = []
1328
+ if (skillHint === 'agent-mutual-learning') {
1329
+ try {
1330
+ const hostContext = await resolveCliOpenClawHostContext({
1331
+ agentId: context.agentId,
1332
+ keyFile: context.keyFile,
1333
+ args,
1334
+ purpose: 'mutual-learning conversation summary'
1335
+ })
1336
+ const summarized = await summarizeOpenClawConversation({
1337
+ localAgentId: context.agentId,
1338
+ remoteAgentId: targetAgentId,
1339
+ selectedSkill: skillHint,
1340
+ originalOwnerText: text,
1341
+ turnLog,
1342
+ localSkillInventory: localSkillInventorySnapshot,
1343
+ openclawAgent: hostContext.resolvedOpenClawAgent,
1344
+ command: hostContext.openclawCommand,
1345
+ cwd: hostContext.openclawCwd,
1346
+ configPath: hostContext.openclawConfigPath,
1347
+ stateDir: hostContext.openclawStateDir,
1348
+ timeoutMs: 60000,
1349
+ gatewayUrl: hostContext.openclawGatewayUrl,
1350
+ gatewayToken: hostContext.openclawGatewayToken,
1351
+ gatewayPassword: hostContext.openclawGatewayPassword
1352
+ })
1353
+ summarizedOverall = clean(summarized.overallSummary)
1354
+ summarizedDetailedConversation = Array.isArray(summarized.detailedConversation)
1355
+ ? summarized.detailedConversation.map((item) => clean(item)).filter(Boolean)
1356
+ : []
1357
+ summarizedDifferentiatedSkills = Array.isArray(summarized.differentiatedSkills)
1358
+ ? summarized.differentiatedSkills.map((item) => clean(item)).filter(Boolean)
1359
+ : []
1360
+ } catch (error) {
1361
+ continuationError = continuationError || `conversation-summary-failed: ${clean(error?.message) || 'unknown error'}`
1362
+ }
1363
+ }
1364
+ const defaultTurnOutline = turnLog.map((turn) => {
1365
+ const outbound = excerpt(turn.outboundText, 120)
1366
+ const reply = excerpt(turn.replyText, 120)
1367
+ const stop = clean(turn.remoteStopReason || turn.localStopReason)
1368
+ return {
1369
+ turnIndex: turn.turnIndex,
1370
+ summary: [
1371
+ outbound ? `I shared or asked "${outbound}"` : 'I sent a message',
1372
+ reply ? `the peer replied "${reply}"` : 'the peer reply had no displayable text',
1373
+ stop ? `(stop: ${stop})` : ''
1374
+ ].filter(Boolean).join(' ')
1375
+ }
1376
+ })
1377
+ const actionItems = []
1378
+ if (skillHint === 'agent-mutual-learning' && clean(localSkillInventorySnapshot)) {
1379
+ actionItems.push('Prepared a verified local skill snapshot before starting the exchange and used it as the baseline for comparison.')
1380
+ }
1381
+ for (const item of summarizedDifferentiatedSkills) {
1382
+ actionItems.push(`Different skill or workflow identified: ${item}.`)
1383
+ }
1384
+ const senderReport = buildSenderBaseReport({
1385
+ localAgentId: context.agentId,
1386
+ targetAgentId,
1387
+ selectedSkill: skillHint,
1388
+ sentAt,
1389
+ originalText: text,
1390
+ sentText: scrubOutboundText(turnLog[0]?.outboundText || outboundText),
1391
+ replyText,
1392
+ replyAt: new Date().toISOString(),
1393
+ peerSessionId: result.peerSessionId,
1394
+ conversationKey,
1395
+ turnCount: turnLog.length || 1,
1396
+ stopReason: finalRemoteControl.stopReason || localStopReason,
1397
+ overallSummary: summarizedOverall,
1398
+ turnOutline: summarizedDetailedConversation.length > 0
1399
+ ? summarizedDetailedConversation.map((summary, index) => ({
1400
+ turnIndex: index + 1,
1401
+ summary: clean(summary).replace(/^Turn\s+\d+\s*:\s*/i, '')
1402
+ }))
1403
+ : defaultTurnOutline,
1404
+ actionItems,
1405
+ detailsHint: continuationError
1406
+ ? `Detailed turn-by-turn exchange is available in the conversation output below. The local AI runtime then failed while preparing the next turn: ${continuationError}`
1407
+ : 'Detailed turn-by-turn exchange is available in the conversation output below.',
1408
+ language: ownerLanguage,
1409
+ timeZone: ownerTimeZone,
1410
+ localTime: true
1411
+ })
1412
+ const ownerDelivery = await pushCliOwnerReport({
1413
+ agentId: context.agentId,
1414
+ keyFile: context.keyFile,
1415
+ args,
1416
+ targetAgentId,
1417
+ selectedSkill: skillHint,
1418
+ ownerReport: senderReport,
1419
+ deliveryId: `sender-success-${conversationKey || randomRequestId('conversation')}`
1420
+ })
1421
+ const deliveredToOwner = Boolean(ownerDelivery.delivered)
1422
+ const ownerFacingText = deliveredToOwner
1423
+ ? ''
1424
+ : renderOwnerFacingReport(senderReport)
1425
+ const payload = {
1426
+ ok: true,
1427
+ ownerDelivery,
1428
+ ownerReplyPolicy: deliveredToOwner ? 'suppress' : 'report',
1429
+ ownerFacingMode: deliveredToOwner ? 'suppress' : 'verbatim',
1430
+ ownerFacingInstruction: deliveredToOwner
1431
+ ? 'The full owner-facing AgentSquared report has already been delivered through the current owner channel. Do not add any extra owner-facing reply, summary, or recap.'
1432
+ : 'Use ownerFacingText verbatim as the owner-facing update for the human owner.',
1433
+ ownerFacingText,
1434
+ ownerFacingLines: toOwnerFacingLines(ownerFacingText),
1435
+ stdoutNoticeCode: deliveredToOwner ? 'OWNER_REPORT_ALREADY_DELIVERED' : '',
1436
+ stdoutLines: []
1437
+ }
1438
+ if (!deliveredToOwner) {
1439
+ payload.targetAgentId = targetAgentId
1440
+ payload.skillHint = skillHint
1441
+ payload.skillHintSource = skillDecision.source
1442
+ payload.skillHintReason = skillDecision.reason
1443
+ payload.ticketExpiresAt = result.ticket?.expiresAt ?? ''
1444
+ payload.peerSessionId = result.peerSessionId ?? ''
1445
+ payload.conversationKey = conversationKey
1446
+ payload.reusedSession = Boolean(result.reusedSession)
1447
+ payload.continuationError = continuationError
1448
+ payload.turnCount = turnLog.length || 1
1449
+ payload.stopReason = finalRemoteControl.stopReason || localStopReason
1450
+ payload.conversationTurns = turnLog
1451
+ payload.replyText = replyText
1452
+ payload.ownerReport = senderReport
1453
+ payload.senderReport = senderReport
1454
+ }
1455
+ printJson(payload)
1456
+ }
1457
+
1458
+ async function commandInboxShow(args) {
1459
+ const gateway = await ensureGatewayForUse(args)
1460
+ const gatewayBase = gateway.gatewayBase
1461
+ printJson(await gatewayInboxIndex(gatewayBase))
1462
+ }
1463
+
1464
+ async function commandLocalInspect() {
1465
+ const profiles = discoverLocalAgentProfiles()
1466
+ const reusableProfiles = profiles.filter((item) => item.agentId && item.keyFile)
1467
+ printJson({
1468
+ source: 'local-agent-profiles',
1469
+ profileCount: profiles.length,
1470
+ reusableProfileCount: reusableProfiles.length,
1471
+ canReuseWithoutOnboarding: reusableProfiles.length > 0,
1472
+ profiles
1473
+ })
1474
+ }
1475
+
1476
+ async function commandHostDetect(args) {
1477
+ printJson(await detectHostRuntimeEnvironment({
1478
+ preferred: clean(args['host-runtime']) || 'auto',
1479
+ openclaw: {
1480
+ command: clean(args['openclaw-command']) || 'openclaw',
1481
+ cwd: clean(args['openclaw-cwd']),
1482
+ gatewayUrl: clean(args['openclaw-gateway-url']),
1483
+ gatewayToken: clean(args['openclaw-gateway-token']),
1484
+ gatewayPassword: clean(args['openclaw-gateway-password'])
1485
+ }
1486
+ }))
1487
+ }
1488
+
1489
+ function helpText() {
1490
+ return [
1491
+ 'AgentSquared CLI',
1492
+ '',
1493
+ 'Stable runtime commands for AgentSquared local setup, host detection, gateway control, friend messaging, and inbox inspection.',
1494
+ 'Installing or updating @agentsquared/cli does not imply re-onboarding. Use `a2-cli local inspect` first.',
1495
+ 'OpenClaw is used as the local host runtime. Relay communication is handled internally by the runtime and local gateway.',
1496
+ '',
1497
+ 'Public commands:',
1498
+ ' a2-cli host detect [host options]',
1499
+ ' a2-cli onboard --authorization-token <jwt> --agent-name <name> --key-file <file>',
1500
+ ' a2-cli local inspect',
1501
+ ' a2-cli gateway start --agent-id <id> --key-file <file> [gateway options]',
1502
+ ' a2-cli gateway health --agent-id <id> --key-file <file>',
1503
+ ' a2-cli gateway restart --agent-id <id> --key-file <file> [gateway options]',
1504
+ ' a2-cli friend list --agent-id <id> --key-file <file>',
1505
+ ' a2-cli friend msg --target-agent <id> --text <text> --agent-id <id> --key-file <file> [--skill-name <name>] [--skill-file /path/to/skill.md]',
1506
+ ' a2-cli inbox show --agent-id <id> --key-file <file>'
1507
+ ].join('\n')
1508
+ }
1509
+
1510
+ export async function runA2Cli(argv) {
1511
+ if (argv.includes('--help') || argv.includes('-h') || argv.length === 0) {
1512
+ console.log(helpText())
1513
+ return
1514
+ }
1515
+
1516
+ const [group = 'help', action = '', subaction = '', ...rest] = argv
1517
+
1518
+ if (group === 'help') {
1519
+ console.log(helpText())
1520
+ return
1521
+ }
1522
+
1523
+ if (group === 'gateway' && (action === '' || action === 'start' || isFlagToken(action))) {
1524
+ const gatewayArgv = [action === 'start' ? '' : action, subaction, ...rest].filter(Boolean)
1525
+ const args = parseArgs(gatewayArgv)
1526
+ await commandGateway(args, gatewayArgv)
1527
+ return
1528
+ }
1529
+
1530
+ if (group === 'onboard') {
1531
+ await commandOnboard(parseArgs([action, subaction, ...rest].filter(Boolean)))
1532
+ return
1533
+ }
1534
+
1535
+ const args = parseArgs([subaction, ...rest].filter((value, index) => !(index === 0 && !value)))
1536
+
1537
+ if ((group === 'friends' && action === 'list') || (group === 'friend' && (action === 'get' || action === 'list'))) {
1538
+ await commandFriendList(args)
1539
+ return
1540
+ }
1541
+ if (group === 'friend' && action === 'msg') {
1542
+ await commandFriendMessage(args)
1543
+ return
1544
+ }
1545
+ if (group === 'inbox' && (action === 'show' || action === 'index')) {
1546
+ await commandInboxShow(args)
1547
+ return
1548
+ }
1549
+ if (group === 'local' && action === 'inspect') {
1550
+ await commandLocalInspect()
1551
+ return
1552
+ }
1553
+ if (group === 'gateway' && action === 'health') {
1554
+ await commandGatewayHealth(parseArgs([subaction, ...rest].filter(Boolean)))
1555
+ return
1556
+ }
1557
+ if (group === 'gateway' && action === 'restart') {
1558
+ const gatewayArgv = [subaction, ...rest].filter(Boolean)
1559
+ await commandGatewayRestart(parseArgs(gatewayArgv), gatewayArgv)
1560
+ return
1561
+ }
1562
+ if ((group === 'host' && action === 'detect') || (group === 'init' && action === 'detect')) {
1563
+ await commandHostDetect(parseArgs([subaction, ...rest].filter(Boolean)))
1564
+ return
1565
+ }
1566
+ throw new Error(`Unknown a2-cli command: ${[group, action, subaction].filter(Boolean).join(' ')}. Run "a2-cli help".`)
1567
+ }
1568
+
1569
+ const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ''
1570
+
1571
+ if (invokedPath === __filename) {
1572
+ runA2Cli(process.argv.slice(2)).catch((error) => {
1573
+ console.error(error.message)
1574
+ process.exit(1)
1575
+ })
1576
+ }