@sandbaseai/cli
Advanced tools
| import type { Client } from "./types.js"; | ||
| /** Immutable release identity approved for the native Skills CLI adapter. */ | ||
| export declare const sandbaseSkillRelease: { | ||
| readonly repository: "https://github.com/sandbaseai/sandbase-skills"; | ||
| readonly release: "v0.1.0"; | ||
| readonly sourceArg: "https://github.com/sandbaseai/sandbase-skills/tree/v0.1.0"; | ||
| readonly commit: "03bc2811987db610cd64cbddfaa79b08e46c0e2f"; | ||
| readonly skillSha256: "583879aa39368577ebadd270434923969cbdeac0e0be73f5db0c2494019418da"; | ||
| readonly minimumCliVersion: "1.5.20"; | ||
| }; | ||
| export interface SkillsAgentRegistration { | ||
| skillsAgentId: string; | ||
| } | ||
| /** Only IDs verified by the release owner may be passed to the Skills CLI. */ | ||
| export declare const skillsAgentRegistry: Partial<Record<Client, SkillsAgentRegistration>>; | ||
| export type NativeSkillStatus = "installed" | "already_installed" | "removed" | "confirmation_required" | "failed"; | ||
| export type NativeSkillFailure = "skill_source_unavailable" | "skills_cli_unavailable" | "unsupported_by_skills_cli" | "readback_failed"; | ||
| export interface NativeSkillResult { | ||
| status: NativeSkillStatus; | ||
| code?: NativeSkillFailure; | ||
| message: string; | ||
| } | ||
| export interface SkillsCommandResult { | ||
| code: number | null; | ||
| stdout: string; | ||
| stderr: string; | ||
| } | ||
| export type SkillsCommandRunner = (args: readonly string[]) => Promise<SkillsCommandResult>; | ||
| export type SkillsReadbackVerifier = (json: string, agent: string) => Promise<boolean>; | ||
| /** Reject every mutable or incomplete source tuple before starting any child process. */ | ||
| export declare function validateSandbaseSkillRelease(release?: typeof sandbaseSkillRelease): boolean; | ||
| export declare const systemSkillsRunner: SkillsCommandRunner; | ||
| export declare const verifySkillsReadback: SkillsReadbackVerifier; | ||
| export declare function installNativeSkill(client: Client, runner?: SkillsCommandRunner, verify?: SkillsReadbackVerifier): Promise<NativeSkillResult>; | ||
| export declare function inspectNativeSkill(client: Client, runner?: SkillsCommandRunner, verify?: SkillsReadbackVerifier): Promise<NativeSkillResult>; | ||
| export declare function removeNativeSkill(client: Client, runner?: SkillsCommandRunner, verify?: SkillsReadbackVerifier): Promise<NativeSkillResult>; |
| import { spawn } from "node:child_process"; | ||
| import { createHash } from "node:crypto"; | ||
| import { readFile } from "node:fs/promises"; | ||
| import { join } from "node:path"; | ||
| /** Immutable release identity approved for the native Skills CLI adapter. */ | ||
| export const sandbaseSkillRelease = { | ||
| repository: "https://github.com/sandbaseai/sandbase-skills", | ||
| release: "v0.1.0", | ||
| sourceArg: "https://github.com/sandbaseai/sandbase-skills/tree/v0.1.0", | ||
| commit: "03bc2811987db610cd64cbddfaa79b08e46c0e2f", | ||
| skillSha256: "583879aa39368577ebadd270434923969cbdeac0e0be73f5db0c2494019418da", | ||
| minimumCliVersion: "1.5.20", | ||
| }; | ||
| /** Only IDs verified by the release owner may be passed to the Skills CLI. */ | ||
| export const skillsAgentRegistry = { | ||
| "kiro-cli": { skillsAgentId: "kiro-cli" }, | ||
| }; | ||
| function versionAtLeast(actual, minimum) { | ||
| const parse = (value) => value.match(/\b(\d+)\.(\d+)\.(\d+)\b/)?.slice(1).map(Number); | ||
| const a = parse(actual); | ||
| const b = parse(minimum); | ||
| if (!a || !b) | ||
| return false; | ||
| for (let index = 0; index < 3; index++) { | ||
| if (a[index] !== b[index]) | ||
| return a[index] > b[index]; | ||
| } | ||
| return true; | ||
| } | ||
| /** Reject every mutable or incomplete source tuple before starting any child process. */ | ||
| export function validateSandbaseSkillRelease(release = sandbaseSkillRelease) { | ||
| return release.repository === "https://github.com/sandbaseai/sandbase-skills" | ||
| && release.release === "v0.1.0" | ||
| && release.sourceArg === "https://github.com/sandbaseai/sandbase-skills/tree/v0.1.0" | ||
| && release.commit === "03bc2811987db610cd64cbddfaa79b08e46c0e2f" | ||
| && release.skillSha256 === "583879aa39368577ebadd270434923969cbdeac0e0be73f5db0c2494019418da" | ||
| && release.minimumCliVersion === "1.5.20"; | ||
| } | ||
| export const systemSkillsRunner = async (args) => new Promise(resolve => { | ||
| const child = spawn("npx", ["-y", "skills", ...args], { stdio: ["ignore", "pipe", "pipe"] }); | ||
| let stdout = ""; | ||
| let stderr = ""; | ||
| child.stdout.on("data", data => { stdout += String(data); }); | ||
| child.stderr.on("data", data => { stderr += String(data); }); | ||
| child.on("error", () => resolve({ code: null, stdout: "", stderr: "" })); | ||
| child.on("close", code => resolve({ code, stdout, stderr })); | ||
| }); | ||
| function unavailable() { return { status: "failed", code: "skills_cli_unavailable", message: "Skills CLI is unavailable or below the required version; no native Skill was changed." }; } | ||
| function sourceUnavailable() { return { status: "failed", code: "skill_source_unavailable", message: "The verified SandBase Skill source is unavailable; no native Skill was changed." }; } | ||
| // The first-party listing must echo the exact immutable selector. A repository | ||
| // name or a tag alone could refer to a different source and is not ownership proof. | ||
| export const verifySkillsReadback = async (output, agent) => { | ||
| try { | ||
| const entries = JSON.parse(output); | ||
| const candidates = []; | ||
| const visit = (value) => { | ||
| if (Array.isArray(value)) { | ||
| value.forEach(visit); | ||
| return; | ||
| } | ||
| if (!value || typeof value !== "object") | ||
| return; | ||
| const item = value; | ||
| if ("sourceUrl" in item || "agents" in item || "path" in item) | ||
| candidates.push(item); | ||
| Object.values(item).forEach(visit); | ||
| }; | ||
| visit(entries); | ||
| const agentLabel = agent === "kiro-cli" ? "Kiro CLI" : ""; | ||
| for (const candidate of candidates) { | ||
| if (candidate.sourceUrl !== `${sandbaseSkillRelease.repository}.git` || typeof candidate.path !== "string" || !JSON.stringify(candidate.agents).includes(agentLabel)) | ||
| continue; | ||
| const skill = await readFile(join(candidate.path, "SKILL.md"), "utf8"); | ||
| if (createHash("sha256").update(skill, "utf8").digest("hex") === sandbaseSkillRelease.skillSha256) | ||
| return true; | ||
| } | ||
| } | ||
| catch { /* malformed/unreadable first-party readback is not proof */ } | ||
| return false; | ||
| }; | ||
| async function invoke(runner, args) { | ||
| try { | ||
| return await runner(args); | ||
| } | ||
| catch { | ||
| return undefined; | ||
| } | ||
| } | ||
| async function probe(client, runner) { | ||
| if (!validateSandbaseSkillRelease()) | ||
| return sourceUnavailable(); | ||
| const registration = skillsAgentRegistry[client]; | ||
| if (!registration) | ||
| return { status: "failed", code: "unsupported_by_skills_cli", message: `No verified Skills CLI agent mapping exists for ${client}; no native Skill was changed.` }; | ||
| const version = await invoke(runner, ["--version"]); | ||
| if (!version || version.code !== 0 || !versionAtLeast(`${version.stdout}\n${version.stderr}`, sandbaseSkillRelease.minimumCliVersion)) | ||
| return unavailable(); | ||
| // `skills add <source> --agent <id> --list` is the documented capability | ||
| // probe. It must succeed before an install or lifecycle command is attempted. | ||
| const listed = await invoke(runner, ["add", sandbaseSkillRelease.sourceArg, "--agent", registration.skillsAgentId, "--list"]); | ||
| if (!listed || listed.code !== 0) | ||
| return unavailable(); | ||
| return { agent: registration.skillsAgentId, listed }; | ||
| } | ||
| export async function installNativeSkill(client, runner = systemSkillsRunner, verify = verifySkillsReadback) { | ||
| const checked = await probe(client, runner); | ||
| if ("status" in checked) | ||
| return checked; | ||
| const before = await invoke(runner, ["list", "-g", "-a", checked.agent, "--json"]); | ||
| if (!before || before.code !== 0) | ||
| return unavailable(); | ||
| if (await verify(before.stdout, checked.agent)) | ||
| return { status: "already_installed", message: "Native SandBase Skill is already installed and verified by Skills CLI readback." }; | ||
| const added = await invoke(runner, ["add", sandbaseSkillRelease.sourceArg, "-g", "-a", checked.agent]); | ||
| if (!added || added.code !== 0) | ||
| return unavailable(); | ||
| const readback = await invoke(runner, ["list", "-g", "-a", checked.agent, "--json"]); | ||
| if (!readback || readback.code !== 0 || !(await verify(readback.stdout, checked.agent))) | ||
| return { status: "failed", code: "readback_failed", message: "Skills CLI could not prove the SandBase source for this agent; no success was reported." }; | ||
| return { status: "installed", message: "Native SandBase Skill was installed and verified by Skills CLI readback." }; | ||
| } | ||
| export async function inspectNativeSkill(client, runner = systemSkillsRunner, verify = verifySkillsReadback) { | ||
| const checked = await probe(client, runner); | ||
| if ("status" in checked) | ||
| return checked; | ||
| const readback = await invoke(runner, ["list", "-g", "-a", checked.agent, "--json"]); | ||
| if (!readback || readback.code !== 0) | ||
| return unavailable(); | ||
| return await verify(readback.stdout, checked.agent) | ||
| ? { status: "already_installed", message: "Native SandBase Skill ownership is verified by Skills CLI readback." } | ||
| : { status: "confirmation_required", message: "Native SandBase Skill is not listed for this agent." }; | ||
| } | ||
| export async function removeNativeSkill(client, runner = systemSkillsRunner, verify = verifySkillsReadback) { | ||
| const checked = await probe(client, runner); | ||
| if ("status" in checked) | ||
| return checked; | ||
| const before = await invoke(runner, ["list", "-g", "-a", checked.agent, "--json"]); | ||
| if (!before || before.code !== 0) | ||
| return unavailable(); | ||
| if (!(await verify(before.stdout, checked.agent))) | ||
| return { status: "confirmation_required", message: "Skills CLI cannot prove SandBase ownership for this agent; no native Skill was removed." }; | ||
| // Skills CLI removes by its first-party Skill name, never by a URL. This is | ||
| // reached only after source, agent and digest ownership have been proven. | ||
| const removed = await invoke(runner, ["remove", "sandbase", "-g", "-a", checked.agent]); | ||
| if (!removed || removed.code !== 0) | ||
| return unavailable(); | ||
| const readback = await invoke(runner, ["list", "-g", "-a", checked.agent, "--json"]); | ||
| if (!readback || readback.code !== 0 || await verify(readback.stdout, checked.agent)) | ||
| return { status: "failed", code: "readback_failed", message: "Skills CLI could not prove native Skill removal; no removal success was reported." }; | ||
| return { status: "removed", message: "Native SandBase Skill removal was verified by Skills CLI readback." }; | ||
| } |
| import { type Client, type ConnectClient } from "./types.js"; | ||
| import { AuthorizationApi } from "./auth/api.js"; | ||
| import { type CredentialStore } from "./credentials/store.js"; | ||
| import { type SkillsCommandRunner, type SkillsReadbackVerifier } from "./skills-cli.js"; | ||
| type Detection = { | ||
@@ -15,7 +16,9 @@ installed: boolean; | ||
| detect?: (client: Client) => Detection; | ||
| skillsRunner?: SkillsCommandRunner; | ||
| skillsReadbackVerifier?: SkillsReadbackVerifier; | ||
| } | ||
| 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 declare function doctor(client?: ConnectClient, store?: CredentialStore, detect?: (client: Client) => Detection, skillsRunner?: SkillsCommandRunner, skillsReadbackVerifier?: SkillsReadbackVerifier): Promise<boolean>; | ||
| export declare function unregister(client?: ConnectClient, store?: CredentialStore, detect?: (client: Client) => Detection, skillsRunner?: SkillsCommandRunner, skillsReadbackVerifier?: SkillsReadbackVerifier): Promise<void>; | ||
| export {}; |
+44
-5
@@ -10,2 +10,3 @@ import { spawn } from "node:child_process"; | ||
| import { inspectSkill, installSkill, removeSkill, sharedSkillReferences, skillFallback, skillInvocation } from "./skills.js"; | ||
| import { inspectNativeSkill, installNativeSkill, removeNativeSkill, skillsAgentRegistry } from "./skills-cli.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 }); }); | ||
@@ -39,2 +40,4 @@ async function compensate(api, authorizationId, token) { for (const delay of [0, 250, 750]) { | ||
| const v2 = capabilityRegistry[client]; | ||
| if (skillsAgentRegistry[client]) | ||
| continue; | ||
| if (!detect(client).installed || (v2.implementation === "implemented" && (capability.mcpMode === "auto" || capability.mcpMode === "desktop"))) | ||
@@ -63,2 +66,13 @@ continue; | ||
| } | ||
| async function configureNativeSkill(client, detected, log, runner, verify) { | ||
| if (!skillsAgentRegistry[client]) | ||
| return; | ||
| if (!detected.installed) { | ||
| log(`${client}: status=confirmation_required, mcp=not_configured, skill=confirmation_required, invocation=${nativeCapabilities[client].invocation}, next_step=${clientProfiles[client].label} is not locally detected; no Skills CLI invocation was attempted.`); | ||
| return; | ||
| } | ||
| const result = await installNativeSkill(client, runner, verify); | ||
| const status = result.status === "failed" ? "failed" : "confirmation_required"; | ||
| log(`${client}: status=${status}, mcp=not_configured, skill=${result.status}, invocation=${nativeCapabilities[client].invocation}, next_step=${result.message}`); | ||
| } | ||
| async function connectOne(client, deps) { | ||
@@ -72,2 +86,6 @@ const api = deps.api || new AuthorizationApi((process.env.SANDBASE_API_URL || "https://sandbase.ai").replace(/\/$/, "")); | ||
| const v2 = capabilityRegistry[client]; | ||
| if (skillsAgentRegistry[client]) { | ||
| await configureNativeSkill(client, detect(client), log, deps.skillsRunner, deps.skillsReadbackVerifier); | ||
| return; | ||
| } | ||
| if (v2.implementation === "blocked") { | ||
@@ -78,2 +96,7 @@ log(`${client}: status=failed, mcp=not_configured, skill=not_configured, invocation=${capability.invocation}, next_step=${v2.nextStep}`); | ||
| if (capability.mcpMode === "manual" || capability.mcpMode === "none") { | ||
| const detected = detect(client); | ||
| if (skillsAgentRegistry[client]) { | ||
| await configureNativeSkill(client, detected, log, deps.skillsRunner, deps.skillsReadbackVerifier); | ||
| return; | ||
| } | ||
| const v2 = capabilityRegistry[client]; | ||
@@ -119,4 +142,9 @@ log(`${client}: status=${v2.status}, mcp=not_configured, skill=not_configured, invocation=${capability.invocation}, next_step=${v2.nextStep}`); | ||
| const targets = plannedAutoClients(detect); | ||
| const nativeSkillTargets = clients.filter(target => !!skillsAgentRegistry[target] && detect(target).installed); | ||
| // Native Skills are an independent local lifecycle: do not let their result | ||
| // change MCP authorization, configuration, or rollback decisions. | ||
| for (const target of nativeSkillTargets) | ||
| await configureNativeSkill(target, detect(target), log, deps.skillsRunner, deps.skillsReadbackVerifier); | ||
| if (!targets.length) { | ||
| log("No installed compatible clients support automatic configuration. Use --client <client> for manual or skill setup guidance."); | ||
| log("No installed compatible clients support automatic MCP configuration. Use --client <client> for manual or skill setup guidance."); | ||
| logDetectedActionRequired(detect, log); | ||
@@ -168,4 +196,4 @@ return; | ||
| 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]; | ||
| export async function doctor(client = "auto", store = new FileCredentialStore(), detect = detectClient, skillsRunner, skillsReadbackVerifier) { | ||
| const targets = client === "auto" ? [...new Set([...plannedAutoClients(detect), ...clients.filter(target => !!skillsAgentRegistry[target] && detect(target).installed)])] : [client]; | ||
| if (!targets.length) { | ||
@@ -177,2 +205,8 @@ console.log("No installed compatible clients support automatic configuration."); | ||
| for (const target of targets) { | ||
| if (skillsAgentRegistry[target]) { | ||
| const native = await inspectNativeSkill(target, skillsRunner, skillsReadbackVerifier); | ||
| console.log(`${target}: status=${native.status === "failed" ? "failed" : "confirmation_required"}, mcp=not_configured, skill=${native.status}, invocation=${nativeCapabilities[target].invocation}, verification=read_only_probe, next_step=${native.message}`); | ||
| healthy = healthy && native.status === "already_installed"; | ||
| continue; | ||
| } | ||
| const credential = await store.get(target); | ||
@@ -192,4 +226,4 @@ const profile = clientProfiles[target]; | ||
| } | ||
| export async function unregister(client = "auto", store = new FileCredentialStore(), detect = detectClient) { | ||
| const targets = client === "auto" ? plannedAutoClients(detect) : [client]; | ||
| export async function unregister(client = "auto", store = new FileCredentialStore(), detect = detectClient, skillsRunner, skillsReadbackVerifier) { | ||
| const targets = client === "auto" ? [...new Set([...plannedAutoClients(detect), ...clients.filter(target => !!skillsAgentRegistry[target] && detect(target).installed)])] : [client]; | ||
| if (!targets.length) { | ||
@@ -200,2 +234,7 @@ console.log("No installed compatible clients support automatic configuration."); | ||
| for (const target of targets) { | ||
| if (skillsAgentRegistry[target]) { | ||
| const result = await removeNativeSkill(target, skillsRunner, skillsReadbackVerifier); | ||
| console.log(`${target}: mcp=not_configured, skill=${result.status}, next_step=${result.message}`); | ||
| continue; | ||
| } | ||
| if (capabilityRegistry[target].implementation === "blocked") { | ||
@@ -202,0 +241,0 @@ console.log(`${target}: status=failed, next_step=${capabilityRegistry[target].nextStep}`); |
+1
-1
| { | ||
| "name": "@sandbaseai/cli", | ||
| "version": "0.1.6", | ||
| "version": "0.1.7", | ||
| "description": "Secure SandBase MCP onboarding CLI", | ||
@@ -5,0 +5,0 @@ "type": "module", |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
87918
18.77%29
7.41%1399
19.57%31
6.9%6
20%