New:Socket for Asana Is Now Available.Learn more
Get Started

@sandbaseai/cli

Package Overview
Dependencies
Maintainers
2
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sandbaseai/cli - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
+4
-2
dist/adapters/index.js

@@ -5,2 +5,3 @@ import { mkdir, readFile } from "node:fs/promises";

import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { parse as parseToml } from "smol-toml";

@@ -12,4 +13,5 @@ import { parseDocument } from "yaml";

const defaultIO = { backup, write: atomicWrite, restore };
export function detectClient(client) { const profile = clientProfiles[client]; if (!profile.executable || profile.mode !== "auto")
return { installed: true, detail: "manual setup" }; const result = spawnSync(profile.executable, ["--version"], { encoding: "utf8", timeout: 5000 }); const detail = (result.stdout || result.stderr || "").trim().split("\n")[0] || "not found"; return { installed: result.status === 0, detail }; }
export function detectClient(client) { const profile = clientProfiles[client]; if (profile.mode !== "auto")
return { installed: true, detail: "manual setup" }; if (!profile.executable)
return existsSync(configPath(client)) ? { installed: true, detail: "existing configuration" } : { installed: false, detail: "configuration not found" }; const result = spawnSync(profile.executable, ["--version"], { encoding: "utf8", timeout: 5000 }); const detail = (result.stdout || result.stderr || "").trim().split("\n")[0] || "not found"; return { installed: result.status === 0, detail }; }
const start = "# >>> sandbase managed >>>", end = "# <<< sandbase managed <<<";

