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.4
to
0.1.5
+1
-0
assets/skills/sandbase/SKILL.md
---
name: sandbase
description: Use the already configured SandBase MCP tools for relevant requests.
disable-model-invocation: true
---

@@ -5,0 +6,0 @@

+2
-3

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

const defaultIO = { backup, write: atomicWrite, restore };
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 }; }
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 start = "# >>> sandbase managed >>>", end = "# <<< sandbase managed <<<";

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

import type { Client } from "./types.js";
export type InstallMode = "auto" | "manual" | "skill";
export type SkillTier = "s1_slash" | "s2_skill" | "s3_fallback" | "s4_none";
export type SkillTier = "s1_slash" | "s3_promotion" | "s3_fallback" | "s4_none";
export type MCPMode = "auto" | "desktop" | "manual" | "none";
export type SkillMode = "shared_skill" | "client_skill" | "prompt" | "none";
export type Invocation = "slash" | "skill_picker" | "mcp_chat" | "desktop_ui" | "prompt" | "none";
export type Verification = "fixture" | "read_only_probe" | "user_action" | "real_client_matrix";
export type CapabilityStatus = "configured" | "already_configured" | "action_required" | "unsupported" | "failed";
export interface NativeCapability {
mcpMode: MCPMode;
skillMode: SkillMode;
invocation: Invocation;
verification: Verification;
guide: string;
uninstall: "managed_only";
terminal: Exclude<CapabilityStatus, "configured" | "already_configured" | "failed">;
}
export interface ClientProfile {

@@ -13,3 +27,5 @@ id: Client;

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

@@ -28,5 +28,40 @@ export const clientProfiles = {

export const skillTiers = {
codex: "s2_skill", "claude-code": "s1_slash", cursor: "s3_fallback", hermes: "s3_fallback",
codex: "s3_promotion", "claude-code": "s3_fallback", 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",
};
const guide = (client, action, check) => `${client}: ${action} Completion check: ${check} Recovery: keep existing settings unchanged and retry after correcting the client setup.`;
export const nativeCapabilities = {
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" },
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" },
"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" },
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" },
"claude-desktop": { mcpMode: "desktop", skillMode: "none", invocation: "desktop_ui", verification: "read_only_probe", guide: guide("Claude Desktop", "Restart the desktop app and inspect its MCP tools panel.", "SandBase tools appear in the desktop UI."), uninstall: "managed_only", terminal: "action_required" },
"qwen-code": { mcpMode: "auto", skillMode: "none", invocation: "mcp_chat", verification: "read_only_probe", guide: guide("Qwen Code", "Restart Qwen Code and use the configured SandBase MCP tools.", "SandBase tools are listed by the client."), uninstall: "managed_only", terminal: "action_required" },
windsurf: { mcpMode: "manual", skillMode: "none", invocation: "desktop_ui", verification: "user_action", guide: guide("Windsurf", "Open its MCP settings and import the SandBase MCP server using your locally stored credential.", "The MCP settings page lists SandBase."), uninstall: "managed_only", terminal: "action_required" },
opencode: { mcpMode: "manual", skillMode: "none", invocation: "mcp_chat", verification: "user_action", guide: guide("OpenCode", "Use its documented MCP import flow and add the SandBase server.", "OpenCode lists SandBase tools."), uninstall: "managed_only", terminal: "action_required" },
chatgpt: { mcpMode: "manual", skillMode: "none", invocation: "none", verification: "user_action", guide: guide("ChatGPT", "No verified safe personal/workspace MCP import path is available.", "Do not add unverified configuration."), uninstall: "managed_only", terminal: "unsupported" },
warp: { mcpMode: "manual", skillMode: "none", invocation: "desktop_ui", verification: "user_action", guide: guide("Warp", "Open Warp's MCP settings and follow its documented server import flow.", "SandBase is visible in Warp's MCP settings."), uninstall: "managed_only", terminal: "action_required" },
trae: { mcpMode: "none", skillMode: "prompt", invocation: "prompt", verification: "user_action", guide: guide("Trae", "Paste a SandBase MCP usage prompt into the client after configuring its supported integration.", "The client confirms the prompt or tool availability."), uninstall: "managed_only", terminal: "action_required" },
"kimi-cli": { mcpMode: "manual", skillMode: "none", invocation: "mcp_chat", verification: "user_action", guide: guide("Kimi CLI", "Use Kimi CLI's documented MCP import flow.", "SandBase tools are visible in Kimi CLI."), uninstall: "managed_only", terminal: "action_required" },
"kiro-cli": { mcpMode: "manual", skillMode: "none", invocation: "mcp_chat", verification: "user_action", guide: guide("Kiro CLI", "Use Kiro CLI's documented MCP import flow.", "SandBase tools are visible in Kiro CLI."), uninstall: "managed_only", terminal: "action_required" },
amp: { mcpMode: "manual", skillMode: "none", invocation: "mcp_chat", verification: "user_action", guide: guide("Amp", "Use Amp's documented MCP import flow.", "SandBase tools are visible in Amp."), uninstall: "managed_only", terminal: "action_required" },
crush: { mcpMode: "manual", skillMode: "none", invocation: "mcp_chat", verification: "user_action", guide: guide("Crush", "Use Crush's documented MCP import flow.", "SandBase tools are visible in Crush."), uninstall: "managed_only", terminal: "action_required" },
"iflow-cli": { mcpMode: "manual", skillMode: "none", invocation: "mcp_chat", verification: "user_action", guide: guide("iFlow CLI", "Use iFlow CLI's documented MCP import flow.", "SandBase tools are visible in iFlow CLI."), uninstall: "managed_only", terminal: "action_required" },
qoder: { mcpMode: "none", skillMode: "prompt", invocation: "prompt", verification: "user_action", guide: guide("Qoder", "Paste the SandBase MCP usage prompt into Qoder.", "Qoder confirms the prompt or tool availability."), uninstall: "managed_only", terminal: "action_required" },
workbuddy: { mcpMode: "none", skillMode: "prompt", invocation: "prompt", verification: "user_action", guide: guide("WorkBuddy", "Paste the SandBase MCP usage prompt into WorkBuddy.", "WorkBuddy confirms the prompt or tool availability."), uninstall: "managed_only", terminal: "action_required" },
cowork: { mcpMode: "manual", skillMode: "none", invocation: "desktop_ui", verification: "user_action", guide: guide("Cowork", "Open Cowork's MCP settings and use its documented import flow.", "SandBase appears in Cowork's MCP settings."), uninstall: "managed_only", terminal: "action_required" },
pi: { mcpMode: "none", skillMode: "prompt", invocation: "prompt", verification: "user_action", guide: guide("Pi", "Paste the SandBase MCP usage prompt into Pi.", "Pi confirms the prompt or tool availability."), uninstall: "managed_only", terminal: "action_required" },
openclaw: { mcpMode: "none", skillMode: "prompt", invocation: "prompt", verification: "user_action", guide: guide("OpenClaw", "Paste the SandBase MCP usage prompt into OpenClaw.", "OpenClaw confirms the prompt or tool availability."), uninstall: "managed_only", terminal: "action_required" },
antigravity: { mcpMode: "none", skillMode: "prompt", invocation: "prompt", verification: "user_action", guide: guide("Antigravity", "Paste the SandBase MCP usage prompt into Antigravity.", "Antigravity confirms the prompt or tool availability."), uninstall: "managed_only", terminal: "action_required" },
};
export function assertNativeCapabilities() {
for (const client of Object.keys(clientProfiles)) {
const capability = nativeCapabilities[client];
if (!capability || !capability.mcpMode || !capability.skillMode || !capability.invocation || !capability.verification || !capability.guide || !capability.uninstall || !capability.terminal)
throw new Error(`Invalid native capability for ${client}`);
}
}
assertNativeCapabilities();
export function clientList() {

@@ -33,0 +68,0 @@ return Object.keys(clientProfiles).join("|");

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

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

@@ -3,0 +3,0 @@ import { type CredentialStore } from "./credentials/store.js";

import { spawn } from "node:child_process";
import { platform } from "node:os";
import { clients } from "./types.js";
import { AuthorizationApi } from "./auth/api.js";
import { authorize } from "./auth/flow.js";
import { autoClients, clientProfiles } from "./clients.js";
import { autoClients, clientProfiles, nativeCapabilities } from "./clients.js";
import { FileCredentialStore } from "./credentials/store.js";
import { configure, detectClient, installBridge, isConfigured, rollback, rollbackBridge, unregister as removeAdapter } from "./adapters/index.js";
import { inspectSkill, installSkill, removeSkill, skillFallback, skillInvocation } from "./skills.js";
import { inspectSkill, installSkill, removeSkill, sharedSkillReferences, skillFallback, skillInvocation } from "./skills.js";
const sleep = (ms, signal) => new Promise((resolve, reject) => { const t = setTimeout(resolve, ms); signal?.addEventListener("abort", () => { clearTimeout(t); reject(new Error("Authorization cancelled")); }, { once: true }); });

@@ -33,2 +34,10 @@ async function compensate(api, authorizationId, token) { for (const delay of [0, 250, 750]) {

function plannedAutoClients(detect) { return autoClients().filter(client => detect(client).installed); }
function logDetectedActionRequired(detect, log) {
for (const client of clients) {
const capability = nativeCapabilities[client];
if (capability.mcpMode === "auto" || capability.mcpMode === "desktop" || !detect(client).installed)
continue;
log(`${client}: status=${capability.terminal}, mcp=not_configured, skill=${capability.skillMode}, invocation=${capability.invocation}. ${capability.guide}`);
}
}
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 }; }

@@ -55,2 +64,7 @@ async function restoreCredential(store, client, previous) { if (previous)

const profile = clientProfiles[client];
const capability = nativeCapabilities[client];
if (capability.mcpMode === "manual" || capability.mcpMode === "none") {
log(`${client}: status=${capability.terminal}, mcp=not_configured, skill=${capability.skillMode}, invocation=${capability.invocation}. ${capability.guide}`);
return;
}
const detected = detect(client);

@@ -94,2 +108,3 @@ if (!detected.installed)

log("No installed compatible clients support automatic configuration. Use --client <client> for manual or skill setup guidance.");
logDetectedActionRequired(detect, log);
return;

@@ -126,2 +141,3 @@ }

log(`Partial success: failed clients were rolled back: ${failed.join(", ")}.`);
logDetectedActionRequired(detect, log);
exchange.cleanup_token = "";

@@ -153,4 +169,6 @@ }

const skill = await inspectSkill(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." : "Native SandBase Skill is not installed.";
console.log(`${target}: mode=${profile.mode}, installed=${detected.installed ? "yes" : "no"}, config=${configured ? profile.mode === "auto" ? "ok" : "manual" : "missing"}, skill=${skill}, credential=${credential ? credential.keyPrefix + "…" : "missing"}, url=${credential?.mcpUrl || "unknown"}, scope=${credential?.scope.join(",") || "unknown"}. ${skillDetail}`);
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 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}`);
healthy = healthy && detected.installed && !!credential && configured && !["missing", "modified"].includes(skill);

@@ -157,0 +175,0 @@ }

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

}
export interface SkillProbe {
compatible: boolean;
message: string;
}
export declare function sharedSkillPath(env?: NodeJS.ProcessEnv): string;
export declare function skillPath(client: Client, env?: NodeJS.ProcessEnv): string | undefined;
export declare function skillFallback(client: Client): string;
export declare function skillInvocation(client: Client): string;
export declare function sharedSkillReferences(env?: NodeJS.ProcessEnv): Promise<Client[]>;
export declare function probeSkill(client: Client, env?: NodeJS.ProcessEnv): Promise<SkillProbe>;
export declare function inspectSkill(client: Client, env?: NodeJS.ProcessEnv): Promise<SkillState>;

@@ -15,0 +22,0 @@ export declare function installSkill(client: Client, env?: NodeJS.ProcessEnv): Promise<SkillInstallResult>;

@@ -0,4 +1,6 @@

import { createHash } from "node:crypto";
import { mkdir, readFile, rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { isConfigured } from "./adapters/index.js";
import { atomicWrite, backup, readOptional, restore } from "./fs-safe.js";

@@ -8,6 +10,11 @@ import { clientProfiles, skillTiers } from "./clients.js";

const ownershipMarker = "<!-- sandbase-cli-managed: sandbase -->";
export function skillPath(client, env = process.env) {
const sharedClients = ["cursor", "codex"];
function sha256(content) { return createHash("sha256").update(content, "utf8").digest("hex"); }
function sharedRoot(env = process.env) { const home = env.HOME || sandbaseHome(env); return join(home, ".agents", "skills", "sandbase"); }
export function sharedSkillPath(env = process.env) { return join(sharedRoot(env), "SKILL.md"); }
function metadataPath(env = process.env) { return join(sharedRoot(env), ".sandbase-managed.json"); }
function legacySkillPath(client, env = process.env) {
const home = env.HOME || sandbaseHome(env);
if (client === "claude-code")
return join(home, ".claude", "skills", "sandbase", "SKILL.md");
if (client === "cursor")
return join(home, ".cursor", "skills", "sandbase", "SKILL.md");
if (client === "codex")

@@ -17,12 +24,13 @@ return join(env.CODEX_HOME || join(home, ".codex"), "skills", "sandbase", "SKILL.md");

}
export function skillPath(client, env = process.env) { return sharedClients.includes(client) ? sharedSkillPath(env) : undefined; }
export function skillFallback(client) {
if (client === "cursor" || client === "hermes")
if (client === "claude-code" || client === "hermes")
return `${clientProfiles[client].label} native SandBase Skill is not available yet. Use the configured SandBase MCP tools directly in chat.`;
return "No native SandBase Skill is available for this client. Use the configured SandBase MCP tools directly.";
}
export function skillInvocation(client) { return client === "claude-code" ? "Type / and select /sandbase to get SandBase MCP tool guidance." : "Open Codex's Skill picker and select SandBase, or reference $sandbase where that native affordance is available."; }
function nativeTier(client) { const tier = skillTiers[client]; return tier === "s1_slash" || tier === "s2_skill"; }
function isOwned(content) { return content.includes(ownershipMarker) && /^name:\s*sandbase\s*$/m.test(content); }
async function asset() {
const bundled = fileURLToPath(new URL("../assets/skills/sandbase/SKILL.md", import.meta.url));
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"; }
function marked(content) { return content.includes(ownershipMarker) && /^name:\s*sandbase\s*$/m.test(content); }
async function asset(relative = "../assets/skills/sandbase/SKILL.md") {
const bundled = fileURLToPath(new URL(relative, import.meta.url));
try {

@@ -37,2 +45,67 @@ return await readFile(bundled, "utf8");

}
async function sharedOwnership(env = process.env) {
const [skill, metadata] = await Promise.all([readOptional(sharedSkillPath(env)), readOptional(metadataPath(env))]);
if (!skill && !metadata)
return "missing";
if (!skill || !metadata || !marked(skill))
return "ambiguous";
try {
const parsed = JSON.parse(metadata);
return parsed.owner === "sandbase-cli" && parsed.sha256 === sha256(skill) ? "owned" : "ambiguous";
}
catch {
return "ambiguous";
}
}
async function withLock(env, work) {
const lock = join(dirname(sharedRoot(env)), ".sandbase-skill.lock");
await mkdir(dirname(lock), { recursive: true, mode: 0o700 });
for (let attempt = 0; attempt < 100; attempt++) {
try {
await mkdir(lock, { mode: 0o700 });
try {
return await work();
}
finally {
await rm(lock, { recursive: true, force: true });
}
}
catch (error) {
if (error.code !== "EEXIST")
throw error;
await new Promise(resolve => setTimeout(resolve, 20));
}
}
throw new Error("Another SandBase Skill installation is still running. Retry after it finishes.");
}
async function legacyCopies(env) {
const copies = [];
for (const client of sharedClients) {
const path = legacySkillPath(client, env);
const content = await readOptional(path);
if (content)
copies.push({ client, path, content });
}
return copies;
}
function verifiedLegacy(content) { return marked(content); }
export async function sharedSkillReferences(env = process.env) { const refs = []; for (const client of sharedClients)
if (await isConfigured(client, env))
refs.push(client); return refs; }
export async function probeSkill(client, env = process.env) {
if (!nativeTier(client))
return { compatible: true, message: "No promotion probe is required." };
try {
if ((await sharedOwnership(env)) === "ambiguous")
return { compatible: false, message: "The shared SandBase Skill ownership or checksum is invalid. It was left untouched; resolve it manually, then retry." };
const copies = await legacyCopies(env);
const unknown = copies.find(copy => !verifiedLegacy(copy.content));
if (unknown)
return { compatible: false, message: `An unmanaged legacy ${clientProfiles[unknown.client].label} SandBase Skill was left untouched. Resolve it manually, then retry.` };
return { compatible: true, message: "Shared Agent Skills root is available for installation." };
}
catch {
return { compatible: false, message: "The shared Agent Skills root is not readable. Check its local configuration and retry." };
}
}
export async function inspectSkill(client, env = process.env) {

@@ -44,4 +117,4 @@ const tier = skillTiers[client];

return "unsupported";
const current = await readOptional(skillPath(client, env));
return !current ? "missing" : isOwned(current) ? "installed" : "modified";
const ownership = await sharedOwnership(env);
return ownership === "owned" ? "installed" : ownership === "missing" ? "missing" : "modified";
}

@@ -53,21 +126,35 @@ export async function installSkill(client, env = process.env) {

}
const path = skillPath(client, env);
const desired = await asset();
const current = await readOptional(path);
if (current && !isOwned(current))
throw new Error(`Refusing to overwrite an unmanaged ${clientProfiles[client].label} SandBase Skill artifact. Resolve it manually, then retry.`);
if (current === desired)
return { state: "already_configured", message: skillInvocation(client), path, changed: false };
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
const backupPath = await backup(path);
try {
await atomicWrite(path, desired, 0o600);
if ((await readOptional(path)) !== desired || !isOwned(desired))
throw new Error("Native Skill verification failed");
}
catch (error) {
await restore(path, backupPath);
throw error;
}
return backupPath ? { state: "configured", message: skillInvocation(client), path, backup: backupPath, changed: true } : { state: "configured", message: skillInvocation(client), path, changed: true };
return withLock(env, async () => {
const probe = await probeSkill(client, env);
if (!probe.compatible)
throw new Error(probe.message);
const desired = await asset();
const skillPath = sharedSkillPath(env);
const metaPath = metadataPath(env);
const metadata = JSON.stringify({ owner: "sandbase-cli", sha256: sha256(desired) }) + "\n";
const current = await readOptional(skillPath);
const copies = await legacyCopies(env);
const needsWrite = current !== desired || await sharedOwnership(env) !== "owned";
const skillBackup = await backup(skillPath);
const metaBackup = await backup(metaPath);
const legacyBackups = await Promise.all(copies.map(async (copy) => ({ ...copy, backup: await backup(copy.path) })));
try {
if (needsWrite) {
await atomicWrite(skillPath, desired, 0o600);
await atomicWrite(metaPath, metadata, 0o600);
}
if (await sharedOwnership(env) !== "owned")
throw new Error("Shared native Skill verification failed");
for (const copy of copies)
await rm(copy.path);
}
catch (error) {
await restore(skillPath, skillBackup);
await restore(metaPath, metaBackup);
await Promise.all(legacyBackups.map(copy => restore(copy.path, copy.backup)));
throw error;
}
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 };
});
}

@@ -79,17 +166,25 @@ export async function rollbackSkill(result) { if (result.changed && result.path)

return false;
const path = skillPath(client, env);
const current = await readOptional(path);
if (!current)
return false;
if (!isOwned(current))
throw new Error(`Refusing to remove an unmanaged ${clientProfiles[client].label} SandBase Skill artifact. Remove it manually if intended.`);
const backupPath = await backup(path);
try {
await rm(path);
}
catch (error) {
await restore(path, backupPath);
throw error;
}
return true;
return withLock(env, async () => {
if ((await sharedSkillReferences(env)).length)
return false;
const ownership = await sharedOwnership(env);
if (ownership === "missing")
return false;
if (ownership !== "owned")
throw new Error("The shared SandBase Skill ownership or checksum is invalid. It was left untouched; remove it manually if intended.");
const skillPath = sharedSkillPath(env);
const metaPath = metadataPath(env);
const skillBackup = await backup(skillPath);
const metaBackup = await backup(metaPath);
try {
await rm(skillPath);
await rm(metaPath);
}
catch (error) {
await restore(skillPath, skillBackup);
await restore(metaPath, metaBackup);
throw error;
}
return true;
});
}
{
"name": "@sandbaseai/cli",
"version": "0.1.4",
"version": "0.1.5",
"description": "Secure SandBase MCP onboarding CLI",

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