llm-switcher 1.2.11 → 1.3.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/CHANGELOG.md +31 -0
- package/README.md +124 -20
- package/README.vi.md +123 -20
- package/config.example.json +13 -1
- package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
- package/docs/response-matrix.json +1130 -1130
- package/formats.mjs +128 -12
- package/hook-status.mjs +5 -4
- package/mcp.mjs +6 -5
- package/package.json +1 -1
- package/plugin.mjs +80 -9
- package/proxy.mjs +211 -20
- package/service.mjs +12 -6
- package/shim.mjs +62 -20
- package/skills/llm-switcher/SKILL.md +93 -93
- package/state.mjs +50 -13
- package/switch +0 -0
- package/switch.cmd +2 -2
- package/switch.mjs +115 -24
- package/tests/agy.e2e.test.mjs +529 -0
- package/tests/agy.unit.test.mjs +190 -0
- package/tests/dashboard.e2e.test.mjs +18 -17
- package/tests/formats.test.mjs +67 -1
- package/tests/gateway.e2e.test.mjs +20 -2
- package/tests/helpers.mjs +24 -24
- package/tests/live-optimizer-interop.mjs +205 -205
- package/tests/mcp.test.mjs +5 -5
- package/tests/plugin.test.mjs +4 -2
- package/tests/service.test.mjs +27 -1
- package/tests/shim.test.mjs +165 -0
- package/tests/state.test.mjs +8 -8
- package/tests/update.e2e.test.mjs +197 -0
- package/tests/update.test.mjs +145 -0
- package/ui.html +121 -8
- package/update.mjs +98 -0
- package/verify-gateway.mjs +35 -0
package/switch.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
suggestedRcFiles, auditRunningProcesses
|
|
21
21
|
} from './shim.mjs';
|
|
22
22
|
import {
|
|
23
|
-
serviceEnv, systemdUnit, launchdPlist, scheduledTaskXml, decodeConsoleText, portFromServiceText, writeServiceFile
|
|
23
|
+
serviceEnv, systemdUnit, launchdPlist, scheduledTaskXml, decodeConsoleText, portFromServiceText, autoupdateFromServiceText, writeServiceFile
|
|
24
24
|
} from './service.mjs';
|
|
25
25
|
|
|
26
26
|
const proxyScript = path.join(ROOT_DIR, 'proxy.mjs');
|
|
@@ -44,7 +44,7 @@ if (!config) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// R5: two commands, one per tool. The openai and vertex commands went with their input routes.
|
|
47
|
-
const TARGET_ALIASES = { claude: 'claude', codex: 'codex' };
|
|
47
|
+
const TARGET_ALIASES = { claude: 'claude', codex: 'codex', agy: 'agy' };
|
|
48
48
|
|
|
49
49
|
// Strip --port/-p and their value from positional args. -p is the global option everywhere, as in
|
|
50
50
|
// resolvePort; `switch port <n>` is the command that changes the port.
|
|
@@ -272,6 +272,9 @@ async function changePort(newPortStr) {
|
|
|
272
272
|
const target = await probeGateway(p);
|
|
273
273
|
if (isHeld(target)) refuseForeignPort(p, 'this switcher', target);
|
|
274
274
|
const svc = installedService();
|
|
275
|
+
// The unit is rewritten below. It keeps the choice that `service install --no-autoupdate` made.
|
|
276
|
+
const svcText = svc ? serviceText(svc) : null;
|
|
277
|
+
const autoupdate = svcText === null || autoupdateFromServiceText(svcText);
|
|
275
278
|
const wasRunning = await checkProxyRunning(oldPort);
|
|
276
279
|
// Stop the old gateway even when a service is installed: it may be a copy started outside the unit.
|
|
277
280
|
if (wasRunning) {
|
|
@@ -301,7 +304,7 @@ async function changePort(newPortStr) {
|
|
|
301
304
|
} catch (err) {
|
|
302
305
|
console.error(`[Error] Failed to restore config.json during rollback: ${err.message}`);
|
|
303
306
|
}
|
|
304
|
-
if (svc) installService(oldPort);
|
|
307
|
+
if (svc) installService(oldPort, { autoupdate });
|
|
305
308
|
else if (wasRunning) startProxyBackground(oldPort);
|
|
306
309
|
let up = !svc && !wasRunning;
|
|
307
310
|
for (let i = 0; i < 20 && !up; i++) { await sleep(250); up = await checkProxyRunning(oldPort); }
|
|
@@ -311,7 +314,7 @@ async function changePort(newPortStr) {
|
|
|
311
314
|
if (svc) {
|
|
312
315
|
// The unit fixes the port on its command line, so it is rewritten and restarted, not fought.
|
|
313
316
|
console.log(`Reinstalling the ${svc} service on port ${p}...`);
|
|
314
|
-
if (!installService(p)) await rollBack(`The ${svc} service could not be installed for port ${p}.`);
|
|
317
|
+
if (!installService(p, { autoupdate })) await rollBack(`The ${svc} service could not be installed for port ${p}.`);
|
|
315
318
|
let up = false;
|
|
316
319
|
for (let i = 0; i < 20 && !up; i++) { await sleep(250); up = await checkProxyRunning(p); }
|
|
317
320
|
if (!up) await rollBack(`The ${svc} service did not come up on port ${p}. See ${proxyLogPath}.`);
|
|
@@ -334,7 +337,7 @@ async function changePort(newPortStr) {
|
|
|
334
337
|
}
|
|
335
338
|
|
|
336
339
|
function printProfile(profile) {
|
|
337
|
-
console.log(`Input Target: ${show(profile.inFormat || 'auto').toUpperCase()}`);
|
|
340
|
+
console.log(`Input Target: ${show(profile.tool || profile.inFormat || 'auto').toUpperCase()}`);
|
|
338
341
|
console.log(`Routing: ${profile.outFormat ? `out=${show(profile.outFormat)}` : `mode=${show(profile.mode || 'hybrid')}`}`);
|
|
339
342
|
console.log(`Upstream: ${show(profile.baseURL || '(not set)')}`);
|
|
340
343
|
for (const slot of modelSlotsForProfile(profile)) {
|
|
@@ -344,7 +347,7 @@ function printProfile(profile) {
|
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
function printTargets(activeMap) {
|
|
347
|
-
const labels = { claude: 'Claude Code', codex: 'Codex' };
|
|
350
|
+
const labels = { claude: 'Claude Code', codex: 'Codex', agy: 'Antigravity' };
|
|
348
351
|
for (const t of TOOLS) {
|
|
349
352
|
console.log(` ${labels[t].padEnd(12)} (${t.padEnd(11)}) -> ${activeMap[t] ? show(activeMap[t]) : 'OFF (official)'}`);
|
|
350
353
|
}
|
|
@@ -526,7 +529,7 @@ async function turnOff(targetArg) {
|
|
|
526
529
|
if (targetArg) {
|
|
527
530
|
const target = TARGET_ALIASES[targetArg.toLowerCase()];
|
|
528
531
|
if (!target) {
|
|
529
|
-
console.error(`[Error] Unknown target "${targetArg}". Use one of: claude, codex`);
|
|
532
|
+
console.error(`[Error] Unknown target "${targetArg}". Use one of: claude, codex, agy`);
|
|
530
533
|
process.exit(1);
|
|
531
534
|
}
|
|
532
535
|
if (refused) {
|
|
@@ -561,7 +564,7 @@ async function turnOff(targetArg) {
|
|
|
561
564
|
console.log('Deactivating Proxy and restoring official endpoints...');
|
|
562
565
|
if (refused) {
|
|
563
566
|
// Both pointers through the same writer. Nothing below runs until those bytes are on disk (R7b).
|
|
564
|
-
casOff(
|
|
567
|
+
casOff(TOOLS);
|
|
565
568
|
reportSettings(clearLaunchState(port));
|
|
566
569
|
const result = await stopProxy(port);
|
|
567
570
|
reportStopped(result, port);
|
|
@@ -588,7 +591,7 @@ async function turnOff(targetArg) {
|
|
|
588
591
|
// Only a CLI whose target is active is expected to go through the gateway.
|
|
589
592
|
function auditActiveClis() {
|
|
590
593
|
const map = getActiveMap(config);
|
|
591
|
-
return auditRunningProcesses(
|
|
594
|
+
return auditRunningProcesses(TOOLS.filter(t => map[t]));
|
|
592
595
|
}
|
|
593
596
|
|
|
594
597
|
// settings.json edits are never silent: name every value the switcher removed.
|
|
@@ -669,16 +672,22 @@ function serviceStart(kind) {
|
|
|
669
672
|
} catch {}
|
|
670
673
|
}
|
|
671
674
|
|
|
672
|
-
/** The
|
|
673
|
-
function
|
|
675
|
+
/** The installed service definition, or null when it cannot be read. */
|
|
676
|
+
function serviceText(kind) {
|
|
674
677
|
try {
|
|
675
|
-
if (kind === 'systemd') return
|
|
676
|
-
if (kind === 'launchd') return
|
|
677
|
-
if (kind === 'schtasks') return
|
|
678
|
+
if (kind === 'systemd') return fs.readFileSync(SYSTEMD_UNIT, 'utf8');
|
|
679
|
+
if (kind === 'launchd') return fs.readFileSync(LAUNCHD_PLIST, 'utf8');
|
|
680
|
+
if (kind === 'schtasks') return decodeConsoleText(execFileSync('schtasks', ['/Query', '/TN', 'LLMSwitcher', '/XML']));
|
|
678
681
|
} catch {}
|
|
679
682
|
return null;
|
|
680
683
|
}
|
|
681
684
|
|
|
685
|
+
/** The port on the service's command line, or null when it cannot be read. */
|
|
686
|
+
function servicePort(kind) {
|
|
687
|
+
const text = serviceText(kind);
|
|
688
|
+
return text === null ? null : portFromServiceText(text);
|
|
689
|
+
}
|
|
690
|
+
|
|
682
691
|
function reportBackup(backup) {
|
|
683
692
|
if (backup) console.log(`[Service] The previous definition differed and is kept as ${backup}.`);
|
|
684
693
|
}
|
|
@@ -693,7 +702,7 @@ function serviceStop(kind) {
|
|
|
693
702
|
}
|
|
694
703
|
|
|
695
704
|
// Writes the service for `port` and (re)starts it, so a running unit picks up the new command line.
|
|
696
|
-
function installService(port) {
|
|
705
|
+
function installService(port, { autoupdate = true } = {}) {
|
|
697
706
|
const nodeBin = process.execPath;
|
|
698
707
|
const env = serviceEnv();
|
|
699
708
|
if (process.platform === 'win32') {
|
|
@@ -702,12 +711,12 @@ function installService(port) {
|
|
|
702
711
|
const userId = process.env.USERDOMAIN && process.env.USERNAME ? `${process.env.USERDOMAIN}\\${process.env.USERNAME}` : os.userInfo().username;
|
|
703
712
|
const xmlPath = path.join(dir, 'task.xml');
|
|
704
713
|
// Task Scheduler reads the XML as UTF-16, the encoding it declares.
|
|
705
|
-
fs.writeFileSync(xmlPath, Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(scheduledTaskXml({ nodeBin, script: proxyScript, port, userId }), 'utf16le')]));
|
|
714
|
+
fs.writeFileSync(xmlPath, Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(scheduledTaskXml({ nodeBin, script: proxyScript, port, userId, autoupdate }), 'utf16le')]));
|
|
706
715
|
execFileSync('schtasks', ['/Create', '/TN', 'LLMSwitcher', '/XML', xmlPath, '/F'], { stdio: 'inherit' });
|
|
707
716
|
try { execFileSync('schtasks', ['/End', '/TN', 'LLMSwitcher'], { stdio: 'ignore' }); } catch {}
|
|
708
717
|
execFileSync('schtasks', ['/Run', '/TN', 'LLMSwitcher'], { stdio: 'ignore' });
|
|
709
718
|
if (env.length) console.log(`[WARN] The scheduled task does not receive ${env.map(([k]) => k).join(', ')}. Set them as User environment variables.`);
|
|
710
|
-
console.log(
|
|
719
|
+
console.log(`[SUCCESS] Installed and started Windows Scheduled Task "LLMSwitcher" (${autoupdate ? 'auto-updates on logon and starts' : 'auto-starts on logon'}).`);
|
|
711
720
|
return true;
|
|
712
721
|
} catch (err) {
|
|
713
722
|
console.error('[Error] Failed to register the scheduled task:', err.message);
|
|
@@ -718,7 +727,7 @@ function installService(port) {
|
|
|
718
727
|
}
|
|
719
728
|
if (process.platform === 'darwin') {
|
|
720
729
|
try {
|
|
721
|
-
reportBackup(writeServiceFile(LAUNCHD_PLIST, launchdPlist({ nodeBin, script: proxyScript, port, logPath: proxyLogPath, env })));
|
|
730
|
+
reportBackup(writeServiceFile(LAUNCHD_PLIST, launchdPlist({ nodeBin, script: proxyScript, port, logPath: proxyLogPath, env, autoupdate })));
|
|
722
731
|
try { execFileSync('launchctl', ['unload', LAUNCHD_PLIST], { stdio: 'ignore' }); } catch {}
|
|
723
732
|
execFileSync('launchctl', ['load', LAUNCHD_PLIST], { stdio: 'inherit' });
|
|
724
733
|
console.log('[SUCCESS] Installed and started macOS launchd service.');
|
|
@@ -729,7 +738,7 @@ function installService(port) {
|
|
|
729
738
|
}
|
|
730
739
|
}
|
|
731
740
|
try {
|
|
732
|
-
reportBackup(writeServiceFile(SYSTEMD_UNIT, systemdUnit({ nodeBin, script: proxyScript, port, env })));
|
|
741
|
+
reportBackup(writeServiceFile(SYSTEMD_UNIT, systemdUnit({ nodeBin, script: proxyScript, port, env, autoupdate })));
|
|
733
742
|
systemctlUser(['daemon-reload'], { stdio: 'inherit' });
|
|
734
743
|
systemctlUser(['enable', 'llm-switcher'], { stdio: 'inherit' });
|
|
735
744
|
systemctlUser(['restart', 'llm-switcher'], { stdio: 'inherit' });
|
|
@@ -745,7 +754,8 @@ async function manageService(action) {
|
|
|
745
754
|
const port = getTargetPort();
|
|
746
755
|
|
|
747
756
|
if (action === 'install') {
|
|
748
|
-
|
|
757
|
+
const autoupdate = !process.argv.includes('--no-autoupdate');
|
|
758
|
+
if (!installService(port, { autoupdate })) process.exit(1);
|
|
749
759
|
return;
|
|
750
760
|
}
|
|
751
761
|
|
|
@@ -789,13 +799,15 @@ async function managePlugin(action = 'status') {
|
|
|
789
799
|
|
|
790
800
|
if (act === 'install' || act === 'on') {
|
|
791
801
|
const { CURRENT_VERSION } = await import('./version.mjs');
|
|
792
|
-
const { installed, failed, paths } = installPlugin({}, CURRENT_VERSION);
|
|
802
|
+
const { installed, skipped, failed, paths } = installPlugin({}, CURRENT_VERSION);
|
|
793
803
|
for (const tool of installed) console.log(`[OK] ${tool}: ${paths[tool]}`);
|
|
804
|
+
for (const k of skipped) console.log(`[Skip] ${k.tool}: ${k.reason}`);
|
|
794
805
|
for (const f of failed) console.error(`[Error] ${f.tool}: ${f.reason}`);
|
|
795
806
|
if (installed.length) {
|
|
796
807
|
console.log('\nNeither settings.json nor config.toml was opened. Each tool reads a file of its own.');
|
|
797
808
|
console.log(`Claude Code loads it on the next session as ${PLUGIN_NAME}@skills-dir.`);
|
|
798
809
|
console.log('Codex asks you once to trust a new hook. Accept it, or the hook stays off.');
|
|
810
|
+
if (installed.includes('agy')) console.log(`agy: ${pluginStatus().agy.note}.`);
|
|
799
811
|
console.log('\nTo remove it: switch plugin uninstall');
|
|
800
812
|
}
|
|
801
813
|
if (failed.length) process.exit(1);
|
|
@@ -816,7 +828,10 @@ async function managePlugin(action = 'status') {
|
|
|
816
828
|
console.log(` ${st.claude.path}`);
|
|
817
829
|
console.log(`Codex : ${st.codex.installed ? 'INSTALLED' : 'not installed'}`);
|
|
818
830
|
console.log(` ${st.codex.path}`);
|
|
819
|
-
|
|
831
|
+
console.log(`Antigravity: ${st.agy.installed ? 'INSTALLED' : 'not installed'}`);
|
|
832
|
+
console.log(` ${st.agy.path}${st.agy.error ? ` (${st.agy.error})` : ''}`);
|
|
833
|
+
console.log(` ${st.agy.note}`);
|
|
834
|
+
if (!st.claude.installed || !st.codex.installed || !st.agy.installed) {
|
|
820
835
|
console.log(`\nThe notice is optional. Without it the gateway still routes every request,`);
|
|
821
836
|
console.log(`and the shim still raises its own notice at launch.`);
|
|
822
837
|
console.log(`To add it: switch plugin install`);
|
|
@@ -1106,6 +1121,78 @@ async function showVersion() {
|
|
|
1106
1121
|
else console.log('This is the latest version.');
|
|
1107
1122
|
}
|
|
1108
1123
|
|
|
1124
|
+
async function gatewayPid(port) {
|
|
1125
|
+
try {
|
|
1126
|
+
const r = await fetch(`http://127.0.0.1:${port}/health?challenge=update`, { signal: AbortSignal.timeout(2000) });
|
|
1127
|
+
return (await r.json()).pid ?? null;
|
|
1128
|
+
} catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// A running gateway updates itself and hands over to the new code, so the CLI and the dashboard
|
|
1134
|
+
// share one update path. With no gateway up, the files are updated here and the next start runs them.
|
|
1135
|
+
async function runUpdate() {
|
|
1136
|
+
const port = getTargetPort();
|
|
1137
|
+
if (!(await checkProxyRunning(port))) {
|
|
1138
|
+
const { applyUpdate } = await import('./update.mjs');
|
|
1139
|
+
try {
|
|
1140
|
+
const r = await applyUpdate({ logger: (text) => console.log(` ${text}`) });
|
|
1141
|
+
console.log(r.updated ? `[SUCCESS] Updated v${r.from} -> v${r.to}.` : r.reason);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.error(`[Error] ${err.message}`);
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const oldPid = await gatewayPid(port);
|
|
1149
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/update`, {
|
|
1150
|
+
method: 'POST',
|
|
1151
|
+
headers: { 'Content-Type': 'application/json', 'x-llm-switcher-token': readAdminToken() || '' },
|
|
1152
|
+
body: '{}'
|
|
1153
|
+
});
|
|
1154
|
+
if (!res.ok) {
|
|
1155
|
+
console.error(`[Error] ${(await res.json().catch(() => ({}))).error || `HTTP ${res.status}`}`);
|
|
1156
|
+
process.exit(1);
|
|
1157
|
+
}
|
|
1158
|
+
let result = null;
|
|
1159
|
+
let failed = null;
|
|
1160
|
+
let buf = '';
|
|
1161
|
+
const decoder = new TextDecoder();
|
|
1162
|
+
for await (const chunk of res.body) {
|
|
1163
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
1164
|
+
let i;
|
|
1165
|
+
while ((i = buf.indexOf('\n\n')) !== -1) {
|
|
1166
|
+
const block = buf.slice(0, i);
|
|
1167
|
+
buf = buf.slice(i + 2);
|
|
1168
|
+
const event = /^event: (.*)$/m.exec(block)?.[1];
|
|
1169
|
+
const data = JSON.parse(/^data: (.*)$/m.exec(block)?.[1] || 'null');
|
|
1170
|
+
if (event === 'log') console.log(` ${show(data.text)}`);
|
|
1171
|
+
else if (event === 'result') result = data;
|
|
1172
|
+
else if (event === 'error') failed = data.message;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (failed || !result) {
|
|
1176
|
+
console.error(`[Error] ${show(failed || 'The gateway closed the update stream without a result.')}`);
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
if (!result.updated) {
|
|
1180
|
+
console.log(show(result.reason));
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
console.log(`Updated v${show(result.from)} -> v${show(result.to)}. Waiting for the new gateway...`);
|
|
1184
|
+
for (let i = 0; i < 60; i++) {
|
|
1185
|
+
await sleep(250);
|
|
1186
|
+
const pid = await gatewayPid(port);
|
|
1187
|
+
if (pid && pid !== oldPid && (await checkProxyRunning(port))) {
|
|
1188
|
+
console.log(`[SUCCESS] The gateway on port ${port} runs v${show(result.to)}.`);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
console.error(`[Error] The new gateway did not answer on port ${port}. See ${proxyLogPath}.`);
|
|
1193
|
+
process.exit(1);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1109
1196
|
async function showModels(target) {
|
|
1110
1197
|
const { syncLocalCatalog, refreshCatalog } = await import('./catalog.mjs');
|
|
1111
1198
|
if (optionValue('--refresh') || process.argv.includes('--refresh')) {
|
|
@@ -1169,6 +1256,8 @@ if (cmd === 'off' || cmd === 'stop') {
|
|
|
1169
1256
|
await showModels(subArg);
|
|
1170
1257
|
} else if (cmd === 'status' || cmd === 'st') {
|
|
1171
1258
|
await showStatus();
|
|
1259
|
+
} else if (cmd === 'update' || cmd === 'upgrade') {
|
|
1260
|
+
await runUpdate();
|
|
1172
1261
|
} else if (cmd === 'on' || cmd === 'start') {
|
|
1173
1262
|
await turnOn(subArg);
|
|
1174
1263
|
} else if (cmd && findProfileKey(config, rawCmd)) {
|
|
@@ -1179,17 +1268,19 @@ if (cmd === 'off' || cmd === 'stop') {
|
|
|
1179
1268
|
console.log(' switch status # Show multi-CLI active status');
|
|
1180
1269
|
console.log(' switch version # Show the version and check for a newer one');
|
|
1181
1270
|
console.log(' switch doctor # Audit environment, settings & routing');
|
|
1271
|
+
console.log(' switch update # Install the newest release and restart the gateway');
|
|
1182
1272
|
console.log(' switch on [profile] # Start gateway & activate profile for all compatible targets');
|
|
1183
1273
|
console.log(' switch <profile> # Activate profile for all compatible targets');
|
|
1184
1274
|
console.log(' switch claude <profile> # Set active profile for Claude Code');
|
|
1185
1275
|
console.log(' switch codex <profile> # Set active profile for Codex');
|
|
1276
|
+
console.log(' switch agy <profile> # Set active profile for the Antigravity CLI (agy)');
|
|
1186
1277
|
console.log(' switch port <number> # Change gateway port');
|
|
1187
|
-
console.log(' switch service install # Install OS background autostart service');
|
|
1278
|
+
console.log(' switch service install # Install OS background autostart service (auto-updates on logon)');
|
|
1188
1279
|
console.log(' switch service uninstall # Uninstall background autostart service');
|
|
1189
1280
|
console.log(' switch shim install # Auto-inject env into resumed sessions (claude --resume)');
|
|
1190
1281
|
console.log(' switch shim status # Check shims + detect sessions bypassing the gateway');
|
|
1191
1282
|
console.log(' switch shim uninstall # Remove launcher shims');
|
|
1192
|
-
console.log(' switch plugin install # Optional launch notice inside Claude Code &
|
|
1283
|
+
console.log(' switch plugin install # Optional launch notice inside Claude Code, Codex & agy');
|
|
1193
1284
|
console.log(' switch plugin status # Is that notice installed');
|
|
1194
1285
|
console.log(' switch plugin uninstall # Remove that notice');
|
|
1195
1286
|
console.log(' switch off [target] # Restore official endpoints (all, or one target)');
|