@askalf/dario 3.25.0 → 3.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -456,6 +456,19 @@ async function help() {
456
456
  dario backend remove N Remove an OpenAI-compat backend
457
457
  dario shim -- CMD ARGS Run CMD inside the dario shim (experimental,
458
458
  stealth fingerprint via in-process fetch patch)
459
+ dario subagent install Register ~/.claude/agents/dario.md so Claude Code
460
+ can delegate dario diagnostics / template-refresh
461
+ operations to a named sub-agent (v3.26)
462
+ dario subagent remove Remove the registered sub-agent file
463
+ dario subagent status Show whether the sub-agent is installed
464
+ dario mcp Run dario as an MCP (Model Context Protocol)
465
+ server on stdio. Exposes read-only tools
466
+ (doctor, status, accounts_list, backends_list,
467
+ subagent_status, fingerprint_info) so MCP
468
+ clients (Claude Desktop, IDEs, etc.) can
469
+ inspect dario's state. No destructive ops
470
+ are exposed — mutations still require the
471
+ CLI. (v3.27)
459
472
  dario doctor Print a health report: dario / Node / CC /
460
473
  template / drift / OAuth / pool / backends
461
474
 
@@ -574,6 +587,113 @@ async function shim() {
574
587
  process.exit(1);
575
588
  }
576
589
  }
