@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/LICENSE +21 -0
- package/README.md +420 -0
- package/a2_cli.mjs +1576 -0
- package/adapters/index.mjs +79 -0
- package/adapters/openclaw/adapter.mjs +1020 -0
- package/adapters/openclaw/cli.mjs +89 -0
- package/adapters/openclaw/detect.mjs +259 -0
- package/adapters/openclaw/helpers.mjs +827 -0
- package/adapters/openclaw/ws_client.mjs +740 -0
- package/bin/a2-cli.js +8 -0
- package/lib/conversation/policy.mjs +122 -0
- package/lib/conversation/store.mjs +223 -0
- package/lib/conversation/templates.mjs +419 -0
- package/lib/gateway/api.mjs +28 -0
- package/lib/gateway/inbox.mjs +344 -0
- package/lib/gateway/lifecycle.mjs +602 -0
- package/lib/gateway/runtime_state.mjs +388 -0
- package/lib/gateway/server.mjs +883 -0
- package/lib/gateway/state.mjs +175 -0
- package/lib/routing/agent_router.mjs +511 -0
- package/lib/runtime/executor.mjs +380 -0
- package/lib/runtime/keys.mjs +85 -0
- package/lib/runtime/report.mjs +302 -0
- package/lib/runtime/safety.mjs +72 -0
- package/lib/shared/paths.mjs +155 -0
- package/lib/shared/primitives.mjs +43 -0
- package/lib/transport/http_json.mjs +96 -0
- package/lib/transport/libp2p.mjs +397 -0
- package/lib/transport/peer_session.mjs +857 -0
- package/lib/transport/relay_http.mjs +110 -0
- package/package.json +53 -0
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
|
+
}
|