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.5
to
0.1.6
+44
-9
dist/adapters/index.js

@@ -12,8 +12,11 @@ import { mkdir, readFile } from "node:fs/promises";

const defaultIO = { backup, write: atomicWrite, restore };
export function detectClient(client) { const profile = clientProfiles[client]; if (!profile.executable)
return profile.mode === "auto" && existsSync(configPath(client)) ? { installed: true, detail: "existing configuration" } : { installed: false, detail: "not detected" }; 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 ownershipEnvironment = "SANDBASE_CLI_MANAGED";
export function detectClient(client) { const profile = clientProfiles[client]; const path = configPath(client); if (!profile.executable)
return profile.mode === "auto" && existsSync(path) ? { installed: true, detail: "existing configuration" } : { installed: false, detail: "not detected" }; const result = spawnSync(profile.executable, ["--version"], { encoding: "utf8", timeout: 5000 }); const detail = (result.stdout || result.stderr || "").trim().split("\n")[0] || "not found"; if (result.status === 0)
return { installed: true, detail }; if (client === "codex" && existsSync(path))
return { installed: true, detail: "Codex CLI probe failed; existing Codex configuration detected" }; return { installed: false, detail }; }
const start = "# >>> sandbase managed >>>", end = "# <<< sandbase managed <<<";
function block(client, bridge) {
if (client === "codex")
return `${start}\n[mcp_servers.sandbase]\ncommand = "node"\nargs = ["${bridge.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}", "--client", "codex"]\n${end}`;
return `${start}\n[mcp_servers.sandbase]\ncommand = "node"\nargs = ["${bridge.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}", "--client", "codex"]\nenv = { SANDBASE_CLI_MANAGED = "1" }\n${end}`;
return ` ${start}\n sandbase:\n command: node\n args:\n - ${JSON.stringify(bridge)}\n - --client\n - ${client}\n ${end}`;

@@ -35,8 +38,15 @@ }

return cleaned.replace(/^mcp_servers:[ \t]*$/m, match => `${match}\n${own}`); return cleaned.replace(/\s*$/, "") + (cleaned.trim() ? "\n" : "") + `mcp_servers:\n${own}\n`; }
function ownedJsonEntry(value, client) { if (!value || typeof value !== "object" || Array.isArray(value))
return false; const entry = value; return entry.command === "node" && Array.isArray(entry.args) && (client === undefined || entry.args.includes(client)) && entry.env?.[ownershipEnvironment] === "1"; }
function ownedCodexEntry(value) { if (!value || typeof value !== "object" || Array.isArray(value))
return false; const entry = value; return entry.command === "node" && Array.isArray(entry.args) && entry.args.length === 3 && typeof entry.args[0] === "string" && entry.args[0].endsWith("sandbase-mcp-bridge.mjs") && entry.args[1] === "--client" && entry.args[2] === "codex" && entry.env?.[ownershipEnvironment] === "1"; }
function jsonUpdate(raw, client, bridge, remove = false) { const obj = raw ? JSON.parse(raw) : {}; if (!obj || Array.isArray(obj))
throw new Error("Client configuration root must be an object"); const servers = obj.mcpServers === undefined ? {} : obj.mcpServers; if (!servers || typeof servers !== "object" || Array.isArray(servers))
throw new Error("mcpServers must be an object"); const map = servers; if (remove)
delete map.sandbase;
throw new Error("mcpServers must be an object"); const map = servers; const existing = map.sandbase; if (existing !== undefined && !ownedJsonEntry(existing, client))
throw new Error("A non-SandBase MCP entry named sandbase was left untouched"); if (remove) {
if (existing !== undefined)
delete map.sandbase;
}
else
map.sandbase = { command: "node", args: [bridge, "--client", client] }; obj.mcpServers = map; return JSON.stringify(obj, null, 2) + "\n"; }
map.sandbase = { command: "node", args: [bridge, "--client", client], env: { [ownershipEnvironment]: "1" } }; obj.mcpServers = map; return JSON.stringify(obj, null, 2) + "\n"; }
function validateText(client, text) { if (!text.trim())

@@ -69,3 +79,10 @@ return; const adapter = clientProfiles[client].adapter; if (adapter === "codex") {

validateText(client, text);
next = profile.adapter === "json" ? jsonUpdate(raw, client, bridge) : profile.adapter === "yaml" ? hermesUpdate(text, bridge, client) : managedText(text.includes(start) ? text : removeCodexEntry(text), block(client, bridge));
if (profile.adapter === "codex" && /\[mcp_servers\.sandbase\]/.test(text)) {
const parsed = parseToml(text);
if (!text.includes(start) || !ownedCodexEntry(parsed.mcp_servers?.sandbase))
throw new Error("A non-SandBase MCP entry named sandbase was left untouched");
}
if (profile.adapter === "yaml" && /^ sandbase:[ \t]*$/m.test(text) && !text.includes(start))
throw new Error("A non-SandBase MCP entry named sandbase was left untouched");
next = profile.adapter === "json" ? jsonUpdate(raw, client, bridge) : profile.adapter === "yaml" ? hermesUpdate(text, bridge, client) : managedText(text.includes(start) ? text : text, block(client, bridge));
validateText(client, next);

@@ -89,2 +106,9 @@ }

validateText(client, raw);
if (profile.adapter !== "json" && !raw.includes(start))
return false;
if (profile.adapter === "codex") {
const parsed = parseToml(raw);
if (!ownedCodexEntry(parsed.mcp_servers?.sandbase))
return false;
}
next = profile.adapter === "json" ? jsonUpdate(raw, client, "", true) : profile.adapter === "codex" ? removeCodexEntry(removeManaged(raw)) : removeHermesEntry(removeManaged(raw));

@@ -108,3 +132,3 @@ validateText(client, next);

const o = JSON.parse(raw);
return !!o.mcpServers?.sandbase;
return ownedJsonEntry(o.mcpServers?.sandbase, client);
}

