@rk0429/agentic-relay 0.6.4 → 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 +573 -226
  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,11 +616,223 @@ 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
+
719
+ // src/mcp-server/tools/spawn-agents-parallel.ts
720
+ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
721
+ const envContext = buildContextFromEnv();
722
+ if (envContext.depth >= guard.getConfig().maxDepth) {
723
+ const reason = `Max depth exceeded: ${envContext.depth} >= ${guard.getConfig().maxDepth}`;
724
+ logger.warn(`Batch spawn blocked by RecursionGuard: ${reason}`);
725
+ return {
726
+ results: agents.map((_, index) => ({
727
+ index,
728
+ sessionId: "",
729
+ exitCode: 1,
730
+ stdout: "",
731
+ stderr: `Batch spawn blocked: ${reason}`,
732
+ error: reason
733
+ })),
734
+ totalCount: agents.length,
735
+ successCount: 0,
736
+ failureCount: agents.length
737
+ };
738
+ }
739
+ const currentCount = guard.getCallCount(envContext.traceId);
740
+ const maxCalls = guard.getConfig().maxCallsPerSession;
741
+ if (currentCount + agents.length > maxCalls) {
742
+ const reason = `Batch would exceed max calls per session: ${currentCount} + ${agents.length} > ${maxCalls}`;
743
+ logger.warn(`Batch spawn blocked by RecursionGuard: ${reason}`);
744
+ return {
745
+ results: agents.map((_, index) => ({
746
+ index,
747
+ sessionId: "",
748
+ exitCode: 1,
749
+ stdout: "",
750
+ stderr: `Batch spawn blocked: ${reason}`,
751
+ error: reason
752
+ })),
753
+ totalCount: agents.length,
754
+ successCount: 0,
755
+ failureCount: agents.length
756
+ };
757
+ }
758
+ const cwd = process.cwd();
759
+ const beforeSnapshot = await takeSnapshot(cwd);
760
+ onProgress?.({ stage: "spawning", percent: 5 });
761
+ let completedCount = 0;
762
+ const settled = await Promise.allSettled(
763
+ agents.map(
764
+ (agent) => executeSpawnAgent(
765
+ agent,
766
+ registry2,
767
+ sessionManager2,
768
+ guard,
769
+ hooksEngine2,
770
+ contextMonitor2,
771
+ backendSelector,
772
+ childHttpUrl
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
+ })
781
+ )
782
+ );
783
+ const results = settled.map((outcome, index) => {
784
+ if (outcome.status === "fulfilled") {
785
+ const r = outcome.value;
786
+ const base = {
787
+ index,
788
+ sessionId: r.sessionId,
789
+ exitCode: r.exitCode,
790
+ stdout: r.stdout,
791
+ stderr: r.stderr,
792
+ ...r.nativeSessionId ? { nativeSessionId: r.nativeSessionId } : {},
793
+ ...r.metadata ? { metadata: r.metadata } : {}
794
+ };
795
+ if (r.exitCode !== 0) {
796
+ base.originalInput = agents[index];
797
+ }
798
+ return base;
799
+ }
800
+ const errorMessage = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
801
+ return {
802
+ index,
803
+ sessionId: "",
804
+ exitCode: 1,
805
+ stdout: "",
806
+ stderr: errorMessage,
807
+ error: errorMessage,
808
+ originalInput: agents[index]
809
+ };
810
+ });
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 });
819
+ return {
820
+ results,
821
+ totalCount: agents.length,
822
+ successCount,
823
+ failureCount: agents.length - successCount,
824
+ ...conflictResult.hasConflicts ? { conflicts: conflictResult.conflicts, hasConflicts: true } : {}
825
+ };
826
+ }
827
+ var init_spawn_agents_parallel = __esm({
828
+ "src/mcp-server/tools/spawn-agents-parallel.ts"() {
829
+ "use strict";
830
+ init_spawn_agent();
831
+ init_conflict_detector();
832
+ init_logger();
833
+ }
834
+ });
835
+
545
836
  // src/mcp-server/tools/list-sessions.ts
546
837
  import { z as z3 } from "zod";