590
+ async function subagent() {
591
+ const sub = args[1] ?? 'status';
592
+ const { installSubagent, removeSubagent, loadSubagentStatus, SUBAGENT_NAME } = await import('./subagent.js');
593
+ if (sub === 'install') {
594
+ const r = installSubagent();
595
+ console.log('');
596
+ console.log(' dario — Sub-agent install');
597
+ console.log(' ─────────────────────────');
598
+ console.log('');
599
+ if (r.action === 'unchanged') {
600
+ console.log(` Already up to date at ${r.path} (v${r.version}).`);
601
+ }
602
+ else {
603
+ console.log(` ${r.action === 'created' ? 'Installed' : 'Updated'} at ${r.path} (v${r.version}).`);
604
+ }
605
+ console.log('');
606
+ console.log(' Claude Code will pick up the new sub-agent on its next startup.');
607
+ console.log(` Invoke it from CC with: "Use the ${SUBAGENT_NAME} sub-agent to …"`);
608
+ console.log('');
609
+ return;
610
+ }
611
+ if (sub === 'remove' || sub === 'uninstall') {
612
+ const r = removeSubagent();
613
+ console.log('');
614
+ console.log(' dario — Sub-agent remove');
615
+ console.log(' ────────────────────────');
616
+ console.log('');
617
+ if (r.removed) {
618
+ console.log(` Removed ${r.path}.`);
619
+ }
620
+ else {
621
+ console.log(` Nothing to remove — ${r.path} was not present.`);
622
+ }
623
+ console.log('');
624
+ return;
625
+ }
626
+ if (sub === 'status') {
627
+ const s = loadSubagentStatus();
628
+ console.log('');
629
+ console.log(' dario — Sub-agent status');
630
+ console.log(' ────────────────────────');
631
+ console.log('');
632
+ console.log(` Path: ${s.path}`);
633
+ console.log(` ~/.claude/agents: ${s.agentsDirExists ? 'exists' : 'missing (Claude Code not installed?)'}`);
634
+ if (!s.installed) {
635
+ console.log(' Installed: no');
636
+ console.log('');
637
+ console.log(' Install with: dario subagent install');
638
+ }
639
+ else {
640
+ console.log(` Installed: yes (v${s.fileVersion ?? 'unknown'})`);
641
+ if (!s.current) {
642
+ console.log(' Note: file version does not match installed dario — run `dario subagent install` to refresh.');
643
+ }
644
+ }
645
+ console.log('');
646
+ return;
647
+ }
648
+ console.error('');
649
+ console.error(' Usage: dario subagent <install | remove | status>');
650
+ console.error('');
651
+ console.error(' install Write ~/.claude/agents/dario.md so Claude Code can');
652
+ console.error(' delegate dario diagnostics to a named sub-agent.');
653
+ console.error(' remove Remove the installed sub-agent file.');
654
+ console.error(' status Report whether the sub-agent is installed (default).');
655
+ console.error('');
656
+ process.exit(1);
657
+ }
658
+ async function mcp() {
659
+ // MCP-over-stdio: protocol frames on stdout ONLY. Any stray console.log
660
+ // from downstream modules (doctor / oauth / accounts helpers) would
661
+ // corrupt the frame stream, so redirect them to stderr defensively for
662
+ // the lifetime of the server. Restored in the finally block for tests /
663
+ // embedders that re-use the process after `dario mcp`.
664
+ const origLog = console.log;
665
+ const origInfo = console.info;
666
+ console.log = (...a) => console.error(...a);
667
+ console.info = (...a) => console.error(...a);
668
+ try {
669
+ const [{ buildDefaultToolRegistry }, { runMcpServer }] = await Promise.all([
670
+ import('./mcp/tools.js'),
671
+ import('./mcp/server.js'),
672
+ ]);
673
+ const { readFile } = await import('node:fs/promises');
674
+ const { fileURLToPath } = await import('node:url');
675
+ const here = join(fileURLToPath(import.meta.url), '..', '..');
676
+ let pkgVersion = 'unknown';
677
+ try {
678
+ const pkg = JSON.parse(await readFile(join(here, 'package.json'), 'utf-8'));
679
+ if (typeof pkg.version === 'string')
680
+ pkgVersion = pkg.version;
681
+ }
682
+ catch {
683
+ // package.json missing or malformed — fall back to 'unknown' but let
684
+ // the server keep running so tool responses are still usable.
685
+ }
686
+ const tools = await buildDefaultToolRegistry();
687
+ await runMcpServer({
688
+ tools,
689
+ server: { name: 'dario', version: pkgVersion },
690
+ });
691
+ }
692
+ finally {
693
+ console.log = origLog;
694
+ console.info = origInfo;
695
+ }
696
+ }
577
697
  async function doctor() {
578
698
  const { runChecks, formatChecks, exitCodeFor } = await import('./doctor.js');
579
699
  console.log('');
@@ -612,6 +732,8 @@ const commands = {
612
732
  accounts,
613
733
  backend,
614
734
  shim,
735
+ subagent,
736
+ mcp,
615
737
  doctor,
616
738
  help,
617
739
  version,
package/dist/doctor.js CHANGED
@@ -197,6 +197,30 @@ export async function runChecks() {
197
197
  catch (err) {
198
198
  checks.push({ status: 'warn', label: 'Backends', detail: `check failed: ${err.message}` });
199
199
  }
200
+ // ---- CC sub-agent (v3.26, direction #2)
201
+ try {
202
+ const { loadSubagentStatus } = await import('./subagent.js');
203
+ const s = loadSubagentStatus();
204
+ if (!s.agentsDirExists) {
205
+ checks.push({ status: 'info', label: 'Sub-agent', detail: 'not installed (~/.claude/agents missing — Claude Code not installed?)' });
206
+ }
207
+ else if (!s.installed) {
208
+ checks.push({ status: 'info', label: 'Sub-agent', detail: 'not installed — run `dario subagent install` to enable CC integration' });
209
+ }
210
+ else if (!s.current) {
211
+ checks.push({
212
+ status: 'warn',
213
+ label: 'Sub-agent',
214
+ detail: `installed v${s.fileVersion ?? 'unknown'}, does not match this dario — run \`dario subagent install\` to refresh`,
215
+ });
216
+ }
217
+ else {
218
+ checks.push({ status: 'ok', label: 'Sub-agent', detail: `installed v${s.fileVersion} at ${s.path}` });
219
+ }
220
+ }
221
+ catch (err) {
222
+ checks.push({ status: 'warn', label: 'Sub-agent', detail: `check failed: ${err.message}` });
223
+ }
200
224
  // ---- ~/.dario dir
201
225
  try {
202
226
  const home = join(homedir(), '.dario');
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Minimal MCP (Model Context Protocol) implementation — JSON-RPC 2.0 + the
3
+ * subset of methods dario needs to expose as an MCP server (direction #4,
4
+ * v3.27). Zero-runtime-deps policy means we don't pull in
5
+ * `@modelcontextprotocol/sdk`; the protocol surface we need is small enough
6
+ * to hand-roll correctly.
7
+ *
8
+ * MCP over stdio uses newline-delimited JSON — each line on stdin is one
9
+ * complete JSON-RPC message; each line on stdout is one complete response
10
+ * or notification. That's what `parseLine` and `encodeMessage` handle.
11
+ *
12
+ * We implement three methods from the MCP spec:
13
+ * initialize — handshake, server replies with capabilities.
14
+ * tools/list — enumerate exposed tools + their JSON schemas.
15
+ * tools/call — invoke a named tool with structured arguments.
16
+ *
17
+ * Plus the standard JSON-RPC error shapes. Notifications (no-`id` messages)
18
+ * are accepted and, for the only one we care about (`notifications/initialized`),
19
+ * acknowledged silently.
20
+ *
21
+ * Kept pure on purpose so the tests can exercise every branch without any
22
+ * stdio — `handleMessage` takes a raw JSON-RPC payload and a tool registry
23
+ * and returns either a response string or null (for notifications).
24
+ */
25
+ /** MCP spec revisions ship with different wire quirks; pin the one we test against. */
26
+ export declare const MCP_PROTOCOL_VERSION = "2024-11-05";
27
+ /** JSON-RPC 2.0 error codes (https://www.jsonrpc.org/specification). */
28
+ export declare const JSONRPC_ERRORS: {
29
+ readonly PARSE_ERROR: -32700;
30
+ readonly INVALID_REQUEST: -32600;
31
+ readonly METHOD_NOT_FOUND: -32601;
32
+ readonly INVALID_PARAMS: -32602;
33
+ readonly INTERNAL_ERROR: -32603;
34
+ };
35
+ export interface JsonRpcRequest {
36
+ jsonrpc: '2.0';
37
+ id: string | number;
38
+ method: string;
39
+ params?: unknown;
40
+ }
41
+ export interface JsonRpcNotification {
42
+ jsonrpc: '2.0';
43
+ method: string;
44
+ params?: unknown;
45
+ }
46
+ export interface JsonRpcResponse {
47
+ jsonrpc: '2.0';
48
+ id: string | number | null;
49
+ result?: unknown;
50
+ error?: {
51
+ code: number;
52
+ message: string;
53
+ data?: unknown;
54
+ };
55
+ }
56
+ export type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification;
57
+ /** Shape of an MCP tool's content block — we only emit text content. */
58
+ export interface McpToolContent {
59
+ type: 'text';
60
+ text: string;
61
+ }
62
+ export interface McpToolResult {
63
+ content: McpToolContent[];
64
+ /** If a tool ran but the operation itself failed (e.g. upstream error), set isError: true. */
65
+ isError?: boolean;
66
+ }
67
+ export interface McpTool {
68
+ name: string;
69
+ description: string;
70
+ inputSchema: {
71
+ type: 'object';
72
+ properties: Record<string, unknown>;
73
+ required?: string[];
74
+ };
75
+ handler: (args: Record<string, unknown>) => Promise<McpToolResult>;
76
+ }
77
+ /** Server identity the client sees on `initialize`. */
78
+ export interface ServerInfo {
79
+ name: string;
80
+ version: string;
81
+ }
82
+ /**
83
+ * Parse one newline-delimited JSON line into a JSON-RPC message.
84
+ * Returns `{ ok: true, msg }` on success or `{ ok: false, error }` so the
85
+ * caller can emit the canonical -32700 parse-error response. Blank lines
86
+ * are reported as ok=false with no error — they're legal stdio framing
87
+ * noise, not protocol violations.
88
+ */
89
+ export declare function parseLine(line: string): {
90
+ ok: true;
91
+ msg: JsonRpcMessage;
92
+ } | {
93
+ ok: false;
94
+ error: string | null;
95
+ };
96
+ /**
97
+ * Shape a successful response. `id` is echoed from the request.
98
+ */
99
+ export declare function successResponse(id: string | number, result: unknown): JsonRpcResponse;
100
+ /**
101
+ * Shape an error response with a JSON-RPC error code + message. When the
102
+ * originating message was unparseable (no id extractable), pass `null`.
103
+ */
104
+ export declare function errorResponse(id: string | number | null, code: number, message: string, data?: unknown): JsonRpcResponse;
105
+ /** Encode a response as a newline-terminated string ready for stdout. */
106
+ export declare function encodeMessage(msg: JsonRpcResponse): string;
107
+ /**
108
+ * Core request-dispatch routine. Pure: given a parsed message + a tool
109
+ * registry + server identity, returns the JSON-RPC response (or null for
110
+ * a notification that expects no reply).
111
+ */
112
+ export declare function handleMessage(msg: JsonRpcMessage, tools: McpTool[], server: ServerInfo): Promise<JsonRpcResponse | null>;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Minimal MCP (Model Context Protocol) implementation — JSON-RPC 2.0 + the
3
+ * subset of methods dario needs to expose as an MCP server (direction #4,
4
+ * v3.27). Zero-runtime-deps policy means we don't pull in
5
+ * `@modelcontextprotocol/sdk`; the protocol surface we need is small enough
6
+ * to hand-roll correctly.
7
+ *
8
+ * MCP over stdio uses newline-delimited JSON — each line on stdin is one
9
+ * complete JSON-RPC message; each line on stdout is one complete response
10
+ * or notification. That's what `parseLine` and `encodeMessage` handle.
11
+ *
12
+ * We implement three methods from the MCP spec:
13
+ * initialize — handshake, server replies with capabilities.
14
+ * tools/list — enumerate exposed tools + their JSON schemas.
15
+ * tools/call — invoke a named tool with structured arguments.
16
+ *
17
+ * Plus the standard JSON-RPC error shapes. Notifications (no-`id` messages)
18
+ * are accepted and, for the only one we care about (`notifications/initialized`),
19
+ * acknowledged silently.
20
+ *
21
+ * Kept pure on purpose so the tests can exercise every branch without any
22
+ * stdio — `handleMessage` takes a raw JSON-RPC payload and a tool registry
23
+ * and returns either a response string or null (for notifications).
24
+ */
25
+ /** MCP spec revisions ship with different wire quirks; pin the one we test against. */
26
+ export const MCP_PROTOCOL_VERSION = '2024-11-05';
27
+ /** JSON-RPC 2.0 error codes (https://www.jsonrpc.org/specification). */
28
+ export const JSONRPC_ERRORS = {
29
+ PARSE_ERROR: -32700,
30
+ INVALID_REQUEST: -32600,
31
+ METHOD_NOT_FOUND: -32601,
32
+ INVALID_PARAMS: -32602,
33
+ INTERNAL_ERROR: -32603,
34
+ };
35
+ /**
36
+ * Parse one newline-delimited JSON line into a JSON-RPC message.
37
+ * Returns `{ ok: true, msg }` on success or `{ ok: false, error }` so the
38
+ * caller can emit the canonical -32700 parse-error response. Blank lines
39
+ * are reported as ok=false with no error — they're legal stdio framing
40
+ * noise, not protocol violations.
41
+ */
42
+ export function parseLine(line) {
43
+ const trimmed = line.trim();
44
+ if (trimmed.length === 0)
45
+ return { ok: false, error: null };
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(trimmed);
49
+ }
50
+ catch (e) {
51
+ return { ok: false, error: e.message };
52
+ }
53
+ if (!parsed || typeof parsed !== 'object')
54
+ return { ok: false, error: 'top-level not an object' };
55
+ const obj = parsed;
56
+ if (obj.jsonrpc !== '2.0')
57
+ return { ok: false, error: 'missing or wrong jsonrpc version' };
58
+ if (typeof obj.method !== 'string')
59
+ return { ok: false, error: 'method must be a string' };
60
+ return { ok: true, msg: obj };
61
+ }
62
+ /**
63
+ * Shape a successful response. `id` is echoed from the request.
64
+ */
65
+ export function successResponse(id, result) {
66
+ return { jsonrpc: '2.0', id, result };
67
+ }
68
+ /**
69
+ * Shape an error response with a JSON-RPC error code + message. When the
70
+ * originating message was unparseable (no id extractable), pass `null`.
71
+ */
72
+ export function errorResponse(id, code, message, data) {
73
+ const err = { code, message };
74
+ if (data !== undefined)
75
+ err.data = data;
76
+ return { jsonrpc: '2.0', id, error: err };
77
+ }
78
+ /** Encode a response as a newline-terminated string ready for stdout. */
79
+ export function encodeMessage(msg) {
80
+ return JSON.stringify(msg) + '\n';
81
+ }
82
+ /**
83
+ * Core request-dispatch routine. Pure: given a parsed message + a tool
84
+ * registry + server identity, returns the JSON-RPC response (or null for
85
+ * a notification that expects no reply).
86
+ */
87
+ export async function handleMessage(msg, tools, server) {
88
+ const isNotification = !('id' in msg) || msg.id === undefined;
89
+ const id = isNotification ? null : msg.id;
90
+ // Notifications — no response expected. Only handle the ones we care about;
91
+ // silently ignore others (per JSON-RPC spec).
92
+ if (isNotification) {
93
+ return null;
94
+ }
95
+ const reqId = id;
96
+ switch (msg.method) {
97
+ case 'initialize':
98
+ return successResponse(reqId, {
99
+ protocolVersion: MCP_PROTOCOL_VERSION,
100
+ capabilities: { tools: {} },
101
+ serverInfo: server,
102
+ });
103
+ case 'tools/list':
104
+ return successResponse(reqId, {
105
+ tools: tools.map((t) => ({
106
+ name: t.name,
107
+ description: t.description,
108
+ inputSchema: t.inputSchema,
109
+ })),
110
+ });
111
+ case 'tools/call': {
112
+ const params = (msg.params ?? {});
113
+ if (typeof params.name !== 'string') {
114
+ return errorResponse(reqId, JSONRPC_ERRORS.INVALID_PARAMS, 'tools/call requires string `name`');
115
+ }
116
+ const tool = tools.find((t) => t.name === params.name);
117
+ if (!tool) {
118
+ return errorResponse(reqId, JSONRPC_ERRORS.METHOD_NOT_FOUND, `unknown tool: ${params.name}`);
119
+ }
120
+ const argsVal = params.arguments;
121
+ const args = (argsVal && typeof argsVal === 'object' && !Array.isArray(argsVal))
122
+ ? argsVal
123
+ : {};
124
+ try {
125
+ const result = await tool.handler(args);
126
+ return successResponse(reqId, result);
127
+ }
128
+ catch (err) {
129
+ return errorResponse(reqId, JSONRPC_ERRORS.INTERNAL_ERROR, `tool handler threw: ${err.message}`);
130
+ }
131
+ }
132
+ default:
133
+ return errorResponse(reqId, JSONRPC_ERRORS.METHOD_NOT_FOUND, `unknown method: ${msg.method}`);
134
+ }
135
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * MCP server runtime — bridges newline-delimited JSON-RPC on stdio to the
3
+ * pure `handleMessage` dispatcher in `./protocol.ts`. Everything stateful
4
+ * (stream I/O, serialization, logging) lives here; the protocol module
5
+ * stays pure so its tests don't need fake streams.
6
+ *
7
+ * Why serial: for await (const line of rl) processes one line at a time
8
+ * and we await the handler before reading the next. MCP clients tolerate
9
+ * both ordered and out-of-order responses, but ordered keeps the stdio
10
+ * frame sequence deterministic, avoids interleaving partial writes, and
11
+ * — the boring reason — matches what the tests can easily assert on.
12
+ *
13
+ * Why `runMcpServer` takes injectable streams: makes the event loop
14
+ * testable end-to-end with a PassThrough pair, no child process needed.
15
+ */
16
+ import { type McpTool, type ServerInfo } from './protocol.js';
17
+ export interface RunServerOptions {
18
+ tools: McpTool[];
19
+ server: ServerInfo;
20
+ /** Stream of newline-delimited JSON-RPC messages. Defaults to `process.stdin`. */
21
+ stdin?: NodeJS.ReadableStream;
22
+ /** Sink for newline-delimited JSON-RPC responses. Defaults to `process.stdout`. */
23
+ stdout?: NodeJS.WritableStream;
24
+ /** Diagnostic channel. Defaults to `process.stderr`. */
25
+ stderr?: NodeJS.WritableStream;
26
+ /** Hook fired when a handler throws unexpectedly — primarily for tests. */
27
+ onError?: (err: unknown, line: string) => void;
28
+ }
29
+ export declare function runMcpServer(opts: RunServerOptions): Promise<void>;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * MCP server runtime — bridges newline-delimited JSON-RPC on stdio to the
3
+ * pure `handleMessage` dispatcher in `./protocol.ts`. Everything stateful
4
+ * (stream I/O, serialization, logging) lives here; the protocol module
5
+ * stays pure so its tests don't need fake streams.
6
+ *
7
+ * Why serial: for await (const line of rl) processes one line at a time
8
+ * and we await the handler before reading the next. MCP clients tolerate
9
+ * both ordered and out-of-order responses, but ordered keeps the stdio
10
+ * frame sequence deterministic, avoids interleaving partial writes, and
11
+ * — the boring reason — matches what the tests can easily assert on.
12
+ *
13
+ * Why `runMcpServer` takes injectable streams: makes the event loop
14
+ * testable end-to-end with a PassThrough pair, no child process needed.
15
+ */
16
+ import { createInterface } from 'node:readline';
17
+ import { handleMessage, parseLine, errorResponse, encodeMessage, JSONRPC_ERRORS, } from './protocol.js';
18
+ /**
19
+ * Back-pressure-aware line write. Using the callback form of
20
+ * `stream.write` means we wait for the chunk to drain before proceeding —
21
+ * on a slow stdout consumer that prevents us from buffering unboundedly.
22
+ */
23
+ function writeLine(stream, chunk) {
24
+ return new Promise((resolve, reject) => {
25
+ stream.write(chunk, (err) => (err ? reject(err) : resolve()));
26
+ });
27
+ }
28
+ export async function runMcpServer(opts) {
29
+ const stdin = opts.stdin ?? process.stdin;
30
+ const stdout = opts.stdout ?? process.stdout;
31
+ const stderr = opts.stderr ?? process.stderr;
32
+ const rl = createInterface({ input: stdin, crlfDelay: Infinity });
33
+ for await (const line of rl) {
34
+ const parsed = parseLine(line);
35
+ if (!parsed.ok) {
36
+ // Blank lines are legal framing noise, not errors — skip silently.
37
+ if (parsed.error === null)
38
+ continue;
39
+ await writeLine(stdout, encodeMessage(errorResponse(null, JSONRPC_ERRORS.PARSE_ERROR, `parse error: ${parsed.error}`)));
40
+ continue;
41
+ }
42
+ try {
43
+ const response = await handleMessage(parsed.msg, opts.tools, opts.server);
44
+ if (response !== null) {
45
+ await writeLine(stdout, encodeMessage(response));
46
+ }
47
+ }
48
+ catch (err) {
49
+ // `handleMessage` already wraps tool-handler errors into -32603 responses;
50
+ // anything reaching here is a bug in the dispatcher itself. Still, don't
51
+ // let it crash the server — emit a synthetic error response and log.
52
+ const message = err?.message ?? 'internal error';
53
+ const id = 'id' in parsed.msg && parsed.msg.id !== undefined
54
+ ? parsed.msg.id
55
+ : null;
56
+ if (opts.onError)
57
+ opts.onError(err, line);
58
+ else
59
+ stderr.write(`[dario mcp] unhandled: ${message}\n`);
60
+ await writeLine(stdout, encodeMessage(errorResponse(id, JSONRPC_ERRORS.INTERNAL_ERROR, message)));
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * MCP tool registry for the dario MCP server (v3.27, direction #4).
3
+ *
4
+ * Each tool wraps an existing dario subsystem that's already covered by
5
+ * its own tests, so these wrappers stay thin — fetch data, format it as
6
+ * text content, return. All tools are read-only. Destructive operations
7
+ * (login/logout, accounts add/remove, backend add/remove, proxy start)
8
+ * are deliberately NOT exposed: an MCP client shouldn't be able to
9
+ * mutate the user's dario state just by being connected, same boundary
10
+ * as the sub-agent prompt (v3.26).
11
+ *
12
+ * `buildToolRegistry` is a factory so tests can inject fake backends for
13
+ * each dario subsystem. In production, `buildDefaultToolRegistry` wires
14
+ * up the real dynamic imports — the imports live inside the factory so
15
+ * `src/mcp/protocol.ts` stays a pure module, decoupled from any of
16
+ * dario's heavier code paths.
17
+ */
18
+ import type { McpTool } from './protocol.js';
19
+ /**
20
+ * Injectable data sources for the tool handlers. Production wiring in
21
+ * `buildDefaultToolRegistry` fills these in with the real dario imports;
22
+ * tests can substitute pure synthetic data to avoid touching network /
23
+ * filesystem / OAuth state.
24
+ */
25
+ export interface ToolDataSources {
26
+ doctor: () => Promise<Array<{
27
+ status: string;
28
+ label: string;
29
+ detail: string;
30
+ }>>;
31
+ status: () => Promise<{
32
+ authenticated: boolean;
33
+ status: string;
34
+ expiresIn?: string;
35
+ canRefresh?: boolean;
36
+ }>;
37
+ accounts: () => Promise<Array<{
38
+ alias: string;
39
+ expiresAt: number;
40
+ }>>;
41
+ backends: () => Promise<Array<{
42
+ name: string;
43
+ baseUrl: string;
44
+ model?: string;
45
+ }>>;
46
+ subagent: () => Promise<{
47
+ installed: boolean;
48
+ path: string;
49
+ fileVersion: string | null;
50
+ current: boolean;
51
+ agentsDirExists: boolean;
52
+ }>;
53
+ fingerprint: () => Promise<{
54
+ runtime: string;
55
+ runtimeVersion: string;
56
+ status: string;
57
+ detail: string;
58
+ templateSource: string;
59
+ templateSchema: number | null;
60
+ }>;
61
+ darioVersion: () => string;
62
+ }
63
+ export declare function buildToolRegistry(data: ToolDataSources): McpTool[];
64
+ /**
65
+ * Default production wiring — imports dario's real subsystems. Kept out
66
+ * of `buildToolRegistry` so the registry factory stays pure over its
67
+ * data sources (and the unit tests don't pay for dynamic imports).
68
+ */
69
+ export declare function buildDefaultToolRegistry(): Promise<McpTool[]>;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * MCP tool registry for the dario MCP server (v3.27, direction #4).
3
+ *
4
+ * Each tool wraps an existing dario subsystem that's already covered by
5
+ * its own tests, so these wrappers stay thin — fetch data, format it as
6
+ * text content, return. All tools are read-only. Destructive operations
7
+ * (login/logout, accounts add/remove, backend add/remove, proxy start)
8
+ * are deliberately NOT exposed: an MCP client shouldn't be able to
9
+ * mutate the user's dario state just by being connected, same boundary
10
+ * as the sub-agent prompt (v3.26).
11
+ *
12
+ * `buildToolRegistry` is a factory so tests can inject fake backends for
13
+ * each dario subsystem. In production, `buildDefaultToolRegistry` wires
14
+ * up the real dynamic imports — the imports live inside the factory so
15
+ * `src/mcp/protocol.ts` stays a pure module, decoupled from any of
16
+ * dario's heavier code paths.
17
+ */
18
+ function textResult(text, isError = false) {
19
+ return {
20
+ content: [{ type: 'text', text }],
21
+ ...(isError ? { isError: true } : {}),
22
+ };
23
+ }
24
+ export function buildToolRegistry(data) {
25
+ const emptyObjectSchema = { type: 'object', properties: {}, required: [] };
26
+ return [
27
+ {
28
+ name: 'doctor',
29
+ description: 'Run dario\'s health-report checks and return the formatted output. Covers: dario version, Node, platform, runtime TLS fingerprint, CC binary + compat, template source + drift, OAuth state, account pool, backends, CC sub-agent install. No side effects.',
30
+ inputSchema: emptyObjectSchema,
31
+ handler: async () => {
32
+ const checks = await data.doctor();
33
+ if (checks.length === 0)
34
+ return textResult('No checks produced output.');
35
+ const labelWidth = checks.reduce((n, c) => Math.max(n, c.label.length), 0);
36
+ const prefix = {
37
+ ok: '[ OK ]', warn: '[WARN]', fail: '[FAIL]', info: '[INFO]',
38
+ };
39
+ const lines = checks.map((c) => `${prefix[c.status] ?? '[????]'} ${c.label.padEnd(labelWidth)} ${c.detail}`);
40
+ const failed = checks.filter((c) => c.status === 'fail').length;
41
+ const warned = checks.filter((c) => c.status === 'warn').length;
42
+ const summary = `\n\nSummary: ${checks.length} checks — ${failed} fail, ${warned} warn`;
43
+ return textResult(lines.join('\n') + summary);
44
+ },
45
+ },
46
+ {
47
+ name: 'status',
48
+ description: 'Report the dario OAuth authentication status: whether credentials are present, valid, and when they expire. Read-only.',
49
+ inputSchema: emptyObjectSchema,
50
+ handler: async () => {
51
+ const s = await data.status();
52
+ if (!s.authenticated) {
53
+ const detail = s.status === 'none'
54
+ ? 'No credentials — run `dario login`.'
55
+ : s.status === 'expired' && s.canRefresh
56
+ ? 'Credentials expired but refreshable — run `dario refresh` or `dario proxy`.'
57
+ : `Not authenticated (status: ${s.status}).`;
58
+ return textResult(`Authenticated: no\n${detail}`);
59
+ }
60
+ return textResult(`Authenticated: yes\nStatus: ${s.status}\nExpires in: ${s.expiresIn ?? 'unknown'}`);
61
+ },
62
+ },
63
+ {
64
+ name: 'accounts_list',
65
+ description: 'List the accounts configured in dario\'s multi-account pool (~/.dario/accounts/). Returns alias + token expiry per account. Read-only.',
66
+ inputSchema: emptyObjectSchema,
67
+ handler: async () => {
68
+ const accounts = await data.accounts();
69
+ if (accounts.length === 0) {
70
+ return textResult('No pool accounts configured. dario runs in single-account mode from ~/.dario/credentials.json.');
71
+ }
72
+ const now = Date.now();
73
+ const lines = accounts.map((a) => {
74
+ const msLeft = Math.max(0, a.expiresAt - now);
75
+ const hours = Math.floor(msLeft / 3600000);
76
+ const mins = Math.floor((msLeft % 3600000) / 60000);
77
+ const expiry = msLeft > 0 ? `${hours}h ${mins}m` : 'expired';
78
+ return ` ${a.alias.padEnd(20)} token expires in ${expiry}`;
79
+ });
80
+ const note = accounts.length < 2
81
+ ? '\n\nPool mode activates at 2+ accounts — currently single-account.'
82
+ : '';
83
+ return textResult(`${accounts.length} account${accounts.length === 1 ? '' : 's'}:\n${lines.join('\n')}${note}`);
84
+ },
85
+ },
86
+ {
87
+ name: 'backends_list',
88
+ description: 'List configured OpenAI-compat backends (OpenAI, OpenRouter, Groq, LiteLLM, Ollama, etc.). Read-only — does not expose API keys.',
89
+ inputSchema: emptyObjectSchema,
90
+ handler: async () => {
91
+ const backends = await data.backends();
92
+ if (backends.length === 0) {
93
+ return textResult('No OpenAI-compat backends configured. Claude subscription is the only route.');
94
+ }
95
+ const lines = backends.map((b) => ` ${b.name.padEnd(20)} ${b.baseUrl}${b.model ? ` (default model: ${b.model})` : ''}`);
96
+ return textResult(`${backends.length} backend${backends.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
97
+ },
98
+ },
99
+ {
100
+ name: 'subagent_status',
101
+ description: 'Report whether the dario CC sub-agent (~/.claude/agents/dario.md) is installed and whether it matches the running dario version. Read-only.',
102
+ inputSchema: emptyObjectSchema,
103
+ handler: async () => {
104
+ const s = await data.subagent();
105
+ const lines = [];
106
+ lines.push(`Path: ${s.path}`);
107
+ lines.push(`~/.claude/agents exists: ${s.agentsDirExists ? 'yes' : 'no'}`);
108
+ lines.push(`Installed: ${s.installed ? `yes (v${s.fileVersion ?? 'unknown'})` : 'no'}`);
109
+ if (s.installed && !s.current) {
110
+ lines.push('Note: file version does not match running dario — run `dario subagent install` to refresh.');
111
+ }
112
+ if (!s.installed && s.agentsDirExists) {
113
+ lines.push('Install with: `dario subagent install`.');
114
+ }
115
+ return textResult(lines.join('\n'));
116
+ },
117
+ },
118
+ {
119
+ name: 'fingerprint_info',
120
+ description: 'Report dario\'s runtime / TLS fingerprint state: whether the proxy is running under Bun (matches CC\'s TLS stack) or Node (diverges), which template source is active (live-captured vs bundled), and the template schema version. Read-only.',
121
+ inputSchema: emptyObjectSchema,
122
+ handler: async () => {
123
+ const f = await data.fingerprint();
124
+ const lines = [];
125
+ lines.push(`Runtime: ${f.runtime} ${f.runtimeVersion}`);
126
+ lines.push(`TLS status: ${f.status}`);
127
+ lines.push(`TLS detail: ${f.detail}`);
128
+ lines.push(`Template source: ${f.templateSource}`);
129
+ lines.push(`Template schema: v${f.templateSchema ?? '?'}`);
130
+ lines.push(`dario version: ${data.darioVersion()}`);
131
+ return textResult(lines.join('\n'));
132
+ },
133
+ },
134
+ ];
135
+ }
136
+ /**
137
+ * Default production wiring — imports dario's real subsystems. Kept out
138
+ * of `buildToolRegistry` so the registry factory stays pure over its
139
+ * data sources (and the unit tests don't pay for dynamic imports).
140
+ */
141
+ export async function buildDefaultToolRegistry() {
142
+ const [doctorMod, oauthMod, accountsMod, backendMod, subagentMod, runtimeMod, templateMod, pkgVersion] = await Promise.all([
143
+ import('../doctor.js'),
144
+ import('../oauth.js'),
145
+ import('../accounts.js'),
146
+ import('../openai-backend.js'),
147
+ import('../subagent.js'),
148
+ import('../runtime-fingerprint.js'),
149
+ import('../cc-template.js'),
150
+ readDarioVersion(),
151
+ ]);
152
+ return buildToolRegistry({
153
+ doctor: async () => {
154
+ const checks = await doctorMod.runChecks();
155
+ return checks.map((c) => ({
156
+ status: c.status,
157
+ label: c.label,
158
+ detail: c.detail,
159
+ }));
160
+ },
161
+ status: async () => oauthMod.getStatus(),
162
+ accounts: async () => {
163
+ const loaded = await accountsMod.loadAllAccounts();
164
+ return loaded.map((a) => ({
165
+ alias: a.alias,
166
+ expiresAt: a.expiresAt,
167
+ }));
168
+ },
169
+ backends: async () => {
170
+ const backends = await backendMod.listBackends();
171
+ return backends.map((b) => ({
172
+ name: b.name,
173
+ baseUrl: b.baseUrl,
174
+ model: b.model,
175
+ }));
176
+ },
177
+ subagent: async () => subagentMod.loadSubagentStatus(),
178
+ fingerprint: async () => {
179
+ const rt = runtimeMod.detectRuntimeFingerprint();
180
+ const tmpl = templateMod.CC_TEMPLATE;
181
+ return {
182
+ runtime: rt.runtime,
183
+ runtimeVersion: rt.runtimeVersion,
184
+ status: rt.status,
185
+ detail: rt.detail,
186
+ templateSource: tmpl._source ?? 'unknown',
187
+ templateSchema: tmpl._schemaVersion ?? null,
188
+ };
189
+ },
190
+ darioVersion: () => pkgVersion,
191
+ });
192
+ }
193
+ async function readDarioVersion() {
194
+ try {
195
+ const { readFileSync } = await import('node:fs');
196
+ const { fileURLToPath } = await import('node:url');
197
+ const { dirname, join } = await import('node:path');
198
+ const here = dirname(fileURLToPath(import.meta.url));
199
+ const pkg = JSON.parse(readFileSync(join(here, '..', '..', 'package.json'), 'utf-8'));
200
+ return typeof pkg.version === 'string' ? pkg.version : 'unknown';
201
+ }
202
+ catch {
203
+ return 'unknown';
204
+ }
205
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * CC sub-agent hook (v3.26, direction #2).
3
+ *
4
+ * Claude Code reads sub-agent definitions from `~/.claude/agents/*.md` —
5
+ * a YAML-frontmatter markdown file that exposes a tool-scoped prompt
6
+ * context CC can delegate work into. Installing a "dario" sub-agent
7
+ * gives the user (or CC itself, via the Task tool) a named handle to
8
+ * delegate dario operations into: refreshing the baked template from
9
+ * a live capture, checking proxy health, listing pool / backend state.
10
+ *
11
+ * The sub-agent runs with Bash + Read tool access only — it can invoke
12
+ * the `dario` CLI to produce reports, but it cannot modify dario state
13
+ * (accounts add/remove, backend configuration, etc.) without the user
14
+ * explicitly running those commands in their own session. That boundary
15
+ * is baked into the prompt so the sub-agent doesn't accidentally take
16
+ * destructive actions on the user's behalf.
17
+ *
18
+ * The file content is versioned via an inline `dario-sub-agent-version:`
19
+ * marker so a later release can detect stale installations (same axis as
20
+ * the `_schemaVersion` check on the live-fingerprint cache). `readStatus`
21
+ * is pure over `(fileExists, fileBody)` so the tests exercise every
22
+ * branch without touching the filesystem.
23
+ */
24
+ export declare const SUBAGENT_NAME = "dario";
25
+ export declare const SUBAGENT_FILENAME = "dario.md";
26
+ /** `~/.claude/agents/dario.md`. */
27
+ export declare function getSubagentPath(): string;
28
+ /**
29
+ * Construct the full sub-agent file body for a given dario version. Pure
30
+ * function — the tests pin the output so a change to the content is a
31
+ * deliberate, diffable update.
32
+ */
33
+ export declare function buildSubagentFile(darioVersion: string): string;
34
+ export interface SubagentStatus {
35
+ installed: boolean;
36
+ path: string;
37
+ /** Parsed from the inline `dario-sub-agent-version:` marker when the file is present. */
38
+ fileVersion: string | null;
39
+ /** Whether the installed file matches the version currently being built. */
40
+ current: boolean;
41
+ /** Whether `~/.claude/agents/` exists (is CC installed / agents dir created?). */
42
+ agentsDirExists: boolean;
43
+ }
44
+ /**
45
+ * Pure status computation over (fileExists, fileBody, currentVersion).
46
+ * Separated from `loadStatus` so tests can feed synthetic bodies and
47
+ * exercise every branch without the filesystem.
48
+ */
49
+ export declare function computeSubagentStatus(path: string, fileExists: boolean, fileBody: string | null, agentsDirExists: boolean, currentVersion: string): SubagentStatus;
50
+ /**
51
+ * Read the current on-disk status. Safe to call whether or not
52
+ * `~/.claude/` exists; a missing directory is reported via
53
+ * `agentsDirExists: false` so the caller can decide whether to create it
54
+ * (install) or just skip (status).
55
+ */
56
+ export declare function loadSubagentStatus(): SubagentStatus;
57
+ /**
58
+ * Install or refresh the sub-agent. Creates `~/.claude/agents/` if it
59
+ * doesn't exist. Returns what happened so the CLI can log accurately.
60
+ */
61
+ export declare function installSubagent(): {
62
+ path: string;
63
+ action: 'created' | 'updated' | 'unchanged';
64
+ version: string;
65
+ };
66
+ /**
67
+ * Remove the sub-agent file. Idempotent — returns `{ removed: false }`
68
+ * if the file wasn't present. Does not remove the parent `~/.claude/agents/`
69
+ * directory even if it becomes empty (user may have other sub-agents).
70
+ */
71
+ export declare function removeSubagent(): {
72
+ path: string;
73
+ removed: boolean;
74
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * CC sub-agent hook (v3.26, direction #2).
3
+ *
4
+ * Claude Code reads sub-agent definitions from `~/.claude/agents/*.md` —
5
+ * a YAML-frontmatter markdown file that exposes a tool-scoped prompt
6
+ * context CC can delegate work into. Installing a "dario" sub-agent
7
+ * gives the user (or CC itself, via the Task tool) a named handle to
8
+ * delegate dario operations into: refreshing the baked template from
9
+ * a live capture, checking proxy health, listing pool / backend state.
10
+ *
11
+ * The sub-agent runs with Bash + Read tool access only — it can invoke
12
+ * the `dario` CLI to produce reports, but it cannot modify dario state
13
+ * (accounts add/remove, backend configuration, etc.) without the user
14
+ * explicitly running those commands in their own session. That boundary
15
+ * is baked into the prompt so the sub-agent doesn't accidentally take
16
+ * destructive actions on the user's behalf.
17
+ *
18
+ * The file content is versioned via an inline `dario-sub-agent-version:`
19
+ * marker so a later release can detect stale installations (same axis as
20
+ * the `_schemaVersion` check on the live-fingerprint cache). `readStatus`
21
+ * is pure over `(fileExists, fileBody)` so the tests exercise every
22
+ * branch without touching the filesystem.
23
+ */
24
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
25
+ import { homedir } from 'node:os';
26
+ import { join, dirname } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ export const SUBAGENT_NAME = 'dario';
30
+ export const SUBAGENT_FILENAME = `${SUBAGENT_NAME}.md`;
31
+ /** `~/.claude/agents/dario.md`. */
32
+ export function getSubagentPath() {
33
+ return join(homedir(), '.claude', 'agents', SUBAGENT_FILENAME);
34
+ }
35
+ /**
36
+ * Construct the full sub-agent file body for a given dario version. Pure
37
+ * function — the tests pin the output so a change to the content is a
38
+ * deliberate, diffable update.
39
+ */
40
+ export function buildSubagentFile(darioVersion) {
41
+ return `---
42
+ name: ${SUBAGENT_NAME}
43
+ description: Use this sub-agent for dario-related diagnostics and template-refresh operations. It can invoke the \`dario\` CLI (via Bash) to run a health report, refresh the baked CC request template from a live capture, or check the proxy's account pool and backend configuration. It will not modify dario state (credentials, accounts, backends) without explicit user authorization.
44
+ tools: Bash, Read
45
+ ---
46
+
47
+ <!-- dario-sub-agent-version: ${darioVersion} -->
48
+ <!-- managed by: dario subagent install / remove -->
49
+
50
+ You are **dario's integration sub-agent**. You have access to the \`dario\` CLI via the Bash tool. Your job is to help the user with dario-related diagnostics and refresh operations by running read-only or user-requested commands and summarizing the output.
51
+
52
+ ## What you can do (read-only — no user confirmation required)
53
+
54
+ - **\`dario doctor\`** — produce a health report covering Node / platform, runtime TLS fingerprint, CC binary + compatibility range, template source + drift, OAuth state, account pool, and backend configuration. Summarize any \`[WARN]\` or \`[FAIL]\` rows in plain language and suggest the fix hinted in the detail column.
55
+ - **\`dario status\`** — quick auth status (authenticated, expires in, claim).
56
+ - **\`dario accounts list\`** — list the configured account pool and per-account token expiry.
57
+ - **\`dario backend list\`** — list configured OpenAI-compat backends (OpenRouter, Groq, LiteLLM, etc.).
58
+ - **\`dario --version\`** — report the installed dario version.
59
+
60
+ ## What requires explicit user authorization first
61
+
62
+ Before invoking any of these, ask the user to confirm:
63
+
64
+ - \`dario login\` / \`dario refresh\` — mutates credentials.
65
+ - \`dario accounts add/remove\` — mutates the account pool.
66
+ - \`dario backend add/remove\` — mutates backend configuration.
67
+ - \`dario logout\` — deletes stored credentials.
68
+
69
+ ## What you should NOT do
70
+
71
+ - Do not run \`dario proxy\` — the proxy is a long-running server; invoking it from a sub-agent context would block the parent CC session indefinitely.
72
+ - Do not modify \`~/.dario/\` files directly (credentials.json, accounts/, backends.json). Use the CLI.
73
+ - Do not dump credentials, tokens, or bearer values in your output.
74
+
75
+ ## Style
76
+
77
+ - Lead with the headline answer (one line).
78
+ - For diagnostics, group findings by severity (FAIL → WARN → OK).
79
+ - When suggesting a fix, quote the exact command the user should run.
80
+ - Keep output concise — the user is delegating to you because they want a summary, not a transcript.
81
+ `;
82
+ }
83
+ /**
84
+ * Pure status computation over (fileExists, fileBody, currentVersion).
85
+ * Separated from `loadStatus` so tests can feed synthetic bodies and
86
+ * exercise every branch without the filesystem.
87
+ */
88
+ export function computeSubagentStatus(path, fileExists, fileBody, agentsDirExists, currentVersion) {
89
+ if (!fileExists || fileBody === null) {
90
+ return { installed: false, path, fileVersion: null, current: false, agentsDirExists };
91
+ }
92
+ const m = /<!-- dario-sub-agent-version: ([^ ]+) -->/.exec(fileBody);
93
+ const fileVersion = m ? m[1] : null;
94
+ const current = fileVersion === currentVersion;
95
+ return { installed: true, path, fileVersion, current, agentsDirExists };
96
+ }
97
+ /**
98
+ * Read the current on-disk status. Safe to call whether or not
99
+ * `~/.claude/` exists; a missing directory is reported via
100
+ * `agentsDirExists: false` so the caller can decide whether to create it
101
+ * (install) or just skip (status).
102
+ */
103
+ export function loadSubagentStatus() {
104
+ const path = getSubagentPath();
105
+ const agentsDir = dirname(path);
106
+ const agentsDirExists = existsSync(agentsDir);
107
+ const fileExists = existsSync(path);
108
+ let fileBody = null;
109
+ if (fileExists) {
110
+ try {
111
+ fileBody = readFileSync(path, 'utf-8');
112
+ }
113
+ catch {
114
+ fileBody = null;
115
+ }
116
+ }
117
+ return computeSubagentStatus(path, fileExists, fileBody, agentsDirExists, currentDarioVersion());
118
+ }
119
+ /**
120
+ * Install or refresh the sub-agent. Creates `~/.claude/agents/` if it
121
+ * doesn't exist. Returns what happened so the CLI can log accurately.
122
+ */
123
+ export function installSubagent() {
124
+ const path = getSubagentPath();
125
+ const agentsDir = dirname(path);
126
+ if (!existsSync(agentsDir)) {
127
+ mkdirSync(agentsDir, { recursive: true });
128
+ }
129
+ const version = currentDarioVersion();
130
+ const desired = buildSubagentFile(version);
131
+ let existingBody = null;
132
+ const exists = existsSync(path);
133
+ if (exists) {
134
+ try {
135
+ existingBody = readFileSync(path, 'utf-8');
136
+ }
137
+ catch {
138
+ existingBody = null;
139
+ }
140
+ }
141
+ if (existingBody === desired) {
142
+ return { path, action: 'unchanged', version };
143
+ }
144
+ writeFileSync(path, desired, 'utf-8');
145
+ return { path, action: exists ? 'updated' : 'created', version };
146
+ }
147
+ /**
148
+ * Remove the sub-agent file. Idempotent — returns `{ removed: false }`
149
+ * if the file wasn't present. Does not remove the parent `~/.claude/agents/`
150
+ * directory even if it becomes empty (user may have other sub-agents).
151
+ */
152
+ export function removeSubagent() {
153
+ const path = getSubagentPath();
154
+ if (!existsSync(path))
155
+ return { path, removed: false };
156
+ unlinkSync(path);
157
+ return { path, removed: true };
158
+ }
159
+ function currentDarioVersion() {
160
+ try {
161
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
162
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
163
+ }
164
+ catch {
165
+ return '0.0.0';
166
+ }
167
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askalf/dario",
3
- "version": "3.25.0",
3
+ "version": "3.27.0",
4
4
  "description": "A local LLM router. One endpoint, every provider — Claude subscriptions, OpenAI, OpenRouter, Groq, local LiteLLM, any OpenAI-compat endpoint — your tools don't need to change.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc && cp src/cc-template-data.json dist/ && node -e \"require('fs').mkdirSync('dist/shim',{recursive:true})\" && cp src/shim/runtime.cjs dist/shim/",
24
- "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
24
+ "test": "node test/issue-29-tool-translation.mjs && node test/hybrid-tools.mjs && node test/tool-schema-contract.mjs && node test/scrub-paths.mjs && node test/provider-prefix.mjs && node test/analytics-recording.mjs && node test/analytics-billing-bucket.mjs && node test/failover-429.mjs && node test/pool-sticky.mjs && node test/sealed-pool.mjs && node test/live-fingerprint.mjs && node test/shim-runtime.mjs && node test/shim-e2e.mjs && node test/proxy-header-order.mjs && node test/proxy-body-order.mjs && node test/runtime-fingerprint.mjs && node test/pacing.mjs && node test/stream-drain.mjs && node test/subagent.mjs && node test/mcp-protocol.mjs && node test/mcp-tools.mjs && node test/mcp-e2e.mjs && node test/drift-detection.mjs && node test/compat-range.mjs && node test/doctor-formatter.mjs && node test/atomic-write.mjs && node test/account-refresh-singleflight.mjs && node test/streaming-edge-cases.mjs && node test/client-detection.mjs && node test/manual-oauth-flow.mjs && node test/scrub-template.mjs",
25
25
  "audit": "npm audit --production --audit-level=high",
26
26
  "prepublishOnly": "npm run build",
27
27
  "start": "node dist/cli.js",