@rk0429/agentic-relay 0.7.0 → 0.8.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 +5 -1
  2. package/dist/relay.mjs +457 -252
  3. package/package.json +9 -2
package/README.md CHANGED
@@ -156,6 +156,10 @@ Example configuration:
156
156
  | `OPENAI_API_KEY` | Passed through to Codex CLI (optional with subscription) | -- |
157
157
  | `GEMINI_API_KEY` | Passed through to Gemini CLI (optional with subscription) | -- |
158
158
 
159
+ ### Security Considerations
160
+
161
+ - **Claude adapter permission bypass**: By default, the Claude adapter runs with `bypassPermissions` mode to enable non-interactive sub-agent execution. This means spawned Claude Code agents can execute tools without user confirmation. To change this behavior, set the `RELAY_CLAUDE_PERMISSION_MODE` environment variable to `default`.
162
+
159
163
  ## MCP Server Mode
160
164
 
161
165
  agentic-relay can act as an MCP (Model Context Protocol) server, allowing any MCP-capable client to spawn sub-agents across all three backends. This is how nested sub-agent orchestration works -- a parent agent calls relay via MCP, which spawns a child agent on any backend, and that child can call relay again to spawn a grandchild.
@@ -317,7 +321,7 @@ src/
317
321
  - **Process management**: execa (interactive modes, Gemini CLI)
318
322
  - **Validation**: zod
319
323
  - **Logging**: consola
320
- - **Testing**: vitest (507 tests across 28 files)
324
+ - **Testing**: vitest (771 tests across 35 files)
321
325
  - **Coverage**: @vitest/coverage-v8
322
326
 
323
327
  ## License