547
838
  async function executeListSessions(input, sessionManager2) {
@@ -654,32 +945,35 @@ var init_backend_selector = __esm({
654
945
  this.agentToBackendMap = config?.agentToBackendMap ?? DEFAULT_AGENT_TO_BACKEND_MAP;
655
946
  }
656
947
  selectBackend(context) {
948
+ return this.selectBackendWithReason(context).backend;
949
+ }
950
+ selectBackendWithReason(context) {
657
951
  const { availableBackends, preferredBackend, agentType, taskType } = context;
658
952
  if (availableBackends.length === 0) {
659
953
  throw new Error("No backends available");
660
954
  }
661
955
  if (preferredBackend && availableBackends.includes(preferredBackend)) {
662
- return preferredBackend;
956
+ return { backend: preferredBackend, reason: "preferredBackend" };
663
957
  }
664
958
  if (agentType) {
665
959
  const mapped = this.agentToBackendMap[agentType];
666
960
  if (mapped && availableBackends.includes(mapped)) {
667
- return mapped;
961
+ return { backend: mapped, reason: `agentType:${agentType}\u2192${mapped}` };
668
962
  }
669
963
  if (!mapped && availableBackends.includes("claude")) {
670
- return "claude";
964
+ return { backend: "claude", reason: `agentType:${agentType}\u2192claude(unmapped)` };
671
965
  }
672
966
  }
673
967
  if (taskType) {
674
968
  const mapped = TASK_TYPE_TO_BACKEND_MAP[taskType];
675
969
  if (mapped && availableBackends.includes(mapped)) {
676
- return mapped;
970
+ return { backend: mapped, reason: `taskType:${taskType}\u2192${mapped}` };
677
971
  }
678
972
  }
679
973
  if (availableBackends.includes(this.defaultBackend)) {
680
- return this.defaultBackend;
974
+ return { backend: this.defaultBackend, reason: "default" };
681
975
  }
682
- return availableBackends[0];
976
+ return { backend: availableBackends[0], reason: "fallback" };
683
977
  }
684
978
  };
685
979
  }
@@ -696,17 +990,24 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
696
990
  import { createServer } from "http";
697
991
  import { randomUUID } from "crypto";
698
992
  import { z as z5 } from "zod";
