@rk0429/agentic-relay 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/relay.mjs +1744 -837
  3. package/package.json +1 -1
package/dist/relay.mjs CHANGED
@@ -1,27 +1,16 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/bin/relay.ts
4
- import { defineCommand as defineCommand10, runMain } from "citty";
5
- import { join as join5 } from "path";
6
- import { homedir as homedir3 } from "os";
7
-
8
- // src/infrastructure/process-manager.ts
9
- import { execa } from "execa";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
10
11
 
11
12
  // src/infrastructure/logger.ts
12
13
  import { createConsola } from "consola";
13
- var LOG_LEVEL_MAP = {
14
- silent: -1,
15
- fatal: 0,
16
- error: 0,
17
- warn: 1,
18
- log: 2,
19
- info: 3,
20
- success: 3,
21
- debug: 4,
22
- trace: 5,
23
- verbose: 5
24
- };
25
14
  function resolveLogLevel() {
26
15
  const envLevel = process.env["RELAY_LOG_LEVEL"]?.toLowerCase();
27
16
  if (envLevel && envLevel in LOG_LEVEL_MAP) {
@@ -29,127 +18,734 @@ function resolveLogLevel() {
29
18
  }
30
19
  return 3;
31
20
  }
32
- var logger = createConsola({
33
- level: resolveLogLevel(),
34
- defaults: {
35
- tag: "relay"
21
+ var LOG_LEVEL_MAP, logger;
22
+ var init_logger = __esm({
23
+ "src/infrastructure/logger.ts"() {
24
+ "use strict";
25
+ LOG_LEVEL_MAP = {
26
+ silent: -1,
27
+ fatal: 0,
28
+ error: 0,
29
+ warn: 1,
30
+ log: 2,
31
+ info: 3,
32
+ success: 3,
33
+ debug: 4,
34
+ trace: 5,
35
+ verbose: 5
36
+ };
37
+ logger = createConsola({
38
+ level: resolveLogLevel(),
39
+ defaults: {
40
+ tag: "relay"
41
+ }
42
+ });
36
43
  }
37
44
  });
38
45
 
39
- // src/infrastructure/process-manager.ts
40
- var ProcessManager = class {
41
- activeProcesses = /* @__PURE__ */ new Set();
42
- constructor() {
43
- this.setupSignalHandlers();
44
- }
45
- async spawnInteractive(command, args, options) {
46
- logger.debug(`Spawning interactive: ${command} ${args.join(" ")}`);
47
- const execaOptions = {
48
- stdio: "inherit",
49
- cwd: options?.cwd,
50
- env: options?.env,
51
- extendEnv: options?.env ? false : true,
52
- timeout: options?.timeout,
53
- reject: false
46
+ // src/mcp-server/recursion-guard.ts
47
+ import { createHash } from "crypto";
48
+ var RecursionGuard;
49
+ var init_recursion_guard = __esm({
50
+ "src/mcp-server/recursion-guard.ts"() {
51
+ "use strict";
52
+ RecursionGuard = class _RecursionGuard {
53
+ constructor(config = {
54
+ maxDepth: 5,
55
+ maxCallsPerSession: 20,
56
+ timeoutSec: 300
57
+ }) {
58
+ this.config = config;
59
+ }
60
+ static MAX_ENTRIES = 1e3;
61
+ callCounts = /* @__PURE__ */ new Map();
62
+ promptHashes = /* @__PURE__ */ new Map();
63
+ /** Remove entries older than timeoutSec and enforce size limits */
64
+ cleanup() {
65
+ const now = Date.now();
66
+ const ttlMs = this.config.timeoutSec * 1e3;
67
+ for (const [key, value] of this.callCounts) {
68
+ if (now - value.createdAt > ttlMs) {
69
+ this.callCounts.delete(key);
70
+ }
71
+ }
72
+ for (const [key, value] of this.promptHashes) {
73
+ if (now - value.createdAt > ttlMs) {
74
+ this.promptHashes.delete(key);
75
+ }
76
+ }
77
+ if (this.callCounts.size > _RecursionGuard.MAX_ENTRIES) {
78
+ const sorted = [...this.callCounts.entries()].sort(
79
+ (a, b) => a[1].createdAt - b[1].createdAt
80
+ );
81
+ for (let i = 0; i < sorted.length - _RecursionGuard.MAX_ENTRIES; i++) {
82
+ this.callCounts.delete(sorted[i][0]);
83
+ }
84
+ }
85
+ if (this.promptHashes.size > _RecursionGuard.MAX_ENTRIES) {
86
+ const sorted = [...this.promptHashes.entries()].sort(
87
+ (a, b) => a[1].createdAt - b[1].createdAt
88
+ );
89
+ for (let i = 0; i < sorted.length - _RecursionGuard.MAX_ENTRIES; i++) {
90
+ this.promptHashes.delete(sorted[i][0]);
91
+ }
92
+ }
93
+ }
94
+ /** Check if a spawn is allowed */
95
+ canSpawn(context) {
96
+ this.cleanup();
97
+ if (context.depth >= this.config.maxDepth) {
98
+ return {
99
+ allowed: false,
100
+ reason: `Max depth exceeded: ${context.depth} >= ${this.config.maxDepth}`
101
+ };
102
+ }
103
+ const currentCount = this.callCounts.get(context.traceId)?.count ?? 0;
104
+ if (currentCount >= this.config.maxCallsPerSession) {
105
+ return {
106
+ allowed: false,
107
+ reason: `Max calls per session exceeded: ${currentCount} >= ${this.config.maxCallsPerSession}`
108
+ };
109
+ }
110
+ if (this.detectLoop(context.traceId, context.backend, context.promptHash)) {
111
+ return {
112
+ allowed: false,
113
+ reason: `Loop detected: same (backend=${context.backend}, promptHash=${context.promptHash}) appeared 3+ times in trace ${context.traceId}`
114
+ };
115
+ }
116
+ return { allowed: true };
117
+ }
118
+ /** Record a spawn invocation */
119
+ recordSpawn(context) {
120
+ this.cleanup();
121
+ const entry = this.callCounts.get(context.traceId);
122
+ if (entry) {
123
+ entry.count += 1;
124
+ } else {
125
+ this.callCounts.set(context.traceId, { count: 1, createdAt: Date.now() });
126
+ }
127
+ const key = `${context.backend}:${context.promptHash}`;
128
+ const hashEntry = this.promptHashes.get(context.traceId);
129
+ if (hashEntry) {
130
+ hashEntry.hashes.push(key);
131
+ } else {
132
+ this.promptHashes.set(context.traceId, { hashes: [key], createdAt: Date.now() });
133
+ }
134
+ }
135
+ /** Detect if the same (backend + promptHash) combination has appeared 3+ times */
136
+ detectLoop(traceId, backend, promptHash) {
137
+ const key = `${backend}:${promptHash}`;
138
+ const hashes = this.promptHashes.get(traceId)?.hashes ?? [];
139
+ const count = hashes.filter((h) => h === key).length;
140
+ return count >= 3;
141
+ }
142
+ /** Get current config (for testing/inspection) */
143
+ getConfig() {
144
+ return { ...this.config };
145
+ }
146
+ /** Get call count for a trace */
147
+ getCallCount(traceId) {
148
+ return this.callCounts.get(traceId)?.count ?? 0;
149
+ }
150
+ /** Utility: compute a prompt hash */
151
+ static hashPrompt(prompt) {
152
+ return createHash("sha256").update(prompt).digest("hex").slice(0, 16);
153
+ }
54
154
  };
55
- const proc = execa(command, args, execaOptions);
56
- this.activeProcesses.add(proc);
57
- try {
58
- await proc;
59
- } finally {
60
- this.activeProcesses.delete(proc);
61
- }
62
155
  }
63
- async execute(command, args, options) {
64
- logger.debug(`Executing: ${command} ${args.join(" ")}`);
65
- const stdinMode = options?.stdinMode ?? "pipe";
66
- const stdio = stdinMode === "pipe" ? "pipe" : [stdinMode, "pipe", "pipe"];
67
- const execaOptions = {
68
- stdio,
69
- cwd: options?.cwd,
70
- env: options?.env,
71
- extendEnv: options?.env ? false : true,
72
- timeout: options?.timeout,
73
- reject: false
156
+ });
157
+
158
+ // src/mcp-server/tools/spawn-agent.ts
159
+ import { z as z2 } from "zod";
160
+ import { nanoid as nanoid2 } from "nanoid";
161
+ function buildContextFromEnv() {
162
+ const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${nanoid2()}`;
163
+ const parentSessionId = process.env["RELAY_PARENT_SESSION_ID"] ?? null;
164
+ const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
165
+ return { traceId, parentSessionId, depth };
166
+ }
167
+ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2) {
168
+ const envContext = buildContextFromEnv();
169
+ const promptHash = RecursionGuard.hashPrompt(input.prompt);
170
+ const context = {
171
+ traceId: envContext.traceId,
172
+ depth: envContext.depth,
173
+ backend: input.backend,
174
+ promptHash
175
+ };
176
+ const guardResult = guard.canSpawn(context);
177
+ if (!guardResult.allowed) {
178
+ logger.warn(`Spawn blocked by RecursionGuard: ${guardResult.reason}`);
179
+ return {
180
+ sessionId: "",
181
+ exitCode: 1,
182
+ stdout: "",
183
+ stderr: `Spawn blocked: ${guardResult.reason}`
74
184
  };
75
- const proc = execa(command, args, execaOptions);
76
- this.activeProcesses.add(proc);
77
- try {
78
- const result = await proc;
79
- return {
80
- exitCode: result.exitCode ?? 1,
81
- stdout: result.stdout?.toString() ?? "",
82
- stderr: result.stderr?.toString() ?? ""
83
- };
84
- } finally {
85
- this.activeProcesses.delete(proc);
86
- }
87
185
  }
88
- async executeWithInput(command, args, stdinData, options) {
89
- logger.debug(`Executing with stdin: ${command} ${args.join(" ")}`);
90
- const execaOptions = {
91
- stdio: "pipe",
92
- input: stdinData,
93
- cwd: options?.cwd,
94
- env: options?.env,
95
- timeout: options?.timeout,
96
- reject: false
186
+ const adapter = registry2.get(input.backend);
187
+ const installed = await adapter.isInstalled();
188
+ if (!installed) {
189
+ return {
190
+ sessionId: "",
191
+ exitCode: 1,
192
+ stdout: "",
193
+ stderr: `Backend "${input.backend}" is not available. Use list_available_backends to see available options.`
97
194
  };
98
- const proc = execa(command, args, execaOptions);
99
- this.activeProcesses.add(proc);
195
+ }
196
+ const session = await sessionManager2.create({
197
+ backendId: input.backend,
198
+ parentSessionId: envContext.parentSessionId ?? void 0,
199
+ depth: envContext.depth + 1
200
+ });
201
+ if (hooksEngine2) {
100
202
  try {
101
- const result = await proc;
102
- return {
103
- exitCode: result.exitCode ?? 1,
104
- stdout: result.stdout?.toString() ?? "",
105
- stderr: result.stderr?.toString() ?? ""
203
+ const hookInput = {
204
+ event: "pre-spawn",
205
+ sessionId: session.relaySessionId,
206
+ backendId: input.backend,
207
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
208
+ data: {
209
+ prompt: input.prompt,
210
+ agent: input.agent,
211
+ model: input.model
212
+ }
106
213
  };
107
- } finally {
108
- this.activeProcesses.delete(proc);
214
+ await hooksEngine2.emit("pre-spawn", hookInput);
215
+ } catch (error) {
216
+ logger.debug(
217
+ `pre-spawn hook error: ${error instanceof Error ? error.message : String(error)}`
218
+ );
109
219
  }
110
220
  }
111
- setupSignalHandlers() {
112
- const handleSignal = (signal) => {
113
- logger.debug(`Received ${signal}, forwarding to child processes`);
114
- for (const proc of this.activeProcesses) {
115
- proc.kill(signal);
221
+ try {
222
+ let result;
223
+ if (input.resumeSessionId) {
224
+ if (!adapter.continueSession) {
225
+ return {
226
+ sessionId: session.relaySessionId,
227
+ exitCode: 1,
228
+ stdout: "",
229
+ stderr: `Backend "${input.backend}" does not support session continuation (continueSession).`
230
+ };
116
231
  }
117
- };
118
- process.on("SIGINT", () => handleSignal("SIGINT"));
119
- process.on("SIGTERM", () => handleSignal("SIGTERM"));
120
- }
121
- };
122
-
123
- // src/adapters/adapter-registry.ts
124
- var AdapterRegistry = class {
125
- adapters = /* @__PURE__ */ new Map();
126
- register(adapter) {
127
- this.adapters.set(adapter.id, adapter);
128
- }
129
- get(id) {
130
- const adapter = this.adapters.get(id);
131
- if (!adapter) {
132
- throw new Error(
133
- `Backend "${id}" is not registered. Available: ${[...this.adapters.keys()].join(", ")}`
232
+ result = await adapter.continueSession(input.resumeSessionId, input.prompt);
233
+ } else {
234
+ result = await adapter.execute({
235
+ prompt: input.prompt,
236
+ agent: input.agent,
237
+ systemPrompt: input.systemPrompt,
238
+ model: input.model,
239
+ maxTurns: input.maxTurns,
240
+ mcpContext: {
241
+ parentSessionId: session.relaySessionId,
242
+ depth: envContext.depth + 1,
243
+ maxDepth: guard.getConfig().maxDepth,
244
+ traceId: envContext.traceId
245
+ }
246
+ });
247
+ }
248
+ if (contextMonitor2) {
249
+ const estimatedTokens = Math.ceil(
250
+ (result.stdout.length + result.stderr.length) / 4
251
+ );
252
+ contextMonitor2.updateUsage(
253
+ session.relaySessionId,
254
+ input.backend,
255
+ estimatedTokens
134
256
  );
135
257
  }
136
- return adapter;
137
- }
138
- has(id) {
139
- return this.adapters.has(id);
258
+ guard.recordSpawn(context);
259
+ const status = result.exitCode === 0 ? "completed" : "error";
260
+ await sessionManager2.update(session.relaySessionId, { status });
261
+ if (hooksEngine2) {
262
+ try {
263
+ const hookInput = {
264
+ event: "post-spawn",
265
+ sessionId: session.relaySessionId,
266
+ backendId: input.backend,
267
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
268
+ data: {
269
+ exitCode: result.exitCode,
270
+ status
271
+ }
272
+ };
273
+ await hooksEngine2.emit("post-spawn", hookInput);
274
+ } catch (hookError) {
275
+ logger.debug(
276
+ `post-spawn hook error: ${hookError instanceof Error ? hookError.message : String(hookError)}`
277
+ );
278
+ }
279
+ }
280
+ return {
281
+ sessionId: session.relaySessionId,
282
+ exitCode: result.exitCode,
283
+ stdout: result.stdout,
284
+ stderr: result.stderr,
285
+ nativeSessionId: result.nativeSessionId
286
+ };
287
+ } catch (error) {
288
+ await sessionManager2.update(session.relaySessionId, { status: "error" });
289
+ const message = error instanceof Error ? error.message : String(error);
290
+ return {
291
+ sessionId: session.relaySessionId,
292
+ exitCode: 1,
293
+ stdout: "",
294
+ stderr: message
295
+ };
140
296
  }
141
- list() {
142
- return [...this.adapters.values()];
297
+ }
298
+ var spawnAgentInputSchema;
299
+ var init_spawn_agent = __esm({
300
+ "src/mcp-server/tools/spawn-agent.ts"() {
301
+ "use strict";
302
+ init_recursion_guard();
303
+ init_logger();
304
+ spawnAgentInputSchema = z2.object({
305
+ backend: z2.enum(["claude", "codex", "gemini"]),
306
+ prompt: z2.string(),
307
+ agent: z2.string().optional(),
308
+ systemPrompt: z2.string().optional(),
309
+ resumeSessionId: z2.string().optional(),
310
+ model: z2.string().optional(),
311
+ maxTurns: z2.number().optional()
312
+ });
143
313
  }
144
- };
314
+ });
145
315
 
146
- // src/adapters/base-adapter.ts
147
- var BaseAdapter = class {
148
- constructor(processManager2) {
149
- this.processManager = processManager2;
150
- }
151
- async isInstalled() {
152
- try {
316
+ // src/mcp-server/tools/list-sessions.ts
317
+ import { z as z3 } from "zod";
318
+ async function executeListSessions(input, sessionManager2) {
319
+ const sessions = await sessionManager2.list({
320
+ backendId: input.backend,
321
+ limit: input.limit
322
+ });
323
+ return {
324
+ sessions: sessions.map((s) => ({
325
+ relaySessionId: s.relaySessionId,
326
+ backendId: s.backendId,
327
+ status: s.status,
328
+ createdAt: s.createdAt.toISOString()
329
+ }))
330
+ };
331
+ }
332
+ var listSessionsInputSchema;
333
+ var init_list_sessions = __esm({
334
+ "src/mcp-server/tools/list-sessions.ts"() {
335
+ "use strict";
336
+ listSessionsInputSchema = z3.object({
337
+ backend: z3.enum(["claude", "codex", "gemini"]).optional(),
338
+ limit: z3.number().optional().default(10)
339
+ });
340
+ }
341
+ });
342
+
343
+ // src/mcp-server/tools/get-context-status.ts
344
+ import { z as z4 } from "zod";
345
+ async function executeGetContextStatus(input, sessionManager2, contextMonitor2) {
346
+ const session = await sessionManager2.get(input.sessionId);
347
+ if (!session) {
348
+ throw new Error(`Session not found: ${input.sessionId}`);
349
+ }
350
+ if (contextMonitor2) {
351
+ const usage = contextMonitor2.getUsage(input.sessionId);
352
+ if (usage) {
353
+ return {
354
+ sessionId: input.sessionId,
355
+ backendId: usage.backendId,
356
+ usagePercent: usage.usagePercent,
357
+ isEstimated: usage.isEstimated,
358
+ contextWindow: usage.contextWindow,
359
+ compactThreshold: usage.compactThreshold,
360
+ estimatedTokens: usage.estimatedTokens,
361
+ remainingBeforeCompact: usage.remainingBeforeCompact,
362
+ notifyThreshold: usage.notifyThreshold
363
+ };
364
+ }
365
+ }
366
+ return {
367
+ sessionId: input.sessionId,
368
+ usagePercent: 0,
369
+ isEstimated: true
370
+ };
371
+ }
372
+ var getContextStatusInputSchema;
373
+ var init_get_context_status = __esm({
374
+ "src/mcp-server/tools/get-context-status.ts"() {
375
+ "use strict";
376
+ getContextStatusInputSchema = z4.object({
377
+ sessionId: z4.string()
378
+ });
379
+ }
380
+ });
381
+
382
+ // src/mcp-server/tools/list-available-backends.ts
383
+ async function executeListAvailableBackends(registry2) {
384
+ const backends = [];
385
+ for (const adapter of registry2.list()) {
386
+ const health = await adapter.checkHealth();
387
+ backends.push({
388
+ id: adapter.id,
389
+ ...health
390
+ });
391
+ }
392
+ return backends;
393
+ }
394
+ var init_list_available_backends = __esm({
395
+ "src/mcp-server/tools/list-available-backends.ts"() {
396
+ "use strict";
397
+ }
398
+ });
399
+
400
+ // src/mcp-server/server.ts
401
+ var server_exports = {};
402
+ __export(server_exports, {
403
+ RelayMCPServer: () => RelayMCPServer
404
+ });
405
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
406
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
407
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
408
+ import { createServer } from "http";
409
+ import { randomUUID } from "crypto";
410
+ import { z as z5 } from "zod";
411
+ var RelayMCPServer;
412
+ var init_server = __esm({
413
+ "src/mcp-server/server.ts"() {
414
+ "use strict";
415
+ init_recursion_guard();
416
+ init_spawn_agent();
417
+ init_list_sessions();
418
+ init_get_context_status();
419
+ init_list_available_backends();
420
+ init_logger();
421
+ RelayMCPServer = class {
422
+ constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
423
+ this.registry = registry2;
424
+ this.sessionManager = sessionManager2;
425
+ this.hooksEngine = hooksEngine2;
426
+ this.contextMonitor = contextMonitor2;
427
+ this.guard = new RecursionGuard(guardConfig);
428
+ this.server = new McpServer({
429
+ name: "agentic-relay",
430
+ version: "0.4.0"
431
+ });
432
+ this.registerTools();
433
+ }
434
+ server;
435
+ guard;
436
+ registerTools() {
437
+ this.server.tool(
438
+ "spawn_agent",
439
+ "Spawn a sub-agent on the specified backend CLI (Claude Code, Codex CLI, or Gemini CLI). The agent executes the given prompt in non-interactive mode and returns the result. Use 'agent' for named agent configurations (Claude only), or 'systemPrompt' for custom role instructions (all backends).",
440
+ {
441
+ backend: z5.enum(["claude", "codex", "gemini"]),
442
+ prompt: z5.string(),
443
+ agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
444
+ systemPrompt: z5.string().optional().describe(
445
+ "System prompt / role instructions for the sub-agent (all backends)"
446
+ ),
447
+ resumeSessionId: z5.string().optional(),
448
+ model: z5.string().optional(),
449
+ maxTurns: z5.number().optional()
450
+ },
451
+ async (params) => {
452
+ try {
453
+ const result = await executeSpawnAgent(
454
+ params,
455
+ this.registry,
456
+ this.sessionManager,
457
+ this.guard,
458
+ this.hooksEngine,
459
+ this.contextMonitor
460
+ );
461
+ const isError = result.exitCode !== 0;
462
+ const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
463
+
464
+ ${result.stdout}`;
465
+ return {
466
+ content: [{ type: "text", text }],
467
+ isError
468
+ };
469
+ } catch (error) {
470
+ const message = error instanceof Error ? error.message : String(error);
471
+ return {
472
+ content: [{ type: "text", text: `Error: ${message}` }],
473
+ isError: true
474
+ };
475
+ }
476
+ }
477
+ );
478
+ this.server.tool(
479
+ "list_sessions",
480
+ "List relay sessions, optionally filtered by backend.",
481
+ {
482
+ backend: z5.enum(["claude", "codex", "gemini"]).optional(),
483
+ limit: z5.number().optional()
484
+ },
485
+ async (params) => {
486
+ try {
487
+ const result = await executeListSessions(
488
+ { backend: params.backend, limit: params.limit ?? 10 },
489
+ this.sessionManager
490
+ );
491
+ return {
492
+ content: [
493
+ {
494
+ type: "text",
495
+ text: JSON.stringify(result, null, 2)
496
+ }
497
+ ]
498
+ };
499
+ } catch (error) {
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ return {
502
+ content: [{ type: "text", text: `Error: ${message}` }],
503
+ isError: true
504
+ };
505
+ }
506
+ }
507
+ );
508
+ this.server.tool(
509
+ "get_context_status",
510
+ "Get the context usage status of a relay session. Returns usage data from ContextMonitor when available, otherwise estimated values.",
511
+ {
512
+ sessionId: z5.string()
513
+ },
514
+ async (params) => {
515
+ try {
516
+ const result = await executeGetContextStatus(
517
+ params,
518
+ this.sessionManager,
519
+ this.contextMonitor
520
+ );
521
+ return {
522
+ content: [
523
+ {
524
+ type: "text",
525
+ text: JSON.stringify(result, null, 2)
526
+ }
527
+ ]
528
+ };
529
+ } catch (error) {
530
+ const message = error instanceof Error ? error.message : String(error);
531
+ return {
532
+ content: [{ type: "text", text: `Error: ${message}` }],
533
+ isError: true
534
+ };
535
+ }
536
+ }
537
+ );
538
+ this.server.tool(
539
+ "list_available_backends",
540
+ "List all registered backends with their health status. Use this before spawn_agent to check which backends are available.",
541
+ {},
542
+ async () => {
543
+ try {
544
+ const result = await executeListAvailableBackends(this.registry);
545
+ return {
546
+ content: [
547
+ {
548
+ type: "text",
549
+ text: JSON.stringify(result, null, 2)
550
+ }
551
+ ]
552
+ };
553
+ } catch (error) {
554
+ const message = error instanceof Error ? error.message : String(error);
555
+ return {
556
+ content: [{ type: "text", text: `Error: ${message}` }],
557
+ isError: true
558
+ };
559
+ }
560
+ }
561
+ );
562
+ }
563
+ async start(options) {
564
+ const transportType = options?.transport ?? "stdio";
565
+ if (transportType === "stdio") {
566
+ logger.info("Starting agentic-relay MCP server (stdio transport)...");
567
+ const transport = new StdioServerTransport();
568
+ await this.server.connect(transport);
569
+ return;
570
+ }
571
+ const port = options?.port ?? 3100;
572
+ logger.info(
573
+ `Starting agentic-relay MCP server (HTTP transport on port ${port})...`
574
+ );
575
+ const httpTransport = new StreamableHTTPServerTransport({
576
+ sessionIdGenerator: () => randomUUID()
577
+ });
578
+ const httpServer = createServer(async (req, res) => {
579
+ const url = req.url ?? "";
580
+ if (url === "/mcp" || url.startsWith("/mcp?")) {
581
+ await httpTransport.handleRequest(req, res);
582
+ } else {
583
+ res.writeHead(404, { "Content-Type": "text/plain" });
584
+ res.end("Not found");
585
+ }
586
+ });
587
+ this._httpServer = httpServer;
588
+ await this.server.connect(httpTransport);
589
+ await new Promise((resolve) => {
590
+ httpServer.listen(port, () => {
591
+ logger.info(`MCP server listening on http://localhost:${port}/mcp`);
592
+ resolve();
593
+ });
594
+ });
595
+ await new Promise((resolve) => {
596
+ httpServer.on("close", resolve);
597
+ });
598
+ }
599
+ /** Exposed for testing and graceful shutdown */
600
+ get httpServer() {
601
+ return this._httpServer;
602
+ }
603
+ _httpServer;
604
+ };
605
+ }
606
+ });
607
+
608
+ // src/bin/relay.ts
609
+ import { defineCommand as defineCommand10, runMain } from "citty";
610
+ import { join as join8 } from "path";
611
+ import { homedir as homedir6 } from "os";
612
+
613
+ // src/infrastructure/process-manager.ts
614
+ init_logger();
615
+ import { execa } from "execa";
616
+ var ProcessManager = class {
617
+ activeProcesses = /* @__PURE__ */ new Set();
618
+ constructor() {
619
+ this.setupSignalHandlers();
620
+ }
621
+ async spawnInteractive(command, args, options) {
622
+ logger.debug(`Spawning interactive: ${command} ${args.join(" ")}`);
623
+ const execaOptions = {
624
+ stdio: "inherit",
625
+ cwd: options?.cwd,
626
+ env: options?.env,
627
+ extendEnv: options?.env ? false : true,
628
+ timeout: options?.timeout,
629
+ reject: false
630
+ };
631
+ const proc = execa(command, args, execaOptions);
632
+ this.activeProcesses.add(proc);
633
+ try {
634
+ await proc;
635
+ } finally {
636
+ this.activeProcesses.delete(proc);
637
+ }
638
+ }
639
+ async execute(command, args, options) {
640
+ logger.debug(`Executing: ${command} ${args.join(" ")}`);
641
+ const stdinMode = options?.stdinMode ?? "pipe";
642
+ const stdio = stdinMode === "pipe" ? "pipe" : [stdinMode, "pipe", "pipe"];
643
+ const execaOptions = {
644
+ stdio,
645
+ cwd: options?.cwd,
646
+ env: options?.env,
647
+ extendEnv: options?.env ? false : true,
648
+ timeout: options?.timeout,
649
+ reject: false
650
+ };
651
+ const proc = execa(command, args, execaOptions);
652
+ this.activeProcesses.add(proc);
653
+ try {
654
+ const result = await proc;
655
+ return {
656
+ exitCode: result.exitCode ?? 1,
657
+ stdout: result.stdout?.toString() ?? "",
658
+ stderr: result.stderr?.toString() ?? ""
659
+ };
660
+ } finally {
661
+ this.activeProcesses.delete(proc);
662
+ }
663
+ }
664
+ async executeWithInput(command, args, stdinData, options) {
665
+ logger.debug(`Executing with stdin: ${command} ${args.join(" ")}`);
666
+ const execaOptions = {
667
+ stdio: "pipe",
668
+ input: stdinData,
669
+ cwd: options?.cwd,
670
+ env: options?.env,
671
+ timeout: options?.timeout,
672
+ reject: false
673
+ };
674
+ const proc = execa(command, args, execaOptions);
675
+ this.activeProcesses.add(proc);
676
+ try {
677
+ const result = await proc;
678
+ return {
679
+ exitCode: result.exitCode ?? 1,
680
+ stdout: result.stdout?.toString() ?? "",
681
+ stderr: result.stderr?.toString() ?? ""
682
+ };
683
+ } finally {
684
+ this.activeProcesses.delete(proc);
685
+ }
686
+ }
687
+ setupSignalHandlers() {
688
+ const handleSignal = (signal) => {
689
+ logger.debug(`Received ${signal}, forwarding to child processes`);
690
+ for (const proc of this.activeProcesses) {
691
+ proc.kill(signal);
692
+ }
693
+ };
694
+ process.on("SIGINT", () => handleSignal("SIGINT"));
695
+ process.on("SIGTERM", () => handleSignal("SIGTERM"));
696
+ }
697
+ };
698
+
699
+ // src/adapters/adapter-registry.ts
700
+ var AdapterRegistry = class {
701
+ adapters = /* @__PURE__ */ new Map();
702
+ factories = /* @__PURE__ */ new Map();
703
+ register(adapter) {
704
+ this.factories.delete(adapter.id);
705
+ this.adapters.set(adapter.id, adapter);
706
+ }
707
+ registerLazy(id, factory) {
708
+ this.adapters.delete(id);
709
+ this.factories.set(id, factory);
710
+ }
711
+ get(id) {
712
+ let adapter = this.adapters.get(id);
713
+ if (!adapter) {
714
+ const factory = this.factories.get(id);
715
+ if (factory) {
716
+ adapter = factory();
717
+ this.adapters.set(id, adapter);
718
+ this.factories.delete(id);
719
+ }
720
+ }
721
+ if (!adapter) {
722
+ const allIds = [...this.adapters.keys(), ...this.factories.keys()];
723
+ throw new Error(
724
+ `Backend "${id}" is not registered. Available: ${allIds.join(", ")}`
725
+ );
726
+ }
727
+ return adapter;
728
+ }
729
+ has(id) {
730
+ return this.adapters.has(id) || this.factories.has(id);
731
+ }
732
+ list() {
733
+ for (const [id, factory] of this.factories) {
734
+ this.adapters.set(id, factory());
735
+ }
736
+ this.factories.clear();
737
+ return [...this.adapters.values()];
738
+ }
739
+ };
740
+
741
+ // src/adapters/base-adapter.ts
742
+ init_logger();
743
+ var BaseAdapter = class {
744
+ constructor(processManager2) {
745
+ this.processManager = processManager2;
746
+ }
747
+ async isInstalled() {
748
+ try {
153
749
  const result = await this.processManager.execute("which", [this.command]);
154
750
  return result.exitCode === 0;
155
751
  } catch {
@@ -167,6 +763,42 @@ var BaseAdapter = class {
167
763
  }
168
764
  return result.stdout.trim();
169
765
  }
766
+ async continueSession(_nativeSessionId, _prompt) {
767
+ return {
768
+ exitCode: 1,
769
+ stdout: "",
770
+ stderr: `continueSession not supported for ${this.id}`
771
+ };
772
+ }
773
+ async checkHealth() {
774
+ const HEALTH_TIMEOUT = 5e3;
775
+ const installed = await Promise.race([
776
+ this.isInstalled(),
777
+ new Promise(
778
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
779
+ )
780
+ ]).catch(() => false);
781
+ if (!installed) {
782
+ return {
783
+ installed: false,
784
+ authenticated: false,
785
+ healthy: false,
786
+ message: `${this.id} is not installed`
787
+ };
788
+ }
789
+ const version = await Promise.race([
790
+ this.getVersion(),
791
+ new Promise(
792
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
793
+ )
794
+ ]).catch(() => void 0);
795
+ return {
796
+ installed: true,
797
+ authenticated: true,
798
+ healthy: true,
799
+ version
800
+ };
801
+ }
170
802
  async getMCPConfig() {
171
803
  logger.warn(`getMCPConfig not implemented for ${this.id}`);
172
804
  return [];
@@ -250,10 +882,13 @@ function mapCommonToNative(backendId, flags) {
250
882
  }
251
883
 
252
884
  // src/adapters/claude-adapter.ts
253
- import {
254
- query,
255
- listSessions
256
- } from "@anthropic-ai/claude-agent-sdk";
885
+ init_logger();
886
+ import { readFile, writeFile, mkdir } from "fs/promises";
887
+ import { homedir } from "os";
888
+ import { join, dirname } from "path";
889
+ async function loadClaudeSDK() {
890
+ return await import("@anthropic-ai/claude-agent-sdk");
891
+ }
257
892
  var CLAUDE_NESTING_ENV_VARS = [
258
893
  "CLAUDECODE",
259
894
  "CLAUDE_CODE_SSE_PORT",
@@ -262,6 +897,51 @@ var CLAUDE_NESTING_ENV_VARS = [
262
897
  var ClaudeAdapter = class extends BaseAdapter {
263
898
  id = "claude";
264
899
  command = "claude";
900
+ getConfigPath() {
901
+ return join(homedir(), ".claude.json");
902
+ }
903
+ async checkHealth() {
904
+ const HEALTH_TIMEOUT = 5e3;
905
+ const installed = await Promise.race([
906
+ this.isInstalled(),
907
+ new Promise(
908
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
909
+ )
910
+ ]).catch(() => false);
911
+ if (!installed) {
912
+ return {
913
+ installed: false,
914
+ authenticated: false,
915
+ healthy: false,
916
+ message: "claude is not installed"
917
+ };
918
+ }
919
+ const version = await Promise.race([
920
+ this.getVersion(),
921
+ new Promise(
922
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
923
+ )
924
+ ]).catch(() => void 0);
925
+ let authenticated = true;
926
+ try {
927
+ const result = await Promise.race([
928
+ this.processManager.execute(this.command, ["auth", "status"]),
929
+ new Promise(
930
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
931
+ )
932
+ ]);
933
+ authenticated = result.exitCode === 0;
934
+ } catch {
935
+ authenticated = true;
936
+ }
937
+ return {
938
+ installed: true,
939
+ authenticated,
940
+ healthy: authenticated,
941
+ version,
942
+ ...!authenticated ? { message: "claude authentication not configured" } : {}
943
+ };
944
+ }
265
945
  mapFlags(flags) {
266
946
  return {
267
947
  args: mapCommonToNative("claude", flags)
@@ -283,11 +963,13 @@ var ClaudeAdapter = class extends BaseAdapter {
283
963
  const env = this.buildCleanEnv(flags);
284
964
  const permissionMode = this.getPermissionMode();
285
965
  try {
966
+ const { query } = await loadClaudeSDK();
286
967
  const options = {
287
968
  env,
288
969
  cwd: process.cwd(),
289
970
  ...flags.model ? { model: flags.model } : {},
290
- ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
971
+ ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
972
+ ...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
291
973
  };
292
974
  if (permissionMode === "bypassPermissions") {
293
975
  options.permissionMode = "bypassPermissions";
@@ -316,7 +998,8 @@ var ClaudeAdapter = class extends BaseAdapter {
316
998
  return {
317
999
  exitCode: isError ? 1 : 0,
318
1000
  stdout: resultText,
319
- stderr: errorMessages.join("\n")
1001
+ stderr: errorMessages.join("\n"),
1002
+ ...sessionId ? { nativeSessionId: sessionId } : {}
320
1003
  };
321
1004
  } catch (error) {
322
1005
  return {
@@ -333,11 +1016,13 @@ var ClaudeAdapter = class extends BaseAdapter {
333
1016
  const env = this.buildCleanEnv(flags);
334
1017
  const permissionMode = this.getPermissionMode();
335
1018
  try {
1019
+ const { query } = await loadClaudeSDK();
336
1020
  const options = {
337
1021
  env,
338
1022
  cwd: process.cwd(),
339
1023
  ...flags.model ? { model: flags.model } : {},
340
- ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
1024
+ ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
1025
+ ...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
341
1026
  };
342
1027
  if (permissionMode === "bypassPermissions") {
343
1028
  options.permissionMode = "bypassPermissions";
@@ -371,11 +1056,13 @@ var ClaudeAdapter = class extends BaseAdapter {
371
1056
  message: `Running ${toolName}...`
372
1057
  };
373
1058
  } else if (message.type === "result") {
1059
+ const nativeSessionId = message.session_id || void 0;
374
1060
  if (message.subtype === "success") {
375
1061
  const resultText = message.result;
376
1062
  yield {
377
1063
  type: "done",
378
- result: { exitCode: 0, stdout: resultText, stderr: "" }
1064
+ result: { exitCode: 0, stdout: resultText, stderr: "" },
1065
+ nativeSessionId
379
1066
  };
380
1067
  } else {
381
1068
  const errors = message.errors;
@@ -385,7 +1072,8 @@ var ClaudeAdapter = class extends BaseAdapter {
385
1072
  exitCode: 1,
386
1073
  stdout: "",
387
1074
  stderr: errors.join("\n")
388
- }
1075
+ },
1076
+ nativeSessionId
389
1077
  };
390
1078
  }
391
1079
  }
@@ -399,7 +1087,45 @@ var ClaudeAdapter = class extends BaseAdapter {
399
1087
  };
400
1088
  }
401
1089
  }
402
- async resumeSession(sessionId, flags) {
1090
+ async continueSession(nativeSessionId, prompt) {
1091
+ try {
1092
+ const { query } = await loadClaudeSDK();
1093
+ const permissionMode = this.getPermissionMode();
1094
+ const options = {
1095
+ resume: nativeSessionId,
1096
+ maxTurns: 1,
1097
+ cwd: process.cwd()
1098
+ };
1099
+ if (permissionMode === "bypassPermissions") {
1100
+ options.permissionMode = "bypassPermissions";
1101
+ options.allowDangerouslySkipPermissions = true;
1102
+ }
1103
+ const q = query({
1104
+ prompt,
1105
+ options
1106
+ });
1107
+ let resultText = "";
1108
+ for await (const message of q) {
1109
+ if (message.type === "result") {
1110
+ if (message.subtype === "success") {
1111
+ resultText = message.result;
1112
+ }
1113
+ }
1114
+ }
1115
+ return {
1116
+ exitCode: 0,
1117
+ stdout: resultText,
1118
+ stderr: ""
1119
+ };
1120
+ } catch (error) {
1121
+ return {
1122
+ exitCode: 1,
1123
+ stdout: "",
1124
+ stderr: error instanceof Error ? error.message : String(error)
1125
+ };
1126
+ }
1127
+ }
1128
+ async resumeSession(sessionId, flags) {
403
1129
  await this.processManager.spawnInteractive(
404
1130
  this.command,
405
1131
  ["-r", sessionId],
@@ -422,6 +1148,7 @@ var ClaudeAdapter = class extends BaseAdapter {
422
1148
  }
423
1149
  async listNativeSessions() {
424
1150
  try {
1151
+ const { listSessions } = await loadClaudeSDK();
425
1152
  const sessions = await listSessions({ limit: 20 });
426
1153
  return sessions.map((s) => ({
427
1154
  nativeId: s.sessionId,
@@ -447,13 +1174,214 @@ var ClaudeAdapter = class extends BaseAdapter {
447
1174
  logger.success(`Claude CLI updated: ${result.stdout.trim()}`);
448
1175
  }
449
1176
  }
1177
+ async getMCPConfig() {
1178
+ const configPath = this.getConfigPath();
1179
+ try {
1180
+ const raw = await readFile(configPath, "utf-8");
1181
+ const config = JSON.parse(raw);
1182
+ const mcpServers = config.mcpServers;
1183
+ if (!mcpServers || typeof mcpServers !== "object") {
1184
+ return [];
1185
+ }
1186
+ return Object.entries(mcpServers).map(([name, entry]) => {
1187
+ const server = {
1188
+ name,
1189
+ command: entry.command
1190
+ };
1191
+ if (entry.args) server.args = entry.args;
1192
+ if (entry.env) server.env = entry.env;
1193
+ return server;
1194
+ });
1195
+ } catch {
1196
+ return [];
1197
+ }
1198
+ }
1199
+ async setMCPConfig(servers) {
1200
+ const configPath = this.getConfigPath();
1201
+ let config = {};
1202
+ try {
1203
+ const raw = await readFile(configPath, "utf-8");
1204
+ config = JSON.parse(raw);
1205
+ } catch {
1206
+ }
1207
+ const mcpServers = {};
1208
+ for (const server of servers) {
1209
+ const entry = {
1210
+ command: server.command
1211
+ };
1212
+ if (server.args) entry.args = server.args;
1213
+ if (server.env) entry.env = server.env;
1214
+ const serverName = server.name ?? server.command;
1215
+ mcpServers[serverName] = entry;
1216
+ }
1217
+ config.mcpServers = mcpServers;
1218
+ await mkdir(dirname(configPath), { recursive: true });
1219
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", {
1220
+ mode: 384
1221
+ });
1222
+ }
450
1223
  };
451
1224
 
452
1225
  // src/adapters/codex-adapter.ts
453
- import { Codex } from "@openai/codex-sdk";
1226
+ init_logger();
1227
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1228
+ import { homedir as homedir2 } from "os";
1229
+ import { join as join2, dirname as dirname2 } from "path";
1230
+ async function loadCodexSDK() {
1231
+ return await import("@openai/codex-sdk");
1232
+ }
1233
+ function parseTOMLMcpServers(content) {
1234
+ const lines = content.split("\n");
1235
+ const preambleLines = [];
1236
+ const postambleLines = [];
1237
+ const mcpServers = /* @__PURE__ */ new Map();
1238
+ let currentServerName = null;
1239
+ let currentServer = { command: "" };
1240
+ let inMcpSection = false;
1241
+ let pastMcpSections = false;
1242
+ for (const line of lines) {
1243
+ const tableMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
1244
+ if (tableMatch) {
1245
+ if (currentServerName !== null) {
1246
+ mcpServers.set(currentServerName, currentServer);
1247
+ currentServerName = null;
1248
+ }
1249
+ const tableName = tableMatch[1].trim();
1250
+ const mcpMatch = tableName.match(/^mcp_servers\.(.+)$/);
1251
+ if (mcpMatch) {
1252
+ inMcpSection = true;
1253
+ pastMcpSections = false;
1254
+ currentServerName = mcpMatch[1];
1255
+ currentServer = { command: "" };
1256
+ continue;
1257
+ } else {
1258
+ if (inMcpSection) {
1259
+ pastMcpSections = true;
1260
+ inMcpSection = false;
1261
+ }
1262
+ }
1263
+ }
1264
+ if (inMcpSection && currentServerName !== null) {
1265
+ const kvMatch = line.match(/^\s*(\w+)\s*=\s*(.+)$/);
1266
+ if (kvMatch) {
1267
+ const key = kvMatch[1];
1268
+ const rawValue = kvMatch[2].trim();
1269
+ if (key === "command") {
1270
+ currentServer.command = parseTOMLString(rawValue);
1271
+ } else if (key === "args") {
1272
+ currentServer.args = parseTOMLStringArray(rawValue);
1273
+ }
1274
+ }
1275
+ continue;
1276
+ }
1277
+ if (pastMcpSections || mcpServers.size > 0 && !inMcpSection && currentServerName === null && tableMatch) {
1278
+ postambleLines.push(line);
1279
+ } else if (!inMcpSection && currentServerName === null) {
1280
+ preambleLines.push(line);
1281
+ } else {
1282
+ postambleLines.push(line);
1283
+ }
1284
+ }
1285
+ if (currentServerName !== null) {
1286
+ mcpServers.set(currentServerName, currentServer);
1287
+ }
1288
+ return { preambleLines, postambleLines, mcpServers };
1289
+ }
1290
+ function parseTOMLString(raw) {
1291
+ const match = raw.match(/^"(.*)"$/);
1292
+ if (match) {
1293
+ return match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
1294
+ }
1295
+ const singleMatch = raw.match(/^'(.*)'$/);
1296
+ if (singleMatch) {
1297
+ return singleMatch[1];
1298
+ }
1299
+ return raw;
1300
+ }
1301
+ function parseTOMLStringArray(raw) {
1302
+ const match = raw.match(/^\[(.*)]\s*$/);
1303
+ if (!match) return [];
1304
+ const inner = match[1].trim();
1305
+ if (!inner) return [];
1306
+ const result = [];
1307
+ const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^']*)'/g;
1308
+ let m;
1309
+ while ((m = regex.exec(inner)) !== null) {
1310
+ if (m[1] !== void 0) {
1311
+ result.push(m[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
1312
+ } else if (m[2] !== void 0) {
1313
+ result.push(m[2]);
1314
+ }
1315
+ }
1316
+ return result;
1317
+ }
1318
+ function toTOMLString(value) {
1319
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1320
+ }
1321
+ function toTOMLStringArray(values) {
1322
+ return `[${values.map(toTOMLString).join(", ")}]`;
1323
+ }
1324
+ function generateMcpServersTOML(servers) {
1325
+ const parts = [];
1326
+ for (const server of servers) {
1327
+ const serverName = server.name ?? server.command;
1328
+ parts.push(`[mcp_servers.${serverName}]`);
1329
+ parts.push(`command = ${toTOMLString(server.command)}`);
1330
+ if (server.args && server.args.length > 0) {
1331
+ parts.push(`args = ${toTOMLStringArray(server.args)}`);
1332
+ }
1333
+ parts.push("");
1334
+ }
1335
+ return parts.join("\n");
1336
+ }
454
1337
  var CodexAdapter = class extends BaseAdapter {
455
1338
  id = "codex";
456
1339
  command = "codex";
1340
+ getConfigPath() {
1341
+ return join2(homedir2(), ".codex", "config.toml");
1342
+ }
1343
+ async checkHealth() {
1344
+ const HEALTH_TIMEOUT = 5e3;
1345
+ const installed = await Promise.race([
1346
+ this.isInstalled(),
1347
+ new Promise(
1348
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1349
+ )
1350
+ ]).catch(() => false);
1351
+ if (!installed) {
1352
+ return {
1353
+ installed: false,
1354
+ authenticated: false,
1355
+ healthy: false,
1356
+ message: "codex is not installed"
1357
+ };
1358
+ }
1359
+ const version = await Promise.race([
1360
+ this.getVersion(),
1361
+ new Promise(
1362
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1363
+ )
1364
+ ]).catch(() => void 0);
1365
+ let authenticated = true;
1366
+ try {
1367
+ const result = await Promise.race([
1368
+ this.processManager.execute(this.command, ["login", "status"]),
1369
+ new Promise(
1370
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1371
+ )
1372
+ ]);
1373
+ authenticated = result.exitCode === 0;
1374
+ } catch {
1375
+ authenticated = true;
1376
+ }
1377
+ return {
1378
+ installed: true,
1379
+ authenticated,
1380
+ healthy: authenticated,
1381
+ version,
1382
+ ...!authenticated ? { message: "codex authentication not configured" } : {}
1383
+ };
1384
+ }
457
1385
  mapFlags(flags) {
458
1386
  const args = mapCommonToNative("codex", flags);
459
1387
  if (flags.outputFormat === "json") {
@@ -471,16 +1399,41 @@ var CodexAdapter = class extends BaseAdapter {
471
1399
  }
472
1400
  await this.processManager.spawnInteractive(this.command, args);
473
1401
  }
1402
+ /**
1403
+ * Resolve the effective system prompt from flags.
1404
+ * Priority: systemPrompt > agent fallback > none
1405
+ */
1406
+ resolveSystemPrompt(flags) {
1407
+ if (flags.systemPrompt) return flags.systemPrompt;
1408
+ if (flags.agent) {
1409
+ return `You are acting as the "${flags.agent}" agent. Follow the instructions and role defined for this agent.`;
1410
+ }
1411
+ return void 0;
1412
+ }
1413
+ /**
1414
+ * Build the effective prompt with system instructions prepended if needed.
1415
+ * Codex SDK does not support a native instructions/systemPrompt parameter,
1416
+ * so we inject role context via a prompt prefix.
1417
+ */
1418
+ buildEffectivePrompt(prompt, systemPrompt) {
1419
+ if (!systemPrompt) return prompt;
1420
+ return `[System Instructions]
1421
+ ${systemPrompt}
1422
+
1423
+ [User Request]
1424
+ ${prompt}`;
1425
+ }
474
1426
  async execute(flags) {
475
1427
  if (!flags.prompt) {
476
1428
  throw new Error("execute requires a prompt (-p flag)");
477
1429
  }
478
- if (flags.agent) {
479
- logger.warn(
480
- `Codex CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
481
- );
482
- }
1430
+ const systemPrompt = this.resolveSystemPrompt(flags);
1431
+ const effectivePrompt = this.buildEffectivePrompt(
1432
+ flags.prompt,
1433
+ systemPrompt
1434
+ );
483
1435
  try {
1436
+ const { Codex } = await loadCodexSDK();
484
1437
  const codexOptions = {};
485
1438
  if (flags.mcpContext) {
486
1439
  codexOptions.env = {
@@ -496,11 +1449,12 @@ var CodexAdapter = class extends BaseAdapter {
496
1449
  workingDirectory: process.cwd(),
497
1450
  approvalPolicy: "never"
498
1451
  });
499
- const result = await thread.run(flags.prompt);
1452
+ const result = await thread.run(effectivePrompt);
500
1453
  return {
501
1454
  exitCode: 0,
502
1455
  stdout: result.finalResponse,
503
- stderr: ""
1456
+ stderr: "",
1457
+ ...thread.id ? { nativeSessionId: thread.id } : {}
504
1458
  };
505
1459
  } catch (error) {
506
1460
  return {
@@ -514,12 +1468,13 @@ var CodexAdapter = class extends BaseAdapter {
514
1468
  if (!flags.prompt) {
515
1469
  throw new Error("executeStreaming requires a prompt (-p flag)");
516
1470
  }
517
- if (flags.agent) {
518
- logger.warn(
519
- `Codex CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
520
- );
521
- }
1471
+ const systemPrompt = this.resolveSystemPrompt(flags);
1472
+ const effectivePrompt = this.buildEffectivePrompt(
1473
+ flags.prompt,
1474
+ systemPrompt
1475
+ );
522
1476
  try {
1477
+ const { Codex } = await loadCodexSDK();
523
1478
  const codexOptions = {};
524
1479
  if (flags.mcpContext) {
525
1480
  codexOptions.env = {
@@ -535,10 +1490,13 @@ var CodexAdapter = class extends BaseAdapter {
535
1490
  workingDirectory: process.cwd(),
536
1491
  approvalPolicy: "never"
537
1492
  });
538
- const streamedTurn = await thread.runStreamed(flags.prompt);
1493
+ const streamedTurn = await thread.runStreamed(effectivePrompt);
539
1494
  const completedMessages = [];
1495
+ let threadId;
540
1496
  for await (const event of streamedTurn.events) {
541
- if (event.type === "item.started") {
1497
+ if (event.type === "thread.started") {
1498
+ threadId = event.thread_id;
1499
+ } else if (event.type === "item.started") {
542
1500
  const item = event.item;
543
1501
  if (item?.type === "agent_message" && item.text) {
544
1502
  yield { type: "text", text: item.text };
@@ -586,13 +1544,15 @@ var CodexAdapter = class extends BaseAdapter {
586
1544
  const finalResponse = completedMessages.join("\n");
587
1545
  yield {
588
1546
  type: "done",
589
- result: { exitCode: 0, stdout: finalResponse, stderr: "" }
1547
+ result: { exitCode: 0, stdout: finalResponse, stderr: "" },
1548
+ nativeSessionId: threadId ?? thread.id ?? void 0
590
1549
  };
591
1550
  } else if (event.type === "turn.failed") {
592
1551
  const errorMessage = event.error?.message ?? "Turn failed";
593
1552
  yield {
594
1553
  type: "done",
595
- result: { exitCode: 1, stdout: "", stderr: errorMessage }
1554
+ result: { exitCode: 1, stdout: "", stderr: errorMessage },
1555
+ nativeSessionId: threadId ?? thread.id ?? void 0
596
1556
  };
597
1557
  } else if (event.type === "error") {
598
1558
  yield {
@@ -610,6 +1570,28 @@ var CodexAdapter = class extends BaseAdapter {
610
1570
  };
611
1571
  }
612
1572
  }
1573
+ async continueSession(nativeSessionId, prompt) {
1574
+ try {
1575
+ const { Codex } = await loadCodexSDK();
1576
+ const codex = new Codex();
1577
+ const thread = codex.resumeThread(nativeSessionId, {
1578
+ workingDirectory: process.cwd(),
1579
+ approvalPolicy: "never"
1580
+ });
1581
+ const result = await thread.run(prompt);
1582
+ return {
1583
+ exitCode: 0,
1584
+ stdout: result.finalResponse,
1585
+ stderr: ""
1586
+ };
1587
+ } catch (error) {
1588
+ return {
1589
+ exitCode: 1,
1590
+ stdout: "",
1591
+ stderr: error instanceof Error ? error.message : String(error)
1592
+ };
1593
+ }
1594
+ }
613
1595
  async resumeSession(sessionId, flags) {
614
1596
  const args = [];
615
1597
  if (flags.model) {
@@ -647,12 +1629,94 @@ var CodexAdapter = class extends BaseAdapter {
647
1629
  logger.success(`Codex CLI updated: ${result.stdout.trim()}`);
648
1630
  }
649
1631
  }
1632
+ async getMCPConfig() {
1633
+ const configPath = this.getConfigPath();
1634
+ try {
1635
+ const raw = await readFile2(configPath, "utf-8");
1636
+ const parsed = parseTOMLMcpServers(raw);
1637
+ return Array.from(parsed.mcpServers.entries()).map(
1638
+ ([name, entry]) => {
1639
+ const server = {
1640
+ name,
1641
+ command: entry.command
1642
+ };
1643
+ if (entry.args && entry.args.length > 0) {
1644
+ server.args = entry.args;
1645
+ }
1646
+ return server;
1647
+ }
1648
+ );
1649
+ } catch {
1650
+ return [];
1651
+ }
1652
+ }
1653
+ async setMCPConfig(servers) {
1654
+ const configPath = this.getConfigPath();
1655
+ let existingContent = "";
1656
+ try {
1657
+ existingContent = await readFile2(configPath, "utf-8");
1658
+ } catch {
1659
+ }
1660
+ const parsed = parseTOMLMcpServers(existingContent);
1661
+ let preamble = parsed.preambleLines.join("\n");
1662
+ preamble = preamble.replace(/\n+$/, "");
1663
+ const mcpSection = generateMcpServersTOML(servers);
1664
+ let postamble = parsed.postambleLines.join("\n");
1665
+ postamble = postamble.replace(/^\n+/, "");
1666
+ const parts = [];
1667
+ if (preamble) parts.push(preamble);
1668
+ if (mcpSection) parts.push(mcpSection);
1669
+ if (postamble) parts.push(postamble);
1670
+ let output = parts.join("\n\n");
1671
+ if (!output.endsWith("\n")) output += "\n";
1672
+ await mkdir2(dirname2(configPath), { recursive: true });
1673
+ await writeFile2(configPath, output, { mode: 384 });
1674
+ }
650
1675
  };
651
1676
 
652
1677
  // src/adapters/gemini-adapter.ts
1678
+ init_logger();
1679
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1680
+ import { homedir as homedir3 } from "os";
1681
+ import { join as join3, dirname as dirname3 } from "path";
653
1682
  var GeminiAdapter = class extends BaseAdapter {
654
1683
  id = "gemini";
655
1684
  command = "gemini";
1685
+ getConfigPath() {
1686
+ return join3(homedir3(), ".gemini", "settings.json");
1687
+ }
1688
+ async checkHealth() {
1689
+ const HEALTH_TIMEOUT = 5e3;
1690
+ const installed = await Promise.race([
1691
+ this.isInstalled(),
1692
+ new Promise(
1693
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1694
+ )
1695
+ ]).catch(() => false);
1696
+ if (!installed) {
1697
+ return {
1698
+ installed: false,
1699
+ authenticated: false,
1700
+ healthy: false,
1701
+ message: "gemini is not installed"
1702
+ };
1703
+ }
1704
+ const version = await Promise.race([
1705
+ this.getVersion(),
1706
+ new Promise(
1707
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1708
+ )
1709
+ ]).catch(() => void 0);
1710
+ const hasApiKey = !!process.env["GEMINI_API_KEY"];
1711
+ const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
1712
+ const authenticated = hasApiKey || hasGoogleAdc || true;
1713
+ return {
1714
+ installed: true,
1715
+ authenticated,
1716
+ healthy: true,
1717
+ version
1718
+ };
1719
+ }
656
1720
  mapFlags(flags) {
657
1721
  const args = mapCommonToNative("gemini", flags);
658
1722
  if (flags.outputFormat) {
@@ -673,15 +1737,34 @@ var GeminiAdapter = class extends BaseAdapter {
673
1737
  }
674
1738
  await this.processManager.spawnInteractive(this.command, args);
675
1739
  }
1740
+ /**
1741
+ * Resolve the effective prompt with system instructions prepended if needed.
1742
+ * Gemini CLI has no native system prompt flag, so we use a prompt prefix.
1743
+ * Priority: systemPrompt > agent fallback > none
1744
+ */
1745
+ buildEffectivePrompt(flags) {
1746
+ const prompt = flags.prompt;
1747
+ if (flags.systemPrompt) {
1748
+ return `[System Instructions]
1749
+ ${flags.systemPrompt}
1750
+
1751
+ [User Request]
1752
+ ${prompt}`;
1753
+ }
1754
+ if (flags.agent) {
1755
+ return `[System Instructions]
1756
+ You are acting as the "${flags.agent}" agent.
1757
+
1758
+ [User Request]
1759
+ ${prompt}`;
1760
+ }
1761
+ return prompt;
1762
+ }
676
1763
  async execute(flags) {
677
1764
  if (!flags.prompt) {
678
1765
  throw new Error("execute requires a prompt (-p flag)");
679
1766
  }
680
- if (flags.agent) {
681
- logger.warn(
682
- `Gemini CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
683
- );
684
- }
1767
+ const effectivePrompt = this.buildEffectivePrompt(flags);
685
1768
  const args = [];
686
1769
  if (flags.model) {
687
1770
  args.push("--model", flags.model);
@@ -692,7 +1775,7 @@ var GeminiAdapter = class extends BaseAdapter {
692
1775
  if (flags.verbose) {
693
1776
  args.push("--verbose");
694
1777
  }
695
- args.push("-p", flags.prompt);
1778
+ args.push("-p", effectivePrompt);
696
1779
  return this.processManager.execute(this.command, args);
697
1780
  }
698
1781
  async resumeSession(sessionId, flags) {
@@ -756,18 +1839,64 @@ var GeminiAdapter = class extends BaseAdapter {
756
1839
  logger.success(`Gemini CLI updated: ${result.stdout.trim()}`);
757
1840
  }
758
1841
  }
1842
+ async getMCPConfig() {
1843
+ const configPath = this.getConfigPath();
1844
+ try {
1845
+ const raw = await readFile3(configPath, "utf-8");
1846
+ const config = JSON.parse(raw);
1847
+ const mcpServers = config.mcpServers;
1848
+ if (!mcpServers || typeof mcpServers !== "object") {
1849
+ return [];
1850
+ }
1851
+ return Object.entries(mcpServers).map(([name, entry]) => {
1852
+ const server = {
1853
+ name,
1854
+ command: entry.command
1855
+ };
1856
+ if (entry.args) server.args = entry.args;
1857
+ if (entry.env) server.env = entry.env;
1858
+ return server;
1859
+ });
1860
+ } catch {
1861
+ return [];
1862
+ }
1863
+ }
1864
+ async setMCPConfig(servers) {
1865
+ const configPath = this.getConfigPath();
1866
+ let config = {};
1867
+ try {
1868
+ const raw = await readFile3(configPath, "utf-8");
1869
+ config = JSON.parse(raw);
1870
+ } catch {
1871
+ }
1872
+ const mcpServers = {};
1873
+ for (const server of servers) {
1874
+ const entry = {
1875
+ command: server.command
1876
+ };
1877
+ if (server.args) entry.args = server.args;
1878
+ if (server.env) entry.env = server.env;
1879
+ const serverName = server.name ?? server.command;
1880
+ mcpServers[serverName] = entry;
1881
+ }
1882
+ config.mcpServers = mcpServers;
1883
+ await mkdir3(dirname3(configPath), { recursive: true });
1884
+ await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", {
1885
+ mode: 384
1886
+ });
1887
+ }
759
1888
  };
760
1889
 
761
1890
  // src/core/session-manager.ts
762
- import { readFile, writeFile, readdir, mkdir, chmod } from "fs/promises";
763
- import { join } from "path";
764
- import { homedir } from "os";
1891
+ import { readFile as readFile4, writeFile as writeFile4, readdir, mkdir as mkdir4, chmod } from "fs/promises";
1892
+ import { join as join4 } from "path";
1893
+ import { homedir as homedir4 } from "os";
765
1894
  import { nanoid } from "nanoid";
766
1895
  function getRelayHome() {
767
- return process.env["RELAY_HOME"] ?? join(homedir(), ".relay");
1896
+ return process.env["RELAY_HOME"] ?? join4(homedir4(), ".relay");
768
1897
  }
769
1898
  function getSessionsDir(relayHome2) {
770
- return join(relayHome2, "sessions");
1899
+ return join4(relayHome2, "sessions");
771
1900
  }
772
1901
  function toSessionData(session) {
773
1902
  return {
@@ -791,13 +1920,13 @@ var SessionManager = class _SessionManager {
791
1920
  }
792
1921
  /** Ensure the sessions directory exists. */
793
1922
  async ensureDir() {
794
- await mkdir(this.sessionsDir, { recursive: true });
1923
+ await mkdir4(this.sessionsDir, { recursive: true });
795
1924
  }
796
1925
  sessionPath(relaySessionId) {
797
1926
  if (!_SessionManager.SESSION_ID_PATTERN.test(relaySessionId)) {
798
1927
  throw new Error(`Invalid session ID: ${relaySessionId}`);
799
1928
  }
800
- return join(this.sessionsDir, `${relaySessionId}.json`);
1929
+ return join4(this.sessionsDir, `${relaySessionId}.json`);
801
1930
  }
802
1931
  /** Create a new relay session. */
803
1932
  async create(params) {
@@ -814,7 +1943,7 @@ var SessionManager = class _SessionManager {
814
1943
  status: "active"
815
1944
  };
816
1945
  const sessionFilePath = this.sessionPath(session.relaySessionId);
817
- await writeFile(
1946
+ await writeFile4(
818
1947
  sessionFilePath,
819
1948
  JSON.stringify(toSessionData(session), null, 2),
820
1949
  "utf-8"
@@ -834,7 +1963,7 @@ var SessionManager = class _SessionManager {
834
1963
  updatedAt: /* @__PURE__ */ new Date()
835
1964
  };
836
1965
  const updateFilePath = this.sessionPath(relaySessionId);
837
- await writeFile(
1966
+ await writeFile4(
838
1967
  updateFilePath,
839
1968
  JSON.stringify(toSessionData(updated), null, 2),
840
1969
  "utf-8"
@@ -845,7 +1974,7 @@ var SessionManager = class _SessionManager {
845
1974
  async get(relaySessionId) {
846
1975
  const filePath = this.sessionPath(relaySessionId);
847
1976
  try {
848
- const raw = await readFile(filePath, "utf-8");
1977
+ const raw = await readFile4(filePath, "utf-8");
849
1978
  return fromSessionData(JSON.parse(raw));
850
1979
  } catch {
851
1980
  return null;
@@ -869,7 +1998,7 @@ var SessionManager = class _SessionManager {
869
1998
  const sessions = [];
870
1999
  for (const file of jsonFiles) {
871
2000
  try {
872
- const raw = await readFile(join(this.sessionsDir, file), "utf-8");
2001
+ const raw = await readFile4(join4(this.sessionsDir, file), "utf-8");
873
2002
  const session = fromSessionData(JSON.parse(raw));
874
2003
  if (filter?.backendId && session.backendId !== filter.backendId) {
875
2004
  continue;
@@ -889,14 +2018,14 @@ var SessionManager = class _SessionManager {
889
2018
  };
890
2019
 
891
2020
  // src/core/config-manager.ts
892
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
893
- import { join as join2 } from "path";
2021
+ import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir5, chmod as chmod2 } from "fs/promises";
2022
+ import { join as join5 } from "path";
894
2023
 
895
2024
  // src/schemas/config.schema.ts
896
2025
  import { z } from "zod";
897
2026
  var backendIdSchema = z.enum(["claude", "codex", "gemini"]);
898
2027
  var mcpServerConfigSchema = z.object({
899
- name: z.string(),
2028
+ name: z.string().optional(),
900
2029
  command: z.string(),
901
2030
  args: z.array(z.string()).optional(),
902
2031
  env: z.record(z.string()).optional()
@@ -920,6 +2049,10 @@ var hookDefinitionSchema = z.object({
920
2049
  var hooksConfigSchema = z.object({
921
2050
  definitions: z.array(hookDefinitionSchema)
922
2051
  });
2052
+ var backendContextConfigSchema = z.object({
2053
+ contextWindow: z.number().positive().optional(),
2054
+ compactThreshold: z.number().positive().optional()
2055
+ }).optional();
923
2056
  var relayConfigSchema = z.object({
924
2057
  defaultBackend: backendIdSchema.optional(),
925
2058
  mcpServers: z.record(mcpServerConfigSchema).optional(),
@@ -930,9 +2063,16 @@ var relayConfigSchema = z.object({
930
2063
  }).optional(),
931
2064
  hooks: hooksConfigSchema.optional(),
932
2065
  contextMonitor: z.object({
933
- enabled: z.boolean(),
934
- thresholdPercent: z.number().min(0).max(100),
935
- notifyMethod: z.enum(["stderr", "hook"])
2066
+ enabled: z.boolean().optional(),
2067
+ thresholdPercent: z.number().min(0).max(100).optional(),
2068
+ notifyThreshold: z.number().positive().optional(),
2069
+ notifyPercent: z.number().min(0).max(100).optional(),
2070
+ notifyMethod: z.enum(["stderr", "hook"]).optional(),
2071
+ backends: z.object({
2072
+ claude: backendContextConfigSchema,
2073
+ codex: backendContextConfigSchema,
2074
+ gemini: backendContextConfigSchema
2075
+ }).optional()
936
2076
  }).optional(),
937
2077
  mcpServerMode: z.object({
938
2078
  maxDepth: z.number().int().positive(),
@@ -946,6 +2086,7 @@ var relayConfigSchema = z.object({
946
2086
  });
947
2087
 
948
2088
  // src/core/config-manager.ts
2089
+ init_logger();
949
2090
  function deepMerge(target, source) {
950
2091
  const result = { ...target };
951
2092
  for (const key of Object.keys(source)) {
@@ -1033,8 +2174,8 @@ var ConfigManager = class {
1033
2174
  const existing = await this.readConfigFile(filePath);
1034
2175
  setByPath(existing, key, value);
1035
2176
  const dir = filePath.substring(0, filePath.lastIndexOf("/"));
1036
- await mkdir2(dir, { recursive: true });
1037
- await writeFile2(filePath, JSON.stringify(existing, null, 2), "utf-8");
2177
+ await mkdir5(dir, { recursive: true });
2178
+ await writeFile5(filePath, JSON.stringify(existing, null, 2), "utf-8");
1038
2179
  await chmod2(filePath, 384);
1039
2180
  }
1040
2181
  /**
@@ -1043,7 +2184,12 @@ var ConfigManager = class {
1043
2184
  async syncMCPConfig(registry2) {
1044
2185
  const config = await this.getConfig();
1045
2186
  const mcpServers = config.mcpServers ?? {};
1046
- const servers = Object.values(mcpServers);
2187
+ const servers = Object.entries(mcpServers).map(
2188
+ ([key, server]) => ({
2189
+ ...server,
2190
+ name: server.name || key
2191
+ })
2192
+ );
1047
2193
  const adapters = registry2.list();
1048
2194
  for (const adapter of adapters) {
1049
2195
  try {
@@ -1060,11 +2206,11 @@ var ConfigManager = class {
1060
2206
  getFilePath(scope) {
1061
2207
  switch (scope) {
1062
2208
  case "global":
1063
- return join2(this.globalDir, "config.json");
2209
+ return join5(this.globalDir, "config.json");
1064
2210
  case "project":
1065
- return join2(this.projectDir ?? "", "config.json");
2211
+ return join5(this.projectDir ?? "", "config.json");
1066
2212
  case "local":
1067
- return join2(this.projectDir ?? "", "config.local.json");
2213
+ return join5(this.projectDir ?? "", "config.local.json");
1068
2214
  }
1069
2215
  }
1070
2216
  mapConfigScope(scope) {
@@ -1081,7 +2227,7 @@ var ConfigManager = class {
1081
2227
  }
1082
2228
  async readConfigFile(filePath) {
1083
2229
  try {
1084
- const raw = await readFile2(filePath, "utf-8");
2230
+ const raw = await readFile5(filePath, "utf-8");
1085
2231
  const parsed = JSON.parse(raw);
1086
2232
  if (!isPlainObject(parsed)) return {};
1087
2233
  return parsed;
@@ -1110,6 +2256,7 @@ var ConfigManager = class {
1110
2256
  };
1111
2257
 
1112
2258
  // src/core/auth-manager.ts
2259
+ init_logger();
1113
2260
  var AuthManager = class {
1114
2261
  constructor(registry2) {
1115
2262
  this.registry = registry2;
@@ -1183,6 +2330,7 @@ var EventBus = class {
1183
2330
  };
1184
2331
 
1185
2332
  // src/core/hooks-engine.ts
2333
+ init_logger();
1186
2334
  var DEFAULT_TIMEOUT_MS = 1e4;
1187
2335
  var DEFAULT_HOOK_OUTPUT = {
1188
2336
  allow: true,
@@ -1320,34 +2468,75 @@ var HooksEngine = class _HooksEngine {
1320
2468
  };
1321
2469
 
1322
2470
  // src/core/context-monitor.ts
2471
+ var DEFAULT_BACKEND_CONTEXT = {
2472
+ claude: { contextWindow: 2e5, compactThreshold: 19e4 },
2473
+ codex: { contextWindow: 272e3, compactThreshold: 258400 },
2474
+ gemini: { contextWindow: 1048576, compactThreshold: 524288 }
2475
+ };
2476
+ var DEFAULT_NOTIFY_PERCENT = 70;
1323
2477
  var DEFAULT_CONFIG = {
1324
2478
  enabled: true,
1325
- thresholdPercent: 75,
1326
- notifyMethod: "stderr"
2479
+ notifyMethod: "hook"
1327
2480
  };
1328
2481
  var ContextMonitor = class {
1329
2482
  constructor(hooksEngine2, config) {
1330
2483
  this.hooksEngine = hooksEngine2;
1331
2484
  this.config = { ...DEFAULT_CONFIG, ...config };
2485
+ if (this.config.thresholdPercent !== void 0 && this.config.notifyPercent === void 0 && this.config.notifyThreshold === void 0) {
2486
+ this.config.notifyPercent = this.config.thresholdPercent;
2487
+ }
1332
2488
  }
1333
2489
  config;
1334
2490
  usageMap = /* @__PURE__ */ new Map();
2491
+ /** Get backend context config, merging user overrides with defaults */
2492
+ getBackendConfig(backendId) {
2493
+ const defaults = DEFAULT_BACKEND_CONTEXT[backendId];
2494
+ const overrides = this.config.backends?.[backendId];
2495
+ return {
2496
+ contextWindow: overrides?.contextWindow ?? defaults.contextWindow,
2497
+ compactThreshold: overrides?.compactThreshold ?? defaults.compactThreshold
2498
+ };
2499
+ }
2500
+ /** Calculate the notification threshold in tokens for a given backend */
2501
+ getNotifyThreshold(backendId) {
2502
+ if (this.config.notifyThreshold !== void 0) {
2503
+ return this.config.notifyThreshold;
2504
+ }
2505
+ const backendConfig = this.getBackendConfig(backendId);
2506
+ const notifyPercent = this.config.notifyPercent ?? DEFAULT_NOTIFY_PERCENT;
2507
+ return Math.round(backendConfig.contextWindow * notifyPercent / 100);
2508
+ }
1335
2509
  /** Update token usage for a session and check threshold */
1336
- updateUsage(sessionId, backendId, estimatedTokens, maxTokens) {
2510
+ updateUsage(sessionId, backendId, estimatedTokens) {
1337
2511
  if (!this.config.enabled) return;
1338
- const usagePercent = maxTokens > 0 ? Math.round(estimatedTokens / maxTokens * 100) : 0;
2512
+ const backendConfig = this.getBackendConfig(backendId);
2513
+ const contextWindow = backendConfig.contextWindow;
2514
+ const usagePercent = contextWindow > 0 ? Math.round(estimatedTokens / contextWindow * 100) : 0;
1339
2515
  const existing = this.usageMap.get(sessionId);
1340
- const wasNotified = existing?.notified ?? false;
2516
+ let wasNotified = existing?.notified ?? false;
2517
+ if (existing && estimatedTokens < existing.estimatedTokens * 0.7) {
2518
+ wasNotified = false;
2519
+ }
1341
2520
  this.usageMap.set(sessionId, {
1342
2521
  estimatedTokens,
1343
- maxTokens,
2522
+ contextWindow,
2523
+ compactThreshold: backendConfig.compactThreshold,
1344
2524
  usagePercent,
1345
2525
  backendId,
1346
2526
  notified: wasNotified
1347
2527
  });
1348
- if (usagePercent >= this.config.thresholdPercent && !wasNotified) {
1349
- this.usageMap.get(sessionId).notified = true;
1350
- this.notify(sessionId, backendId, usagePercent);
2528
+ const notifyAt = this.getNotifyThreshold(backendId);
2529
+ if (estimatedTokens >= notifyAt && !wasNotified) {
2530
+ const entry = this.usageMap.get(sessionId);
2531
+ entry.notified = true;
2532
+ this.notify(
2533
+ sessionId,
2534
+ backendId,
2535
+ usagePercent,
2536
+ estimatedTokens,
2537
+ contextWindow,
2538
+ backendConfig.compactThreshold
2539
+ );
1351
2540
  }
1352
2541
  }
1353
2542
  /** Get usage info for a session */
@@ -1356,17 +2545,31 @@ var ContextMonitor = class {
1356
2545
  if (!entry) return null;
1357
2546
  return {
1358
2547
  usagePercent: entry.usagePercent,
1359
- isEstimated: true
2548
+ isEstimated: true,
2549
+ backendId: entry.backendId,
2550
+ contextWindow: entry.contextWindow,
2551
+ compactThreshold: entry.compactThreshold,
2552
+ estimatedTokens: entry.estimatedTokens,
2553
+ remainingBeforeCompact: Math.max(
2554
+ 0,
2555
+ entry.compactThreshold - entry.estimatedTokens
2556
+ ),
2557
+ notifyThreshold: this.getNotifyThreshold(entry.backendId)
1360
2558
  };
1361
2559
  }
1362
2560
  /** Remove usage tracking for a session */
1363
2561
  removeSession(sessionId) {
1364
2562
  this.usageMap.delete(sessionId);
1365
2563
  }
1366
- notify(sessionId, backendId, usagePercent) {
2564
+ notify(sessionId, backendId, usagePercent, currentTokens, contextWindow, compactThreshold) {
2565
+ const remainingBeforeCompact = Math.max(
2566
+ 0,
2567
+ compactThreshold - currentTokens
2568
+ );
2569
+ const warningMessage = `${backendId} session ${sessionId} at ${usagePercent}% (${currentTokens}/${contextWindow} tokens). Compact in ~${remainingBeforeCompact} tokens. Save your work state now.`;
1367
2570
  if (this.config.notifyMethod === "stderr") {
1368
2571
  process.stderr.write(
1369
- `[relay] Context usage warning: session ${sessionId} is at ${usagePercent}% (threshold: ${this.config.thresholdPercent}%)
2572
+ `[relay] Context warning: ${warningMessage}
1370
2573
  `
1371
2574
  );
1372
2575
  } else if (this.config.notifyMethod === "hook" && this.hooksEngine) {
@@ -1377,7 +2580,10 @@ var ContextMonitor = class {
1377
2580
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1378
2581
  data: {
1379
2582
  usagePercent,
1380
- thresholdPercent: this.config.thresholdPercent
2583
+ currentTokens,
2584
+ contextWindow,
2585
+ compactThreshold,
2586
+ remainingBeforeCompact
1381
2587
  }
1382
2588
  };
1383
2589
  void this.hooksEngine.emit("on-context-threshold", hookInput);
@@ -1386,6 +2592,7 @@ var ContextMonitor = class {
1386
2592
  };
1387
2593
 
1388
2594
  // src/commands/backend.ts
2595
+ init_logger();
1389
2596
  import { defineCommand } from "citty";
1390
2597
 
1391
2598
  // src/adapters/install-guides.ts
@@ -1437,6 +2644,11 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
1437
2644
  verbose: {
1438
2645
  type: "boolean",
1439
2646
  description: "Enable verbose output"
2647
+ },
2648
+ dryRun: {
2649
+ type: "boolean",
2650
+ description: "Show what would be executed without running it",
2651
+ default: false
1440
2652
  }
1441
2653
  },
1442
2654
  async run({ args }) {
@@ -1460,6 +2672,23 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
1460
2672
  flags.outputFormat = args.outputFormat;
1461
2673
  }
1462
2674
  if (args.verbose) flags.verbose = true;
2675
+ if (args.dryRun) {
2676
+ const nativeFlags = adapter.mapFlags(flags);
2677
+ const dryRunOutput = {
2678
+ backend: backendId,
2679
+ installed: await adapter.isInstalled(),
2680
+ nativeArgs: nativeFlags.args,
2681
+ relayEnv: {
2682
+ RELAY_HOME: process.env["RELAY_HOME"] ?? "~/.relay",
2683
+ RELAY_LOG_LEVEL: process.env["RELAY_LOG_LEVEL"] ?? "info"
2684
+ },
2685
+ sessionTracking: !!sessionManager2,
2686
+ hooksEnabled: !!hooksEngine2,
2687
+ contextMonitorEnabled: !!contextMonitor2
2688
+ };
2689
+ console.log(JSON.stringify(dryRunOutput, null, 2));
2690
+ return;
2691
+ }
1463
2692
  let relaySessionId;
1464
2693
  if (sessionManager2) {
1465
2694
  try {
@@ -1491,6 +2720,7 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
1491
2720
  try {
1492
2721
  if (flags.prompt) {
1493
2722
  logger.debug(`Executing prompt on ${backendId}`);
2723
+ let nativeSessionId;
1494
2724
  if (adapter.executeStreaming) {
1495
2725
  for await (const event of adapter.executeStreaming(flags)) {
1496
2726
  switch (event.type) {
@@ -1521,18 +2751,25 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
1521
2751
  break;
1522
2752
  case "usage": {
1523
2753
  if (contextMonitor2 && relaySessionId) {
1524
- const maxTokens = backendId === "gemini" ? 128e3 : 2e5;
1525
2754
  contextMonitor2.updateUsage(
1526
2755
  relaySessionId,
1527
2756
  backendId,
1528
- event.inputTokens + event.outputTokens,
1529
- maxTokens
2757
+ event.inputTokens + event.outputTokens
1530
2758
  );
1531
2759
  }
1532
2760
  break;
1533
2761
  }
1534
2762
  case "done":
1535
2763
  process.exitCode = event.result.exitCode;
2764
+ nativeSessionId = event.nativeSessionId;
2765
+ if (event.nativeSessionId && sessionManager2 && relaySessionId) {
2766
+ try {
2767
+ await sessionManager2.update(relaySessionId, {
2768
+ nativeSessionId: event.nativeSessionId
2769
+ });
2770
+ } catch {
2771
+ }
2772
+ }
1536
2773
  break;
1537
2774
  }
1538
2775
  }
@@ -1541,6 +2778,15 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
1541
2778
  if (result.stdout) process.stdout.write(result.stdout);
1542
2779
  if (result.stderr) process.stderr.write(result.stderr);
1543
2780
  process.exitCode = result.exitCode;
2781
+ nativeSessionId = result.nativeSessionId;
2782
+ if (result.nativeSessionId && sessionManager2 && relaySessionId) {
2783
+ try {
2784
+ await sessionManager2.update(relaySessionId, {
2785
+ nativeSessionId: result.nativeSessionId
2786
+ });
2787
+ } catch {
2788
+ }
2789
+ }
1544
2790
  }
1545
2791
  } else if (flags.continue) {
1546
2792
  logger.debug(`Continuing latest session on ${backendId}`);
@@ -1614,642 +2860,187 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
1614
2860
  }
1615
2861
  }
1616
2862
  throw error;
1617
- }
1618
- }
1619
- });
1620
- }
1621
-
1622
- // src/commands/update.ts
1623
- import { defineCommand as defineCommand2 } from "citty";
1624
-
1625
- // src/infrastructure/version-check.ts
1626
- async function getLatestNpmVersion(packageName) {
1627
- try {
1628
- const response = await fetch(
1629
- `https://registry.npmjs.org/${packageName}/latest`,
1630
- {
1631
- headers: { Accept: "application/json" },
1632
- signal: AbortSignal.timeout(5e3)
1633
- }
1634
- );
1635
- if (!response.ok) {
1636
- logger.debug(
1637
- `npm registry returned ${response.status} for ${packageName}`
1638
- );
1639
- return null;
1640
- }
1641
- const data = await response.json();
1642
- return data.version ?? null;
1643
- } catch (error) {
1644
- logger.debug(
1645
- `Failed to check npm registry: ${error instanceof Error ? error.message : String(error)}`
1646
- );
1647
- return null;
1648
- }
1649
- }
1650
- function compareSemver(a, b) {
1651
- const partsA = a.replace(/^v/, "").split(".").map(Number);
1652
- const partsB = b.replace(/^v/, "").split(".").map(Number);
1653
- for (let i = 0; i < 3; i++) {
1654
- const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
1655
- if (diff !== 0) return diff;
1656
- }
1657
- return 0;
1658
- }
1659
-
1660
- // src/commands/update.ts
1661
- var PACKAGE_NAME = "@rk0429/agentic-relay";
1662
- var CURRENT_VERSION = "0.3.0";
1663
- function createUpdateCommand(registry2) {
1664
- return defineCommand2({
1665
- meta: {
1666
- name: "update",
1667
- description: "Update relay and all installed backend CLI tools"
1668
- },
1669
- async run() {
1670
- logger.info("Checking for relay updates...");
1671
- const latestVersion = await getLatestNpmVersion(PACKAGE_NAME);
1672
- if (latestVersion) {
1673
- if (compareSemver(latestVersion, CURRENT_VERSION) > 0) {
1674
- logger.warn(
1675
- `A newer version of relay is available: ${latestVersion} (current: ${CURRENT_VERSION})`
1676
- );
1677
- logger.info(` Run: npm install -g ${PACKAGE_NAME}@latest`);
1678
- } else {
1679
- logger.success(`relay is up to date (${CURRENT_VERSION})`);
1680
- }
1681
- } else {
1682
- logger.debug("Could not check for relay updates");
1683
- }
1684
- const adapters = registry2.list();
1685
- if (adapters.length === 0) {
1686
- logger.warn("No backends registered");
1687
- return;
1688
- }
1689
- for (const adapter of adapters) {
1690
- const installed = await adapter.isInstalled();
1691
- if (!installed) {
1692
- logger.info(`Skipping ${adapter.id}: not installed`);
1693
- continue;
1694
- }
1695
- logger.info(`Updating ${adapter.id}...`);
1696
- try {
1697
- await adapter.update();
1698
- } catch (error) {
1699
- logger.error(
1700
- `Failed to update ${adapter.id}: ${error instanceof Error ? error.message : String(error)}`
1701
- );
1702
- }
1703
- }
1704
- }
1705
- });
1706
- }
1707
-
1708
- // src/commands/config.ts
1709
- import { defineCommand as defineCommand3 } from "citty";
1710
- function createConfigCommand(configManager2) {
1711
- return defineCommand3({
1712
- meta: {
1713
- name: "config",
1714
- description: "Manage relay configuration"
1715
- },
1716
- subCommands: {
1717
- get: defineCommand3({
1718
- meta: {
1719
- name: "get",
1720
- description: "Get a config value by key (dot-path notation)"
1721
- },
1722
- args: {
1723
- key: {
1724
- type: "positional",
1725
- description: "Config key (e.g. mcpServerMode.maxDepth)",
1726
- required: true
1727
- }
1728
- },
1729
- async run({ args }) {
1730
- const key = String(args.key);
1731
- const value = await configManager2.get(key);
1732
- if (value === void 0) {
1733
- logger.info(`Key "${key}" is not set`);
1734
- } else {
1735
- const output = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
1736
- console.log(output);
1737
- }
1738
- }
1739
- }),
1740
- set: defineCommand3({
1741
- meta: {
1742
- name: "set",
1743
- description: "Set a config value (default: global scope)"
1744
- },
1745
- args: {
1746
- project: {
1747
- type: "boolean",
1748
- description: "Write to project scope instead of global"
1749
- },
1750
- key: {
1751
- type: "positional",
1752
- description: "Config key (e.g. defaultBackend)",
1753
- required: true
1754
- },
1755
- value: {
1756
- type: "positional",
1757
- description: "Config value",
1758
- required: true
1759
- }
1760
- },
1761
- async run({ args }) {
1762
- const scope = args.project ? "project" : "global";
1763
- const key = String(args.key);
1764
- const rawValue = String(args.value);
1765
- const parsed = parseValue(rawValue);
1766
- await configManager2.set(scope, key, parsed);
1767
- logger.success(`Set ${key} = ${JSON.stringify(parsed)} (${scope})`);
1768
- }
1769
- })
1770
- },
1771
- async run() {
1772
- const config = await configManager2.getConfig();
1773
- console.log(JSON.stringify(config, null, 2));
1774
- }
1775
- });
1776
- }
1777
- function parseValue(raw) {
1778
- if (raw === "true") return true;
1779
- if (raw === "false") return false;
1780
- if (raw === "null") return null;
1781
- const num = Number(raw);
1782
- if (!Number.isNaN(num) && raw.trim() !== "") return num;
1783
- try {
1784
- const parsed = JSON.parse(raw);
1785
- if (typeof parsed === "object") return parsed;
1786
- } catch {
1787
- }
1788
- return raw;
1789
- }
1790
-
1791
- // src/commands/mcp.ts
1792
- import { defineCommand as defineCommand4 } from "citty";
1793
-
1794
- // src/mcp-server/server.ts
1795
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1796
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1797
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1798
- import { createServer } from "http";
1799
- import { randomUUID } from "crypto";
1800
- import { z as z5 } from "zod";
1801
-
1802
- // src/mcp-server/recursion-guard.ts
1803
- import { createHash } from "crypto";
1804
- var RecursionGuard = class _RecursionGuard {
1805
- constructor(config = {
1806
- maxDepth: 5,
1807
- maxCallsPerSession: 20,
1808
- timeoutSec: 300
1809
- }) {
1810
- this.config = config;
1811
- }
1812
- static MAX_ENTRIES = 1e3;
1813
- callCounts = /* @__PURE__ */ new Map();
1814
- promptHashes = /* @__PURE__ */ new Map();
1815
- /** Remove entries older than timeoutSec and enforce size limits */
1816
- cleanup() {
1817
- const now = Date.now();
1818
- const ttlMs = this.config.timeoutSec * 1e3;
1819
- for (const [key, value] of this.callCounts) {
1820
- if (now - value.createdAt > ttlMs) {
1821
- this.callCounts.delete(key);
1822
- }
1823
- }
1824
- for (const [key, value] of this.promptHashes) {
1825
- if (now - value.createdAt > ttlMs) {
1826
- this.promptHashes.delete(key);
1827
- }
1828
- }
1829
- if (this.callCounts.size > _RecursionGuard.MAX_ENTRIES) {
1830
- const sorted = [...this.callCounts.entries()].sort(
1831
- (a, b) => a[1].createdAt - b[1].createdAt
1832
- );
1833
- for (let i = 0; i < sorted.length - _RecursionGuard.MAX_ENTRIES; i++) {
1834
- this.callCounts.delete(sorted[i][0]);
1835
- }
1836
- }
1837
- if (this.promptHashes.size > _RecursionGuard.MAX_ENTRIES) {
1838
- const sorted = [...this.promptHashes.entries()].sort(
1839
- (a, b) => a[1].createdAt - b[1].createdAt
1840
- );
1841
- for (let i = 0; i < sorted.length - _RecursionGuard.MAX_ENTRIES; i++) {
1842
- this.promptHashes.delete(sorted[i][0]);
1843
- }
1844
- }
1845
- }
1846
- /** Check if a spawn is allowed */
1847
- canSpawn(context) {
1848
- this.cleanup();
1849
- if (context.depth >= this.config.maxDepth) {
1850
- return {
1851
- allowed: false,
1852
- reason: `Max depth exceeded: ${context.depth} >= ${this.config.maxDepth}`
1853
- };
1854
- }
1855
- const currentCount = this.callCounts.get(context.traceId)?.count ?? 0;
1856
- if (currentCount >= this.config.maxCallsPerSession) {
1857
- return {
1858
- allowed: false,
1859
- reason: `Max calls per session exceeded: ${currentCount} >= ${this.config.maxCallsPerSession}`
1860
- };
1861
- }
1862
- if (this.detectLoop(context.traceId, context.backend, context.promptHash)) {
1863
- return {
1864
- allowed: false,
1865
- reason: `Loop detected: same (backend=${context.backend}, promptHash=${context.promptHash}) appeared 3+ times in trace ${context.traceId}`
1866
- };
1867
- }
1868
- return { allowed: true };
1869
- }
1870
- /** Record a spawn invocation */
1871
- recordSpawn(context) {
1872
- this.cleanup();
1873
- const entry = this.callCounts.get(context.traceId);
1874
- if (entry) {
1875
- entry.count += 1;
1876
- } else {
1877
- this.callCounts.set(context.traceId, { count: 1, createdAt: Date.now() });
1878
- }
1879
- const key = `${context.backend}:${context.promptHash}`;
1880
- const hashEntry = this.promptHashes.get(context.traceId);
1881
- if (hashEntry) {
1882
- hashEntry.hashes.push(key);
1883
- } else {
1884
- this.promptHashes.set(context.traceId, { hashes: [key], createdAt: Date.now() });
1885
- }
1886
- }
1887
- /** Detect if the same (backend + promptHash) combination has appeared 3+ times */
1888
- detectLoop(traceId, backend, promptHash) {
1889
- const key = `${backend}:${promptHash}`;
1890
- const hashes = this.promptHashes.get(traceId)?.hashes ?? [];
1891
- const count = hashes.filter((h) => h === key).length;
1892
- return count >= 3;
1893
- }
1894
- /** Get current config (for testing/inspection) */
1895
- getConfig() {
1896
- return { ...this.config };
1897
- }
1898
- /** Get call count for a trace */
1899
- getCallCount(traceId) {
1900
- return this.callCounts.get(traceId)?.count ?? 0;
1901
- }
1902
- /** Utility: compute a prompt hash */
1903
- static hashPrompt(prompt) {
1904
- return createHash("sha256").update(prompt).digest("hex").slice(0, 16);
1905
- }
1906
- };
1907
-
1908
- // src/mcp-server/tools/spawn-agent.ts
1909
- import { z as z2 } from "zod";
1910
- import { nanoid as nanoid2 } from "nanoid";
1911
- var spawnAgentInputSchema = z2.object({
1912
- backend: z2.enum(["claude", "codex", "gemini"]),
1913
- prompt: z2.string(),
1914
- agent: z2.string().optional(),
1915
- resumeSessionId: z2.string().optional(),
1916
- model: z2.string().optional(),
1917
- maxTurns: z2.number().optional()
1918
- });
1919
- function buildContextFromEnv() {
1920
- const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${nanoid2()}`;
1921
- const parentSessionId = process.env["RELAY_PARENT_SESSION_ID"] ?? null;
1922
- const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
1923
- return { traceId, parentSessionId, depth };
1924
- }
1925
- async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2) {
1926
- const envContext = buildContextFromEnv();
1927
- const promptHash = RecursionGuard.hashPrompt(input.prompt);
1928
- const context = {
1929
- traceId: envContext.traceId,
1930
- depth: envContext.depth,
1931
- backend: input.backend,
1932
- promptHash
1933
- };
1934
- const guardResult = guard.canSpawn(context);
1935
- if (!guardResult.allowed) {
1936
- logger.warn(`Spawn blocked by RecursionGuard: ${guardResult.reason}`);
1937
- return {
1938
- sessionId: "",
1939
- exitCode: 1,
1940
- stdout: "",
1941
- stderr: `Spawn blocked: ${guardResult.reason}`
1942
- };
1943
- }
1944
- const adapter = registry2.get(input.backend);
1945
- const installed = await adapter.isInstalled();
1946
- if (!installed) {
1947
- return {
1948
- sessionId: "",
1949
- exitCode: 1,
1950
- stdout: "",
1951
- stderr: `Backend "${input.backend}" is not installed`
1952
- };
1953
- }
1954
- const session = await sessionManager2.create({
1955
- backendId: input.backend,
1956
- parentSessionId: envContext.parentSessionId ?? void 0,
1957
- depth: envContext.depth + 1
1958
- });
1959
- if (hooksEngine2) {
1960
- try {
1961
- const hookInput = {
1962
- event: "pre-spawn",
1963
- sessionId: session.relaySessionId,
1964
- backendId: input.backend,
1965
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1966
- data: {
1967
- prompt: input.prompt,
1968
- agent: input.agent,
1969
- model: input.model
1970
- }
1971
- };
1972
- await hooksEngine2.emit("pre-spawn", hookInput);
1973
- } catch (error) {
1974
- logger.debug(
1975
- `pre-spawn hook error: ${error instanceof Error ? error.message : String(error)}`
1976
- );
1977
- }
1978
- }
1979
- try {
1980
- const result = await adapter.execute({
1981
- prompt: input.prompt,
1982
- agent: input.agent,
1983
- model: input.model,
1984
- maxTurns: input.maxTurns,
1985
- resume: input.resumeSessionId,
1986
- mcpContext: {
1987
- parentSessionId: session.relaySessionId,
1988
- depth: envContext.depth + 1,
1989
- maxDepth: guard.getConfig().maxDepth,
1990
- traceId: envContext.traceId
1991
- }
1992
- });
1993
- if (contextMonitor2) {
1994
- const estimatedTokens = Math.ceil(
1995
- (result.stdout.length + result.stderr.length) / 4
1996
- );
1997
- const maxTokens = input.backend === "gemini" ? 128e3 : 2e5;
1998
- contextMonitor2.updateUsage(
1999
- session.relaySessionId,
2000
- input.backend,
2001
- estimatedTokens,
2002
- maxTokens
2003
- );
2004
- }
2005
- guard.recordSpawn(context);
2006
- const status = result.exitCode === 0 ? "completed" : "error";
2007
- await sessionManager2.update(session.relaySessionId, { status });
2008
- if (hooksEngine2) {
2009
- try {
2010
- const hookInput = {
2011
- event: "post-spawn",
2012
- sessionId: session.relaySessionId,
2013
- backendId: input.backend,
2014
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2015
- data: {
2016
- exitCode: result.exitCode,
2017
- status
2018
- }
2019
- };
2020
- await hooksEngine2.emit("post-spawn", hookInput);
2021
- } catch (hookError) {
2022
- logger.debug(
2023
- `post-spawn hook error: ${hookError instanceof Error ? hookError.message : String(hookError)}`
2024
- );
2025
- }
2026
- }
2027
- return {
2028
- sessionId: session.relaySessionId,
2029
- exitCode: result.exitCode,
2030
- stdout: result.stdout,
2031
- stderr: result.stderr
2032
- };
2033
- } catch (error) {
2034
- await sessionManager2.update(session.relaySessionId, { status: "error" });
2035
- const message = error instanceof Error ? error.message : String(error);
2036
- return {
2037
- sessionId: session.relaySessionId,
2038
- exitCode: 1,
2039
- stdout: "",
2040
- stderr: message
2041
- };
2042
- }
2043
- }
2044
-
2045
- // src/mcp-server/tools/list-sessions.ts
2046
- import { z as z3 } from "zod";
2047
- var listSessionsInputSchema = z3.object({
2048
- backend: z3.enum(["claude", "codex", "gemini"]).optional(),
2049
- limit: z3.number().optional().default(10)
2050
- });
2051
- async function executeListSessions(input, sessionManager2) {
2052
- const sessions = await sessionManager2.list({
2053
- backendId: input.backend,
2054
- limit: input.limit
2055
- });
2056
- return {
2057
- sessions: sessions.map((s) => ({
2058
- relaySessionId: s.relaySessionId,
2059
- backendId: s.backendId,
2060
- status: s.status,
2061
- createdAt: s.createdAt.toISOString()
2062
- }))
2063
- };
2064
- }
2065
-
2066
- // src/mcp-server/tools/get-context-status.ts
2067
- import { z as z4 } from "zod";
2068
- var getContextStatusInputSchema = z4.object({
2069
- sessionId: z4.string()
2070
- });
2071
- async function executeGetContextStatus(input, sessionManager2, contextMonitor2) {
2072
- const session = await sessionManager2.get(input.sessionId);
2073
- if (!session) {
2074
- throw new Error(`Session not found: ${input.sessionId}`);
2075
- }
2076
- if (contextMonitor2) {
2077
- const usage = contextMonitor2.getUsage(input.sessionId);
2078
- if (usage) {
2079
- return {
2080
- sessionId: input.sessionId,
2081
- usagePercent: usage.usagePercent,
2082
- isEstimated: usage.isEstimated
2083
- };
2084
- }
2085
- }
2086
- return {
2087
- sessionId: input.sessionId,
2088
- usagePercent: 0,
2089
- isEstimated: true
2090
- };
2863
+ }
2864
+ }
2865
+ });
2091
2866
  }
2092
2867
 
2093
- // src/mcp-server/server.ts
2094
- var RelayMCPServer = class {
2095
- constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
2096
- this.registry = registry2;
2097
- this.sessionManager = sessionManager2;
2098
- this.hooksEngine = hooksEngine2;
2099
- this.contextMonitor = contextMonitor2;
2100
- this.guard = new RecursionGuard(guardConfig);
2101
- this.server = new McpServer({
2102
- name: "agentic-relay",
2103
- version: "0.3.0"
2104
- });
2105
- this.registerTools();
2106
- }
2107
- server;
2108
- guard;
2109
- registerTools() {
2110
- this.server.tool(
2111
- "spawn_agent",
2112
- "Spawn a sub-agent on the specified backend CLI (Claude Code, Codex CLI, or Gemini CLI). The agent executes the given prompt in non-interactive mode and returns the result.",
2113
- {
2114
- backend: z5.enum(["claude", "codex", "gemini"]),
2115
- prompt: z5.string(),
2116
- agent: z5.string().optional(),
2117
- resumeSessionId: z5.string().optional(),
2118
- model: z5.string().optional(),
2119
- maxTurns: z5.number().optional()
2120
- },
2121
- async (params) => {
2122
- try {
2123
- const result = await executeSpawnAgent(
2124
- params,
2125
- this.registry,
2126
- this.sessionManager,
2127
- this.guard,
2128
- this.hooksEngine,
2129
- this.contextMonitor
2130
- );
2131
- const isError = result.exitCode !== 0;
2132
- const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
2868
+ // src/commands/update.ts
2869
+ init_logger();
2870
+ import { defineCommand as defineCommand2 } from "citty";
2133
2871
 
2134
- ${result.stdout}`;
2135
- return {
2136
- content: [{ type: "text", text }],
2137
- isError
2138
- };
2139
- } catch (error) {
2140
- const message = error instanceof Error ? error.message : String(error);
2141
- return {
2142
- content: [{ type: "text", text: `Error: ${message}` }],
2143
- isError: true
2144
- };
2145
- }
2872
+ // src/infrastructure/version-check.ts
2873
+ init_logger();
2874
+ async function getLatestNpmVersion(packageName) {
2875
+ try {
2876
+ const response = await fetch(
2877
+ `https://registry.npmjs.org/${packageName}/latest`,
2878
+ {
2879
+ headers: { Accept: "application/json" },
2880
+ signal: AbortSignal.timeout(5e3)
2146
2881
  }
2147
2882
  );
2148
- this.server.tool(
2149
- "list_sessions",
2150
- "List relay sessions, optionally filtered by backend.",
2151
- {
2152
- backend: z5.enum(["claude", "codex", "gemini"]).optional(),
2153
- limit: z5.number().optional()
2154
- },
2155
- async (params) => {
2156
- try {
2157
- const result = await executeListSessions(
2158
- { backend: params.backend, limit: params.limit ?? 10 },
2159
- this.sessionManager
2883
+ if (!response.ok) {
2884
+ logger.debug(
2885
+ `npm registry returned ${response.status} for ${packageName}`
2886
+ );
2887
+ return null;
2888
+ }
2889
+ const data = await response.json();
2890
+ return data.version ?? null;
2891
+ } catch (error) {
2892
+ logger.debug(
2893
+ `Failed to check npm registry: ${error instanceof Error ? error.message : String(error)}`
2894
+ );
2895
+ return null;
2896
+ }
2897
+ }
2898
+ function compareSemver(a, b) {
2899
+ const partsA = a.replace(/^v/, "").split(".").map(Number);
2900
+ const partsB = b.replace(/^v/, "").split(".").map(Number);
2901
+ for (let i = 0; i < 3; i++) {
2902
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
2903
+ if (diff !== 0) return diff;
2904
+ }
2905
+ return 0;
2906
+ }
2907
+
2908
+ // src/commands/update.ts
2909
+ var PACKAGE_NAME = "@rk0429/agentic-relay";
2910
+ var CURRENT_VERSION = "0.4.0";
2911
+ function createUpdateCommand(registry2) {
2912
+ return defineCommand2({
2913
+ meta: {
2914
+ name: "update",
2915
+ description: "Update relay and all installed backend CLI tools"
2916
+ },
2917
+ async run() {
2918
+ logger.info("Checking for relay updates...");
2919
+ const latestVersion = await getLatestNpmVersion(PACKAGE_NAME);
2920
+ if (latestVersion) {
2921
+ if (compareSemver(latestVersion, CURRENT_VERSION) > 0) {
2922
+ logger.warn(
2923
+ `A newer version of relay is available: ${latestVersion} (current: ${CURRENT_VERSION})`
2160
2924
  );
2161
- return {
2162
- content: [
2163
- {
2164
- type: "text",
2165
- text: JSON.stringify(result, null, 2)
2166
- }
2167
- ]
2168
- };
2169
- } catch (error) {
2170
- const message = error instanceof Error ? error.message : String(error);
2171
- return {
2172
- content: [{ type: "text", text: `Error: ${message}` }],
2173
- isError: true
2174
- };
2925
+ logger.info(` Run: npm install -g ${PACKAGE_NAME}@latest`);
2926
+ } else {
2927
+ logger.success(`relay is up to date (${CURRENT_VERSION})`);
2175
2928
  }
2929
+ } else {
2930
+ logger.debug("Could not check for relay updates");
2176
2931
  }
2177
- );
2178
- this.server.tool(
2179
- "get_context_status",
2180
- "Get the context usage status of a relay session. Returns usage data from ContextMonitor when available, otherwise estimated values.",
2181
- {
2182
- sessionId: z5.string()
2183
- },
2184
- async (params) => {
2932
+ const adapters = registry2.list();
2933
+ if (adapters.length === 0) {
2934
+ logger.warn("No backends registered");
2935
+ return;
2936
+ }
2937
+ for (const adapter of adapters) {
2938
+ const installed = await adapter.isInstalled();
2939
+ if (!installed) {
2940
+ logger.info(`Skipping ${adapter.id}: not installed`);
2941
+ continue;
2942
+ }
2943
+ logger.info(`Updating ${adapter.id}...`);
2185
2944
  try {
2186
- const result = await executeGetContextStatus(
2187
- params,
2188
- this.sessionManager,
2189
- this.contextMonitor
2190
- );
2191
- return {
2192
- content: [
2193
- {
2194
- type: "text",
2195
- text: JSON.stringify(result, null, 2)
2196
- }
2197
- ]
2198
- };
2945
+ await adapter.update();
2199
2946
  } catch (error) {
2200
- const message = error instanceof Error ? error.message : String(error);
2201
- return {
2202
- content: [{ type: "text", text: `Error: ${message}` }],
2203
- isError: true
2204
- };
2947
+ logger.error(
2948
+ `Failed to update ${adapter.id}: ${error instanceof Error ? error.message : String(error)}`
2949
+ );
2205
2950
  }
2206
2951
  }
2207
- );
2208
- }
2209
- async start(options) {
2210
- const transportType = options?.transport ?? "stdio";
2211
- if (transportType === "stdio") {
2212
- logger.info("Starting agentic-relay MCP server (stdio transport)...");
2213
- const transport = new StdioServerTransport();
2214
- await this.server.connect(transport);
2215
- return;
2216
2952
  }
2217
- const port = options?.port ?? 3100;
2218
- logger.info(
2219
- `Starting agentic-relay MCP server (HTTP transport on port ${port})...`
2220
- );
2221
- const httpTransport = new StreamableHTTPServerTransport({
2222
- sessionIdGenerator: () => randomUUID()
2223
- });
2224
- const httpServer = createServer(async (req, res) => {
2225
- const url = req.url ?? "";
2226
- if (url === "/mcp" || url.startsWith("/mcp?")) {
2227
- await httpTransport.handleRequest(req, res);
2228
- } else {
2229
- res.writeHead(404, { "Content-Type": "text/plain" });
2230
- res.end("Not found");
2231
- }
2232
- });
2233
- this._httpServer = httpServer;
2234
- await this.server.connect(httpTransport);
2235
- await new Promise((resolve) => {
2236
- httpServer.listen(port, () => {
2237
- logger.info(`MCP server listening on http://localhost:${port}/mcp`);
2238
- resolve();
2239
- });
2240
- });
2241
- await new Promise((resolve) => {
2242
- httpServer.on("close", resolve);
2243
- });
2244
- }
2245
- /** Exposed for testing and graceful shutdown */
2246
- get httpServer() {
2247
- return this._httpServer;
2953
+ });
2954
+ }
2955
+
2956
+ // src/commands/config.ts
2957
+ init_logger();
2958
+ import { defineCommand as defineCommand3 } from "citty";
2959
+ function createConfigCommand(configManager2) {
2960
+ return defineCommand3({
2961
+ meta: {
2962
+ name: "config",
2963
+ description: "Manage relay configuration"
2964
+ },
2965
+ subCommands: {
2966
+ get: defineCommand3({
2967
+ meta: {
2968
+ name: "get",
2969
+ description: "Get a config value by key (dot-path notation)"
2970
+ },
2971
+ args: {
2972
+ key: {
2973
+ type: "positional",
2974
+ description: "Config key (e.g. mcpServerMode.maxDepth)",
2975
+ required: true
2976
+ }
2977
+ },
2978
+ async run({ args }) {
2979
+ const key = String(args.key);
2980
+ const value = await configManager2.get(key);
2981
+ if (value === void 0) {
2982
+ logger.info(`Key "${key}" is not set`);
2983
+ } else {
2984
+ const output = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
2985
+ console.log(output);
2986
+ }
2987
+ }
2988
+ }),
2989
+ set: defineCommand3({
2990
+ meta: {
2991
+ name: "set",
2992
+ description: "Set a config value (default: global scope)"
2993
+ },
2994
+ args: {
2995
+ project: {
2996
+ type: "boolean",
2997
+ description: "Write to project scope instead of global"
2998
+ },
2999
+ key: {
3000
+ type: "positional",
3001
+ description: "Config key (e.g. defaultBackend)",
3002
+ required: true
3003
+ },
3004
+ value: {
3005
+ type: "positional",
3006
+ description: "Config value",
3007
+ required: true
3008
+ }
3009
+ },
3010
+ async run({ args }) {
3011
+ const scope = args.project ? "project" : "global";
3012
+ const key = String(args.key);
3013
+ const rawValue = String(args.value);
3014
+ const parsed = parseValue(rawValue);
3015
+ await configManager2.set(scope, key, parsed);
3016
+ logger.success(`Set ${key} = ${JSON.stringify(parsed)} (${scope})`);
3017
+ }
3018
+ })
3019
+ },
3020
+ async run() {
3021
+ const config = await configManager2.getConfig();
3022
+ console.log(JSON.stringify(config, null, 2));
3023
+ }
3024
+ });
3025
+ }
3026
+ function parseValue(raw) {
3027
+ if (raw === "true") return true;
3028
+ if (raw === "false") return false;
3029
+ if (raw === "null") return null;
3030
+ const num = Number(raw);
3031
+ if (!Number.isNaN(num) && raw.trim() !== "") return num;
3032
+ try {
3033
+ const parsed = JSON.parse(raw);
3034
+ if (typeof parsed === "object") return parsed;
3035
+ } catch {
2248
3036
  }
2249
- _httpServer;
2250
- };
3037
+ return raw;
3038
+ }
2251
3039
 
2252
3040
  // src/commands/mcp.ts
3041
+ init_server();
3042
+ init_logger();
3043
+ import { defineCommand as defineCommand4 } from "citty";
2253
3044
  function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngine2, contextMonitor2) {
2254
3045
  return defineCommand4({
2255
3046
  meta: {
@@ -2424,6 +3215,7 @@ function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngin
2424
3215
  }
2425
3216
 
2426
3217
  // src/commands/auth.ts
3218
+ init_logger();
2427
3219
  import { defineCommand as defineCommand5 } from "citty";
2428
3220
  function createBackendAuthCommand(backendId, authManager2) {
2429
3221
  return defineCommand5({
@@ -2478,6 +3270,7 @@ function createAuthCommand(authManager2) {
2478
3270
  }
2479
3271
 
2480
3272
  // src/commands/sessions.ts
3273
+ init_logger();
2481
3274
  import { defineCommand as defineCommand6 } from "citty";
2482
3275
  function formatDate(date) {
2483
3276
  const y = date.getFullYear();
@@ -2593,9 +3386,12 @@ function createVersionCommand(registry2) {
2593
3386
 
2594
3387
  // src/commands/doctor.ts
2595
3388
  import { defineCommand as defineCommand8 } from "citty";
2596
- import { access, constants } from "fs/promises";
2597
- import { join as join3 } from "path";
2598
- import { homedir as homedir2 } from "os";
3389
+ import { access, constants, readdir as readdir2 } from "fs/promises";
3390
+ import { join as join6 } from "path";
3391
+ import { homedir as homedir5 } from "os";
3392
+ import { execFile } from "child_process";
3393
+ import { promisify } from "util";
3394
+ var execFileAsync = promisify(execFile);
2599
3395
  async function checkNodeVersion() {
2600
3396
  const version = process.version;
2601
3397
  const major = Number(version.slice(1).split(".")[0]);
@@ -2652,8 +3448,8 @@ async function checkConfig(configManager2) {
2652
3448
  }
2653
3449
  }
2654
3450
  async function checkSessionsDir() {
2655
- const relayHome2 = process.env["RELAY_HOME"] ?? join3(homedir2(), ".relay");
2656
- const sessionsDir = join3(relayHome2, "sessions");
3451
+ const relayHome2 = process.env["RELAY_HOME"] ?? join6(homedir5(), ".relay");
3452
+ const sessionsDir = join6(relayHome2, "sessions");
2657
3453
  try {
2658
3454
  await access(sessionsDir, constants.W_OK);
2659
3455
  return {
@@ -2687,6 +3483,107 @@ async function checkMCPSDK() {
2687
3483
  };
2688
3484
  }
2689
3485
  }
3486
+ async function checkMCPServerCommands(configManager2) {
3487
+ const results = [];
3488
+ try {
3489
+ const config = await configManager2.getConfig();
3490
+ const mcpServers = config.mcpServers ?? {};
3491
+ for (const [name, server] of Object.entries(mcpServers)) {
3492
+ const command = server.command;
3493
+ try {
3494
+ await execFileAsync("which", [command]);
3495
+ results.push({
3496
+ label: `MCP server: ${name}`,
3497
+ ok: true,
3498
+ detail: `MCP server "${name}": command "${command}" found`
3499
+ });
3500
+ } catch {
3501
+ results.push({
3502
+ label: `MCP server: ${name}`,
3503
+ ok: true,
3504
+ detail: `MCP server "${name}": command "${command}" not found in PATH`,
3505
+ hint: `Ensure "${command}" is installed and available in your PATH`
3506
+ });
3507
+ }
3508
+ }
3509
+ } catch {
3510
+ }
3511
+ return results;
3512
+ }
3513
+ async function checkMCPServerInstantiation() {
3514
+ try {
3515
+ const { RelayMCPServer: RelayMCPServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3516
+ if (typeof RelayMCPServer2 !== "function") {
3517
+ return {
3518
+ label: "MCP server",
3519
+ ok: false,
3520
+ detail: "MCP server module is not a constructor",
3521
+ hint: "Reinstall dependencies: pnpm install"
3522
+ };
3523
+ }
3524
+ return {
3525
+ label: "MCP server",
3526
+ ok: true,
3527
+ detail: "MCP server module loadable"
3528
+ };
3529
+ } catch (error) {
3530
+ return {
3531
+ label: "MCP server",
3532
+ ok: false,
3533
+ detail: `MCP server module not loadable: ${error instanceof Error ? error.message : String(error)}`,
3534
+ hint: "Reinstall dependencies: pnpm install"
3535
+ };
3536
+ }
3537
+ }
3538
+ async function checkBackendAuthEnv() {
3539
+ const envVars = {
3540
+ claude: "ANTHROPIC_API_KEY",
3541
+ codex: "OPENAI_API_KEY",
3542
+ gemini: "GEMINI_API_KEY"
3543
+ };
3544
+ const results = [];
3545
+ for (const [backend, envVar] of Object.entries(envVars)) {
3546
+ const value = process.env[envVar];
3547
+ if (value) {
3548
+ results.push({
3549
+ label: `Auth: ${backend}`,
3550
+ ok: true,
3551
+ detail: `${envVar} is set`
3552
+ });
3553
+ } else {
3554
+ results.push({
3555
+ label: `Auth: ${backend}`,
3556
+ ok: true,
3557
+ detail: `${envVar} not set`,
3558
+ hint: "API key not set; may use subscription auth"
3559
+ });
3560
+ }
3561
+ }
3562
+ return results;
3563
+ }
3564
+ async function checkSessionsDiskUsage() {
3565
+ const relayHome2 = process.env["RELAY_HOME"] ?? join6(homedir5(), ".relay");
3566
+ const sessionsDir = join6(relayHome2, "sessions");
3567
+ try {
3568
+ const entries = await readdir2(sessionsDir);
3569
+ const fileCount = entries.length;
3570
+ if (fileCount >= 100) {
3571
+ return {
3572
+ label: "Sessions disk usage",
3573
+ ok: true,
3574
+ detail: `${fileCount} session files in ${sessionsDir}`,
3575
+ hint: "Consider cleaning up old sessions: relay sessions list --limit 10"
3576
+ };
3577
+ }
3578
+ return {
3579
+ label: "Sessions disk usage",
3580
+ ok: true,
3581
+ detail: `${fileCount} session files in ${sessionsDir}`
3582
+ };
3583
+ } catch {
3584
+ return null;
3585
+ }
3586
+ }
2690
3587
  function createDoctorCommand(registry2, configManager2) {
2691
3588
  return defineCommand8({
2692
3589
  meta: {
@@ -2704,15 +3601,24 @@ function createDoctorCommand(registry2, configManager2) {
2704
3601
  checks.push(await checkConfig(configManager2));
2705
3602
  checks.push(await checkSessionsDir());
2706
3603
  checks.push(await checkMCPSDK());
3604
+ const mcpCommandChecks = await checkMCPServerCommands(configManager2);
3605
+ checks.push(...mcpCommandChecks);
3606
+ checks.push(await checkMCPServerInstantiation());
3607
+ const authChecks = await checkBackendAuthEnv();
3608
+ checks.push(...authChecks);
3609
+ const diskCheck = await checkSessionsDiskUsage();
3610
+ if (diskCheck) {
3611
+ checks.push(diskCheck);
3612
+ }
2707
3613
  let hasFailures = false;
2708
3614
  for (const check of checks) {
2709
3615
  const icon = check.ok ? "\u2713" : "\u2717";
2710
3616
  console.log(` ${icon} ${check.detail}`);
2711
3617
  if (!check.ok) {
2712
3618
  hasFailures = true;
2713
- if (check.hint) {
2714
- console.log(` ${check.hint}`);
2715
- }
3619
+ }
3620
+ if (check.hint) {
3621
+ console.log(` ${check.hint}`);
2716
3622
  }
2717
3623
  }
2718
3624
  if (hasFailures) {
@@ -2728,9 +3634,10 @@ function createDoctorCommand(registry2, configManager2) {
2728
3634
  }
2729
3635
 
2730
3636
  // src/commands/init.ts
3637
+ init_logger();
2731
3638
  import { defineCommand as defineCommand9 } from "citty";
2732
- import { mkdir as mkdir3, writeFile as writeFile3, access as access2, readFile as readFile3 } from "fs/promises";
2733
- import { join as join4 } from "path";
3639
+ import { mkdir as mkdir6, writeFile as writeFile6, access as access2, readFile as readFile6 } from "fs/promises";
3640
+ import { join as join7 } from "path";
2734
3641
  var DEFAULT_CONFIG2 = {
2735
3642
  defaultBackend: "claude",
2736
3643
  backends: {},
@@ -2744,8 +3651,8 @@ function createInitCommand() {
2744
3651
  },
2745
3652
  async run() {
2746
3653
  const projectDir = process.cwd();
2747
- const relayDir = join4(projectDir, ".relay");
2748
- const configPath = join4(relayDir, "config.json");
3654
+ const relayDir = join7(projectDir, ".relay");
3655
+ const configPath = join7(relayDir, "config.json");
2749
3656
  try {
2750
3657
  await access2(relayDir);
2751
3658
  logger.info(
@@ -2754,16 +3661,16 @@ function createInitCommand() {
2754
3661
  return;
2755
3662
  } catch {
2756
3663
  }
2757
- await mkdir3(relayDir, { recursive: true });
2758
- await writeFile3(
3664
+ await mkdir6(relayDir, { recursive: true });
3665
+ await writeFile6(
2759
3666
  configPath,
2760
3667
  JSON.stringify(DEFAULT_CONFIG2, null, 2) + "\n",
2761
3668
  "utf-8"
2762
3669
  );
2763
3670
  logger.success(`Created ${configPath}`);
2764
- const gitignorePath = join4(projectDir, ".gitignore");
3671
+ const gitignorePath = join7(projectDir, ".gitignore");
2765
3672
  try {
2766
- const gitignoreContent = await readFile3(gitignorePath, "utf-8");
3673
+ const gitignoreContent = await readFile6(gitignorePath, "utf-8");
2767
3674
  if (!gitignoreContent.includes(".relay/config.local.json")) {
2768
3675
  logger.info(
2769
3676
  'Tip: Add ".relay/config.local.json" to your .gitignore to keep local config out of version control.'
@@ -2782,12 +3689,12 @@ function createInitCommand() {
2782
3689
  // src/bin/relay.ts
2783
3690
  var processManager = new ProcessManager();
2784
3691
  var registry = new AdapterRegistry();
2785
- registry.register(new ClaudeAdapter(processManager));
2786
- registry.register(new CodexAdapter(processManager));
2787
- registry.register(new GeminiAdapter(processManager));
3692
+ registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
3693
+ registry.registerLazy("codex", () => new CodexAdapter(processManager));
3694
+ registry.registerLazy("gemini", () => new GeminiAdapter(processManager));
2788
3695
  var sessionManager = new SessionManager();
2789
- var relayHome = process.env["RELAY_HOME"] ?? join5(homedir3(), ".relay");
2790
- var projectRelayDir = join5(process.cwd(), ".relay");
3696
+ var relayHome = process.env["RELAY_HOME"] ?? join8(homedir6(), ".relay");
3697
+ var projectRelayDir = join8(process.cwd(), ".relay");
2791
3698
  var configManager = new ConfigManager(relayHome, projectRelayDir);
2792
3699
  var authManager = new AuthManager(registry);
2793
3700
  var eventBus = new EventBus();