package/dist/relay.mjs CHANGED
@@ -159,7 +159,7 @@ var init_recursion_guard = __esm({
159
159
  import { z as z2 } from "zod";
160
160
  import { nanoid as nanoid2 } from "nanoid";
161
161
  import { existsSync, readFileSync } from "fs";
162
- import { join as join6 } from "path";
162
+ import { join as join6, normalize, resolve, sep } from "path";
163
163
  function buildContextInjection(metadata) {
164
164
  const parts = [];
165
165
  if (metadata.stateContent && typeof metadata.stateContent === "string") {
@@ -190,15 +190,28 @@ function readPreviousState(dailynoteDir) {
190
190
  return null;
191
191
  }
192
192
  }
193
- function readAgentDefinition(definitionPath) {
193
+ function validatePathWithinProject(filePath, projectRoot) {
194
+ if (filePath.trim().length === 0) {
195
+ throw new Error("Path is empty");
196
+ }
197
+ const normalizedProjectRoot = normalize(resolve(projectRoot));
198
+ const resolvedPath = normalize(resolve(normalizedProjectRoot, filePath));
199
+ const projectRootPrefix = normalizedProjectRoot.endsWith(sep) ? normalizedProjectRoot : `${normalizedProjectRoot}${sep}`;
200
+ if (resolvedPath !== normalizedProjectRoot && !resolvedPath.startsWith(projectRootPrefix)) {
201
+ throw new Error(`Path traversal detected: ${filePath}`);
202
+ }
203
+ return resolvedPath;
204
+ }
205
+ function readAgentDefinition(definitionPath, projectRoot = process.cwd()) {
194
206
  try {
195
- if (!existsSync(definitionPath)) {
196
- logger.warn(`Agent definition file not found at ${definitionPath}`);
207
+ const safeDefinitionPath = validatePathWithinProject(definitionPath, projectRoot);
208
+ if (!existsSync(safeDefinitionPath)) {
209
+ logger.warn(`Agent definition file not found at ${safeDefinitionPath}`);
197
210
  return null;
198
211
  }
199
- const content = readFileSync(definitionPath, "utf-8");
212
+ const content = readFileSync(safeDefinitionPath, "utf-8");
200
213
  if (content.trim().length === 0) {
201
- logger.warn(`Agent definition file is empty at ${definitionPath}`);
214
+ logger.warn(`Agent definition file is empty at ${safeDefinitionPath}`);
202
215
  return null;
203
216
  }
204
217
  return content;
@@ -209,9 +222,10 @@ function readAgentDefinition(definitionPath) {
209
222
  return null;
210
223
  }
211
224
  }
212
- function readSkillContext(skillContext) {
225
+ function readSkillContext(skillContext, projectRoot = process.cwd()) {
213
226
  try {
214
- const skillMdPath = join6(skillContext.skillPath, "SKILL.md");
227
+ const safeSkillPath = validatePathWithinProject(skillContext.skillPath, projectRoot);
228
+ const skillMdPath = validatePathWithinProject(join6(safeSkillPath, "SKILL.md"), projectRoot);
215
229
  if (!existsSync(skillMdPath)) {
216
230
  logger.warn(
217
231
  `SKILL.md not found at ${skillMdPath}`
@@ -222,12 +236,12 @@ function readSkillContext(skillContext) {
222
236
  const skillContent = readFileSync(skillMdPath, "utf-8");
223
237
  parts.push(skillContent);
224
238
  if (skillContext.subskill) {
225
- const subskillPath = join6(
226
- skillContext.skillPath,
239
+ const subskillPath = validatePathWithinProject(join6(
240
+ safeSkillPath,
227
241
  "subskills",
228
242
  skillContext.subskill,
229
243
  "SUBSKILL.md"
230
- );
244
+ ), projectRoot);
231
245
  if (existsSync(subskillPath)) {
232
246
  const subskillContent = readFileSync(subskillPath, "utf-8");
233
247
  parts.push(subskillContent);
@@ -299,8 +313,10 @@ function buildContextFromEnv() {
299
313
  const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
300
314
  return { traceId, parentSessionId, depth };
301
315
  }
302
- async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl) {
316
+ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
317
+ onProgress?.({ stage: "initializing", percent: 0 });
303
318
  let effectiveBackend = input.backend;
319
+ let selectionReason = "direct";
304
320
  if (backendSelector) {
305
321
  const availableBackends = registry2.listIds();
306
322
  const selectionContext = {
@@ -309,7 +325,9 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
309
325
  agentType: input.agent,
310
326
  taskType: input.taskType
311
327
  };
312
- effectiveBackend = backendSelector.selectBackend(selectionContext);
328
+ const selectionResult = backendSelector.selectBackendWithReason(selectionContext);
329
+ effectiveBackend = selectionResult.backend;
330
+ selectionReason = selectionResult.reason;
313
331
  }
314
332
  const envContext = buildContextFromEnv();
315
333
  const promptHash = RecursionGuard.hashPrompt(input.prompt);
@@ -418,41 +436,91 @@ ${defText}
418
436
  ${wrapped}` : wrapped;
419
437
  }
420
438
  }
421
- try {
422
- let result;
423
- if (input.resumeSessionId) {
424
- if (!adapter.continueSession) {
439
+ let effectivePrompt = input.prompt;
440
+ if (input.taskInstructionPath) {
441
+ try {
442
+ const projectRoot = process.cwd();
443
+ const safePath = validatePathWithinProject(input.taskInstructionPath, projectRoot);
444
+ if (!existsSync(safePath)) {
425
445
  return {
426
- sessionId: session.relaySessionId,
446
+ sessionId: "",
427
447
  exitCode: 1,
428
448
  stdout: "",
429
- stderr: `Backend "${effectiveBackend}" does not support session continuation (continueSession).`
449
+ stderr: `Task instruction file not found: ${input.taskInstructionPath}`
430
450
  };
431
451
  }
432
- result = await adapter.continueSession(input.resumeSessionId, input.prompt);
433
- } else {
434
- let mcpServers;
435
- if (childHttpUrl) {
436
- const parentMcpServers = readProjectMcpJson();
437
- if (Object.keys(parentMcpServers).length > 0) {
438
- mcpServers = buildChildMcpServers(parentMcpServers, childHttpUrl);
452
+ const instructionContent = readFileSync(safePath, "utf-8");
453
+ effectivePrompt = `${instructionContent}
454
+
455
+ ${input.prompt}`;
456
+ } catch (error) {
457
+ const message = error instanceof Error ? error.message : String(error);
458
+ return {
459
+ sessionId: "",
460
+ exitCode: 1,
461
+ stdout: "",
462
+ stderr: `Failed to read task instruction file: ${message}`
463
+ };
464
+ }
465
+ }
466
+ onProgress?.({ stage: "spawning", percent: 10 });
467
+ try {
468
+ let result;
469
+ const executePromise = (async () => {
470
+ if (input.resumeSessionId) {
471
+ if (!adapter.continueSession) {
472
+ return {
473
+ exitCode: 1,
474
+ stdout: "",
475
+ stderr: `Backend "${effectiveBackend}" does not support session continuation (continueSession).`,
476
+ _noSession: true
477
+ };
439
478
  }
479
+ return adapter.continueSession(input.resumeSessionId, effectivePrompt);
480
+ } else {
481
+ let mcpServers;
482
+ if (childHttpUrl) {
483
+ const parentMcpServers = readProjectMcpJson();
484
+ if (Object.keys(parentMcpServers).length > 0) {
485
+ mcpServers = buildChildMcpServers(parentMcpServers, childHttpUrl);
486
+ }
487
+ }
488
+ return adapter.execute({
489
+ prompt: effectivePrompt,
490
+ agent: input.agent,
491
+ systemPrompt: enhancedSystemPrompt,
492
+ model: input.model,
493
+ maxTurns: input.maxTurns,
494
+ mcpContext: {
495
+ parentSessionId: session.relaySessionId,
496
+ depth: envContext.depth + 1,
497
+ maxDepth: guard.getConfig().maxDepth,
498
+ traceId: envContext.traceId
499
+ },
500
+ ...mcpServers ? { mcpServers } : {}
501
+ });
440
502
  }
441
- result = await adapter.execute({
442
- prompt: input.prompt,
443
- agent: input.agent,
444
- systemPrompt: enhancedSystemPrompt,
445
- model: input.model,
446
- maxTurns: input.maxTurns,
447
- mcpContext: {
448
- parentSessionId: session.relaySessionId,
449
- depth: envContext.depth + 1,
450
- maxDepth: guard.getConfig().maxDepth,
451
- traceId: envContext.traceId
452
- },
453
- ...mcpServers ? { mcpServers } : {}
454
- });
503
+ })();
504
+ if (input.timeoutMs) {
505
+ const timeoutPromise = new Promise(
506
+ (_, reject) => setTimeout(
507
+ () => reject(new Error(`Agent execution timed out after ${input.timeoutMs}ms`)),
508
+ input.timeoutMs
509
+ )
510
+ );
511
+ result = await Promise.race([executePromise, timeoutPromise]);
512
+ } else {
513
+ result = await executePromise;
514
+ }
515
+ if (result && "_noSession" in result) {
516
+ return {
517
+ sessionId: session.relaySessionId,
518
+ exitCode: result.exitCode,
519
+ stdout: result.stdout,
520
+ stderr: result.stderr
521
+ };
455
522
  }
523
+ onProgress?.({ stage: "executing", percent: 50 });
456
524
  if (contextMonitor2) {
457
525
  const estimatedTokens = Math.ceil(
458
526
  (result.stdout.length + result.stderr.length) / 4
@@ -466,6 +534,16 @@ ${wrapped}` : wrapped;
466
534
  guard.recordSpawn(context);
467
535
  const status = result.exitCode === 0 ? "completed" : "error";
468
536
  await sessionManager2.update(session.relaySessionId, { status });
537
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
538
+ const metadata = {
539
+ durationMs: new Date(completedAt).getTime() - new Date(spawnStartedAt).getTime(),
540
+ selectedBackend: effectiveBackend,
541
+ ...input.preferredBackend ? { requestedBackend: input.preferredBackend } : {},
542
+ selectionReason,
543
+ startedAt: spawnStartedAt,
544
+ completedAt
545
+ };
546
+ onProgress?.({ stage: "completed", percent: 100 });
469
547
  if (hooksEngine2) {
470
548
  try {
471
549
  const postSpawnData = {
@@ -502,7 +580,8 @@ ${wrapped}` : wrapped;
502
580
  exitCode: result.exitCode,
503
581
  stdout: result.stdout,
504
582
  stderr: result.stderr,
505
- nativeSessionId: result.nativeSessionId
583
+ nativeSessionId: result.nativeSessionId,
584
+ metadata
506
585
  };
507
586
  } catch (error) {
508
587
  await sessionManager2.update(session.relaySessionId, { status: "error" });
@@ -537,13 +616,108 @@ var init_spawn_agent = __esm({
537
616
  definitionPath: z2.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
538
617
  }).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
539
618
  preferredBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
540
- taskType: z2.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
619
+ taskType: z2.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified."),
620
+ timeoutMs: z2.number().optional().describe("Timeout in milliseconds for agent execution. Default: no timeout."),
621
+ taskInstructionPath: z2.string().optional().describe(
622
+ "Path to a file containing task instructions. Content is prepended to the prompt. Path is resolved relative to the project root and validated against path traversal."
623
+ )
541
624
  });
542
625
  }
543
626
  });
544
627
 
628
+ // src/mcp-server/tools/conflict-detector.ts
629
+ import { execFile } from "child_process";
630
+ import { promisify } from "util";
631
+ async function takeSnapshot(cwd) {
632
+ try {
633
+ const [diffResult, lsResult] = await Promise.all([
634
+ execFileAsync("git", ["diff", "--name-only", "HEAD"], { cwd }).catch(() => ({ stdout: "" })),
635
+ execFileAsync("git", ["ls-files", "-m"], { cwd }).catch(() => ({ stdout: "" }))
636
+ ]);
637
+ const files = /* @__PURE__ */ new Set();
638
+ for (const line of diffResult.stdout.split("\n")) {
639
+ const trimmed = line.trim();
640
+ if (trimmed) files.add(trimmed);
641
+ }
642
+ for (const line of lsResult.stdout.split("\n")) {
643
+ const trimmed = line.trim();
644
+ if (trimmed) files.add(trimmed);
645
+ }
646
+ return files;
647
+ } catch (error) {
648
+ logger.warn(
649
+ `Failed to take git snapshot: ${error instanceof Error ? error.message : String(error)}`
650
+ );
651
+ return /* @__PURE__ */ new Set();
652
+ }
653
+ }
654
+ function extractMentionedPaths(stdout) {
655
+ const paths = /* @__PURE__ */ new Set();
656
+ const patterns = [
657
+ /(?:^|\s|["'`])(\.\/.+?\.[a-zA-Z]{1,5})(?:\s|["'`]|$)/gm,
658
+ /(?:^|\s|["'`])(src\/.+?\.[a-zA-Z]{1,5})(?:\s|["'`]|$)/gm,
659
+ /(?:^|\s|["'`])([a-zA-Z][a-zA-Z0-9_-]*(?:\/[a-zA-Z0-9_.-]+){1,10}\.[a-zA-Z]{1,5})(?:\s|["'`]|$)/gm
660
+ ];
661
+ for (const pattern of patterns) {
662
+ let match;
663
+ while ((match = pattern.exec(stdout)) !== null) {
664
+ let path = match[1];
665
+ if (path.startsWith("./")) {
666
+ path = path.slice(2);
667
+ }
668
+ paths.add(path);
669
+ }
670
+ }
671
+ return paths;
672
+ }
673
+ async function detectConflicts(before, after, agentResults) {
674
+ const newlyModified = /* @__PURE__ */ new Set();
675
+ for (const file of after) {
676
+ if (!before.has(file)) {
677
+ newlyModified.add(file);
678
+ }
679
+ }
680
+ if (newlyModified.size === 0) {
681
+ return { conflicts: [], hasConflicts: false };
682
+ }
683
+ const agentPaths = /* @__PURE__ */ new Map();
684
+ for (const agent of agentResults) {
685
+ agentPaths.set(agent.index, extractMentionedPaths(agent.stdout));
686
+ }
687
+ const fileToAgents = /* @__PURE__ */ new Map();
688
+ for (const file of newlyModified) {
689
+ const matchingAgents = [];
690
+ for (const [index, paths] of agentPaths) {
691
+ for (const mentionedPath of paths) {
692
+ if (file === mentionedPath || file.endsWith(mentionedPath) || mentionedPath.endsWith(file)) {
693
+ matchingAgents.push(index);
694
+ break;
695
+ }
696
+ }
697
+ }
698
+ if (matchingAgents.length > 1) {
699
+ fileToAgents.set(file, matchingAgents);
700
+ }
701
+ }
702
+ const conflicts = Array.from(fileToAgents.entries()).map(
703
+ ([path, agents]) => ({ path, agents })
704
+ );
705
+ return {
706
+ conflicts,
707
+ hasConflicts: conflicts.length > 0
708
+ };
709
+ }
710
+ var execFileAsync;
711
+ var init_conflict_detector = __esm({
712
+ "src/mcp-server/tools/conflict-detector.ts"() {
713
+ "use strict";
714
+ init_logger();
715
+ execFileAsync = promisify(execFile);
716
+ }
717
+ });
718
+
545
719
  // src/mcp-server/tools/spawn-agents-parallel.ts
546
- async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl) {
720
+ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
547
721
  const envContext = buildContextFromEnv();
548
722
  if (envContext.depth >= guard.getConfig().maxDepth) {
549
723
  const reason = `Max depth exceeded: ${envContext.depth} >= ${guard.getConfig().maxDepth}`;
@@ -581,6 +755,10 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
581
755
  failureCount: agents.length
582
756
  };
583
757
  }
758
+ const cwd = process.cwd();
759
+ const beforeSnapshot = await takeSnapshot(cwd);
760
+ onProgress?.({ stage: "spawning", percent: 5 });
761
+ let completedCount = 0;
584
762
  const settled = await Promise.allSettled(
585
763
  agents.map(
586
764
  (agent) => executeSpawnAgent(
@@ -592,20 +770,32 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
592
770
  contextMonitor2,
593
771
  backendSelector,
594
772
  childHttpUrl
595
- )
773
+ ).then((result) => {
774
+ completedCount++;
775
+ onProgress?.({
776
+ stage: `completed ${completedCount}/${agents.length}`,
777
+ percent: Math.round(completedCount / agents.length * 90) + 5
778
+ });
779
+ return result;
780
+ })
596
781
  )
597
782
  );
598
783
  const results = settled.map((outcome, index) => {
599
784
  if (outcome.status === "fulfilled") {
600
785
  const r = outcome.value;
601
- return {
786
+ const base = {
602
787
  index,
603
788
  sessionId: r.sessionId,
604
789
  exitCode: r.exitCode,
605
790
  stdout: r.stdout,
606
791
  stderr: r.stderr,
607
- ...r.nativeSessionId ? { nativeSessionId: r.nativeSessionId } : {}
792
+ ...r.nativeSessionId ? { nativeSessionId: r.nativeSessionId } : {},
793
+ ...r.metadata ? { metadata: r.metadata } : {}
608
794
  };
795
+ if (r.exitCode !== 0) {
796
+ base.originalInput = agents[index];
797
+ }
798
+ return base;
609
799
  }
610
800
  const errorMessage = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
611
801
  return {
@@ -614,21 +804,31 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
614
804
  exitCode: 1,
615
805
  stdout: "",
616
806
  stderr: errorMessage,
617
- error: errorMessage
807
+ error: errorMessage,
808
+ originalInput: agents[index]
618
809
  };
619
810
  });
620
811
  const successCount = results.filter((r) => r.exitCode === 0).length;
812
+ const afterSnapshot = await takeSnapshot(cwd);
813
+ const agentResultsForConflict = results.map((r) => ({
814
+ index: r.index,
815
+ stdout: r.stdout
816
+ }));
817
+ const conflictResult = await detectConflicts(beforeSnapshot, afterSnapshot, agentResultsForConflict);
818
+ onProgress?.({ stage: "completed", percent: 100 });
621
819
  return {
622
820
  results,
623
821
  totalCount: agents.length,
624
822
  successCount,
625
- failureCount: agents.length - successCount
823
+ failureCount: agents.length - successCount,
824
+ ...conflictResult.hasConflicts ? { conflicts: conflictResult.conflicts, hasConflicts: true } : {}
626
825
  };
627
826
  }
628
827
  var init_spawn_agents_parallel = __esm({
629
828
  "src/mcp-server/tools/spawn-agents-parallel.ts"() {
630
829
  "use strict";
631
830
  init_spawn_agent();
831
+ init_conflict_detector();
632
832
  init_logger();
633
833
  }
634
834
  });
@@ -745,32 +945,35 @@ var init_backend_selector = __esm({
745
945
  this.agentToBackendMap = config?.agentToBackendMap ?? DEFAULT_AGENT_TO_BACKEND_MAP;
746
946
  }
747
947
  selectBackend(context) {
948
+ return this.selectBackendWithReason(context).backend;
949
+ }
950
+ selectBackendWithReason(context) {
748
951
  const { availableBackends, preferredBackend, agentType, taskType } = context;
749
952
  if (availableBackends.length === 0) {
750
953
  throw new Error("No backends available");
751
954
  }
752
955
  if (preferredBackend && availableBackends.includes(preferredBackend)) {
753
- return preferredBackend;
956
+ return { backend: preferredBackend, reason: "preferredBackend" };
754
957
  }
755
958
  if (agentType) {
756
959
  const mapped = this.agentToBackendMap[agentType];
757
960
  if (mapped && availableBackends.includes(mapped)) {
758
- return mapped;
961
+ return { backend: mapped, reason: `agentType:${agentType}\u2192${mapped}` };
759
962
  }
760
963
  if (!mapped && availableBackends.includes("claude")) {
761
- return "claude";
964
+ return { backend: "claude", reason: `agentType:${agentType}\u2192claude(unmapped)` };
762
965
  }
763
966
  }
764
967
  if (taskType) {
765
968
  const mapped = TASK_TYPE_TO_BACKEND_MAP[taskType];
766
969
  if (mapped && availableBackends.includes(mapped)) {
767
- return mapped;
970
+ return { backend: mapped, reason: `taskType:${taskType}\u2192${mapped}` };
768
971
  }
769
972
  }
770
973
  if (availableBackends.includes(this.defaultBackend)) {
771
- return this.defaultBackend;
974
+ return { backend: this.defaultBackend, reason: "default" };
772
975
  }
773
- return availableBackends[0];
976
+ return { backend: availableBackends[0], reason: "fallback" };
774
977
  }
775
978
  };
776
979
  }
@@ -787,7 +990,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
787
990
  import { createServer } from "http";
788
991
  import { randomUUID } from "crypto";
789
992
  import { z as z5 } from "zod";
790
- var RelayMCPServer;
993
+ var spawnAgentsParallelInputShape, MAX_CHILD_HTTP_SESSIONS, RelayMCPServer;
791
994
  var init_server = __esm({
792
995
  "src/mcp-server/server.ts"() {
793
996
  "use strict";
@@ -799,6 +1002,12 @@ var init_server = __esm({
799
1002
  init_list_available_backends();
800
1003
  init_backend_selector();
801
1004
  init_logger();
1005
+ spawnAgentsParallelInputShape = {
1006
+ agents: z5.array(spawnAgentInputSchema).min(1).max(10).describe(
1007
+ "Array of agent configurations to execute in parallel (1-10 agents)"
1008
+ )
1009
+ };
1010
+ MAX_CHILD_HTTP_SESSIONS = 100;
802
1011
  RelayMCPServer = class {
803
1012
  constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
804
1013
  this.registry = registry2;
@@ -809,7 +1018,7 @@ var init_server = __esm({
809
1018
  this.backendSelector = new BackendSelector();
810
1019
  this.server = new McpServer({
811
1020
  name: "agentic-relay",
812
- version: "0.7.0"
1021
+ version: "0.8.0"
813
1022
  });
814
1023
  this.registerTools(this.server);
815
1024
  }
@@ -830,28 +1039,23 @@ var init_server = __esm({
830
1039
  server.tool(
831
1040
  "spawn_agent",
832
1041
  "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), 'systemPrompt' for custom role instructions (all backends), 'skillContext' to inject a skill definition (SKILL.md/SUBSKILL.md), 'agentDefinition' to inject an agent definition file into the sub-agent's system prompt, 'preferredBackend' to override automatic backend selection, or 'taskType' to hint at the task nature for backend selection.",
833
- {
834
- backend: z5.enum(["claude", "codex", "gemini"]),
835
- prompt: z5.string(),
836
- agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
837
- systemPrompt: z5.string().optional().describe(
838
- "System prompt / role instructions for the sub-agent (all backends)"
839
- ),
840
- resumeSessionId: z5.string().optional(),
841
- model: z5.string().optional(),
842
- maxTurns: z5.number().optional(),
843
- skillContext: z5.object({
844
- skillPath: z5.string().describe("Path to the skill directory (e.g., '.agents/skills/software-engineer/')"),
845
- subskill: z5.string().optional().describe("Specific subskill to activate")
846
- }).optional().describe("Skill context to inject into the sub-agent's system prompt"),
847
- agentDefinition: z5.object({
848
- definitionPath: z5.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
849
- }).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
850
- preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
851
- taskType: z5.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
852
- },
853
- async (params) => {
1042
+ spawnAgentInputSchema.shape,
1043
+ async (params, extra) => {
854
1044
  try {
1045
+ const onProgress = (progress) => {
1046
+ const progressToken = params._meta ? params._meta.progressToken : void 0;
1047
+ if (progressToken !== void 0) {
1048
+ void extra.sendNotification({
1049
+ method: "notifications/progress",
1050
+ params: {
1051
+ progressToken,
1052
+ progress: progress.percent ?? 0,
1053
+ total: 100,
1054
+ message: progress.stage
1055
+ }
1056
+ });
1057
+ }
1058
+ };
855
1059
  const result = await executeSpawnAgent(
856
1060
  params,
857
1061
  this.registry,
@@ -860,12 +1064,20 @@ var init_server = __esm({
860
1064
  this.hooksEngine,
861
1065
  this.contextMonitor,
862
1066
  this.backendSelector,
863
- this._childHttpUrl
1067
+ this._childHttpUrl,
1068
+ onProgress
864
1069
  );
865
1070
  const isError = result.exitCode !== 0;
866
- const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
1071
+ let text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
867
1072
 
868
1073
  ${result.stdout}`;
1074
+ if (result.metadata) {
1075
+ text += `
1076
+
1077
+ <metadata>
1078
+ ${JSON.stringify(result.metadata, null, 2)}
1079
+ </metadata>`;
1080
+ }
869
1081
  return {
870
1082
  content: [{ type: "text", text }],
871
1083
  isError
@@ -881,31 +1093,71 @@ ${result.stdout}`;
881
1093
  );
882
1094
  server.tool(
883
1095
  "spawn_agents_parallel",
884
- "Spawn multiple sub-agents in parallel across available backends. Each agent entry accepts the same parameters as spawn_agent. All agents are executed concurrently via Promise.allSettled, and results are returned as an array with per-agent status. RecursionGuard batch pre-validation ensures the entire batch fits within call limits before execution begins.",
1096
+ "Spawn multiple sub-agents in parallel across available backends. Each agent entry accepts the same parameters as spawn_agent. All agents are executed concurrently via Promise.allSettled, and results are returned as an array with per-agent status. RecursionGuard batch pre-validation ensures the entire batch fits within call limits before execution begins. Failed results include 'originalInput' for retry via retry_failed_agents.",
1097
+ spawnAgentsParallelInputShape,
1098
+ async (params, extra) => {
1099
+ try {
1100
+ const onProgress = (progress) => {
1101
+ const progressToken = params._meta ? params._meta.progressToken : void 0;
1102
+ if (progressToken !== void 0) {
1103
+ void extra.sendNotification({
1104
+ method: "notifications/progress",
1105
+ params: {
1106
+ progressToken,
1107
+ progress: progress.percent ?? 0,
1108
+ total: 100,
1109
+ message: progress.stage
1110
+ }
1111
+ });
1112
+ }
1113
+ };
1114
+ const result = await executeSpawnAgentsParallel(
1115
+ params.agents,
1116
+ this.registry,
1117
+ this.sessionManager,
1118
+ this.guard,
1119
+ this.hooksEngine,
1120
+ this.contextMonitor,
1121
+ this.backendSelector,
1122
+ this._childHttpUrl,
1123
+ onProgress
1124
+ );
1125
+ const isError = result.failureCount === result.totalCount;
1126
+ let text = "";
1127
+ if (result.hasConflicts) {
1128
+ text += "\u26A0 FILE CONFLICTS DETECTED: Multiple agents modified the same files.\n";
1129
+ text += `Conflicting files: ${result.conflicts.map((c) => c.path).join(", ")}
1130
+
1131
+ `;
1132
+ }
1133
+ text += JSON.stringify(result, null, 2);
1134
+ return {
1135
+ content: [{ type: "text", text }],
1136
+ isError
1137
+ };
1138
+ } catch (error) {
1139
+ const message = error instanceof Error ? error.message : String(error);
1140
+ return {
1141
+ content: [{ type: "text", text: `Error: ${message}` }],
1142
+ isError: true
1143
+ };
1144
+ }
1145
+ }
1146
+ );
1147
+ server.tool(
1148
+ "retry_failed_agents",
1149
+ "Retry only the failed agents from a previous spawn_agents_parallel call. Pass the failed results array (with originalInput) directly.",
885
1150
  {
886
- agents: z5.array(z5.object({
887
- backend: z5.enum(["claude", "codex", "gemini"]),
888
- prompt: z5.string(),
889
- agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
890
- systemPrompt: z5.string().optional().describe("System prompt / role instructions for the sub-agent (all backends)"),
891
- resumeSessionId: z5.string().optional(),
892
- model: z5.string().optional(),
893
- maxTurns: z5.number().optional(),
894
- skillContext: z5.object({
895
- skillPath: z5.string().describe("Path to the skill directory"),
896
- subskill: z5.string().optional().describe("Specific subskill to activate")
897
- }).optional().describe("Skill context to inject into the sub-agent's system prompt"),
898
- agentDefinition: z5.object({
899
- definitionPath: z5.string().describe("Path to the agent definition file")
900
- }).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
901
- preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override."),
902
- taskType: z5.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection.")
903
- })).min(1).max(10).describe("Array of agent configurations to execute in parallel (1-10 agents)")
1151
+ failedResults: z5.array(z5.object({
1152
+ index: z5.number(),
1153
+ originalInput: spawnAgentInputSchema
1154
+ })).min(1).describe("Array of failed results with their original input configurations")
904
1155
  },
905
1156
  async (params) => {
906
1157
  try {
1158
+ const agents = params.failedResults.map((r) => r.originalInput);
907
1159
  const result = await executeSpawnAgentsParallel(
908
- params.agents,
1160
+ agents,
909
1161
  this.registry,
910
1162
  this.sessionManager,
911
1163
  this.guard,
@@ -1041,14 +1293,14 @@ ${result.stdout}`;
1041
1293
  });
1042
1294
  this._httpServer = httpServer;
1043
1295
  await this.server.connect(httpTransport);
1044
- await new Promise((resolve) => {
1045
- httpServer.listen(port, () => {
1296
+ await new Promise((resolve2) => {
1297
+ httpServer.listen(port, "127.0.0.1", () => {
1046
1298
  logger.info(`MCP server listening on http://localhost:${port}/mcp`);
1047
- resolve();
1299
+ resolve2();
1048
1300
  });
1049
1301
  });
1050
- await new Promise((resolve) => {
1051
- httpServer.on("close", resolve);
1302
+ await new Promise((resolve2) => {
1303
+ httpServer.on("close", resolve2);
1052
1304
  });
1053
1305
  }
1054
1306
  /**
@@ -1079,7 +1331,7 @@ ${result.stdout}`;
1079
1331
  });
1080
1332
  const server = new McpServer({
1081
1333
  name: "agentic-relay",
1082
- version: "0.7.0"
1334
+ version: "0.8.0"
1083
1335
  });
1084
1336
  this.registerTools(server);
1085
1337
  transport.onclose = () => {
@@ -1095,15 +1347,31 @@ ${result.stdout}`;
1095
1347
  if (sid) {
1096
1348
  sessions.set(sid, { transport, server });
1097
1349
  logger.debug(`Child MCP session created: ${sid}`);
1350
+ if (sessions.size > MAX_CHILD_HTTP_SESSIONS) {
1351
+ const oldestEntry = sessions.entries().next().value;
1352
+ if (oldestEntry) {
1353
+ const [oldestSessionId, oldestSession] = oldestEntry;
1354
+ sessions.delete(oldestSessionId);
1355
+ logger.warn(
1356
+ `Child MCP session evicted due to limit (${MAX_CHILD_HTTP_SESSIONS}): ${oldestSessionId}`
1357
+ );
1358
+ void oldestSession.transport.close().catch((error) => {
1359
+ const message = error instanceof Error ? error.message : String(error);
1360
+ logger.debug(
1361
+ `Failed to close evicted child MCP session ${oldestSessionId}: ${message}`
1362
+ );
1363
+ });
1364
+ }
1365
+ }
1098
1366
  }
1099
1367
  });
1100
1368
  this._childHttpServer = httpServer;
1101
- await new Promise((resolve) => {
1369
+ await new Promise((resolve2) => {
1102
1370
  httpServer.listen(0, "127.0.0.1", () => {
1103
1371
  const addr = httpServer.address();
1104
1372
  this._childHttpUrl = `http://127.0.0.1:${addr.port}/mcp`;
1105
1373
  logger.info(`Child MCP server listening on ${this._childHttpUrl}`);
1106
- resolve();
1374
+ resolve2();
1107
1375
  });
1108
1376
  });
1109
1377
  }
@@ -1255,10 +1523,11 @@ var AdapterRegistry = class {
1255
1523
 
1256
1524
  // src/adapters/base-adapter.ts
1257
1525
  init_logger();
1258
- var BaseAdapter = class {
1526
+ var BaseAdapter = class _BaseAdapter {
1259
1527
  constructor(processManager2) {
1260
1528
  this.processManager = processManager2;
1261
1529
  }
1530
+ static HEALTH_TIMEOUT_MS = 5e3;
1262
1531
  async isInstalled() {
1263
1532
  try {
1264
1533
  const result = await this.processManager.execute("which", [this.command]);
@@ -1286,11 +1555,13 @@ var BaseAdapter = class {
1286
1555
  };
1287
1556
  }
1288
1557
  async checkHealth() {
1289
- const HEALTH_TIMEOUT = 5e3;
1290
1558
  const installed = await Promise.race([
1291
1559
  this.isInstalled(),
1292
1560
  new Promise(
1293
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1561
+ (_, reject) => setTimeout(
1562
+ () => reject(new Error("timeout")),
1563
+ _BaseAdapter.HEALTH_TIMEOUT_MS
1564
+ )
1294
1565
  )
1295
1566
  ]).catch(() => false);
1296
1567
  if (!installed) {
@@ -1304,14 +1575,29 @@ var BaseAdapter = class {
1304
1575
  const version = await Promise.race([
1305
1576
  this.getVersion(),
1306
1577
  new Promise(
1307
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1578
+ (_, reject) => setTimeout(
1579
+ () => reject(new Error("timeout")),
1580
+ _BaseAdapter.HEALTH_TIMEOUT_MS
1581
+ )
1308
1582
  )
1309
1583
  ]).catch(() => void 0);
1584
+ const authStatus = await Promise.race([
1585
+ this.checkAuthStatus(),
1586
+ new Promise(
1587
+ (_, reject) => setTimeout(
1588
+ () => reject(new Error("timeout")),
1589
+ _BaseAdapter.HEALTH_TIMEOUT_MS
1590
+ )
1591
+ )
1592
+ ]).catch(() => ({ authenticated: true }));
1593
+ const authenticated = authStatus.authenticated;
1594
+ const message = authStatus.message ?? (!authenticated ? `${this.id} authentication not configured` : void 0);
1310
1595
  return {
1311
1596
  installed: true,
1312
- authenticated: true,
1313
- healthy: true,
1314
- version
1597
+ authenticated,
1598
+ healthy: authenticated,
1599
+ version,
1600
+ ...message ? { message } : {}
1315
1601
  };
1316
1602
  }
1317
1603
  async getMCPConfig() {
@@ -1426,45 +1712,14 @@ var ClaudeAdapter = class extends BaseAdapter {
1426
1712
  getConfigPath() {
1427
1713
  return join(homedir(), ".claude.json");
1428
1714
  }
1429
- async checkHealth() {
1430
- const HEALTH_TIMEOUT = 5e3;
1431
- const installed = await Promise.race([
1432
- this.isInstalled(),
1433
- new Promise(
1434
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1435
- )
1436
- ]).catch(() => false);
1437
- if (!installed) {
1438
- return {
1439
- installed: false,
1440
- authenticated: false,
1441
- healthy: false,
1442
- message: "claude is not installed"
1443
- };
1444
- }
1445
- const version = await Promise.race([
1446
- this.getVersion(),
1447
- new Promise(
1448
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1449
- )
1450
- ]).catch(() => void 0);
1451
- let authenticated = true;
1452
- try {
1453
- const result = await Promise.race([
1454
- this.processManager.execute(this.command, ["auth", "status"]),
1455
- new Promise(
1456
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1457
- )
1458
- ]);
1459
- authenticated = result.exitCode === 0;
1460
- } catch {
1461
- authenticated = true;
1462
- }
1715
+ async checkAuthStatus() {
1716
+ const result = await this.processManager.execute(this.command, [
1717
+ "auth",
1718
+ "status"
1719
+ ]);
1720
+ const authenticated = result.exitCode === 0;
1463
1721
  return {
1464
- installed: true,
1465
1722
  authenticated,
1466
- healthy: authenticated,
1467
- version,
1468
1723
  ...!authenticated ? { message: "claude authentication not configured" } : {}
1469
1724
  };
1470
1725
  }
@@ -1903,48 +2158,32 @@ var CodexAdapter = class extends BaseAdapter {
1903
2158
  getConfigPath() {
1904
2159
  return join2(homedir2(), ".codex", "config.toml");
1905
2160
  }
1906
- async checkHealth() {
1907
- const HEALTH_TIMEOUT = 5e3;
1908
- const installed = await Promise.race([
1909
- this.isInstalled(),
1910
- new Promise(
1911
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1912
- )
1913
- ]).catch(() => false);
1914
- if (!installed) {
1915
- return {
1916
- installed: false,
1917
- authenticated: false,
1918
- healthy: false,
1919
- message: "codex is not installed"
1920
- };
1921
- }
1922
- const version = await Promise.race([
1923
- this.getVersion(),
1924
- new Promise(
1925
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1926
- )
1927
- ]).catch(() => void 0);
1928
- let authenticated = true;
1929
- try {
1930
- const result = await Promise.race([
1931
- this.processManager.execute(this.command, ["login", "status"]),
1932
- new Promise(
1933
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1934
- )
1935
- ]);
1936
- authenticated = result.exitCode === 0;
1937
- } catch {
1938
- authenticated = true;
1939
- }
2161
+ async checkAuthStatus() {
2162
+ const result = await this.processManager.execute(this.command, [
2163
+ "login",
2164
+ "status"
2165
+ ]);
2166
+ const authenticated = result.exitCode === 0;
1940
2167
  return {
1941
- installed: true,
1942
2168
  authenticated,
1943
- healthy: authenticated,
1944
- version,
1945
2169
  ...!authenticated ? { message: "codex authentication not configured" } : {}
1946
2170
  };
1947
2171
  }
2172
+ buildCodexOptions(flags) {
2173
+ if (!flags.mcpContext) {
2174
+ return {};
2175
+ }
2176
+ const env = {};
2177
+ for (const [key, value] of Object.entries(process.env)) {
2178
+ if (value !== void 0) {
2179
+ env[key] = value;
2180
+ }
2181
+ }
2182
+ env.RELAY_TRACE_ID = flags.mcpContext.traceId;
2183
+ env.RELAY_PARENT_SESSION_ID = flags.mcpContext.parentSessionId;
2184
+ env.RELAY_DEPTH = String(flags.mcpContext.depth);
2185
+ return { env };
2186
+ }
1948
2187
  mapFlags(flags) {
1949
2188
  const args = mapCommonToNative("codex", flags);
1950
2189
  if (flags.outputFormat === "json") {
@@ -1997,16 +2236,7 @@ ${prompt}`;
1997
2236
  );
1998
2237
  try {
1999
2238
  const { Codex } = await loadCodexSDK();
2000
- const codexOptions = {};
2001
- if (flags.mcpContext) {
2002
- codexOptions.env = {
2003
- ...process.env,
2004
- RELAY_TRACE_ID: flags.mcpContext.traceId,
2005
- RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
2006
- RELAY_DEPTH: String(flags.mcpContext.depth)
2007
- };
2008
- }
2009
- const codex = new Codex(codexOptions);
2239
+ const codex = new Codex(this.buildCodexOptions(flags));
2010
2240
  const thread = codex.startThread({
2011
2241
  ...flags.model ? { model: flags.model } : {},
2012
2242
  workingDirectory: process.cwd(),
@@ -2038,16 +2268,7 @@ ${prompt}`;
2038
2268
  );
2039
2269
  try {
2040
2270
  const { Codex } = await loadCodexSDK();
2041
- const codexOptions = {};
2042
- if (flags.mcpContext) {
2043
- codexOptions.env = {
2044
- ...process.env,
2045
- RELAY_TRACE_ID: flags.mcpContext.traceId,
2046
- RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
2047
- RELAY_DEPTH: String(flags.mcpContext.depth)
2048
- };
2049
- }
2050
- const codex = new Codex(codexOptions);
2271
+ const codex = new Codex(this.buildCodexOptions(flags));
2051
2272
  const thread = codex.startThread({
2052
2273
  ...flags.model ? { model: flags.model } : {},
2053
2274
  workingDirectory: process.cwd(),
@@ -2061,15 +2282,15 @@ ${prompt}`;
2061
2282
  threadId = event.thread_id;
2062
2283
  } else if (event.type === "item.started") {
2063
2284
  const item = event.item;
2064
- if (item?.type === "agent_message" && item.text) {
2285
+ if (item.type === "agent_message" && item.text) {
2065
2286
  yield { type: "text", text: item.text };
2066
- } else if (item?.type === "command_execution") {
2287
+ } else if (item.type === "command_execution") {
2067
2288
  yield {
2068
2289
  type: "tool_start",
2069
2290
  tool: item.command ?? "command",
2070
2291
  id: item.id ?? ""
2071
2292
  };
2072
- } else if (item?.type === "file_change") {
2293
+ } else if (item.type === "file_change") {
2073
2294
  yield {
2074
2295
  type: "tool_start",
2075
2296
  tool: "file_change",
@@ -2078,17 +2299,17 @@ ${prompt}`;
2078
2299
  }
2079
2300
  } else if (event.type === "item.completed") {
2080
2301
  const item = event.item;
2081
- if (item?.type === "agent_message" && item.text) {
2302
+ if (item.type === "agent_message" && item.text) {
2082
2303
  completedMessages.push(item.text);
2083
2304
  yield { type: "text", text: item.text };
2084
- } else if (item?.type === "command_execution") {
2305
+ } else if (item.type === "command_execution") {
2085
2306
  yield {
2086
2307
  type: "tool_end",
2087
2308
  tool: item.command ?? "command",
2088
2309
  id: item.id ?? "",
2089
2310
  result: item.aggregated_output
2090
2311
  };
2091
- } else if (item?.type === "file_change") {
2312
+ } else if (item.type === "file_change") {
2092
2313
  yield {
2093
2314
  type: "tool_end",
2094
2315
  tool: "file_change",
@@ -2111,7 +2332,7 @@ ${prompt}`;
2111
2332
  nativeSessionId: threadId ?? thread.id ?? void 0
2112
2333
  };
2113
2334
  } else if (event.type === "turn.failed") {
2114
- const errorMessage = event.error?.message ?? "Turn failed";
2335
+ const errorMessage = event.error.message ?? "Turn failed";
2115
2336
  yield {
2116
2337
  type: "done",
2117
2338
  result: { exitCode: 1, stdout: "", stderr: errorMessage },
@@ -2248,37 +2469,18 @@ var GeminiAdapter = class extends BaseAdapter {
2248
2469
  getConfigPath() {
2249
2470
  return join3(homedir3(), ".gemini", "settings.json");
2250
2471
  }
2251
- async checkHealth() {
2252
- const HEALTH_TIMEOUT = 5e3;
2253
- const installed = await Promise.race([
2254
- this.isInstalled(),
2255
- new Promise(
2256
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
2257
- )
2258
- ]).catch(() => false);
2259
- if (!installed) {
2472
+ async checkAuthStatus() {
2473
+ const hasApiKey = !!process.env["GEMINI_API_KEY"];
2474
+ const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
2475
+ const authenticated = hasApiKey || hasGoogleAdc;
2476
+ if (!authenticated) {
2260
2477
  return {
2261
- installed: false,
2262
- authenticated: false,
2263
- healthy: false,
2264
- message: "gemini is not installed"
2478
+ // Optimistic fallback: Gemini may still authenticate via ADC at runtime.
2479
+ authenticated: true,
2480
+ message: "Gemini authentication status unknown - ADC may be available at runtime"
2265
2481
  };
2266
2482
  }
2267
- const version = await Promise.race([
2268
- this.getVersion(),
2269
- new Promise(
2270
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
2271
- )
2272
- ]).catch(() => void 0);
2273
- const hasApiKey = !!process.env["GEMINI_API_KEY"];
2274
- const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
2275
- const authenticated = hasApiKey || hasGoogleAdc || true;
2276
- return {
2277
- installed: true,
2278
- authenticated,
2279
- healthy: true,
2280
- version
2281
- };
2483
+ return { authenticated };
2282
2484
  }
2283
2485
  mapFlags(flags) {
2284
2486
  const args = mapCommonToNative("gemini", flags);
@@ -3167,6 +3369,7 @@ var HooksEngine = class _HooksEngine {
3167
3369
  };
3168
3370
 
3169
3371
  // src/core/context-monitor.ts
3372
+ init_logger();
3170
3373
  var DEFAULT_BACKEND_CONTEXT = {
3171
3374
  claude: { contextWindow: 2e5, compactThreshold: 19e4 },
3172
3375
  codex: { contextWindow: 272e3, compactThreshold: 258400 },
@@ -3285,7 +3488,9 @@ var ContextMonitor = class {
3285
3488
  remainingBeforeCompact
3286
3489
  }
3287
3490
  };
3288
- void this.hooksEngine.emit("on-context-threshold", hookInput);
3491
+ void this.hooksEngine.emit("on-context-threshold", hookInput).catch(
3492
+ (e) => logger.debug("Context threshold hook error:", e)
3493
+ );
3289
3494
  }
3290
3495
  }
3291
3496
  };
@@ -4061,7 +4266,7 @@ function createVersionCommand(registry2) {
4061
4266
  description: "Show relay and backend versions"
4062
4267
  },
4063
4268
  async run() {
4064
- const relayVersion = "0.7.0";
4269
+ const relayVersion = "0.8.0";
4065
4270
  console.log(`agentic-relay v${relayVersion}`);
4066
4271
  console.log("");
4067
4272
  console.log("Backends:");
@@ -4088,9 +4293,9 @@ import { defineCommand as defineCommand8 } from "citty";
4088
4293
  import { access, constants, readdir as readdir2 } from "fs/promises";
4089
4294
  import { join as join7 } from "path";
4090
4295
  import { homedir as homedir5 } from "os";
4091
- import { execFile } from "child_process";
4092
- import { promisify } from "util";
4093
- var execFileAsync = promisify(execFile);
4296
+ import { execFile as execFile2 } from "child_process";
4297
+ import { promisify as promisify2 } from "util";
4298
+ var execFileAsync2 = promisify2(execFile2);
4094
4299
  async function checkNodeVersion() {
4095
4300
  const version = process.version;
4096
4301
  const major = Number(version.slice(1).split(".")[0]);
@@ -4190,7 +4395,7 @@ async function checkMCPServerCommands(configManager2) {
4190
4395
  for (const [name, server] of Object.entries(mcpServers)) {
4191
4396
  const command = server.command;
4192
4397
  try {
4193
- await execFileAsync("which", [command]);
4398
+ await execFileAsync2("which", [command]);
4194
4399
  results.push({
4195
4400
  label: `MCP server: ${name}`,
4196
4401
  ok: true,
@@ -4386,6 +4591,7 @@ function createInitCommand() {
4386
4591
  }
4387
4592
 
4388
4593
  // src/bin/relay.ts
4594
+ init_logger();
4389
4595
  var processManager = new ProcessManager();
4390
4596
  var registry = new AdapterRegistry();
4391
4597
  registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
@@ -4406,12 +4612,11 @@ void configManager.getConfig().then((config) => {
4406
4612
  if (config.contextMonitor) {
4407
4613
  contextMonitor = new ContextMonitor(hooksEngine, config.contextMonitor);
4408
4614
  }
4409
- }).catch(() => {
4410
- });
4615
+ }).catch((e) => logger.debug("Config load failed:", e));
4411
4616
  var main = defineCommand10({
4412
4617
  meta: {
4413
4618
  name: "relay",
4414
- version: "0.7.0",
4619
+ version: "0.8.0",
4415
4620
  description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
4416
4621
  },
4417
4622
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk0429/agentic-relay",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI with MCP-based multi-layer sub-agent orchestration",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -40,7 +40,9 @@
40
40
  "test": "vitest run",
41
41
  "test:coverage": "vitest run --coverage",
42
42
  "test:watch": "vitest",
43
- "lint": "tsc --noEmit",
43
+ "lint": "tsc --noEmit && eslint .",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check .",
44
46
  "prepublishOnly": "pnpm test && pnpm build"
45
47
  },
46
48
  "engines": {
@@ -57,10 +59,15 @@
57
59
  "zod": "^3.24.2"
58
60
  },
59
61
  "devDependencies": {
62
+ "@eslint/js": "^10.0.1",
60
63
  "@types/node": "^25.3.0",
61
64
  "@vitest/coverage-v8": "^3.2.4",
65
+ "eslint": "^10.0.2",
66
+ "eslint-config-prettier": "^10.1.8",
67
+ "prettier": "^3.8.1",
62
68
  "tsup": "^8.4.0",
63
69
  "typescript": "^5.7.3",
70
+ "typescript-eslint": "^8.56.1",
64
71
  "vitest": "^3.0.7"
65
72
  },
66
73
  "pnpm": {