@rk0429/agentic-relay 0.2.0 → 0.3.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 +14 -5
- package/dist/relay.mjs +485 -51
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# agentic-relay
|
|
2
2
|
|
|
3
|
+
[](https://github.com/RK0429/agentic-relay/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@rk0429/agentic-relay)
|
|
5
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
3
8
|
A unified CLI that brings Claude Code, Codex CLI, and Gemini CLI under a single interface -- solving tool fragmentation, enabling multi-layer sub-agent orchestration via MCP, and providing proactive context window monitoring.
|
|
4
9
|
|
|
5
10
|
## Why agentic-relay?
|
|
@@ -94,7 +99,9 @@ relay mcp list # List MCP servers
|
|
|
94
99
|
relay mcp add <name> -- CMD # Add MCP server
|
|
95
100
|
relay mcp remove <name> # Remove MCP server
|
|
96
101
|
relay mcp sync # Sync MCP config to backends
|
|
97
|
-
relay mcp serve
|
|
102
|
+
relay mcp serve # Start as MCP server (stdio, default)
|
|
103
|
+
relay mcp serve --transport http # Start as MCP server (HTTP)
|
|
104
|
+
relay mcp serve --transport http --port 8080 # Custom port
|
|
98
105
|
```
|
|
99
106
|
|
|
100
107
|
## Configuration
|
|
@@ -144,9 +151,10 @@ Example configuration:
|
|
|
144
151
|
| `RELAY_LOG_LEVEL` | Log level (debug/info/warn/error) | `info` |
|
|
145
152
|
| `RELAY_MAX_DEPTH` | Max recursion depth in MCP server mode | `5` |
|
|
146
153
|
| `RELAY_CONTEXT_THRESHOLD` | Context warning threshold (%) | `75` |
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
154
|
+
| `RELAY_CLAUDE_PERMISSION_MODE` | Claude permission mode (`bypassPermissions` or `default`) | `bypassPermissions` |
|
|
155
|
+
| `ANTHROPIC_API_KEY` | Passed through to Claude Code (optional with subscription) | -- |
|
|
156
|
+
| `OPENAI_API_KEY` | Passed through to Codex CLI (optional with subscription) | -- |
|
|
157
|
+
| `GEMINI_API_KEY` | Passed through to Gemini CLI (optional with subscription) | -- |
|
|
150
158
|
|
|
151
159
|
## MCP Server Mode
|
|
152
160
|
|
|
@@ -309,7 +317,8 @@ src/
|
|
|
309
317
|
- **Process management**: execa (interactive modes, Gemini CLI)
|
|
310
318
|
- **Validation**: zod
|
|
311
319
|
- **Logging**: consola
|
|
312
|
-
- **Testing**: vitest (
|
|
320
|
+
- **Testing**: vitest (352 tests across 18 files)
|
|
321
|
+
- **Coverage**: @vitest/coverage-v8
|
|
313
322
|
|
|
314
323
|
## License
|
|
315
324
|
|
package/dist/relay.mjs
CHANGED
|
@@ -273,22 +273,29 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
273
273
|
env: this.buildCleanEnv(flags)
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
|
+
getPermissionMode() {
|
|
277
|
+
return process.env["RELAY_CLAUDE_PERMISSION_MODE"] === "default" ? "default" : "bypassPermissions";
|
|
278
|
+
}
|
|
276
279
|
async execute(flags) {
|
|
277
280
|
if (!flags.prompt) {
|
|
278
281
|
throw new Error("execute requires a prompt (-p flag)");
|
|
279
282
|
}
|
|
280
283
|
const env = this.buildCleanEnv(flags);
|
|
284
|
+
const permissionMode = this.getPermissionMode();
|
|
281
285
|
try {
|
|
286
|
+
const options = {
|
|
287
|
+
env,
|
|
288
|
+
cwd: process.cwd(),
|
|
289
|
+
...flags.model ? { model: flags.model } : {},
|
|
290
|
+
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
|
|
291
|
+
};
|
|
292
|
+
if (permissionMode === "bypassPermissions") {
|
|
293
|
+
options.permissionMode = "bypassPermissions";
|
|
294
|
+
options.allowDangerouslySkipPermissions = true;
|
|
295
|
+
}
|
|
282
296
|
const q = query({
|
|
283
297
|
prompt: flags.prompt,
|
|
284
|
-
options
|
|
285
|
-
env,
|
|
286
|
-
cwd: process.cwd(),
|
|
287
|
-
...flags.model ? { model: flags.model } : {},
|
|
288
|
-
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
|
|
289
|
-
permissionMode: "bypassPermissions",
|
|
290
|
-
allowDangerouslySkipPermissions: true
|
|
291
|
-
}
|
|
298
|
+
options
|
|
292
299
|
});
|
|
293
300
|
let resultText = "";
|
|
294
301
|
let sessionId = "";
|
|
@@ -319,6 +326,79 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
319
326
|
};
|
|
320
327
|
}
|
|
321
328
|
}
|
|
329
|
+
async *executeStreaming(flags) {
|
|
330
|
+
if (!flags.prompt) {
|
|
331
|
+
throw new Error("executeStreaming requires a prompt (-p flag)");
|
|
332
|
+
}
|
|
333
|
+
const env = this.buildCleanEnv(flags);
|
|
334
|
+
const permissionMode = this.getPermissionMode();
|
|
335
|
+
try {
|
|
336
|
+
const options = {
|
|
337
|
+
env,
|
|
338
|
+
cwd: process.cwd(),
|
|
339
|
+
...flags.model ? { model: flags.model } : {},
|
|
340
|
+
...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
|
|
341
|
+
};
|
|
342
|
+
if (permissionMode === "bypassPermissions") {
|
|
343
|
+
options.permissionMode = "bypassPermissions";
|
|
344
|
+
options.allowDangerouslySkipPermissions = true;
|
|
345
|
+
}
|
|
346
|
+
const q = query({
|
|
347
|
+
prompt: flags.prompt,
|
|
348
|
+
options
|
|
349
|
+
});
|
|
350
|
+
for await (const message of q) {
|
|
351
|
+
if (message.type === "assistant") {
|
|
352
|
+
const betaMessage = message.message;
|
|
353
|
+
if (betaMessage?.content) {
|
|
354
|
+
for (const block of betaMessage.content) {
|
|
355
|
+
if (block.type === "text" && block.text) {
|
|
356
|
+
yield { type: "text", text: block.text };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (betaMessage?.usage) {
|
|
361
|
+
yield {
|
|
362
|
+
type: "usage",
|
|
363
|
+
inputTokens: betaMessage.usage.input_tokens ?? 0,
|
|
364
|
+
outputTokens: betaMessage.usage.output_tokens ?? 0
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
} else if (message.type === "tool_progress") {
|
|
368
|
+
const toolName = message.tool_name ?? "unknown";
|
|
369
|
+
yield {
|
|
370
|
+
type: "status",
|
|
371
|
+
message: `Running ${toolName}...`
|
|
372
|
+
};
|
|
373
|
+
} else if (message.type === "result") {
|
|
374
|
+
if (message.subtype === "success") {
|
|
375
|
+
const resultText = message.result;
|
|
376
|
+
yield {
|
|
377
|
+
type: "done",
|
|
378
|
+
result: { exitCode: 0, stdout: resultText, stderr: "" }
|
|
379
|
+
};
|
|
380
|
+
} else {
|
|
381
|
+
const errors = message.errors;
|
|
382
|
+
yield {
|
|
383
|
+
type: "done",
|
|
384
|
+
result: {
|
|
385
|
+
exitCode: 1,
|
|
386
|
+
stdout: "",
|
|
387
|
+
stderr: errors.join("\n")
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
395
|
+
yield { type: "error", message: errorMessage };
|
|
396
|
+
yield {
|
|
397
|
+
type: "done",
|
|
398
|
+
result: { exitCode: 1, stdout: "", stderr: errorMessage }
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
322
402
|
async resumeSession(sessionId, flags) {
|
|
323
403
|
await this.processManager.spawnInteractive(
|
|
324
404
|
this.command,
|
|
@@ -430,6 +510,106 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
430
510
|
};
|
|
431
511
|
}
|
|
432
512
|
}
|
|
513
|
+
async *executeStreaming(flags) {
|
|
514
|
+
if (!flags.prompt) {
|
|
515
|
+
throw new Error("executeStreaming requires a prompt (-p flag)");
|
|
516
|
+
}
|
|
517
|
+
if (flags.agent) {
|
|
518
|
+
logger.warn(
|
|
519
|
+
`Codex CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const codexOptions = {};
|
|
524
|
+
if (flags.mcpContext) {
|
|
525
|
+
codexOptions.env = {
|
|
526
|
+
...process.env,
|
|
527
|
+
RELAY_TRACE_ID: flags.mcpContext.traceId,
|
|
528
|
+
RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
|
|
529
|
+
RELAY_DEPTH: String(flags.mcpContext.depth)
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const codex = new Codex(codexOptions);
|
|
533
|
+
const thread = codex.startThread({
|
|
534
|
+
...flags.model ? { model: flags.model } : {},
|
|
535
|
+
workingDirectory: process.cwd(),
|
|
536
|
+
approvalPolicy: "never"
|
|
537
|
+
});
|
|
538
|
+
const streamedTurn = await thread.runStreamed(flags.prompt);
|
|
539
|
+
const completedMessages = [];
|
|
540
|
+
for await (const event of streamedTurn.events) {
|
|
541
|
+
if (event.type === "item.started") {
|
|
542
|
+
const item = event.item;
|
|
543
|
+
if (item?.type === "agent_message" && item.text) {
|
|
544
|
+
yield { type: "text", text: item.text };
|
|
545
|
+
} else if (item?.type === "command_execution") {
|
|
546
|
+
yield {
|
|
547
|
+
type: "tool_start",
|
|
548
|
+
tool: item.command ?? "command",
|
|
549
|
+
id: item.id ?? ""
|
|
550
|
+
};
|
|
551
|
+
} else if (item?.type === "file_change") {
|
|
552
|
+
yield {
|
|
553
|
+
type: "tool_start",
|
|
554
|
+
tool: "file_change",
|
|
555
|
+
id: item.id ?? ""
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
} else if (event.type === "item.completed") {
|
|
559
|
+
const item = event.item;
|
|
560
|
+
if (item?.type === "agent_message" && item.text) {
|
|
561
|
+
completedMessages.push(item.text);
|
|
562
|
+
yield { type: "text", text: item.text };
|
|
563
|
+
} else if (item?.type === "command_execution") {
|
|
564
|
+
yield {
|
|
565
|
+
type: "tool_end",
|
|
566
|
+
tool: item.command ?? "command",
|
|
567
|
+
id: item.id ?? "",
|
|
568
|
+
result: item.aggregated_output
|
|
569
|
+
};
|
|
570
|
+
} else if (item?.type === "file_change") {
|
|
571
|
+
yield {
|
|
572
|
+
type: "tool_end",
|
|
573
|
+
tool: "file_change",
|
|
574
|
+
id: item.id ?? ""
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
} else if (event.type === "turn.completed") {
|
|
578
|
+
const usage = event.usage;
|
|
579
|
+
if (usage) {
|
|
580
|
+
yield {
|
|
581
|
+
type: "usage",
|
|
582
|
+
inputTokens: (usage.input_tokens ?? 0) + (usage.cached_input_tokens ?? 0),
|
|
583
|
+
outputTokens: usage.output_tokens ?? 0
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const finalResponse = completedMessages.join("\n");
|
|
587
|
+
yield {
|
|
588
|
+
type: "done",
|
|
589
|
+
result: { exitCode: 0, stdout: finalResponse, stderr: "" }
|
|
590
|
+
};
|
|
591
|
+
} else if (event.type === "turn.failed") {
|
|
592
|
+
const errorMessage = event.error?.message ?? "Turn failed";
|
|
593
|
+
yield {
|
|
594
|
+
type: "done",
|
|
595
|
+
result: { exitCode: 1, stdout: "", stderr: errorMessage }
|
|
596
|
+
};
|
|
597
|
+
} else if (event.type === "error") {
|
|
598
|
+
yield {
|
|
599
|
+
type: "error",
|
|
600
|
+
message: event.message ?? "Unknown error"
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} catch (error) {
|
|
605
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
606
|
+
yield { type: "error", message: errorMessage };
|
|
607
|
+
yield {
|
|
608
|
+
type: "done",
|
|
609
|
+
result: { exitCode: 1, stdout: "", stderr: errorMessage }
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
433
613
|
async resumeSession(sessionId, flags) {
|
|
434
614
|
const args = [];
|
|
435
615
|
if (flags.model) {
|
|
@@ -579,7 +759,7 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
579
759
|
};
|
|
580
760
|
|
|
581
761
|
// src/core/session-manager.ts
|
|
582
|
-
import { readFile, writeFile, readdir, mkdir } from "fs/promises";
|
|
762
|
+
import { readFile, writeFile, readdir, mkdir, chmod } from "fs/promises";
|
|
583
763
|
import { join } from "path";
|
|
584
764
|
import { homedir } from "os";
|
|
585
765
|
import { nanoid } from "nanoid";
|
|
@@ -603,7 +783,8 @@ function fromSessionData(data) {
|
|
|
603
783
|
updatedAt: new Date(data.updatedAt)
|
|
604
784
|
};
|
|
605
785
|
}
|
|
606
|
-
var SessionManager = class {
|
|
786
|
+
var SessionManager = class _SessionManager {
|
|
787
|
+
static SESSION_ID_PATTERN = /^relay-[A-Za-z0-9_-]+$/;
|
|
607
788
|
sessionsDir;
|
|
608
789
|
constructor(sessionsDir) {
|
|
609
790
|
this.sessionsDir = sessionsDir ?? getSessionsDir(getRelayHome());
|
|
@@ -613,6 +794,9 @@ var SessionManager = class {
|
|
|
613
794
|
await mkdir(this.sessionsDir, { recursive: true });
|
|
614
795
|
}
|
|
615
796
|
sessionPath(relaySessionId) {
|
|
797
|
+
if (!_SessionManager.SESSION_ID_PATTERN.test(relaySessionId)) {
|
|
798
|
+
throw new Error(`Invalid session ID: ${relaySessionId}`);
|
|
799
|
+
}
|
|
616
800
|
return join(this.sessionsDir, `${relaySessionId}.json`);
|
|
617
801
|
}
|
|
618
802
|
/** Create a new relay session. */
|
|
@@ -629,11 +813,13 @@ var SessionManager = class {
|
|
|
629
813
|
updatedAt: now,
|
|
630
814
|
status: "active"
|
|
631
815
|
};
|
|
816
|
+
const sessionFilePath = this.sessionPath(session.relaySessionId);
|
|
632
817
|
await writeFile(
|
|
633
|
-
|
|
818
|
+
sessionFilePath,
|
|
634
819
|
JSON.stringify(toSessionData(session), null, 2),
|
|
635
820
|
"utf-8"
|
|
636
821
|
);
|
|
822
|
+
await chmod(sessionFilePath, 384);
|
|
637
823
|
return session;
|
|
638
824
|
}
|
|
639
825
|
/** Update an existing session. */
|
|
@@ -647,16 +833,19 @@ var SessionManager = class {
|
|
|
647
833
|
...updates,
|
|
648
834
|
updatedAt: /* @__PURE__ */ new Date()
|
|
649
835
|
};
|
|
836
|
+
const updateFilePath = this.sessionPath(relaySessionId);
|
|
650
837
|
await writeFile(
|
|
651
|
-
|
|
838
|
+
updateFilePath,
|
|
652
839
|
JSON.stringify(toSessionData(updated), null, 2),
|
|
653
840
|
"utf-8"
|
|
654
841
|
);
|
|
842
|
+
await chmod(updateFilePath, 384);
|
|
655
843
|
}
|
|
656
844
|
/** Get a session by relay session ID. */
|
|
657
845
|
async get(relaySessionId) {
|
|
846
|
+
const filePath = this.sessionPath(relaySessionId);
|
|
658
847
|
try {
|
|
659
|
-
const raw = await readFile(
|
|
848
|
+
const raw = await readFile(filePath, "utf-8");
|
|
660
849
|
return fromSessionData(JSON.parse(raw));
|
|
661
850
|
} catch {
|
|
662
851
|
return null;
|
|
@@ -700,7 +889,7 @@ var SessionManager = class {
|
|
|
700
889
|
};
|
|
701
890
|
|
|
702
891
|
// src/core/config-manager.ts
|
|
703
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
892
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
|
|
704
893
|
import { join as join2 } from "path";
|
|
705
894
|
|
|
706
895
|
// src/schemas/config.schema.ts
|
|
@@ -752,7 +941,8 @@ var relayConfigSchema = z.object({
|
|
|
752
941
|
}).optional(),
|
|
753
942
|
telemetry: z.object({
|
|
754
943
|
enabled: z.boolean()
|
|
755
|
-
}).optional()
|
|
944
|
+
}).optional(),
|
|
945
|
+
claudePermissionMode: z.enum(["default", "bypassPermissions"]).optional()
|
|
756
946
|
});
|
|
757
947
|
|
|
758
948
|
// src/core/config-manager.ts
|
|
@@ -845,6 +1035,7 @@ var ConfigManager = class {
|
|
|
845
1035
|
const dir = filePath.substring(0, filePath.lastIndexOf("/"));
|
|
846
1036
|
await mkdir2(dir, { recursive: true });
|
|
847
1037
|
await writeFile2(filePath, JSON.stringify(existing, null, 2), "utf-8");
|
|
1038
|
+
await chmod2(filePath, 384);
|
|
848
1039
|
}
|
|
849
1040
|
/**
|
|
850
1041
|
* Syncs MCP server config to all registered backend adapters.
|
|
@@ -998,18 +1189,54 @@ var DEFAULT_HOOK_OUTPUT = {
|
|
|
998
1189
|
message: "",
|
|
999
1190
|
metadata: {}
|
|
1000
1191
|
};
|
|
1001
|
-
var HooksEngine = class {
|
|
1192
|
+
var HooksEngine = class _HooksEngine {
|
|
1002
1193
|
constructor(eventBus2, processManager2) {
|
|
1003
1194
|
this.eventBus = eventBus2;
|
|
1004
1195
|
this.processManager = processManager2;
|
|
1005
1196
|
}
|
|
1197
|
+
static COMMAND_PATTERN = /^[a-zA-Z0-9_./-]+$/;
|
|
1198
|
+
static ARG_PATTERN = /^[a-zA-Z0-9_.=:/-]+$/;
|
|
1006
1199
|
definitions = [];
|
|
1007
1200
|
registered = false;
|
|
1201
|
+
validateCommand(command) {
|
|
1202
|
+
if (!command || command.trim().length === 0) {
|
|
1203
|
+
throw new Error("Hook command cannot be empty");
|
|
1204
|
+
}
|
|
1205
|
+
if (!_HooksEngine.COMMAND_PATTERN.test(command)) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`Hook command contains invalid characters: "${command}". Only alphanumeric characters, dots, underscores, hyphens, and slashes are allowed.`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
if (command.includes("..")) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`Hook command contains path traversal: "${command}"`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
validateArgs(args) {
|
|
1217
|
+
for (const arg of args) {
|
|
1218
|
+
if (!_HooksEngine.ARG_PATTERN.test(arg)) {
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
`Hook argument contains invalid characters: "${arg}". Only alphanumeric characters, dots, underscores, equals, colons, hyphens, and slashes are allowed.`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1008
1225
|
/** Load hook definitions from config and register listeners on EventBus */
|
|
1009
1226
|
loadConfig(config) {
|
|
1010
|
-
this.definitions = config.definitions.filter(
|
|
1011
|
-
(def
|
|
1012
|
-
|
|
1227
|
+
this.definitions = config.definitions.filter((def) => {
|
|
1228
|
+
if (def.enabled === false) return false;
|
|
1229
|
+
try {
|
|
1230
|
+
this.validateCommand(def.command);
|
|
1231
|
+
this.validateArgs(def.args ?? []);
|
|
1232
|
+
return true;
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
logger.warn(
|
|
1235
|
+
`Skipping hook with invalid command: ${error instanceof Error ? error.message : String(error)}`
|
|
1236
|
+
);
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1013
1240
|
if (!this.registered) {
|
|
1014
1241
|
for (const event of this.getUniqueEvents()) {
|
|
1015
1242
|
this.eventBus.on(event, async () => {
|
|
@@ -1056,6 +1283,8 @@ var HooksEngine = class {
|
|
|
1056
1283
|
return this.definitions.filter((def) => def.event === event).length;
|
|
1057
1284
|
}
|
|
1058
1285
|
async executeHook(def, input) {
|
|
1286
|
+
this.validateCommand(def.command);
|
|
1287
|
+
this.validateArgs(def.args ?? []);
|
|
1059
1288
|
const timeoutMs = def.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
1060
1289
|
const stdinData = JSON.stringify(input);
|
|
1061
1290
|
const startTime = Date.now();
|
|
@@ -1167,7 +1396,7 @@ var INSTALL_GUIDES = {
|
|
|
1167
1396
|
};
|
|
1168
1397
|
|
|
1169
1398
|
// src/commands/backend.ts
|
|
1170
|
-
function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine2) {
|
|
1399
|
+
function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine2, contextMonitor2) {
|
|
1171
1400
|
return defineCommand({
|
|
1172
1401
|
meta: {
|
|
1173
1402
|
name: backendId,
|
|
@@ -1262,10 +1491,57 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
|
|
|
1262
1491
|
try {
|
|
1263
1492
|
if (flags.prompt) {
|
|
1264
1493
|
logger.debug(`Executing prompt on ${backendId}`);
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1494
|
+
if (adapter.executeStreaming) {
|
|
1495
|
+
for await (const event of adapter.executeStreaming(flags)) {
|
|
1496
|
+
switch (event.type) {
|
|
1497
|
+
case "text":
|
|
1498
|
+
process.stdout.write(event.text);
|
|
1499
|
+
break;
|
|
1500
|
+
case "tool_start":
|
|
1501
|
+
if (flags.verbose) {
|
|
1502
|
+
process.stderr.write(`
|
|
1503
|
+
[${event.tool}] started
|
|
1504
|
+
`);
|
|
1505
|
+
}
|
|
1506
|
+
break;
|
|
1507
|
+
case "tool_end":
|
|
1508
|
+
if (flags.verbose) {
|
|
1509
|
+
process.stderr.write(`[${event.tool}] completed
|
|
1510
|
+
`);
|
|
1511
|
+
}
|
|
1512
|
+
break;
|
|
1513
|
+
case "status":
|
|
1514
|
+
if (flags.verbose) {
|
|
1515
|
+
process.stderr.write(`[status] ${event.message}
|
|
1516
|
+
`);
|
|
1517
|
+
}
|
|
1518
|
+
break;
|
|
1519
|
+
case "error":
|
|
1520
|
+
process.stderr.write(event.message);
|
|
1521
|
+
break;
|
|
1522
|
+
case "usage": {
|
|
1523
|
+
if (contextMonitor2 && relaySessionId) {
|
|
1524
|
+
const maxTokens = backendId === "gemini" ? 128e3 : 2e5;
|
|
1525
|
+
contextMonitor2.updateUsage(
|
|
1526
|
+
relaySessionId,
|
|
1527
|
+
backendId,
|
|
1528
|
+
event.inputTokens + event.outputTokens,
|
|
1529
|
+
maxTokens
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
break;
|
|
1533
|
+
}
|
|
1534
|
+
case "done":
|
|
1535
|
+
process.exitCode = event.result.exitCode;
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
const result = await adapter.execute(flags);
|
|
1541
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
1542
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
1543
|
+
process.exitCode = result.exitCode;
|
|
1544
|
+
}
|
|
1269
1545
|
} else if (flags.continue) {
|
|
1270
1546
|
logger.debug(`Continuing latest session on ${backendId}`);
|
|
1271
1547
|
if (sessionManager2) {
|
|
@@ -1345,13 +1621,66 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
|
|
|
1345
1621
|
|
|
1346
1622
|
// src/commands/update.ts
|
|
1347
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";
|
|
1348
1663
|
function createUpdateCommand(registry2) {
|
|
1349
1664
|
return defineCommand2({
|
|
1350
1665
|
meta: {
|
|
1351
1666
|
name: "update",
|
|
1352
|
-
description: "Update all installed backend CLI tools"
|
|
1667
|
+
description: "Update relay and all installed backend CLI tools"
|
|
1353
1668
|
},
|
|
1354
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
|
+
}
|
|
1355
1684
|
const adapters = registry2.list();
|
|
1356
1685
|
if (adapters.length === 0) {
|
|
1357
1686
|
logger.warn("No backends registered");
|
|
@@ -1465,11 +1794,14 @@ import { defineCommand as defineCommand4 } from "citty";
|
|
|
1465
1794
|
// src/mcp-server/server.ts
|
|
1466
1795
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1467
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";
|
|
1468
1800
|
import { z as z5 } from "zod";
|
|
1469
1801
|
|
|
1470
1802
|
// src/mcp-server/recursion-guard.ts
|
|
1471
1803
|
import { createHash } from "crypto";
|
|
1472
|
-
var RecursionGuard = class {
|
|
1804
|
+
var RecursionGuard = class _RecursionGuard {
|
|
1473
1805
|
constructor(config = {
|
|
1474
1806
|
maxDepth: 5,
|
|
1475
1807
|
maxCallsPerSession: 20,
|
|
@@ -1477,17 +1809,50 @@ var RecursionGuard = class {
|
|
|
1477
1809
|
}) {
|
|
1478
1810
|
this.config = config;
|
|
1479
1811
|
}
|
|
1812
|
+
static MAX_ENTRIES = 1e3;
|
|
1480
1813
|
callCounts = /* @__PURE__ */ new Map();
|
|
1481
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
|
+
}
|
|
1482
1846
|
/** Check if a spawn is allowed */
|
|
1483
1847
|
canSpawn(context) {
|
|
1848
|
+
this.cleanup();
|
|
1484
1849
|
if (context.depth >= this.config.maxDepth) {
|
|
1485
1850
|
return {
|
|
1486
1851
|
allowed: false,
|
|
1487
1852
|
reason: `Max depth exceeded: ${context.depth} >= ${this.config.maxDepth}`
|
|
1488
1853
|
};
|
|
1489
1854
|
}
|
|
1490
|
-
const currentCount = this.callCounts.get(context.traceId) ?? 0;
|
|
1855
|
+
const currentCount = this.callCounts.get(context.traceId)?.count ?? 0;
|
|
1491
1856
|
if (currentCount >= this.config.maxCallsPerSession) {
|
|
1492
1857
|
return {
|
|
1493
1858
|
allowed: false,
|
|
@@ -1504,17 +1869,25 @@ var RecursionGuard = class {
|
|
|
1504
1869
|
}
|
|
1505
1870
|
/** Record a spawn invocation */
|
|
1506
1871
|
recordSpawn(context) {
|
|
1507
|
-
|
|
1508
|
-
this.callCounts.
|
|
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
|
+
}
|
|
1509
1879
|
const key = `${context.backend}:${context.promptHash}`;
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
+
}
|
|
1513
1886
|
}
|
|
1514
1887
|
/** Detect if the same (backend + promptHash) combination has appeared 3+ times */
|
|
1515
1888
|
detectLoop(traceId, backend, promptHash) {
|
|
1516
1889
|
const key = `${backend}:${promptHash}`;
|
|
1517
|
-
const hashes = this.promptHashes.get(traceId) ?? [];
|
|
1890
|
+
const hashes = this.promptHashes.get(traceId)?.hashes ?? [];
|
|
1518
1891
|
const count = hashes.filter((h) => h === key).length;
|
|
1519
1892
|
return count >= 3;
|
|
1520
1893
|
}
|
|
@@ -1524,7 +1897,7 @@ var RecursionGuard = class {
|
|
|
1524
1897
|
}
|
|
1525
1898
|
/** Get call count for a trace */
|
|
1526
1899
|
getCallCount(traceId) {
|
|
1527
|
-
return this.callCounts.get(traceId) ?? 0;
|
|
1900
|
+
return this.callCounts.get(traceId)?.count ?? 0;
|
|
1528
1901
|
}
|
|
1529
1902
|
/** Utility: compute a prompt hash */
|
|
1530
1903
|
static hashPrompt(prompt) {
|
|
@@ -1549,7 +1922,7 @@ function buildContextFromEnv() {
|
|
|
1549
1922
|
const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
|
|
1550
1923
|
return { traceId, parentSessionId, depth };
|
|
1551
1924
|
}
|
|
1552
|
-
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2) {
|
|
1925
|
+
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2) {
|
|
1553
1926
|
const envContext = buildContextFromEnv();
|
|
1554
1927
|
const promptHash = RecursionGuard.hashPrompt(input.prompt);
|
|
1555
1928
|
const context = {
|
|
@@ -1617,6 +1990,18 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
1617
1990
|
traceId: envContext.traceId
|
|
1618
1991
|
}
|
|
1619
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
|
+
}
|
|
1620
2005
|
guard.recordSpawn(context);
|
|
1621
2006
|
const status = result.exitCode === 0 ? "completed" : "error";
|
|
1622
2007
|
await sessionManager2.update(session.relaySessionId, { status });
|
|
@@ -1715,7 +2100,7 @@ var RelayMCPServer = class {
|
|
|
1715
2100
|
this.guard = new RecursionGuard(guardConfig);
|
|
1716
2101
|
this.server = new McpServer({
|
|
1717
2102
|
name: "agentic-relay",
|
|
1718
|
-
version: "0.
|
|
2103
|
+
version: "0.3.0"
|
|
1719
2104
|
});
|
|
1720
2105
|
this.registerTools();
|
|
1721
2106
|
}
|
|
@@ -1740,7 +2125,8 @@ var RelayMCPServer = class {
|
|
|
1740
2125
|
this.registry,
|
|
1741
2126
|
this.sessionManager,
|
|
1742
2127
|
this.guard,
|
|
1743
|
-
this.hooksEngine
|
|
2128
|
+
this.hooksEngine,
|
|
2129
|
+
this.contextMonitor
|
|
1744
2130
|
);
|
|
1745
2131
|
const isError = result.exitCode !== 0;
|
|
1746
2132
|
const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
|
|
@@ -1820,11 +2206,47 @@ ${result.stdout}`;
|
|
|
1820
2206
|
}
|
|
1821
2207
|
);
|
|
1822
2208
|
}
|
|
1823
|
-
async start() {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
+
}
|
|
2217
|
+
const port = options?.port ?? 3100;
|
|
2218
|
+
logger.info(
|
|
2219
|
+
`Starting agentic-relay MCP server (HTTP transport on port ${port})...`
|
|
2220
|
+
);
|
|
2221
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
2222
|
+
sessionIdGenerator: () => randomUUID()
|
|
2223
|
+
});
|
|
2224
|
+
const httpServer = createServer(async (req, res) => {
|
|
2225
|
+
const url = req.url ?? "";
|
|
2226
|
+
if (url === "/mcp" || url.startsWith("/mcp?")) {
|
|
2227
|
+
await httpTransport.handleRequest(req, res);
|
|
2228
|
+
} else {
|
|
2229
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
2230
|
+
res.end("Not found");
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
this._httpServer = httpServer;
|
|
2234
|
+
await this.server.connect(httpTransport);
|
|
2235
|
+
await new Promise((resolve) => {
|
|
2236
|
+
httpServer.listen(port, () => {
|
|
2237
|
+
logger.info(`MCP server listening on http://localhost:${port}/mcp`);
|
|
2238
|
+
resolve();
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
2241
|
+
await new Promise((resolve) => {
|
|
2242
|
+
httpServer.on("close", resolve);
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
/** Exposed for testing and graceful shutdown */
|
|
2246
|
+
get httpServer() {
|
|
2247
|
+
return this._httpServer;
|
|
1827
2248
|
}
|
|
2249
|
+
_httpServer;
|
|
1828
2250
|
};
|
|
1829
2251
|
|
|
1830
2252
|
// src/commands/mcp.ts
|
|
@@ -1948,9 +2370,21 @@ function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngin
|
|
|
1948
2370
|
serve: defineCommand4({
|
|
1949
2371
|
meta: {
|
|
1950
2372
|
name: "serve",
|
|
1951
|
-
description: "Start relay as an MCP server
|
|
2373
|
+
description: "Start relay as an MCP server"
|
|
1952
2374
|
},
|
|
1953
|
-
|
|
2375
|
+
args: {
|
|
2376
|
+
transport: {
|
|
2377
|
+
type: "string",
|
|
2378
|
+
description: "Transport type: stdio or http (default: stdio)",
|
|
2379
|
+
default: "stdio"
|
|
2380
|
+
},
|
|
2381
|
+
port: {
|
|
2382
|
+
type: "string",
|
|
2383
|
+
description: "Port for HTTP transport (default: 3100)",
|
|
2384
|
+
default: "3100"
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
async run({ args }) {
|
|
1954
2388
|
if (!sessionManager2) {
|
|
1955
2389
|
logger.error("SessionManager is required for MCP server mode");
|
|
1956
2390
|
process.exitCode = 1;
|
|
@@ -1968,6 +2402,8 @@ function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngin
|
|
|
1968
2402
|
}
|
|
1969
2403
|
} catch {
|
|
1970
2404
|
}
|
|
2405
|
+
const transport = args.transport === "http" ? "http" : "stdio";
|
|
2406
|
+
const port = parseInt(String(args.port), 10) || 3100;
|
|
1971
2407
|
const server = new RelayMCPServer(
|
|
1972
2408
|
registry2,
|
|
1973
2409
|
sessionManager2,
|
|
@@ -1975,7 +2411,7 @@ function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngin
|
|
|
1975
2411
|
hooksEngine2,
|
|
1976
2412
|
contextMonitor2
|
|
1977
2413
|
);
|
|
1978
|
-
await server.start();
|
|
2414
|
+
await server.start({ transport, port });
|
|
1979
2415
|
}
|
|
1980
2416
|
})
|
|
1981
2417
|
},
|
|
@@ -2356,15 +2792,13 @@ var configManager = new ConfigManager(relayHome, projectRelayDir);
|
|
|
2356
2792
|
var authManager = new AuthManager(registry);
|
|
2357
2793
|
var eventBus = new EventBus();
|
|
2358
2794
|
var hooksEngine = new HooksEngine(eventBus, processManager);
|
|
2359
|
-
var contextMonitor = new ContextMonitor(hooksEngine
|
|
2360
|
-
enabled: false
|
|
2361
|
-
// Will be enabled from config at runtime
|
|
2362
|
-
});
|
|
2795
|
+
var contextMonitor = new ContextMonitor(hooksEngine);
|
|
2363
2796
|
void configManager.getConfig().then((config) => {
|
|
2364
2797
|
if (config.hooks) {
|
|
2365
2798
|
hooksEngine.loadConfig(config.hooks);
|
|
2366
2799
|
}
|
|
2367
2800
|
if (config.contextMonitor) {
|
|
2801
|
+
contextMonitor = new ContextMonitor(hooksEngine, config.contextMonitor);
|
|
2368
2802
|
}
|
|
2369
2803
|
}).catch(() => {
|
|
2370
2804
|
});
|
|
@@ -2375,9 +2809,9 @@ var main = defineCommand10({
|
|
|
2375
2809
|
description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
|
|
2376
2810
|
},
|
|
2377
2811
|
subCommands: {
|
|
2378
|
-
claude: createBackendCommand("claude", registry, sessionManager, hooksEngine),
|
|
2379
|
-
codex: createBackendCommand("codex", registry, sessionManager, hooksEngine),
|
|
2380
|
-
gemini: createBackendCommand("gemini", registry, sessionManager, hooksEngine),
|
|
2812
|
+
claude: createBackendCommand("claude", registry, sessionManager, hooksEngine, contextMonitor),
|
|
2813
|
+
codex: createBackendCommand("codex", registry, sessionManager, hooksEngine, contextMonitor),
|
|
2814
|
+
gemini: createBackendCommand("gemini", registry, sessionManager, hooksEngine, contextMonitor),
|
|
2381
2815
|
update: createUpdateCommand(registry),
|
|
2382
2816
|
config: createConfigCommand(configManager),
|
|
2383
2817
|
mcp: createMCPCommand(configManager, registry, sessionManager, hooksEngine, contextMonitor),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rk0429/agentic-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI with MCP-based multi-layer sub-agent orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"build": "tsup",
|
|
39
39
|
"dev": "tsup --watch",
|
|
40
40
|
"test": "vitest run",
|
|
41
|
+
"test:coverage": "vitest run --coverage",
|
|
41
42
|
"test:watch": "vitest",
|
|
42
43
|
"lint": "tsc --noEmit",
|
|
43
44
|
"prepublishOnly": "pnpm test && pnpm build"
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"@types/node": "^25.3.0",
|
|
61
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
60
62
|
"tsup": "^8.4.0",
|
|
61
63
|
"typescript": "^5.7.3",
|
|
62
64
|
"vitest": "^3.0.7"
|