@@ -16,0 +18,0 @@ function block(client, bridge) {

@@ -1,2 +0,2 @@

import type { Client, Exchange, Grant } from "../types.js";
import type { ConnectClient, Exchange, Grant } from "../types.js";
export interface Secrets {

@@ -20,3 +20,3 @@ requestId: string;

private request;
create(client: Client, s: Secrets): Promise<Grant>;
create(client: ConnectClient, s: Secrets): Promise<Grant>;
status(id: string, secret: string): Promise<{

@@ -23,0 +23,0 @@ status: string;

@@ -1,2 +0,2 @@

import type { Client, Exchange } from "../types.js";
import type { ConnectClient, Exchange } from "../types.js";
import { AuthorizationApi } from "./api.js";

@@ -9,5 +9,5 @@ export interface FlowOptions {

}
export declare function authorize(api: AuthorizationApi, client: Client, options: FlowOptions): Promise<{
export declare function authorize(api: AuthorizationApi, client: ConnectClient, options: FlowOptions): Promise<{
authorizationId: string;
exchange: Exchange;
}>;
#!/usr/bin/env node
import { connect, doctor, unregister } from "./commands.js";
import { detectClient } from "./adapters/index.js";
import { clientList, clientProfiles } from "./clients.js";
import { clients, isClient } from "./types.js";
import { clientList } from "./clients.js";
import { isClient } from "./types.js";
if (Number(process.versions.node.split(".")[0]) < 20) {

@@ -13,11 +12,5 @@ console.error("SandBase CLI requires Node.js 20 or newer. Upgrade Node.js and retry.");

const value = at >= 0 ? args[at + 1] : undefined;
function autoClient() {
return clients.find(client => {
const profile = clientProfiles[client];
return profile.mode === "auto" && !!profile.executable && detectClient(client).installed;
});
}
const resolved = value === "auto" || (!value && command === "connect") ? autoClient() : value && isClient(value) ? value : undefined;
if (!command || !resolved || (command !== "connect" && !value) || !["connect", "doctor", "unregister"].includes(command)) {
console.error(`Usage: sandbase <connect|doctor|unregister> --client <${clientList()}|auto>`);
const client = value === undefined || value === "auto" ? "auto" : isClient(value) ? value : undefined;
if (!command || !client || !["connect", "doctor", "unregister"].includes(command) || (at >= 0 && !value)) {
console.error(`Usage: sandbase <connect|doctor|unregister> [--client <${clientList()}|auto>]`);
process.exit(2);

@@ -30,9 +23,9 @@ }

if (command === "connect")
await connect(resolved, { signal: controller.signal });
await connect(client, { signal: controller.signal });
else if (command === "doctor") {
if (!(await doctor(resolved)))
if (!(await doctor(client)))
process.exitCode = 1;
}
else
await unregister(resolved);
await unregister(client);
}

@@ -44,4 +37,4 @@ catch (e) {

else
console.error(message.replace(/sk-[A-Za-z0-9_-]+/g, "[REDACTED]"));
console.error(message.replace(/(?:sk|cln)-[A-Za-z0-9_-]+/g, "[REDACTED]"));
process.exitCode = 1;
}

@@ -12,1 +12,2 @@ import type { Client } from "./types.js";

export declare function clientList(): string;
export declare function autoClients(): Client[];

@@ -30,1 +30,4 @@ export const clientProfiles = {

}
export function autoClients() {
return Object.values(clientProfiles).filter(profile => profile.mode === "auto" && !!profile.adapter).map(profile => profile.id);
}

@@ -1,17 +0,20 @@

import type { Client } from "./types.js";
import type { Client, ConnectClient } from "./types.js";
import { AuthorizationApi } from "./auth/api.js";
import { FileCredentialStore } from "./credentials/store.js";
export declare function openBrowser(url: string): Promise<void>;
export declare function connect(client: Client, deps?: {
import { type CredentialStore } from "./credentials/store.js";
type Detection = {
installed: boolean;
detail: string;
};
export interface CommandDependencies {
api?: AuthorizationApi;
store?: FileCredentialStore;
store?: CredentialStore;
open?: (url: string) => Promise<void>;
signal?: AbortSignal;
log?: (s: string) => void;
detect?: (client: Client) => {
installed: boolean;
detail: string;
};
}): Promise<void>;
export declare function doctor(client: Client, store?: FileCredentialStore): Promise<boolean>;
export declare function unregister(client: Client, store?: FileCredentialStore): Promise<void>;
detect?: (client: Client) => Detection;
}
export declare function openBrowser(url: string): Promise<void>;
export declare function connect(client?: ConnectClient, deps?: CommandDependencies): Promise<void>;
export declare function doctor(client?: ConnectClient, store?: CredentialStore, detect?: (client: Client) => Detection): Promise<boolean>;
export declare function unregister(client?: ConnectClient, store?: CredentialStore, detect?: (client: Client) => Detection): Promise<void>;
export {};

@@ -5,3 +5,3 @@ import { spawn } from "node:child_process";

import { authorize } from "./auth/flow.js";
import { clientProfiles } from "./clients.js";
import { autoClients, clientProfiles } from "./clients.js";
import { FileCredentialStore } from "./credentials/store.js";

@@ -20,26 +20,29 @@ import { configure, detectClient, installBridge, isConfigured, rollback, rollbackBridge, unregister as removeAdapter } from "./adapters/index.js";

export async function openBrowser(url) { const cmd = platform() === "darwin" ? "open" : platform() === "win32" ? "cmd" : "xdg-open"; const args = platform() === "win32" ? ["/c", "start", "", url] : [url]; const child = spawn(cmd, args, { detached: true, stdio: "ignore" }); child.unref(); }
function mcpServerSnippet(client, bridgePath) {
return JSON.stringify({ mcpServers: { sandbase: { command: "node", args: [bridgePath, "--client", client] } } }, null, 2);
}
function nextStepPrompts(label) {
return [`2. In ${label}, try asking:`, ` - List the available SandBase MCP tools.`, ` - Use SandBase to fetch Elon Musk's latest 10 posts on Twitter.`];
}
function mcpServerSnippet(client, bridgePath) { return JSON.stringify({ mcpServers: { sandbase: { command: "node", args: [bridgePath, "--client", client] } } }, null, 2); }
function nextStepPrompts(label) { return [`2. In ${label}, try asking:`, " - List the available SandBase MCP tools.", " - Use SandBase to fetch Elon Musk's latest 10 posts on Twitter."]; }
function successMessage(client, record, bridgePath, configuredPath) {
const profile = clientProfiles[client];
const label = profile.label;
const common = [``, `SandBase MCP is connected.`, ``, `Client: ${label} (${client})`, `MCP URL: ${record.mcpUrl}`, `Credential: ${record.keyPrefix}... (${record.scope.join(",")})`, `Bridge: ${bridgePath}`];
const common = ["", "SandBase MCP is connected.", "", `Client: ${label} (${client})`, `MCP URL: ${record.mcpUrl}`, `Credential: ${record.keyPrefix}... (${record.scope.join(",")})`, `Bridge: ${bridgePath}`];
if (profile.mode === "auto")
return [...common, `Config: ${configuredPath || "updated"}`, ``, `Next steps:`, `1. Restart or reload ${label} so it picks up the new MCP configuration.`, ...nextStepPrompts(label), ``, `Manage access: revoke the "CLI Login" key in SandBase Dashboard when you no longer need it.`].join("\n");
return [...common, `Config: ${configuredPath || "updated"}`, "", "Next steps:", `1. Restart or reload ${label} so it picks up the new MCP configuration.`, ...nextStepPrompts(label), "", 'Manage access: revoke the "CLI Login" key in SandBase Dashboard when you no longer need it.'].join("\n");
if (profile.mode === "skill")
return [...common, ``, `Skill/prompt setup required.`, `Copy the following instruction into ${label}:`, ``, `Install the SandBase MCP bridge for ${label}. Use this local MCP server configuration and keep the scope limited to SandBase:`, mcpServerSnippet(client, bridgePath), `After setup, reload ${label} if needed.`, ``, `Next steps:`, `1. Finish the skill/prompt setup in ${label}.`, ...nextStepPrompts(label), ``, `Manage access: revoke the "CLI Login" key in SandBase Dashboard when you no longer need it.`].join("\n");
return [...common, ``, `Manual MCP configuration required.`, `Add this MCP server configuration to ${label}:`, ``, mcpServerSnippet(client, bridgePath), ``, `Next steps:`, `1. Save the configuration and restart or reload ${label}.`, ...nextStepPrompts(label), ``, `Manage access: revoke the "CLI Login" key in SandBase Dashboard when you no longer need it.`].join("\n");
return [...common, "", "Skill/prompt setup required.", `Copy the following instruction into ${label}:`, "", `Install the SandBase MCP bridge for ${label}. Use this local MCP server configuration and keep the scope limited to SandBase:`, mcpServerSnippet(client, bridgePath), `After setup, reload ${label} if needed.`, "", "Next steps:", "1. Finish the skill/prompt setup in ${label}.", ...nextStepPrompts(label), "", 'Manage access: revoke the "CLI Login" key in SandBase Dashboard when you no longer need it.'].join("\n");
return [...common, "", "Manual MCP configuration required.", `Add this MCP server configuration to ${label}:`, "", mcpServerSnippet(client, bridgePath), "", "Next steps:", `1. Save the configuration and restart or reload ${label}.`, ...nextStepPrompts(label), "", 'Manage access: revoke the "CLI Login" key in SandBase Dashboard when you no longer need it.'].join("\n");
}
export async function connect(client, deps = {}) {
function plannedAutoClients(detect) { return autoClients().filter(client => detect(client).installed); }
function recordFor(client, exchange) { return { credential: exchange.credential, credentialId: exchange.credential_id, keyPrefix: exchange.key_prefix, client, scope: exchange.scope, mcpUrl: exchange.mcp_url, createdAt: exchange.created_at }; }
async function restoreCredential(store, client, previous) { if (previous)
await store.save(previous);
else
await store.remove(client); }
async function connectOne(client, deps) {
const api = deps.api || new AuthorizationApi((process.env.SANDBASE_API_URL || "https://sandbase.ai").replace(/\/$/, ""));
const store = deps.store || new FileCredentialStore();
const log = deps.log || console.log;
const detect = deps.detect || detectClient;
const profile = clientProfiles[client];
const detected = (deps.detect || detectClient)(client);
const detected = detect(client);
if (!detected.installed)
throw new Error(`${profile.label} is not installed or is incompatible (${detected.detail}). Install a supported version, then retry.`);
throw new Error(`${profile.label} is not installed or is incompatible. Install a supported version, then retry.`);
const previous = await store.get(client);

@@ -50,3 +53,3 @@ const { authorizationId, exchange } = await authorize(api, client, { open: deps.open || openBrowser, sleep, log, ...(deps.signal ? { signal: deps.signal } : {}) });

try {
const record = { credential: exchange.credential, credentialId: exchange.credential_id, keyPrefix: exchange.key_prefix, client, scope: exchange.scope, mcpUrl: exchange.mcp_url, createdAt: exchange.created_at };
const record = recordFor(client, exchange);
await store.save(record);

@@ -65,6 +68,3 @@ bridgeResult = await installBridge();

await rollbackBridge(bridgeResult).catch(() => undefined);
if (previous)
await store.save(previous).catch(() => undefined);
else
await store.remove(client).catch(() => undefined);
await restoreCredential(store, client, previous).catch(() => undefined);
if (!(await compensate(api, authorizationId, exchange.cleanup_token)))

@@ -76,3 +76,81 @@ console.error("WARNING: automatic credential cleanup failed. Revoke the new CLI credential in SandBase Dashboard immediately.");

}
export async function doctor(client, store = new FileCredentialStore()) { const credential = await store.get(client); const profile = clientProfiles[client]; const configured = profile.mode === "auto" ? await isConfigured(client) : !!credential; const detected = detectClient(client); console.log(`${client}: mode=${profile.mode}, installed=${detected.installed ? "yes" : "no"}, config=${configured ? profile.mode === "auto" ? "ok" : "manual" : "missing"}, credential=${credential ? credential.keyPrefix + "…" : "missing"}, url=${credential?.mcpUrl || "unknown"}, scope=${credential?.scope.join(",") || "unknown"}`); return detected.installed && !!credential && configured; }
export async function unregister(client, store = new FileCredentialStore()) { const removed = await removeAdapter(client); await store.remove(client); console.log(`Removed local SandBase registration for ${client}.${removed ? "" : " No managed client configuration was changed."} Revoke the server credential in SandBase Dashboard if it is no longer needed.`); }
async function connectAuto(deps) {
const api = deps.api || new AuthorizationApi((process.env.SANDBASE_API_URL || "https://sandbase.ai").replace(/\/$/, ""));
const store = deps.store || new FileCredentialStore();
const log = deps.log || console.log;
const detect = deps.detect || detectClient;
const targets = plannedAutoClients(detect);
if (!targets.length) {
log("No installed compatible clients support automatic configuration. Use --client <client> for manual or skill setup guidance.");
return;
}
const { authorizationId, exchange } = await authorize(api, "auto", { open: deps.open || openBrowser, sleep, log, ...(deps.signal ? { signal: deps.signal } : {}) });
let bridgeResult;
const succeeded = [], failed = [];
try {
bridgeResult = await installBridge();
for (const client of targets) {
const previous = await store.get(client);
let configured;
try {
const record = recordFor(client, exchange);
await store.save(record);
configured = await configure(client, bridgeResult.path);
if (!(await isConfigured(client)))
throw new Error("Client configuration verification failed");
succeeded.push(client);
}
catch {
if (configured)
await rollback(configured).catch(() => undefined);
await restoreCredential(store, client, previous).catch(() => undefined);
failed.push(client);
}
}
if (!succeeded.length)
throw new Error("No clients were configured successfully.");
log(`Configured clients: ${succeeded.join(", ")}.`);
if (failed.length)
log(`Partial success: failed clients were rolled back: ${failed.join(", ")}.`);
exchange.cleanup_token = "";
}
catch (e) {
if (bridgeResult)
await rollbackBridge(bridgeResult).catch(() => undefined);
if (!(await compensate(api, authorizationId, exchange.cleanup_token)))
console.error("WARNING: automatic credential cleanup failed. Revoke the new CLI credential in SandBase Dashboard immediately.");
exchange.cleanup_token = "";
throw e;
}
}
export async function connect(client = "auto", deps = {}) { if (client === "auto")
return connectAuto(deps); return connectOne(client, deps); }
export async function doctor(client = "auto", store = new FileCredentialStore(), detect = detectClient) {
const targets = client === "auto" ? plannedAutoClients(detect) : [client];
if (!targets.length) {
console.log("No installed compatible clients support automatic configuration.");
return false;
}
let healthy = true;
for (const target of targets) {
const credential = await store.get(target);
const profile = clientProfiles[target];
const configured = profile.mode === "auto" ? await isConfigured(target) : !!credential;
const detected = detect(target);
console.log(`${target}: mode=${profile.mode}, installed=${detected.installed ? "yes" : "no"}, config=${configured ? profile.mode === "auto" ? "ok" : "manual" : "missing"}, credential=${credential ? credential.keyPrefix + "…" : "missing"}, url=${credential?.mcpUrl || "unknown"}, scope=${credential?.scope.join(",") || "unknown"}`);
healthy = healthy && detected.installed && !!credential && configured;
}
return healthy;
}
export async function unregister(client = "auto", store = new FileCredentialStore(), detect = detectClient) {
const targets = client === "auto" ? plannedAutoClients(detect) : [client];
if (!targets.length) {
console.log("No installed compatible clients support automatic configuration.");
return;
}
for (const target of targets) {
const removed = await removeAdapter(target);
await store.remove(target);
console.log(`Removed local SandBase registration for ${target}.${removed ? "" : " No managed client configuration was changed."} Revoke the server credential in SandBase Dashboard if it is no longer needed.`);
}
}
export declare const clients: readonly ["codex", "claude-code", "cursor", "windsurf", "gemini-cli", "opencode", "chatgpt", "hermes", "openclaw", "antigravity", "claude-desktop", "cursor-cli", "warp", "trae", "kimi-cli", "qwen-code", "kiro-cli", "amp", "crush", "iflow-cli", "qoder", "workbuddy", "cowork", "pi"];
export type Client = typeof clients[number];
export type ConnectClient = Client | "auto";
export declare function isClient(value: string): value is Client;

@@ -24,3 +25,3 @@ export interface CredentialRecord {

key_prefix: string;
client: Client;
client: ConnectClient;
scope: string[];

@@ -27,0 +28,0 @@ mcp_url: string;

{
"name": "@sandbaseai/cli",
"version": "0.1.2",
"version": "0.1.3",
"description": "Secure SandBase MCP onboarding CLI",

@@ -5,0 +5,0 @@ "type": "module",