@askalf/dario 3.31.21 → 3.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/cc-oauth-detect.js +10 -0
- package/dist/cc-template-data.json +6 -6
- package/dist/cc-template.d.ts +25 -0
- package/dist/cc-template.js +96 -8
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +269 -3
- package/dist/doctor.js +69 -0
- package/dist/live-fingerprint.d.ts +1 -1
- package/dist/live-fingerprint.js +1 -1
- package/dist/mcp/tools.d.ts +28 -0
- package/dist/mcp/tools.js +104 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.js +191 -10
- package/dist/runtime-fingerprint.d.ts +26 -0
- package/dist/runtime-fingerprint.js +43 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -181,8 +181,19 @@ async function proxy() {
|
|
|
181
181
|
const passthrough = args.includes('--passthrough') || args.includes('--thin');
|
|
182
182
|
const preserveTools = args.includes('--preserve-tools') || args.includes('--keep-tools');
|
|
183
183
|
const hybridTools = args.includes('--hybrid-tools') || args.includes('--context-inject');
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
const mergeTools = args.includes('--merge-tools') || args.includes('--append-tools');
|
|
185
|
+
// The three modes shape the outbound `tools` array differently;
|
|
186
|
+
// combining any two would mean two different bodies. Caught here so
|
|
187
|
+
// the operator gets a clear error instead of one flag silently
|
|
188
|
+
// winning. startProxy enforces the same mutex defensively.
|
|
189
|
+
const toolModeCount = [preserveTools, hybridTools, mergeTools].filter(Boolean).length;
|
|
190
|
+
if (toolModeCount > 1) {
|
|
191
|
+
const picked = [
|
|
192
|
+
preserveTools && '--preserve-tools',
|
|
193
|
+
hybridTools && '--hybrid-tools',
|
|
194
|
+
mergeTools && '--merge-tools',
|
|
195
|
+
].filter(Boolean).join(', ');
|
|
196
|
+
console.error(`[dario] tool-routing flags are mutually exclusive. Pick one (got: ${picked}).`);
|
|
186
197
|
process.exit(1);
|
|
187
198
|
}
|
|
188
199
|
// Opt-out for v3.19.3's text-tool-client auto-detection. Operators who
|
|
@@ -268,6 +279,17 @@ async function proxy() {
|
|
|
268
279
|
// the server side, so passing through a too-high value returns a clean
|
|
269
280
|
// 400 rather than silently accepting beyond-model-max.
|
|
270
281
|
const maxTokens = resolveMaxTokensFlag(args, process.env['DARIO_MAX_TOKENS']);
|
|
282
|
+
// --log-file <path> — append a one-line JSON record per completed
|
|
283
|
+
// request. Useful for backgrounded proxies where stdout is unobserved.
|
|
284
|
+
// Falls back to DARIO_LOG_FILE; off by default. Path is opened with
|
|
285
|
+
// append mode so multiple proxy restarts share a rolling history.
|
|
286
|
+
const logFile = parseLogFileFlag(args) ?? process.env['DARIO_LOG_FILE'] ?? undefined;
|
|
287
|
+
// --passthrough-betas=name1,name2 — operator-pinned beta allow-list.
|
|
288
|
+
// Names listed here are always forwarded to Anthropic regardless of
|
|
289
|
+
// CC's captured set or the client's own beta header; bypasses the
|
|
290
|
+
// billable-filter. Empty values are dropped. Falls back to
|
|
291
|
+
// DARIO_PASSTHROUGH_BETAS env var.
|
|
292
|
+
const passthroughBetas = parsePassthroughBetasFlag(args, process.env['DARIO_PASSTHROUGH_BETAS']);
|
|
271
293
|
// Non-loopback bind without DARIO_API_KEY turns dario into an open
|
|
272
294
|
// OAuth-subscription relay for anyone on the reachable network. Refuse
|
|
273
295
|
// to start rather than rely on the operator to read the startup banner.
|
|
@@ -287,7 +309,56 @@ async function proxy() {
|
|
|
287
309
|
console.error(`[dario] Override (not recommended): pass --unsafe-no-auth if you have out-of-band network controls and accept the risk.`);
|
|
288
310
|
process.exit(1);
|
|
289
311
|
}
|
|
290
|
-
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, effort, maxTokens });
|
|
312
|
+
await startProxy({ port, host, verbose, verboseBodies, model, passthrough, preserveTools, hybridTools, mergeTools, noAutoDetect, strictTls, pacingMinMs, pacingJitterMs, drainOnClose, sessionIdleRotateMs, sessionRotateJitterMs, sessionMaxAgeMs, sessionPerClient, preserveOrchestrationTags, noLiveCapture, strictTemplate, maxConcurrent, maxQueued, queueTimeoutMs, effort, maxTokens, logFile, passthroughBetas });
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Parse `--passthrough-betas=<csv>` (or the env-var fallback) into a
|
|
316
|
+
* deduped, trimmed list. The CLI flag wins over the env var when both
|
|
317
|
+
* are set — that's the convention every other dario flag uses.
|
|
318
|
+
*
|
|
319
|
+
* Edge cases:
|
|
320
|
+
* - `--passthrough-betas=` (explicit empty) → returns []. The
|
|
321
|
+
* operator typed an empty value; this is the documented "clear the
|
|
322
|
+
* env-default, run with no pinned betas" override.
|
|
323
|
+
* - flag missing entirely → falls back to envVar.
|
|
324
|
+
* - empty entries / whitespace-only entries / duplicates are dropped.
|
|
325
|
+
*/
|
|
326
|
+
export function parsePassthroughBetasFlag(args, envVar) {
|
|
327
|
+
const eqArg = args.find((a) => a.startsWith('--passthrough-betas='));
|
|
328
|
+
// When the flag is present at all (even with an empty value), it owns
|
|
329
|
+
// the result. Only fall back to the env var when the flag is absent.
|
|
330
|
+
const raw = eqArg !== undefined ? eqArg.slice('--passthrough-betas='.length) : envVar;
|
|
331
|
+
if (!raw)
|
|
332
|
+
return [];
|
|
333
|
+
const seen = new Set();
|
|
334
|
+
const out = [];
|
|
335
|
+
for (const piece of raw.split(',')) {
|
|
336
|
+
const trimmed = piece.trim();
|
|
337
|
+
if (trimmed.length > 0 && !seen.has(trimmed)) {
|
|
338
|
+
seen.add(trimmed);
|
|
339
|
+
out.push(trimmed);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Parse `--log-file=<path>` or `--log-file <path>`. Returns the path
|
|
346
|
+
* string when present, undefined otherwise. An empty path (e.g.
|
|
347
|
+
* `--log-file=`) is treated as unset so the env-var fallback can apply.
|
|
348
|
+
*/
|
|
349
|
+
function parseLogFileFlag(args) {
|
|
350
|
+
const eqArg = args.find(a => a.startsWith('--log-file='));
|
|
351
|
+
if (eqArg) {
|
|
352
|
+
const value = eqArg.slice('--log-file='.length);
|
|
353
|
+
return value.length > 0 ? value : undefined;
|
|
354
|
+
}
|
|
355
|
+
const idx = args.indexOf('--log-file');
|
|
356
|
+
if (idx >= 0 && idx + 1 < args.length) {
|
|
357
|
+
const value = args[idx + 1];
|
|
358
|
+
if (value && !value.startsWith('-'))
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
291
362
|
}
|
|
292
363
|
/**
|
|
293
364
|
* Parse `--max-tokens=<N|client>` + `DARIO_MAX_TOKENS` env (dario#88).
|
|
@@ -671,11 +742,30 @@ async function help() {
|
|
|
671
742
|
DARIO_API_KEY — with redacted previews and
|
|
672
743
|
a targeted diagnosis (dario#97 class). Use
|
|
673
744
|
--timeout-ms=N to adjust the 30s default.
|
|
745
|
+
dario doctor --bun-bootstrap
|
|
746
|
+
One-shot Bun installer. Closes the gap
|
|
747
|
+
between "doctor warned about Node-only TLS
|
|
748
|
+
fingerprint" and "Bun on PATH" without
|
|
749
|
+
copy-pasting a curl-to-shell line. Skips
|
|
750
|
+
when Bun is already installed. Pure
|
|
751
|
+
delegation to the official installer at
|
|
752
|
+
bun.com — dario does not vendor or pin a
|
|
753
|
+
Bun version.
|
|
674
754
|
dario config Print the effective configuration (port,
|
|
675
755
|
host, DARIO_API_KEY state, OAuth status,
|
|
676
756
|
pool, backends, paths) with credentials
|
|
677
757
|
redacted. Safe to paste into bug reports.
|
|
678
758
|
--json for structured output.
|
|
759
|
+
dario usage Burn-rate summary of the running proxy's
|
|
760
|
+
traffic (last 60 min): requests, token
|
|
761
|
+
totals, subscription % vs. extra-usage,
|
|
762
|
+
per-account rotation if pool mode is on.
|
|
763
|
+
Hits /analytics on the local proxy. Works
|
|
764
|
+
only when proxy is running; for a one-off
|
|
765
|
+
rate-limit snapshot from Anthropic, see
|
|
766
|
+
\`dario doctor --usage\`. --port=N to target
|
|
767
|
+
a non-default port; --json for the raw
|
|
768
|
+
/analytics payload.
|
|
679
769
|
dario upgrade npm install -g @askalf/dario@latest with a
|
|
680
770
|
pre-flight current-vs-latest check.
|
|
681
771
|
|
|
@@ -691,6 +781,14 @@ async function help() {
|
|
|
691
781
|
Loses subscription routing; use for custom agents
|
|
692
782
|
--hybrid-tools Remap to CC tools, inject sessionId/requestId/etc.
|
|
693
783
|
Keeps subscription routing for custom agents
|
|
784
|
+
--merge-tools Send CC's canonical tools first, append the
|
|
785
|
+
client's custom tools after (deduped by name).
|
|
786
|
+
Model can call either; tool calls flow back
|
|
787
|
+
unchanged. EXPERIMENTAL — Anthropic's billing
|
|
788
|
+
classifier may flip routing on the appended
|
|
789
|
+
tail. Validate with --verbose on the first
|
|
790
|
+
1-2 requests. Mutually exclusive with
|
|
791
|
+
--preserve-tools and --hybrid-tools.
|
|
694
792
|
--no-auto-detect Disable Cline/Kilo/Roo auto-preserve-tools
|
|
695
793
|
(v3.19.3 behavior). Keeps CC fingerprint
|
|
696
794
|
intact even when a text-tool client is
|
|
@@ -801,6 +899,20 @@ async function help() {
|
|
|
801
899
|
--verbose, -v Log all requests
|
|
802
900
|
--verbose=2, -vv Also dump redacted request bodies
|
|
803
901
|
(env: DARIO_LOG_BODIES=1)
|
|
902
|
+
--log-file=PATH Append one JSON-ND record per completed
|
|
903
|
+
request to PATH. Useful for backgrounded
|
|
904
|
+
proxies where stdout is unobserved (where
|
|
905
|
+
--verbose can't help). Secrets scrubbed,
|
|
906
|
+
no request bodies. Env: DARIO_LOG_FILE.
|
|
907
|
+
--passthrough-betas=CSV Beta flags to ALWAYS forward upstream
|
|
908
|
+
regardless of CC's captured set or the
|
|
909
|
+
client's anthropic-beta header. Bypasses
|
|
910
|
+
the billable-beta filter. Per-account
|
|
911
|
+
rejection cache still applies (so a flag
|
|
912
|
+
upstream 400's gets dropped, not retried
|
|
913
|
+
forever). Use when you know a beta works
|
|
914
|
+
on your account but isn't in the captured
|
|
915
|
+
template. Env: DARIO_PASSTHROUGH_BETAS.
|
|
804
916
|
|
|
805
917
|
Quick start:
|
|
806
918
|
dario login # auto-detects Claude Code credentials
|
|
@@ -982,6 +1094,53 @@ async function doctor() {
|
|
|
982
1094
|
const usage = args.includes('--usage');
|
|
983
1095
|
const asJson = args.includes('--json');
|
|
984
1096
|
const authCheck = args.includes('--auth-check');
|
|
1097
|
+
const bunBoot = args.includes('--bun-bootstrap');
|
|
1098
|
+
if (bunBoot) {
|
|
1099
|
+
// One-shot Bun installer. Closes the gap between "doctor warned
|
|
1100
|
+
// about Node-only TLS fingerprint" and "Bun is on PATH" without
|
|
1101
|
+
// making the user copy-paste a curl line from the README.
|
|
1102
|
+
// Probe first so we don't reinstall on a Bun-already-present host.
|
|
1103
|
+
const { probeBunVersion, bunBootstrap } = await import('./runtime-fingerprint.js');
|
|
1104
|
+
console.log('');
|
|
1105
|
+
console.log(' dario — Bun bootstrap');
|
|
1106
|
+
console.log(' ─────────────────────');
|
|
1107
|
+
console.log('');
|
|
1108
|
+
const existing = probeBunVersion();
|
|
1109
|
+
if (existing) {
|
|
1110
|
+
console.log(` Bun v${existing} already on PATH — nothing to install.`);
|
|
1111
|
+
console.log(' If dario is still running on Node, the auto-relaunch was bypassed (DARIO_NO_BUN set,');
|
|
1112
|
+
console.log(' or invoked through a wrapper that strips it). Re-run \`dario proxy\` directly.');
|
|
1113
|
+
console.log('');
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
console.log(' Bun is not on PATH. Running the official upstream installer:');
|
|
1117
|
+
console.log('');
|
|
1118
|
+
const result = await bunBootstrap();
|
|
1119
|
+
console.log('');
|
|
1120
|
+
if (result.exitCode === 0) {
|
|
1121
|
+
// Probe again — installer may write into a directory that the
|
|
1122
|
+
// current shell doesn't have on PATH yet (typical: ~/.bun/bin
|
|
1123
|
+
// appended to a profile that hasn't reloaded). We can't fix that
|
|
1124
|
+
// for the running shell; just call it out so the user knows what
|
|
1125
|
+
// to do next.
|
|
1126
|
+
const after = probeBunVersion();
|
|
1127
|
+
if (after) {
|
|
1128
|
+
console.log(` Bun v${after} installed. Re-run \`dario proxy\` to auto-relaunch under it.`);
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
console.log(' Installer reported success, but \`bun --version\` still fails from this shell.');
|
|
1132
|
+
console.log(' Open a new terminal (or source the profile the installer touched), then re-run');
|
|
1133
|
+
console.log(' \`dario doctor\` to confirm.');
|
|
1134
|
+
}
|
|
1135
|
+
console.log('');
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
console.error(` Installer exited with code ${result.exitCode}.`);
|
|
1139
|
+
console.error(` Manual fallback: ${result.runner}`);
|
|
1140
|
+
console.error(' Or visit https://bun.com for platform-specific instructions.');
|
|
1141
|
+
console.error('');
|
|
1142
|
+
process.exit(result.exitCode);
|
|
1143
|
+
}
|
|
985
1144
|
if (authCheck) {
|
|
986
1145
|
console.log('');
|
|
987
1146
|
console.log(' dario — Auth Check');
|
|
@@ -1133,6 +1292,112 @@ async function upgrade() {
|
|
|
1133
1292
|
console.log(' Upgrade complete. Run `dario --version` to confirm.');
|
|
1134
1293
|
console.log('');
|
|
1135
1294
|
}
|
|
1295
|
+
/**
|
|
1296
|
+
* `dario usage` — focused burn-rate summary of the running proxy's
|
|
1297
|
+
* traffic. Hits `/analytics` on the local proxy (default port 3456,
|
|
1298
|
+
* overridable with --port=N or DARIO_USAGE_PORT) and prints a
|
|
1299
|
+
* human-readable digest: requests in the last hour, token totals,
|
|
1300
|
+
* subscription % vs. extra-usage, per-account rotation if pool mode
|
|
1301
|
+
* is active.
|
|
1302
|
+
*
|
|
1303
|
+
* When the proxy isn't running on the expected port, prints a hint
|
|
1304
|
+
* pointing at `dario doctor --usage` (which fires a Haiku rate-limit
|
|
1305
|
+
* probe directly to Anthropic — different purpose, but the closest
|
|
1306
|
+
* substitute when there's no live proxy traffic to summarize).
|
|
1307
|
+
*
|
|
1308
|
+
* --json mode emits the raw /analytics payload for machine consumption
|
|
1309
|
+
* (CI dashboards, status bars, the MCP `usage` tool that wraps this).
|
|
1310
|
+
*/
|
|
1311
|
+
async function usage() {
|
|
1312
|
+
const portArg = args.find(a => a.startsWith('--port='));
|
|
1313
|
+
const port = portArg
|
|
1314
|
+
? parseInt(portArg.split('=')[1], 10)
|
|
1315
|
+
: process.env['DARIO_USAGE_PORT']
|
|
1316
|
+
? parseInt(process.env['DARIO_USAGE_PORT'], 10)
|
|
1317
|
+
: 3456;
|
|
1318
|
+
const asJson = args.includes('--json');
|
|
1319
|
+
const url = `http://127.0.0.1:${port}/analytics`;
|
|
1320
|
+
let payload = null;
|
|
1321
|
+
let connectError = null;
|
|
1322
|
+
try {
|
|
1323
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
1324
|
+
if (!res.ok) {
|
|
1325
|
+
connectError = `proxy responded ${res.status}`;
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
payload = await res.json();
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
catch (err) {
|
|
1332
|
+
connectError = err instanceof Error ? err.message : String(err);
|
|
1333
|
+
}
|
|
1334
|
+
if (asJson) {
|
|
1335
|
+
if (payload) {
|
|
1336
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
process.stdout.write(JSON.stringify({ error: 'proxy not reachable', port, detail: connectError }, null, 2) + '\n');
|
|
1340
|
+
process.exit(1);
|
|
1341
|
+
}
|
|
1342
|
+
console.log('');
|
|
1343
|
+
console.log(' dario — Usage');
|
|
1344
|
+
console.log(' ─────────────');
|
|
1345
|
+
console.log('');
|
|
1346
|
+
if (!payload) {
|
|
1347
|
+
console.log(` Proxy not reachable on http://127.0.0.1:${port} (${connectError ?? 'no response'}).`);
|
|
1348
|
+
console.log(' `dario usage` summarizes traffic from a running proxy (live history).');
|
|
1349
|
+
console.log(' For a one-off rate-limit snapshot from Anthropic, run:');
|
|
1350
|
+
console.log('');
|
|
1351
|
+
console.log(' dario doctor --usage');
|
|
1352
|
+
console.log('');
|
|
1353
|
+
console.log(' Costs ~1 subscription request; works without a running proxy.');
|
|
1354
|
+
console.log('');
|
|
1355
|
+
process.exit(1);
|
|
1356
|
+
}
|
|
1357
|
+
// Pool mode response shape:
|
|
1358
|
+
// { window: { minutes, requests, ...stats }, allTime: {...},
|
|
1359
|
+
// perAccount, perModel, utilization, predictions }
|
|
1360
|
+
// Single-account mode response shape:
|
|
1361
|
+
// { mode: 'single-account', note: '...' }
|
|
1362
|
+
if (payload.mode === 'single-account') {
|
|
1363
|
+
console.log(' Mode: single-account');
|
|
1364
|
+
console.log('');
|
|
1365
|
+
console.log(` ${payload.note}`);
|
|
1366
|
+
console.log('');
|
|
1367
|
+
console.log(' For a live snapshot of your subscription rate limit, run:');
|
|
1368
|
+
console.log(' dario doctor --usage');
|
|
1369
|
+
console.log('');
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const win = payload.window;
|
|
1373
|
+
const allTime = payload.allTime;
|
|
1374
|
+
const perAccount = payload.perAccount;
|
|
1375
|
+
console.log(' Mode: pool');
|
|
1376
|
+
console.log(` Window: last ${win?.minutes ?? 60} minutes`);
|
|
1377
|
+
console.log('');
|
|
1378
|
+
console.log(` Requests: ${win?.requests ?? 0}` + (allTime ? ` (all-time: ${allTime.requests ?? 0})` : ''));
|
|
1379
|
+
if (win && win.requests > 0) {
|
|
1380
|
+
console.log(` Input tokens: ${(win.totalInputTokens ?? 0).toLocaleString()}`);
|
|
1381
|
+
console.log(` Output tokens: ${(win.totalOutputTokens ?? 0).toLocaleString()}`);
|
|
1382
|
+
console.log(` Avg latency: ${win.avgLatencyMs ?? 0} ms`);
|
|
1383
|
+
if ((win.errorRate ?? 0) > 0) {
|
|
1384
|
+
console.log(` Error rate: ${((win.errorRate ?? 0) * 100).toFixed(1)}%`);
|
|
1385
|
+
}
|
|
1386
|
+
console.log(` Subscription %: ${win.subscriptionPercent ?? 0}%`);
|
|
1387
|
+
if ((win.estimatedCost ?? 0) > 0) {
|
|
1388
|
+
console.log(` Est. cost: $${(win.estimatedCost ?? 0).toFixed(4)} (would-be API cost)`);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (perAccount && Object.keys(perAccount).length > 0) {
|
|
1392
|
+
console.log('');
|
|
1393
|
+
console.log(' Per-account:');
|
|
1394
|
+
const aliasWidth = Math.max(...Object.keys(perAccount).map((a) => a.length));
|
|
1395
|
+
for (const [alias, stats] of Object.entries(perAccount)) {
|
|
1396
|
+
console.log(` ${alias.padEnd(aliasWidth)} ${stats.requests} req${stats.requests === 1 ? '' : 's'} (${stats.subscriptionPercent}% subscription)`);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
console.log('');
|
|
1400
|
+
}
|
|
1136
1401
|
// Main
|
|
1137
1402
|
const commands = {
|
|
1138
1403
|
login,
|
|
@@ -1148,6 +1413,7 @@ const commands = {
|
|
|
1148
1413
|
doctor,
|
|
1149
1414
|
config,
|
|
1150
1415
|
upgrade,
|
|
1416
|
+
usage,
|
|
1151
1417
|
help,
|
|
1152
1418
|
version,
|
|
1153
1419
|
'--help': help,
|
package/dist/doctor.js
CHANGED
|
@@ -210,6 +210,38 @@ export async function runChecks(opts = {}) {
|
|
|
210
210
|
catch (err) {
|
|
211
211
|
checks.push({ status: 'fail', label: 'Template', detail: `load failed: ${err.message}` });
|
|
212
212
|
}
|
|
213
|
+
// ---- Per-request overhead surfacing.
|
|
214
|
+
// The CC system prompt + tool definitions are injected into every
|
|
215
|
+
// non-passthrough request and dominate the input-token cost on small
|
|
216
|
+
// turns. Anthropic caches them after the first hit (cache_creation
|
|
217
|
+
// tokens on call 1, then cache_read on subsequent calls within the
|
|
218
|
+
// 5-min/1-hr TTL), but non-CC users routing heavy tooling get
|
|
219
|
+
// surprised by the first-request charge. Surface the size up front
|
|
220
|
+
// so they can plan.
|
|
221
|
+
//
|
|
222
|
+
// No token estimate — char counts and tool count are factual; the
|
|
223
|
+
// tokenizer ratio varies enough between prose and tool-schema JSON
|
|
224
|
+
// (compressible structural keys) that any single divisor is
|
|
225
|
+
// misleading. Operators who want the exact number can read it off
|
|
226
|
+
// their first request's `cache_creation_input_tokens` once the proxy
|
|
227
|
+
// is warm. `--usage` adds the live snapshot for those who want it.
|
|
228
|
+
try {
|
|
229
|
+
const promptChars = CC_TEMPLATE.system_prompt?.length ?? 0;
|
|
230
|
+
const toolCount = (CC_TEMPLATE.tools ?? []).length;
|
|
231
|
+
const toolChars = JSON.stringify(CC_TEMPLATE.tools ?? []).length;
|
|
232
|
+
if (promptChars > 0 || toolCount > 0) {
|
|
233
|
+
checks.push({
|
|
234
|
+
status: 'info',
|
|
235
|
+
label: 'Overhead',
|
|
236
|
+
detail: `${promptChars.toLocaleString()} chars system prompt + ${toolCount} tool defs ` +
|
|
237
|
+
`(${toolChars.toLocaleString()} chars JSON-serialized) injected per non-passthrough ` +
|
|
238
|
+
`request. Cached after first hit; read-cost only on subsequent calls within ` +
|
|
239
|
+
`the 5-min/1-hr TTL. Exact token count surfaces as cache_creation_input_tokens ` +
|
|
240
|
+
`on the first response (or run \`dario doctor --usage\`).`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch { /* don't let overhead reporting break the doctor */ }
|
|
213
245
|
// ---- Template drift
|
|
214
246
|
try {
|
|
215
247
|
const drift = detectDrift(CC_TEMPLATE);
|
|
@@ -429,6 +461,43 @@ export async function runChecks(opts = {}) {
|
|
|
429
461
|
(expired > 0 ? `, ${expired} expired` : '') +
|
|
430
462
|
(aliases.length < 2 ? ' (pool activates at 2+)' : ''),
|
|
431
463
|
});
|
|
464
|
+
// Next-account-in-rotation surfacing. The proxy's per-request
|
|
465
|
+
// selector picks by max headroom (with 7d_<family> per-model
|
|
466
|
+
// bucket considered when a request's model family is known);
|
|
467
|
+
// doctor doesn't know the next request's model so it reports
|
|
468
|
+
// the family-agnostic pick. That's still the right preview for
|
|
469
|
+
// operators wondering "if I send a request right now, which
|
|
470
|
+
// account gets it?" — it matches `pool.select()` with no family
|
|
471
|
+
// hint, the same call the proxy uses when no model is parsed
|
|
472
|
+
// yet (e.g. on misshapen requests). Bypassed when only one
|
|
473
|
+
// account is loaded since "rotation" doesn't apply.
|
|
474
|
+
if (aliases.length >= 2) {
|
|
475
|
+
try {
|
|
476
|
+
const { AccountPool } = await import('./pool.js');
|
|
477
|
+
const pool = new AccountPool();
|
|
478
|
+
for (const acc of loaded) {
|
|
479
|
+
pool.add(acc.alias, {
|
|
480
|
+
accessToken: acc.accessToken,
|
|
481
|
+
refreshToken: acc.refreshToken,
|
|
482
|
+
expiresAt: acc.expiresAt,
|
|
483
|
+
deviceId: acc.deviceId,
|
|
484
|
+
accountUuid: acc.accountUuid,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
const next = pool.select();
|
|
488
|
+
const ps = pool.status();
|
|
489
|
+
checks.push({
|
|
490
|
+
status: 'info',
|
|
491
|
+
label: 'Pool routing',
|
|
492
|
+
detail: next
|
|
493
|
+
? `next: ${next.alias} (max-headroom select; ${ps.healthy}/${ps.accounts} healthy)`
|
|
494
|
+
: `no eligible account — all rejected or near-expiry (${ps.exhausted}/${ps.accounts} exhausted)`,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
catch (err) {
|
|
498
|
+
checks.push({ status: 'warn', label: 'Pool routing', detail: `check failed: ${err.message}` });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
432
501
|
}
|
|
433
502
|
}
|
|
434
503
|
catch (err) {
|
|
@@ -282,7 +282,7 @@ export declare function _resetInstalledVersionProbeForTest(): void;
|
|
|
282
282
|
*/
|
|
283
283
|
export declare const SUPPORTED_CC_RANGE: {
|
|
284
284
|
readonly min: "1.0.0";
|
|
285
|
-
readonly maxTested: "2.1.
|
|
285
|
+
readonly maxTested: "2.1.122";
|
|
286
286
|
};
|
|
287
287
|
/**
|
|
288
288
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/live-fingerprint.js
CHANGED
|
@@ -777,7 +777,7 @@ export function _resetInstalledVersionProbeForTest() {
|
|
|
777
777
|
*/
|
|
778
778
|
export const SUPPORTED_CC_RANGE = {
|
|
779
779
|
min: '1.0.0',
|
|
780
|
-
maxTested: '2.1.
|
|
780
|
+
maxTested: '2.1.122',
|
|
781
781
|
};
|
|
782
782
|
/**
|
|
783
783
|
* Compare two dotted-numeric version strings. Returns negative if `a<b`,
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -22,6 +22,32 @@ import type { McpTool } from './protocol.js';
|
|
|
22
22
|
* tests can substitute pure synthetic data to avoid touching network /
|
|
23
23
|
* filesystem / OAuth state.
|
|
24
24
|
*/
|
|
25
|
+
/**
|
|
26
|
+
* Shape returned by the `usage` data source. Mirrors the public subset
|
|
27
|
+
* of /analytics — keeps the MCP tool decoupled from internal Analytics
|
|
28
|
+
* record fields. Single-account mode returns `{ mode: 'single-account' }`
|
|
29
|
+
* with no stats since Analytics only collects in pool mode.
|
|
30
|
+
*/
|
|
31
|
+
export interface UsageSummary {
|
|
32
|
+
mode: 'pool' | 'single-account';
|
|
33
|
+
reachable: boolean;
|
|
34
|
+
port?: number;
|
|
35
|
+
detail?: string;
|
|
36
|
+
window?: {
|
|
37
|
+
minutes: number;
|
|
38
|
+
requests: number;
|
|
39
|
+
totalInputTokens: number;
|
|
40
|
+
totalOutputTokens: number;
|
|
41
|
+
avgLatencyMs: number;
|
|
42
|
+
errorRate: number;
|
|
43
|
+
subscriptionPercent: number;
|
|
44
|
+
estimatedCost: number;
|
|
45
|
+
};
|
|
46
|
+
perAccount?: Record<string, {
|
|
47
|
+
requests: number;
|
|
48
|
+
subscriptionPercent: number;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
25
51
|
export interface ToolDataSources {
|
|
26
52
|
doctor: () => Promise<Array<{
|
|
27
53
|
status: string;
|
|
@@ -58,6 +84,8 @@ export interface ToolDataSources {
|
|
|
58
84
|
templateSource: string;
|
|
59
85
|
templateSchema: number | null;
|
|
60
86
|
}>;
|
|
87
|
+
/** Burn-rate / consumption summary; see UsageSummary for the shape. */
|
|
88
|
+
usage: () => Promise<UsageSummary>;
|
|
61
89
|
darioVersion: () => string;
|
|
62
90
|
}
|
|
63
91
|
export declare function buildToolRegistry(data: ToolDataSources): McpTool[];
|
package/dist/mcp/tools.js
CHANGED
|
@@ -131,6 +131,56 @@ export function buildToolRegistry(data) {
|
|
|
131
131
|
return textResult(lines.join('\n'));
|
|
132
132
|
},
|
|
133
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: 'usage',
|
|
136
|
+
description: 'Burn-rate summary of the running dario proxy\'s traffic over the last 60 minutes: requests, token totals, subscription % vs. extra-usage, per-account rotation if pool mode is on. Read-only — fetches /analytics from the local proxy. Returns a compact text summary; pair with the `dario usage --json` CLI for the full /analytics payload.',
|
|
137
|
+
inputSchema: emptyObjectSchema,
|
|
138
|
+
handler: async () => {
|
|
139
|
+
const u = await data.usage();
|
|
140
|
+
if (!u.reachable) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
lines.push(`Proxy not reachable on port ${u.port ?? 3456}.`);
|
|
143
|
+
if (u.detail)
|
|
144
|
+
lines.push(`Detail: ${u.detail}`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('`usage` summarizes traffic from a running proxy. Start `dario proxy`, then re-call.');
|
|
147
|
+
lines.push('For a one-off rate-limit snapshot, run `dario doctor --usage` (~1 subscription request).');
|
|
148
|
+
return textResult(lines.join('\n'), true);
|
|
149
|
+
}
|
|
150
|
+
if (u.mode === 'single-account') {
|
|
151
|
+
return textResult([
|
|
152
|
+
'Mode: single-account',
|
|
153
|
+
'',
|
|
154
|
+
'Analytics history is collected only in pool mode (2+ accounts in ~/.dario/accounts/).',
|
|
155
|
+
'For a one-off rate-limit snapshot from Anthropic, run `dario doctor --usage`.',
|
|
156
|
+
].join('\n'));
|
|
157
|
+
}
|
|
158
|
+
const lines = [];
|
|
159
|
+
const w = u.window;
|
|
160
|
+
lines.push('Mode: pool');
|
|
161
|
+
lines.push(`Window: last ${w?.minutes ?? 60} minutes`);
|
|
162
|
+
lines.push(`Requests: ${w?.requests ?? 0}`);
|
|
163
|
+
if (w && w.requests > 0) {
|
|
164
|
+
lines.push(`Input tokens: ${w.totalInputTokens.toLocaleString()}`);
|
|
165
|
+
lines.push(`Output tokens: ${w.totalOutputTokens.toLocaleString()}`);
|
|
166
|
+
lines.push(`Avg latency: ${w.avgLatencyMs} ms`);
|
|
167
|
+
if (w.errorRate > 0)
|
|
168
|
+
lines.push(`Error rate: ${(w.errorRate * 100).toFixed(1)}%`);
|
|
169
|
+
lines.push(`Subscription %: ${w.subscriptionPercent}%`);
|
|
170
|
+
if (w.estimatedCost > 0)
|
|
171
|
+
lines.push(`Est. cost: $${w.estimatedCost.toFixed(4)} (would-be API cost)`);
|
|
172
|
+
}
|
|
173
|
+
if (u.perAccount && Object.keys(u.perAccount).length > 0) {
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('Per-account:');
|
|
176
|
+
const aliasWidth = Math.max(...Object.keys(u.perAccount).map((a) => a.length));
|
|
177
|
+
for (const [alias, stats] of Object.entries(u.perAccount)) {
|
|
178
|
+
lines.push(` ${alias.padEnd(aliasWidth)} ${stats.requests} req${stats.requests === 1 ? '' : 's'} (${stats.subscriptionPercent}% subscription)`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return textResult(lines.join('\n'));
|
|
182
|
+
},
|
|
183
|
+
},
|
|
134
184
|
];
|
|
135
185
|
}
|
|
136
186
|
/**
|
|
@@ -187,9 +237,63 @@ export async function buildDefaultToolRegistry() {
|
|
|
187
237
|
templateSchema: tmpl._schemaVersion ?? null,
|
|
188
238
|
};
|
|
189
239
|
},
|
|
240
|
+
usage: async () => fetchUsage(),
|
|
190
241
|
darioVersion: () => pkgVersion,
|
|
191
242
|
});
|
|
192
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Fetch the local proxy's `/analytics` endpoint and shape it into the
|
|
246
|
+
* MCP-tool surface. Port resolution mirrors `dario usage`:
|
|
247
|
+
* DARIO_USAGE_PORT, then DARIO_PORT (proxy's own default-port env), then
|
|
248
|
+
* 3456. 3-second timeout — we don't block the MCP client on a slow
|
|
249
|
+
* proxy.
|
|
250
|
+
*
|
|
251
|
+
* Failure modes are returned as `{ reachable: false, detail }` rather
|
|
252
|
+
* than thrown, so the tool handler can present a helpful message
|
|
253
|
+
* instead of a generic protocol error.
|
|
254
|
+
*/
|
|
255
|
+
async function fetchUsage() {
|
|
256
|
+
const port = process.env.DARIO_USAGE_PORT
|
|
257
|
+
? parseInt(process.env.DARIO_USAGE_PORT, 10)
|
|
258
|
+
: process.env.DARIO_PORT
|
|
259
|
+
? parseInt(process.env.DARIO_PORT, 10)
|
|
260
|
+
: 3456;
|
|
261
|
+
try {
|
|
262
|
+
const res = await fetch(`http://127.0.0.1:${port}/analytics`, { signal: AbortSignal.timeout(3000) });
|
|
263
|
+
if (!res.ok) {
|
|
264
|
+
return { mode: 'pool', reachable: false, port, detail: `HTTP ${res.status}` };
|
|
265
|
+
}
|
|
266
|
+
const body = await res.json();
|
|
267
|
+
if (body.mode === 'single-account') {
|
|
268
|
+
return { mode: 'single-account', reachable: true, port };
|
|
269
|
+
}
|
|
270
|
+
const w = body.window;
|
|
271
|
+
return {
|
|
272
|
+
mode: 'pool',
|
|
273
|
+
reachable: true,
|
|
274
|
+
port,
|
|
275
|
+
window: {
|
|
276
|
+
minutes: w?.minutes ?? 60,
|
|
277
|
+
requests: w?.requests ?? 0,
|
|
278
|
+
totalInputTokens: w?.totalInputTokens ?? 0,
|
|
279
|
+
totalOutputTokens: w?.totalOutputTokens ?? 0,
|
|
280
|
+
avgLatencyMs: w?.avgLatencyMs ?? 0,
|
|
281
|
+
errorRate: w?.errorRate ?? 0,
|
|
282
|
+
subscriptionPercent: w?.subscriptionPercent ?? 0,
|
|
283
|
+
estimatedCost: w?.estimatedCost ?? 0,
|
|
284
|
+
},
|
|
285
|
+
perAccount: body.perAccount,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
return {
|
|
290
|
+
mode: 'pool',
|
|
291
|
+
reachable: false,
|
|
292
|
+
port,
|
|
293
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
193
297
|
async function readDarioVersion() {
|
|
194
298
|
try {
|
|
195
299
|
const { readFileSync } = await import('node:fs');
|