@askalf/dario 3.25.0 → 3.26.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,11 @@ 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
459
464
  dario doctor Print a health report: dario / Node / CC /
460
465
  template / drift / OAuth / pool / backends
461
466
 
@@ -574,6 +579,74 @@ async function shim() {
574
579
  process.exit(1);
575
580
  }
576
581
  }
582
+ async function subagent() {
583
+ const sub = args[1] ?? 'status';
584
+ const { installSubagent, removeSubagent, loadSubagentStatus, SUBAGENT_NAME } = await import('./subagent.js');
585
+ if (sub === 'install') {
586
+ const r = installSubagent();
587
+ console.log('');
588
+ console.log(' dario — Sub-agent install');
589
+ console.log(' ─────────────────────────');
590
+ console.log('');
591
+ if (r.action === 'unchanged') {
592
+ console.log(` Already up to date at ${r.path} (v${r.version}).`);
593
+ }
594
+ else {
595
+ console.log(` ${r.action === 'created' ? 'Installed' : 'Updated'} at ${r.path} (v${r.version}).`);
596
+ }
597
+ console.log('');
598
+ console.log(' Claude Code will pick up the new sub-agent on its next startup.');
599
+ console.log(` Invoke it from CC with: "Use the ${SUBAGENT_NAME} sub-agent to …"`);
600
+ console.log('');
601
+ return;
602
+ }
603
+ if (sub === 'remove' || sub === 'uninstall') {
604
+ const r = removeSubagent();
605
+ console.log('');
606
+ console.log(' dario — Sub-agent remove');
607
+ console.log(' ────────────────────────');
608
+ console.log('');
609
+ if (r.removed) {
610
+ console.log(` Removed ${r.path}.`);
611
+ }
612
+ else {
613
+ console.log(` Nothing to remove — ${r.path} was not present.`);
614
+ }
615
+ console.log('');
616
+ return;
617
+ }
618
+ if (sub === 'status') {
619
+ const s = loadSubagentStatus();
620
+ console.log('');
621
+ console.log(' dario — Sub-agent status');
622
+ console.log(' ────────────────────────');
623
+ console.log('');
624
+ console.log(` Path: ${s.path}`);
625
+ console.log(` ~/.claude/agents: ${s.agentsDirExists ? 'exists' : 'missing (Claude Code not installed?)'}`);
626
+ if (!s.installed) {
627
+ console.log(' Installed: no');
628
+ console.log('');
629
+ console.log(' Install with: dario subagent install');
630
+ }
631
+ else {
632
+ console.log(` Installed: yes (v${s.fileVersion ?? 'unknown'})`);
633
+ if (!s.current) {
634
+ console.log(' Note: file version does not match installed dario — run `dario subagent install` to refresh.');
635
+ }
636
+ }
637
+ console.log('');
638
+ return;
639
+ }
640
+ console.error('');
641
+ console.error(' Usage: dario subagent <install | remove | status>');
642
+ console.error('');
643
+ console.error(' install Write ~/.claude/agents/dario.md so Claude Code can');
644
+ console.error(' delegate dario diagnostics to a named sub-agent.');
645
+ console.error(' remove Remove the installed sub-agent file.');
646
+ console.error(' status Report whether the sub-agent is installed (default).');
647
+ console.error('');
648
+ process.exit(1);
649
+ }
577
650
  async function doctor() {
578
651
  const { runChecks, formatChecks, exitCodeFor } = await import('./doctor.js');
579
652
  console.log('');
@@ -612,6 +685,7 @@ const commands = {
612
685
  accounts,
613
686
  backend,
614
687
  shim,
688
+ subagent,
615
689
  doctor,
616
690
  help,
617
691
  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,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.26.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/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",