699
- var RelayMCPServer;
993
+ var spawnAgentsParallelInputShape, MAX_CHILD_HTTP_SESSIONS, RelayMCPServer;
700
994
  var init_server = __esm({
701
995
  "src/mcp-server/server.ts"() {
702
996
  "use strict";
703
997
  init_recursion_guard();
704
998
  init_spawn_agent();
999
+ init_spawn_agents_parallel();
705
1000
  init_list_sessions();
706
1001
  init_get_context_status();
707
1002
  init_list_available_backends();
708
1003
  init_backend_selector();
709
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;
710
1011
  RelayMCPServer = class {
711
1012
  constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
712
1013
  this.registry = registry2;
@@ -717,7 +1018,7 @@ var init_server = __esm({
717
1018
  this.backendSelector = new BackendSelector();
718
1019
  this.server = new McpServer({
719
1020
  name: "agentic-relay",
720
- version: "0.6.4"
1021
+ version: "0.8.0"
721
1022
  });
722
1023
  this.registerTools(this.server);
723
1024
  }
@@ -738,28 +1039,23 @@ var init_server = __esm({
738
1039
  server.tool(
739
1040
  "spawn_agent",
740
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.",
741
- {
742
- backend: z5.enum(["claude", "codex", "gemini"]),
743
- prompt: z5.string(),
744
- agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
745
- systemPrompt: z5.string().optional().describe(
746
- "System prompt / role instructions for the sub-agent (all backends)"
747
- ),
748
- resumeSessionId: z5.string().optional(),
749
- model: z5.string().optional(),
750
- maxTurns: z5.number().optional(),
751
- skillContext: z5.object({
752
- skillPath: z5.string().describe("Path to the skill directory (e.g., '.agents/skills/software-engineer/')"),
753
- subskill: z5.string().optional().describe("Specific subskill to activate")
754
- }).optional().describe("Skill context to inject into the sub-agent's system prompt"),
755
- agentDefinition: z5.object({
756
- definitionPath: z5.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
757
- }).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
758
- preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
759
- taskType: z5.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
760
- },
761
- async (params) => {
1042
+ spawnAgentInputSchema.shape,
1043
+ async (params, extra) => {
762
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
+ };
763
1059
  const result = await executeSpawnAgent(
764
1060
  params,
765
1061
  this.registry,
@@ -768,12 +1064,110 @@ var init_server = __esm({
768
1064
  this.hooksEngine,
769
1065
  this.contextMonitor,
770
1066
  this.backendSelector,
771
- this._childHttpUrl
1067
+ this._childHttpUrl,
1068
+ onProgress
772
1069
  );
773
1070
  const isError = result.exitCode !== 0;
774
- 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}
775
1072
 
776
1073
  ${result.stdout}`;
1074
+ if (result.metadata) {
1075
+ text += `
1076
+
1077
+ <metadata>
1078
+ ${JSON.stringify(result.metadata, null, 2)}
1079
+ </metadata>`;
1080
+ }
1081
+ return {
1082
+ content: [{ type: "text", text }],
1083
+ isError
1084
+ };
1085
+ } catch (error) {
1086
+ const message = error instanceof Error ? error.message : String(error);
1087
+ return {
1088
+ content: [{ type: "text", text: `Error: ${message}` }],
1089
+ isError: true
1090
+ };
1091
+ }
1092
+ }
1093
+ );
1094
+ server.tool(
1095
+ "spawn_agents_parallel",
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.",
1150
+ {
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")
1155
+ },
1156
+ async (params) => {
1157
+ try {
1158
+ const agents = params.failedResults.map((r) => r.originalInput);
1159
+ const result = await executeSpawnAgentsParallel(
1160
+ agents,
1161
+ this.registry,
1162
+ this.sessionManager,
1163
+ this.guard,
1164
+ this.hooksEngine,
1165
+ this.contextMonitor,
1166
+ this.backendSelector,
1167
+ this._childHttpUrl
1168
+ );
1169
+ const isError = result.failureCount === result.totalCount;
1170
+ const text = JSON.stringify(result, null, 2);
777
1171
  return {
778
1172
  content: [{ type: "text", text }],
779
1173
  isError
@@ -899,14 +1293,14 @@ ${result.stdout}`;
899
1293
  });
900
1294
  this._httpServer = httpServer;
901
1295
  await this.server.connect(httpTransport);
902
- await new Promise((resolve) => {
903
- httpServer.listen(port, () => {
1296
+ await new Promise((resolve2) => {
1297
+ httpServer.listen(port, "127.0.0.1", () => {
904
1298
  logger.info(`MCP server listening on http://localhost:${port}/mcp`);
905
- resolve();
1299
+ resolve2();
906
1300
  });
907
1301
  });
908
- await new Promise((resolve) => {
909
- httpServer.on("close", resolve);
1302
+ await new Promise((resolve2) => {
1303
+ httpServer.on("close", resolve2);
910
1304
  });
911
1305
  }
912
1306
  /**
@@ -937,7 +1331,7 @@ ${result.stdout}`;
937
1331
  });
938
1332
  const server = new McpServer({
939
1333
  name: "agentic-relay",
940
- version: "0.6.4"
1334
+ version: "0.8.0"
941
1335
  });
942
1336
  this.registerTools(server);
943
1337
  transport.onclose = () => {
@@ -953,15 +1347,31 @@ ${result.stdout}`;
953
1347
  if (sid) {
954
1348
  sessions.set(sid, { transport, server });
955
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
+ }
956
1366
  }
957
1367
  });
958
1368
  this._childHttpServer = httpServer;
959
- await new Promise((resolve) => {
1369
+ await new Promise((resolve2) => {
960
1370
  httpServer.listen(0, "127.0.0.1", () => {
961
1371
  const addr = httpServer.address();
962
1372
  this._childHttpUrl = `http://127.0.0.1:${addr.port}/mcp`;
963
1373
  logger.info(`Child MCP server listening on ${this._childHttpUrl}`);
964
- resolve();
1374
+ resolve2();
965
1375
  });
966
1376
  });
967
1377
  }
@@ -1113,10 +1523,11 @@ var AdapterRegistry = class {
1113
1523
 
1114
1524
  // src/adapters/base-adapter.ts
1115
1525
  init_logger();
1116
- var BaseAdapter = class {
1526
+ var BaseAdapter = class _BaseAdapter {
1117
1527
  constructor(processManager2) {
1118
1528
  this.processManager = processManager2;
1119
1529
  }
1530
+ static HEALTH_TIMEOUT_MS = 5e3;
1120
1531
  async isInstalled() {
1121
1532
  try {
1122
1533
  const result = await this.processManager.execute("which", [this.command]);
@@ -1144,11 +1555,13 @@ var BaseAdapter = class {
1144
1555
  };
1145
1556
  }
1146
1557
  async checkHealth() {
1147
- const HEALTH_TIMEOUT = 5e3;
1148
1558
  const installed = await Promise.race([
1149
1559
  this.isInstalled(),
1150
1560
  new Promise(
1151
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1561
+ (_, reject) => setTimeout(
1562
+ () => reject(new Error("timeout")),
1563
+ _BaseAdapter.HEALTH_TIMEOUT_MS
1564
+ )
1152
1565
  )
1153
1566
  ]).catch(() => false);
1154
1567
  if (!installed) {
@@ -1162,14 +1575,29 @@ var BaseAdapter = class {
1162
1575
  const version = await Promise.race([
1163
1576
  this.getVersion(),
1164
1577
  new Promise(
1165
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1578
+ (_, reject) => setTimeout(
1579
+ () => reject(new Error("timeout")),
1580
+ _BaseAdapter.HEALTH_TIMEOUT_MS
1581
+ )
1166
1582
  )
1167
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);
1168
1595
  return {
1169
1596
  installed: true,
1170
- authenticated: true,
1171
- healthy: true,
1172
- version
1597
+ authenticated,
1598
+ healthy: authenticated,
1599
+ version,
1600
+ ...message ? { message } : {}
1173
1601
  };
1174
1602
  }
1175
1603
  async getMCPConfig() {
@@ -1284,45 +1712,14 @@ var ClaudeAdapter = class extends BaseAdapter {
1284
1712
  getConfigPath() {
1285
1713
  return join(homedir(), ".claude.json");
1286
1714
  }
1287
- async checkHealth() {
1288
- const HEALTH_TIMEOUT = 5e3;
1289
- const installed = await Promise.race([
1290
- this.isInstalled(),
1291
- new Promise(
1292
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1293
- )
1294
- ]).catch(() => false);
1295
- if (!installed) {
1296
- return {
1297
- installed: false,
1298
- authenticated: false,
1299
- healthy: false,
1300
- message: "claude is not installed"
1301
- };
1302
- }
1303
- const version = await Promise.race([
1304
- this.getVersion(),
1305
- new Promise(
1306
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1307
- )
1308
- ]).catch(() => void 0);
1309
- let authenticated = true;
1310
- try {
1311
- const result = await Promise.race([
1312
- this.processManager.execute(this.command, ["auth", "status"]),
1313
- new Promise(
1314
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1315
- )
1316
- ]);
1317
- authenticated = result.exitCode === 0;
1318
- } catch {
1319
- authenticated = true;
1320
- }
1715
+ async checkAuthStatus() {
1716
+ const result = await this.processManager.execute(this.command, [
1717
+ "auth",
1718
+ "status"
1719
+ ]);
1720
+ const authenticated = result.exitCode === 0;
1321
1721
  return {
1322
- installed: true,
1323
1722
  authenticated,
1324
- healthy: authenticated,
1325
- version,
1326
1723
  ...!authenticated ? { message: "claude authentication not configured" } : {}
1327
1724
  };
1328
1725
  }
@@ -1761,48 +2158,32 @@ var CodexAdapter = class extends BaseAdapter {
1761
2158
  getConfigPath() {
1762
2159
  return join2(homedir2(), ".codex", "config.toml");
1763
2160
  }
1764
- async checkHealth() {
1765
- const HEALTH_TIMEOUT = 5e3;
1766
- const installed = await Promise.race([
1767
- this.isInstalled(),
1768
- new Promise(
1769
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1770
- )
1771
- ]).catch(() => false);
1772
- if (!installed) {
1773
- return {
1774
- installed: false,
1775
- authenticated: false,
1776
- healthy: false,
1777
- message: "codex is not installed"
1778
- };
1779
- }
1780
- const version = await Promise.race([
1781
- this.getVersion(),
1782
- new Promise(
1783
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1784
- )
1785
- ]).catch(() => void 0);
1786
- let authenticated = true;
1787
- try {
1788
- const result = await Promise.race([
1789
- this.processManager.execute(this.command, ["login", "status"]),
1790
- new Promise(
1791
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1792
- )
1793
- ]);
1794
- authenticated = result.exitCode === 0;
1795
- } catch {
1796
- authenticated = true;
1797
- }
2161
+ async checkAuthStatus() {
2162
+ const result = await this.processManager.execute(this.command, [
2163
+ "login",
2164
+ "status"
2165
+ ]);
2166
+ const authenticated = result.exitCode === 0;
1798
2167
  return {
1799
- installed: true,
1800
2168
  authenticated,
1801
- healthy: authenticated,
1802
- version,
1803
2169
  ...!authenticated ? { message: "codex authentication not configured" } : {}
1804
2170
  };
1805
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
+ }
1806
2187
  mapFlags(flags) {
1807
2188
  const args = mapCommonToNative("codex", flags);
1808
2189
  if (flags.outputFormat === "json") {
@@ -1855,16 +2236,7 @@ ${prompt}`;
1855
2236
  );
1856
2237
  try {
1857
2238
  const { Codex } = await loadCodexSDK();
1858
- const codexOptions = {};
1859
- if (flags.mcpContext) {
1860
- codexOptions.env = {
1861
- ...process.env,
1862
- RELAY_TRACE_ID: flags.mcpContext.traceId,
1863
- RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
1864
- RELAY_DEPTH: String(flags.mcpContext.depth)
1865
- };
1866
- }
1867
- const codex = new Codex(codexOptions);
2239
+ const codex = new Codex(this.buildCodexOptions(flags));
1868
2240
  const thread = codex.startThread({
1869
2241
  ...flags.model ? { model: flags.model } : {},
1870
2242
  workingDirectory: process.cwd(),
@@ -1896,16 +2268,7 @@ ${prompt}`;
1896
2268
  );
1897
2269
  try {
1898
2270
  const { Codex } = await loadCodexSDK();
1899
- const codexOptions = {};
1900
- if (flags.mcpContext) {
1901
- codexOptions.env = {
1902
- ...process.env,
1903
- RELAY_TRACE_ID: flags.mcpContext.traceId,
1904
- RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
1905
- RELAY_DEPTH: String(flags.mcpContext.depth)
1906
- };
1907
- }
1908
- const codex = new Codex(codexOptions);
2271
+ const codex = new Codex(this.buildCodexOptions(flags));
1909
2272
  const thread = codex.startThread({
1910
2273
  ...flags.model ? { model: flags.model } : {},
1911
2274
  workingDirectory: process.cwd(),
@@ -1919,15 +2282,15 @@ ${prompt}`;
1919
2282
  threadId = event.thread_id;
1920
2283
  } else if (event.type === "item.started") {
1921
2284
  const item = event.item;
1922
- if (item?.type === "agent_message" && item.text) {
2285
+ if (item.type === "agent_message" && item.text) {
1923
2286
  yield { type: "text", text: item.text };
1924
- } else if (item?.type === "command_execution") {
2287
+ } else if (item.type === "command_execution") {
1925
2288
  yield {
1926
2289
  type: "tool_start",
1927
2290
  tool: item.command ?? "command",
1928
2291
  id: item.id ?? ""
1929
2292
  };
1930
- } else if (item?.type === "file_change") {
2293
+ } else if (item.type === "file_change") {
1931
2294
  yield {
1932
2295
  type: "tool_start",
1933
2296
  tool: "file_change",
@@ -1936,17 +2299,17 @@ ${prompt}`;
1936
2299
  }
1937
2300
  } else if (event.type === "item.completed") {
1938
2301
  const item = event.item;
1939
- if (item?.type === "agent_message" && item.text) {
2302
+ if (item.type === "agent_message" && item.text) {
1940
2303
  completedMessages.push(item.text);
1941
2304
  yield { type: "text", text: item.text };
1942
- } else if (item?.type === "command_execution") {
2305
+ } else if (item.type === "command_execution") {
1943
2306
  yield {
1944
2307
  type: "tool_end",
1945
2308
  tool: item.command ?? "command",
1946
2309
  id: item.id ?? "",
1947
2310
  result: item.aggregated_output
1948
2311
  };
1949
- } else if (item?.type === "file_change") {
2312
+ } else if (item.type === "file_change") {
1950
2313
  yield {
1951
2314
  type: "tool_end",
1952
2315
  tool: "file_change",
@@ -1969,7 +2332,7 @@ ${prompt}`;
1969
2332
  nativeSessionId: threadId ?? thread.id ?? void 0
1970
2333
  };
1971
2334
  } else if (event.type === "turn.failed") {
1972
- const errorMessage = event.error?.message ?? "Turn failed";
2335
+ const errorMessage = event.error.message ?? "Turn failed";
1973
2336
  yield {
1974
2337
  type: "done",
1975
2338
  result: { exitCode: 1, stdout: "", stderr: errorMessage },
@@ -2106,37 +2469,18 @@ var GeminiAdapter = class extends BaseAdapter {
2106
2469
  getConfigPath() {
2107
2470
  return join3(homedir3(), ".gemini", "settings.json");
2108
2471
  }
2109
- async checkHealth() {
2110
- const HEALTH_TIMEOUT = 5e3;
2111
- const installed = await Promise.race([
2112
- this.isInstalled(),
2113
- new Promise(
2114
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
2115
- )
2116
- ]).catch(() => false);
2117
- 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) {
2118
2477
  return {
2119
- installed: false,
2120
- authenticated: false,
2121
- healthy: false,
2122
- 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"
2123
2481
  };
2124
2482
  }
2125
- const version = await Promise.race([
2126
- this.getVersion(),
2127
- new Promise(
2128
- (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
2129
- )
2130
- ]).catch(() => void 0);
2131
- const hasApiKey = !!process.env["GEMINI_API_KEY"];
2132
- const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
2133
- const authenticated = hasApiKey || hasGoogleAdc || true;
2134
- return {
2135
- installed: true,
2136
- authenticated,
2137
- healthy: true,
2138
- version
2139
- };
2483
+ return { authenticated };
2140
2484
  }
2141
2485
  mapFlags(flags) {
2142
2486
  const args = mapCommonToNative("gemini", flags);
@@ -3025,6 +3369,7 @@ var HooksEngine = class _HooksEngine {
3025
3369
  };
3026
3370
 
3027
3371
  // src/core/context-monitor.ts
3372
+ init_logger();
3028
3373
  var DEFAULT_BACKEND_CONTEXT = {
3029
3374
  claude: { contextWindow: 2e5, compactThreshold: 19e4 },
3030
3375
  codex: { contextWindow: 272e3, compactThreshold: 258400 },
@@ -3143,7 +3488,9 @@ var ContextMonitor = class {
3143
3488
  remainingBeforeCompact
3144
3489
  }
3145
3490
  };
3146
- 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
+ );
3147
3494
  }
3148
3495
  }
3149
3496
  };
@@ -3919,7 +4266,7 @@ function createVersionCommand(registry2) {
3919
4266
  description: "Show relay and backend versions"
3920
4267
  },
3921
4268
  async run() {
3922
- const relayVersion = "0.6.4";
4269
+ const relayVersion = "0.8.0";
3923
4270
  console.log(`agentic-relay v${relayVersion}`);
3924
4271
  console.log("");
3925
4272
  console.log("Backends:");
@@ -3946,9 +4293,9 @@ import { defineCommand as defineCommand8 } from "citty";
3946
4293
  import { access, constants, readdir as readdir2 } from "fs/promises";
3947
4294
  import { join as join7 } from "path";
3948
4295
  import { homedir as homedir5 } from "os";
3949
- import { execFile } from "child_process";
3950
- import { promisify } from "util";
3951
- var execFileAsync = promisify(execFile);
4296
+ import { execFile as execFile2 } from "child_process";
4297
+ import { promisify as promisify2 } from "util";
4298
+ var execFileAsync2 = promisify2(execFile2);
3952
4299
  async function checkNodeVersion() {
3953
4300
  const version = process.version;
3954
4301
  const major = Number(version.slice(1).split(".")[0]);
@@ -4048,7 +4395,7 @@ async function checkMCPServerCommands(configManager2) {
4048
4395
  for (const [name, server] of Object.entries(mcpServers)) {
4049
4396
  const command = server.command;
4050
4397
  try {
4051
- await execFileAsync("which", [command]);
4398
+ await execFileAsync2("which", [command]);
4052
4399
  results.push({
4053
4400
  label: `MCP server: ${name}`,
4054
4401
  ok: true,
@@ -4244,6 +4591,7 @@ function createInitCommand() {
4244
4591
  }
4245
4592
 
4246
4593
  // src/bin/relay.ts
4594
+ init_logger();
4247
4595
  var processManager = new ProcessManager();
4248
4596
  var registry = new AdapterRegistry();
4249
4597
  registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
@@ -4264,12 +4612,11 @@ void configManager.getConfig().then((config) => {
4264
4612
  if (config.contextMonitor) {
4265
4613
  contextMonitor = new ContextMonitor(hooksEngine, config.contextMonitor);
4266
4614
  }
4267
- }).catch(() => {
4268
- });
4615
+ }).catch((e) => logger.debug("Config load failed:", e));
4269
4616
  var main = defineCommand10({
4270
4617
  meta: {
4271
4618
  name: "relay",
4272
- version: "0.6.4",
4619
+ version: "0.8.0",
4273
4620
  description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
4274
4621
  },
4275
4622
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk0429/agentic-relay",
3
- "version": "0.6.4",
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": {