@rk0429/agentic-relay 0.4.0 → 0.6.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.
- package/dist/relay.mjs +874 -91
- 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,37 +336,75 @@ 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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
maxDepth: guard.getConfig().maxDepth,
|
|
232
|
-
traceId: envContext.traceId
|
|
374
|
+
let result;
|
|
375
|
+
if (input.resumeSessionId) {
|
|
376
|
+
if (!adapter.continueSession) {
|
|
377
|
+
return {
|
|
378
|
+
sessionId: session.relaySessionId,
|
|
379
|
+
exitCode: 1,
|
|
380
|
+
stdout: "",
|
|
381
|
+
stderr: `Backend "${effectiveBackend}" does not support session continuation (continueSession).`
|
|
382
|
+
};
|
|
233
383
|
}
|
|
234
|
-
|
|
384
|
+
result = await adapter.continueSession(input.resumeSessionId, input.prompt);
|
|
385
|
+
} else {
|
|
386
|
+
result = await adapter.execute({
|
|
387
|
+
prompt: input.prompt,
|
|
388
|
+
agent: input.agent,
|
|
389
|
+
systemPrompt: enhancedSystemPrompt,
|
|
390
|
+
model: input.model,
|
|
391
|
+
maxTurns: input.maxTurns,
|
|
392
|
+
mcpContext: {
|
|
393
|
+
parentSessionId: session.relaySessionId,
|
|
394
|
+
depth: envContext.depth + 1,
|
|
395
|
+
maxDepth: guard.getConfig().maxDepth,
|
|
396
|
+
traceId: envContext.traceId
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
235
400
|
if (contextMonitor2) {
|
|
236
401
|
const estimatedTokens = Math.ceil(
|
|
237
402
|
(result.stdout.length + result.stderr.length) / 4
|
|
238
403
|
);
|
|
239
|
-
const maxTokens = input.backend === "gemini" ? 128e3 : 2e5;
|
|
240
404
|
contextMonitor2.updateUsage(
|
|
241
405
|
session.relaySessionId,
|
|
242
|
-
|
|
243
|
-
estimatedTokens
|
|
244
|
-
maxTokens
|
|
406
|
+
effectiveBackend,
|
|
407
|
+
estimatedTokens
|
|
245
408
|
);
|
|
246
409
|
}
|
|
247
410
|
guard.recordSpawn(context);
|
|
@@ -249,15 +412,27 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
249
412
|
await sessionManager2.update(session.relaySessionId, { status });
|
|
250
413
|
if (hooksEngine2) {
|
|
251
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
|
+
}
|
|
252
430
|
const hookInput = {
|
|
253
431
|
event: "post-spawn",
|
|
254
432
|
sessionId: session.relaySessionId,
|
|
255
|
-
backendId:
|
|
433
|
+
backendId: effectiveBackend,
|
|
256
434
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
257
|
-
data:
|
|
258
|
-
exitCode: result.exitCode,
|
|
259
|
-
status
|
|
260
|
-
}
|
|
435
|
+
data: postSpawnData
|
|
261
436
|
};
|
|
262
437
|
await hooksEngine2.emit("post-spawn", hookInput);
|
|
263
438
|
} catch (hookError) {
|
|
@@ -270,7 +445,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
270
445
|
sessionId: session.relaySessionId,
|
|
271
446
|
exitCode: result.exitCode,
|
|
272
447
|
stdout: result.stdout,
|
|
273
|
-
stderr: result.stderr
|
|
448
|
+
stderr: result.stderr,
|
|
449
|
+
nativeSessionId: result.nativeSessionId
|
|
274
450
|
};
|
|
275
451
|
} catch (error) {
|
|
276
452
|
await sessionManager2.update(session.relaySessionId, { status: "error" });
|
|
@@ -293,9 +469,19 @@ var init_spawn_agent = __esm({
|
|
|
293
469
|
backend: z2.enum(["claude", "codex", "gemini"]),
|
|
294
470
|
prompt: z2.string(),
|
|
295
471
|
agent: z2.string().optional(),
|
|
472
|
+
systemPrompt: z2.string().optional(),
|
|
296
473
|
resumeSessionId: z2.string().optional(),
|
|
297
474
|
model: z2.string().optional(),
|
|
298
|
-
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.")
|
|
299
485
|
});
|
|
300
486
|
}
|
|
301
487
|
});
|
|
@@ -339,8 +525,14 @@ async function executeGetContextStatus(input, sessionManager2, contextMonitor2)
|
|
|
339
525
|
if (usage) {
|
|
340
526
|
return {
|
|
341
527
|
sessionId: input.sessionId,
|
|
528
|
+
backendId: usage.backendId,
|
|
342
529
|
usagePercent: usage.usagePercent,
|
|
343
|
-
isEstimated: usage.isEstimated
|
|
530
|
+
isEstimated: usage.isEstimated,
|
|
531
|
+
contextWindow: usage.contextWindow,
|
|
532
|
+
compactThreshold: usage.compactThreshold,
|
|
533
|
+
estimatedTokens: usage.estimatedTokens,
|
|
534
|
+
remainingBeforeCompact: usage.remainingBeforeCompact,
|
|
535
|
+
notifyThreshold: usage.notifyThreshold
|
|
344
536
|
};
|
|
345
537
|
}
|
|
346
538
|
}
|
|
@@ -360,6 +552,83 @@ var init_get_context_status = __esm({
|
|
|
360
552
|
}
|
|
361
553
|
});
|
|
362
554
|
|
|
555
|
+
// src/mcp-server/tools/list-available-backends.ts
|
|
556
|
+
async function executeListAvailableBackends(registry2) {
|
|
557
|
+
const backends = [];
|
|
558
|
+
for (const adapter of registry2.list()) {
|
|
559
|
+
const health = await adapter.checkHealth();
|
|
560
|
+
backends.push({
|
|
561
|
+
id: adapter.id,
|
|
562
|
+
...health
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return backends;
|
|
566
|
+
}
|
|
567
|
+
var init_list_available_backends = __esm({
|
|
568
|
+
"src/mcp-server/tools/list-available-backends.ts"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
}
|
|
571
|
+
});
|
|
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
|
+
|
|
363
632
|
// src/mcp-server/server.ts
|
|
364
633
|
var server_exports = {};
|
|
365
634
|
__export(server_exports, {
|
|
@@ -379,6 +648,8 @@ var init_server = __esm({
|
|
|
379
648
|
init_spawn_agent();
|
|
380
649
|
init_list_sessions();
|
|
381
650
|
init_get_context_status();
|
|
651
|
+
init_list_available_backends();
|
|
652
|
+
init_backend_selector();
|
|
382
653
|
init_logger();
|
|
383
654
|
RelayMCPServer = class {
|
|
384
655
|
constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
|
|
@@ -387,6 +658,7 @@ var init_server = __esm({
|
|
|
387
658
|
this.hooksEngine = hooksEngine2;
|
|
388
659
|
this.contextMonitor = contextMonitor2;
|
|
389
660
|
this.guard = new RecursionGuard(guardConfig);
|
|
661
|
+
this.backendSelector = new BackendSelector();
|
|
390
662
|
this.server = new McpServer({
|
|
391
663
|
name: "agentic-relay",
|
|
392
664
|
version: "0.4.0"
|
|
@@ -395,17 +667,30 @@ var init_server = __esm({
|
|
|
395
667
|
}
|
|
396
668
|
server;
|
|
397
669
|
guard;
|
|
670
|
+
backendSelector;
|
|
398
671
|
registerTools() {
|
|
399
672
|
this.server.tool(
|
|
400
673
|
"spawn_agent",
|
|
401
|
-
"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.",
|
|
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.",
|
|
402
675
|
{
|
|
403
676
|
backend: z5.enum(["claude", "codex", "gemini"]),
|
|
404
677
|
prompt: z5.string(),
|
|
405
|
-
agent: z5.string().optional(),
|
|
678
|
+
agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
|
|
679
|
+
systemPrompt: z5.string().optional().describe(
|
|
680
|
+
"System prompt / role instructions for the sub-agent (all backends)"
|
|
681
|
+
),
|
|
406
682
|
resumeSessionId: z5.string().optional(),
|
|
407
683
|
model: z5.string().optional(),
|
|
408
|
-
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.")
|
|
409
694
|
},
|
|
410
695
|
async (params) => {
|
|
411
696
|
try {
|
|
@@ -415,7 +700,8 @@ var init_server = __esm({
|
|
|
415
700
|
this.sessionManager,
|
|
416
701
|
this.guard,
|
|
417
702
|
this.hooksEngine,
|
|
418
|
-
this.contextMonitor
|
|
703
|
+
this.contextMonitor,
|
|
704
|
+
this.backendSelector
|
|
419
705
|
);
|
|
420
706
|
const isError = result.exitCode !== 0;
|
|
421
707
|
const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
|
|
@@ -494,6 +780,30 @@ ${result.stdout}`;
|
|
|
494
780
|
}
|
|
495
781
|
}
|
|
496
782
|
);
|
|
783
|
+
this.server.tool(
|
|
784
|
+
"list_available_backends",
|
|
785
|
+
"List all registered backends with their health status. Use this before spawn_agent to check which backends are available.",
|
|
786
|
+
{},
|
|
787
|
+
async () => {
|
|
788
|
+
try {
|
|
789
|
+
const result = await executeListAvailableBackends(this.registry);
|
|
790
|
+
return {
|
|
791
|
+
content: [
|
|
792
|
+
{
|
|
793
|
+
type: "text",
|
|
794
|
+
text: JSON.stringify(result, null, 2)
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
};
|
|
798
|
+
} catch (error) {
|
|
799
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
800
|
+
return {
|
|
801
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
802
|
+
isError: true
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
);
|
|
497
807
|
}
|
|
498
808
|
async start(options) {
|
|
499
809
|
const transportType = options?.transport ?? "stdio";
|
|
@@ -542,7 +852,7 @@ ${result.stdout}`;
|
|
|
542
852
|
|
|
543
853
|
// src/bin/relay.ts
|
|
544
854
|
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
545
|
-
import { join as
|
|
855
|
+
import { join as join9 } from "path";
|
|
546
856
|
import { homedir as homedir6 } from "os";
|
|
547
857
|
|
|
548
858
|
// src/infrastructure/process-manager.ts
|
|
@@ -671,6 +981,10 @@ var AdapterRegistry = class {
|
|
|
671
981
|
this.factories.clear();
|
|
672
982
|
return [...this.adapters.values()];
|
|
673
983
|
}
|
|
984
|
+
/** Return registered backend IDs without instantiating lazy adapters */
|
|
985
|
+
listIds() {
|
|
986
|
+
return [.../* @__PURE__ */ new Set([...this.adapters.keys(), ...this.factories.keys()])];
|
|
987
|
+
}
|
|
674
988
|
};
|
|
675
989
|
|
|
676
990
|
// src/adapters/base-adapter.ts
|
|
@@ -698,6 +1012,42 @@ var BaseAdapter = class {
|
|
|
698
1012
|
}
|
|
699
1013
|
return result.stdout.trim();
|
|
700
1014
|
}
|
|
1015
|
+
async continueSession(_nativeSessionId, _prompt) {
|
|
1016
|
+
return {
|
|
1017
|
+
exitCode: 1,
|
|
1018
|
+
stdout: "",
|
|
1019
|
+
stderr: `continueSession not supported for ${this.id}`
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async checkHealth() {
|
|
1023
|
+
const HEALTH_TIMEOUT = 5e3;
|
|
1024
|
+
const installed = await Promise.race([
|
|
1025
|
+
this.isInstalled(),
|
|
1026
|
+
new Promise(
|
|
1027
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1028
|
+
)
|
|
1029
|
+
]).catch(() => false);
|
|
1030
|
+
if (!installed) {
|
|
1031
|
+
return {
|
|
1032
|
+
installed: false,
|
|
1033
|
+
authenticated: false,
|
|
1034
|
+
healthy: false,
|
|
1035
|
+
message: `${this.id} is not installed`
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const version = await Promise.race([
|
|
1039
|
+
this.getVersion(),
|
|
1040
|
+
new Promise(
|
|
1041
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1042
|
+
)
|
|
1043
|
+
]).catch(() => void 0);
|
|
1044
|
+
return {
|
|
1045
|
+
installed: true,
|
|
1046
|
+
authenticated: true,
|
|
1047
|
+
healthy: true,
|
|
1048
|
+
version
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
701
1051
|
async getMCPConfig() {
|
|
702
1052
|
logger.warn(`getMCPConfig not implemented for ${this.id}`);
|
|
703
1053
|
return [];
|
|
@@ -799,6 +1149,48 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
799
1149
|
getConfigPath() {
|
|
800
1150
|
return join(homedir(), ".claude.json");
|
|
801
1151
|
}
|
|
1152
|
+
async checkHealth() {
|
|
1153
|
+
const HEALTH_TIMEOUT = 5e3;
|
|
1154
|
+
const installed = await Promise.race([
|
|
1155
|
+
this.isInstalled(),
|
|
1156
|
+
new Promise(
|
|
1157
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1158
|
+
)
|
|
1159
|
+
]).catch(() => false);
|
|
1160
|
+
if (!installed) {
|
|
1161
|
+
return {
|
|
1162
|
+
installed: false,
|
|
1163
|
+
authenticated: false,
|
|
1164
|
+
healthy: false,
|
|
1165
|
+
message: "claude is not installed"
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
const version = await Promise.race([
|
|
1169
|
+
this.getVersion(),
|
|
1170
|
+
new Promise(
|
|
1171
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1172
|
+
)
|
|
1173
|
+
]).catch(() => void 0);
|
|
1174
|
+
let authenticated = true;
|
|
1175
|
+
try {
|
|
1176
|
+
const result = await Promise.race([
|
|
1177
|
+
this.processManager.execute(this.command, ["auth", "status"]),
|
|
1178
|
+
new Promise(
|
|
1179
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1180
|
+
)
|
|
1181
|
+
]);
|
|
1182
|
+
authenticated = result.exitCode === 0;
|
|
1183
|
+
} catch {
|
|
1184
|
+
authenticated = true;
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
installed: true,
|
|
1188
|
+
authenticated,
|
|
1189
|
+
healthy: authenticated,
|
|
1190
|
+
version,
|
|
1191
|
+
...!authenticated ? { message: "claude authentication not configured" } : {}
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
802
1194
|
mapFlags(flags) {
|
|
803
1195
|
return {
|
|
804
1196
|
args: mapCommonToNative("claude", flags)
|
|
@@ -825,7 +1217,8 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
825
1217
|
env,
|
|
826
1218
|
cwd: process.cwd(),
|
|
827
1219
|
...flags.model ? { model: flags.model } : {},
|
|
828
|
-
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
|
|
1220
|
+
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
|
|
1221
|
+
...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
|
|
829
1222
|
};
|
|
830
1223
|
if (permissionMode === "bypassPermissions") {
|
|
831
1224
|
options.permissionMode = "bypassPermissions";
|
|
@@ -877,7 +1270,8 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
877
1270
|
env,
|
|
878
1271
|
cwd: process.cwd(),
|
|
879
1272
|
...flags.model ? { model: flags.model } : {},
|
|
880
|
-
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
|
|
1273
|
+
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
|
|
1274
|
+
...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
|
|
881
1275
|
};
|
|
882
1276
|
if (permissionMode === "bypassPermissions") {
|
|
883
1277
|
options.permissionMode = "bypassPermissions";
|
|
@@ -942,6 +1336,44 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
942
1336
|
};
|
|
943
1337
|
}
|
|
944
1338
|
}
|
|
1339
|
+
async continueSession(nativeSessionId, prompt) {
|
|
1340
|
+
try {
|
|
1341
|
+
const { query } = await loadClaudeSDK();
|
|
1342
|
+
const permissionMode = this.getPermissionMode();
|
|
1343
|
+
const options = {
|
|
1344
|
+
resume: nativeSessionId,
|
|
1345
|
+
maxTurns: 1,
|
|
1346
|
+
cwd: process.cwd()
|
|
1347
|
+
};
|
|
1348
|
+
if (permissionMode === "bypassPermissions") {
|
|
1349
|
+
options.permissionMode = "bypassPermissions";
|
|
1350
|
+
options.allowDangerouslySkipPermissions = true;
|
|
1351
|
+
}
|
|
1352
|
+
const q = query({
|
|
1353
|
+
prompt,
|
|
1354
|
+
options
|
|
1355
|
+
});
|
|
1356
|
+
let resultText = "";
|
|
1357
|
+
for await (const message of q) {
|
|
1358
|
+
if (message.type === "result") {
|
|
1359
|
+
if (message.subtype === "success") {
|
|
1360
|
+
resultText = message.result;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
exitCode: 0,
|
|
1366
|
+
stdout: resultText,
|
|
1367
|
+
stderr: ""
|
|
1368
|
+
};
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
return {
|
|
1371
|
+
exitCode: 1,
|
|
1372
|
+
stdout: "",
|
|
1373
|
+
stderr: error instanceof Error ? error.message : String(error)
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
945
1377
|
async resumeSession(sessionId, flags) {
|
|
946
1378
|
await this.processManager.spawnInteractive(
|
|
947
1379
|
this.command,
|
|
@@ -1157,6 +1589,48 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1157
1589
|
getConfigPath() {
|
|
1158
1590
|
return join2(homedir2(), ".codex", "config.toml");
|
|
1159
1591
|
}
|
|
1592
|
+
async checkHealth() {
|
|
1593
|
+
const HEALTH_TIMEOUT = 5e3;
|
|
1594
|
+
const installed = await Promise.race([
|
|
1595
|
+
this.isInstalled(),
|
|
1596
|
+
new Promise(
|
|
1597
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1598
|
+
)
|
|
1599
|
+
]).catch(() => false);
|
|
1600
|
+
if (!installed) {
|
|
1601
|
+
return {
|
|
1602
|
+
installed: false,
|
|
1603
|
+
authenticated: false,
|
|
1604
|
+
healthy: false,
|
|
1605
|
+
message: "codex is not installed"
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
const version = await Promise.race([
|
|
1609
|
+
this.getVersion(),
|
|
1610
|
+
new Promise(
|
|
1611
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1612
|
+
)
|
|
1613
|
+
]).catch(() => void 0);
|
|
1614
|
+
let authenticated = true;
|
|
1615
|
+
try {
|
|
1616
|
+
const result = await Promise.race([
|
|
1617
|
+
this.processManager.execute(this.command, ["login", "status"]),
|
|
1618
|
+
new Promise(
|
|
1619
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1620
|
+
)
|
|
1621
|
+
]);
|
|
1622
|
+
authenticated = result.exitCode === 0;
|
|
1623
|
+
} catch {
|
|
1624
|
+
authenticated = true;
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
installed: true,
|
|
1628
|
+
authenticated,
|
|
1629
|
+
healthy: authenticated,
|
|
1630
|
+
version,
|
|
1631
|
+
...!authenticated ? { message: "codex authentication not configured" } : {}
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1160
1634
|
mapFlags(flags) {
|
|
1161
1635
|
const args = mapCommonToNative("codex", flags);
|
|
1162
1636
|
if (flags.outputFormat === "json") {
|
|
@@ -1174,15 +1648,39 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1174
1648
|
}
|
|
1175
1649
|
await this.processManager.spawnInteractive(this.command, args);
|
|
1176
1650
|
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Resolve the effective system prompt from flags.
|
|
1653
|
+
* Priority: systemPrompt > agent fallback > none
|
|
1654
|
+
*/
|
|
1655
|
+
resolveSystemPrompt(flags) {
|
|
1656
|
+
if (flags.systemPrompt) return flags.systemPrompt;
|
|
1657
|
+
if (flags.agent) {
|
|
1658
|
+
return `You are acting as the "${flags.agent}" agent. Follow the instructions and role defined for this agent.`;
|
|
1659
|
+
}
|
|
1660
|
+
return void 0;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Build the effective prompt with system instructions prepended if needed.
|
|
1664
|
+
* Codex SDK does not support a native instructions/systemPrompt parameter,
|
|
1665
|
+
* so we inject role context via a prompt prefix.
|
|
1666
|
+
*/
|
|
1667
|
+
buildEffectivePrompt(prompt, systemPrompt) {
|
|
1668
|
+
if (!systemPrompt) return prompt;
|
|
1669
|
+
return `[System Instructions]
|
|
1670
|
+
${systemPrompt}
|
|
1671
|
+
|
|
1672
|
+
[User Request]
|
|
1673
|
+
${prompt}`;
|
|
1674
|
+
}
|
|
1177
1675
|
async execute(flags) {
|
|
1178
1676
|
if (!flags.prompt) {
|
|
1179
1677
|
throw new Error("execute requires a prompt (-p flag)");
|
|
1180
1678
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1679
|
+
const systemPrompt = this.resolveSystemPrompt(flags);
|
|
1680
|
+
const effectivePrompt = this.buildEffectivePrompt(
|
|
1681
|
+
flags.prompt,
|
|
1682
|
+
systemPrompt
|
|
1683
|
+
);
|
|
1186
1684
|
try {
|
|
1187
1685
|
const { Codex } = await loadCodexSDK();
|
|
1188
1686
|
const codexOptions = {};
|
|
@@ -1200,11 +1698,12 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1200
1698
|
workingDirectory: process.cwd(),
|
|
1201
1699
|
approvalPolicy: "never"
|
|
1202
1700
|
});
|
|
1203
|
-
const result = await thread.run(
|
|
1701
|
+
const result = await thread.run(effectivePrompt);
|
|
1204
1702
|
return {
|
|
1205
1703
|
exitCode: 0,
|
|
1206
1704
|
stdout: result.finalResponse,
|
|
1207
|
-
stderr: ""
|
|
1705
|
+
stderr: "",
|
|
1706
|
+
...thread.id ? { nativeSessionId: thread.id } : {}
|
|
1208
1707
|
};
|
|
1209
1708
|
} catch (error) {
|
|
1210
1709
|
return {
|
|
@@ -1218,11 +1717,11 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1218
1717
|
if (!flags.prompt) {
|
|
1219
1718
|
throw new Error("executeStreaming requires a prompt (-p flag)");
|
|
1220
1719
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1720
|
+
const systemPrompt = this.resolveSystemPrompt(flags);
|
|
1721
|
+
const effectivePrompt = this.buildEffectivePrompt(
|
|
1722
|
+
flags.prompt,
|
|
1723
|
+
systemPrompt
|
|
1724
|
+
);
|
|
1226
1725
|
try {
|
|
1227
1726
|
const { Codex } = await loadCodexSDK();
|
|
1228
1727
|
const codexOptions = {};
|
|
@@ -1240,10 +1739,13 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1240
1739
|
workingDirectory: process.cwd(),
|
|
1241
1740
|
approvalPolicy: "never"
|
|
1242
1741
|
});
|
|
1243
|
-
const streamedTurn = await thread.runStreamed(
|
|
1742
|
+
const streamedTurn = await thread.runStreamed(effectivePrompt);
|
|
1244
1743
|
const completedMessages = [];
|
|
1744
|
+
let threadId;
|
|
1245
1745
|
for await (const event of streamedTurn.events) {
|
|
1246
|
-
if (event.type === "
|
|
1746
|
+
if (event.type === "thread.started") {
|
|
1747
|
+
threadId = event.thread_id;
|
|
1748
|
+
} else if (event.type === "item.started") {
|
|
1247
1749
|
const item = event.item;
|
|
1248
1750
|
if (item?.type === "agent_message" && item.text) {
|
|
1249
1751
|
yield { type: "text", text: item.text };
|
|
@@ -1291,13 +1793,15 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1291
1793
|
const finalResponse = completedMessages.join("\n");
|
|
1292
1794
|
yield {
|
|
1293
1795
|
type: "done",
|
|
1294
|
-
result: { exitCode: 0, stdout: finalResponse, stderr: "" }
|
|
1796
|
+
result: { exitCode: 0, stdout: finalResponse, stderr: "" },
|
|
1797
|
+
nativeSessionId: threadId ?? thread.id ?? void 0
|
|
1295
1798
|
};
|
|
1296
1799
|
} else if (event.type === "turn.failed") {
|
|
1297
1800
|
const errorMessage = event.error?.message ?? "Turn failed";
|
|
1298
1801
|
yield {
|
|
1299
1802
|
type: "done",
|
|
1300
|
-
result: { exitCode: 1, stdout: "", stderr: errorMessage }
|
|
1803
|
+
result: { exitCode: 1, stdout: "", stderr: errorMessage },
|
|
1804
|
+
nativeSessionId: threadId ?? thread.id ?? void 0
|
|
1301
1805
|
};
|
|
1302
1806
|
} else if (event.type === "error") {
|
|
1303
1807
|
yield {
|
|
@@ -1315,6 +1819,28 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1315
1819
|
};
|
|
1316
1820
|
}
|
|
1317
1821
|
}
|
|
1822
|
+
async continueSession(nativeSessionId, prompt) {
|
|
1823
|
+
try {
|
|
1824
|
+
const { Codex } = await loadCodexSDK();
|
|
1825
|
+
const codex = new Codex();
|
|
1826
|
+
const thread = codex.resumeThread(nativeSessionId, {
|
|
1827
|
+
workingDirectory: process.cwd(),
|
|
1828
|
+
approvalPolicy: "never"
|
|
1829
|
+
});
|
|
1830
|
+
const result = await thread.run(prompt);
|
|
1831
|
+
return {
|
|
1832
|
+
exitCode: 0,
|
|
1833
|
+
stdout: result.finalResponse,
|
|
1834
|
+
stderr: ""
|
|
1835
|
+
};
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
return {
|
|
1838
|
+
exitCode: 1,
|
|
1839
|
+
stdout: "",
|
|
1840
|
+
stderr: error instanceof Error ? error.message : String(error)
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1318
1844
|
async resumeSession(sessionId, flags) {
|
|
1319
1845
|
const args = [];
|
|
1320
1846
|
if (flags.model) {
|
|
@@ -1408,6 +1934,38 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
1408
1934
|
getConfigPath() {
|
|
1409
1935
|
return join3(homedir3(), ".gemini", "settings.json");
|
|
1410
1936
|
}
|
|
1937
|
+
async checkHealth() {
|
|
1938
|
+
const HEALTH_TIMEOUT = 5e3;
|
|
1939
|
+
const installed = await Promise.race([
|
|
1940
|
+
this.isInstalled(),
|
|
1941
|
+
new Promise(
|
|
1942
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1943
|
+
)
|
|
1944
|
+
]).catch(() => false);
|
|
1945
|
+
if (!installed) {
|
|
1946
|
+
return {
|
|
1947
|
+
installed: false,
|
|
1948
|
+
authenticated: false,
|
|
1949
|
+
healthy: false,
|
|
1950
|
+
message: "gemini is not installed"
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
const version = await Promise.race([
|
|
1954
|
+
this.getVersion(),
|
|
1955
|
+
new Promise(
|
|
1956
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1957
|
+
)
|
|
1958
|
+
]).catch(() => void 0);
|
|
1959
|
+
const hasApiKey = !!process.env["GEMINI_API_KEY"];
|
|
1960
|
+
const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
|
|
1961
|
+
const authenticated = hasApiKey || hasGoogleAdc || true;
|
|
1962
|
+
return {
|
|
1963
|
+
installed: true,
|
|
1964
|
+
authenticated,
|
|
1965
|
+
healthy: true,
|
|
1966
|
+
version
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1411
1969
|
mapFlags(flags) {
|
|
1412
1970
|
const args = mapCommonToNative("gemini", flags);
|
|
1413
1971
|
if (flags.outputFormat) {
|
|
@@ -1428,15 +1986,34 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
1428
1986
|
}
|
|
1429
1987
|
await this.processManager.spawnInteractive(this.command, args);
|
|
1430
1988
|
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Resolve the effective prompt with system instructions prepended if needed.
|
|
1991
|
+
* Gemini CLI has no native system prompt flag, so we use a prompt prefix.
|
|
1992
|
+
* Priority: systemPrompt > agent fallback > none
|
|
1993
|
+
*/
|
|
1994
|
+
buildEffectivePrompt(flags) {
|
|
1995
|
+
const prompt = flags.prompt;
|
|
1996
|
+
if (flags.systemPrompt) {
|
|
1997
|
+
return `[System Instructions]
|
|
1998
|
+
${flags.systemPrompt}
|
|
1999
|
+
|
|
2000
|
+
[User Request]
|
|
2001
|
+
${prompt}`;
|
|
2002
|
+
}
|
|
2003
|
+
if (flags.agent) {
|
|
2004
|
+
return `[System Instructions]
|
|
2005
|
+
You are acting as the "${flags.agent}" agent.
|
|
2006
|
+
|
|
2007
|
+
[User Request]
|
|
2008
|
+
${prompt}`;
|
|
2009
|
+
}
|
|
2010
|
+
return prompt;
|
|
2011
|
+
}
|
|
1431
2012
|
async execute(flags) {
|
|
1432
2013
|
if (!flags.prompt) {
|
|
1433
2014
|
throw new Error("execute requires a prompt (-p flag)");
|
|
1434
2015
|
}
|
|
1435
|
-
|
|
1436
|
-
logger.warn(
|
|
1437
|
-
`Gemini CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
|
|
1438
|
-
);
|
|
1439
|
-
}
|
|
2016
|
+
const effectivePrompt = this.buildEffectivePrompt(flags);
|
|
1440
2017
|
const args = [];
|
|
1441
2018
|
if (flags.model) {
|
|
1442
2019
|
args.push("--model", flags.model);
|
|
@@ -1447,7 +2024,7 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
1447
2024
|
if (flags.verbose) {
|
|
1448
2025
|
args.push("--verbose");
|
|
1449
2026
|
}
|
|
1450
|
-
args.push("-p",
|
|
2027
|
+
args.push("-p", effectivePrompt);
|
|
1451
2028
|
return this.processManager.execute(this.command, args);
|
|
1452
2029
|
}
|
|
1453
2030
|
async resumeSession(sessionId, flags) {
|
|
@@ -1703,6 +2280,7 @@ var mcpServerConfigSchema = z.object({
|
|
|
1703
2280
|
env: z.record(z.string()).optional()
|
|
1704
2281
|
});
|
|
1705
2282
|
var hookEventSchema = z.enum([
|
|
2283
|
+
"session-init",
|
|
1706
2284
|
"pre-prompt",
|
|
1707
2285
|
"post-response",
|
|
1708
2286
|
"on-error",
|
|
@@ -1718,9 +2296,24 @@ var hookDefinitionSchema = z.object({
|
|
|
1718
2296
|
enabled: z.boolean().optional(),
|
|
1719
2297
|
onError: z.enum(["ignore", "warn", "abort"]).optional()
|
|
1720
2298
|
});
|
|
2299
|
+
var hookChainStepSchema = z.object({
|
|
2300
|
+
stepId: z.string(),
|
|
2301
|
+
hook: hookDefinitionSchema,
|
|
2302
|
+
pipeOutput: z.boolean().optional()
|
|
2303
|
+
});
|
|
2304
|
+
var hookChainSchema = z.object({
|
|
2305
|
+
name: z.string(),
|
|
2306
|
+
steps: z.array(hookChainStepSchema).min(1),
|
|
2307
|
+
onError: z.enum(["ignore", "warn", "fail"]).optional()
|
|
2308
|
+
});
|
|
1721
2309
|
var hooksConfigSchema = z.object({
|
|
1722
|
-
definitions: z.array(hookDefinitionSchema)
|
|
2310
|
+
definitions: z.array(hookDefinitionSchema),
|
|
2311
|
+
chains: z.array(hookChainSchema).optional()
|
|
1723
2312
|
});
|
|
2313
|
+
var backendContextConfigSchema = z.object({
|
|
2314
|
+
contextWindow: z.number().positive().optional(),
|
|
2315
|
+
compactThreshold: z.number().positive().optional()
|
|
2316
|
+
}).optional();
|
|
1724
2317
|
var relayConfigSchema = z.object({
|
|
1725
2318
|
defaultBackend: backendIdSchema.optional(),
|
|
1726
2319
|
mcpServers: z.record(mcpServerConfigSchema).optional(),
|
|
@@ -1731,9 +2324,16 @@ var relayConfigSchema = z.object({
|
|
|
1731
2324
|
}).optional(),
|
|
1732
2325
|
hooks: hooksConfigSchema.optional(),
|
|
1733
2326
|
contextMonitor: z.object({
|
|
1734
|
-
enabled: z.boolean(),
|
|
1735
|
-
thresholdPercent: z.number().min(0).max(100),
|
|
1736
|
-
|
|
2327
|
+
enabled: z.boolean().optional(),
|
|
2328
|
+
thresholdPercent: z.number().min(0).max(100).optional(),
|
|
2329
|
+
notifyThreshold: z.number().positive().optional(),
|
|
2330
|
+
notifyPercent: z.number().min(0).max(100).optional(),
|
|
2331
|
+
notifyMethod: z.enum(["stderr", "hook"]).optional(),
|
|
2332
|
+
backends: z.object({
|
|
2333
|
+
claude: backendContextConfigSchema,
|
|
2334
|
+
codex: backendContextConfigSchema,
|
|
2335
|
+
gemini: backendContextConfigSchema
|
|
2336
|
+
}).optional()
|
|
1737
2337
|
}).optional(),
|
|
1738
2338
|
mcpServerMode: z.object({
|
|
1739
2339
|
maxDepth: z.number().int().positive(),
|
|
@@ -2006,6 +2606,7 @@ var HooksEngine = class _HooksEngine {
|
|
|
2006
2606
|
static COMMAND_PATTERN = /^[a-zA-Z0-9_./-]+$/;
|
|
2007
2607
|
static ARG_PATTERN = /^[a-zA-Z0-9_.=:/-]+$/;
|
|
2008
2608
|
definitions = [];
|
|
2609
|
+
chains = [];
|
|
2009
2610
|
registered = false;
|
|
2010
2611
|
validateCommand(command) {
|
|
2011
2612
|
if (!command || command.trim().length === 0) {
|
|
@@ -2046,6 +2647,7 @@ var HooksEngine = class _HooksEngine {
|
|
|
2046
2647
|
return false;
|
|
2047
2648
|
}
|
|
2048
2649
|
});
|
|
2650
|
+
this.chains = config.chains ?? [];
|
|
2049
2651
|
if (!this.registered) {
|
|
2050
2652
|
for (const event of this.getUniqueEvents()) {
|
|
2051
2653
|
this.eventBus.on(event, async () => {
|
|
@@ -2087,10 +2689,132 @@ var HooksEngine = class _HooksEngine {
|
|
|
2087
2689
|
await this.eventBus.emit(event, input);
|
|
2088
2690
|
return results;
|
|
2089
2691
|
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Emit a hook event, execute all matching hooks, and return merged metadata.
|
|
2694
|
+
* Merge rule: later hooks overwrite earlier hooks' metadata fields.
|
|
2695
|
+
* If any hook returns allow: false, subsequent hooks are skipped.
|
|
2696
|
+
*/
|
|
2697
|
+
async emitAndCollectMetadata(event, hookInput) {
|
|
2698
|
+
const matchingDefs = this.definitions.filter((def) => def.event === event);
|
|
2699
|
+
const results = [];
|
|
2700
|
+
let mergedMetadata = {};
|
|
2701
|
+
for (const def of matchingDefs) {
|
|
2702
|
+
try {
|
|
2703
|
+
const result = await this.executeHook(def, hookInput);
|
|
2704
|
+
results.push(result);
|
|
2705
|
+
if (result.output.metadata && typeof result.output.metadata === "object") {
|
|
2706
|
+
mergedMetadata = { ...mergedMetadata, ...result.output.metadata };
|
|
2707
|
+
}
|
|
2708
|
+
if (!result.output.allow) {
|
|
2709
|
+
logger.info(
|
|
2710
|
+
`Hook "${def.command}" returned allow: false, skipping subsequent hooks for event "${event}"`
|
|
2711
|
+
);
|
|
2712
|
+
break;
|
|
2713
|
+
}
|
|
2714
|
+
} catch (error) {
|
|
2715
|
+
const strategy = def.onError ?? "warn";
|
|
2716
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2717
|
+
if (strategy === "abort") {
|
|
2718
|
+
throw new Error(
|
|
2719
|
+
`Hook "${def.command}" failed (abort): ${message}`
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
if (strategy === "warn") {
|
|
2723
|
+
logger.warn(
|
|
2724
|
+
`Hook "${def.command}" failed (warn): ${message}`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
results.push({
|
|
2728
|
+
exitCode: 1,
|
|
2729
|
+
stdout: "",
|
|
2730
|
+
stderr: message,
|
|
2731
|
+
durationMs: 0,
|
|
2732
|
+
output: { ...DEFAULT_HOOK_OUTPUT }
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
await this.eventBus.emit(event, hookInput);
|
|
2737
|
+
return { results, mergedMetadata };
|
|
2738
|
+
}
|
|
2090
2739
|
/** Get the count of enabled definitions for a given event */
|
|
2091
2740
|
getDefinitionCount(event) {
|
|
2092
2741
|
return this.definitions.filter((def) => def.event === event).length;
|
|
2093
2742
|
}
|
|
2743
|
+
/** Get all loaded chain names */
|
|
2744
|
+
getChainNames() {
|
|
2745
|
+
return this.chains.map((c) => c.name);
|
|
2746
|
+
}
|
|
2747
|
+
/**
|
|
2748
|
+
* Execute a named hook chain.
|
|
2749
|
+
* Steps execute in sequence. If pipeOutput is true, previous step's
|
|
2750
|
+
* HookOutput is merged into the next step's HookInput.data.
|
|
2751
|
+
*/
|
|
2752
|
+
async executeChain(chainName, initialInput) {
|
|
2753
|
+
const chain = this.chains.find((c) => c.name === chainName);
|
|
2754
|
+
if (!chain) {
|
|
2755
|
+
throw new Error(`Hook chain "${chainName}" not found`);
|
|
2756
|
+
}
|
|
2757
|
+
const errorStrategy = chain.onError ?? "warn";
|
|
2758
|
+
const stepResults = [];
|
|
2759
|
+
let mergedMetadata = {};
|
|
2760
|
+
let previousOutput;
|
|
2761
|
+
let overallSuccess = true;
|
|
2762
|
+
for (const step of chain.steps) {
|
|
2763
|
+
const startTime = Date.now();
|
|
2764
|
+
let stepInput;
|
|
2765
|
+
if (step.pipeOutput && previousOutput) {
|
|
2766
|
+
stepInput = {
|
|
2767
|
+
...initialInput,
|
|
2768
|
+
data: {
|
|
2769
|
+
...initialInput.data,
|
|
2770
|
+
...previousOutput.metadata,
|
|
2771
|
+
_previousMessage: previousOutput.message,
|
|
2772
|
+
_previousAllow: previousOutput.allow
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
} else {
|
|
2776
|
+
stepInput = { ...initialInput };
|
|
2777
|
+
}
|
|
2778
|
+
try {
|
|
2779
|
+
const result = await this.executeHook(step.hook, stepInput);
|
|
2780
|
+
const durationMs = Date.now() - startTime;
|
|
2781
|
+
stepResults.push({
|
|
2782
|
+
stepId: step.stepId,
|
|
2783
|
+
success: true,
|
|
2784
|
+
output: result.output,
|
|
2785
|
+
durationMs
|
|
2786
|
+
});
|
|
2787
|
+
if (result.output.metadata && typeof result.output.metadata === "object") {
|
|
2788
|
+
mergedMetadata = { ...mergedMetadata, ...result.output.metadata };
|
|
2789
|
+
}
|
|
2790
|
+
previousOutput = result.output;
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
const durationMs = Date.now() - startTime;
|
|
2793
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2794
|
+
stepResults.push({
|
|
2795
|
+
stepId: step.stepId,
|
|
2796
|
+
success: false,
|
|
2797
|
+
error: message,
|
|
2798
|
+
durationMs
|
|
2799
|
+
});
|
|
2800
|
+
overallSuccess = false;
|
|
2801
|
+
if (errorStrategy === "fail") {
|
|
2802
|
+
break;
|
|
2803
|
+
}
|
|
2804
|
+
if (errorStrategy === "warn") {
|
|
2805
|
+
logger.warn(
|
|
2806
|
+
`Hook chain "${chainName}" step "${step.stepId}" failed (warn): ${message}`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
return {
|
|
2812
|
+
chainName,
|
|
2813
|
+
stepResults,
|
|
2814
|
+
success: overallSuccess,
|
|
2815
|
+
mergedMetadata
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2094
2818
|
async executeHook(def, input) {
|
|
2095
2819
|
this.validateCommand(def.command);
|
|
2096
2820
|
this.validateArgs(def.args ?? []);
|
|
@@ -2129,34 +2853,75 @@ var HooksEngine = class _HooksEngine {
|
|
|
2129
2853
|
};
|
|
2130
2854
|
|
|
2131
2855
|
// src/core/context-monitor.ts
|
|
2856
|
+
var DEFAULT_BACKEND_CONTEXT = {
|
|
2857
|
+
claude: { contextWindow: 2e5, compactThreshold: 19e4 },
|
|
2858
|
+
codex: { contextWindow: 272e3, compactThreshold: 258400 },
|
|
2859
|
+
gemini: { contextWindow: 1048576, compactThreshold: 524288 }
|
|
2860
|
+
};
|
|
2861
|
+
var DEFAULT_NOTIFY_PERCENT = 70;
|
|
2132
2862
|
var DEFAULT_CONFIG = {
|
|
2133
2863
|
enabled: true,
|
|
2134
|
-
|
|
2135
|
-
notifyMethod: "stderr"
|
|
2864
|
+
notifyMethod: "hook"
|
|
2136
2865
|
};
|
|
2137
2866
|
var ContextMonitor = class {
|
|
2138
2867
|
constructor(hooksEngine2, config) {
|
|
2139
2868
|
this.hooksEngine = hooksEngine2;
|
|
2140
2869
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
2870
|
+
if (this.config.thresholdPercent !== void 0 && this.config.notifyPercent === void 0 && this.config.notifyThreshold === void 0) {
|
|
2871
|
+
this.config.notifyPercent = this.config.thresholdPercent;
|
|
2872
|
+
}
|
|
2141
2873
|
}
|
|
2142
2874
|
config;
|
|
2143
2875
|
usageMap = /* @__PURE__ */ new Map();
|
|
2876
|
+
/** Get backend context config, merging user overrides with defaults */
|
|
2877
|
+
getBackendConfig(backendId) {
|
|
2878
|
+
const defaults = DEFAULT_BACKEND_CONTEXT[backendId];
|
|
2879
|
+
const overrides = this.config.backends?.[backendId];
|
|
2880
|
+
return {
|
|
2881
|
+
contextWindow: overrides?.contextWindow ?? defaults.contextWindow,
|
|
2882
|
+
compactThreshold: overrides?.compactThreshold ?? defaults.compactThreshold
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
/** Calculate the notification threshold in tokens for a given backend */
|
|
2886
|
+
getNotifyThreshold(backendId) {
|
|
2887
|
+
if (this.config.notifyThreshold !== void 0) {
|
|
2888
|
+
return this.config.notifyThreshold;
|
|
2889
|
+
}
|
|
2890
|
+
const backendConfig = this.getBackendConfig(backendId);
|
|
2891
|
+
const notifyPercent = this.config.notifyPercent ?? DEFAULT_NOTIFY_PERCENT;
|
|
2892
|
+
return Math.round(backendConfig.contextWindow * notifyPercent / 100);
|
|
2893
|
+
}
|
|
2144
2894
|
/** Update token usage for a session and check threshold */
|
|
2145
|
-
updateUsage(sessionId, backendId, estimatedTokens
|
|
2895
|
+
updateUsage(sessionId, backendId, estimatedTokens) {
|
|
2146
2896
|
if (!this.config.enabled) return;
|
|
2147
|
-
const
|
|
2897
|
+
const backendConfig = this.getBackendConfig(backendId);
|
|
2898
|
+
const contextWindow = backendConfig.contextWindow;
|
|
2899
|
+
const usagePercent = contextWindow > 0 ? Math.round(estimatedTokens / contextWindow * 100) : 0;
|
|
2148
2900
|
const existing = this.usageMap.get(sessionId);
|
|
2149
|
-
|
|
2901
|
+
let wasNotified = existing?.notified ?? false;
|
|
2902
|
+
if (existing && estimatedTokens < existing.estimatedTokens * 0.7) {
|
|
2903
|
+
wasNotified = false;
|
|
2904
|
+
}
|
|
2150
2905
|
this.usageMap.set(sessionId, {
|
|
2151
2906
|
estimatedTokens,
|
|
2152
|
-
|
|
2907
|
+
contextWindow,
|
|
2908
|
+
compactThreshold: backendConfig.compactThreshold,
|
|
2153
2909
|
usagePercent,
|
|
2154
2910
|
backendId,
|
|
2155
2911
|
notified: wasNotified
|
|
2156
2912
|
});
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
this.
|
|
2913
|
+
const notifyAt = this.getNotifyThreshold(backendId);
|
|
2914
|
+
if (estimatedTokens >= notifyAt && !wasNotified) {
|
|
2915
|
+
const entry = this.usageMap.get(sessionId);
|
|
2916
|
+
entry.notified = true;
|
|
2917
|
+
this.notify(
|
|
2918
|
+
sessionId,
|
|
2919
|
+
backendId,
|
|
2920
|
+
usagePercent,
|
|
2921
|
+
estimatedTokens,
|
|
2922
|
+
contextWindow,
|
|
2923
|
+
backendConfig.compactThreshold
|
|
2924
|
+
);
|
|
2160
2925
|
}
|
|
2161
2926
|
}
|
|
2162
2927
|
/** Get usage info for a session */
|
|
@@ -2165,17 +2930,31 @@ var ContextMonitor = class {
|
|
|
2165
2930
|
if (!entry) return null;
|
|
2166
2931
|
return {
|
|
2167
2932
|
usagePercent: entry.usagePercent,
|
|
2168
|
-
isEstimated: true
|
|
2933
|
+
isEstimated: true,
|
|
2934
|
+
backendId: entry.backendId,
|
|
2935
|
+
contextWindow: entry.contextWindow,
|
|
2936
|
+
compactThreshold: entry.compactThreshold,
|
|
2937
|
+
estimatedTokens: entry.estimatedTokens,
|
|
2938
|
+
remainingBeforeCompact: Math.max(
|
|
2939
|
+
0,
|
|
2940
|
+
entry.compactThreshold - entry.estimatedTokens
|
|
2941
|
+
),
|
|
2942
|
+
notifyThreshold: this.getNotifyThreshold(entry.backendId)
|
|
2169
2943
|
};
|
|
2170
2944
|
}
|
|
2171
2945
|
/** Remove usage tracking for a session */
|
|
2172
2946
|
removeSession(sessionId) {
|
|
2173
2947
|
this.usageMap.delete(sessionId);
|
|
2174
2948
|
}
|
|
2175
|
-
notify(sessionId, backendId, usagePercent) {
|
|
2949
|
+
notify(sessionId, backendId, usagePercent, currentTokens, contextWindow, compactThreshold) {
|
|
2950
|
+
const remainingBeforeCompact = Math.max(
|
|
2951
|
+
0,
|
|
2952
|
+
compactThreshold - currentTokens
|
|
2953
|
+
);
|
|
2954
|
+
const warningMessage = `${backendId} session ${sessionId} at ${usagePercent}% (${currentTokens}/${contextWindow} tokens). Compact in ~${remainingBeforeCompact} tokens. Save your work state now.`;
|
|
2176
2955
|
if (this.config.notifyMethod === "stderr") {
|
|
2177
2956
|
process.stderr.write(
|
|
2178
|
-
`[relay] Context
|
|
2957
|
+
`[relay] Context warning: ${warningMessage}
|
|
2179
2958
|
`
|
|
2180
2959
|
);
|
|
2181
2960
|
} else if (this.config.notifyMethod === "hook" && this.hooksEngine) {
|
|
@@ -2186,7 +2965,10 @@ var ContextMonitor = class {
|
|
|
2186
2965
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2187
2966
|
data: {
|
|
2188
2967
|
usagePercent,
|
|
2189
|
-
|
|
2968
|
+
currentTokens,
|
|
2969
|
+
contextWindow,
|
|
2970
|
+
compactThreshold,
|
|
2971
|
+
remainingBeforeCompact
|
|
2190
2972
|
}
|
|
2191
2973
|
};
|
|
2192
2974
|
void this.hooksEngine.emit("on-context-threshold", hookInput);
|
|
@@ -2323,6 +3105,7 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
|
|
|
2323
3105
|
try {
|
|
2324
3106
|
if (flags.prompt) {
|
|
2325
3107
|
logger.debug(`Executing prompt on ${backendId}`);
|
|
3108
|
+
let nativeSessionId;
|
|
2326
3109
|
if (adapter.executeStreaming) {
|
|
2327
3110
|
for await (const event of adapter.executeStreaming(flags)) {
|
|
2328
3111
|
switch (event.type) {
|
|
@@ -2353,18 +3136,17 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
|
|
|
2353
3136
|
break;
|
|
2354
3137
|
case "usage": {
|
|
2355
3138
|
if (contextMonitor2 && relaySessionId) {
|
|
2356
|
-
const maxTokens = backendId === "gemini" ? 128e3 : 2e5;
|
|
2357
3139
|
contextMonitor2.updateUsage(
|
|
2358
3140
|
relaySessionId,
|
|
2359
3141
|
backendId,
|
|
2360
|
-
event.inputTokens + event.outputTokens
|
|
2361
|
-
maxTokens
|
|
3142
|
+
event.inputTokens + event.outputTokens
|
|
2362
3143
|
);
|
|
2363
3144
|
}
|
|
2364
3145
|
break;
|
|
2365
3146
|
}
|
|
2366
3147
|
case "done":
|
|
2367
3148
|
process.exitCode = event.result.exitCode;
|
|
3149
|
+
nativeSessionId = event.nativeSessionId;
|
|
2368
3150
|
if (event.nativeSessionId && sessionManager2 && relaySessionId) {
|
|
2369
3151
|
try {
|
|
2370
3152
|
await sessionManager2.update(relaySessionId, {
|
|
@@ -2381,6 +3163,7 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
|
|
|
2381
3163
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
2382
3164
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
2383
3165
|
process.exitCode = result.exitCode;
|
|
3166
|
+
nativeSessionId = result.nativeSessionId;
|
|
2384
3167
|
if (result.nativeSessionId && sessionManager2 && relaySessionId) {
|
|
2385
3168
|
try {
|
|
2386
3169
|
await sessionManager2.update(relaySessionId, {
|
|
@@ -2989,7 +3772,7 @@ function createVersionCommand(registry2) {
|
|
|
2989
3772
|
// src/commands/doctor.ts
|
|
2990
3773
|
import { defineCommand as defineCommand8 } from "citty";
|
|
2991
3774
|
import { access, constants, readdir as readdir2 } from "fs/promises";
|
|
2992
|
-
import { join as
|
|
3775
|
+
import { join as join7 } from "path";
|
|
2993
3776
|
import { homedir as homedir5 } from "os";
|
|
2994
3777
|
import { execFile } from "child_process";
|
|
2995
3778
|
import { promisify } from "util";
|
|
@@ -3050,8 +3833,8 @@ async function checkConfig(configManager2) {
|
|
|
3050
3833
|
}
|
|
3051
3834
|
}
|
|
3052
3835
|
async function checkSessionsDir() {
|
|
3053
|
-
const relayHome2 = process.env["RELAY_HOME"] ??
|
|
3054
|
-
const sessionsDir =
|
|
3836
|
+
const relayHome2 = process.env["RELAY_HOME"] ?? join7(homedir5(), ".relay");
|
|
3837
|
+
const sessionsDir = join7(relayHome2, "sessions");
|
|
3055
3838
|
try {
|
|
3056
3839
|
await access(sessionsDir, constants.W_OK);
|
|
3057
3840
|
return {
|
|
@@ -3164,8 +3947,8 @@ async function checkBackendAuthEnv() {
|
|
|
3164
3947
|
return results;
|
|
3165
3948
|
}
|
|
3166
3949
|
async function checkSessionsDiskUsage() {
|
|
3167
|
-
const relayHome2 = process.env["RELAY_HOME"] ??
|
|
3168
|
-
const sessionsDir =
|
|
3950
|
+
const relayHome2 = process.env["RELAY_HOME"] ?? join7(homedir5(), ".relay");
|
|
3951
|
+
const sessionsDir = join7(relayHome2, "sessions");
|
|
3169
3952
|
try {
|
|
3170
3953
|
const entries = await readdir2(sessionsDir);
|
|
3171
3954
|
const fileCount = entries.length;
|
|
@@ -3239,7 +4022,7 @@ function createDoctorCommand(registry2, configManager2) {
|
|
|
3239
4022
|
init_logger();
|
|
3240
4023
|
import { defineCommand as defineCommand9 } from "citty";
|
|
3241
4024
|
import { mkdir as mkdir6, writeFile as writeFile6, access as access2, readFile as readFile6 } from "fs/promises";
|
|
3242
|
-
import { join as
|
|
4025
|
+
import { join as join8 } from "path";
|
|
3243
4026
|
var DEFAULT_CONFIG2 = {
|
|
3244
4027
|
defaultBackend: "claude",
|
|
3245
4028
|
backends: {},
|
|
@@ -3253,8 +4036,8 @@ function createInitCommand() {
|
|
|
3253
4036
|
},
|
|
3254
4037
|
async run() {
|
|
3255
4038
|
const projectDir = process.cwd();
|
|
3256
|
-
const relayDir =
|
|
3257
|
-
const configPath =
|
|
4039
|
+
const relayDir = join8(projectDir, ".relay");
|
|
4040
|
+
const configPath = join8(relayDir, "config.json");
|
|
3258
4041
|
try {
|
|
3259
4042
|
await access2(relayDir);
|
|
3260
4043
|
logger.info(
|
|
@@ -3270,7 +4053,7 @@ function createInitCommand() {
|
|
|
3270
4053
|
"utf-8"
|
|
3271
4054
|
);
|
|
3272
4055
|
logger.success(`Created ${configPath}`);
|
|
3273
|
-
const gitignorePath =
|
|
4056
|
+
const gitignorePath = join8(projectDir, ".gitignore");
|
|
3274
4057
|
try {
|
|
3275
4058
|
const gitignoreContent = await readFile6(gitignorePath, "utf-8");
|
|
3276
4059
|
if (!gitignoreContent.includes(".relay/config.local.json")) {
|
|
@@ -3295,8 +4078,8 @@ registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
|
|
|
3295
4078
|
registry.registerLazy("codex", () => new CodexAdapter(processManager));
|
|
3296
4079
|
registry.registerLazy("gemini", () => new GeminiAdapter(processManager));
|
|
3297
4080
|
var sessionManager = new SessionManager();
|
|
3298
|
-
var relayHome = process.env["RELAY_HOME"] ??
|
|
3299
|
-
var projectRelayDir =
|
|
4081
|
+
var relayHome = process.env["RELAY_HOME"] ?? join9(homedir6(), ".relay");
|
|
4082
|
+
var projectRelayDir = join9(process.cwd(), ".relay");
|
|
3300
4083
|
var configManager = new ConfigManager(relayHome, projectRelayDir);
|
|
3301
4084
|
var authManager = new AuthManager(registry);
|
|
3302
4085
|
var eventBus = new EventBus();
|