Official
claude
Claude Code agent provider: runs the Claude Code CLI headless for each account.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/claude@0.2.0
Permissions in 0.2.0
Files
skills.test.ts4.6 KB
// Ported from the Rust plugin's skills.rs tests.
import assert from "node:assert/strict";
import { useApi, test, run as runEffect, files, tempDir } from "./testing.ts";
import { mkdirSync, writeFileSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { frontmatter, listSkills } from "./skills.ts";
import { apiFiles } from "./files.ts";
import { diskFs, fakeApi } from "../sdk/testing.ts";
function writeSkill(root: string, name: string, body: string) {
mkdirSync(join(root, name), { recursive: true });
writeFileSync(join(root, name, "SKILL.md"), body);
}
test("frontmatter reads the three keys that matter", () => {
const fields = frontmatter(
"---\nname: review\ndescription: Reviews code\ndisable-model-invocation: true\n---\nbody\n",
);
assert.equal(fields.description, "Reviews code");
assert.equal(fields.disableModelInvocation, true);
assert.equal(fields.userInvocable, true);
// A file with no frontmatter is still a skill, without a description.
assert.deepEqual(frontmatter("just a body\n"), {
description: "",
disableModelInvocation: false,
userInvocable: true,
});
assert.equal(frontmatter("---\nuser-invocable: false\n---\n").userInvocable, false);
// The body is not read as frontmatter once the block closed.
assert.equal(frontmatter("---\ndescription: a\n---\ndescription: b\n").description, "a");
assert.equal(frontmatter('---\ndescription: "quoted"\n---\n').description, "quoted");
});
test("the config directory's copy wins a name clash", async () => {
const home = tempDir();
const workspace = tempDir();
writeSkill(join(home, "skills"), "review", "---\ndescription: user copy\n---\n");
writeSkill(join(workspace, ".claude/skills"), "review", "---\ndescription: project copy\n---\n");
writeSkill(join(workspace, ".claude/skills"), "ship", "---\ndescription: ship it\n---\n");
const skills = await runEffect(listSkills(files(), workspace, home));
assert.deepEqual(skills, [
{ name: "review", description: "user copy", source: "user", manualOnly: false },
{ name: "ship", description: "ship it", source: "project", manualOnly: false },
]);
});
test("settings switch skills off and make them manual", async () => {
const home = tempDir();
const workspace = tempDir();
writeFileSync(
join(home, "settings.json"),
JSON.stringify({ skillOverrides: { hidden: "off", manual: "user-invocable-only", plain: "on" } }),
);
for (const name of ["hidden", "manual", "plain"])
writeSkill(join(home, "skills"), name, "---\ndescription: d\n---\n");
let skills = await runEffect(listSkills(files(), workspace, home));
assert.deepEqual(
skills.map((skill) => skill.name),
["manual", "plain"],
);
assert.equal(skills[0].manualOnly, true);
assert.equal(skills[1].manualOnly, false);
// One unreadable value drops the whole map rather than applying half of
// a policy the administrator wrote.
writeFileSync(join(home, "settings.json"), JSON.stringify({ skillOverrides: { hidden: "off", manual: true } }));
skills = await runEffect(listSkills(files(), workspace, home));
assert.equal(skills.length, 3);
});
test("disable-model-invocation becomes manual only", async () => {
const home = tempDir();
const workspace = tempDir();
writeSkill(join(home, "skills"), "audit", "---\ndescription: d\ndisable-model-invocation: true\n---\n");
writeSkill(join(home, "skills"), "modelonly", "---\ndescription: d\nuser-invocable: false\n---\n");
const skills = await runEffect(listSkills(files(), workspace, home));
assert.equal(skills.length, 1, "a skill the user cannot start does not belong in the picker");
assert.equal(skills[0].manualOnly, true);
});
/// The broker checks a read against the path it resolves to, so a skill
/// linked in from outside the grants cannot be read; the CLI's command
/// list still names it.
test("a skill whose file no grant covers comes from the CLI's commands", async () => {
const home = tempDir();
const elsewhere = tempDir();
const workspace = tempDir();
writeSkill(elsewhere, "linked", "---\ndescription: from the file\n---\n");
mkdirSync(join(home, "skills"), { recursive: true });
symlinkSync(join(elsewhere, "linked"), join(home, "skills", "linked"));
const granted = (useApi(fakeApi({ fs: diskFs({ allow: (path) => !path.includes("/linked/") }) })), apiFiles());
const skills = await runEffect(
listSkills(granted, workspace, home, [{ name: "linked", description: "from the CLI" }]),
);
assert.deepEqual(skills, [{ name: "linked", description: "from the CLI", source: "user", manualOnly: false }]);
assert.deepEqual(await runEffect(listSkills(granted, workspace, home, [])), [], "nothing proves it exists");
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 128.7 KB | 4 permissions | Listed |
No comments yet.