@@ -114,2 +138,13 @@ catch {

}
} return raw.includes(start) && raw.includes(end); }
} if (!raw.includes(start) || !raw.includes(end))
return false; try {
if (profile.adapter === "codex") {
const parsed = parseToml(raw);
return ownedCodexEntry(parsed.mcp_servers?.sandbase);
}
const parsed = parseDocument(raw).toJS();
return !!parsed?.mcp_servers?.sandbase;
}
catch {
return false;
} }

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

}
export type RuntimeStatus = "configured" | "already_configured" | "confirmation_required" | "unsupported" | "failed";
export type ImplementationState = "implemented" | "blocked";
export interface Evidence {
source: string;
retrieved: string;
conclusion: string;
kind: "first_party" | "internal_blocker";
}
export interface InstallerDescriptor {
mcp: "adapter" | "confirmation" | "none";
skill: "shared" | "private" | "confirmation" | "none";
}
export interface ValidatorDescriptor {
mcp: "read_back" | "confirmation" | "none";
skill: "ownership_checksum" | "real_client_matrix" | "confirmation" | "none";
}
export interface CapabilityV2 {
evidence: Evidence;
installer: InstallerDescriptor;
validator: ValidatorDescriptor;
implementation: ImplementationState;
status: RuntimeStatus;
nextStep: string;
}
export interface ClientProfile {

@@ -29,4 +53,5 @@ id: Client;

export declare const nativeCapabilities: Record<Client, NativeCapability>;
export declare const capabilityRegistry: Record<Client, CapabilityV2>;
export declare function assertNativeCapabilities(): void;
export declare function clientList(): string;
export declare function autoClients(): Client[];

@@ -28,3 +28,3 @@ export const clientProfiles = {

export const skillTiers = {
codex: "s3_promotion", "claude-code": "s3_fallback", cursor: "s3_promotion", hermes: "s3_fallback",
codex: "s3_promotion", "claude-code": "s1_slash", cursor: "s3_promotion", hermes: "s3_fallback",
windsurf: "s4_none", "gemini-cli": "s4_none", opencode: "s4_none", chatgpt: "s4_none", openclaw: "s4_none", antigravity: "s4_none", "claude-desktop": "s4_none", "cursor-cli": "s4_none", warp: "s4_none", trae: "s4_none", "kimi-cli": "s4_none", "qwen-code": "s4_none", "kiro-cli": "s4_none", amp: "s4_none", crush: "s4_none", "iflow-cli": "s4_none", qoder: "s4_none", workbuddy: "s4_none", cowork: "s4_none", pi: "s4_none",

@@ -35,5 +35,5 @@ };

codex: { mcpMode: "auto", skillMode: "shared_skill", invocation: "skill_picker", verification: "read_only_probe", guide: guide("Codex", "Restart Codex and use its Skill picker to select SandBase; do not assume a slash command.", "SandBase MCP tools are visible in the session."), uninstall: "managed_only", terminal: "action_required" },
"claude-code": { mcpMode: "auto", skillMode: "shared_skill", invocation: "mcp_chat", verification: "read_only_probe", guide: guide("Claude Code", "Restart Claude Code and use its configured MCP tools; native Skill discovery is not claimed.", "SandBase MCP tools are visible in chat."), uninstall: "managed_only", terminal: "action_required" },
"claude-code": { mcpMode: "auto", skillMode: "client_skill", invocation: "slash", verification: "real_client_matrix", guide: guide("Claude Code", "Restart Claude Code; native Skill discovery remains unverified until the real-client matrix is complete.", "SandBase MCP tools and the private Skill are installed."), uninstall: "managed_only", terminal: "action_required" },
cursor: { mcpMode: "auto", skillMode: "shared_skill", invocation: "slash", verification: "real_client_matrix", guide: guide("Cursor", "Restart Cursor, type /, and look for /sandbase.", "A real-client matrix confirms /sandbase discovery and invocation."), uninstall: "managed_only", terminal: "action_required" },
"cursor-cli": { mcpMode: "auto", skillMode: "none", invocation: "mcp_chat", verification: "read_only_probe", guide: guide("Cursor CLI", "Restart the CLI session and use the configured SandBase MCP tools.", "SandBase tools are listed by the client."), uninstall: "managed_only", terminal: "action_required" },
"cursor-cli": { mcpMode: "none", skillMode: "none", invocation: "none", verification: "user_action", guide: guide("Cursor CLI", "No independent MCP configuration schema is available yet.", "Do not reuse desktop Cursor configuration."), uninstall: "managed_only", terminal: "action_required" },
"gemini-cli": { mcpMode: "auto", skillMode: "none", invocation: "mcp_chat", verification: "read_only_probe", guide: guide("Gemini CLI", "Restart the CLI session and use the configured SandBase MCP tools.", "SandBase tools are listed by the client."), uninstall: "managed_only", terminal: "action_required" },

@@ -60,2 +60,23 @@ hermes: { mcpMode: "auto", skillMode: "prompt", invocation: "mcp_chat", verification: "read_only_probe", guide: guide("Hermes", "Restart Hermes, then ask it to use the configured SandBase MCP tools.", "Hermes lists or calls SandBase tools."), uninstall: "managed_only", terminal: "action_required" },

};
function v2(client, capability) {
const evidence = {
codex: { source: "https://developers.openai.com/codex/skills/", retrieved: "2026-07-29", conclusion: "Codex supports SKILL.md; the existing B-063 adapter provides the MCP transaction.", kind: "first_party" },
"claude-code": { source: "https://docs.anthropic.com/en/docs/claude-code/skills", retrieved: "2026-07-29", conclusion: "Personal Skills use ~/.claude/skills/<skill-name>/SKILL.md and the directory name maps to its command.", kind: "first_party" },
cursor: { source: "https://docs.cursor.com/context/skills", retrieved: "2026-07-28", conclusion: "The user-level ~/.agents/skills root is discovered by Cursor; UI discovery remains subject to the real-client matrix.", kind: "first_party" },
};
const implemented = client === "codex" || client === "claude-code" || client === "cursor";
const blocker = client === "cursor-cli" ? "Cursor CLI MCP installation is blocked until an independent first-party schema is recorded; desktop Cursor configuration must not be reused." : "This client batch has no completed installer and validator in the current iteration.";
if (!implemented)
return {
evidence: { source: "docs/design/features/REQ-20260728-e4a8c1b9d2f0-all-agent-native-capabilities/design.md#2.1", retrieved: "2026-07-29", conclusion: blocker, kind: "internal_blocker" },
installer: { mcp: "none", skill: "none" }, validator: { mcp: "none", skill: "none" }, implementation: "blocked", status: "failed", nextStep: blocker,
};
const hasSkill = capability.skillMode === "shared_skill" || capability.skillMode === "client_skill";
return {
evidence: evidence[client], installer: { mcp: "adapter", skill: client === "claude-code" ? "private" : "shared" },
validator: { mcp: "read_back", skill: client === "cursor" || client === "claude-code" ? "real_client_matrix" : "ownership_checksum" }, implementation: "implemented", status: "confirmation_required",
nextStep: capability.guide,
};
}
export const capabilityRegistry = Object.fromEntries(Object.keys(clientProfiles).map(client => [client, v2(client, nativeCapabilities[client])]));
export function assertNativeCapabilities() {

@@ -66,2 +87,15 @@ for (const client of Object.keys(clientProfiles)) {

throw new Error(`Invalid native capability for ${client}`);
const v2 = capabilityRegistry[client];
if (!v2?.evidence.source || !v2.evidence.retrieved || !v2.evidence.conclusion || !v2.evidence.kind || !v2.installer || !v2.validator || !v2.implementation || !v2.status || !v2.nextStep)
throw new Error(`Invalid capability v2 for ${client}`);
if (v2.implementation === "blocked" && (v2.status !== "failed" || v2.installer.mcp !== "none" || v2.installer.skill !== "none" || v2.validator.mcp !== "none" || v2.validator.skill !== "none"))
throw new Error(`Blocked capability must not be exposed as implemented for ${client}`);
if (v2.implementation === "implemented" && v2.evidence.kind !== "first_party")
throw new Error(`Implemented capability lacks first-party evidence for ${client}`);
if (v2.status === "unsupported" || v2.status === "confirmation_required") {
if (v2.status === "unsupported" && !v2.evidence.conclusion)
throw new Error(`Unsupported capability lacks evidence for ${client}`);
}
if (v2.status === "confirmation_required" && v2.installer.mcp === "none" && v2.installer.skill === "none")
throw new Error(`Confirmation cannot replace an installer for ${client}`);
}

@@ -74,3 +108,3 @@ }

export function autoClients() {
return Object.values(clientProfiles).filter(profile => profile.mode === "auto" && !!profile.adapter).map(profile => profile.id);
return Object.values(clientProfiles).filter(profile => profile.mode === "auto" && !!profile.adapter && capabilityRegistry[profile.id].implementation === "implemented").map(profile => profile.id);
}
+27
-12

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

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

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

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) {
function successMessage(client, bridgePath) {
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})`, "Credential was stored locally with restricted permissions."];
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, "", "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")

@@ -39,5 +39,6 @@ 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");

const capability = nativeCapabilities[client];
if (capability.mcpMode === "auto" || capability.mcpMode === "desktop" || !detect(client).installed)
const v2 = capabilityRegistry[client];
if (!detect(client).installed || (v2.implementation === "implemented" && (capability.mcpMode === "auto" || capability.mcpMode === "desktop")))
continue;
log(`${client}: status=${capability.terminal}, mcp=not_configured, skill=${capability.skillMode}, invocation=${capability.invocation}. ${capability.guide}`);
log(`${client}: status=${v2.status}, mcp=not_configured, skill=not_configured, invocation=${capability.invocation}, next_step=${v2.nextStep}`);
}

@@ -53,7 +54,10 @@ }

const result = await installSkill(client);
log(`Native Skill for ${client}: ${result.state}. ${result.message}`);
const status = result.state === "already_configured" ? "already_configured" : result.state === "configured" ? "configured" : "confirmation_required";
log(`${client}: status=${status}, mcp=configured, skill=${result.state}, invocation=${nativeCapabilities[client].invocation}, next_step=${result.message}`);
return status;
}
catch (error) {
const message = error instanceof Error ? error.message : "Unknown native Skill failure";
log(`Native Skill for ${client}: failed. ${message}`);
log(`${client}: status=failed, mcp=configured, skill=failed, invocation=${nativeCapabilities[client].invocation}, next_step=${message}`);
return "failed";
}

@@ -68,4 +72,10 @@ }

const capability = nativeCapabilities[client];
const v2 = capabilityRegistry[client];
if (v2.implementation === "blocked") {
log(`${client}: status=failed, mcp=not_configured, skill=not_configured, invocation=${capability.invocation}, next_step=${v2.nextStep}`);
return;
}
if (capability.mcpMode === "manual" || capability.mcpMode === "none") {
log(`${client}: status=${capability.terminal}, mcp=not_configured, skill=${capability.skillMode}, invocation=${capability.invocation}. ${capability.guide}`);
const v2 = capabilityRegistry[client];
log(`${client}: status=${v2.status}, mcp=not_configured, skill=not_configured, invocation=${capability.invocation}, next_step=${v2.nextStep}`);
return;

@@ -87,3 +97,3 @@ }

throw new Error("Client configuration verification failed");
log(successMessage(client, record, bridgeResult.path, configured.path));
log(successMessage(client, bridgeResult.path));
await configureSkill(client, log);

@@ -172,5 +182,6 @@ exchange.cleanup_token = "";

const capability = nativeCapabilities[target];
const skillDetail = skill === "installed" ? skillInvocation(target) : skill === "fallback" ? skillFallback(target) : skill === "unsupported" ? "No native SandBase Skill is available for this client." : skill === "modified" ? "Managed native Skill is missing or modified; it was left untouched." : capability.guide;
const skillDetail = skill === "installed" ? skillInvocation(target) : skill === "fallback" ? skillFallback(target) : skill === "unsupported" ? "No native SandBase Skill is available for this client." : skill === "modified" ? "Managed native Skill is missing or modified; it was left untouched." : capabilityRegistry[target].nextStep;
const references = await sharedSkillReferences();
console.log(`${target}: status=${capability.terminal}, mode=${profile.mode}, installed=${detected.installed ? "yes" : "no"}, config=${configured ? profile.mode === "auto" ? "ok" : "manual" : "missing"}, skill=${skill}, invocation=${capability.invocation}, verification=${capability.verification}, shared_skill_references=${references.join(",") || "none"}, credential=${credential ? credential.keyPrefix + "…" : "missing"}, url=${credential?.mcpUrl || "unknown"}, scope=${credential?.scope.join(",") || "unknown"}. ${skillDetail}`);
const status = configured && !!credential && skill === "installed" ? "configured" : "failed";
console.log(`${target}: status=${status}, mcp=${configured ? "configured" : "not_configured"}, skill=${skill}, invocation=${capability.invocation}, verification=${capability.verification}, shared_skill_references=${references.join(",") || "none"}, next_step=${skillDetail}`);
healthy = healthy && detected.installed && !!credential && configured && !["missing", "modified"].includes(skill);

@@ -187,2 +198,6 @@ }

for (const target of targets) {
if (capabilityRegistry[target].implementation === "blocked") {
console.log(`${target}: status=failed, next_step=${capabilityRegistry[target].nextStep}`);
continue;
}
const removed = await removeAdapter(target);

@@ -189,0 +204,0 @@ const removedSkill = await removeSkill(target);

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

backup?: string;
metadataPath?: string;
metadataBackup?: string | undefined;
changed: boolean;

@@ -10,0 +12,0 @@ }

@@ -11,2 +11,3 @@ import { createHash } from "node:crypto";

const sharedClients = ["cursor", "codex"];
const privateClients = ["claude-code"];
function sha256(content) { return createHash("sha256").update(content, "utf8").digest("hex"); }

@@ -16,2 +17,5 @@ function sharedRoot(env = process.env) { const home = env.HOME || sandbaseHome(env); return join(home, ".agents", "skills", "sandbase"); }

function metadataPath(env = process.env) { return join(sharedRoot(env), ".sandbase-managed.json"); }
function privateRoot(client, env = process.env) { const home = env.HOME || sandbaseHome(env); return client === "claude-code" ? join(home, ".claude", "skills", "sandbase") : undefined; }
function privateSkillPath(client, env = process.env) { const root = privateRoot(client, env); return root ? join(root, "SKILL.md") : undefined; }
function privateMetadataPath(client, env = process.env) { const root = privateRoot(client, env); return root ? join(root, ".sandbase-managed.json") : undefined; }
function legacySkillPath(client, env = process.env) {

@@ -25,3 +29,3 @@ const home = env.HOME || sandbaseHome(env);

}
export function skillPath(client, env = process.env) { return sharedClients.includes(client) ? sharedSkillPath(env) : undefined; }
export function skillPath(client, env = process.env) { return sharedClients.includes(client) ? sharedSkillPath(env) : privateSkillPath(client, env); }
export function skillFallback(client) {

@@ -32,4 +36,4 @@ if (client === "claude-code" || client === "hermes")

}
export function skillInvocation(client) { return `${clientProfiles[client].label} uses the shared SandBase Skill. Restart the client, type /, and look for /sandbase; real-client verification is pending.`; }
function nativeTier(client) { return skillTiers[client] === "s3_promotion"; }
export function skillInvocation(client) { return `${clientProfiles[client].label} has a SandBase Skill artifact installed. Native discovery remains unverified until the real-client matrix is complete.`; }
function nativeTier(client) { return skillTiers[client] === "s3_promotion" || skillTiers[client] === "s1_slash"; }
function marked(content) { return content.includes(ownershipMarker) && /^name:\s*sandbase\s*$/m.test(content); }

@@ -61,2 +65,20 @@ async function asset(relative = "../assets/skills/sandbase/SKILL.md") {

}
async function privateOwnership(client, env = process.env) {
const skill = privateSkillPath(client, env);
const metadata = privateMetadataPath(client, env);
if (!skill || !metadata)
return "missing";
const [content, meta] = await Promise.all([readOptional(skill), readOptional(metadata)]);
if (!content && !meta)
return "missing";
if (!content || !meta || !marked(content))
return "ambiguous";
try {
const parsed = JSON.parse(meta);
return parsed.owner === "sandbase-cli" && parsed.client === client && parsed.sha256 === sha256(content) ? "owned" : "ambiguous";
}
catch {
return "ambiguous";
}
}
async function withLock(env, work) {

@@ -85,3 +107,3 @@ const lock = join(dirname(sharedRoot(env)), ".sandbase-skill.lock");

const copies = [];
for (const client of sharedClients) {
for (const client of ["cursor", "codex"]) {
const path = legacySkillPath(client, env);

@@ -102,2 +124,7 @@ const content = await readOptional(path);

try {
if (privateClients.includes(client)) {
if ((await privateOwnership(client, env)) === "ambiguous")
return { compatible: false, message: `The ${clientProfiles[client].label} SandBase Skill ownership or checksum is invalid. It was left untouched; resolve it manually, then retry.` };
return { compatible: true, message: `${clientProfiles[client].label} personal Skills root is available for installation.` };
}
if ((await sharedOwnership(env)) === "ambiguous")

@@ -121,2 +148,6 @@ return { compatible: false, message: "The shared SandBase Skill ownership or checksum is invalid. It was left untouched; resolve it manually, then retry." };

return "unsupported";
if (privateClients.includes(client)) {
const ownership = await privateOwnership(client, env);
return ownership === "owned" ? "installed" : ownership === "missing" ? "missing" : "modified";
}
const ownership = await sharedOwnership(env);

@@ -130,2 +161,4 @@ return ownership === "owned" ? "installed" : ownership === "missing" ? "missing" : "modified";

}
if (privateClients.includes(client))
return installPrivateSkill(client, env);
return withLock(env, async () => {

@@ -162,10 +195,42 @@ const probe = await probeSkill(client, env);

const changed = needsWrite || copies.length > 0;
return skillBackup ? { state: changed ? "configured" : "already_configured", message: skillInvocation(client), path: skillPath, backup: skillBackup, changed } : { state: changed ? "configured" : "already_configured", message: skillInvocation(client), path: skillPath, changed };
return skillBackup ? { state: changed ? "configured" : "already_configured", message: skillInvocation(client), path: skillPath, backup: skillBackup, metadataPath: metaPath, metadataBackup: metaBackup, changed } : { state: changed ? "configured" : "already_configured", message: skillInvocation(client), path: skillPath, metadataPath: metaPath, metadataBackup: metaBackup, changed };
});
}
export async function rollbackSkill(result) { if (result.changed && result.path)
await restore(result.path, result.backup); }
async function installPrivateSkill(client, env) {
const path = privateSkillPath(client, env);
const meta = privateMetadataPath(client, env);
return withLock(env, async () => {
const probe = await probeSkill(client, env);
if (!probe.compatible)
throw new Error(probe.message);
const desired = await asset();
const desiredMeta = JSON.stringify({ owner: "sandbase-cli", client, sha256: sha256(desired) }) + "\n";
const current = await readOptional(path);
const needsWrite = current !== desired || await privateOwnership(client, env) !== "owned";
const skillBackup = await backup(path);
const metaBackup = await backup(meta);
try {
if (needsWrite) {
await atomicWrite(path, desired, 0o600);
await atomicWrite(meta, desiredMeta, 0o600);
}
if (await privateOwnership(client, env) !== "owned")
throw new Error(`${clientProfiles[client].label} native Skill verification failed`);
}
catch (error) {
await restore(path, skillBackup);
await restore(meta, metaBackup);
throw error;
}
return skillBackup ? { state: needsWrite ? "configured" : "already_configured", message: `${clientProfiles[client].label} personal SandBase Skill is installed; /sandbase still requires real-client verification.`, path, backup: skillBackup, metadataPath: meta, metadataBackup: metaBackup, changed: needsWrite } : { state: needsWrite ? "configured" : "already_configured", message: `${clientProfiles[client].label} personal SandBase Skill is installed; /sandbase still requires real-client verification.`, path, metadataPath: meta, metadataBackup: metaBackup, changed: needsWrite };
});
}
export async function rollbackSkill(result) { if (!result.changed || !result.path)
return; await restore(result.path, result.backup); if (result.metadataPath)
await restore(result.metadataPath, result.metadataBackup); }
export async function removeSkill(client, env = process.env) {
if (!nativeTier(client))
return false;
if (privateClients.includes(client))
return removePrivateSkill(client, env);
return withLock(env, async () => {

@@ -195,1 +260,22 @@ if ((await sharedSkillReferences(env)).length)

}
async function removePrivateSkill(client, env) {
const path = privateSkillPath(client, env);
const meta = privateMetadataPath(client, env);
const ownership = await privateOwnership(client, env);
if (ownership === "missing")
return false;
if (ownership !== "owned")
throw new Error(`The ${clientProfiles[client].label} SandBase Skill ownership or checksum is invalid. It was left untouched; remove it manually if intended.`);
const skillBackup = await backup(path);
const metaBackup = await backup(meta);
try {
await rm(path);
await rm(meta);
}
catch (error) {
await restore(path, skillBackup);
await restore(meta, metaBackup);
throw error;
}
return true;
}
{
"name": "@sandbaseai/cli",
"version": "0.1.5",
"version": "0.1.6",
"description": "Secure SandBase MCP onboarding CLI",

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