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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/agents/_skills.js +15 -0
- package/dist/agents/_toml.js +66 -0
- package/dist/agents/antigravity.js +68 -0
- package/dist/agents/claude/index.js +31 -0
- package/dist/agents/claude/mcp.js +164 -0
- package/dist/agents/claude/paths.js +32 -0
- package/dist/agents/claude/plugins.js +82 -0
- package/dist/agents/claude/settings.js +43 -0
- package/dist/agents/claude/skills.js +75 -0
- package/dist/agents/codex.js +53 -0
- package/dist/agents/copilot.js +72 -0
- package/dist/agents/cursor.js +32 -0
- package/dist/agents/devin.js +41 -0
- package/dist/agents/droid.js +48 -0
- package/dist/agents/gemini.js +51 -0
- package/dist/agents/grok.js +107 -0
- package/dist/agents/index.js +41 -0
- package/dist/agents/more.js +483 -0
- package/dist/agents/others.js +192 -0
- package/dist/agents/qwen.js +46 -0
- package/dist/core/generic.js +334 -0
- package/dist/core/groups.js +94 -0
- package/dist/core/i18n.js +832 -0
- package/dist/core/jsonio.js +136 -0
- package/dist/core/logo.js +128 -0
- package/dist/core/stash.js +21 -0
- package/dist/core/toml.js +138 -0
- package/dist/core/types.js +4 -0
- package/dist/core/yaml.js +31 -0
- package/dist/index.js +467 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { logo } from "./core/logo.js";
|
|
7
|
+
import { BACKUP_DIR, HOME, state } from "./core/jsonio.js";
|
|
8
|
+
import { initGroupsFile } from "./core/groups.js";
|
|
9
|
+
import { getLang, isLang, LANGS, resolveLang, saveLang, setLang, t } from "./core/i18n.js";
|
|
10
|
+
import { scopeLabel as defaultScopeLabel, sectionKindTitle, SECTION_KINDS } from "./core/types.js";
|
|
11
|
+
import { AGENTS, findAgent, isInstalled } from "./agents/index.js";
|
|
12
|
+
import { claudeAiConnectorsDisabled, setClaudeAiConnectors } from "./agents/claude/index.js";
|
|
13
|
+
const CONNECTORS_URL = "https://claude.ai/settings/connectors";
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const a = { rest: [], project: process.cwd(), dryRun: false, all: false };
|
|
16
|
+
for (let i = 0; i < argv.length; i++) {
|
|
17
|
+
const v = argv[i];
|
|
18
|
+
if (v === "-p" || v === "--project")
|
|
19
|
+
a.project = path.resolve(argv[++i]);
|
|
20
|
+
else if (v === "-s" || v === "--scope")
|
|
21
|
+
a.scopes = parseScopes(argv[++i]);
|
|
22
|
+
else if (v === "-n" || v === "--dry-run")
|
|
23
|
+
a.dryRun = true;
|
|
24
|
+
else if (v === "-l" || v === "--lang")
|
|
25
|
+
a.lang = argv[++i];
|
|
26
|
+
else if (v === "-a" || v === "--all")
|
|
27
|
+
a.all = true;
|
|
28
|
+
else if (v === "-h" || v === "--help")
|
|
29
|
+
a.cmd = "help";
|
|
30
|
+
else if (!a.cmd)
|
|
31
|
+
a.cmd = v;
|
|
32
|
+
else
|
|
33
|
+
a.rest.push(v);
|
|
34
|
+
}
|
|
35
|
+
return a;
|
|
36
|
+
}
|
|
37
|
+
function parseScopes(v) {
|
|
38
|
+
const map = { user: ["user"], project: ["project"], local: ["local"], both: ["user", "project"], all: ["user", "project", "local"] };
|
|
39
|
+
const s = map[v];
|
|
40
|
+
if (!s)
|
|
41
|
+
throw new Error(t("err.badScope", { scope: v }));
|
|
42
|
+
return s;
|
|
43
|
+
}
|
|
44
|
+
const help = () => t("help", {
|
|
45
|
+
agents: AGENTS.map((a) => a.id).join(", "),
|
|
46
|
+
kinds: SECTION_KINDS.join(" | "),
|
|
47
|
+
langs: Object.keys(LANGS).join("|"),
|
|
48
|
+
});
|
|
49
|
+
// ---------------------------------------------------------------- core
|
|
50
|
+
const sectionTitle = (s) => (s.title ? t(s.title) : sectionKindTitle(s.kind));
|
|
51
|
+
const scopeLabel = (s, sc) => (s.scopeLabel?.[sc] ? t(s.scopeLabel[sc]) : defaultScopeLabel(sc));
|
|
52
|
+
/** Fits the requested scopes to those of the section (project ↔ local when only one exists). */
|
|
53
|
+
function fitScopes(sec, scopes) {
|
|
54
|
+
const out = scopes.map((s) => sec.scopes.includes(s) ? s
|
|
55
|
+
: s === "project" && sec.scopes.includes("local") ? "local"
|
|
56
|
+
: s === "local" && sec.scopes.includes("project") ? "project" : s);
|
|
57
|
+
return [...new Set(out)].filter((s) => sec.scopes.includes(s));
|
|
58
|
+
}
|
|
59
|
+
const matchesKind = (s, kind) => s.kind === kind || `${s.kind}s` === kind || s.kind === `${kind}s`;
|
|
60
|
+
function sectionsOf(agent, kind) {
|
|
61
|
+
const secs = agent.sections.filter((s) => matchesKind(s, kind));
|
|
62
|
+
if (!secs.length)
|
|
63
|
+
throw new Error(t("err.nothingForKind", { agent: agent.name, kind, kinds: agent.sections.map((s) => s.kind).join(", ") || t("label.noKind") }));
|
|
64
|
+
return secs;
|
|
65
|
+
}
|
|
66
|
+
function applyDesired(sec, ctx, scopes, desired) {
|
|
67
|
+
for (const scope of scopes)
|
|
68
|
+
for (const item of sec.list(ctx, scope)) {
|
|
69
|
+
const want = desired.get(item.id);
|
|
70
|
+
if (want !== undefined && want !== item.enabled)
|
|
71
|
+
sec.set(ctx, scope, item, want);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const dryTag = () => (state.dryRun ? `[${t("label.dryRun")}] ` : "");
|
|
75
|
+
function printChanges(start = 0) {
|
|
76
|
+
const ch = state.changes.slice(start);
|
|
77
|
+
if (!ch.length)
|
|
78
|
+
return console.log(pc.dim(t("msg.noChange")));
|
|
79
|
+
for (const c of ch)
|
|
80
|
+
console.log(`${state.dryRun ? pc.yellow(`[${t("label.dryRun")}]`) : pc.green("✔")} ${c}`);
|
|
81
|
+
}
|
|
82
|
+
function openUrl(url) {
|
|
83
|
+
const [cmd, args] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
84
|
+
spawn(cmd, args, { detached: true, stdio: "ignore" }).unref();
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------- non-interactive
|
|
87
|
+
function printList(agent, secs, ctx, scopes) {
|
|
88
|
+
for (const sec of secs)
|
|
89
|
+
for (const scope of fitScopes(sec, scopes ?? sec.scopes)) {
|
|
90
|
+
console.log(pc.bold(`\n${agent.name} — ${sectionTitle(sec)} — ${scopeLabel(sec, scope)}`));
|
|
91
|
+
let items = [];
|
|
92
|
+
try {
|
|
93
|
+
items = sec.list(ctx, scope);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
console.log(pc.red(` ${t("label.error")}: ${e.message}`));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!items.length)
|
|
100
|
+
console.log(pc.dim(` (${t("label.none")})`));
|
|
101
|
+
let group = "";
|
|
102
|
+
for (const it of items) {
|
|
103
|
+
if (it.group !== group)
|
|
104
|
+
console.log(pc.cyan(` ${(group = it.group)}`));
|
|
105
|
+
console.log(` ${it.enabled ? pc.green("●") : pc.red("○")} ${it.label}${it.hint ? pc.dim(" " + it.hint) : ""}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function printLang() {
|
|
110
|
+
console.log(t("msg.currentLang", { lang: `${getLang()} (${LANGS[getLang()]})` }));
|
|
111
|
+
for (const [code, name] of Object.entries(LANGS))
|
|
112
|
+
console.log(` ${code === getLang() ? pc.green("●") : pc.dim("○")} ${code} ${name}`);
|
|
113
|
+
}
|
|
114
|
+
function runCli(a) {
|
|
115
|
+
const ctx = { root: a.project };
|
|
116
|
+
switch (a.cmd) {
|
|
117
|
+
case "help":
|
|
118
|
+
if (process.stdout.isTTY)
|
|
119
|
+
console.log(`${logo()}\n`);
|
|
120
|
+
return console.log(help());
|
|
121
|
+
case "groups":
|
|
122
|
+
return console.log(t("msg.groupsFile", { file: initGroupsFile() }));
|
|
123
|
+
case "lang": {
|
|
124
|
+
const code = a.rest[0];
|
|
125
|
+
if (!code)
|
|
126
|
+
return printLang();
|
|
127
|
+
if (!isLang(code))
|
|
128
|
+
throw new Error(t("err.badLang", { lang: code, langs: Object.keys(LANGS).join(", ") }));
|
|
129
|
+
saveLang(code);
|
|
130
|
+
setLang(code);
|
|
131
|
+
return console.log(t("msg.langSaved", { lang: `${code} (${LANGS[code]})` }));
|
|
132
|
+
}
|
|
133
|
+
case "agents":
|
|
134
|
+
// Only detected agents, unless --all asks for every supported one.
|
|
135
|
+
for (const ag of a.all ? AGENTS : AGENTS.filter(isInstalled)) {
|
|
136
|
+
const ok = isInstalled(ag);
|
|
137
|
+
console.log(`${ok ? pc.green("●") : pc.dim("○")} ${ag.id.padEnd(12)} ${ag.name.padEnd(26)} ${pc.dim(ag.sections.map((s) => sectionTitle(s)).join(", ") || t("label.nothingManageable"))}`);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
case "list": {
|
|
141
|
+
const [who, kind] = a.rest;
|
|
142
|
+
if (!who || !kind)
|
|
143
|
+
throw new Error(t("usage.list"));
|
|
144
|
+
const agents = who === "all" ? AGENTS.filter(isInstalled) : [findAgent(who)];
|
|
145
|
+
for (const ag of agents) {
|
|
146
|
+
const secs = ag.sections.filter((s) => matchesKind(s, kind));
|
|
147
|
+
if (secs.length)
|
|
148
|
+
printList(ag, secs, ctx, a.scopes);
|
|
149
|
+
else if (who !== "all")
|
|
150
|
+
sectionsOf(ag, kind);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
case "enable":
|
|
155
|
+
case "disable": {
|
|
156
|
+
const [who, kind, ...ids] = a.rest;
|
|
157
|
+
if (!who || !kind || !ids.length)
|
|
158
|
+
throw new Error(t("usage.toggle", { cmd: a.cmd }));
|
|
159
|
+
const agent = findAgent(who);
|
|
160
|
+
const secs = sectionsOf(agent, kind);
|
|
161
|
+
const todo = new Set(ids);
|
|
162
|
+
for (const sec of secs) {
|
|
163
|
+
const scopes = fitScopes(sec, a.scopes ?? [sec.scopes[0]]);
|
|
164
|
+
const items = scopes.flatMap((s) => sec.list(ctx, s));
|
|
165
|
+
const hits = ids.map((id) => items.find((it) => it.id === id) ?? items.find((it) => it.label === id)).filter(Boolean);
|
|
166
|
+
for (const h of hits)
|
|
167
|
+
todo.delete(ids.find((id) => id === h.id || id === h.label));
|
|
168
|
+
applyDesired(sec, ctx, scopes, new Map(hits.map((h) => [h.id, a.cmd === "enable"])));
|
|
169
|
+
}
|
|
170
|
+
if (todo.size)
|
|
171
|
+
throw new Error(t("err.notFound", { ids: [...todo].join(", "), agent: agent.id, kind }));
|
|
172
|
+
return printChanges();
|
|
173
|
+
}
|
|
174
|
+
case "claudeai": {
|
|
175
|
+
if (!["on", "off"].includes(a.rest[0]))
|
|
176
|
+
throw new Error(t("usage.claudeai"));
|
|
177
|
+
for (const s of a.scopes ?? ["user"])
|
|
178
|
+
setClaudeAiConnectors(ctx, s, a.rest[0] === "off");
|
|
179
|
+
return printChanges();
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`${t("err.unknownCommand", { cmd: a.cmd ?? "" })}\n\n${help()}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------- interactive
|
|
186
|
+
/*
|
|
187
|
+
* Navigation: Esc goes back to the previous menu
|
|
188
|
+
* (list → scope → category → agent → main menu, which quits).
|
|
189
|
+
*/
|
|
190
|
+
function logNew(start) {
|
|
191
|
+
for (const c of state.changes.slice(start))
|
|
192
|
+
p.log.success(`${dryTag()}${c}`);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Grouped checkbox list, summary, then apply.
|
|
196
|
+
* Returns false when the user went back without applying anything.
|
|
197
|
+
*/
|
|
198
|
+
async function pickAndApply(title, items, apply) {
|
|
199
|
+
const groups = {};
|
|
200
|
+
for (const it of items)
|
|
201
|
+
(groups[it.group] ??= []).push({ value: it.id, label: it.label, hint: it.hint });
|
|
202
|
+
let initial = items.filter((i) => i.enabled).map((i) => i.id);
|
|
203
|
+
for (;;) {
|
|
204
|
+
const selected = await p.groupMultiselect({
|
|
205
|
+
message: `${title} ${pc.dim(t("prompt.listKeys"))}`,
|
|
206
|
+
options: groups,
|
|
207
|
+
initialValues: initial,
|
|
208
|
+
required: false,
|
|
209
|
+
selectableGroups: true,
|
|
210
|
+
maxItems: Math.max(10, (process.stdout.rows ?? 30) - 8),
|
|
211
|
+
});
|
|
212
|
+
if (p.isCancel(selected))
|
|
213
|
+
return false;
|
|
214
|
+
const chosen = new Set(selected);
|
|
215
|
+
const changed = items.filter((i) => chosen.has(i.id) !== i.enabled);
|
|
216
|
+
if (!changed.length) {
|
|
217
|
+
p.log.info(t("msg.nothingToChange"));
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
p.note(changed.map((i) => `${chosen.has(i.id) ? pc.green(`+ ${t("label.enable")}`) : pc.red(`- ${t("label.disable")}`)} ${i.label}${i.hint ? pc.dim(" " + i.hint) : ""}`).join("\n"), t("msg.changesCount", { n: changed.length }));
|
|
221
|
+
const ok = await p.confirm({ message: t(state.dryRun ? "prompt.simulate" : "prompt.apply") });
|
|
222
|
+
if (p.isCancel(ok) || !ok) {
|
|
223
|
+
// Back to the list, keeping the checked boxes.
|
|
224
|
+
initial = selected;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const start = state.changes.length;
|
|
228
|
+
try {
|
|
229
|
+
apply(new Map(changed.map((i) => [i.id, chosen.has(i.id)])));
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
p.log.error(e.message);
|
|
233
|
+
}
|
|
234
|
+
logNew(start);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const back = () => pc.dim(t("prompt.escBack"));
|
|
239
|
+
async function chooseScopes(sec) {
|
|
240
|
+
if (sec.scopes.length === 1)
|
|
241
|
+
return sec.scopes;
|
|
242
|
+
const opts = sec.scopes.map((s) => ({ value: [s], label: scopeLabel(sec, s) }));
|
|
243
|
+
const proj = sec.scopes.find((s) => s !== "user");
|
|
244
|
+
if (proj && sec.scopes.includes("user") && sec.kind !== "hooks" && sec.kind !== "commands" && sec.kind !== "agents")
|
|
245
|
+
opts.push({ value: ["user", proj], label: t("scope.userProject") });
|
|
246
|
+
const v = await p.select({ message: `${sectionTitle(sec)} — ${t("prompt.scope")} ${back()}`, options: opts });
|
|
247
|
+
return p.isCancel(v) ? undefined : v;
|
|
248
|
+
}
|
|
249
|
+
async function editSection(agent, sec, ctx) {
|
|
250
|
+
for (;;) {
|
|
251
|
+
const scopes = await chooseScopes(sec);
|
|
252
|
+
if (!scopes)
|
|
253
|
+
return;
|
|
254
|
+
for (const s of scopes) {
|
|
255
|
+
const n = sec.note?.(s, ctx);
|
|
256
|
+
if (n)
|
|
257
|
+
p.note(n, `${agent.name} — ${scopeLabel(sec, s)}`);
|
|
258
|
+
}
|
|
259
|
+
// An item is checked when it is enabled in every chosen scope.
|
|
260
|
+
const merged = new Map();
|
|
261
|
+
try {
|
|
262
|
+
for (const s of scopes)
|
|
263
|
+
for (const it of sec.list(ctx, s)) {
|
|
264
|
+
const prev = merged.get(it.id);
|
|
265
|
+
merged.set(it.id, prev ? { ...prev, enabled: prev.enabled && it.enabled } : it);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
p.log.error(e.message);
|
|
270
|
+
if (sec.scopes.length === 1)
|
|
271
|
+
return;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (!merged.size) {
|
|
275
|
+
p.log.info(t("msg.noItems", { title: sectionTitle(sec) }));
|
|
276
|
+
if (sec.scopes.length === 1)
|
|
277
|
+
return;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const done = await pickAndApply(`${agent.name} — ${sectionTitle(sec)}`, [...merged.values()], (d) => applyDesired(sec, ctx, scopes, d));
|
|
281
|
+
if (done || sec.scopes.length === 1)
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function editClaudeAi(ctx) {
|
|
286
|
+
const u = claudeAiConnectorsDisabled(ctx, "user") === true;
|
|
287
|
+
const l = claudeAiConnectorsDisabled(ctx, "local") === true;
|
|
288
|
+
p.note(t("claudeai.status", {
|
|
289
|
+
user: u ? pc.red(t("claudeai.off")) : pc.green(t("claudeai.on")),
|
|
290
|
+
local: l ? pc.red(t("claudeai.off")) : pc.green(t("claudeai.inherited")),
|
|
291
|
+
url: CONNECTORS_URL,
|
|
292
|
+
}), t("label.claudeaiConnectors"));
|
|
293
|
+
const action = await p.select({
|
|
294
|
+
message: `${t("prompt.action")} ${back()}`,
|
|
295
|
+
options: [
|
|
296
|
+
{ value: "user-off", label: t("claudeai.userOff"), hint: "disableClaudeAiConnectors" },
|
|
297
|
+
{ value: "user-on", label: t("claudeai.userOn") },
|
|
298
|
+
{ value: "local-off", label: t("claudeai.localOff"), hint: ".claude/settings.local.json" },
|
|
299
|
+
{ value: "local-on", label: t("claudeai.localOn") },
|
|
300
|
+
{ value: "open", label: t("claudeai.open"), hint: t("claudeai.openHint") },
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
if (p.isCancel(action))
|
|
304
|
+
return;
|
|
305
|
+
if (action === "open")
|
|
306
|
+
return openUrl(CONNECTORS_URL);
|
|
307
|
+
const [scope, onOff] = action.split("-");
|
|
308
|
+
const start = state.changes.length;
|
|
309
|
+
setClaudeAiConnectors(ctx, scope, onOff === "off");
|
|
310
|
+
logNew(start);
|
|
311
|
+
}
|
|
312
|
+
async function editAgent(agent, ctx) {
|
|
313
|
+
if (agent.notes?.length)
|
|
314
|
+
p.note(agent.notes.map((k) => t(k)).join("\n"), agent.name);
|
|
315
|
+
for (;;) {
|
|
316
|
+
const choice = await p.select({
|
|
317
|
+
message: `${agent.name} — ${t("prompt.category")} ${back()}`,
|
|
318
|
+
options: [
|
|
319
|
+
...agent.sections.map((s, i) => ({ value: String(i), label: sectionTitle(s) })),
|
|
320
|
+
...(agent.id === "claude" ? [{ value: "claudeai", label: t("label.claudeaiConnectors"), hint: t("claudeai.globalHint") }] : []),
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
if (p.isCancel(choice))
|
|
324
|
+
return;
|
|
325
|
+
if (choice === "claudeai")
|
|
326
|
+
await editClaudeAi(ctx);
|
|
327
|
+
else
|
|
328
|
+
await editSection(agent, agent.sections[Number(choice)], ctx);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Cross view: the MCP servers of every agent, grouped by function. */
|
|
332
|
+
async function crossMcp(ctx) {
|
|
333
|
+
for (;;) {
|
|
334
|
+
const scope = await p.select({
|
|
335
|
+
message: `${t("menu.crossMcp")} — ${t("prompt.scope")} ${back()}`,
|
|
336
|
+
options: [{ value: "user", label: t("scope.user") }, { value: "project", label: t("scope.projectShort") }],
|
|
337
|
+
});
|
|
338
|
+
if (p.isCancel(scope))
|
|
339
|
+
return;
|
|
340
|
+
const targets = [];
|
|
341
|
+
const items = [];
|
|
342
|
+
for (const agent of AGENTS.filter(isInstalled))
|
|
343
|
+
for (const sec of agent.sections.filter((s) => s.kind === "mcp")) {
|
|
344
|
+
const sc = fitScopes(sec, [scope])[0];
|
|
345
|
+
if (!sc)
|
|
346
|
+
continue;
|
|
347
|
+
try {
|
|
348
|
+
for (const it of sec.list(ctx, sc))
|
|
349
|
+
items.push({ ...it, id: `${agent.id}::${it.id}`, hint: `${agent.name}${it.hint ? " · " + it.hint : ""}` });
|
|
350
|
+
targets.push({ agent, sec, scope: sc });
|
|
351
|
+
}
|
|
352
|
+
catch (e) {
|
|
353
|
+
p.log.warn(`${agent.name}: ${e.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!items.length) {
|
|
357
|
+
p.log.info(t("msg.noMcp"));
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const done = await pickAndApply(t("menu.crossMcp"), items, (desired) => {
|
|
361
|
+
for (const tg of targets) {
|
|
362
|
+
const prefix = `${tg.agent.id}::`;
|
|
363
|
+
const mine = new Map([...desired].filter(([id]) => id.startsWith(prefix)).map(([id, v]) => [id.slice(prefix.length), v]));
|
|
364
|
+
if (mine.size)
|
|
365
|
+
applyDesired(tg.sec, ctx, [tg.scope], mine);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
if (done)
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function chooseLang() {
|
|
373
|
+
const v = await p.select({
|
|
374
|
+
message: `${t("menu.language")} ${back()}`,
|
|
375
|
+
options: Object.entries(LANGS).map(([value, label]) => ({ value, label, hint: value === getLang() ? "●" : undefined })),
|
|
376
|
+
initialValue: getLang(),
|
|
377
|
+
});
|
|
378
|
+
if (p.isCancel(v))
|
|
379
|
+
return;
|
|
380
|
+
setLang(v);
|
|
381
|
+
saveLang(v);
|
|
382
|
+
p.log.success(t("msg.langSaved", { lang: `${v} (${LANGS[v]})` }));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Keeps the terminal in raw mode for the whole session. Between prompts, clack
|
|
386
|
+
* switches back to line mode; on Windows the console then starts a line read
|
|
387
|
+
* that swallows the next key until Enter is pressed (hence an Esc that seemed
|
|
388
|
+
* "stuck" after going back). In raw mode Ctrl+C no longer raises SIGINT, so it
|
|
389
|
+
* is handled here to quit.
|
|
390
|
+
*/
|
|
391
|
+
function holdRawMode() {
|
|
392
|
+
const stdin = process.stdin;
|
|
393
|
+
if (!stdin.isTTY)
|
|
394
|
+
return () => { };
|
|
395
|
+
const original = stdin.setRawMode.bind(stdin);
|
|
396
|
+
original(true);
|
|
397
|
+
stdin.setRawMode = (mode) => (mode ? original(true) : stdin);
|
|
398
|
+
const onKey = (_, key) => {
|
|
399
|
+
if (key?.ctrl && key.name === "c") {
|
|
400
|
+
release();
|
|
401
|
+
p.cancel(t("msg.interrupted"));
|
|
402
|
+
process.exit(130);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
stdin.on("keypress", onKey);
|
|
406
|
+
const release = () => {
|
|
407
|
+
stdin.off("keypress", onKey);
|
|
408
|
+
stdin.setRawMode = original;
|
|
409
|
+
original(false);
|
|
410
|
+
};
|
|
411
|
+
return release;
|
|
412
|
+
}
|
|
413
|
+
async function interactive(a) {
|
|
414
|
+
const ctx = { root: a.project };
|
|
415
|
+
const release = holdRawMode();
|
|
416
|
+
console.log(`\n${logo()}\n`);
|
|
417
|
+
p.intro(pc.inverse(" agent-toggle ") + (state.dryRun ? pc.yellow(` ${t("label.dryRun")}`) : ""));
|
|
418
|
+
const installed = AGENTS.filter(isInstalled);
|
|
419
|
+
for (;;) {
|
|
420
|
+
const choice = await p.select({
|
|
421
|
+
message: `${t("menu.project")}: ${pc.cyan(ctx.root)}${ctx.root === HOME ? pc.dim(` (${t("menu.homeDir")})`) : ""}`,
|
|
422
|
+
options: [
|
|
423
|
+
{ value: "::mcp", label: t("menu.crossMcp"), hint: t("menu.crossMcpHint") },
|
|
424
|
+
...installed.map((ag) => ({ value: ag.id, label: ag.name, hint: ag.sections.length ? ag.sections.map((s) => sectionTitle(s)).join(" · ") : t("label.nothingManageable") })),
|
|
425
|
+
{ value: "::project", label: t("menu.changeProject") },
|
|
426
|
+
{ value: "::lang", label: t("menu.language"), hint: LANGS[getLang()] },
|
|
427
|
+
{ value: "::quit", label: t("menu.quit") },
|
|
428
|
+
],
|
|
429
|
+
maxItems: Math.max(8, (process.stdout.rows ?? 30) - 6),
|
|
430
|
+
});
|
|
431
|
+
// Esc in the main menu: quit cleanly.
|
|
432
|
+
if (p.isCancel(choice) || choice === "::quit")
|
|
433
|
+
break;
|
|
434
|
+
if (choice === "::project") {
|
|
435
|
+
const dir = await p.text({ message: t("prompt.projectDir"), initialValue: ctx.root });
|
|
436
|
+
if (!p.isCancel(dir))
|
|
437
|
+
ctx.root = path.resolve(dir);
|
|
438
|
+
}
|
|
439
|
+
else if (choice === "::lang")
|
|
440
|
+
await chooseLang();
|
|
441
|
+
else if (choice === "::mcp")
|
|
442
|
+
await crossMcp(ctx);
|
|
443
|
+
else {
|
|
444
|
+
const agent = findAgent(choice);
|
|
445
|
+
if (!agent.sections.length)
|
|
446
|
+
p.note(agent.notes?.map((k) => t(k)).join("\n") ?? t("label.nothingManageable"), agent.name);
|
|
447
|
+
else
|
|
448
|
+
await editAgent(agent, ctx);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
release();
|
|
452
|
+
p.outro(state.changes.length && !state.dryRun ? t("msg.outroChanged", { dir: BACKUP_DIR }) : t("msg.bye"));
|
|
453
|
+
}
|
|
454
|
+
// ---------------------------------------------------------------- entry point
|
|
455
|
+
const args = parseArgs(process.argv.slice(2));
|
|
456
|
+
try {
|
|
457
|
+
setLang(resolveLang(args.lang));
|
|
458
|
+
state.dryRun = args.dryRun;
|
|
459
|
+
if (!args.cmd)
|
|
460
|
+
await interactive(args);
|
|
461
|
+
else
|
|
462
|
+
runCli(args);
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
console.error(pc.red(`${t("label.error")}: ${e.message}`));
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-toggle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI to enable/disable the MCP servers, plugins, skills, hooks, commands and agents of every installed AI agent CLI (Claude Code, Codex, Grok, Cursor, Copilot, Gemini…)",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsx src/index.ts",
|
|
8
|
+
"start": "node dist/index.js",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"prepack": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@clack/prompts": "^1.8.1",
|
|
16
|
+
"jsonc-parser": "^3.3.1",
|
|
17
|
+
"picocolors": "^1.1.1",
|
|
18
|
+
"smol-toml": "^1.9.0",
|
|
19
|
+
"yaml": "^2.9.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^26.6.3",
|
|
23
|
+
"tsx": "^4.23.15",
|
|
24
|
+
"typescript": "^7.0.2"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"agent-toggle": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"author": "DevOhMyCode",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/devohmycode/agent-toggle.git"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/devohmycode/agent-toggle#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/devohmycode/agent-toggle/issues"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"cli",
|
|
48
|
+
"mcp",
|
|
49
|
+
"claude-code",
|
|
50
|
+
"codex",
|
|
51
|
+
"gemini-cli",
|
|
52
|
+
"ai-agents",
|
|
53
|
+
"skills",
|
|
54
|
+
"hooks",
|
|
55
|
+
"plugins"
|
|
56
|
+
]
|
|
57
|
+
}
|