@axiom-lattice/cli-a2a 0.1.1
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/.turbo/turbo-build.log +51 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +201 -0
- package/README.md +290 -0
- package/__tests__/opencode-executor.test.ts +159 -0
- package/agents/opencode-example/config.json +64 -0
- package/dist/bridge-7ZUDKCZT.mjs +271 -0
- package/dist/bridge-7ZUDKCZT.mjs.map +1 -0
- package/dist/chunk-35NFMGMS.mjs +27 -0
- package/dist/chunk-35NFMGMS.mjs.map +1 -0
- package/dist/chunk-G7AGL2QA.mjs +284 -0
- package/dist/chunk-G7AGL2QA.mjs.map +1 -0
- package/dist/chunk-LXL47XMZ.mjs +43 -0
- package/dist/chunk-LXL47XMZ.mjs.map +1 -0
- package/dist/chunk-NQIDRU47.mjs +178 -0
- package/dist/chunk-NQIDRU47.mjs.map +1 -0
- package/dist/chunk-VSZ3DACI.mjs +179 -0
- package/dist/chunk-VSZ3DACI.mjs.map +1 -0
- package/dist/chunk-VZEH3EPJ.mjs +56 -0
- package/dist/chunk-VZEH3EPJ.mjs.map +1 -0
- package/dist/chunk-WNCDOYZS.mjs +187 -0
- package/dist/chunk-WNCDOYZS.mjs.map +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1562 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +293 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/executor-OWPVDUXH.mjs +11 -0
- package/dist/executor-OWPVDUXH.mjs.map +1 -0
- package/dist/executor-RVBGAWUF.mjs +11 -0
- package/dist/executor-RVBGAWUF.mjs.map +1 -0
- package/dist/executor-XWHWUVQ3.mjs +11 -0
- package/dist/executor-XWHWUVQ3.mjs.map +1 -0
- package/dist/executors-QIIKBUMJ.mjs +15 -0
- package/dist/executors-QIIKBUMJ.mjs.map +1 -0
- package/dist/index.d.mts +295 -0
- package/dist/index.d.ts +295 -0
- package/dist/index.js +942 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +44 -0
- package/dist/index.mjs.map +1 -0
- package/jest.config.js +16 -0
- package/package.json +63 -0
- package/src/bridge.ts +355 -0
- package/src/cli.ts +384 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +16 -0
- package/src/config/loader.ts +121 -0
- package/src/config/types.ts +163 -0
- package/src/executors/claude/client.ts +138 -0
- package/src/executors/claude/executor.ts +111 -0
- package/src/executors/codex/client.ts +139 -0
- package/src/executors/codex/executor.ts +118 -0
- package/src/executors/events.ts +117 -0
- package/src/executors/index.ts +54 -0
- package/src/executors/opencode/client.ts +149 -0
- package/src/executors/opencode/executor.ts +112 -0
- package/src/index.ts +54 -0
- package/src/logger.ts +78 -0
- package/src/server/agent-card.ts +51 -0
- package/src/server/index.ts +124 -0
- package/tsconfig.json +21 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI A2A Gateway — Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @axiom-lattice/cli-a2a --provider opencode --config agents/example.json
|
|
7
|
+
* npx @axiom-lattice/cli-a2a --provider opencode --port 3000 --opencode-url http://localhost:4096
|
|
8
|
+
*
|
|
9
|
+
* Run with --help for full usage.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parseArgs } from 'node:util';
|
|
13
|
+
import { dirname, resolve } from 'node:path';
|
|
14
|
+
import { resolveConfig } from './config/loader.js';
|
|
15
|
+
import type { AgentConfig, ProviderType } from './config/types.js';
|
|
16
|
+
import { createA2AServer } from './server/index.js';
|
|
17
|
+
import { logger, LogLevel } from './logger.js';
|
|
18
|
+
|
|
19
|
+
const log = logger.child('cli');
|
|
20
|
+
|
|
21
|
+
// ─── Version ────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const PKG_VERSION = '0.1.0';
|
|
24
|
+
|
|
25
|
+
// ─── Usage ──────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function printUsage(): void {
|
|
28
|
+
console.log(`
|
|
29
|
+
Usage: cli-a2a [options]
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--provider <name> Provider: opencode | codex | claude
|
|
33
|
+
--config, -c <path> Path to agent JSON config file
|
|
34
|
+
--port, -p <number> A2A server port (default: 3000)
|
|
35
|
+
--hostname <addr> Bind address (default: 0.0.0.0)
|
|
36
|
+
--advertise-host <host> Hostname for agent card URLs
|
|
37
|
+
--agent-name <name> Agent display name
|
|
38
|
+
--agent-description <desc> Agent description
|
|
39
|
+
--log-level <level> debug | info | warn | error (default: info)
|
|
40
|
+
|
|
41
|
+
Gateway Registration:
|
|
42
|
+
--register-url <url> Gateway URL to register this agent with
|
|
43
|
+
--register-key <key> API key for gateway authentication
|
|
44
|
+
--unregister-on-shutdown Remove agent from gateway on exit
|
|
45
|
+
|
|
46
|
+
Bridge (NAT traversal):
|
|
47
|
+
--bridge-url <url> Gateway WebSocket bridge (ws://gateway:8080/api/a2a/bridge)
|
|
48
|
+
--bridge-key <key> API key for bridge authentication
|
|
49
|
+
|
|
50
|
+
OpenCode Options:
|
|
51
|
+
--opencode-url <url> OpenCode server URL (default: http://localhost:4096)
|
|
52
|
+
--directory, -d <path> Project directory
|
|
53
|
+
--model, -m <provider/model> LLM model (e.g. anthropic/claude-sonnet-4)
|
|
54
|
+
--agent <name> OpenCode agent preset
|
|
55
|
+
|
|
56
|
+
Codex Options:
|
|
57
|
+
--codex-model <model> Codex model ID
|
|
58
|
+
--codex-api-key <key> OpenAI API key
|
|
59
|
+
|
|
60
|
+
Claude Options:
|
|
61
|
+
--claude-model <model> Claude Code model ID
|
|
62
|
+
--claude-api-key <key> Anthropic API key
|
|
63
|
+
|
|
64
|
+
--help, -h Show this help
|
|
65
|
+
--version, -v Show version
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
npx @axiom-lattice/cli-a2a --provider opencode
|
|
69
|
+
npx @axiom-lattice/cli-a2a --provider opencode --port 3001 --opencode-url http://localhost:4096
|
|
70
|
+
npx @axiom-lattice/cli-a2a --config ./my-agent.json
|
|
71
|
+
npx @axiom-lattice/cli-a2a --provider opencode --model anthropic/claude-sonnet-4
|
|
72
|
+
npx @axiom-lattice/cli-a2a --provider opencode --register-url http://gateway:8080 --register-key sk-xxx --unregister-on-shutdown
|
|
73
|
+
npx @axiom-lattice/cli-a2a --provider opencode --bridge-url ws://gateway:8080/api/a2a/bridge --bridge-key sk-xxx
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── CLI Args ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function parseCliArgs(): {
|
|
80
|
+
configPath?: string;
|
|
81
|
+
overrides: Partial<AgentConfig>;
|
|
82
|
+
registerUrl?: string;
|
|
83
|
+
registerKey?: string;
|
|
84
|
+
unregisterOnShutdown: boolean;
|
|
85
|
+
bridgeUrl?: string;
|
|
86
|
+
bridgeKey?: string;
|
|
87
|
+
} {
|
|
88
|
+
const { values } = parseArgs({
|
|
89
|
+
options: {
|
|
90
|
+
provider: { type: 'string' },
|
|
91
|
+
config: { type: 'string', short: 'c' },
|
|
92
|
+
port: { type: 'string', short: 'p' },
|
|
93
|
+
hostname: { type: 'string' },
|
|
94
|
+
'advertise-host': { type: 'string' },
|
|
95
|
+
'agent-name': { type: 'string' },
|
|
96
|
+
'agent-description':{ type: 'string' },
|
|
97
|
+
'opencode-url': { type: 'string' },
|
|
98
|
+
directory: { type: 'string', short: 'd' },
|
|
99
|
+
model: { type: 'string', short: 'm' },
|
|
100
|
+
agent: { type: 'string' },
|
|
101
|
+
'codex-model': { type: 'string' },
|
|
102
|
+
'codex-api-key': { type: 'string' },
|
|
103
|
+
'claude-model': { type: 'string' },
|
|
104
|
+
'claude-api-key': { type: 'string' },
|
|
105
|
+
'log-level': { type: 'string' },
|
|
106
|
+
'register-url': { type: 'string' },
|
|
107
|
+
'register-key': { type: 'string' },
|
|
108
|
+
'unregister-on-shutdown': { type: 'boolean' },
|
|
109
|
+
'bridge-url': { type: 'string' },
|
|
110
|
+
'bridge-key': { type: 'string' },
|
|
111
|
+
help: { type: 'boolean', short: 'h' },
|
|
112
|
+
version: { type: 'boolean', short: 'v' },
|
|
113
|
+
},
|
|
114
|
+
strict: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (values.help) {
|
|
118
|
+
printUsage();
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (values.version) {
|
|
123
|
+
console.log(PKG_VERSION);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const overrides: Partial<AgentConfig> = {};
|
|
128
|
+
|
|
129
|
+
// Provider
|
|
130
|
+
if (values.provider) {
|
|
131
|
+
overrides.provider = values.provider as ProviderType;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Server
|
|
135
|
+
if (values.port || values.hostname || values['advertise-host']) {
|
|
136
|
+
overrides.server = {};
|
|
137
|
+
if (values.port) overrides.server.port = parseInt(values.port as string, 10);
|
|
138
|
+
if (values.hostname) overrides.server.hostname = values.hostname as string;
|
|
139
|
+
if (values['advertise-host']) overrides.server.advertiseHost = values['advertise-host'] as string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Agent card
|
|
143
|
+
const agentCardOverrides: Partial<AgentConfig['agentCard']> = {};
|
|
144
|
+
if (values['agent-name']) agentCardOverrides.name = values['agent-name'] as string;
|
|
145
|
+
if (values['agent-description']) agentCardOverrides.description = values['agent-description'] as string;
|
|
146
|
+
if (Object.keys(agentCardOverrides).length > 0) {
|
|
147
|
+
overrides.agentCard = agentCardOverrides as AgentConfig['agentCard'];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// OpenCode
|
|
151
|
+
const hasOpenCode = !!(values['opencode-url'] || values.directory || values.model || values.agent);
|
|
152
|
+
if (hasOpenCode) {
|
|
153
|
+
overrides.opencode = {};
|
|
154
|
+
if (values['opencode-url']) overrides.opencode.baseUrl = values['opencode-url'] as string;
|
|
155
|
+
if (values.directory) overrides.opencode.projectDirectory = values.directory as string;
|
|
156
|
+
if (values.model) overrides.opencode.model = values.model as string;
|
|
157
|
+
if (values.agent) overrides.opencode.agent = values.agent as string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Codex
|
|
161
|
+
const hasCodex = !!(values['codex-model'] || values['codex-api-key']);
|
|
162
|
+
if (hasCodex) {
|
|
163
|
+
overrides.codex = {};
|
|
164
|
+
if (values['codex-model']) overrides.codex.model = values['codex-model'] as string;
|
|
165
|
+
if (values['codex-api-key']) overrides.codex.apiKey = values['codex-api-key'] as string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Claude
|
|
169
|
+
const hasClaude = !!(values['claude-model'] || values['claude-api-key']);
|
|
170
|
+
if (hasClaude) {
|
|
171
|
+
overrides.claude = {};
|
|
172
|
+
if (values['claude-model']) overrides.claude.model = values['claude-model'] as string;
|
|
173
|
+
if (values['claude-api-key']) overrides.claude.apiKey = values['claude-api-key'] as string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Logging
|
|
177
|
+
if (values['log-level']) {
|
|
178
|
+
overrides.logging = { level: values['log-level'] as 'debug' | 'info' | 'warn' | 'error' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const configPath = (values.config) as string | undefined;
|
|
182
|
+
const registerUrl = values['register-url'] as string | undefined;
|
|
183
|
+
const registerKey = values['register-key'] as string | undefined;
|
|
184
|
+
const unregisterOnShutdown = (values['unregister-on-shutdown'] as boolean) ?? false;
|
|
185
|
+
const bridgeUrl = values['bridge-url'] as string | undefined;
|
|
186
|
+
const bridgeKey = values['bridge-key'] as string | undefined;
|
|
187
|
+
return { configPath, overrides, registerUrl, registerKey, unregisterOnShutdown, bridgeUrl, bridgeKey };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Gateway Registration ───────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
async function registerWithGateway(
|
|
193
|
+
registerUrl: string,
|
|
194
|
+
registerKey: string,
|
|
195
|
+
config: Required<AgentConfig>,
|
|
196
|
+
agentCardUrl: string,
|
|
197
|
+
): Promise<{ assistantId: string; agentCardUrl: string } | null> {
|
|
198
|
+
const body = {
|
|
199
|
+
name: config.agentCard.name,
|
|
200
|
+
description: config.agentCard.description,
|
|
201
|
+
graphDefinition: {
|
|
202
|
+
type: 'a2a_remote',
|
|
203
|
+
key: `a2a-${config.provider}-${config.agentCard.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
|
|
204
|
+
name: config.agentCard.name,
|
|
205
|
+
description: config.agentCard.description,
|
|
206
|
+
prompt: config.agentCard.description,
|
|
207
|
+
agentCardUrl,
|
|
208
|
+
timeout: config.timeouts.prompt,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const url = registerUrl.replace(/\/$/, '') + '/api/assistants';
|
|
214
|
+
log.info('Registering with gateway', { url, agentCardUrl });
|
|
215
|
+
|
|
216
|
+
const resp = await fetch(url, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
headers: {
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
'Authorization': `Bearer ${registerKey}`,
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify(body),
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (!resp.ok) {
|
|
226
|
+
const text = await resp.text().catch(() => '');
|
|
227
|
+
log.error('Gateway registration failed', { status: resp.status, body: text });
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await resp.json() as { data?: { id: string } };
|
|
232
|
+
const assistantId = result.data?.id ?? body.graphDefinition.key;
|
|
233
|
+
|
|
234
|
+
log.info('Registered with gateway', { assistantId, agentCardUrl });
|
|
235
|
+
console.log(`Registered as agent: ${assistantId}`);
|
|
236
|
+
console.log(`Agent card: ${agentCardUrl}`);
|
|
237
|
+
|
|
238
|
+
return { assistantId, agentCardUrl };
|
|
239
|
+
} catch (err) {
|
|
240
|
+
log.error('Gateway registration error', { error: (err as Error).message });
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function unregisterFromGateway(
|
|
246
|
+
registerUrl: string,
|
|
247
|
+
registerKey: string,
|
|
248
|
+
assistantId: string,
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
const url = `${registerUrl.replace(/\/$/, '')}/api/assistants/${assistantId}`;
|
|
252
|
+
log.info('Unregistering from gateway', { assistantId });
|
|
253
|
+
|
|
254
|
+
const resp = await fetch(url, {
|
|
255
|
+
method: 'DELETE',
|
|
256
|
+
headers: {
|
|
257
|
+
'Authorization': `Bearer ${registerKey}`,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!resp.ok && resp.status !== 404) {
|
|
262
|
+
log.warn('Gateway unregistration failed', { status: resp.status });
|
|
263
|
+
} else {
|
|
264
|
+
log.info('Unregistered from gateway', { assistantId });
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
log.warn('Gateway unregistration error', { error: (err as Error).message });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
async function main(): Promise<void> {
|
|
274
|
+
const { configPath, overrides, registerUrl, registerKey, unregisterOnShutdown, bridgeUrl, bridgeKey } = parseCliArgs();
|
|
275
|
+
|
|
276
|
+
// Resolve config
|
|
277
|
+
const config = resolveConfig(configPath, overrides);
|
|
278
|
+
|
|
279
|
+
// Set configDir for relative path resolution
|
|
280
|
+
if (configPath) {
|
|
281
|
+
config.configDir = dirname(resolve(configPath));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Apply log level
|
|
285
|
+
const levelStr = config.logging.level ?? 'info';
|
|
286
|
+
const levelMap: Record<string, LogLevel> = {
|
|
287
|
+
debug: LogLevel.DEBUG,
|
|
288
|
+
info: LogLevel.INFO,
|
|
289
|
+
warn: LogLevel.WARN,
|
|
290
|
+
error: LogLevel.ERROR,
|
|
291
|
+
};
|
|
292
|
+
logger.setLevel(levelMap[levelStr] ?? LogLevel.INFO);
|
|
293
|
+
|
|
294
|
+
if (!configPath) {
|
|
295
|
+
log.info('No --config provided — running with defaults + CLI args.');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
log.info('Starting CLI A2A Gateway', {
|
|
299
|
+
provider: config.provider,
|
|
300
|
+
config: configPath ?? '(defaults)',
|
|
301
|
+
agent: config.agentCard.name,
|
|
302
|
+
port: config.server?.port,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Register executors
|
|
306
|
+
const { registerExecutor } = await import('./executors/index.js');
|
|
307
|
+
const { createOpenCodeExecutor } = await import('./executors/opencode/executor.js');
|
|
308
|
+
const { createCodexExecutor } = await import('./executors/codex/executor.js');
|
|
309
|
+
const { createClaudeExecutor } = await import('./executors/claude/executor.js');
|
|
310
|
+
registerExecutor('opencode', createOpenCodeExecutor);
|
|
311
|
+
registerExecutor('codex', createCodexExecutor);
|
|
312
|
+
registerExecutor('claude', createClaudeExecutor);
|
|
313
|
+
|
|
314
|
+
// Start server
|
|
315
|
+
const server = await createA2AServer(config);
|
|
316
|
+
|
|
317
|
+
const listenPort = config.server?.port ?? 3000;
|
|
318
|
+
const agentId = `a2a-${config.provider}-${listenPort}`;
|
|
319
|
+
|
|
320
|
+
// Connect bridge to gateway (NAT traversal)
|
|
321
|
+
let bridgeClient: { start(): Promise<void>; stop(): void } | null = null;
|
|
322
|
+
if (bridgeUrl && bridgeKey) {
|
|
323
|
+
const { BridgeClient } = await import('./bridge.js');
|
|
324
|
+
const agentCardUrl = `${config.server?.advertiseProtocol ?? 'http'}://${config.server?.advertiseHost ?? 'localhost'}:${listenPort}/.well-known/agent-card.json`;
|
|
325
|
+
|
|
326
|
+
bridgeClient = new BridgeClient({
|
|
327
|
+
bridgeUrl: bridgeUrl.replace(/^http/, 'ws'),
|
|
328
|
+
apiKey: bridgeKey,
|
|
329
|
+
agentId,
|
|
330
|
+
agentCard: {
|
|
331
|
+
name: config.agentCard.name,
|
|
332
|
+
description: config.agentCard.description,
|
|
333
|
+
url: agentCardUrl,
|
|
334
|
+
},
|
|
335
|
+
localBaseUrl: `http://localhost:${listenPort}`,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
bridgeClient.start().then(() => {
|
|
339
|
+
log.info('Bridge connected — agent reachable via gateway proxy');
|
|
340
|
+
console.log(`Bridge: ws://gateway → localhost:${listenPort}`);
|
|
341
|
+
}).catch((err: Error) => {
|
|
342
|
+
log.error('Bridge connection failed', { error: err.message });
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Register with gateway (HTTP-based self-registration)
|
|
347
|
+
let registeredAssistantId: string | null = null;
|
|
348
|
+
if (registerUrl && registerKey) {
|
|
349
|
+
// When bridge is active, the agent card URL must point to the gateway proxy
|
|
350
|
+
// so the orchestrator reaches the agent through the WebSocket bridge, not directly.
|
|
351
|
+
const agentCardUrl = bridgeUrl
|
|
352
|
+
? `${registerUrl.replace(/\/$/, '')}/api/a2a/bridge/proxy/${agentId}/.well-known/agent-card.json`
|
|
353
|
+
: `${config.server?.advertiseProtocol ?? 'http'}://${config.server?.advertiseHost ?? 'localhost'}:${listenPort}/.well-known/agent-card.json`;
|
|
354
|
+
|
|
355
|
+
const reg = await registerWithGateway(
|
|
356
|
+
registerUrl,
|
|
357
|
+
registerKey,
|
|
358
|
+
config,
|
|
359
|
+
agentCardUrl,
|
|
360
|
+
);
|
|
361
|
+
if (reg) {
|
|
362
|
+
registeredAssistantId = reg.assistantId;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Graceful shutdown
|
|
367
|
+
const shutdown = async (signal: string) => {
|
|
368
|
+
log.info(`${signal} received, shutting down...`);
|
|
369
|
+
bridgeClient?.stop();
|
|
370
|
+
if (registeredAssistantId && registerUrl && registerKey && unregisterOnShutdown) {
|
|
371
|
+
await unregisterFromGateway(registerUrl, registerKey, registeredAssistantId);
|
|
372
|
+
}
|
|
373
|
+
await server.shutdown();
|
|
374
|
+
process.exit(0);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
378
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
main().catch((err) => {
|
|
382
|
+
log.error('Fatal error', { error: (err as Error).message, stack: (err as Error).stack });
|
|
383
|
+
process.exit(1);
|
|
384
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration values.
|
|
3
|
+
*
|
|
4
|
+
* Every field has a sensible default so the agent runs without a config file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentConfig } from './types.js';
|
|
8
|
+
|
|
9
|
+
function withDefaults<T extends Record<string, unknown>>(
|
|
10
|
+
partial: Partial<T>,
|
|
11
|
+
defaults: Required<T>,
|
|
12
|
+
): Required<T> {
|
|
13
|
+
const result = { ...defaults } as Record<string, unknown>;
|
|
14
|
+
for (const key of Object.keys(partial) as Array<keyof T>) {
|
|
15
|
+
const val = partial[key] as unknown;
|
|
16
|
+
if (val !== undefined && val !== null) {
|
|
17
|
+
if (
|
|
18
|
+
typeof val === 'object' &&
|
|
19
|
+
!Array.isArray(val) &&
|
|
20
|
+
typeof defaults[key] === 'object' &&
|
|
21
|
+
!Array.isArray(defaults[key])
|
|
22
|
+
) {
|
|
23
|
+
result[key as string] = {
|
|
24
|
+
...(defaults[key] as Record<string, unknown>),
|
|
25
|
+
...(val as Record<string, unknown>),
|
|
26
|
+
};
|
|
27
|
+
} else {
|
|
28
|
+
result[key as string] = val;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result as unknown as Required<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ResolvedConfig = Omit<Required<AgentConfig>, 'opencode' | 'codex' | 'claude' | 'configDir'> & {
|
|
36
|
+
opencode: Required<Required<AgentConfig>['opencode']>;
|
|
37
|
+
codex: Required<Required<AgentConfig>['codex']>;
|
|
38
|
+
claude: Required<Required<AgentConfig>['claude']>;
|
|
39
|
+
configDir: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const DEFAULTS: Readonly<ResolvedConfig> = Object.freeze({
|
|
43
|
+
provider: 'opencode' as const,
|
|
44
|
+
agentCard: {
|
|
45
|
+
name: 'CLI A2A Agent',
|
|
46
|
+
description: 'A multi-provider A2A agent driven by local CLI coding tools.',
|
|
47
|
+
protocolVersion: '0.3.0',
|
|
48
|
+
version: '0.1.0',
|
|
49
|
+
skills: [],
|
|
50
|
+
defaultInputModes: ['text'],
|
|
51
|
+
defaultOutputModes: ['text'],
|
|
52
|
+
streaming: true,
|
|
53
|
+
pushNotifications: false,
|
|
54
|
+
},
|
|
55
|
+
server: {
|
|
56
|
+
port: 3000,
|
|
57
|
+
hostname: '0.0.0.0',
|
|
58
|
+
advertiseHost: 'localhost',
|
|
59
|
+
advertiseProtocol: 'http',
|
|
60
|
+
},
|
|
61
|
+
session: {
|
|
62
|
+
titlePrefix: 'A2A Session',
|
|
63
|
+
reuseByContext: true,
|
|
64
|
+
ttl: 3_600_000,
|
|
65
|
+
cleanupInterval: 300_000,
|
|
66
|
+
},
|
|
67
|
+
features: {
|
|
68
|
+
autoApprovePermissions: true,
|
|
69
|
+
autoAnswerQuestions: true,
|
|
70
|
+
streamArtifactChunks: false,
|
|
71
|
+
enablePollingFallback: true,
|
|
72
|
+
},
|
|
73
|
+
timeouts: {
|
|
74
|
+
prompt: 600_000,
|
|
75
|
+
pollingInterval: 2_000,
|
|
76
|
+
healthCheck: 30_000,
|
|
77
|
+
},
|
|
78
|
+
logging: {
|
|
79
|
+
level: 'info',
|
|
80
|
+
},
|
|
81
|
+
opencode: {
|
|
82
|
+
baseUrl: 'http://localhost:4096',
|
|
83
|
+
projectDirectory: '',
|
|
84
|
+
model: '',
|
|
85
|
+
agent: '',
|
|
86
|
+
systemPrompt: '',
|
|
87
|
+
systemPromptMode: 'append',
|
|
88
|
+
contextFile: 'context.md',
|
|
89
|
+
contextPrompt: '',
|
|
90
|
+
},
|
|
91
|
+
codex: {
|
|
92
|
+
cliPath: 'codex',
|
|
93
|
+
workdir: process.cwd(),
|
|
94
|
+
model: '',
|
|
95
|
+
apiKey: '',
|
|
96
|
+
},
|
|
97
|
+
claude: {
|
|
98
|
+
cliPath: 'claude',
|
|
99
|
+
workdir: process.cwd(),
|
|
100
|
+
model: '',
|
|
101
|
+
apiKey: '',
|
|
102
|
+
},
|
|
103
|
+
configDir: undefined,
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
|
+
} as any);
|
|
106
|
+
|
|
107
|
+
export function resolveDefaults(overrides: Partial<AgentConfig>): Required<AgentConfig> {
|
|
108
|
+
return withDefaults(overrides, DEFAULTS) as unknown as Required<AgentConfig>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { resolveConfig, loadConfigFile } from './loader.js';
|
|
2
|
+
export { resolveDefaults } from './defaults.js';
|
|
3
|
+
export type {
|
|
4
|
+
AgentConfig,
|
|
5
|
+
ProviderType,
|
|
6
|
+
AgentCardConfig,
|
|
7
|
+
SkillConfig,
|
|
8
|
+
ServerConfig,
|
|
9
|
+
SessionConfig,
|
|
10
|
+
FeatureFlags,
|
|
11
|
+
TimeoutConfig,
|
|
12
|
+
LoggingConfig,
|
|
13
|
+
OpenCodeConfig,
|
|
14
|
+
CodexConfig,
|
|
15
|
+
ClaudeCodeConfig,
|
|
16
|
+
} from './types.js';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader — reads JSON config files and resolves the final config
|
|
3
|
+
* by merging layers: defaults ← config file ← env overrides ← CLI args.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname } from 'node:path';
|
|
8
|
+
import { resolveDefaults } from './defaults.js';
|
|
9
|
+
import type { AgentConfig } from './types.js';
|
|
10
|
+
import { logger } from '../logger.js';
|
|
11
|
+
|
|
12
|
+
const log = logger.child('config');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read and parse a JSON config file.
|
|
16
|
+
*/
|
|
17
|
+
export function loadConfigFile(filePath: string): Partial<AgentConfig> {
|
|
18
|
+
const absPath = resolve(filePath);
|
|
19
|
+
if (!existsSync(absPath)) {
|
|
20
|
+
throw new Error(`Config file not found: ${absPath}`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const raw = readFileSync(absPath, 'utf-8');
|
|
24
|
+
return JSON.parse(raw) as Partial<AgentConfig>;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const msg = err instanceof SyntaxError
|
|
27
|
+
? `Invalid JSON in config file: ${err.message}`
|
|
28
|
+
: `Failed to read config file: ${(err as Error).message}`;
|
|
29
|
+
throw new Error(msg);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load env overrides for provider configs.
|
|
35
|
+
*/
|
|
36
|
+
function loadEnvOverrides(): Partial<AgentConfig> {
|
|
37
|
+
const overrides: Partial<AgentConfig> = {
|
|
38
|
+
opencode: {},
|
|
39
|
+
codex: {},
|
|
40
|
+
claude: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// OpenCode
|
|
44
|
+
if (process.env.OPENCODE_URL) overrides.opencode!.baseUrl = process.env.OPENCODE_URL;
|
|
45
|
+
if (process.env.OPENCODE_DIRECTORY) overrides.opencode!.projectDirectory = process.env.OPENCODE_DIRECTORY;
|
|
46
|
+
if (process.env.OPENCODE_MODEL) overrides.opencode!.model = process.env.OPENCODE_MODEL;
|
|
47
|
+
if (process.env.OPENCODE_AGENT) overrides.opencode!.agent = process.env.OPENCODE_AGENT;
|
|
48
|
+
if (process.env.OPENCODE_SYSTEM_PROMPT) overrides.opencode!.systemPrompt = process.env.OPENCODE_SYSTEM_PROMPT;
|
|
49
|
+
|
|
50
|
+
// Codex
|
|
51
|
+
if (process.env.OPENAI_API_KEY) overrides.codex!.apiKey = process.env.OPENAI_API_KEY;
|
|
52
|
+
if (process.env.CODEX_MODEL) overrides.codex!.model = process.env.CODEX_MODEL;
|
|
53
|
+
|
|
54
|
+
// Claude Code
|
|
55
|
+
if (process.env.ANTHROPIC_API_KEY) overrides.claude!.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
56
|
+
if (process.env.CLAUDE_MODEL) overrides.claude!.model = process.env.CLAUDE_MODEL;
|
|
57
|
+
|
|
58
|
+
return overrides;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve full config from defaults + file + env.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveConfig(
|
|
65
|
+
configPath?: string,
|
|
66
|
+
cliOverrides?: Partial<AgentConfig>,
|
|
67
|
+
): Required<AgentConfig> {
|
|
68
|
+
// Layer 1: File
|
|
69
|
+
let fileConfig: Partial<AgentConfig> = {};
|
|
70
|
+
if (configPath) {
|
|
71
|
+
fileConfig = loadConfigFile(configPath);
|
|
72
|
+
log.info('Loaded config file', { path: configPath });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Layer 2: Env
|
|
76
|
+
const envOverrides = loadEnvOverrides();
|
|
77
|
+
|
|
78
|
+
// Layer 3: CLI
|
|
79
|
+
const cliConfig = cliOverrides ?? {};
|
|
80
|
+
|
|
81
|
+
// Deep merge: defaults ← file ← env ← CLI
|
|
82
|
+
// Start with a proper deep copy of merged defaults+file to avoid mutating the frozen defaults
|
|
83
|
+
const cloned = structuredClone(resolveDefaults(fileConfig)) as Required<AgentConfig>;
|
|
84
|
+
const base = cloned;
|
|
85
|
+
|
|
86
|
+
// Then merge env overrides
|
|
87
|
+
mergeOverrides(base, envOverrides);
|
|
88
|
+
|
|
89
|
+
// Then merge CLI overrides
|
|
90
|
+
mergeOverrides(base, cliConfig);
|
|
91
|
+
|
|
92
|
+
// Populate configDir
|
|
93
|
+
if (configPath) {
|
|
94
|
+
base.configDir = dirname(resolve(configPath));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return base;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function mergeOverrides(target: Record<string, unknown>, source: Record<string, unknown>): void {
|
|
101
|
+
for (const key of Object.keys(source)) {
|
|
102
|
+
const srcVal = source[key];
|
|
103
|
+
const tgtVal = target[key];
|
|
104
|
+
if (
|
|
105
|
+
srcVal !== undefined &&
|
|
106
|
+
srcVal !== null &&
|
|
107
|
+
typeof srcVal === 'object' &&
|
|
108
|
+
!Array.isArray(srcVal) &&
|
|
109
|
+
tgtVal !== undefined &&
|
|
110
|
+
typeof tgtVal === 'object' &&
|
|
111
|
+
!Array.isArray(tgtVal)
|
|
112
|
+
) {
|
|
113
|
+
// Do not merge empty override objects
|
|
114
|
+
if (Object.keys(srcVal as Record<string, unknown>).length > 0) {
|
|
115
|
+
mergeOverrides(tgtVal as Record<string, unknown>, srcVal as Record<string, unknown>);
|
|
116
|
+
}
|
|
117
|
+
} else if (srcVal !== undefined && srcVal !== null) {
|
|
118
|
+
target[key] = srcVal;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|