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