@agentsquared/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { spawnSync } from 'node:child_process'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { defaultReceiptFile } from '../shared/paths.mjs'
7
+ import { currentRuntimeRevision } from '../gateway/state.mjs'
8
+ import { loadRuntimeKeyBundle, publicKeyFingerprint } from './keys.mjs'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = path.dirname(__filename)
12
+ const PACKAGE_ROOT = path.resolve(__dirname, '../..')
13
+ const PACKAGE_JSON_PATH = path.join(PACKAGE_ROOT, 'package.json')
14
+
15
+ function clean(value) {
16
+ return `${value ?? ''}`.trim()
17
+ }
18
+
19
+ function readJson(filePath) {
20
+ try {
21
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ function runGit(args = []) {
28
+ const result = spawnSync('git', args, {
29
+ cwd: PACKAGE_ROOT,
30
+ encoding: 'utf8'
31
+ })
32
+ if (result.status !== 0) {
33
+ return { ok: false, stdout: clean(result.stdout), stderr: clean(result.stderr) }
34
+ }
35
+ return { ok: true, stdout: clean(result.stdout), stderr: clean(result.stderr) }
36
+ }
37
+
38
+ function parseGatewayPort(gatewayBase = '') {
39
+ try {
40
+ return new URL(gatewayBase).port || ''
41
+ } catch {
42
+ return ''
43
+ }
44
+ }
45
+
46
+ function parseHumanName(agentId = '') {
47
+ const cleaned = clean(agentId)
48
+ const at = cleaned.lastIndexOf('@')
49
+ return at >= 0 ? cleaned.slice(at + 1) : ''
50
+ }
51
+
52
+ function latestStatusLabel(status = '') {
53
+ switch (clean(status)) {
54
+ case 'up-to-date':
55
+ return 'up to date'
56
+ case 'ahead':
57
+ return 'ahead of upstream'
58
+ case 'behind':
59
+ return 'behind upstream'
60
+ case 'diverged':
61
+ return 'diverged from upstream'
62
+ default:
63
+ return 'unknown'
64
+ }
65
+ }
66
+
67
+ function latestStatus() {
68
+ const upstream = runGit(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}'])
69
+ if (!upstream.ok || !clean(upstream.stdout)) {
70
+ return {
71
+ status: 'unknown',
72
+ detail: 'No upstream tracking branch was available from local git metadata.'
73
+ }
74
+ }
75
+ const counts = runGit(['rev-list', '--left-right', '--count', `HEAD...${clean(upstream.stdout)}`])
76
+ if (!counts.ok) {
77
+ return {
78
+ status: 'unknown',
79
+ detail: 'Upstream comparison was unavailable from local git metadata.'
80
+ }
81
+ }
82
+ const [aheadRaw, behindRaw] = clean(counts.stdout).split(/\s+/)
83
+ const ahead = Number.parseInt(aheadRaw ?? '0', 10) || 0
84
+ const behind = Number.parseInt(behindRaw ?? '0', 10) || 0
85
+ if (ahead === 0 && behind === 0) {
86
+ return { status: 'up-to-date', detail: `Local HEAD matches ${clean(upstream.stdout)}.` }
87
+ }
88
+ if (ahead > 0 && behind === 0) {
89
+ return { status: 'ahead', detail: `Local HEAD is ahead of ${clean(upstream.stdout)} by ${ahead} commit(s).` }
90
+ }
91
+ if (ahead === 0 && behind > 0) {
92
+ return { status: 'behind', detail: `Local HEAD is behind ${clean(upstream.stdout)} by ${behind} commit(s).` }
93
+ }
94
+ return { status: 'diverged', detail: `Local HEAD has diverged from ${clean(upstream.stdout)}.` }
95
+ }
96
+
97
+ export function currentRuntimeMetadata() {
98
+ const pkg = readJson(PACKAGE_JSON_PATH) ?? {}
99
+ const repo = runGit(['remote', 'get-url', 'origin'])
100
+ const commit = runGit(['rev-parse', '--short=12', 'HEAD'])
101
+ return {
102
+ packageName: clean(pkg.name) || '@agentsquared/cli',
103
+ packageVersion: clean(pkg.version) || 'unknown',
104
+ gitCommit: commit.ok ? clean(commit.stdout) : 'unknown',
105
+ repoUrl: repo.ok ? clean(repo.stdout) : 'https://github.com/AgentSquaredNet/agentsquared-cli.git',
106
+ runtimeRevision: currentRuntimeRevision(),
107
+ latest: latestStatus()
108
+ }
109
+ }
110
+
111
+ function receiptFilePath(keyFile, agentId) {
112
+ return defaultReceiptFile(keyFile, agentId)
113
+ }
114
+
115
+ export function loadReceiptForAgent({ keyFile = '', agentId = '' } = {}) {
116
+ const filePath = receiptFilePath(keyFile, agentId)
117
+ const receipt = readJson(filePath)
118
+ return {
119
+ filePath,
120
+ receipt
121
+ }
122
+ }
123
+
124
+ function summarizeUpdateCommits(previousGitCommit = '', currentGitCommit = '') {
125
+ const from = clean(previousGitCommit)
126
+ const to = clean(currentGitCommit)
127
+ if (!from || !to || from === to) {
128
+ return []
129
+ }
130
+ const log = runGit(['log', '--oneline', '--no-merges', `${from}..${to}`])
131
+ if (!log.ok || !clean(log.stdout)) {
132
+ return []
133
+ }
134
+ return clean(log.stdout)
135
+ .split('\n')
136
+ .map((line) => clean(line.replace(/^[0-9a-f]+\s+/, '')))
137
+ .filter(Boolean)
138
+ .slice(0, 6)
139
+ }
140
+
141
+ export function buildRuntimeUpdateSection({
142
+ previousState = null,
143
+ currentRuntime = currentRuntimeMetadata()
144
+ } = {}) {
145
+ const previous = {
146
+ packageVersion: clean(previousState?.runtimePackageVersion || previousState?.skillsPackageVersion) || 'unknown',
147
+ gitCommit: clean(previousState?.runtimeGitCommit || previousState?.skillsGitCommit) || 'unknown',
148
+ runtimeRevision: clean(previousState?.runtimeRevision) || 'unknown'
149
+ }
150
+ const current = {
151
+ packageVersion: clean(currentRuntime.packageVersion) || 'unknown',
152
+ gitCommit: clean(currentRuntime.gitCommit) || 'unknown',
153
+ runtimeRevision: clean(currentRuntime.runtimeRevision) || 'unknown'
154
+ }
155
+ const changed = previous.gitCommit !== 'unknown' && current.gitCommit !== 'unknown'
156
+ ? previous.gitCommit !== current.gitCommit
157
+ : previous.runtimeRevision !== 'unknown' && current.runtimeRevision !== 'unknown' && previous.runtimeRevision !== current.runtimeRevision
158
+ const initialInstall = previous.gitCommit === 'unknown' && previous.runtimeRevision === 'unknown'
159
+ return {
160
+ changed,
161
+ initialInstall,
162
+ from: previous,
163
+ to: current,
164
+ capabilityNotes: summarizeUpdateCommits(previous.gitCommit, current.gitCommit)
165
+ }
166
+ }
167
+
168
+ function extractRelayHealth(gatewayHealth = null) {
169
+ const relayControl = gatewayHealth?.relayControl ?? null
170
+ if (typeof relayControl?.ok === 'boolean') {
171
+ return {
172
+ ok: relayControl.ok,
173
+ detail: clean(relayControl.detail)
174
+ || (relayControl.ok
175
+ ? 'Relay control-plane handshake succeeded.'
176
+ : 'Relay control-plane handshake failed.')
177
+ }
178
+ }
179
+ const startupRelay = Boolean(gatewayHealth?.startupChecks?.relay?.ok)
180
+ const relayAddrs = Array.isArray(gatewayHealth?.relayAddrs) ? gatewayHealth.relayAddrs : []
181
+ return {
182
+ ok: startupRelay && relayAddrs.length > 0,
183
+ detail: startupRelay
184
+ ? (relayAddrs.length > 0 ? 'Relay startup check passed and relay-backed addresses are present.' : 'Relay startup check passed but no relay-backed addresses were reported yet.')
185
+ : clean(gatewayHealth?.startupChecks?.relay?.error) || 'Relay startup check did not pass.'
186
+ }
187
+ }
188
+
189
+ function extractHostRuntimeHealth(gatewayHealth = null, detectedHostRuntime = null) {
190
+ const resolved = clean(detectedHostRuntime?.resolved) || 'none'
191
+ const startupHost = Boolean(gatewayHealth?.startupChecks?.hostRuntime?.ok)
192
+ return {
193
+ ok: resolved === 'none' ? true : startupHost,
194
+ detail: resolved === 'none'
195
+ ? 'No host runtime adapter is active.'
196
+ : (startupHost
197
+ ? `${resolved} host adapter startup check passed.`
198
+ : clean(gatewayHealth?.startupChecks?.hostRuntime?.error) || `${resolved} host adapter startup check failed.`)
199
+ }
200
+ }
201
+
202
+ export function buildStandardRuntimeReport({
203
+ apiBase = 'https://api.agentsquared.net',
204
+ agentId = '',
205
+ keyFile = '',
206
+ detectedHostRuntime = null,
207
+ registration = null,
208
+ gateway = null,
209
+ gatewayHealth = null,
210
+ previousState = null
211
+ } = {}) {
212
+ const currentRuntime = currentRuntimeMetadata()
213
+ let keyBundle = null
214
+ if (keyFile) {
215
+ try {
216
+ keyBundle = loadRuntimeKeyBundle(keyFile)
217
+ } catch {
218
+ keyBundle = null
219
+ }
220
+ }
221
+ const receiptResult = loadReceiptForAgent({ keyFile, agentId })
222
+ const receipt = receiptResult.receipt ?? {}
223
+ const registrationFacts = registration ?? receipt ?? {}
224
+ const relayStatus = extractRelayHealth(gatewayHealth)
225
+ const hostStatus = extractHostRuntimeHealth(gatewayHealth, detectedHostRuntime)
226
+ const effectiveGatewayHealth = gatewayHealth ?? gateway?.health ?? null
227
+ return {
228
+ overall: {
229
+ humanId: registrationFacts.humanId ?? null,
230
+ humanName: clean(registrationFacts.humanName) || parseHumanName(registrationFacts.fullName || agentId),
231
+ agentId: clean(registrationFacts.fullName || agentId),
232
+ chainAgentId: clean(registrationFacts.chainAgentId),
233
+ publicKey: clean(registrationFacts.publicKey || keyBundle?.publicKey),
234
+ publicKeyFingerprint: keyBundle ? publicKeyFingerprint(keyBundle) : '',
235
+ relayApiBase: clean(apiBase) || 'https://api.agentsquared.net',
236
+ runtimeRepoUrl: clean(currentRuntime.repoUrl),
237
+ runtimePackageVersion: clean(currentRuntime.packageVersion),
238
+ runtimeGitCommit: clean(currentRuntime.gitCommit),
239
+ runtimeRevision: clean(currentRuntime.runtimeRevision),
240
+ latestStatus: currentRuntime.latest,
241
+ hostRuntime: clean(detectedHostRuntime?.resolved) || 'none'
242
+ },
243
+ runtimeUpdate: buildRuntimeUpdateSection({
244
+ previousState,
245
+ currentRuntime
246
+ }),
247
+ gatewayStatus: {
248
+ runtimeRevision: clean(currentRuntime.runtimeRevision),
249
+ runtimeGitCommit: clean(currentRuntime.gitCommit),
250
+ runtimePackageVersion: clean(currentRuntime.packageVersion),
251
+ latestStatus: currentRuntime.latest,
252
+ gatewayBase: clean(gateway?.gatewayBase || effectiveGatewayHealth?.gatewayBase),
253
+ listeningPort: parseGatewayPort(clean(gateway?.gatewayBase || effectiveGatewayHealth?.gatewayBase)),
254
+ peerId: clean(effectiveGatewayHealth?.peerId),
255
+ started: Boolean(gateway?.started ?? effectiveGatewayHealth),
256
+ relay: relayStatus,
257
+ hostRuntime: hostStatus,
258
+ startupChecks: effectiveGatewayHealth?.startupChecks ?? null
259
+ }
260
+ }
261
+ }
262
+
263
+ export function buildStandardRuntimeOwnerLines(report = {}) {
264
+ const overall = report.overall ?? {}
265
+ const runtimeUpdate = report.runtimeUpdate ?? {}
266
+ const gatewayStatus = report.gatewayStatus ?? {}
267
+ const lines = [
268
+ 'AgentSquared standard runtime report:',
269
+ `Overall: human=${clean(overall.humanName) || 'unknown'}${overall.humanId != null ? ` (id ${overall.humanId})` : ''}, agent=${clean(overall.agentId) || 'unknown'}, host mode=${clean(overall.hostRuntime) || 'none'}.`,
270
+ `Overall: public key=${clean(overall.publicKey) || 'unknown'}, fingerprint=${clean(overall.publicKeyFingerprint) || 'unknown'}.`,
271
+ `Overall: official relay=${clean(overall.relayApiBase) || 'unknown'}, runtime repo=${clean(overall.runtimeRepoUrl) || 'unknown'}.`,
272
+ `Overall: current package=${clean(overall.runtimePackageVersion) || 'unknown'}, git=${clean(overall.runtimeGitCommit) || 'unknown'}, runtimeRevision=${clean(overall.runtimeRevision) || 'unknown'}, version status=${latestStatusLabel(overall.latestStatus?.status)}.`
273
+ ]
274
+
275
+ if (runtimeUpdate.initialInstall) {
276
+ lines.push('Runtime update: this is the first standard runtime report for the current local AgentSquared setup, so there is no previous version to compare against.')
277
+ } else if (runtimeUpdate.changed) {
278
+ lines.push(
279
+ `Runtime update: upgraded from ${clean(runtimeUpdate.from?.gitCommit) || 'unknown'} (package ${clean(runtimeUpdate.from?.packageVersion) || 'unknown'}) to ${clean(runtimeUpdate.to?.gitCommit) || 'unknown'} (package ${clean(runtimeUpdate.to?.packageVersion) || 'unknown'}).`
280
+ )
281
+ if (Array.isArray(runtimeUpdate.capabilityNotes) && runtimeUpdate.capabilityNotes.length > 0) {
282
+ lines.push(`Runtime update: capability changes include ${runtimeUpdate.capabilityNotes.join('; ')}.`)
283
+ } else {
284
+ lines.push('Runtime update: local git metadata did not include a concise capability summary.')
285
+ }
286
+ } else {
287
+ lines.push(
288
+ `Runtime update: no version change was detected. Current package=${clean(runtimeUpdate.to?.packageVersion) || 'unknown'}, git=${clean(runtimeUpdate.to?.gitCommit) || 'unknown'}.`
289
+ )
290
+ }
291
+
292
+ lines.push(
293
+ `A2 gateway status: running on package=${clean(gatewayStatus.runtimePackageVersion) || 'unknown'}, git=${clean(gatewayStatus.runtimeGitCommit) || 'unknown'}, version status=${latestStatusLabel(gatewayStatus.latestStatus?.status)}.`
294
+ )
295
+ lines.push(
296
+ `A2 gateway status: listening port=${clean(gatewayStatus.listeningPort) || 'unknown'}, Peer ID=${clean(gatewayStatus.peerId) || 'unknown'}.`
297
+ )
298
+ lines.push(
299
+ `A2 gateway status: relay communication is ${gatewayStatus.relay?.ok ? 'healthy' : 'unhealthy'} (${clean(gatewayStatus.relay?.detail) || 'no detail'}), host communication is ${gatewayStatus.hostRuntime?.ok ? 'healthy' : 'unhealthy'} (${clean(gatewayStatus.hostRuntime?.detail) || 'no detail'}).`
300
+ )
301
+ return lines
302
+ }
@@ -0,0 +1,72 @@
1
+ function clean(value) {
2
+ return `${value ?? ''}`.trim()
3
+ }
4
+
5
+ function stripDelimitedBlock(text = '', begin = '', end = '') {
6
+ const start = text.indexOf(begin)
7
+ const finish = text.indexOf(end)
8
+ if (start < 0 || finish < start) {
9
+ return text
10
+ }
11
+ return `${text.slice(0, start)}${text.slice(finish + end.length)}`.trim()
12
+ }
13
+
14
+ function stripAgentSquaredTransportText(text = '') {
15
+ let value = clean(text)
16
+ if (!value) {
17
+ return value
18
+ }
19
+ value = stripDelimitedBlock(value, 'BEGIN_A2_OWNER_REQUEST', 'END_A2_OWNER_REQUEST')
20
+ const strippedLines = value
21
+ .split('\n')
22
+ .filter((line) => {
23
+ const normalized = clean(line)
24
+ if (!normalized) {
25
+ return true
26
+ }
27
+ if (normalized.includes('[AgentSquared]')) {
28
+ return false
29
+ }
30
+ if (normalized === 'This is an AgentSquared private agent message.') {
31
+ return false
32
+ }
33
+ if (normalized === 'Please read the AgentSquared official skill before sending or replying through AgentSquared.') {
34
+ return false
35
+ }
36
+ if (/^(From|To|Sent At \(UTC\)|Owner Request|Local Skill Snapshot):/i.test(normalized)) {
37
+ return false
38
+ }
39
+ if (normalized === 'Please reply to me for my owner.') {
40
+ return false
41
+ }
42
+ return true
43
+ })
44
+ value = strippedLines.join('\n').replace(/\n{3,}/g, '\n\n').trim()
45
+ return value
46
+ }
47
+
48
+ // This module is intentionally narrow.
49
+ // The main safety decision path lives in the host runtime triage prompt.
50
+ // Local code here is only a lightweight outbound redaction layer.
51
+ const SECRET_PATTERNS = [
52
+ /-----begin [a-z0-9 _-]*private key-----/i,
53
+ /\bsk-[a-z0-9]{16,}\b/i,
54
+ /\bghp_[a-z0-9]{20,}\b/i,
55
+ /\bxox[baprs]-[a-z0-9-]{10,}\b/i,
56
+ /\b(?:api|access|refresh|bearer|auth(?:orization)?) token\b/i,
57
+ /\bseed phrase\b/i,
58
+ /\bprivate key\b/i,
59
+ /\bm(nemonic)?\b.{0,20}\bphrase\b/i
60
+ ]
61
+
62
+ export function scrubOutboundText(text = '') {
63
+ let value = clean(text)
64
+ if (!value) {
65
+ return value
66
+ }
67
+ value = stripAgentSquaredTransportText(value)
68
+ for (const pattern of SECRET_PATTERNS) {
69
+ value = value.replace(pattern, '[REDACTED]')
70
+ }
71
+ return value
72
+ }
@@ -0,0 +1,155 @@
1
+ import path from 'node:path'
2
+
3
+ function clean(value) {
4
+ return `${value ?? ''}`.trim()
5
+ }
6
+
7
+ export function safeAgentId(value) {
8
+ return clean(value).replace(/[^a-zA-Z0-9_.-]+/g, '_')
9
+ }
10
+
11
+ export function resolveUserPath(inputPath) {
12
+ return path.resolve(`${inputPath ?? ''}`.replace(/^~(?=$|\/|\\)/, process.env.HOME || '~'))
13
+ }
14
+
15
+ export function resolveHostWorkspaceDir(detectedHostRuntime = null) {
16
+ return clean(
17
+ detectedHostRuntime?.workspaceDir
18
+ ?? detectedHostRuntime?.overviewStatus?.agents?.agents?.find?.((item) => clean(item?.workspaceDir))?.workspaceDir
19
+ ?? detectedHostRuntime?.overviewStatus?.agents?.agents?.[0]?.workspaceDir
20
+ )
21
+ }
22
+
23
+ export function resolveAgentSquaredDir(args = {}, detectedHostRuntime = null) {
24
+ const explicit = clean(args?.['agentsquared-dir'])
25
+ if (explicit) {
26
+ return resolveUserPath(explicit)
27
+ }
28
+ const workspaceDir = resolveHostWorkspaceDir(detectedHostRuntime)
29
+ if (workspaceDir) {
30
+ return path.join(resolveUserPath(workspaceDir), 'AgentSquared')
31
+ }
32
+ if (process.env.HOME) {
33
+ return path.join(process.env.HOME, '.openclaw', 'workspace', 'AgentSquared')
34
+ }
35
+ return path.join(process.cwd(), 'AgentSquared')
36
+ }
37
+
38
+ export function resolveAgentScopeDir(agentNameOrId, args = {}, detectedHostRuntime = null) {
39
+ const safeId = safeAgentId(agentNameOrId)
40
+ if (!safeId) {
41
+ throw new Error('agentNameOrId is required to derive the AgentSquared agent scope directory')
42
+ }
43
+ return path.join(resolveAgentSquaredDir(args, detectedHostRuntime), safeId)
44
+ }
45
+
46
+ export function inferAgentSquaredScopeFromArtifact(filePath) {
47
+ const resolved = resolveUserPath(filePath)
48
+ const name = path.basename(resolved)
49
+ const parent = path.dirname(resolved)
50
+ const grandparent = path.dirname(parent)
51
+
52
+ if ((name === 'runtime-key.json' || name === 'registration-receipt.json' || name === 'onboarding-summary.json') && path.basename(parent) === 'identity') {
53
+ return grandparent
54
+ }
55
+ if ((name === 'gateway.json' || name === 'gateway-peer.key' || name === 'gateway.log' || name === 'openclaw-device.json' || name === 'openclaw-device-auth.json') && path.basename(parent) === 'runtime') {
56
+ return grandparent
57
+ }
58
+ if ((name === 'index.json' || name === 'inbox.md') && path.basename(parent) === 'inbox') {
59
+ return grandparent
60
+ }
61
+ if (path.basename(parent) === 'entries' && path.basename(grandparent) === 'inbox') {
62
+ return path.dirname(grandparent)
63
+ }
64
+ if (name === 'AGENT_RELATIONSHIPS.md') {
65
+ return parent
66
+ }
67
+ return ''
68
+ }
69
+
70
+ export function resolveAgentScopeDirFromKeyFile(keyFile) {
71
+ const scopeDir = inferAgentSquaredScopeFromArtifact(keyFile)
72
+ if (!scopeDir || path.basename(resolveUserPath(keyFile)) !== 'runtime-key.json') {
73
+ throw new Error(`keyFile is not inside the AgentSquared multi-agent layout: ${resolveUserPath(keyFile)}`)
74
+ }
75
+ return scopeDir
76
+ }
77
+
78
+ export function identityDirForAgentScope(scopeDir) {
79
+ return path.join(resolveUserPath(scopeDir), 'identity')
80
+ }
81
+
82
+ export function runtimeDirForAgentScope(scopeDir) {
83
+ return path.join(resolveUserPath(scopeDir), 'runtime')
84
+ }
85
+
86
+ export function inboxDirForAgentScope(scopeDir) {
87
+ return path.join(resolveUserPath(scopeDir), 'inbox')
88
+ }
89
+
90
+ export function defaultRuntimeKeyFile(agentName, args = {}, detectedHostRuntime = null) {
91
+ return path.join(identityDirForAgentScope(resolveAgentScopeDir(agentName, args, detectedHostRuntime)), 'runtime-key.json')
92
+ }
93
+
94
+ function scopeDirForKeyAndAgent(keyFile, agentId = '') {
95
+ try {
96
+ return resolveAgentScopeDirFromKeyFile(keyFile)
97
+ } catch {
98
+ const safeId = safeAgentId(agentId)
99
+ if (!safeId) {
100
+ throw new Error(`Cannot derive AgentSquared scope directory from keyFile without agentId: ${resolveUserPath(keyFile)}`)
101
+ }
102
+ const keyDir = path.dirname(resolveUserPath(keyFile))
103
+ const identityDir = path.basename(keyDir) === 'identity' ? keyDir : path.join(keyDir, 'identity')
104
+ return path.dirname(identityDir)
105
+ }
106
+ }
107
+
108
+ export function defaultGatewayStateFile(keyFile, agentId) {
109
+ if (!keyFile || !agentId) {
110
+ throw new Error('keyFile and agentId are required to derive the gateway state file')
111
+ }
112
+ return path.join(runtimeDirForAgentScope(scopeDirForKeyAndAgent(keyFile, agentId)), 'gateway.json')
113
+ }
114
+
115
+ export function defaultPeerKeyFile(keyFile, agentId) {
116
+ if (!keyFile || !agentId) {
117
+ throw new Error('keyFile and agentId are required to derive the peer key file')
118
+ }
119
+ return path.join(runtimeDirForAgentScope(scopeDirForKeyAndAgent(keyFile, agentId)), 'gateway-peer.key')
120
+ }
121
+
122
+ export function defaultGatewayLogFile(keyFile, agentId) {
123
+ if (!keyFile || !agentId) {
124
+ throw new Error('keyFile and agentId are required to derive the gateway log file')
125
+ }
126
+ return path.join(runtimeDirForAgentScope(scopeDirForKeyAndAgent(keyFile, agentId)), 'gateway.log')
127
+ }
128
+
129
+ export function defaultOpenClawStateDir(keyFile, agentId) {
130
+ if (!keyFile || !agentId) {
131
+ throw new Error('keyFile and agentId are required to derive the OpenClaw state directory')
132
+ }
133
+ return runtimeDirForAgentScope(scopeDirForKeyAndAgent(keyFile, agentId))
134
+ }
135
+
136
+ export function defaultInboxDir(keyFile, agentId) {
137
+ if (!keyFile || !agentId) {
138
+ throw new Error('keyFile and agentId are required to derive the inbox directory')
139
+ }
140
+ return inboxDirForAgentScope(scopeDirForKeyAndAgent(keyFile, agentId))
141
+ }
142
+
143
+ export function defaultReceiptFile(keyFile, fullName = '') {
144
+ if (!keyFile) {
145
+ throw new Error('keyFile is required to derive the receipt file')
146
+ }
147
+ return path.join(identityDirForAgentScope(scopeDirForKeyAndAgent(keyFile, fullName)), 'registration-receipt.json')
148
+ }
149
+
150
+ export function defaultOnboardingSummaryFile(keyFile, fullName = '') {
151
+ if (!keyFile) {
152
+ throw new Error('keyFile is required to derive the onboarding summary file')
153
+ }
154
+ return path.join(identityDirForAgentScope(scopeDirForKeyAndAgent(keyFile, fullName)), 'onboarding-summary.json')
155
+ }
@@ -0,0 +1,43 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ export function parseArgs(argv) {
4
+ const out = { _: [] }
5
+ for (let index = 0; index < argv.length; index += 1) {
6
+ const value = argv[index]
7
+ if (value.startsWith('--')) {
8
+ const key = value.slice(2)
9
+ const next = argv[index + 1]
10
+ if (next && !next.startsWith('--')) {
11
+ out[key] = next
12
+ index += 1
13
+ } else {
14
+ out[key] = 'true'
15
+ }
16
+ continue
17
+ }
18
+ out._.push(value)
19
+ }
20
+ return out
21
+ }
22
+
23
+ export function parseList(value, fallback = []) {
24
+ const raw = (value ?? '').trim()
25
+ if (!raw) return [...fallback]
26
+ return raw.split(',').map((item) => item.trim()).filter(Boolean)
27
+ }
28
+
29
+ export function randomRequestId(prefix = 'req') {
30
+ return `${prefix}_${crypto.randomBytes(8).toString('hex')}`
31
+ }
32
+
33
+ export function utcNow() {
34
+ return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z')
35
+ }
36
+
37
+ export function requireArg(value, message) {
38
+ const trimmed = (value ?? '').trim()
39
+ if (!trimmed) {
40
+ throw new Error(message)
41
+ }
42
+ return trimmed
43
+ }
@@ -0,0 +1,96 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { promisify } from 'node:util'
3
+
4
+ const execFileAsync = promisify(execFile)
5
+
6
+ export const DEFAULT_USER_AGENT = 'curl/8.1.2'
7
+ const STATUS_MARKER = '__AGENTSQUARED_HTTP_STATUS__:'
8
+
9
+ function parseBody(text) {
10
+ const trimmed = text.trim()
11
+ return trimmed ? JSON.parse(trimmed) : {}
12
+ }
13
+
14
+ function buildStatusError(status, text, statusText = '') {
15
+ let data = {}
16
+ try {
17
+ data = parseBody(text)
18
+ } catch {
19
+ data = {}
20
+ }
21
+ const detail = data?.error?.message ?? (text.trim() || statusText || 'Request failed')
22
+ return new Error(`${status} ${detail}`)
23
+ }
24
+
25
+ function isStatusError(error) {
26
+ return typeof error?.message === 'string' && /^[1-5][0-9][0-9]\s/.test(error.message)
27
+ }
28
+
29
+ async function fetchJson(url, { method = 'GET', headers = {}, body } = {}) {
30
+ const response = await fetch(url, {
31
+ method,
32
+ headers,
33
+ body
34
+ })
35
+ const text = await response.text()
36
+ if (!response.ok) {
37
+ throw buildStatusError(response.status, text, response.statusText)
38
+ }
39
+ return parseBody(text)
40
+ }
41
+
42
+ async function curlJson(url, { method = 'GET', headers = {}, body } = {}) {
43
+ const args = [
44
+ '--silent',
45
+ '--show-error',
46
+ '--location',
47
+ '--connect-timeout', '10',
48
+ '--max-time', '60',
49
+ '--request', method,
50
+ ]
51
+ for (const [name, value] of Object.entries(headers)) {
52
+ args.push('--header', `${name}: ${value}`)
53
+ }
54
+ if (body !== undefined) {
55
+ args.push('--data-binary', body)
56
+ }
57
+ args.push('--write-out', `\n${STATUS_MARKER}%{http_code}`, url)
58
+ const { stdout } = await execFileAsync('curl', args, { maxBuffer: 1024 * 1024 * 4 })
59
+ const markerIndex = stdout.lastIndexOf(`\n${STATUS_MARKER}`)
60
+ if (markerIndex < 0) {
61
+ throw new Error('curl response did not include an HTTP status marker')
62
+ }
63
+ const text = stdout.slice(0, markerIndex)
64
+ const status = Number.parseInt(stdout.slice(markerIndex + 1 + STATUS_MARKER.length).trim(), 10)
65
+ if (!Number.isInteger(status)) {
66
+ throw new Error('curl response did not include a valid HTTP status code')
67
+ }
68
+ if (status < 200 || status >= 300) {
69
+ throw buildStatusError(status, text)
70
+ }
71
+ return parseBody(text)
72
+ }
73
+
74
+ export async function requestJson(url, { method = 'GET', headers = {}, payload } = {}) {
75
+ const normalizedHeaders = {
76
+ Accept: 'application/json',
77
+ 'User-Agent': DEFAULT_USER_AGENT,
78
+ ...headers
79
+ }
80
+ let body
81
+ if (payload !== undefined) {
82
+ body = JSON.stringify(payload)
83
+ if (!normalizedHeaders['Content-Type']) {
84
+ normalizedHeaders['Content-Type'] = 'application/json'
85
+ }
86
+ }
87
+
88
+ try {
89
+ return await fetchJson(url, { method, headers: normalizedHeaders, body })
90
+ } catch (error) {
91
+ if (isStatusError(error)) {
92
+ throw error
93
+ }
94
+ return curlJson(url, { method, headers: normalizedHeaders, body })
95
+ }
96
+ }