From ad632b31b2a87c94bab266d08964164a8a786c30 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Thu, 30 Apr 2026 17:46:14 -0400 Subject: [PATCH 1/2] Remove robotnet mcp subcommand and MCP client The hosted MCP server is being decommissioned. Drops: - src/commands/mcp.ts and src/mcp-client.ts (and the test file) - registerMcpCommand wiring in src/index.ts - buildAuthenticatedMcpClient + resolveMcpBearerToken - MCPError, MCP_TIMEOUT_MS, mcpBaseUrl in EndpointConfig - mcpResource on OAuthDiscovery and the MCP-origin resource_servers loop - ROBOTNET_MCP_BASE_URL env var, mcp_base_url JSON, DEFAULT_MCP_BASE_URL - "mcp" doctor endpoint check + mcp_resource line in oauth_discovery output - "mcp" keyword in package.json - MCP references in CLAUDE.md, AGENTS.md, CHANGELOG.md, version.ts comment Tests updated to match new types and resource fixtures. typecheck + 124 tests pass. Bundles in-flight rebrand changes for the same files; pure-rebrand-only files left unstaged for separate commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- AGENTS.md | 11 +- CHANGELOG.md | 19 ++-- CLAUDE.md | 19 ++-- package.json | 15 ++- src/auth/discovery.ts | 14 +-- src/auth/runtime.ts | 72 ++----------- src/commands/mcp.ts | 65 ----------- src/commands/shared.ts | 27 ++--- src/config.ts | 33 +++--- src/doctor.ts | 5 - src/endpoints.ts | 6 +- src/errors.ts | 28 ++--- src/index.ts | 10 +- src/mcp-client.ts | 156 --------------------------- src/retry.ts | 6 +- src/version.ts | 8 +- tests/discovery.test.ts | 9 -- tests/doctor.test.ts | 11 +- tests/helpers.ts | 24 ++--- tests/mcp-client.test.ts | 226 --------------------------------------- tests/runtime.test.ts | 5 +- 21 files changed, 97 insertions(+), 672 deletions(-) delete mode 100644 src/commands/mcp.ts delete mode 100644 src/mcp-client.ts delete mode 100644 tests/mcp-client.test.ts diff --git a/AGENTS.md b/AGENTS.md index c9fb3e7..694e5a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Guidance for AI coding agents (Claude Code, Cursor, Copilot, Aider, etc.) workin ## What this repo is -`@robotnetworks/robonet` — the first-party CLI for RoboNet. Written in TypeScript, distributed via npm and Homebrew, runs on Node.js 18+. +`@robotnetworks/robotnet` — the first-party CLI for RobotNet. Written in TypeScript, distributed via npm and Homebrew, runs on Node.js 18+. Docs: https://docs.robotnet.works/cli @@ -27,16 +27,15 @@ Before proposing a change as complete, always run `npm run typecheck` and `npm t - `src/auth/` — OAuth/PKCE/client credentials, token store - `src/daemon/` — background listener lifecycle - `src/realtime/` — WebSocket listener -- `src/mcp-client.ts` — JSON-RPC 2.0 MCP client - `src/output/` — human and JSON formatters -- `src/errors.ts` — `RoboNetCLIError` hierarchy -- `bin/robonet.js` — published entrypoint (loads `dist/index.js`) +- `src/errors.ts` — `RobotNetCLIError` hierarchy +- `bin/robotnet.js` — published entrypoint (loads `dist/index.js`) - `tests/*.test.ts` — unit tests, run with `node --import tsx --test` ## Conventions to follow - **TypeScript strictness**: fully typed signatures, no implicit `any`, `Literal` unions for enums. -- **Errors**: throw a subclass of `RoboNetCLIError` from command paths so the root handler formats them consistently. Do not `process.exit()` inside a command — throw instead. +- **Errors**: throw a subclass of `RobotNetCLIError` from command paths so the root handler formats them consistently. Do not `process.exit()` inside a command — throw instead. - **HTTP**: route all requests through `src/api/client.ts`. Do not add raw `fetch` calls in commands. - **Auth**: never log tokens, secrets, or authorization headers. The token store is the single source of truth for credentials. - **Output**: support both human output and `--json` for any command that prints data. Use the helpers in `src/output/`. @@ -71,5 +70,5 @@ Adding a new subcommand: ## Contact -- Issues & PRs: https://github.com/RobotNetworks/robonet-cli +- Issues & PRs: https://github.com/RobotNetworks/robotnet-cli - Email: nick@robotnet.works diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d87c7..fce6950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,22 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Set a `User-Agent` header on every outbound HTTP request from the API and - MCP clients. Format follows established CLI conventions: - `robonet-cli/ node/ -`. Helps +- Set a `User-Agent` header on every outbound HTTP request from the API + client. Format follows established CLI conventions: + `robotnet-cli/ node/ -`. Helps server-side logs identify CLI traffic for forensics and analytics bucketing. ### Changed -- Centralized version metadata in `src/version.ts`. Removed duplicated - `package.json` reads from `mcp-client.ts`. +- Centralized version metadata in `src/version.ts`. + +### Removed + +- The `robotnet mcp` subcommand and JSON-RPC MCP client. The hosted MCP + server has been decommissioned; use the REST API and WebSocket listener + instead. ## [0.1.0] - 2026-04-18 ### Added -- Initial public release of `@robotnetworks/robonet`. +- Initial public release of `@robotnetworks/robotnet`. - `robotnet` command with background daemon and realtime listener. - OAuth login (PKCE) and client-credentials flows. -- MCP client (JSON-RPC 2.0) and `doctor` diagnostic command. +- `doctor` diagnostic command. diff --git a/CLAUDE.md b/CLAUDE.md index e6116cc..08187f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) and other AI coding ## Project Overview -`@robotnetworks/robonet` is the first-party CLI for RoboNet — a communication network for AI agents. The CLI handles login, background listener daemons, realtime events over WebSocket, and MCP (Model Context Protocol) tool invocation against the RoboNet API. +`@robotnetworks/robotnet` is the first-party CLI for RobotNet — a communication network for AI agents. The CLI handles login, background listener daemons, and realtime events over WebSocket against the RobotNet API. Documentation: https://docs.robotnet.works/cli @@ -32,24 +32,23 @@ node --import tsx --test tests/token-store.test.ts npm run dev -- --help # Run the built CLI -node bin/robonet.js --help +node bin/robotnet.js --help ``` ## Architecture - `src/index.ts` — CLI entry point; wires up commander and registers each subcommand. -- `src/commands/` — one file per subcommand group (`agents`, `threads`, `mcp`, `login`, …). Each exports a `register*Command(program)` function. +- `src/commands/` — one file per subcommand group (`agents`, `threads`, `login`, …). Each exports a `register*Command(program)` function. - `src/api/` — REST API client and request/response models. - `src/auth/` — OAuth discovery, PKCE flow, client credentials, token persistence, session resolution. - `src/daemon/` — Background listener process lifecycle (spawn, status, stop, state file). - `src/realtime/` — WebSocket listener and event dispatch. -- `src/mcp-client.ts` — JSON-RPC 2.0 MCP client used by `robonet mcp`. -- `src/doctor.ts` — Diagnostic health checks surfaced by `robonet doctor`. +- `src/doctor.ts` — Diagnostic health checks surfaced by `robotnet doctor`. - `src/config.ts` — XDG-compliant config resolution (profiles). -- `src/errors.ts` — Typed error hierarchy (`RoboNetCLIError` and subclasses). +- `src/errors.ts` — Typed error hierarchy (`RobotNetCLIError` and subclasses). - `src/output/` — Formatters for human and JSON output. - `src/retry.ts`, `src/endpoints.ts` — Shared infra for retries and endpoint resolution. -- `bin/robonet.js` — Published entrypoint; loads `dist/index.js`. +- `bin/robotnet.js` — Published entrypoint; loads `dist/index.js`. - `tests/` — Node test runner tests (`*.test.ts`), run via `tsx`. ## Engineering Standards @@ -62,7 +61,7 @@ node bin/robonet.js --help **Code organization** - Business logic stays out of `src/index.ts`. Keep it in `src/commands/*.ts` or a dedicated module. - API calls go through `src/api/client.ts`, never ad-hoc `fetch` in a command. -- Errors thrown from CLI paths should extend `RoboNetCLIError` so the top-level handler can format them. +- Errors thrown from CLI paths should extend `RobotNetCLIError` so the top-level handler can format them. **Naming** - Classes: `PascalCase`; functions and variables: `camelCase`; constants: `SCREAMING_SNAKE_CASE`. @@ -78,11 +77,11 @@ node bin/robonet.js --help ## Contributing -- Open issues and PRs at https://github.com/RobotNetworks/robonet-cli +- Open issues and PRs at https://github.com/RobotNetworks/robotnet-cli - Contact: nick@robotnet.works ## Releases - Version is declared in `package.json` and read at runtime by `src/index.ts`. - `prepublishOnly` runs build + tests; `npm publish` does not need manual steps beyond bumping the version and updating `CHANGELOG.md`. -- Published to npm as `@robotnetworks/robonet` (public scoped package). +- Published to npm as `@robotnetworks/robotnet` (public scoped package). diff --git a/package.json b/package.json index c4a8733..65f7020 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,22 @@ { - "name": "@robotnetworks/robonet", + "name": "@robotnetworks/robotnet", "version": "0.1.11", - "description": "First-party CLI for RoboNet background and realtime workflows.", + "description": "First-party CLI for RobotNet background and realtime workflows.", "keywords": [ - "robonet", + "robotnet", "robotnetworks", "cli", "agents", - "ai", - "mcp" + "ai" ], "homepage": "https://docs.robotnet.works/cli", "bugs": { - "url": "https://github.com/RobotNetworks/robonet-cli/issues", + "url": "https://github.com/RobotNetworks/robotnet-cli/issues", "email": "nick@robotnet.works" }, "repository": { "type": "git", - "url": "git+https://github.com/RobotNetworks/robonet-cli.git" + "url": "git+https://github.com/RobotNetworks/robotnet-cli.git" }, "license": "MIT", "author": "RobotNetworks ", @@ -31,7 +30,7 @@ } }, "bin": { - "robonet": "./bin/robonet.js" + "robotnet": "./bin/robotnet.js" }, "engines": { "node": ">=18" diff --git a/src/auth/discovery.ts b/src/auth/discovery.ts index b16f716..8e703f1 100644 --- a/src/auth/discovery.ts +++ b/src/auth/discovery.ts @@ -1,12 +1,11 @@ import { DISCOVERY_TIMEOUT_MS, type EndpointConfig } from "../endpoints.js"; import { DiscoveryError } from "../errors.js"; -/** Resolved OAuth endpoints plus the resource identifiers for each RoboNet surface (API, MCP, WebSocket). */ +/** Resolved OAuth endpoints plus the resource identifiers for each RobotNet surface (API, WebSocket). */ export interface OAuthDiscovery { readonly authorizationEndpoint: string; readonly tokenEndpoint: string; readonly registrationEndpoint: string; - readonly mcpResource: string; readonly apiResource: string | null; readonly websocketResource: string | null; } @@ -91,9 +90,7 @@ export async function discoverOAuth( ); } - const mcpOrigin = origin(endpoints.mcpBaseUrl); const wsOrigin = origin(endpoints.websocketUrl); - let mcpResource: string | null = null; let websocketResource: string | null = null; const resourceServers = authBody.resource_servers; @@ -103,23 +100,16 @@ export async function discoverOAuth( const record = rs as Record; const resourceId = String(record.resource ?? "").replace(/\/+$/, ""); const resourceOrigin = origin(resourceId); - if (!mcpResource && resourceOrigin === mcpOrigin) { - mcpResource = resourceId; - } else if (!websocketResource && resourceOrigin === wsOrigin) { + if (!websocketResource && resourceOrigin === wsOrigin) { websocketResource = resourceId; } } } - if (!mcpResource) { - mcpResource = endpoints.mcpBaseUrl.replace(/\/+$/, ""); - } - return { authorizationEndpoint, tokenEndpoint, registrationEndpoint, - mcpResource, apiResource, websocketResource, }; diff --git a/src/auth/runtime.ts b/src/auth/runtime.ts index 10ba8f8..e937636 100644 --- a/src/auth/runtime.ts +++ b/src/auth/runtime.ts @@ -35,7 +35,7 @@ async function refreshOrInvalidate( if (err instanceof FatalAuthError) { deleteToken(tokenStorePath); throw new FatalAuthError( - `${err.message}. Stored RoboNet login has been cleared; run \`robonet login\` to re-authenticate.`, + `${err.message}. Stored RobotNet login has been cleared; run \`robotnet login\` to re-authenticate.`, ); } throw err; @@ -61,7 +61,7 @@ export async function resolveRuntimeSession(options: { if (stored && stored.authMode === "pkce" && stored.refreshToken) { if (clientId !== null && clientId !== stored.clientId) { throw new AuthenticationError( - "The provided client ID does not match the stored RoboNet login.", + "The provided client ID does not match the stored RobotNet login.", ); } return resolvePkceSession({ @@ -87,70 +87,12 @@ export async function resolveRuntimeSession(options: { throw new AuthenticationError( "Stored login metadata was created with client credentials. " + "Run the command again with --client-id and --client-secret, " + - "or use `robonet login` for browser-based OAuth.", + "or use `robotnet login` for browser-based OAuth.", ); } throw new AuthenticationError( - "No usable stored login found. Run `robonet login` first, " + - "or provide --client-id and --client-secret.", - ); -} - -/** - * Resolve a bearer token scoped to the MCP resource, refreshing a stored PKCE - * login or performing a client-credentials exchange as needed. Throws - * {@link AuthenticationError} if no usable credentials are found. - */ -export async function resolveMcpBearerToken(options: { - endpoints: EndpointConfig; - tokenStorePath: string; - clientId: string | null; - clientSecret: string | null; - scope?: string; -}): Promise { - const { endpoints, tokenStorePath, clientId, clientSecret, scope = DEFAULT_SCOPES } = options; - const discovery = await discoverOAuth(endpoints); - const stored = loadToken(tokenStorePath); - - if (stored && stored.authMode === "pkce" && stored.refreshToken) { - if (clientId !== null && clientId !== stored.clientId) { - throw new AuthenticationError( - "The provided client ID does not match the stored RoboNet login.", - ); - } - const { token, refreshToken } = await refreshOrInvalidate(tokenStorePath, () => - requestRefreshTokenExchange({ - tokenEndpoint: stored.tokenEndpoint, - clientId: stored.clientId, - refreshToken: stored.refreshToken!, - resource: discovery.mcpResource, - scope, - }), - ); - const refreshed = storedTokenFromPkceLogin({ - token, - tokenEndpoint: stored.tokenEndpoint, - clientId: stored.clientId, - refreshToken, - redirectUri: stored.redirectUri ?? "", - }); - saveToken(tokenStorePath, refreshed); - return token; - } - - if (clientId && clientSecret) { - return requestClientCredentialsToken({ - tokenEndpoint: discovery.tokenEndpoint, - clientId, - clientSecret, - resource: discovery.mcpResource, - scope, - }); - } - - throw new AuthenticationError( - "No usable stored login found for MCP access. Run `robonet login` first, " + + "No usable stored login found. Run `robotnet login` first, " + "or provide --client-id and --client-secret.", ); } @@ -174,7 +116,7 @@ export async function resolveApiBearerToken(options: { if (stored && stored.authMode === "pkce" && stored.refreshToken) { if (clientId !== null && clientId !== stored.clientId) { throw new AuthenticationError( - "The provided client ID does not match the stored RoboNet login.", + "The provided client ID does not match the stored RobotNet login.", ); } if (!isTokenExpired(stored)) { @@ -218,12 +160,12 @@ export async function resolveApiBearerToken(options: { throw new AuthenticationError( "Stored login metadata was created with client credentials. " + "Run the command again with --client-id and --client-secret, " + - "or use `robonet login` for browser-based OAuth.", + "or use `robotnet login` for browser-based OAuth.", ); } throw new AuthenticationError( - "No usable stored login found. Run `robonet login` first, " + + "No usable stored login found. Run `robotnet login` first, " + "or provide --client-id and --client-secret.", ); } diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts deleted file mode 100644 index e7de892..0000000 --- a/src/commands/mcp.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Command } from "commander"; - -import { loadConfig } from "../config.js"; -import { RoboNetCLIError } from "../errors.js"; -import { renderJson } from "../output/json-output.js"; -import { - buildAuthenticatedMcpClient, - jsonOption, - profileTitle, -} from "./shared.js"; - -export function registerMcpCommand(program: Command): void { - const mcpCmd = program - .command("mcp") - .description("Access the full MCP tool surface directly"); - - mcpCmd - .command("tools") - .description("List MCP tools exposed by the RoboNet server") - .addOption(jsonOption()) - .action(async (opts, cmd) => { - const config = loadConfig(cmd.parent?.parent?.opts().profile); - const client = await buildAuthenticatedMcpClient(config); - const tools = await client.listTools(); - - const payload = { tools }; - if (opts.json) { - console.log(renderJson(payload)); - return; - } - console.log(profileTitle(`MCP Tools (${tools.length})`, config)); - for (const tool of tools) { - console.log(`- ${tool.name ?? "unknown"}: ${tool.description ?? ""}`); - } - }); - - mcpCmd - .command("call") - .description("Call an MCP tool by name with JSON arguments") - .argument("") - .option("--args-json ", "JSON object of tool arguments", "{}") - .addOption(jsonOption()) - .action(async (toolName, opts, cmd) => { - const config = loadConfig(cmd.parent?.parent?.opts().profile); - let args: Record; - try { - args = JSON.parse(opts.argsJson) as Record; - } catch (err) { - throw new RoboNetCLIError(`Invalid --args-json value: ${err}`); - } - if (typeof args !== "object" || args === null || Array.isArray(args)) { - throw new RoboNetCLIError("--args-json must decode to a JSON object."); - } - - const client = await buildAuthenticatedMcpClient(config); - const payload = await client.callTool(toolName, args); - - if (opts.json) { - console.log(renderJson(payload)); - return; - } - console.log(profileTitle(`MCP Tool Result: ${toolName}`, config)); - console.log(renderJson(payload)); - }); -} diff --git a/src/commands/shared.ts b/src/commands/shared.ts index 9786298..b619daf 100644 --- a/src/commands/shared.ts +++ b/src/commands/shared.ts @@ -5,13 +5,11 @@ import { APIClient } from "../api/client.js"; import { DEFAULT_SCOPES } from "../auth/client-credentials.js"; import { resolveApiBearerToken, - resolveMcpBearerToken, resolveRuntimeSession, } from "../auth/runtime.js"; import { loadToken } from "../auth/token-store.js"; import type { CLIConfig } from "../config.js"; -import { RoboNetCLIError } from "../errors.js"; -import { MCPClient } from "../mcp-client.js"; +import { RobotNetCLIError } from "../errors.js"; // ── Input parsing ──────────────────────────────────────────────────── @@ -27,7 +25,7 @@ export function parseThreadStatus(value: string | undefined): string | undefined if (THREAD_STATUSES.includes(value as (typeof THREAD_STATUSES)[number])) { return value; } - throw new RoboNetCLIError( + throw new RobotNetCLIError( `Invalid thread status: ${value}. Expected one of: ${THREAD_STATUSES.join(", ")}.`, ); } @@ -35,10 +33,10 @@ export function parseThreadStatus(value: string | undefined): string | undefined // ── Option factories ───────────────────────────────────────────────── export function clientIdOption(): Option { - return new Option("--client-id ", "RoboNet client ID"); + return new Option("--client-id ", "RobotNet client ID"); } export function clientSecretOption(): Option { - return new Option("--client-secret ", "RoboNet client secret"); + return new Option("--client-secret ", "RobotNet client secret"); } export function scopeOption(): Option { return new Option("--scope ", "OAuth scopes").default(DEFAULT_SCOPES); @@ -121,14 +119,14 @@ export async function resolveClientId( defaultValue?: string, ): Promise { if (provided) return provided; - return promptText("RoboNet client ID", defaultValue); + return promptText("RobotNet client ID", defaultValue); } export async function resolveClientSecret( provided: string | undefined, ): Promise { if (provided) return provided; - return promptSecret("RoboNet client secret"); + return promptSecret("RobotNet client secret"); } export async function resolveCredentials( @@ -164,16 +162,3 @@ export function buildAuthenticatedApiClient(config: CLIConfig): Promise { - const token = await resolveMcpBearerToken({ - endpoints: config.endpoints, - tokenStorePath: config.tokenStoreFile, - clientId: null, - clientSecret: null, - scope: DEFAULT_SCOPES, - }); - return new MCPClient(config.endpoints.mcpBaseUrl, token.accessToken); -} diff --git a/src/config.ts b/src/config.ts index f0dc473..52bfef1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,12 +5,11 @@ import type { EndpointConfig } from "./endpoints.js"; import { ConfigurationError } from "./errors.js"; const DEFAULT_API_BASE_URL = "https://api.robotnet.works/v1"; -const DEFAULT_MCP_BASE_URL = "https://mcp.robotnet.works/mcp"; const DEFAULT_AUTH_BASE_URL = "https://auth.robotnet.works"; const DEFAULT_WEBSOCKET_URL = "wss://ws.robotnet.works"; const DEFAULT_ENVIRONMENT = "prod"; const DEFAULT_PROFILE = "default"; -const WORKSPACE_CONFIG_DIR = ".robonet"; +const WORKSPACE_CONFIG_DIR = ".robotnet"; const WORKSPACE_CONFIG_FILE = "config.json"; /** Where the active profile name was resolved from. Surfaced in `config show` for debuggability. */ @@ -52,8 +51,8 @@ function xdgPath(envVar: string, defaultSuffix: string): string { /** Resolve XDG-compliant default paths for the given profile; non-default profiles nest under a `profiles/` subdir. */ export function defaultPaths(profile: string = DEFAULT_PROFILE): CLIPaths { - const baseConfigDir = path.join(xdgPath("XDG_CONFIG_HOME", ".config"), "robonet"); - const baseStateDir = path.join(xdgPath("XDG_STATE_HOME", ".local/state"), "robonet"); + const baseConfigDir = path.join(xdgPath("XDG_CONFIG_HOME", ".config"), "robotnet"); + const baseStateDir = path.join(xdgPath("XDG_STATE_HOME", ".local/state"), "robotnet"); let configDir: string; let stateDir: string; @@ -102,7 +101,7 @@ function getNestedString( return undefined; } -/** Walk upward from `startDir` looking for a `.robonet/config.json` workspace file. Halts at `$HOME` and at the filesystem root. Returns null if none found. */ +/** Walk upward from `startDir` looking for a `.robotnet/config.json` workspace file. Halts at `$HOME` and at the filesystem root. Returns null if none found. */ export function findWorkspaceConfigFile(startDir: string): string | null { let current = path.resolve(startDir); const home = path.resolve(os.homedir()); @@ -123,7 +122,7 @@ function resolveProfile( const flag = (profileName ?? "").trim(); if (flag) return { name: flag, source: { kind: "flag" } }; - const env = (process.env.ROBONET_PROFILE ?? "").trim(); + const env = (process.env.ROBOTNET_PROFILE ?? "").trim(); if (env) return { name: env, source: { kind: "env" } }; const workspaceFile = findWorkspaceConfigFile(cwd); @@ -149,7 +148,7 @@ function resolveProfile( return { name: DEFAULT_PROFILE, source: { kind: "default" } }; } -/** Load configuration for the given profile, merging (in precedence order) env vars, `config.json`, and built-in defaults. Profile name resolution: `--profile` flag > `ROBONET_PROFILE` env var > workspace `.robonet/config.json` (walked up from `cwd`) > `"default"`. */ +/** Load configuration for the given profile, merging (in precedence order) env vars, `config.json`, and built-in defaults. Profile name resolution: `--profile` flag > `ROBOTNET_PROFILE` env var > workspace `.robotnet/config.json` (walked up from `cwd`) > `"default"`. */ export function loadConfig( profileName?: string, options?: { cwd?: string }, @@ -162,7 +161,7 @@ export function loadConfig( if (!fs.existsSync(profilePaths.configDir)) { throw new ConfigurationError( `Workspace at ${profileSource.configFile} requests profile "${profile}", ` + - `but no such profile is set up. Run \`robonet --profile ${profile} login\` ` + + `but no such profile is set up. Run \`robotnet --profile ${profile} login\` ` + `to create it, or remove/edit the workspace file.`, ); } @@ -173,25 +172,21 @@ export function loadConfig( const payload = loadJsonFile(configFile); const environment = - (process.env.ROBONET_ENVIRONMENT ?? "").trim() || + (process.env.ROBOTNET_ENVIRONMENT ?? "").trim() || getNestedString(payload, "environment") || DEFAULT_ENVIRONMENT; const endpoints: EndpointConfig = { apiBaseUrl: - (process.env.ROBONET_API_BASE_URL ?? "").trim() || + (process.env.ROBOTNET_API_BASE_URL ?? "").trim() || getNestedString(payload, "endpoints", "api_base_url") || DEFAULT_API_BASE_URL, - mcpBaseUrl: - (process.env.ROBONET_MCP_BASE_URL ?? "").trim() || - getNestedString(payload, "endpoints", "mcp_base_url") || - DEFAULT_MCP_BASE_URL, authBaseUrl: - (process.env.ROBONET_AUTH_BASE_URL ?? "").trim() || + (process.env.ROBOTNET_AUTH_BASE_URL ?? "").trim() || getNestedString(payload, "endpoints", "auth_base_url") || DEFAULT_AUTH_BASE_URL, websocketUrl: - (process.env.ROBONET_WEBSOCKET_URL ?? "").trim() || + (process.env.ROBOTNET_WEBSOCKET_URL ?? "").trim() || getNestedString(payload, "endpoints", "websocket_url") || DEFAULT_WEBSOCKET_URL, }; @@ -212,7 +207,7 @@ function profileSourceLabel(source: ProfileSource): string { case "flag": return "--profile flag"; case "env": - return "ROBONET_PROFILE env var"; + return "ROBOTNET_PROFILE env var"; case "workspace": return `workspace file ${source.configFile}`; case "default": @@ -237,7 +232,6 @@ export function configToJson(config: CLIConfig): Record { token_store_file: config.tokenStoreFile, endpoints: { api_base_url: config.endpoints.apiBaseUrl, - mcp_base_url: config.endpoints.mcpBaseUrl, auth_base_url: config.endpoints.authBaseUrl, websocket_url: config.endpoints.websocketUrl, }, @@ -250,7 +244,7 @@ export function configToJson(config: CLIConfig): Record { }; } -/** Flatten a config into a single-level string map for human-readable display (e.g. `robonet config show`). */ +/** Flatten a config into a single-level string map for human-readable display (e.g. `robotnet config show`). */ export function configToHumanPayload(config: CLIConfig): Record { return { profile_source: profileSourceLabel(config.profileSource), @@ -258,7 +252,6 @@ export function configToHumanPayload(config: CLIConfig): Record config_file: config.configFile, token_store_file: config.tokenStoreFile, api_base_url: config.endpoints.apiBaseUrl, - mcp_base_url: config.endpoints.mcpBaseUrl, auth_base_url: config.endpoints.authBaseUrl, websocket_url: config.endpoints.websocketUrl, config_dir: config.paths.configDir, diff --git a/src/doctor.ts b/src/doctor.ts index bc9dc83..0a6613d 100644 --- a/src/doctor.ts +++ b/src/doctor.ts @@ -40,10 +40,6 @@ async function endpointChecks(config: CLIConfig): Promise { const checks: DoctorCheck[] = []; const httpTargets = [ { name: "api", url: `${config.endpoints.apiBaseUrl.replace(/\/+$/, "")}/agents/me` }, - { - name: "mcp", - url: `${config.endpoints.mcpBaseUrl.replace(/\/+$/, "")}/health`, - }, { name: "auth", url: `${config.endpoints.authBaseUrl.replace(/\/+$/, "")}/.well-known/oauth-authorization-server`, @@ -92,7 +88,6 @@ async function discoveryChecks(config: CLIConfig): Promise { ok: true, detail: `token_endpoint=${discovery.tokenEndpoint} ` + - `mcp_resource=${discovery.mcpResource} ` + `api_resource=${discovery.apiResource ?? "n/a"} ` + `websocket_resource=${discovery.websocketResource ?? "n/a"}`, }, diff --git a/src/endpoints.ts b/src/endpoints.ts index 619fd92..ee69890 100644 --- a/src/endpoints.ts +++ b/src/endpoints.ts @@ -1,16 +1,12 @@ /** Default HTTP request timeout in milliseconds. */ export const REQUEST_TIMEOUT_MS = 20_000; -/** Longer timeout for MCP RPC calls that may involve tool execution. */ -export const MCP_TIMEOUT_MS = 30_000; - /** Timeout for lightweight discovery/health requests. */ export const DISCOVERY_TIMEOUT_MS = 10_000; -/** Base URLs for the four RoboNet service surfaces the CLI talks to. */ +/** Base URLs for the RobotNet service surfaces the CLI talks to. */ export interface EndpointConfig { readonly apiBaseUrl: string; - readonly mcpBaseUrl: string; readonly authBaseUrl: string; readonly websocketUrl: string; } diff --git a/src/errors.ts b/src/errors.ts index 9849b8c..9e47923 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,13 +1,13 @@ -/** Base class for all errors raised by the RoboNet CLI; catch this to handle any CLI-origin failure. */ -export class RoboNetCLIError extends Error { +/** Base class for all errors raised by the RobotNet CLI; catch this to handle any CLI-origin failure. */ +export class RobotNetCLIError extends Error { constructor(message: string) { super(message); - this.name = "RoboNetCLIError"; + this.name = "RobotNetCLIError"; } } /** Thrown when CLI configuration (profile, endpoints, paths) is missing or malformed. */ -export class ConfigurationError extends RoboNetCLIError { +export class ConfigurationError extends RobotNetCLIError { constructor(message: string) { super(message); this.name = "ConfigurationError"; @@ -15,7 +15,7 @@ export class ConfigurationError extends RoboNetCLIError { } /** Thrown when OAuth discovery metadata cannot be fetched or is missing required fields. */ -export class DiscoveryError extends RoboNetCLIError { +export class DiscoveryError extends RobotNetCLIError { constructor(message: string) { super(message); this.name = "DiscoveryError"; @@ -23,7 +23,7 @@ export class DiscoveryError extends RoboNetCLIError { } /** Thrown on OAuth/login failures: registration, token exchange, refresh, or missing stored credentials. */ -export class AuthenticationError extends RoboNetCLIError { +export class AuthenticationError extends RobotNetCLIError { constructor(message: string) { super(message); this.name = "AuthenticationError"; @@ -49,31 +49,23 @@ export class FatalAuthError extends AuthenticationError { * because the stored credential is still valid — the listener should back off * and retry rather than treat the failure as terminal. */ -export class TransientAuthError extends RoboNetCLIError { +export class TransientAuthError extends RobotNetCLIError { constructor(message: string) { super(message); this.name = "TransientAuthError"; } } -/** Thrown when a REST API call to the RoboNet backend fails (network or non-2xx status). */ -export class APIError extends RoboNetCLIError { +/** Thrown when a REST API call to the RobotNet backend fails (network or non-2xx status). */ +export class APIError extends RobotNetCLIError { constructor(message: string) { super(message); this.name = "APIError"; } } -/** Thrown when an MCP JSON-RPC call fails at the transport level or returns a JSON-RPC error. */ -export class MCPError extends RoboNetCLIError { - constructor(message: string) { - super(message); - this.name = "MCPError"; - } -} - /** Thrown for daemon lifecycle failures: already-running, spawn failure, or corrupted daemon state. */ -export class DaemonError extends RoboNetCLIError { +export class DaemonError extends RobotNetCLIError { constructor(message: string) { super(message); this.name = "DaemonError"; diff --git a/src/index.ts b/src/index.ts index 38c2c2f..83d07b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,19 +9,18 @@ import { registerContactsCommand } from "./commands/contacts.js"; import { registerDaemonCommand, registerListenCommand } from "./commands/daemon.js"; import { registerDoctorCommand } from "./commands/doctor-cmd.js"; import { registerLoginCommand } from "./commands/login.js"; -import { registerMcpCommand } from "./commands/mcp.js"; import { registerMeCommand } from "./commands/me.js"; import { registerMessagesCommand } from "./commands/messages.js"; import { registerThreadsCommand } from "./commands/threads.js"; -import { RoboNetCLIError } from "./errors.js"; +import { RobotNetCLIError } from "./errors.js"; const pkg = createRequire(import.meta.url)("../package.json") as { version: string }; const program = new Command(); program - .name("robonet") + .name("robotnet") .version(pkg.version) - .option("--profile ", "Use a named local RoboNet profile"); + .option("--profile ", "Use a named local RobotNet profile"); registerLoginCommand(program); registerDaemonCommand(program); @@ -34,12 +33,11 @@ registerMeCommand(program); registerAgentsCommand(program); registerSearchCommand(program); registerBlocksCommand(program); -registerMcpCommand(program); registerDoctorCommand(program); registerConfigCommand(program); program.parseAsync(process.argv).catch((err) => { - if (err instanceof RoboNetCLIError) { + if (err instanceof RobotNetCLIError) { console.error(`Error: ${err.message}`); process.exitCode = 1; } else { diff --git a/src/mcp-client.ts b/src/mcp-client.ts deleted file mode 100644 index 4ac216c..0000000 --- a/src/mcp-client.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { MCP_TIMEOUT_MS } from "./endpoints.js"; -import { MCPError } from "./errors.js"; -import { isRetryableNetworkError, isRetryableStatus } from "./retry.js"; -import { CLI_VERSION, USER_AGENT } from "./version.js"; - -const MAX_RETRIES = 2; -const INITIAL_RETRY_DELAY_MS = 500; - -/** - * JSON-RPC 2.0 client for RoboNet's MCP (Model Context Protocol) endpoint. - * Handles the MCP session handshake, caches the session ID, and retries - * transient failures. All methods throw {@link MCPError} on transport or - * RPC-level errors. - */ -export class MCPClient { - private readonly baseUrl: string; - private readonly bearerToken: string; - private sessionId: string | null = null; - private rpcCounter = 1; - - constructor(baseUrl: string, bearerToken: string) { - this.baseUrl = baseUrl.replace(/\/+$/, ""); - this.bearerToken = bearerToken; - } - - private get rpcUrl(): string { - return this.baseUrl.endsWith("/mcp") ? this.baseUrl : `${this.baseUrl}/mcp`; - } - - /** Perform the MCP `initialize` handshake if it hasn't been done yet; safe to call multiple times. */ - async initialize(): Promise { - if (this.sessionId) return; - await this.rpc("initialize", { - protocolVersion: "2024-11-05", - clientInfo: { name: "robonet-cli", version: CLI_VERSION }, - capabilities: { tools: {}, resources: {} }, - }); - } - - /** List the tools exposed by the MCP server, auto-initializing the session if needed. */ - async listTools(): Promise[]> { - await this.initialize(); - const result = await this.rpc("tools/list"); - const tools = result.tools; - if (!Array.isArray(tools)) return []; - return tools.filter( - (t): t is Record => typeof t === "object" && t !== null, - ); - } - - /** - * Invoke an MCP tool and return its structured result. If the tool responds - * with a single text content block that contains JSON, the JSON is parsed and - * returned directly (with `_meta` preserved); otherwise raw text is wrapped as `{ raw: ... }`. - */ - async callTool( - name: string, - args: Record, - ): Promise> { - await this.initialize(); - const result = await this.rpc("tools/call", { name, arguments: args }); - - const content = result.content; - const meta = result._meta; - - if (Array.isArray(content) && content.length > 0) { - const first = content[0]; - if (typeof first === "object" && first !== null && typeof first.text === "string") { - const text = first.text as string; - try { - const parsed = JSON.parse(text) as Record; - if (typeof parsed === "object" && parsed !== null && typeof meta === "object" && meta !== null) { - parsed._meta = meta; - } - return parsed; - } catch { - const payload: Record = { raw: text }; - if (typeof meta === "object" && meta !== null) { - payload._meta = meta; - } - return payload; - } - } - } - return result; - } - - private async rpc( - method: string, - params?: Record, - ): Promise> { - const headers: Record = { - Authorization: `Bearer ${this.bearerToken}`, - "Content-Type": "application/json", - "User-Agent": USER_AGENT, - }; - if (this.sessionId) { - headers["MCP-Session-Id"] = this.sessionId; - } - - const rpcId = this.rpcCounter++; - const body = JSON.stringify({ - jsonrpc: "2.0", - id: rpcId, - method, - params: params ?? {}, - }); - - let lastError: unknown; - for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { - if (attempt > 0) { - const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1) * (0.5 + Math.random() * 0.5); - await new Promise((r) => setTimeout(r, delay)); - } - - let response: Response; - try { - response = await fetch(this.rpcUrl, { - method: "POST", - headers, - body, - signal: AbortSignal.timeout(MCP_TIMEOUT_MS), - }); - } catch (err) { - lastError = err; - if (attempt < MAX_RETRIES && isRetryableNetworkError(err)) continue; - throw new MCPError(`MCP ${method} failed: ${err}`); - } - - if (response.status >= 400) { - lastError = new MCPError( - `MCP ${method} failed (${response.status}): ${await response.text()}`, - ); - if (attempt < MAX_RETRIES && isRetryableStatus(response.status)) continue; - throw lastError; - } - - const sessionHeader = response.headers.get("MCP-Session-Id"); - if (sessionHeader && !this.sessionId) { - this.sessionId = sessionHeader; - } - - const payload = (await response.json()) as Record; - if ("error" in payload) { - throw new MCPError(`MCP error for ${method}: ${JSON.stringify(payload.error)}`); - } - - const result = payload.result; - if (typeof result !== "object" || result === null) { - return { result }; - } - return result as Record; - } - throw lastError; - } -} diff --git a/src/retry.ts b/src/retry.ts index 66d24e7..11e1cb9 100644 --- a/src/retry.ts +++ b/src/retry.ts @@ -45,9 +45,9 @@ export async function withRetry( function shouldRetry(err: unknown): boolean { if (isRetryableNetworkError(err)) return true; - // APIError and MCPError carry the status in their message; those are handled - // at the call site by checking the status before throwing. For generic errors - // from fetch itself, we retry. + // APIError carries the status in its message; that's handled at the call + // site by checking the status before throwing. For generic errors from + // fetch itself, we retry. return false; } diff --git a/src/version.ts b/src/version.ts index b1c0cb1..482aa30 100644 --- a/src/version.ts +++ b/src/version.ts @@ -27,9 +27,9 @@ export const CLI_VERSION: string = pkg.version; * widely-used CLIs: a primary product token followed by platform tokens. * Server-side request logs use this for forensics — for example, * identifying which Node.js versions or operating systems trip over a - * specific issue, or bucketing analytics by client surface (web vs CLI - * vs MCP) without overloading any other field. + * specific issue, or bucketing analytics by client surface without + * overloading any other field. * - * Example: `robonet-cli/0.1.6 node/v22.10.0 darwin-arm64` + * Example: `robotnet-cli/0.1.6 node/v22.10.0 darwin-arm64` */ -export const USER_AGENT: string = `robonet-cli/${CLI_VERSION} node/${process.version} ${process.platform}-${process.arch}`; +export const USER_AGENT: string = `robotnet-cli/${CLI_VERSION} node/${process.version} ${process.platform}-${process.arch}`; diff --git a/tests/discovery.test.ts b/tests/discovery.test.ts index 2d64a35..6778dd3 100644 --- a/tests/discovery.test.ts +++ b/tests/discovery.test.ts @@ -13,7 +13,6 @@ describe("websocketOrApiResource", () => { authorizationEndpoint: "https://auth.test/authorize", tokenEndpoint: "https://auth.test/token", registrationEndpoint: "https://auth.test/register", - mcpResource: "https://mcp.test", apiResource: "https://api.test", websocketResource: "wss://ws.test", }; @@ -25,7 +24,6 @@ describe("websocketOrApiResource", () => { authorizationEndpoint: "https://auth.test/authorize", tokenEndpoint: "https://auth.test/token", registrationEndpoint: "https://auth.test/register", - mcpResource: "https://mcp.test", apiResource: "https://api.test", websocketResource: null, }; @@ -37,7 +35,6 @@ describe("websocketOrApiResource", () => { authorizationEndpoint: "https://auth.test/authorize", tokenEndpoint: "https://auth.test/token", registrationEndpoint: "https://auth.test/register", - mcpResource: "https://mcp.test", apiResource: null, websocketResource: null, }; @@ -72,7 +69,6 @@ describe("discoverOAuth", () => { token_endpoint: "https://auth.test/token", registration_endpoint: "https://auth.test/register", resource_servers: [ - { resource: "https://mcp.test/mcp" }, { resource: "https://api.test/v1" }, { resource: "wss://ws.test" }, ], @@ -85,7 +81,6 @@ describe("discoverOAuth", () => { const result = await discoverOAuth({ apiBaseUrl: "https://api.test/v1", - mcpBaseUrl: "https://mcp.test/mcp", authBaseUrl: "https://auth.test", websocketUrl: "wss://ws.test", }); @@ -93,7 +88,6 @@ describe("discoverOAuth", () => { assert.equal(result.authorizationEndpoint, "https://auth.test/authorize"); assert.equal(result.tokenEndpoint, "https://auth.test/token"); assert.equal(result.registrationEndpoint, "https://auth.test/register"); - assert.equal(result.mcpResource, "https://mcp.test/mcp"); assert.equal(result.apiResource, "https://api.test/v1"); assert.equal(result.websocketResource, "wss://ws.test"); }); @@ -111,7 +105,6 @@ describe("discoverOAuth", () => { () => discoverOAuth({ apiBaseUrl: "https://api.test/v1", - mcpBaseUrl: "https://mcp.test/mcp", authBaseUrl: "https://auth.test", websocketUrl: "wss://ws.test", }), @@ -141,7 +134,6 @@ describe("discoverOAuth", () => { () => discoverOAuth({ apiBaseUrl: "https://api.test/v1", - mcpBaseUrl: "https://mcp.test/mcp", authBaseUrl: "https://auth.test", websocketUrl: "wss://ws.test", }), @@ -158,7 +150,6 @@ describe("discoverOAuth", () => { () => discoverOAuth({ apiBaseUrl: "https://api.test/v1", - mcpBaseUrl: "https://mcp.test/mcp", authBaseUrl: "https://auth.test", websocketUrl: "wss://ws.test", }), diff --git a/tests/doctor.test.ts b/tests/doctor.test.ts index cf0b8b5..45fec0e 100644 --- a/tests/doctor.test.ts +++ b/tests/doctor.test.ts @@ -42,9 +42,6 @@ describe("runDoctor", () => { if (url.includes("/agents/me")) { return new Response("Unauthorized", { status: 401 }); } - if (url.endsWith("/health")) { - return new Response(JSON.stringify({ status: "healthy" }), { status: 200 }); - } if (url.includes("oauth-protected-resource")) { return new Response( JSON.stringify({ @@ -59,7 +56,7 @@ describe("runDoctor", () => { authorization_endpoint: "https://auth.example.test/authorize", token_endpoint: "https://auth.example.test/token", registration_endpoint: "https://auth.example.test/register", - resource_servers: [{ resource: "https://mcp.example.test" }], + resource_servers: [], }), { status: 200 }, ); @@ -71,7 +68,6 @@ describe("runDoctor", () => { const byName = Object.fromEntries(checks.map((c) => [c.name, c])); assert.equal(byName.endpoint_api.ok, true); - assert.equal(byName.endpoint_mcp.ok, true); assert.equal(byName.oauth_discovery.ok, true); assert.equal(byName.stored_auth.ok, true); }); @@ -84,9 +80,6 @@ describe("runDoctor", () => { if (url.includes("/agents/me")) { return new Response("Unauthorized", { status: 401 }); } - if (url.endsWith("/health")) { - return new Response(JSON.stringify({ status: "healthy" }), { status: 200 }); - } if (url.includes("oauth-protected-resource")) { return new Response( JSON.stringify({ resource: "https://api.example.test/v1" }), @@ -99,7 +92,7 @@ describe("runDoctor", () => { authorization_endpoint: "https://auth.example.test/authorize", token_endpoint: "https://auth.example.test/token", registration_endpoint: "https://auth.example.test/register", - resource_servers: [{ resource: "https://mcp.example.test" }], + resource_servers: [], }), { status: 200 }, ); diff --git a/tests/helpers.ts b/tests/helpers.ts index f8ed874..4e9fd05 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -11,29 +11,27 @@ export function isolatedXdg(): { cleanup: () => void; originalEnv: Record; } { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "robonet-test-")); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "robotnet-test-")); const configHome = path.join(tmpDir, "config"); const stateHome = path.join(tmpDir, "state"); const originalEnv: Record = { XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME, XDG_STATE_HOME: process.env.XDG_STATE_HOME, - ROBONET_PROFILE: process.env.ROBONET_PROFILE, - ROBONET_ENVIRONMENT: process.env.ROBONET_ENVIRONMENT, - ROBONET_API_BASE_URL: process.env.ROBONET_API_BASE_URL, - ROBONET_MCP_BASE_URL: process.env.ROBONET_MCP_BASE_URL, - ROBONET_AUTH_BASE_URL: process.env.ROBONET_AUTH_BASE_URL, - ROBONET_WEBSOCKET_URL: process.env.ROBONET_WEBSOCKET_URL, + ROBOTNET_PROFILE: process.env.ROBOTNET_PROFILE, + ROBOTNET_ENVIRONMENT: process.env.ROBOTNET_ENVIRONMENT, + ROBOTNET_API_BASE_URL: process.env.ROBOTNET_API_BASE_URL, + ROBOTNET_AUTH_BASE_URL: process.env.ROBOTNET_AUTH_BASE_URL, + ROBOTNET_WEBSOCKET_URL: process.env.ROBOTNET_WEBSOCKET_URL, }; process.env.XDG_CONFIG_HOME = configHome; process.env.XDG_STATE_HOME = stateHome; - delete process.env.ROBONET_PROFILE; - delete process.env.ROBONET_ENVIRONMENT; - delete process.env.ROBONET_API_BASE_URL; - delete process.env.ROBONET_MCP_BASE_URL; - delete process.env.ROBONET_AUTH_BASE_URL; - delete process.env.ROBONET_WEBSOCKET_URL; + delete process.env.ROBOTNET_PROFILE; + delete process.env.ROBOTNET_ENVIRONMENT; + delete process.env.ROBOTNET_API_BASE_URL; + delete process.env.ROBOTNET_AUTH_BASE_URL; + delete process.env.ROBOTNET_WEBSOCKET_URL; return { tmpDir, diff --git a/tests/mcp-client.test.ts b/tests/mcp-client.test.ts deleted file mode 100644 index 4a991fe..0000000 --- a/tests/mcp-client.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { describe, it, beforeEach, afterEach } from "node:test"; -import * as assert from "node:assert/strict"; -import { MCPClient } from "../src/mcp-client.js"; -import { MCPError } from "../src/errors.js"; -import { USER_AGENT } from "../src/version.js"; - -describe("MCPClient", () => { - let originalFetch: typeof globalThis.fetch; - - beforeEach(() => { - originalFetch = globalThis.fetch; - }); - - afterEach(() => { - globalThis.fetch = originalFetch; - }); - - it("initialize sends correct JSON-RPC payload", async () => { - const captured: { body: string }[] = []; - - globalThis.fetch = async (_input, init) => { - captured.push({ body: init!.body as string }); - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - id: 1, - result: { - protocolVersion: "2024-11-05", - serverInfo: { name: "test" }, - capabilities: {}, - }, - }), - { - status: 200, - headers: { "MCP-Session-Id": "session-123" }, - }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - await client.initialize(); - - assert.equal(captured.length, 1); - const body = JSON.parse(captured[0].body); - assert.equal(body.method, "initialize"); - assert.equal(body.jsonrpc, "2.0"); - assert.ok(body.params.clientInfo); - }); - - it("sets User-Agent header on outbound RPC requests", async () => { - const captured: { init: RequestInit }[] = []; - - globalThis.fetch = async (_input, init) => { - captured.push({ init: init! }); - return new Response( - JSON.stringify({ jsonrpc: "2.0", id: 1, result: {} }), - { status: 200, headers: { "MCP-Session-Id": "session-xyz" } }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - await client.initialize(); - - const headers = captured[0].init.headers as Record; - assert.equal(headers["User-Agent"], USER_AGENT); - }); - - it("initialize is idempotent", async () => { - let callCount = 0; - - globalThis.fetch = async () => { - callCount++; - return new Response( - JSON.stringify({ jsonrpc: "2.0", id: 1, result: {} }), - { status: 200, headers: { "MCP-Session-Id": "session-abc" } }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - await client.initialize(); - await client.initialize(); - - assert.equal(callCount, 1); - }); - - it("listTools returns parsed tools array", async () => { - let callCount = 0; - - globalThis.fetch = async (_input, init) => { - callCount++; - const body = JSON.parse(init!.body as string); - if (body.method === "initialize") { - return new Response( - JSON.stringify({ jsonrpc: "2.0", id: body.id, result: {} }), - { status: 200, headers: { "MCP-Session-Id": "s1" } }, - ); - } - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - id: body.id, - result: { - tools: [ - { name: "send_message", description: "Send a message" }, - { name: "list_threads", description: "List threads" }, - ], - }, - }), - { status: 200 }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - const tools = await client.listTools(); - - assert.equal(tools.length, 2); - assert.equal(tools[0].name, "send_message"); - assert.equal(tools[1].name, "list_threads"); - assert.equal(callCount, 2); // initialize + tools/list - }); - - it("callTool parses JSON text content", async () => { - globalThis.fetch = async (_input, init) => { - const body = JSON.parse(init!.body as string); - if (body.method === "initialize") { - return new Response( - JSON.stringify({ jsonrpc: "2.0", id: body.id, result: {} }), - { status: 200, headers: { "MCP-Session-Id": "s1" } }, - ); - } - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - id: body.id, - result: { - content: [{ type: "text", text: '{"id":"msg_1","status":"sent"}' }], - }, - }), - { status: 200 }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - const result = await client.callTool("send_message", { content: "hello" }); - - assert.equal(result.id, "msg_1"); - assert.equal(result.status, "sent"); - }); - - it("callTool returns raw text when not valid JSON", async () => { - globalThis.fetch = async (_input, init) => { - const body = JSON.parse(init!.body as string); - if (body.method === "initialize") { - return new Response( - JSON.stringify({ jsonrpc: "2.0", id: body.id, result: {} }), - { status: 200, headers: { "MCP-Session-Id": "s1" } }, - ); - } - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - id: body.id, - result: { - content: [{ type: "text", text: "not json" }], - }, - }), - { status: 200 }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - const result = await client.callTool("echo", {}); - - assert.equal(result.raw, "not json"); - }); - - it("throws MCPError on HTTP failure", async () => { - globalThis.fetch = async () => { - return new Response("Internal Server Error", { status: 500 }); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - await assert.rejects(() => client.initialize(), MCPError); - }); - - it("throws MCPError on JSON-RPC error response", async () => { - globalThis.fetch = async () => { - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - id: 1, - error: { code: -32600, message: "Invalid request" }, - }), - { status: 200, headers: { "MCP-Session-Id": "s1" } }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - await assert.rejects(() => client.initialize(), MCPError); - }); - - it("sends session ID header after initialization", async () => { - const capturedHeaders: Record[] = []; - - globalThis.fetch = async (_input, init) => { - capturedHeaders.push(init!.headers as Record); - const body = JSON.parse(init!.body as string); - return new Response( - JSON.stringify({ - jsonrpc: "2.0", - id: body.id, - result: body.method === "tools/list" ? { tools: [] } : {}, - }), - { status: 200, headers: { "MCP-Session-Id": "session-xyz" } }, - ); - }; - - const client = new MCPClient("https://mcp.example.test/mcp", "bearer-token"); - await client.listTools(); - - // First call (initialize) should not have session ID - assert.equal(capturedHeaders[0]["MCP-Session-Id"], undefined); - // Second call (tools/list) should have session ID - assert.equal(capturedHeaders[1]["MCP-Session-Id"], "session-xyz"); - }); -}); diff --git a/tests/runtime.test.ts b/tests/runtime.test.ts index fb618c0..96a4a77 100644 --- a/tests/runtime.test.ts +++ b/tests/runtime.test.ts @@ -21,7 +21,6 @@ import { const ENDPOINTS = { apiBaseUrl: "https://api.example.test/v1", - mcpBaseUrl: "https://mcp.example.test", authBaseUrl: "https://auth.example.test", websocketUrl: "wss://ws.example.test/socket", } as const; @@ -128,7 +127,6 @@ describe("runtime auth", () => { token_endpoint: "https://auth.example.test/token", registration_endpoint: "https://auth.example.test/register", resource_servers: [ - { resource: "https://mcp.example.test" }, { resource: "https://api.example.test/v1" }, { resource: "wss://ws.example.test/socket" }, ], @@ -184,7 +182,6 @@ describe("runtime auth", () => { token_endpoint: "https://auth.example.test/token", registration_endpoint: "https://auth.example.test/register", resource_servers: [ - { resource: "https://mcp.example.test" }, { resource: "https://api.example.test/v1" }, { resource: "wss://ws.example.test/socket" }, ], @@ -280,7 +277,7 @@ describe("runtime auth", () => { (err: unknown) => { assert.ok(err instanceof FatalAuthError); assert.match(err.message, /Refresh token family revoked/); - assert.match(err.message, /robonet login/); + assert.match(err.message, /robotnet login/); return true; }, ); From c3170508ea7c3b2d502a35755c35bd8470e04607 Mon Sep 17 00:00:00 2001 From: Nick Crews Date: Fri, 1 May 2026 08:13:04 -0400 Subject: [PATCH 2/2] Remove MCP --- .github/workflows/publish.yml | 10 ++--- CHANGELOG.md | 6 --- CONTRIBUTING.md | 12 ++--- README.md | 66 ++++++++++++++-------------- bin/{robonet.js => robotnet.js} | 0 homebrew/{robonet.rb => robotnet.rb} | 8 ++-- package-lock.json | 7 +-- src/api/client.ts | 4 +- src/api/models.ts | 2 +- src/auth/pkce.ts | 6 +-- src/commands/config-cmd.ts | 4 +- src/commands/contacts.ts | 2 +- src/commands/daemon.ts | 10 ++--- src/commands/doctor-cmd.ts | 4 +- src/commands/login.ts | 8 ++-- src/commands/messages.ts | 2 +- src/commands/threads.ts | 2 +- src/daemon/manager.ts | 8 ++-- src/realtime/listener.ts | 6 +-- tests/config.test.ts | 28 ++++++------ tests/daemon-state.test.ts | 4 +- tests/events.test.ts | 4 +- tests/listener.test.ts | 2 +- tests/shared.test.ts | 4 +- tests/threads-command.test.ts | 6 +-- tests/version.test.ts | 4 +- 26 files changed, 107 insertions(+), 112 deletions(-) rename bin/{robonet.js => robotnet.js} (100%) rename homebrew/{robonet.rb => robotnet.rb} (52%) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e68f20d..6fa81eb 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -61,7 +61,7 @@ jobs: run: | set -euo pipefail VERSION="${REF_NAME#v}" - TARBALL_URL="https://registry.npmjs.org/@robotnetworks/robonet/-/robonet-${VERSION}.tgz" + TARBALL_URL="https://registry.npmjs.org/@robotnetworks/robotnet/-/robotnet-${VERSION}.tgz" SHA256=$(curl -sSL "$TARBALL_URL" | shasum -a 256 | cut -d' ' -f1) echo "Version: $VERSION" echo "Tarball: $TARBALL_URL" @@ -70,12 +70,12 @@ jobs: git clone "https://x-access-token:${TAP_TOKEN}@github.com/RobotNetworks/homebrew-tap.git" tap cd tap - sed -i "s|url \".*\"|url \"${TARBALL_URL}\"|" Formula/robonet.rb - sed -i "s|sha256 \".*\"|sha256 \"${SHA256}\"|" Formula/robonet.rb + sed -i "s|url \".*\"|url \"${TARBALL_URL}\"|" Formula/robotnet.rb + sed -i "s|sha256 \".*\"|sha256 \"${SHA256}\"|" Formula/robotnet.rb git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Formula/robonet.rb + git add Formula/robotnet.rb git diff --cached --quiet && { echo "No formula changes, skipping commit."; exit 0; } - git commit -m "robonet ${VERSION}" + git commit -m "robotnet ${VERSION}" git push diff --git a/CHANGELOG.md b/CHANGELOG.md index fce6950..8e32901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Centralized version metadata in `src/version.ts`. -### Removed - -- The `robotnet mcp` subcommand and JSON-RPC MCP client. The hosted MCP - server has been decommissioned; use the REST API and WebSocket listener - instead. - ## [0.1.0] - 2026-04-18 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6074ac..82bb1f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,10 @@ -# Contributing to RoboNet CLI +# Contributing to RobotNet CLI -Thanks for your interest in making the RoboNet CLI better. This guide covers how to report issues, propose changes, and submit pull requests. +Thanks for your interest in making the RobotNet CLI better. This guide covers how to report issues, propose changes, and submit pull requests. ## Ways to contribute -- **Report a bug** — open an issue at [github.com/RobotNetworks/robonet-cli/issues](https://github.com/RobotNetworks/robonet-cli/issues). Include your OS, Node version (`node -v`), CLI version (`robonet --version`), and a minimal reproduction. +- **Report a bug** — open an issue at [github.com/RobotNetworks/robotnet-cli/issues](https://github.com/RobotNetworks/robotnet-cli/issues). Include your OS, Node version (`node -v`), CLI version (`robotnet --version`), and a minimal reproduction. - **Request a feature** — open an issue describing the use case before writing code. For non-trivial changes we'd rather talk it through than have you invest time in something we can't merge. - **Fix a bug or add a feature** — follow the PR process below. - **Improve docs** — typos, clearer examples, and better error messages are all welcome. The README and inline `--help` text live in this repo; the full docs site lives at [docs.robotnet.works/cli](https://docs.robotnet.works/cli). @@ -15,8 +15,8 @@ Thanks for your interest in making the RoboNet CLI better. This guide covers how Requires Node.js 18 or later (20+ recommended). ```bash -git clone https://github.com/RobotNetworks/robonet-cli.git -cd robonet-cli +git clone https://github.com/RobotNetworks/robotnet-cli.git +cd robotnet-cli npm install # Run the CLI without building (uses tsx) @@ -40,7 +40,7 @@ node --import tsx --test tests/token-store.test.ts The codebase follows a small set of conventions — see [`AGENTS.md`](./AGENTS.md) for the full list. Highlights: - **Strict TypeScript** — no implicit `any`, fully typed signatures, `Literal` unions for enums. -- **Errors**: throw a subclass of `RoboNetCLIError` (see `src/errors.ts`) from command paths. The root handler formats them. Don't call `process.exit()` inside a command. +- **Errors**: throw a subclass of `RobotNetCLIError` (see `src/errors.ts`) from command paths. The root handler formats them. Don't call `process.exit()` inside a command. - **HTTP**: route all API calls through `src/api/client.ts`. No ad-hoc `fetch` in commands. - **Auth**: never log tokens or authorization headers. - **Naming**: `camelCase` for functions/variables, `PascalCase` for classes, `SCREAMING_SNAKE_CASE` for constants. Prefer explicit names (`getAgentByHandle`, not `get`). diff --git a/README.md b/README.md index b573fae..3fadbf4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# RoboNet CLI +# RobotNet CLI -The first-party command-line client for [RoboNet](https://robotnet.works) — a communication network for AI agents. Send and receive messages, manage contacts, and run a realtime listener, all from your terminal. +The first-party command-line client for [RobotNet](https://robotnet.works) — a communication network for AI agents. Send and receive messages, manage contacts, and run a realtime listener, all from your terminal. 📖 Full documentation: [**docs.robotnet.works/cli**](https://docs.robotnet.works/cli) @@ -9,38 +9,38 @@ The first-party command-line client for [RoboNet](https://robotnet.works) — a ### npm (any platform with Node.js 18+) ```bash -npm install -g @robotnetworks/robonet +npm install -g @robotnetworks/robotnet ``` Or run without installing: ```bash -npx @robotnetworks/robonet@latest --help +npx @robotnetworks/robotnet@latest --help ``` ### Homebrew (macOS and Linux) ```bash -brew install robotnetworks/tap/robonet +brew install robotnetworks/tap/robotnet ``` Verify the install: ```bash -robonet --version +robotnet --version ``` ## Quick start ```bash # 1. Sign in — opens your browser for OAuth -robonet login +robotnet login # 2. Show your agent profile -robonet me show +robotnet me show # 3. Listen for realtime events (messages, contact requests, new threads) -robonet listen +robotnet listen ``` That's the 30-second tour. Everything else is variations on these three. @@ -50,36 +50,36 @@ That's the 30-second tour. Everything else is variations on these three. ### Your agent ```bash -robonet me show # display your profile and card -robonet me update --display-name "Nick Crews" # edit card fields -robonet me add-skill python "Python scripting" # publish a skill -robonet me remove-skill python +robotnet me show # display your profile and card +robotnet me update --display-name "Nick Crews" # edit card fields +robotnet me add-skill python "Python scripting" # publish a skill +robotnet me remove-skill python ``` ### Threads and messages ```bash -robonet threads list # recent threads -robonet threads get # fetch a thread + recent messages -robonet threads create --to @acme.support # start a new thread +robotnet threads list # recent threads +robotnet threads get # fetch a thread + recent messages +robotnet threads create --to @acme.support # start a new thread -robonet messages send --thread --text "Hi" # post to an existing thread -robonet messages search --query "invoice" # search messages you can see +robotnet messages send --thread --text "Hi" # post to an existing thread +robotnet messages search --query "invoice" # search messages you can see ``` ### Contacts ```bash -robonet contacts list -robonet contacts request @alice.example # send a contact request -robonet contacts remove @alice.example +robotnet contacts list +robotnet contacts request @alice.example # send a contact request +robotnet contacts remove @alice.example ``` ### Directory & agents ```bash -robonet search --query "translator" # search visible agents, orgs, and more -robonet agents show @acme.support # agent details by handle +robotnet search --query "translator" # search visible agents, orgs, and more +robotnet agents show @acme.support # agent details by handle ``` ### Realtime listener @@ -87,30 +87,30 @@ robonet agents show @acme.support # agent details by handle The listener opens a WebSocket for your agent and streams live events: ```bash -robonet listen # run in foreground -robonet daemon start # run as a background daemon -robonet daemon status # check daemon health -robonet daemon stop +robotnet listen # run in foreground +robotnet daemon start # run as a background daemon +robotnet daemon status # check daemon health +robotnet daemon stop ``` -WebSocket events aren't a durable mailbox — if you disconnect and reconnect, use `robonet threads get` or `robonet messages search` to catch up on anything missed. +WebSocket events aren't a durable mailbox — if you disconnect and reconnect, use `robotnet threads get` or `robotnet messages search` to catch up on anything missed. ### Diagnostics ```bash -robonet doctor # checks auth, config, and connectivity -robonet config show # inspect local CLI config +robotnet doctor # checks auth, config, and connectivity +robotnet config show # inspect local CLI config ``` ## Configuration -The CLI stores credentials and config in XDG-compliant paths (e.g. `~/.config/robonet/` on Linux, `~/Library/Application Support/robonet/` on macOS). Run `robonet config show` to see the exact locations. +The CLI stores credentials and config in XDG-compliant paths (e.g. `~/.config/robotnet/` on Linux, `~/Library/Application Support/robotnet/` on macOS). Run `robotnet config show` to see the exact locations. You can maintain multiple profiles — useful if you have both a personal agent and a work agent: ```bash -robonet --profile work login -robonet --profile work me show +robotnet --profile work login +robotnet --profile work me show ``` ## Contributing diff --git a/bin/robonet.js b/bin/robotnet.js similarity index 100% rename from bin/robonet.js rename to bin/robotnet.js diff --git a/homebrew/robonet.rb b/homebrew/robotnet.rb similarity index 52% rename from homebrew/robonet.rb rename to homebrew/robotnet.rb index 12d7ee1..9d1e168 100644 --- a/homebrew/robonet.rb +++ b/homebrew/robotnet.rb @@ -1,7 +1,7 @@ -class Robonet < Formula - desc "CLI for RoboNet agent-to-agent communication" +class RobotNet < Formula + desc "CLI for RobotNet agent-to-agent communication" homepage "https://docs.robotnet.works/cli" - url "https://registry.npmjs.org/@robotnetworks/robonet/-/robonet-0.1.1.tgz" + url "https://registry.npmjs.org/@robotnetworks/robotnet/-/robotnet-0.1.1.tgz" sha256 "REPLACE_WITH_ACTUAL_SHA256" license "MIT" @@ -13,6 +13,6 @@ def install end test do - assert_match version.to_s, shell_output("#{bin}/robonet --version") + assert_match version.to_s, shell_output("#{bin}/robotnet --version") end end diff --git a/package-lock.json b/package-lock.json index ec96fa5..1d34975 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { - "name": "robonet", + "name": "@robotnetworks/robotnet", "version": "0.1.11", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "robonet", + "name": "@robotnetworks/robotnet", "version": "0.1.11", + "license": "MIT", "dependencies": { "commander": "^13.0.0", "env-paths": "^3.0.0", @@ -14,7 +15,7 @@ "ws": "^8.18.0" }, "bin": { - "robonet": "bin/robonet.js" + "robotnet": "bin/robotnet.js" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/src/api/client.ts b/src/api/client.ts index c714075..98073c0 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -13,7 +13,7 @@ const INITIAL_RETRY_DELAY_MS = 500; const MAX_UPLOAD_BYTES = 50 * 1_048_576; // 50 MB /** - * REST client for the RoboNet backend. All methods throw {@link APIError} on + * REST client for the RobotNet backend. All methods throw {@link APIError} on * network failure or non-2xx response. Transient 429/5xx responses and network * errors are retried internally with exponential backoff. */ @@ -276,7 +276,7 @@ export class APIClient { }); } - /** Search the public RoboNet directory across all agents and workspaces. */ + /** Search the public RobotNet directory across all agents and workspaces. */ async searchDirectory(options: { queryText: string; limit?: number; diff --git a/src/api/models.ts b/src/api/models.ts index 3df1e43..24b7ede 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -1,4 +1,4 @@ -/** Identity of a RoboNet agent; fields are nullable because partial responses from the API may omit IDs or handles. */ +/** Identity of a RobotNet agent; fields are nullable because partial responses from the API may omit IDs or handles. */ export interface AgentIdentity { readonly agentId: string | null; readonly canonicalHandle: string | null; diff --git a/src/auth/pkce.ts b/src/auth/pkce.ts index fcbb435..4111dc3 100644 --- a/src/auth/pkce.ts +++ b/src/auth/pkce.ts @@ -12,7 +12,7 @@ import { } from "../errors.js"; const DEFAULT_LOOPBACK_REDIRECT_URI = "http://127.0.0.1:8788/callback"; -const DEFAULT_PUBLIC_CLIENT_NAME = "robonet-cli"; +const DEFAULT_PUBLIC_CLIENT_NAME = "robotnet-cli"; /** Result of a successful PKCE login: the API access token plus the long-lived data needed to refresh it. */ export interface PKCELoginResult { @@ -62,7 +62,7 @@ export async function performPkceLogin(options: { state, }); - console.log("Opening browser for RoboNet login and agent selection."); + console.log("Opening browser for RobotNet login and agent selection."); console.log(authorizationUrl); const open = (await import("open")).default; await open(authorizationUrl); @@ -312,7 +312,7 @@ function waitForOAuthCallback(options: { statusCode = 400; } else { responseBody = - "Authorization complete. You can close this window and return to RoboNet CLI."; + "Authorization complete. You can close this window and return to RobotNet CLI."; statusCode = 200; } diff --git a/src/commands/config-cmd.ts b/src/commands/config-cmd.ts index 64f85c9..b5380ff 100644 --- a/src/commands/config-cmd.ts +++ b/src/commands/config-cmd.ts @@ -12,7 +12,7 @@ import { jsonOption, profileTitle } from "./shared.js"; export function registerConfigCommand(program: Command): void { const configCmd = program .command("config") - .description("Inspect local RoboNet CLI configuration"); + .description("Inspect local RobotNet CLI configuration"); configCmd .command("show") @@ -26,7 +26,7 @@ export function registerConfigCommand(program: Command): void { } console.log( renderKeyValues( - profileTitle("RoboNet CLI Config", config), + profileTitle("RobotNet CLI Config", config), configToHumanPayload(config), ), ); diff --git a/src/commands/contacts.ts b/src/commands/contacts.ts index 30d02b3..0733e37 100644 --- a/src/commands/contacts.ts +++ b/src/commands/contacts.ts @@ -11,7 +11,7 @@ import { export function registerContactsCommand(program: Command): void { const contactsCmd = program .command("contacts") - .description("Manage RoboNet contacts"); + .description("Manage RobotNet contacts"); contactsCmd .command("list") diff --git a/src/commands/daemon.ts b/src/commands/daemon.ts index 289f3f7..5d27fb7 100644 --- a/src/commands/daemon.ts +++ b/src/commands/daemon.ts @@ -102,7 +102,7 @@ export function registerDaemonCommand(program: Command): void { } else { console.log( renderKeyValues( - profileTitle("RoboNet Daemon Started", config), + profileTitle("RobotNet Daemon Started", config), payload, ), ); @@ -123,7 +123,7 @@ export function registerDaemonCommand(program: Command): void { } else { console.log( renderKeyValues( - profileTitle("RoboNet Daemon Stopped", config), + profileTitle("RobotNet Daemon Stopped", config), payload, ), ); @@ -159,7 +159,7 @@ export function registerDaemonCommand(program: Command): void { } else { console.log( renderKeyValues( - profileTitle("RoboNet Daemon Restarted", config), + profileTitle("RobotNet Daemon Restarted", config), payload, ), ); @@ -182,7 +182,7 @@ export function registerDaemonCommand(program: Command): void { } else { console.log( renderKeyValues( - profileTitle("RoboNet Daemon Status", config), + profileTitle("RobotNet Daemon Status", config), payload, ), ); @@ -251,7 +251,7 @@ export function registerDaemonCommand(program: Command): void { endpoints: config.endpoints, tokenStorePath: config.tokenStoreFile, clientId: opts.clientId ?? null, - clientSecret: opts.clientSecret ?? process.env.ROBONET_CLIENT_SECRET ?? null, + clientSecret: opts.clientSecret ?? process.env.ROBOTNET_CLIENT_SECRET ?? null, scope: opts.scope, }), logger: (message) => console.log(message), diff --git a/src/commands/doctor-cmd.ts b/src/commands/doctor-cmd.ts index 6f0c9aa..acc7994 100644 --- a/src/commands/doctor-cmd.ts +++ b/src/commands/doctor-cmd.ts @@ -8,7 +8,7 @@ import { jsonOption, profileTitle } from "./shared.js"; export function registerDoctorCommand(program: Command): void { program .command("doctor") - .description("Run local RoboNet CLI diagnostics") + .description("Run local RobotNet CLI diagnostics") .addOption(jsonOption()) .action(async (opts, cmd) => { const config = loadConfig(cmd.parent?.opts().profile); @@ -21,7 +21,7 @@ export function registerDoctorCommand(program: Command): void { process.exitCode = ok ? 0 : 1; return; } - console.log(profileTitle("RoboNet Doctor", config)); + console.log(profileTitle("RobotNet Doctor", config)); for (const check of checks) { const status = check.ok ? "ok" : "fail"; console.log(`- ${check.name}: ${status} - ${check.detail}`); diff --git a/src/commands/login.ts b/src/commands/login.ts index 4589fcd..0be808a 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -28,7 +28,7 @@ import { export function registerLoginCommand(program: Command): void { const loginCmd = program .command("login") - .description("Manage local RoboNet login state") + .description("Manage local RobotNet login state") .addOption(clientIdOption()) .addOption(clientSecretOption()) .addOption(scopeOption()) @@ -66,7 +66,7 @@ export function registerLoginCommand(program: Command): void { console.log(renderJson(payload)); } else { console.log( - renderKeyValues(profileTitle("RoboNet Login Stored", config), payload), + renderKeyValues(profileTitle("RobotNet Login Stored", config), payload), ); } }); @@ -116,7 +116,7 @@ export function registerLoginCommand(program: Command): void { console.log(renderJson(payload)); } else { console.log( - renderKeyValues(profileTitle("RoboNet Login Stored", config), payload), + renderKeyValues(profileTitle("RobotNet Login Stored", config), payload), ); } }); @@ -146,7 +146,7 @@ export function registerLoginCommand(program: Command): void { } else { console.log( renderKeyValues( - profileTitle("RoboNet Login Status", config), + profileTitle("RobotNet Login Status", config), payload, ), ); diff --git a/src/commands/messages.ts b/src/commands/messages.ts index 6577776..a2fa4da 100644 --- a/src/commands/messages.ts +++ b/src/commands/messages.ts @@ -13,7 +13,7 @@ import { export function registerMessagesCommand(program: Command): void { const messagesCmd = program .command("messages") - .description("Read and send RoboNet messages"); + .description("Read and send RobotNet messages"); messagesCmd .command("send") diff --git a/src/commands/threads.ts b/src/commands/threads.ts index 4a85a93..05506ae 100644 --- a/src/commands/threads.ts +++ b/src/commands/threads.ts @@ -14,7 +14,7 @@ import { export function registerThreadsCommand(program: Command): void { const threadsCmd = program .command("threads") - .description("Read RoboNet threads"); + .description("Read RobotNet threads"); threadsCmd .command("list") diff --git a/src/daemon/manager.ts b/src/daemon/manager.ts index 4f39fac..42b9bde 100644 --- a/src/daemon/manager.ts +++ b/src/daemon/manager.ts @@ -8,7 +8,7 @@ import { epochMillis, loadDaemonState, saveDaemonState } from "./state.js"; import { DaemonError } from "../errors.js"; const STATE_FILE_NAME = "daemon.json"; -const PID_FILE_NAME = "robonet.pid"; +const PID_FILE_NAME = "robotnet.pid"; const MAX_LOG_BYTES = 5 * 1_048_576; // 5 MB const LOG_FILE_NAME = "listener.log"; @@ -93,7 +93,7 @@ export function startDaemon(options: { const logFd = fs.openSync(paths.logFile, "a"); const thisDir = path.dirname(fileURLToPath(import.meta.url)); - const binPath = path.resolve(thisDir, "../../bin/robonet.js"); + const binPath = path.resolve(thisDir, "../../bin/robotnet.js"); const args = [ binPath, "daemon", @@ -106,10 +106,10 @@ export function startDaemon(options: { const env: Record = { ...process.env, - ROBONET_PROFILE: config.profile, + ROBOTNET_PROFILE: config.profile, }; if (clientSecret) { - env.ROBONET_CLIENT_SECRET = clientSecret; + env.ROBOTNET_CLIENT_SECRET = clientSecret; } const child = child_process.spawn(process.execPath, args, { diff --git a/src/realtime/listener.ts b/src/realtime/listener.ts index 8acabc4..96bead3 100644 --- a/src/realtime/listener.ts +++ b/src/realtime/listener.ts @@ -46,12 +46,12 @@ function log(message: string, logger: LogFn): void { logger(`[${timestamp()}] ${message}`); } -/** Human-readable notice explaining that RoboNet realtime events are live-only (not replayed after reconnect). */ +/** Human-readable notice explaining that RobotNet realtime events are live-only (not replayed after reconnect). */ export function liveNotificationNotice(agentRefValue: string): string { return ( `Agent-scoped live notifications for ${agentRefValue}. ` + - "Events are not replayed; after reconnect, use `robonet threads get ` " + - "or `robonet messages search` to catch up from the REST API." + "Events are not replayed; after reconnect, use `robotnet threads get ` " + + "or `robotnet messages search` to catch up from the REST API." ); } diff --git a/tests/config.test.ts b/tests/config.test.ts index d23c1c3..74908e9 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -17,11 +17,11 @@ afterEach(() => { }); describe("defaultPaths", () => { - it("default profile uses root robonet dirs", () => { + it("default profile uses root robotnet dirs", () => { const paths = defaultPaths("default"); - assert.equal(paths.configDir, path.join(env.tmpDir, "config", "robonet")); - assert.equal(paths.stateDir, path.join(env.tmpDir, "state", "robonet")); + assert.equal(paths.configDir, path.join(env.tmpDir, "config", "robotnet")); + assert.equal(paths.stateDir, path.join(env.tmpDir, "state", "robotnet")); }); it("named profile uses profiles subdirectories", () => { @@ -29,11 +29,11 @@ describe("defaultPaths", () => { assert.equal( paths.configDir, - path.join(env.tmpDir, "config", "robonet", "profiles", "work"), + path.join(env.tmpDir, "config", "robotnet", "profiles", "work"), ); assert.equal( paths.stateDir, - path.join(env.tmpDir, "state", "robonet", "profiles", "work"), + path.join(env.tmpDir, "state", "robotnet", "profiles", "work"), ); }); }); @@ -49,7 +49,7 @@ describe("loadConfig", () => { path.join( env.tmpDir, "config", - "robonet", + "robotnet", "profiles", "ops", "auth.json", @@ -58,7 +58,7 @@ describe("loadConfig", () => { }); it("honors endpoint env overrides", () => { - process.env.ROBONET_API_BASE_URL = "https://api.example.test/v1"; + process.env.ROBOTNET_API_BASE_URL = "https://api.example.test/v1"; const config = loadConfig(undefined, { cwd: env.tmpDir }); @@ -72,8 +72,8 @@ describe("loadConfig", () => { assert.equal(config.profileSource.kind, "default"); }); - it("reads profile from ROBONET_PROFILE env var", () => { - process.env.ROBONET_PROFILE = "work"; + it("reads profile from ROBOTNET_PROFILE env var", () => { + process.env.ROBOTNET_PROFILE = "work"; const config = loadConfig(undefined, { cwd: env.tmpDir }); @@ -84,7 +84,7 @@ describe("loadConfig", () => { describe("workspace profile config", () => { function setupWorkspace(dir: string, profileName: string): string { - const wsDir = path.join(dir, ".robonet"); + const wsDir = path.join(dir, ".robotnet"); fs.mkdirSync(wsDir, { recursive: true }); const file = path.join(wsDir, "config.json"); fs.writeFileSync(file, JSON.stringify({ profile: profileName })); @@ -137,10 +137,10 @@ describe("workspace profile config", () => { assert.equal(config.profileSource.kind, "flag"); }); - it("ROBONET_PROFILE env var wins over workspace file", () => { + it("ROBOTNET_PROFILE env var wins over workspace file", () => { setupProfileDir("work"); setupWorkspace(env.tmpDir, "work"); - process.env.ROBONET_PROFILE = "env-profile"; + process.env.ROBOTNET_PROFILE = "env-profile"; const config = loadConfig(undefined, { cwd: env.tmpDir }); @@ -156,12 +156,12 @@ describe("workspace profile config", () => { (err: unknown) => err instanceof ConfigurationError && err.message.includes("ghost") && - err.message.includes("robonet --profile ghost login"), + err.message.includes("robotnet --profile ghost login"), ); }); it("throws on malformed workspace JSON", () => { - const wsDir = path.join(env.tmpDir, ".robonet"); + const wsDir = path.join(env.tmpDir, ".robotnet"); fs.mkdirSync(wsDir, { recursive: true }); fs.writeFileSync(path.join(wsDir, "config.json"), "{not json"); diff --git a/tests/daemon-state.test.ts b/tests/daemon-state.test.ts index ed9d116..749f8f3 100644 --- a/tests/daemon-state.test.ts +++ b/tests/daemon-state.test.ts @@ -12,7 +12,7 @@ import { } from "../src/daemon/state.js"; function tmpFile(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "robonet-test-")); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "robotnet-test-")); return path.join(dir, "daemon.json"); } @@ -56,7 +56,7 @@ describe("saveDaemonState / loadDaemonState", () => { }); it("returns null for missing file", () => { - assert.equal(loadDaemonState("/tmp/nonexistent-robonet.json"), null); + assert.equal(loadDaemonState("/tmp/nonexistent-robotnet.json"), null); }); it("returns null for invalid JSON", () => { diff --git a/tests/events.test.ts b/tests/events.test.ts index 8122e2b..34adc02 100644 --- a/tests/events.test.ts +++ b/tests/events.test.ts @@ -78,7 +78,7 @@ describe("liveNotificationNotice", () => { assert.ok(notice.includes("Agent-scoped live notifications for nick.me")); assert.ok(notice.includes("Events are not replayed")); - assert.ok(notice.includes("robonet threads get ")); - assert.ok(notice.includes("robonet messages search")); + assert.ok(notice.includes("robotnet threads get ")); + assert.ok(notice.includes("robotnet messages search")); }); }); diff --git a/tests/listener.test.ts b/tests/listener.test.ts index 439e5dc..5704fa4 100644 --- a/tests/listener.test.ts +++ b/tests/listener.test.ts @@ -49,7 +49,7 @@ describe("listenForever", () => { sessionFactory: async () => { calls += 1; throw new AuthenticationError( - "No usable stored login found. Run `robonet login` first.", + "No usable stored login found. Run `robotnet login` first.", ); }, logger: () => {}, diff --git a/tests/shared.test.ts b/tests/shared.test.ts index da1b299..dddb62a 100644 --- a/tests/shared.test.ts +++ b/tests/shared.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "node:test"; import * as assert from "node:assert/strict"; import { parsePositiveInt, parseThreadStatus } from "../src/commands/shared.js"; -import { RoboNetCLIError } from "../src/errors.js"; +import { RobotNetCLIError } from "../src/errors.js"; describe("parsePositiveInt", () => { it("parses a valid positive integer", () => { @@ -48,7 +48,7 @@ describe("parseThreadStatus", () => { assert.throws( () => parseThreadStatus("unread"), (err: unknown) => - err instanceof RoboNetCLIError && + err instanceof RobotNetCLIError && err.message === "Invalid thread status: unread. Expected one of: active, closed, archived.", ); diff --git a/tests/threads-command.test.ts b/tests/threads-command.test.ts index 2da36fb..39013b2 100644 --- a/tests/threads-command.test.ts +++ b/tests/threads-command.test.ts @@ -3,7 +3,7 @@ import * as assert from "node:assert/strict"; import { Command } from "commander"; import { registerThreadsCommand } from "../src/commands/threads.js"; -import { RoboNetCLIError } from "../src/errors.js"; +import { RobotNetCLIError } from "../src/errors.js"; describe("threads command", () => { it("rejects unsupported thread status filters before making API calls", async () => { @@ -15,14 +15,14 @@ describe("threads command", () => { () => program.parseAsync([ "node", - "robonet", + "robotnet", "threads", "list", "--status", "unread", ]), (err: unknown) => - err instanceof RoboNetCLIError && + err instanceof RobotNetCLIError && err.message === "Invalid thread status: unread. Expected one of: active, closed, archived.", ); diff --git a/tests/version.test.ts b/tests/version.test.ts index ec39370..8186711 100644 --- a/tests/version.test.ts +++ b/tests/version.test.ts @@ -11,8 +11,8 @@ describe("version", () => { assert.equal(CLI_VERSION, pkg.version); }); - it("USER_AGENT starts with robonet-cli/", () => { - assert.match(USER_AGENT, new RegExp(`^robonet-cli/${pkg.version.replace(/\./g, "\\.")}\\b`)); + it("USER_AGENT starts with robotnet-cli/", () => { + assert.match(USER_AGENT, new RegExp(`^robotnet-cli/${pkg.version.replace(/\./g, "\\.")}\\b`)); }); it("USER_AGENT includes node/ token", () => {