@rk0429/agentic-relay 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/relay.mjs +465 -34
- package/package.json +1 -1
package/dist/relay.mjs
CHANGED
|
@@ -158,19 +158,117 @@ var init_recursion_guard = __esm({
|
|
|
158
158
|
// src/mcp-server/tools/spawn-agent.ts
|
|
159
159
|
import { z as z2 } from "zod";
|
|
160
160
|
import { nanoid as nanoid2 } from "nanoid";
|
|
161
|
+
import { existsSync, readFileSync } from "fs";
|
|
162
|
+
import { join as join6 } from "path";
|
|
163
|
+
function buildContextInjection(metadata) {
|
|
164
|
+
const parts = [];
|
|
165
|
+
if (metadata.stateContent && typeof metadata.stateContent === "string") {
|
|
166
|
+
parts.push(`<context-injection type="state">
|
|
167
|
+
${metadata.stateContent}
|
|
168
|
+
</context-injection>`);
|
|
169
|
+
}
|
|
170
|
+
if (Array.isArray(metadata.results) && metadata.results.length > 0) {
|
|
171
|
+
const formatted = metadata.results.map((r, i) => `[${i + 1}] ${r.title} (${r.date}, score: ${r.score})
|
|
172
|
+
${r.snippet}`).join("\n\n");
|
|
173
|
+
parts.push(`<context-injection type="search-results">
|
|
174
|
+
${formatted}
|
|
175
|
+
</context-injection>`);
|
|
176
|
+
}
|
|
177
|
+
return parts.join("\n\n");
|
|
178
|
+
}
|
|
179
|
+
function readPreviousState(dailynoteDir) {
|
|
180
|
+
try {
|
|
181
|
+
const statePath = join6(dailynoteDir, "_state.md");
|
|
182
|
+
if (existsSync(statePath)) {
|
|
183
|
+
return readFileSync(statePath, "utf-8");
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.warn(
|
|
188
|
+
`Failed to read _state.md: ${error instanceof Error ? error.message : String(error)}`
|
|
189
|
+
);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function readAgentDefinition(definitionPath) {
|
|
194
|
+
try {
|
|
195
|
+
if (!existsSync(definitionPath)) {
|
|
196
|
+
logger.warn(`Agent definition file not found at ${definitionPath}`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const content = readFileSync(definitionPath, "utf-8");
|
|
200
|
+
if (content.trim().length === 0) {
|
|
201
|
+
logger.warn(`Agent definition file is empty at ${definitionPath}`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return content;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
logger.warn(
|
|
207
|
+
`Failed to read agent definition: ${error instanceof Error ? error.message : String(error)}`
|
|
208
|
+
);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function readSkillContext(skillContext) {
|
|
213
|
+
try {
|
|
214
|
+
const skillMdPath = join6(skillContext.skillPath, "SKILL.md");
|
|
215
|
+
if (!existsSync(skillMdPath)) {
|
|
216
|
+
logger.warn(
|
|
217
|
+
`SKILL.md not found at ${skillMdPath}`
|
|
218
|
+
);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const parts = [];
|
|
222
|
+
const skillContent = readFileSync(skillMdPath, "utf-8");
|
|
223
|
+
parts.push(skillContent);
|
|
224
|
+
if (skillContext.subskill) {
|
|
225
|
+
const subskillPath = join6(
|
|
226
|
+
skillContext.skillPath,
|
|
227
|
+
"subskills",
|
|
228
|
+
skillContext.subskill,
|
|
229
|
+
"SUBSKILL.md"
|
|
230
|
+
);
|
|
231
|
+
if (existsSync(subskillPath)) {
|
|
232
|
+
const subskillContent = readFileSync(subskillPath, "utf-8");
|
|
233
|
+
parts.push(subskillContent);
|
|
234
|
+
} else {
|
|
235
|
+
logger.warn(
|
|
236
|
+
`SUBSKILL.md not found at ${subskillPath}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return parts.join("\n\n");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
logger.warn(
|
|
243
|
+
`Failed to read skill context: ${error instanceof Error ? error.message : String(error)}`
|
|
244
|
+
);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
161
248
|
function buildContextFromEnv() {
|
|
162
249
|
const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${nanoid2()}`;
|
|
163
250
|
const parentSessionId = process.env["RELAY_PARENT_SESSION_ID"] ?? null;
|
|
164
251
|
const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
|
|
165
252
|
return { traceId, parentSessionId, depth };
|
|
166
253
|
}
|
|
167
|
-
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2) {
|
|
254
|
+
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector) {
|
|
255
|
+
let effectiveBackend = input.backend;
|
|
256
|
+
if (backendSelector) {
|
|
257
|
+
const availableBackends = registry2.listIds();
|
|
258
|
+
const selectionContext = {
|
|
259
|
+
availableBackends,
|
|
260
|
+
preferredBackend: input.preferredBackend,
|
|
261
|
+
agentType: input.agent,
|
|
262
|
+
taskType: input.taskType
|
|
263
|
+
};
|
|
264
|
+
effectiveBackend = backendSelector.selectBackend(selectionContext);
|
|
265
|
+
}
|
|
168
266
|
const envContext = buildContextFromEnv();
|
|
169
267
|
const promptHash = RecursionGuard.hashPrompt(input.prompt);
|
|
170
268
|
const context = {
|
|
171
269
|
traceId: envContext.traceId,
|
|
172
270
|
depth: envContext.depth,
|
|
173
|
-
backend:
|
|
271
|
+
backend: effectiveBackend,
|
|
174
272
|
promptHash
|
|
175
273
|
};
|
|
176
274
|
const guardResult = guard.canSpawn(context);
|
|
@@ -183,27 +281,54 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
183
281
|
stderr: `Spawn blocked: ${guardResult.reason}`
|
|
184
282
|
};
|
|
185
283
|
}
|
|
186
|
-
const adapter = registry2.get(
|
|
284
|
+
const adapter = registry2.get(effectiveBackend);
|
|
187
285
|
const installed = await adapter.isInstalled();
|
|
188
286
|
if (!installed) {
|
|
189
287
|
return {
|
|
190
288
|
sessionId: "",
|
|
191
289
|
exitCode: 1,
|
|
192
290
|
stdout: "",
|
|
193
|
-
stderr: `Backend "${
|
|
291
|
+
stderr: `Backend "${effectiveBackend}" is not available. Use list_available_backends to see available options.`
|
|
194
292
|
};
|
|
195
293
|
}
|
|
294
|
+
const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
196
295
|
const session = await sessionManager2.create({
|
|
197
|
-
backendId:
|
|
296
|
+
backendId: effectiveBackend,
|
|
198
297
|
parentSessionId: envContext.parentSessionId ?? void 0,
|
|
199
298
|
depth: envContext.depth + 1
|
|
200
299
|
});
|
|
300
|
+
let collectedMetadata = {};
|
|
301
|
+
if (hooksEngine2 && !input.resumeSessionId) {
|
|
302
|
+
try {
|
|
303
|
+
const cwd = process.cwd();
|
|
304
|
+
const dailynoteDir = join6(cwd, "daily_note");
|
|
305
|
+
const hookInput = {
|
|
306
|
+
schemaVersion: "1.0",
|
|
307
|
+
event: "session-init",
|
|
308
|
+
sessionId: session.relaySessionId,
|
|
309
|
+
backendId: effectiveBackend,
|
|
310
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
311
|
+
data: {
|
|
312
|
+
workingDirectory: cwd,
|
|
313
|
+
dailynoteDir,
|
|
314
|
+
isFirstSession: !existsSync(join6(dailynoteDir, "_state.md")),
|
|
315
|
+
previousState: readPreviousState(dailynoteDir)
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const { mergedMetadata } = await hooksEngine2.emitAndCollectMetadata("session-init", hookInput);
|
|
319
|
+
collectedMetadata = { ...collectedMetadata, ...mergedMetadata };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
logger.warn(
|
|
322
|
+
`session-init hook error: ${error instanceof Error ? error.message : String(error)}`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
201
326
|
if (hooksEngine2) {
|
|
202
327
|
try {
|
|
203
328
|
const hookInput = {
|
|
204
329
|
event: "pre-spawn",
|
|
205
330
|
sessionId: session.relaySessionId,
|
|
206
|
-
backendId:
|
|
331
|
+
backendId: effectiveBackend,
|
|
207
332
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
208
333
|
data: {
|
|
209
334
|
prompt: input.prompt,
|
|
@@ -211,13 +336,40 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
211
336
|
model: input.model
|
|
212
337
|
}
|
|
213
338
|
};
|
|
214
|
-
await hooksEngine2.
|
|
339
|
+
const { mergedMetadata } = await hooksEngine2.emitAndCollectMetadata("pre-spawn", hookInput);
|
|
340
|
+
collectedMetadata = { ...collectedMetadata, ...mergedMetadata };
|
|
215
341
|
} catch (error) {
|
|
216
342
|
logger.debug(
|
|
217
343
|
`pre-spawn hook error: ${error instanceof Error ? error.message : String(error)}`
|
|
218
344
|
);
|
|
219
345
|
}
|
|
220
346
|
}
|
|
347
|
+
const injectionText = buildContextInjection(collectedMetadata);
|
|
348
|
+
let enhancedSystemPrompt = injectionText ? input.systemPrompt ? `${input.systemPrompt}
|
|
349
|
+
|
|
350
|
+
${injectionText}` : injectionText : input.systemPrompt;
|
|
351
|
+
if (input.skillContext) {
|
|
352
|
+
const skillText = readSkillContext(input.skillContext);
|
|
353
|
+
if (skillText) {
|
|
354
|
+
const wrapped = `<skill-context path="${input.skillContext.skillPath}"${input.skillContext.subskill ? ` subskill="${input.skillContext.subskill}"` : ""}>
|
|
355
|
+
${skillText}
|
|
356
|
+
</skill-context>`;
|
|
357
|
+
enhancedSystemPrompt = enhancedSystemPrompt ? `${enhancedSystemPrompt}
|
|
358
|
+
|
|
359
|
+
${wrapped}` : wrapped;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (input.agentDefinition) {
|
|
363
|
+
const defText = readAgentDefinition(input.agentDefinition.definitionPath);
|
|
364
|
+
if (defText) {
|
|
365
|
+
const wrapped = `<agent-definition source="${input.agentDefinition.definitionPath}">
|
|
366
|
+
${defText}
|
|
367
|
+
</agent-definition>`;
|
|
368
|
+
enhancedSystemPrompt = enhancedSystemPrompt ? `${enhancedSystemPrompt}
|
|
369
|
+
|
|
370
|
+
${wrapped}` : wrapped;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
221
373
|
try {
|
|
222
374
|
let result;
|
|
223
375
|
if (input.resumeSessionId) {
|
|
@@ -226,7 +378,7 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
226
378
|
sessionId: session.relaySessionId,
|
|
227
379
|
exitCode: 1,
|
|
228
380
|
stdout: "",
|
|
229
|
-
stderr: `Backend "${
|
|
381
|
+
stderr: `Backend "${effectiveBackend}" does not support session continuation (continueSession).`
|
|
230
382
|
};
|
|
231
383
|
}
|
|
232
384
|
result = await adapter.continueSession(input.resumeSessionId, input.prompt);
|
|
@@ -234,7 +386,7 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
234
386
|
result = await adapter.execute({
|
|
235
387
|
prompt: input.prompt,
|
|
236
388
|
agent: input.agent,
|
|
237
|
-
systemPrompt:
|
|
389
|
+
systemPrompt: enhancedSystemPrompt,
|
|
238
390
|
model: input.model,
|
|
239
391
|
maxTurns: input.maxTurns,
|
|
240
392
|
mcpContext: {
|
|
@@ -251,7 +403,7 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
251
403
|
);
|
|
252
404
|
contextMonitor2.updateUsage(
|
|
253
405
|
session.relaySessionId,
|
|
254
|
-
|
|
406
|
+
effectiveBackend,
|
|
255
407
|
estimatedTokens
|
|
256
408
|
);
|
|
257
409
|
}
|
|
@@ -260,15 +412,27 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
260
412
|
await sessionManager2.update(session.relaySessionId, { status });
|
|
261
413
|
if (hooksEngine2) {
|
|
262
414
|
try {
|
|
415
|
+
const postSpawnData = {
|
|
416
|
+
exitCode: result.exitCode,
|
|
417
|
+
status,
|
|
418
|
+
taskDescription: input.prompt,
|
|
419
|
+
startedAt: spawnStartedAt,
|
|
420
|
+
backgroundMode: false,
|
|
421
|
+
sessionId: session.relaySessionId,
|
|
422
|
+
selectedBackend: effectiveBackend
|
|
423
|
+
};
|
|
424
|
+
if (input.agent !== void 0) {
|
|
425
|
+
postSpawnData.agentType = input.agent;
|
|
426
|
+
}
|
|
427
|
+
if (input.model !== void 0) {
|
|
428
|
+
postSpawnData.model = input.model;
|
|
429
|
+
}
|
|
263
430
|
const hookInput = {
|
|
264
431
|
event: "post-spawn",
|
|
265
432
|
sessionId: session.relaySessionId,
|
|
266
|
-
backendId:
|
|
433
|
+
backendId: effectiveBackend,
|
|
267
434
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
268
|
-
data:
|
|
269
|
-
exitCode: result.exitCode,
|
|
270
|
-
status
|
|
271
|
-
}
|
|
435
|
+
data: postSpawnData
|
|
272
436
|
};
|
|
273
437
|
await hooksEngine2.emit("post-spawn", hookInput);
|
|
274
438
|
} catch (hookError) {
|
|
@@ -308,7 +472,16 @@ var init_spawn_agent = __esm({
|
|
|
308
472
|
systemPrompt: z2.string().optional(),
|
|
309
473
|
resumeSessionId: z2.string().optional(),
|
|
310
474
|
model: z2.string().optional(),
|
|
311
|
-
maxTurns: z2.number().optional()
|
|
475
|
+
maxTurns: z2.number().optional(),
|
|
476
|
+
skillContext: z2.object({
|
|
477
|
+
skillPath: z2.string().describe("Path to the skill directory (e.g., '.agents/skills/software-engineer/')"),
|
|
478
|
+
subskill: z2.string().optional().describe("Specific subskill to activate")
|
|
479
|
+
}).optional().describe("Skill context to inject into the subagent's system prompt"),
|
|
480
|
+
agentDefinition: z2.object({
|
|
481
|
+
definitionPath: z2.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
|
|
482
|
+
}).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
|
|
483
|
+
preferredBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
|
|
484
|
+
taskType: z2.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
|
|
312
485
|
});
|
|
313
486
|
}
|
|
314
487
|
});
|
|
@@ -397,6 +570,65 @@ var init_list_available_backends = __esm({
|
|
|
397
570
|
}
|
|
398
571
|
});
|
|
399
572
|
|
|
573
|
+
// src/core/backend-selector.ts
|
|
574
|
+
var DEFAULT_AGENT_TO_BACKEND_MAP, TASK_TYPE_TO_BACKEND_MAP, DEFAULT_BACKEND, BackendSelector;
|
|
575
|
+
var init_backend_selector = __esm({
|
|
576
|
+
"src/core/backend-selector.ts"() {
|
|
577
|
+
"use strict";
|
|
578
|
+
DEFAULT_AGENT_TO_BACKEND_MAP = {
|
|
579
|
+
"software-engineer": "codex",
|
|
580
|
+
"devops-engineer": "codex",
|
|
581
|
+
"skill-developer": "codex",
|
|
582
|
+
"security-auditor": "codex",
|
|
583
|
+
"analytics-engineer": "codex",
|
|
584
|
+
"payments-billing": "codex"
|
|
585
|
+
};
|
|
586
|
+
TASK_TYPE_TO_BACKEND_MAP = {
|
|
587
|
+
code: "codex",
|
|
588
|
+
document: "claude",
|
|
589
|
+
analysis: "claude",
|
|
590
|
+
mixed: "claude"
|
|
591
|
+
};
|
|
592
|
+
DEFAULT_BACKEND = "claude";
|
|
593
|
+
BackendSelector = class {
|
|
594
|
+
defaultBackend;
|
|
595
|
+
agentToBackendMap;
|
|
596
|
+
constructor(config) {
|
|
597
|
+
this.defaultBackend = config?.defaultBackend ?? DEFAULT_BACKEND;
|
|
598
|
+
this.agentToBackendMap = config?.agentToBackendMap ?? DEFAULT_AGENT_TO_BACKEND_MAP;
|
|
599
|
+
}
|
|
600
|
+
selectBackend(context) {
|
|
601
|
+
const { availableBackends, preferredBackend, agentType, taskType } = context;
|
|
602
|
+
if (availableBackends.length === 0) {
|
|
603
|
+
throw new Error("No backends available");
|
|
604
|
+
}
|
|
605
|
+
if (preferredBackend && availableBackends.includes(preferredBackend)) {
|
|
606
|
+
return preferredBackend;
|
|
607
|
+
}
|
|
608
|
+
if (agentType) {
|
|
609
|
+
const mapped = this.agentToBackendMap[agentType];
|
|
610
|
+
if (mapped && availableBackends.includes(mapped)) {
|
|
611
|
+
return mapped;
|
|
612
|
+
}
|
|
613
|
+
if (!mapped && availableBackends.includes("claude")) {
|
|
614
|
+
return "claude";
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (taskType) {
|
|
618
|
+
const mapped = TASK_TYPE_TO_BACKEND_MAP[taskType];
|
|
619
|
+
if (mapped && availableBackends.includes(mapped)) {
|
|
620
|
+
return mapped;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (availableBackends.includes(this.defaultBackend)) {
|
|
624
|
+
return this.defaultBackend;
|
|
625
|
+
}
|
|
626
|
+
return availableBackends[0];
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
400
632
|
// src/mcp-server/server.ts
|
|
401
633
|
var server_exports = {};
|
|
402
634
|
__export(server_exports, {
|
|
@@ -417,6 +649,7 @@ var init_server = __esm({
|
|
|
417
649
|
init_list_sessions();
|
|
418
650
|
init_get_context_status();
|
|
419
651
|
init_list_available_backends();
|
|
652
|
+
init_backend_selector();
|
|
420
653
|
init_logger();
|
|
421
654
|
RelayMCPServer = class {
|
|
422
655
|
constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
|
|
@@ -425,6 +658,7 @@ var init_server = __esm({
|
|
|
425
658
|
this.hooksEngine = hooksEngine2;
|
|
426
659
|
this.contextMonitor = contextMonitor2;
|
|
427
660
|
this.guard = new RecursionGuard(guardConfig);
|
|
661
|
+
this.backendSelector = new BackendSelector();
|
|
428
662
|
this.server = new McpServer({
|
|
429
663
|
name: "agentic-relay",
|
|
430
664
|
version: "0.4.0"
|
|
@@ -433,10 +667,11 @@ var init_server = __esm({
|
|
|
433
667
|
}
|
|
434
668
|
server;
|
|
435
669
|
guard;
|
|
670
|
+
backendSelector;
|
|
436
671
|
registerTools() {
|
|
437
672
|
this.server.tool(
|
|
438
673
|
"spawn_agent",
|
|
439
|
-
"Spawn a sub-agent on the specified backend CLI (Claude Code, Codex CLI, or Gemini CLI). The agent executes the given prompt in non-interactive mode and returns the result. Use 'agent' for named agent configurations (Claude only),
|
|
674
|
+
"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.",
|
|
440
675
|
{
|
|
441
676
|
backend: z5.enum(["claude", "codex", "gemini"]),
|
|
442
677
|
prompt: z5.string(),
|
|
@@ -446,7 +681,16 @@ var init_server = __esm({
|
|
|
446
681
|
),
|
|
447
682
|
resumeSessionId: z5.string().optional(),
|
|
448
683
|
model: z5.string().optional(),
|
|
449
|
-
maxTurns: z5.number().optional()
|
|
684
|
+
maxTurns: z5.number().optional(),
|
|
685
|
+
skillContext: z5.object({
|
|
686
|
+
skillPath: z5.string().describe("Path to the skill directory (e.g., '.agents/skills/software-engineer/')"),
|
|
687
|
+
subskill: z5.string().optional().describe("Specific subskill to activate")
|
|
688
|
+
}).optional().describe("Skill context to inject into the sub-agent's system prompt"),
|
|
689
|
+
agentDefinition: z5.object({
|
|
690
|
+
definitionPath: z5.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
|
|
691
|
+
}).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
|
|
692
|
+
preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
|
|
693
|
+
taskType: z5.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
|
|
450
694
|
},
|
|
451
695
|
async (params) => {
|
|
452
696
|
try {
|
|
@@ -456,7 +700,8 @@ var init_server = __esm({
|
|
|
456
700
|
this.sessionManager,
|
|
457
701
|
this.guard,
|
|
458
702
|
this.hooksEngine,
|
|
459
|
-
this.contextMonitor
|
|
703
|
+
this.contextMonitor,
|
|
704
|
+
this.backendSelector
|
|
460
705
|
);
|
|
461
706
|
const isError = result.exitCode !== 0;
|
|
462
707
|
const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
|
|
@@ -607,7 +852,7 @@ ${result.stdout}`;
|
|
|
607
852
|
|
|
608
853
|
// src/bin/relay.ts
|
|
609
854
|
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
610
|
-
import { join as
|
|
855
|
+
import { join as join9 } from "path";
|
|
611
856
|
import { homedir as homedir6 } from "os";
|
|
612
857
|
|
|
613
858
|
// src/infrastructure/process-manager.ts
|
|
@@ -736,6 +981,10 @@ var AdapterRegistry = class {
|
|
|
736
981
|
this.factories.clear();
|
|
737
982
|
return [...this.adapters.values()];
|
|
738
983
|
}
|
|
984
|
+
/** Return registered backend IDs without instantiating lazy adapters */
|
|
985
|
+
listIds() {
|
|
986
|
+
return [.../* @__PURE__ */ new Set([...this.adapters.keys(), ...this.factories.keys()])];
|
|
987
|
+
}
|
|
739
988
|
};
|
|
740
989
|
|
|
741
990
|
// src/adapters/base-adapter.ts
|
|
@@ -894,6 +1143,17 @@ var CLAUDE_NESTING_ENV_VARS = [
|
|
|
894
1143
|
"CLAUDE_CODE_SSE_PORT",
|
|
895
1144
|
"CLAUDE_CODE_ENTRYPOINT"
|
|
896
1145
|
];
|
|
1146
|
+
var DEFAULT_CLAUDE_SDK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1147
|
+
function getClaudeSdkTimeoutMs() {
|
|
1148
|
+
const envVal = process.env["RELAY_CLAUDE_TIMEOUT_MS"];
|
|
1149
|
+
if (envVal) {
|
|
1150
|
+
const parsed = Number(envVal);
|
|
1151
|
+
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
1152
|
+
return parsed;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return DEFAULT_CLAUDE_SDK_TIMEOUT_MS;
|
|
1156
|
+
}
|
|
897
1157
|
var ClaudeAdapter = class extends BaseAdapter {
|
|
898
1158
|
id = "claude";
|
|
899
1159
|
command = "claude";
|
|
@@ -962,11 +1222,16 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
962
1222
|
}
|
|
963
1223
|
const env = this.buildCleanEnv(flags);
|
|
964
1224
|
const permissionMode = this.getPermissionMode();
|
|
1225
|
+
const timeoutMs = getClaudeSdkTimeoutMs();
|
|
1226
|
+
const abortController = new AbortController();
|
|
1227
|
+
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
965
1228
|
try {
|
|
966
1229
|
const { query } = await loadClaudeSDK();
|
|
967
1230
|
const options = {
|
|
1231
|
+
abortController,
|
|
968
1232
|
env,
|
|
969
1233
|
cwd: process.cwd(),
|
|
1234
|
+
mcpServers: {},
|
|
970
1235
|
...flags.model ? { model: flags.model } : {},
|
|
971
1236
|
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
|
|
972
1237
|
...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
|
|
@@ -1002,11 +1267,20 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1002
1267
|
...sessionId ? { nativeSessionId: sessionId } : {}
|
|
1003
1268
|
};
|
|
1004
1269
|
} catch (error) {
|
|
1270
|
+
if (abortController.signal.aborted) {
|
|
1271
|
+
return {
|
|
1272
|
+
exitCode: 1,
|
|
1273
|
+
stdout: "",
|
|
1274
|
+
stderr: `Claude SDK query timed out after ${timeoutMs}ms`
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1005
1277
|
return {
|
|
1006
1278
|
exitCode: 1,
|
|
1007
1279
|
stdout: "",
|
|
1008
1280
|
stderr: error instanceof Error ? error.message : String(error)
|
|
1009
1281
|
};
|
|
1282
|
+
} finally {
|
|
1283
|
+
clearTimeout(timer);
|
|
1010
1284
|
}
|
|
1011
1285
|
}
|
|
1012
1286
|
async *executeStreaming(flags) {
|
|
@@ -1015,11 +1289,16 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1015
1289
|
}
|
|
1016
1290
|
const env = this.buildCleanEnv(flags);
|
|
1017
1291
|
const permissionMode = this.getPermissionMode();
|
|
1292
|
+
const timeoutMs = getClaudeSdkTimeoutMs();
|
|
1293
|
+
const abortController = new AbortController();
|
|
1294
|
+
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
1018
1295
|
try {
|
|
1019
1296
|
const { query } = await loadClaudeSDK();
|
|
1020
1297
|
const options = {
|
|
1298
|
+
abortController,
|
|
1021
1299
|
env,
|
|
1022
1300
|
cwd: process.cwd(),
|
|
1301
|
+
mcpServers: {},
|
|
1023
1302
|
...flags.model ? { model: flags.model } : {},
|
|
1024
1303
|
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
|
|
1025
1304
|
...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
|
|
@@ -1079,22 +1358,29 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1079
1358
|
}
|
|
1080
1359
|
}
|
|
1081
1360
|
} catch (error) {
|
|
1082
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1361
|
+
const errorMessage = abortController.signal.aborted ? `Claude SDK query timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
1083
1362
|
yield { type: "error", message: errorMessage };
|
|
1084
1363
|
yield {
|
|
1085
1364
|
type: "done",
|
|
1086
1365
|
result: { exitCode: 1, stdout: "", stderr: errorMessage }
|
|
1087
1366
|
};
|
|
1367
|
+
} finally {
|
|
1368
|
+
clearTimeout(timer);
|
|
1088
1369
|
}
|
|
1089
1370
|
}
|
|
1090
1371
|
async continueSession(nativeSessionId, prompt) {
|
|
1372
|
+
const timeoutMs = getClaudeSdkTimeoutMs();
|
|
1373
|
+
const abortController = new AbortController();
|
|
1374
|
+
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
1091
1375
|
try {
|
|
1092
1376
|
const { query } = await loadClaudeSDK();
|
|
1093
1377
|
const permissionMode = this.getPermissionMode();
|
|
1094
1378
|
const options = {
|
|
1379
|
+
abortController,
|
|
1095
1380
|
resume: nativeSessionId,
|
|
1096
1381
|
maxTurns: 1,
|
|
1097
|
-
cwd: process.cwd()
|
|
1382
|
+
cwd: process.cwd(),
|
|
1383
|
+
mcpServers: {}
|
|
1098
1384
|
};
|
|
1099
1385
|
if (permissionMode === "bypassPermissions") {
|
|
1100
1386
|
options.permissionMode = "bypassPermissions";
|
|
@@ -1118,11 +1404,20 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1118
1404
|
stderr: ""
|
|
1119
1405
|
};
|
|
1120
1406
|
} catch (error) {
|
|
1407
|
+
if (abortController.signal.aborted) {
|
|
1408
|
+
return {
|
|
1409
|
+
exitCode: 1,
|
|
1410
|
+
stdout: "",
|
|
1411
|
+
stderr: `Claude SDK query timed out after ${timeoutMs}ms`
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1121
1414
|
return {
|
|
1122
1415
|
exitCode: 1,
|
|
1123
1416
|
stdout: "",
|
|
1124
1417
|
stderr: error instanceof Error ? error.message : String(error)
|
|
1125
1418
|
};
|
|
1419
|
+
} finally {
|
|
1420
|
+
clearTimeout(timer);
|
|
1126
1421
|
}
|
|
1127
1422
|
}
|
|
1128
1423
|
async resumeSession(sessionId, flags) {
|
|
@@ -2031,6 +2326,7 @@ var mcpServerConfigSchema = z.object({
|
|
|
2031
2326
|
env: z.record(z.string()).optional()
|
|
2032
2327
|
});
|
|
2033
2328
|
var hookEventSchema = z.enum([
|
|
2329
|
+
"session-init",
|
|
2034
2330
|
"pre-prompt",
|
|
2035
2331
|
"post-response",
|
|
2036
2332
|
"on-error",
|
|
@@ -2046,8 +2342,19 @@ var hookDefinitionSchema = z.object({
|
|
|
2046
2342
|
enabled: z.boolean().optional(),
|
|
2047
2343
|
onError: z.enum(["ignore", "warn", "abort"]).optional()
|
|
2048
2344
|
});
|
|
2345
|
+
var hookChainStepSchema = z.object({
|
|
2346
|
+
stepId: z.string(),
|
|
2347
|
+
hook: hookDefinitionSchema,
|
|
2348
|
+
pipeOutput: z.boolean().optional()
|
|
2349
|
+
});
|
|
2350
|
+
var hookChainSchema = z.object({
|
|
2351
|
+
name: z.string(),
|
|
2352
|
+
steps: z.array(hookChainStepSchema).min(1),
|
|
2353
|
+
onError: z.enum(["ignore", "warn", "fail"]).optional()
|
|
2354
|
+
});
|
|
2049
2355
|
var hooksConfigSchema = z.object({
|
|
2050
|
-
definitions: z.array(hookDefinitionSchema)
|
|
2356
|
+
definitions: z.array(hookDefinitionSchema),
|
|
2357
|
+
chains: z.array(hookChainSchema).optional()
|
|
2051
2358
|
});
|
|
2052
2359
|
var backendContextConfigSchema = z.object({
|
|
2053
2360
|
contextWindow: z.number().positive().optional(),
|
|
@@ -2345,6 +2652,7 @@ var HooksEngine = class _HooksEngine {
|
|
|
2345
2652
|
static COMMAND_PATTERN = /^[a-zA-Z0-9_./-]+$/;
|
|
2346
2653
|
static ARG_PATTERN = /^[a-zA-Z0-9_.=:/-]+$/;
|
|
2347
2654
|
definitions = [];
|
|
2655
|
+
chains = [];
|
|
2348
2656
|
registered = false;
|
|
2349
2657
|
validateCommand(command) {
|
|
2350
2658
|
if (!command || command.trim().length === 0) {
|
|
@@ -2385,6 +2693,7 @@ var HooksEngine = class _HooksEngine {
|
|
|
2385
2693
|
return false;
|
|
2386
2694
|
}
|
|
2387
2695
|
});
|
|
2696
|
+
this.chains = config.chains ?? [];
|
|
2388
2697
|
if (!this.registered) {
|
|
2389
2698
|
for (const event of this.getUniqueEvents()) {
|
|
2390
2699
|
this.eventBus.on(event, async () => {
|
|
@@ -2426,10 +2735,132 @@ var HooksEngine = class _HooksEngine {
|
|
|
2426
2735
|
await this.eventBus.emit(event, input);
|
|
2427
2736
|
return results;
|
|
2428
2737
|
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Emit a hook event, execute all matching hooks, and return merged metadata.
|
|
2740
|
+
* Merge rule: later hooks overwrite earlier hooks' metadata fields.
|
|
2741
|
+
* If any hook returns allow: false, subsequent hooks are skipped.
|
|
2742
|
+
*/
|
|
2743
|
+
async emitAndCollectMetadata(event, hookInput) {
|
|
2744
|
+
const matchingDefs = this.definitions.filter((def) => def.event === event);
|
|
2745
|
+
const results = [];
|
|
2746
|
+
let mergedMetadata = {};
|
|
2747
|
+
for (const def of matchingDefs) {
|
|
2748
|
+
try {
|
|
2749
|
+
const result = await this.executeHook(def, hookInput);
|
|
2750
|
+
results.push(result);
|
|
2751
|
+
if (result.output.metadata && typeof result.output.metadata === "object") {
|
|
2752
|
+
mergedMetadata = { ...mergedMetadata, ...result.output.metadata };
|
|
2753
|
+
}
|
|
2754
|
+
if (!result.output.allow) {
|
|
2755
|
+
logger.info(
|
|
2756
|
+
`Hook "${def.command}" returned allow: false, skipping subsequent hooks for event "${event}"`
|
|
2757
|
+
);
|
|
2758
|
+
break;
|
|
2759
|
+
}
|
|
2760
|
+
} catch (error) {
|
|
2761
|
+
const strategy = def.onError ?? "warn";
|
|
2762
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2763
|
+
if (strategy === "abort") {
|
|
2764
|
+
throw new Error(
|
|
2765
|
+
`Hook "${def.command}" failed (abort): ${message}`
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
if (strategy === "warn") {
|
|
2769
|
+
logger.warn(
|
|
2770
|
+
`Hook "${def.command}" failed (warn): ${message}`
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
results.push({
|
|
2774
|
+
exitCode: 1,
|
|
2775
|
+
stdout: "",
|
|
2776
|
+
stderr: message,
|
|
2777
|
+
durationMs: 0,
|
|
2778
|
+
output: { ...DEFAULT_HOOK_OUTPUT }
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
await this.eventBus.emit(event, hookInput);
|
|
2783
|
+
return { results, mergedMetadata };
|
|
2784
|
+
}
|
|
2429
2785
|
/** Get the count of enabled definitions for a given event */
|
|
2430
2786
|
getDefinitionCount(event) {
|
|
2431
2787
|
return this.definitions.filter((def) => def.event === event).length;
|
|
2432
2788
|
}
|
|
2789
|
+
/** Get all loaded chain names */
|
|
2790
|
+
getChainNames() {
|
|
2791
|
+
return this.chains.map((c) => c.name);
|
|
2792
|
+
}
|
|
2793
|
+
/**
|
|
2794
|
+
* Execute a named hook chain.
|
|
2795
|
+
* Steps execute in sequence. If pipeOutput is true, previous step's
|
|
2796
|
+
* HookOutput is merged into the next step's HookInput.data.
|
|
2797
|
+
*/
|
|
2798
|
+
async executeChain(chainName, initialInput) {
|
|
2799
|
+
const chain = this.chains.find((c) => c.name === chainName);
|
|
2800
|
+
if (!chain) {
|
|
2801
|
+
throw new Error(`Hook chain "${chainName}" not found`);
|
|
2802
|
+
}
|
|
2803
|
+
const errorStrategy = chain.onError ?? "warn";
|
|
2804
|
+
const stepResults = [];
|
|
2805
|
+
let mergedMetadata = {};
|
|
2806
|
+
let previousOutput;
|
|
2807
|
+
let overallSuccess = true;
|
|
2808
|
+
for (const step of chain.steps) {
|
|
2809
|
+
const startTime = Date.now();
|
|
2810
|
+
let stepInput;
|
|
2811
|
+
if (step.pipeOutput && previousOutput) {
|
|
2812
|
+
stepInput = {
|
|
2813
|
+
...initialInput,
|
|
2814
|
+
data: {
|
|
2815
|
+
...initialInput.data,
|
|
2816
|
+
...previousOutput.metadata,
|
|
2817
|
+
_previousMessage: previousOutput.message,
|
|
2818
|
+
_previousAllow: previousOutput.allow
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
} else {
|
|
2822
|
+
stepInput = { ...initialInput };
|
|
2823
|
+
}
|
|
2824
|
+
try {
|
|
2825
|
+
const result = await this.executeHook(step.hook, stepInput);
|
|
2826
|
+
const durationMs = Date.now() - startTime;
|
|
2827
|
+
stepResults.push({
|
|
2828
|
+
stepId: step.stepId,
|
|
2829
|
+
success: true,
|
|
2830
|
+
output: result.output,
|
|
2831
|
+
durationMs
|
|
2832
|
+
});
|
|
2833
|
+
if (result.output.metadata && typeof result.output.metadata === "object") {
|
|
2834
|
+
mergedMetadata = { ...mergedMetadata, ...result.output.metadata };
|
|
2835
|
+
}
|
|
2836
|
+
previousOutput = result.output;
|
|
2837
|
+
} catch (error) {
|
|
2838
|
+
const durationMs = Date.now() - startTime;
|
|
2839
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2840
|
+
stepResults.push({
|
|
2841
|
+
stepId: step.stepId,
|
|
2842
|
+
success: false,
|
|
2843
|
+
error: message,
|
|
2844
|
+
durationMs
|
|
2845
|
+
});
|
|
2846
|
+
overallSuccess = false;
|
|
2847
|
+
if (errorStrategy === "fail") {
|
|
2848
|
+
break;
|
|
2849
|
+
}
|
|
2850
|
+
if (errorStrategy === "warn") {
|
|
2851
|
+
logger.warn(
|
|
2852
|
+
`Hook chain "${chainName}" step "${step.stepId}" failed (warn): ${message}`
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return {
|
|
2858
|
+
chainName,
|
|
2859
|
+
stepResults,
|
|
2860
|
+
success: overallSuccess,
|
|
2861
|
+
mergedMetadata
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2433
2864
|
async executeHook(def, input) {
|
|
2434
2865
|
this.validateCommand(def.command);
|
|
2435
2866
|
this.validateArgs(def.args ?? []);
|
|
@@ -3387,7 +3818,7 @@ function createVersionCommand(registry2) {
|
|
|
3387
3818
|
// src/commands/doctor.ts
|
|
3388
3819
|
import { defineCommand as defineCommand8 } from "citty";
|
|
3389
3820
|
import { access, constants, readdir as readdir2 } from "fs/promises";
|
|
3390
|
-
import { join as
|
|
3821
|
+
import { join as join7 } from "path";
|
|
3391
3822
|
import { homedir as homedir5 } from "os";
|
|
3392
3823
|
import { execFile } from "child_process";
|
|
3393
3824
|
import { promisify } from "util";
|
|
@@ -3448,8 +3879,8 @@ async function checkConfig(configManager2) {
|
|
|
3448
3879
|
}
|
|
3449
3880
|
}
|
|
3450
3881
|
async function checkSessionsDir() {
|
|
3451
|
-
const relayHome2 = process.env["RELAY_HOME"] ??
|
|
3452
|
-
const sessionsDir =
|
|
3882
|
+
const relayHome2 = process.env["RELAY_HOME"] ?? join7(homedir5(), ".relay");
|
|
3883
|
+
const sessionsDir = join7(relayHome2, "sessions");
|
|
3453
3884
|
try {
|
|
3454
3885
|
await access(sessionsDir, constants.W_OK);
|
|
3455
3886
|
return {
|
|
@@ -3562,8 +3993,8 @@ async function checkBackendAuthEnv() {
|
|
|
3562
3993
|
return results;
|
|
3563
3994
|
}
|
|
3564
3995
|
async function checkSessionsDiskUsage() {
|
|
3565
|
-
const relayHome2 = process.env["RELAY_HOME"] ??
|
|
3566
|
-
const sessionsDir =
|
|
3996
|
+
const relayHome2 = process.env["RELAY_HOME"] ?? join7(homedir5(), ".relay");
|
|
3997
|
+
const sessionsDir = join7(relayHome2, "sessions");
|
|
3567
3998
|
try {
|
|
3568
3999
|
const entries = await readdir2(sessionsDir);
|
|
3569
4000
|
const fileCount = entries.length;
|
|
@@ -3637,7 +4068,7 @@ function createDoctorCommand(registry2, configManager2) {
|
|
|
3637
4068
|
init_logger();
|
|
3638
4069
|
import { defineCommand as defineCommand9 } from "citty";
|
|
3639
4070
|
import { mkdir as mkdir6, writeFile as writeFile6, access as access2, readFile as readFile6 } from "fs/promises";
|
|
3640
|
-
import { join as
|
|
4071
|
+
import { join as join8 } from "path";
|
|
3641
4072
|
var DEFAULT_CONFIG2 = {
|
|
3642
4073
|
defaultBackend: "claude",
|
|
3643
4074
|
backends: {},
|
|
@@ -3651,8 +4082,8 @@ function createInitCommand() {
|
|
|
3651
4082
|
},
|
|
3652
4083
|
async run() {
|
|
3653
4084
|
const projectDir = process.cwd();
|
|
3654
|
-
const relayDir =
|
|
3655
|
-
const configPath =
|
|
4085
|
+
const relayDir = join8(projectDir, ".relay");
|
|
4086
|
+
const configPath = join8(relayDir, "config.json");
|
|
3656
4087
|
try {
|
|
3657
4088
|
await access2(relayDir);
|
|
3658
4089
|
logger.info(
|
|
@@ -3668,7 +4099,7 @@ function createInitCommand() {
|
|
|
3668
4099
|
"utf-8"
|
|
3669
4100
|
);
|
|
3670
4101
|
logger.success(`Created ${configPath}`);
|
|
3671
|
-
const gitignorePath =
|
|
4102
|
+
const gitignorePath = join8(projectDir, ".gitignore");
|
|
3672
4103
|
try {
|
|
3673
4104
|
const gitignoreContent = await readFile6(gitignorePath, "utf-8");
|
|
3674
4105
|
if (!gitignoreContent.includes(".relay/config.local.json")) {
|
|
@@ -3693,8 +4124,8 @@ registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
|
|
|
3693
4124
|
registry.registerLazy("codex", () => new CodexAdapter(processManager));
|
|
3694
4125
|
registry.registerLazy("gemini", () => new GeminiAdapter(processManager));
|
|
3695
4126
|
var sessionManager = new SessionManager();
|
|
3696
|
-
var relayHome = process.env["RELAY_HOME"] ??
|
|
3697
|
-
var projectRelayDir =
|
|
4127
|
+
var relayHome = process.env["RELAY_HOME"] ?? join9(homedir6(), ".relay");
|
|
4128
|
+
var projectRelayDir = join9(process.cwd(), ".relay");
|
|
3698
4129
|
var configManager = new ConfigManager(relayHome, projectRelayDir);
|
|
3699
4130
|
var authManager = new AuthManager(registry);
|
|
3700
4131
|
var eventBus = new EventBus();
|
package/package.json
CHANGED