agent-toggle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ import { P, projectKey, projectPaths } from "./paths.js";
2
+ import { readJson, writeJson } from "../../core/jsonio.js";
3
+ /** settings.json file of a scope. */
4
+ export function settingsFile(ctx, scope) {
5
+ if (scope === "user")
6
+ return P.userSettings;
7
+ const pp = projectPaths(ctx.root);
8
+ return scope === "project" ? pp.settings : pp.settingsLocal;
9
+ }
10
+ export function readSettings(ctx, scope) {
11
+ return readJson(settingsFile(ctx, scope));
12
+ }
13
+ /** Reads, modifies, then immediately rewrites a settings.json. */
14
+ export function updateSettings(ctx, scope, what, fn) {
15
+ const file = settingsFile(ctx, scope);
16
+ const s = readJson(file);
17
+ fn(s);
18
+ writeJson(file, s, what);
19
+ }
20
+ /** Scopes a scope inherits from, strongest first (itself included). */
21
+ export function chain(scope) {
22
+ return scope === "local" ? ["local", "project", "user"] : scope === "project" ? ["project", "user"] : ["user"];
23
+ }
24
+ /**
25
+ * ~/.claude.json: Claude Code rewrites it constantly during sessions.
26
+ * It is re-read right before each write to limit races.
27
+ */
28
+ export function readClaudeJson() {
29
+ return readJson(P.claudeJson);
30
+ }
31
+ /** projects[...] keys designating this project (Windows: drive letter case varies). */
32
+ export function projectKeys(cj, root) {
33
+ // Windows: drive letter case varies between keys; elsewhere paths are case-sensitive.
34
+ const fold = (s) => (process.platform === "win32" ? s.toLowerCase() : s);
35
+ const want = fold(projectKey(root));
36
+ const keys = Object.keys(cj.projects ?? {}).filter((k) => fold(k.replace(/\\/g, "/")) === want);
37
+ return keys.length ? keys : [projectKey(root)];
38
+ }
39
+ export function updateClaudeJson(what, fn) {
40
+ const cj = readClaudeJson();
41
+ fn(cj);
42
+ writeJson(P.claudeJson, cj, what);
43
+ }
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { P, projectPaths } from "./paths.js";
4
+ import { isLink, listDir, toggled } from "../../core/jsonio.js";
5
+ import { chain, readSettings, updateSettings } from "./settings.js";
6
+ import { t } from "../../core/i18n.js";
7
+ function skillsIn(dir) {
8
+ return listDir(dir)
9
+ .filter((d) => (d.isDirectory() || d.isSymbolicLink()) && fs.existsSync(path.join(dir, d.name, "SKILL.md")))
10
+ .map((d) => d.name);
11
+ }
12
+ function override(ctx, scope, name) {
13
+ for (const sc of chain(scope)) {
14
+ const v = readSettings(ctx, sc).skillOverrides?.[name];
15
+ if (typeof v === "string")
16
+ return { value: v, from: sc };
17
+ }
18
+ return {};
19
+ }
20
+ /**
21
+ * Skills utilisateur et projet, via le setting officiel skillOverrides
22
+ * ("off" = hidden from the model and from /name). Files do not move, so
23
+ * skill sync scripts and the other agents are not affected.
24
+ * Skills shipped by a plugin are not covered: they are disabled with the plugin.
25
+ */
26
+ export const skillsSection = {
27
+ kind: "skills",
28
+ scopes: ["user", "project", "local"],
29
+ note: () => t("note.claudeSkills"),
30
+ list(ctx, scope) {
31
+ const user = skillsIn(P.userSkills);
32
+ const proj = skillsIn(projectPaths(ctx.root).skills);
33
+ const items = [];
34
+ const push = (name, group, hint) => {
35
+ const o = override(ctx, scope, name);
36
+ items.push({
37
+ id: name,
38
+ label: name,
39
+ hint: o.value && o.value !== "on" ? `${hint} · ${o.value} (${o.from})` : hint,
40
+ group,
41
+ enabled: o.value !== "off",
42
+ });
43
+ };
44
+ for (const n of user)
45
+ push(n, `${t("group.user")} (~/.claude/skills)`, isLink(path.join(P.userSkills, n)) ? t("hint.link") : t("hint.local"));
46
+ if (scope !== "user")
47
+ for (const n of proj)
48
+ push(n, `${t("group.project")} (.claude/skills)`, t("hint.project"));
49
+ const known = new Set(items.map((i) => i.id));
50
+ for (const sc of chain(scope))
51
+ for (const n of Object.keys(readSettings(ctx, sc).skillOverrides ?? {}))
52
+ if (!known.has(n)) {
53
+ known.add(n);
54
+ push(n, t("group.overrides"), t("hint.notFound"));
55
+ }
56
+ return items.sort((a, b) => a.group.localeCompare(b.group) || a.label.localeCompare(b.label));
57
+ },
58
+ set(ctx, scope, item, enabled) {
59
+ const parentScope = scope === "local" ? "project" : scope === "project" ? "user" : undefined;
60
+ const parentOff = parentScope ? override(ctx, parentScope, item.id).value === "off" : false;
61
+ updateSettings(ctx, scope, toggled(`Skill ${item.id}`, enabled, scope), (s) => {
62
+ s.skillOverrides ??= {};
63
+ if (enabled) {
64
+ if (parentOff)
65
+ s.skillOverrides[item.id] = "on";
66
+ else
67
+ delete s.skillOverrides[item.id];
68
+ }
69
+ else
70
+ s.skillOverrides[item.id] = "off";
71
+ if (!Object.keys(s.skillOverrides).length)
72
+ delete s.skillOverrides;
73
+ });
74
+ },
75
+ };
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ import { dirSection, hooksSection, shortPath } from "../core/generic.js";
3
+ import { home, pathKey, toggled } from "../core/jsonio.js";
4
+ import { addArrayTable, readToml, removeArrayTables } from "../core/toml.js";
5
+ import { t } from "../core/i18n.js";
6
+ import { tomlFlagSection } from "./_toml.js";
7
+ import { skillsIn } from "./_skills.js";
8
+ const CODEX_HOME = process.env.CODEX_HOME ?? home(".codex");
9
+ const config = { user: () => path.join(CODEX_HOME, "config.toml"), project: (c) => path.join(c.root, ".codex", "config.toml") };
10
+ const configFor = (scope, ctx) => (scope === "user" ? config.user() : config.project(ctx));
11
+ const norm = pathKey;
12
+ /** Skills: [[skills.config]] path = ".../SKILL.md", enabled = false (native). */
13
+ const skills = {
14
+ kind: "skills",
15
+ scopes: ["user", "project"],
16
+ note: (s, c) => `${shortPath(configFor(s, c))} · [[skills.config]] enabled = false`,
17
+ list(ctx, scope) {
18
+ const found = scope === "user"
19
+ ? skillsIn([home(".agents", "skills"), path.join(CODEX_HOME, "skills")], t("group.user"))
20
+ : skillsIn([path.join(ctx.root, ".agents", "skills")], t("group.project"));
21
+ const off = new Set((readToml(configFor(scope, ctx)).skills?.config ?? [])
22
+ .filter((e) => e.enabled === false).map((e) => norm(String(e.path))));
23
+ return found.map((s) => ({ id: s.file, label: s.id, hint: s.hint, group: s.group, enabled: !off.has(norm(s.file)) }))
24
+ .sort((a, b) => a.label.localeCompare(b.label));
25
+ },
26
+ set(ctx, scope, item, enabled) {
27
+ const file = configFor(scope, ctx);
28
+ const what = toggled(`Skill ${item.label}`, enabled, shortPath(file));
29
+ if (enabled)
30
+ removeArrayTables(file, ["skills", "config"], (d) => norm(String(d.path ?? "")) === norm(item.id) && d.enabled === false, what);
31
+ else
32
+ addArrayTable(file, ["skills", "config"], { path: item.id, enabled: false }, what);
33
+ },
34
+ };
35
+ export const codex = {
36
+ id: "codex",
37
+ name: "OpenAI Codex",
38
+ bins: ["codex"],
39
+ dirs: [".codex"],
40
+ sections: [
41
+ tomlFlagSection({ kind: "mcp", files: config, table: ["mcp_servers"], key: "enabled", off: false }),
42
+ tomlFlagSection({ kind: "plugins", files: config, table: ["plugins"], key: "enabled", off: false, on: true }),
43
+ skills,
44
+ hooksSection({
45
+ agent: "codex",
46
+ files: { user: () => path.join(CODEX_HOME, "hooks.json"), project: (c) => path.join(c.root, ".codex", "hooks.json") },
47
+ key: ["hooks"],
48
+ }),
49
+ dirSection({ kind: "commands", title: "title.codexPrompts", dirs: { user: () => path.join(CODEX_HOME, "prompts") }, entry: { ext: [".md"] } }),
50
+ dirSection({ kind: "agents", dirs: { user: () => path.join(CODEX_HOME, "agents"), project: (c) => path.join(c.root, ".codex", "agents") }, entry: { ext: [".toml"] } }),
51
+ ],
52
+ notes: ["notes.codex.trust", "notes.codex.hookTrust"],
53
+ };
@@ -0,0 +1,72 @@
1
+ import path from "node:path";
2
+ import { boolMapSection, dirSection, hooksSection, listSection } from "../core/generic.js";
3
+ import { home, listDir, readJson } from "../core/jsonio.js";
4
+ import { mcpGroup } from "../core/groups.js";
5
+ import { t } from "../core/i18n.js";
6
+ import { skillsIn } from "./_skills.js";
7
+ const H = process.env.COPILOT_HOME ?? home(".copilot");
8
+ const settings = {
9
+ user: () => path.join(H, "settings.json"),
10
+ project: (c) => path.join(c.root, ".github", "copilot", "settings.json"),
11
+ local: (c) => path.join(c.root, ".github", "copilot", "settings.local.json"),
12
+ };
13
+ function mcpServers(ctx) {
14
+ const out = [];
15
+ for (const [file, tag] of [
16
+ [path.join(H, "mcp-config.json"), t("hint.user")],
17
+ [path.join(ctx.root, ".mcp.json"), ".mcp.json"],
18
+ [path.join(ctx.root, ".github", "mcp.json"), ".github/mcp.json"],
19
+ ])
20
+ for (const id of Object.keys(readJson(file).mcpServers ?? {}))
21
+ out.push({ id, hint: tag, group: mcpGroup(id) });
22
+ return out;
23
+ }
24
+ export const copilot = {
25
+ id: "copilot",
26
+ name: "GitHub Copilot CLI",
27
+ bins: ["copilot"],
28
+ dirs: [".copilot"],
29
+ sections: [
30
+ listSection({
31
+ kind: "mcp",
32
+ files: settings,
33
+ path: ["disabledMcpServers"],
34
+ discover: (ctx) => mcpServers(ctx),
35
+ note: "note.copilotMcpUnion",
36
+ }),
37
+ boolMapSection({
38
+ kind: "plugins",
39
+ files: { user: settings.user, project: settings.project },
40
+ path: ["enabledPlugins"],
41
+ discover: () => (readJson(path.join(H, "config.json")).installedPlugins ?? []).map((p) => ({
42
+ id: p.marketplace ? `${p.name}@${p.marketplace}` : p.name,
43
+ hint: p.version,
44
+ enabled: p.enabled !== false,
45
+ })),
46
+ }),
47
+ listSection({
48
+ kind: "skills",
49
+ files: { user: settings.user, project: settings.project },
50
+ path: ["disabledSkills"],
51
+ discover: (ctx) => [
52
+ ...skillsIn([path.join(H, "skills"), home(".agents", "skills")], t("group.user")),
53
+ ...skillsIn([".github/skills", ".agents/skills", ".claude/skills"].map((d) => path.join(ctx.root, d)), t("group.project")),
54
+ ],
55
+ }),
56
+ hooksSection({
57
+ agent: "copilot",
58
+ title: "title.hookSettings",
59
+ files: settings,
60
+ key: ["hooks"],
61
+ disableAll: { path: ["disableAllHooks"], off: true },
62
+ }),
63
+ dirSection({ kind: "hooks", title: "title.hookFiles", dirs: { user: () => path.join(H, "hooks"), project: (c) => path.join(c.root, ".github", "hooks") }, entry: { ext: [".json"] } }),
64
+ listSection({
65
+ kind: "agents",
66
+ files: { user: settings.user, project: settings.project },
67
+ path: ["subagents", "disabledSubagents"],
68
+ discover: (ctx) => [path.join(H, "agents"), path.join(ctx.root, ".github", "agents")].flatMap((d, i) => listDir(d).filter((f) => f.name.endsWith(".md")).map((f) => ({ id: f.name.replace(/(\.agent)?\.md$/, ""), group: t(i ? "group.project" : "group.user") }))),
69
+ note: "note.copilotBuiltinAgents",
70
+ }),
71
+ ],
72
+ };
@@ -0,0 +1,32 @@
1
+ import path from "node:path";
2
+ import { dirSection, hooksSection, mcpSection } from "../core/generic.js";
3
+ import { home } from "../core/jsonio.js";
4
+ const C = home(".cursor");
5
+ export const cursor = {
6
+ id: "cursor",
7
+ name: "Cursor Agent",
8
+ bins: ["cursor-agent"],
9
+ dirs: [".cursor"],
10
+ sections: [
11
+ mcpSection({
12
+ agent: "cursor",
13
+ files: { user: () => path.join(C, "mcp.json"), project: (c) => path.join(c.root, ".cursor", "mcp.json") },
14
+ key: ["mcpServers"],
15
+ disable: { type: "flag", key: "disabled", off: true },
16
+ }),
17
+ dirSection({
18
+ kind: "skills",
19
+ dirs: { user: () => path.join(C, "skills"), project: (c) => path.join(c.root, ".cursor", "skills") },
20
+ entry: { dir: "SKILL.md" },
21
+ note: "note.linkMoved",
22
+ }),
23
+ hooksSection({
24
+ agent: "cursor",
25
+ files: { user: () => path.join(C, "hooks.json"), project: (c) => path.join(c.root, ".cursor", "hooks.json") },
26
+ key: ["hooks"],
27
+ }),
28
+ dirSection({ kind: "commands", dirs: { user: () => path.join(C, "commands"), project: (c) => path.join(c.root, ".cursor", "commands") }, entry: { ext: [".md"] } }),
29
+ dirSection({ kind: "agents", dirs: { user: () => path.join(C, "agents"), project: (c) => path.join(c.root, ".cursor", "agents") }, entry: { ext: [".md"] } }),
30
+ ],
31
+ notes: ["notes.cursor.plugins"],
32
+ };
@@ -0,0 +1,41 @@
1
+ import path from "node:path";
2
+ import { dirSection, hooksSection, mcpSection } from "../core/generic.js";
3
+ import { home } from "../core/jsonio.js";
4
+ // On Windows, Devin keeps its user config in %APPDATA%\devin.
5
+ const D = process.platform === "win32" ? path.join(process.env.APPDATA ?? home("AppData", "Roaming"), "devin") : home(".config", "devin");
6
+ export const devin = {
7
+ id: "devin",
8
+ name: "Devin CLI",
9
+ bins: ["devin"],
10
+ dirs: [".devin", D],
11
+ sections: [
12
+ mcpSection({
13
+ agent: "devin",
14
+ files: {
15
+ user: () => path.join(D, "mcp_config.json"),
16
+ project: (c) => path.join(c.root, ".devin", "mcp_config.json"),
17
+ local: (c) => path.join(c.root, ".devin", "mcp_config.local.json"),
18
+ },
19
+ key: ["mcpServers"],
20
+ disable: { type: "flag", key: "disabled", off: true },
21
+ note: "note.devinMcp",
22
+ }),
23
+ dirSection({
24
+ kind: "skills",
25
+ dirs: { user: () => path.join(D, "skills"), project: (c) => path.join(c.root, ".devin", "skills") },
26
+ entry: { dir: "SKILL.md" },
27
+ note: "note.dirMoved",
28
+ }),
29
+ hooksSection({
30
+ agent: "devin",
31
+ files: {
32
+ user: () => path.join(D, "config.json"),
33
+ project: (c) => path.join(c.root, ".devin", "config.json"),
34
+ local: (c) => path.join(c.root, ".devin", "config.local.json"),
35
+ },
36
+ key: ["hooks"],
37
+ }),
38
+ dirSection({ kind: "agents", dirs: { user: () => home(".config", "devin", "agents"), project: (c) => path.join(c.root, ".devin", "agents") }, entry: { ext: [".md"] } }),
39
+ ],
40
+ notes: ["notes.devin.plugins"],
41
+ };
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { boolMapSection, dirSection, hooksSection, listSection, mcpSection } from "../core/generic.js";
3
+ import { exists, home } from "../core/jsonio.js";
4
+ import { t } from "../core/i18n.js";
5
+ import { skillsIn } from "./_skills.js";
6
+ const F = home(".factory");
7
+ const settings = {
8
+ user: () => path.join(F, "settings.json"),
9
+ project: (c) => path.join(c.root, ".factory", "settings.json"),
10
+ local: (c) => path.join(c.root, ".factory", "settings.local.json"),
11
+ };
12
+ /** Hooks: hooks.json when it exists (it takes precedence), otherwise the hooks key of settings.json. */
13
+ const hooksFile = (dir, settingsFile) => (exists(path.join(dir, "hooks.json")) ? path.join(dir, "hooks.json") : settingsFile);
14
+ export const droid = {
15
+ id: "droid",
16
+ name: "Factory Droid",
17
+ bins: ["droid"],
18
+ dirs: [".factory"],
19
+ sections: [
20
+ mcpSection({
21
+ agent: "droid",
22
+ files: { user: () => path.join(F, "mcp.json"), project: (c) => path.join(c.root, ".factory", "mcp.json") },
23
+ key: ["mcpServers"],
24
+ disable: { type: "flag", key: "disabled", off: true },
25
+ }),
26
+ boolMapSection({ kind: "plugins", files: settings, path: ["enabledPlugins"], discover: () => [] }),
27
+ listSection({
28
+ kind: "skills",
29
+ files: { user: settings.user, project: settings.project },
30
+ path: ["disabledSkills"],
31
+ discover: (ctx) => [
32
+ ...skillsIn([path.join(F, "skills"), home(".agents", "skills")], t("group.user")),
33
+ ...skillsIn([path.join(ctx.root, ".factory", "skills"), path.join(ctx.root, ".agents", "skills")], t("group.project")),
34
+ ],
35
+ }),
36
+ hooksSection({
37
+ agent: "droid",
38
+ files: {
39
+ user: () => hooksFile(F, settings.user()),
40
+ project: (c) => hooksFile(path.join(c.root, ".factory"), settings.project(c)),
41
+ },
42
+ key: ["hooks"],
43
+ disableAll: { path: ["hooks", "hooksDisabled"], off: true },
44
+ }),
45
+ dirSection({ kind: "commands", dirs: { user: () => path.join(F, "commands"), project: (c) => path.join(c.root, ".factory", "commands") }, entry: { ext: [".md"] } }),
46
+ dirSection({ kind: "agents", title: "title.droids", dirs: { user: () => path.join(F, "droids"), project: (c) => path.join(c.root, ".factory", "droids") }, entry: { ext: [".md"] } }),
47
+ ],
48
+ };
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { dirSection, hooksSection, listSection, scopeGroup, shortPath } from "../core/generic.js";
3
+ import { editJson, getPath, home, readJson, toggled } from "../core/jsonio.js";
4
+ import { mcpGroup } from "../core/groups.js";
5
+ import { t } from "../core/i18n.js";
6
+ import { skillsIn } from "./_skills.js";
7
+ const G = home(".gemini");
8
+ const settings = { user: () => path.join(G, "settings.json"), project: (c) => path.join(c.root, ".gemini", "settings.json") };
9
+ const settingsFor = (scope, ctx) => (scope === "user" ? settings.user() : settings.project(ctx));
10
+ const servers = (ctx) => [settings.user(), settings.project(ctx)].flatMap((f, i) => Object.keys(readJson(f).mcpServers ?? {}).map((id) => ({ id, hint: t(i ? "hint.project" : "hint.user"), group: mcpGroup(id) })));
11
+ /** Names of the .md agent files of a directory. */
12
+ function agentFiles(dir) {
13
+ const s = dirSection({ kind: "agents", dirs: { user: () => dir }, entry: { ext: [".md"] } });
14
+ return s.list({ root: "" }, "user").filter((i) => i.enabled).map((i) => i.label);
15
+ }
16
+ /** Subagents: agents.overrides.<name>.enabled = false. */
17
+ const agents = {
18
+ kind: "agents",
19
+ scopes: ["user", "project"],
20
+ note: (s, c) => `${shortPath(settingsFor(s, c))} · agents.overrides.<name>.enabled`,
21
+ list(ctx, scope) {
22
+ const over = getPath(readJson(settingsFor(scope, ctx)), ["agents", "overrides"]) ?? {};
23
+ const dir = scope === "user" ? path.join(G, "agents") : path.join(ctx.root, ".gemini", "agents");
24
+ const names = new Set([...Object.keys(over), ...agentFiles(dir)]);
25
+ return [...names].sort().map((id) => ({ id, label: id, group: scopeGroup(scope), enabled: over[id]?.enabled !== false }));
26
+ },
27
+ set(ctx, scope, item, enabled) {
28
+ const file = settingsFor(scope, ctx);
29
+ editJson(file, toggled(`Agent ${item.id}`, enabled, shortPath(file)), [{ path: ["agents", "overrides", item.id, "enabled"], value: enabled ? undefined : false }]);
30
+ },
31
+ };
32
+ export const gemini = {
33
+ id: "gemini",
34
+ name: "Gemini CLI",
35
+ bins: ["gemini"],
36
+ // ~/.gemini is shared with Antigravity: only the binary counts.
37
+ dirs: [],
38
+ sections: [
39
+ listSection({ kind: "mcp", files: settings, path: ["mcp", "excluded"], discover: (ctx) => servers(ctx) }),
40
+ listSection({
41
+ kind: "skills",
42
+ files: settings,
43
+ path: ["skills", "disabled"],
44
+ discover: (ctx, scope) => scope === "user" ? skillsIn([path.join(G, "skills")], t("group.user")) : skillsIn([path.join(ctx.root, ".gemini", "skills")], t("group.project")),
45
+ }),
46
+ hooksSection({ agent: "gemini", files: settings, key: ["hooks"], disableAll: { path: ["hooksConfig", "enabled"], off: false } }),
47
+ dirSection({ kind: "commands", dirs: { user: () => path.join(G, "commands"), project: (c) => path.join(c.root, ".gemini", "commands") }, entry: { ext: [".toml"] }, sep: ":" }),
48
+ agents,
49
+ ],
50
+ notes: ["notes.gemini.extensions"],
51
+ };
@@ -0,0 +1,107 @@
1
+ import path from "node:path";
2
+ import { agentCliJson, describeServer, dirSection, runAgentCli, shortPath, sortByGroup } from "../core/generic.js";
3
+ import { home, readJson, toggled, toggleInList } from "../core/jsonio.js";
4
+ import { readToml, setTomlKey } from "../core/toml.js";
5
+ import { mcpGroup } from "../core/groups.js";
6
+ import { t } from "../core/i18n.js";
7
+ import { tomlListSection } from "./_toml.js";
8
+ import { skillsIn } from "./_skills.js";
9
+ const GROK_HOME = process.env.GROK_HOME ?? home(".grok");
10
+ const userConfig = () => path.join(GROK_HOME, "config.toml");
11
+ const projectConfig = (c) => path.join(c.root, ".grok", "config.toml");
12
+ /**
13
+ * MCP. User: native disabled_mcp_servers list, valid for every source
14
+ * (config, .mcp.json, Claude/Cursor compat, plugins).
15
+ * Project: enabled = false on the servers defined in .grok/config.toml.
16
+ */
17
+ const mcp = {
18
+ kind: "mcp",
19
+ scopes: ["user", "project"],
20
+ note: (s, c) => s === "user"
21
+ ? `${shortPath(userConfig())} · disabled_mcp_servers (${t("note.allSources")})`
22
+ : `${shortPath(projectConfig(c))} · [mcp_servers.<name>] enabled = false`,
23
+ list(ctx, scope) {
24
+ const items = new Map();
25
+ const add = (id, hint, enabled) => {
26
+ if (!items.has(id))
27
+ items.set(id, { id, label: id, hint, group: mcpGroup(id), enabled });
28
+ };
29
+ if (scope === "user") {
30
+ const cfg = readToml(userConfig());
31
+ const off = cfg.disabled_mcp_servers ?? [];
32
+ for (const [n, d] of Object.entries(cfg.mcp_servers ?? {}))
33
+ add(n, describeServer(d), !off.includes(n) && d.enabled !== false);
34
+ for (const [n, d] of Object.entries(readToml(projectConfig(ctx)).mcp_servers ?? {}))
35
+ add(n, `${t("hint.project")} · ${describeServer(d)}`, !off.includes(n));
36
+ for (const [n, d] of Object.entries(readJson(path.join(ctx.root, ".mcp.json")).mcpServers ?? {}))
37
+ add(n, `.mcp.json · ${describeServer(d)}`, !off.includes(n));
38
+ for (const n of off)
39
+ add(n, t("hint.otherSource"), false);
40
+ }
41
+ else {
42
+ for (const [n, d] of Object.entries(readToml(projectConfig(ctx)).mcp_servers ?? {}))
43
+ add(n, describeServer(d), d.enabled !== false);
44
+ }
45
+ return sortByGroup([...items.values()]);
46
+ },
47
+ set(ctx, scope, item, enabled) {
48
+ const label = `MCP ${item.id}`;
49
+ if (scope === "user") {
50
+ const list = toggleInList(readToml(userConfig()).disabled_mcp_servers, item.id, !enabled);
51
+ setTomlKey(userConfig(), [], "disabled_mcp_servers", list.length ? list : undefined, toggled(label, enabled, shortPath(userConfig())));
52
+ // An enabled = false set elsewhere would prevent re-enabling.
53
+ if (enabled && readToml(userConfig()).mcp_servers?.[item.id]?.enabled === false)
54
+ setTomlKey(userConfig(), ["mcp_servers", item.id], "enabled", undefined, toggled(label, true, "enabled = false"));
55
+ }
56
+ else
57
+ setTomlKey(projectConfig(ctx), ["mcp_servers", item.id], "enabled", enabled ? undefined : false, toggled(label, enabled, shortPath(projectConfig(ctx))));
58
+ },
59
+ };
60
+ /** Plugins: state read from [plugins], switched through "grok plugin enable|disable". */
61
+ const plugins = {
62
+ kind: "plugins",
63
+ scopes: ["user"],
64
+ note: () => t("note.grokPlugins"),
65
+ list() {
66
+ const cfg = readToml(userConfig()).plugins ?? {};
67
+ const on = cfg.enabled ?? [];
68
+ const off = cfg.disabled ?? [];
69
+ const found = agentCliJson("grok", ["plugin", "list", "--json"]) ?? [];
70
+ return found.map((p) => ({
71
+ id: p.name,
72
+ label: p.name,
73
+ hint: `${p.marketplace ?? ""} ${p.version ?? ""}`.trim(),
74
+ group: t("group.installed"),
75
+ enabled: !off.includes(p.name) && (on.length === 0 || on.includes(p.name)),
76
+ }));
77
+ },
78
+ set(_ctx, _scope, item, enabled) {
79
+ runAgentCli("grok", ["plugin", enabled ? "enable" : "disable", item.id], toggled(`Plugin ${item.id}`, enabled, "grok plugin"));
80
+ },
81
+ };
82
+ export const grok = {
83
+ id: "grok",
84
+ name: "Grok Build",
85
+ bins: ["grok"],
86
+ dirs: [".grok"],
87
+ sections: [
88
+ mcp,
89
+ plugins,
90
+ tomlListSection({
91
+ kind: "skills",
92
+ files: { user: userConfig },
93
+ table: ["skills"],
94
+ key: "disabled",
95
+ discover: (ctx) => [
96
+ ...skillsIn([path.join(GROK_HOME, "skills"), home(".agents", "skills")], t("group.user")),
97
+ ...skillsIn([path.join(ctx.root, ".grok", "skills"), path.join(ctx.root, ".agents", "skills")], t("group.project")),
98
+ ...skillsIn([path.join(GROK_HOME, "bundled", "skills")], t("group.builtin")),
99
+ ],
100
+ note: "note.grokSkills",
101
+ }),
102
+ dirSection({ kind: "hooks", title: "title.hookFiles", dirs: { user: () => path.join(GROK_HOME, "hooks"), project: (c) => path.join(c.root, ".grok", "hooks") }, entry: { ext: [".json"] } }),
103
+ dirSection({ kind: "commands", dirs: { user: () => path.join(GROK_HOME, "commands"), project: (c) => path.join(c.root, ".grok", "commands") }, entry: { ext: [".md"] }, sep: ":" }),
104
+ dirSection({ kind: "agents", dirs: { user: () => path.join(GROK_HOME, "agents"), project: (c) => path.join(c.root, ".grok", "agents") }, entry: { ext: [".md"] } }),
105
+ ],
106
+ notes: ["notes.grok.compat", "notes.grok.trust"],
107
+ };
@@ -0,0 +1,41 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { exists, home } from "../core/jsonio.js";
4
+ import { t } from "../core/i18n.js";
5
+ import { claude } from "./claude/index.js";
6
+ import { codex } from "./codex.js";
7
+ import { grok } from "./grok.js";
8
+ import { droid } from "./droid.js";
9
+ import { cursor } from "./cursor.js";
10
+ import { copilot } from "./copilot.js";
11
+ import { devin } from "./devin.js";
12
+ import { gemini } from "./gemini.js";
13
+ import { antigravity } from "./antigravity.js";
14
+ import { qwen } from "./qwen.js";
15
+ import { amp, crush, kilo, kiro, opencode, warp } from "./others.js";
16
+ import { aider, amazonq, auggie, cline, codebuddy, codebuff, commandcode, continueCli, goose, hermes, iflow, junie, kimi, letta, openhands, pi, plandex, qoder, qodo, rovodev, trae, vibe, windsurf, } from "./more.js";
17
+ /** Every supported agent; only the detected ones are shown by default. */
18
+ export const AGENTS = [
19
+ claude, codex, gemini, grok, cursor, copilot, devin, antigravity, qwen, droid, opencode, kilo, amp, crush, kiro, amazonq,
20
+ goose, cline, continueCli, auggie, junie, openhands, kimi, vibe, rovodev, qoder, codebuddy, letta, pi, codebuff, hermes,
21
+ commandcode, windsurf, trae, iflow, qodo, aider, plandex, warp,
22
+ ];
23
+ function onPath(bin) {
24
+ const r = process.platform === "win32"
25
+ ? spawnSync("where", [bin], { encoding: "utf8" })
26
+ : spawnSync("sh", ["-c", `command -v ${bin}`], { encoding: "utf8" });
27
+ return r.status === 0 && !!r.stdout.trim();
28
+ }
29
+ const cache = new Map();
30
+ /** Installed = a binary on the PATH, or failing that a characteristic directory. */
31
+ export function isInstalled(a) {
32
+ if (!cache.has(a.id))
33
+ cache.set(a.id, a.bins.some(onPath) || a.dirs.some((d) => exists(path.isAbsolute(d) ? d : home(d))));
34
+ return cache.get(a.id);
35
+ }
36
+ export function findAgent(id) {
37
+ const a = AGENTS.find((x) => x.id === id || x.bins.includes(id));
38
+ if (!a)
39
+ throw new Error(t("err.unknownAgent", { id, agents: AGENTS.map((x) => x.id).join(", ") }));
40
+ return a;
41
+ }