@druve/cli
v0.5.2
Published
Druve CLI — bring-your-own-key, locally-executed, license-gated AI agents. Provider setup, license activation, and local agent execution.
Maintainers
Readme
@druve/cli
Provider-agnostic CLI for Druve's bring-your-own-key, locally-executed, license-gated agents. A developer packages an agent; a customer runs it on their own machine against their own model-provider key. Druve's server never holds a provider API key: it only issues and checks licenses.
Runnable ESM, no build step. bin/druve.mjs runs directly on Node 18+.
Full spec + rationale: ../webapp/PROGRESS.md ("ARCHITECTURE PIVOT") and
../BLUEPRINT.md §6.6.
Install
npm install -g @druve/cliLayout
| File | Role |
|---|---|
| bin/druve.mjs | CLI entrypoint, all commands |
| src/providers.mjs | getModelClient(config) shim + testProviderConnection (echo / anthropic / openai / openrouter / custom) |
| src/api.mjs | Thin fetch wrapper over the /api/license/* + /api/cli/* routes |
| src/storage.mjs | Local ~/.druve/ files (credentials, provider configs, licenses), all chmod 600 |
| src/mcp.mjs | MCP server (stdio) — exposes hired agents as tools to an MCP client |
| examples/echo-agent/index.mjs | Minimal agent (imports the shim by relative path) |
| examples/my-agent/index.js | Same, importing @druve/cli/providers, the shape a real dev ships |
Connect to Claude, ChatGPT, or any MCP client
druve mcp starts an MCP server over stdio, exposing this account's already-hired
agents as tools (list_hires, run_agent). It's a new front door onto the exact
same local execution druve run already does — same license file, same saved
provider key, nothing new trusted with anything.
Requires druve login once first. Then add this to your MCP client's config
(Claude Desktop's claude_desktop_config.json, Claude Code via claude mcp add,
or the equivalent for any other MCP client):
{
"mcpServers": {
"druve": {
"command": "npx",
"args": ["-y", "@druve/cli@latest", "mcp"]
}
}
}The agent contract
A dev-authored agent is one ESM file that exports handleMessage and imports
only the provider shim, never a specific SDK, so the same file runs on
whatever provider the customer picks. import '@druve/cli/providers' resolves via
Node's normal module resolution from the agent file's own location, so druve
needs to be a real dependency of the agent's project (npm install @druve/cli
there). Installing the CLI globally is not enough on its own for local
development, though druve run/druve quickstart vendor this automatically
for GitHub-hosted agents (see src/github.mjs):
import { getModelClient } from '@druve/cli/providers';
export async function handleMessage(input, providerConfig) {
const client = getModelClient(providerConfig);
return client.complete(input);
}Commands
Developer (never license-gated: your key, your machine)
druve dev test <agent-path> [input] run locally. Provider resolves from
DRUVE_DEV_PROVIDER (JSON) → ANTHROPIC_API_KEY
→ OPENAI_API_KEY → echo.
druve publish <agent-path> validate the agent, sha256 content-address it,
write a local bundle under ./dist/, and print a
druve:local:<hash> reference to paste into the
Build wizard's "Package source" field.Customer (license-gated)
druve quickstart <hire-id> <agent-ref>
the one-line path: login (if needed), pick +
save a provider, activate, and run a demo call,
all in one guided command. This is the exact
line the hire dashboard hands out, with both
args already filled in.
druve login browser device-code flow (RFC 8628). Stores a
session in ~/.druve/credentials.json.
druve setup <hire-id> pick + save this hire's model provider. Interactive,
or flags: --provider anthropic|openai|openrouter|custom|echo
--key <api-key> [--endpoint <url>] [--test]. Writes
~/.druve/providers/<hire-id>.json (never leaves this box).
`druve model <hire-id>` is an alias to reconfigure later.
druve activate <hire-id> claim the one license seat for this hire. Stores the
signed token in ~/.druve/licenses/<hire-id>.json.
druve run <hire-id> <agent> [input]
re-validate the license, then run the agent locally
with the saved provider credential. Every run checks
the license, so a revoked/moved seat stops working.
druve deactivate <hire-id> free the seat so you can activate on another device.Typical customer first run: druve quickstart <hire-id> <agent-ref> does all
of the below in one guided command. The manual, step-by-step equivalent is
druve login → druve setup <hire-id> → druve activate <hire-id> →
druve run <hire-id> <agent> "...".
Environment variables
| Var | Effect |
|---|---|
| DRUVE_API | API base URL (default: the live Druve site; set to http://localhost:3000 for local dev against next dev) |
| DRUVE_TOKEN | Supabase access token, overrides stored login (handy for scripting/CI) |
| DRUVE_DEV_PROVIDER | JSON provider config for dev test (highest priority) |
| ANTHROPIC_API_KEY / OPENAI_API_KEY | Auto-used by dev test if no DRUVE_DEV_PROVIDER |
| DRUVE_DEVICE_LABEL | Friendly device name shown on the hire dashboard at activate time |
Security model
- The provider credential lives only in
~/.druve/providers/<hire-id>.jsonon the customer's machine (chmod 600). It is never sent to Druve, only the license activate/validate/deactivate calls leave, carryingdevice_id+ token. loginuses the device-code grant: the token-retrievingdevice_codeis a 256-bit secret that never appears in a browser or URL; the browser only ever sees a short human-confirmeduser_code.runvalidates the license on every invocation, so it's a real license check, not a one-time unlock.
Not done yet (honest scope)
druve publishwrites a local content-addressed bundle. Uploading it to Druve storage + fetching the package atruntime needs a storage backend decision, not built. GitHub-hosted agents (github:owner/repo@sha:pathrefs, what the Build wizard produces from a pasted GitHub URL) work today and don't need this.- The hire dashboard's one-line install is
npx @druve/cli@latest start <code>, no separate installer script needed sincenpxfetches the published npm package directly. The@latestmatters: without it,npxcan cache an old semver-range resolution from a prior run on the same machine and silently stick there forever (0.x versions don't auto-upgrade across minors under a caret range) — cost a real debugging session to find. - Real provider clients (
anthropic/openai/…) insrc/providers.mjsare minimalfetchimplementations (no streaming/retries/model selection).echois fully functional and proves the install → license → run loop end to end.
