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.6
to
0.1.7
+36
dist/skills-cli.d.ts
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." };
}
+5
-2
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 {};

@@ -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}`);

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

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