@sandbaseai/cli
Advanced tools
| --- | ||
| name: sandbase | ||
| description: Use the already configured SandBase MCP tools for relevant requests. | ||
| disable-model-invocation: true | ||
| --- | ||
@@ -5,0 +6,0 @@ |
@@ -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) { |
+17
-1
| 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[]; |
+36
-1
@@ -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"; |
+22
-4
| 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 @@ } |
+7
-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>; |
+140
-45
@@ -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; | ||
| }); | ||
| } |
+1
-1
| { | ||
| "name": "@sandbaseai/cli", | ||
| "version": "0.1.4", | ||
| "version": "0.1.5", | ||
| "description": "Secure SandBase MCP onboarding CLI", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
61563
32.11%969
23.6%25
38.89%