clisponsor 1.0.11 → 1.0.16

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CLIsponsor Hook
2
2
 
3
- Hook package codebase for the `clisponsor` installer and Codex, Claude Code, and Gemini hook templates.
3
+ Hook package codebase for the `clisponsor` installer and Codex, Claude Code, Gemini, Antigravity, OpenCode, Pi, GitHub Copilot CLI, and Qwen Code hook templates.
4
4
 
5
5
  This codebase owns local installation, device login, diagnostics, and hook adapters. It must not contain public website, dashboard app, API account, or ad-serving server code.
6
6
 
@@ -16,6 +16,30 @@ const DEFAULT_BACKEND_BASE_URL = process.env.CLISPONSOR_BACKEND_BASE_URL || "htt
16
16
  const HOOK_VERSION = "1.0.0";
17
17
  const NETWORK_TIMEOUT_MS = 3000;
18
18
  const ANTIGRAVITY_EVENTS = ["PreInvocation"];
19
+ const OPENCODE_NON_TUI_COMMANDS = new Set([
20
+ "completion",
21
+ "acp",
22
+ "mcp",
23
+ "run",
24
+ "debug",
25
+ "providers",
26
+ "auth",
27
+ "agent",
28
+ "upgrade",
29
+ "uninstall",
30
+ "serve",
31
+ "web",
32
+ "models",
33
+ "stats",
34
+ "export",
35
+ "import",
36
+ "github",
37
+ "pr",
38
+ "session",
39
+ "plugin",
40
+ "plug",
41
+ "db",
42
+ ]);
19
43
 
20
44
  function argValue(name) {
21
45
  const prefix = `${name}=`;
@@ -130,6 +154,85 @@ function chmodExecutable(file) {
130
154
  } catch {}
131
155
  }
132
156
 
157
+ function xdgConfigHome() {
158
+ return process.env.XDG_CONFIG_HOME || path.join(HOME, ".config");
159
+ }
160
+
161
+ function openCodeConfigDir() {
162
+ return process.env.OPENCODE_CONFIG_DIR || path.join(xdgConfigHome(), "opencode");
163
+ }
164
+
165
+ function copilotHome() {
166
+ return process.env.COPILOT_HOME || path.join(HOME, ".copilot");
167
+ }
168
+
169
+ function qwenHome() {
170
+ return process.env.QWEN_HOME || path.join(HOME, ".qwen");
171
+ }
172
+
173
+ function colorEnabled() {
174
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
175
+ }
176
+
177
+ function color(value, code) {
178
+ if (!colorEnabled()) return value;
179
+ return `\u001b[${code}m${value}\u001b[0m`;
180
+ }
181
+
182
+ function green(value) {
183
+ return color(value, "32");
184
+ }
185
+
186
+ function red(value) {
187
+ return color(value, "31");
188
+ }
189
+
190
+ function cyan(value) {
191
+ return color(value, "36");
192
+ }
193
+
194
+ function dim(value) {
195
+ return color(value, "2");
196
+ }
197
+
198
+ function bold(value) {
199
+ return color(value, "1");
200
+ }
201
+
202
+ function targetSuffix(spec) {
203
+ return dim(`(${spec.command})`);
204
+ }
205
+
206
+ function installLine(symbol, spec, message, colorFn = (value) => value) {
207
+ console.log(`${colorFn(symbol)} ${spec.label} ${targetSuffix(spec)} - ${message}`);
208
+ }
209
+
210
+ function startInstallSpinner(spec) {
211
+ if (!process.stdout.isTTY) return () => {};
212
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
213
+ let index = 0;
214
+ const render = () => {
215
+ process.stdout.write(`\r${cyan(frames[index % frames.length])} ${spec.label} ${targetSuffix(spec)} - installing...`);
216
+ index += 1;
217
+ };
218
+ render();
219
+ const timer = setInterval(render, 80);
220
+ return () => {
221
+ clearInterval(timer);
222
+ process.stdout.write("\r\u001b[2K");
223
+ };
224
+ }
225
+
226
+ function withoutInstallLogs(action) {
227
+ const originalLog = console.log;
228
+ console.log = () => {};
229
+ try {
230
+ return action();
231
+ } finally {
232
+ console.log = originalLog;
233
+ }
234
+ }
235
+
133
236
  function commandExists(command) {
134
237
  const paths = String(process.env.PATH || "").split(path.delimiter).filter(Boolean);
135
238
  const extensions = process.platform === "win32" ? String(process.env.PATHEXT || ".EXE;.CMD;.BAT").split(";") : [""];
@@ -162,6 +265,10 @@ function isClisponsorCommand(value) {
162
265
  (value.includes("clisponsor_claude_hook.mjs") ||
163
266
  value.includes("clisponsor_gemini_hook.mjs") ||
164
267
  value.includes("clisponsor_antigravity_hook.mjs") ||
268
+ value.includes("clisponsor_opencode_plugin") ||
269
+ value.includes("clisponsor_pi_extension") ||
270
+ value.includes("clisponsor_copilot_hook.mjs") ||
271
+ value.includes("clisponsor_qwen_hook.mjs") ||
165
272
  value.includes(`${path.sep}.clisponsor${path.sep}`) ||
166
273
  value.includes("/.clisponsor/") ||
167
274
  value.includes("\\.clisponsor\\"))
@@ -235,6 +342,28 @@ function addGeminiCommandHook(settings, eventName, matcher, command) {
235
342
  return true;
236
343
  }
237
344
 
345
+ function addQwenCommandHook(settings, eventName, command) {
346
+ if (!isPlainObject(settings.hooks)) settings.hooks = {};
347
+ const current = Array.isArray(settings.hooks[eventName]) ? settings.hooks[eventName] : [];
348
+ const exists = current.some((entry) => isClisponsorHookEntry(entry));
349
+ if (exists) return false;
350
+
351
+ settings.hooks[eventName] = [
352
+ ...current,
353
+ {
354
+ hooks: [
355
+ {
356
+ type: "command",
357
+ command,
358
+ name: "clisponsor",
359
+ timeout: 5000,
360
+ },
361
+ ],
362
+ },
363
+ ];
364
+ return true;
365
+ }
366
+
238
367
  function removeClaudeCommandHooks(settings) {
239
368
  if (!isPlainObject(settings.hooks)) return false;
240
369
  let changed = false;
@@ -481,6 +610,270 @@ try {
481
610
  `;
482
611
  }
483
612
 
613
+ function openCodePluginSource() {
614
+ return `import fs from "node:fs";
615
+ import crypto from "node:crypto";
616
+
617
+ const CONFIG_PATH = ${JSON.stringify(CONFIG_PATH)};
618
+ const HOOK_VERSION = ${JSON.stringify(HOOK_VERSION)};
619
+ const NON_TUI_COMMANDS = new Set(${JSON.stringify([...OPENCODE_NON_TUI_COMMANDS])});
620
+
621
+ function readConfig() {
622
+ try {
623
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
624
+ } catch {
625
+ return {};
626
+ }
627
+ }
628
+
629
+ function likelyTuiInvocation() {
630
+ const args = process.argv.slice(2).filter((arg) => !arg.startsWith("-"));
631
+ if (args.length === 0) return true;
632
+ return !NON_TUI_COMMANDS.has(args[0]);
633
+ }
634
+
635
+ function sponsoredLine(line) {
636
+ return "[Sponsored] " + line;
637
+ }
638
+
639
+ export const CLIsponsorOpenCodePlugin = async ({ client }) => {
640
+ if (!likelyTuiInvocation()) return {};
641
+ let startSessionRequested = false;
642
+
643
+ async function serve(event, placement, metadata = {}) {
644
+ if (placement === "StartSession") {
645
+ if (startSessionRequested) return;
646
+ startSessionRequested = true;
647
+ }
648
+ const cfg = readConfig();
649
+ const serveBaseUrl = cfg.serveBaseUrl || cfg.apiBaseUrl;
650
+ if (!serveBaseUrl || !cfg.userId || !cfg.deviceCode || !cfg.deviceSecret) return;
651
+
652
+ try {
653
+ const res = await fetch(serveBaseUrl + "/v1/ads/serve", {
654
+ method: "POST",
655
+ headers: {
656
+ "content-type": "application/json",
657
+ "authorization": "Bearer " + cfg.deviceSecret,
658
+ "x-clisponsor-hook-version": HOOK_VERSION,
659
+ },
660
+ body: JSON.stringify({
661
+ user_id: cfg.userId,
662
+ device_code: cfg.deviceCode,
663
+ client: "OpenCode",
664
+ hook_event: event,
665
+ placement,
666
+ idempotency_key: crypto.randomUUID(),
667
+ metadata: {
668
+ hookVersion: HOOK_VERSION,
669
+ openCode: metadata,
670
+ },
671
+ }),
672
+ });
673
+ if (!res.ok) return;
674
+ const ad = await res.json();
675
+ if (!ad.display_line) return;
676
+ await client.tui.showToast({
677
+ body: {
678
+ title: "CLIsponsor " + placement,
679
+ message: sponsoredLine(ad.display_line),
680
+ variant: "info",
681
+ duration: 10000,
682
+ },
683
+ });
684
+ } catch {}
685
+ }
686
+
687
+ const startupTimer = setTimeout(() => {
688
+ void serve("plugin.start", "StartSession");
689
+ }, 2500);
690
+ startupTimer.unref?.();
691
+
692
+ return {
693
+ event: async ({ event }) => {
694
+ if (event.type === "session.created") {
695
+ await serve("session.created", "StartSession", {
696
+ sessionID: event.properties?.sessionID,
697
+ });
698
+ } else if (event.type === "session.idle") {
699
+ await serve("session.idle", "EndTurn", {
700
+ sessionID: event.properties?.sessionID,
701
+ });
702
+ }
703
+ },
704
+ "chat.message": async (input) => {
705
+ await serve("chat.message", "StartTurn", {
706
+ sessionID: input.sessionID,
707
+ agent: input.agent,
708
+ });
709
+ },
710
+ };
711
+ };
712
+ `;
713
+ }
714
+
715
+ function piExtensionSource() {
716
+ return `import fs from "node:fs";
717
+ import crypto from "node:crypto";
718
+
719
+ const CONFIG_PATH = ${JSON.stringify(CONFIG_PATH)};
720
+ const HOOK_VERSION = ${JSON.stringify(HOOK_VERSION)};
721
+
722
+ function readConfig() {
723
+ try {
724
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
725
+ } catch {
726
+ return {};
727
+ }
728
+ }
729
+
730
+ function sponsoredLine(line) {
731
+ return "[Sponsored] " + line;
732
+ }
733
+
734
+ function canShow(ctx) {
735
+ return Boolean(ctx?.hasUI && ctx?.ui?.notify);
736
+ }
737
+
738
+ export default function CLIsponsorPiExtension(pi) {
739
+ async function serve(event, placement, ctx, metadata = {}) {
740
+ if (!canShow(ctx)) return;
741
+ const cfg = readConfig();
742
+ const serveBaseUrl = cfg.serveBaseUrl || cfg.apiBaseUrl;
743
+ if (!serveBaseUrl || !cfg.userId || !cfg.deviceCode || !cfg.deviceSecret) return;
744
+
745
+ try {
746
+ const res = await fetch(serveBaseUrl + "/v1/ads/serve", {
747
+ method: "POST",
748
+ headers: {
749
+ "content-type": "application/json",
750
+ "authorization": "Bearer " + cfg.deviceSecret,
751
+ "x-clisponsor-hook-version": HOOK_VERSION,
752
+ },
753
+ body: JSON.stringify({
754
+ user_id: cfg.userId,
755
+ device_code: cfg.deviceCode,
756
+ client: "Pi",
757
+ hook_event: event,
758
+ placement,
759
+ idempotency_key: crypto.randomUUID(),
760
+ metadata: {
761
+ hookVersion: HOOK_VERSION,
762
+ pi: metadata,
763
+ },
764
+ }),
765
+ });
766
+ if (!res.ok) return;
767
+ const ad = await res.json();
768
+ if (!ad.display_line) return;
769
+ ctx.ui.notify("CLIsponsor " + placement + "\\n" + sponsoredLine(ad.display_line), "info");
770
+ } catch {}
771
+ }
772
+
773
+ pi.on("session_start", async (event, ctx) => {
774
+ await serve("session_start", "StartSession", ctx, {
775
+ reason: event?.reason,
776
+ });
777
+ });
778
+
779
+ pi.on("agent_start", async (_event, ctx) => {
780
+ await serve("agent_start", "StartTurn", ctx);
781
+ });
782
+
783
+ pi.on("agent_end", async (_event, ctx) => {
784
+ await serve("agent_end", "EndTurn", ctx);
785
+ });
786
+ }
787
+ `;
788
+ }
789
+
790
+ function copilotHookSource() {
791
+ return `#!/usr/bin/env node
792
+ import fs from "node:fs";
793
+ import crypto from "node:crypto";
794
+
795
+ const event = process.argv[2] || "userPromptSubmitted";
796
+ const CONFIG_PATH = ${JSON.stringify(CONFIG_PATH)};
797
+ const HOOK_VERSION = ${JSON.stringify(HOOK_VERSION)};
798
+ const placements = {
799
+ sessionStart: "StartSession",
800
+ userPromptSubmitted: "StartTurn",
801
+ agentStop: "EndTurn",
802
+ };
803
+
804
+ function sponsoredLine(line) {
805
+ return "[Sponsored] " + line;
806
+ }
807
+
808
+ function readStdin() {
809
+ return new Promise((resolve) => {
810
+ let data = "";
811
+ process.stdin.on("data", (chunk) => (data += chunk));
812
+ process.stdin.on("end", () => resolve(data));
813
+ });
814
+ }
815
+
816
+ function readConfig() {
817
+ try {
818
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
819
+ } catch {
820
+ return {};
821
+ }
822
+ }
823
+
824
+ function progress(message) {
825
+ console.log(JSON.stringify({ type: "progress", message }));
826
+ }
827
+
828
+ const inputRaw = await readStdin();
829
+ let input = {};
830
+ try {
831
+ input = inputRaw.trim() ? JSON.parse(inputRaw) : {};
832
+ } catch {}
833
+
834
+ try {
835
+ const placement = placements[event];
836
+ if (!placement) process.exit(0);
837
+ const cfg = readConfig();
838
+ const serveBaseUrl = cfg.serveBaseUrl || cfg.apiBaseUrl;
839
+ if (!serveBaseUrl || !cfg.userId || !cfg.deviceCode || !cfg.deviceSecret) process.exit(0);
840
+
841
+ const res = await fetch(serveBaseUrl + "/v1/ads/serve", {
842
+ method: "POST",
843
+ headers: {
844
+ "content-type": "application/json",
845
+ "authorization": "Bearer " + cfg.deviceSecret,
846
+ "x-clisponsor-hook-version": HOOK_VERSION,
847
+ },
848
+ body: JSON.stringify({
849
+ user_id: cfg.userId,
850
+ device_code: cfg.deviceCode,
851
+ client: "GitHubCopilot",
852
+ hook_event: event,
853
+ placement,
854
+ idempotency_key: crypto.randomUUID(),
855
+ metadata: {
856
+ hookVersion: HOOK_VERSION,
857
+ copilot: {
858
+ sessionId: input.sessionId || input.session_id,
859
+ source: input.source,
860
+ reason: input.reason,
861
+ stopReason: input.stopReason || input.stop_reason,
862
+ },
863
+ },
864
+ }),
865
+ });
866
+ if (!res.ok) process.exit(0);
867
+ const ad = await res.json();
868
+ if (!ad.display_line) process.exit(0);
869
+ progress("CLIsponsor " + placement + ": " + sponsoredLine(ad.display_line));
870
+ console.log(JSON.stringify({}));
871
+ } catch {
872
+ process.exit(0);
873
+ }
874
+ `;
875
+ }
876
+
484
877
  function installGemini() {
485
878
  if (!commandExists("gemini")) {
486
879
  console.log("Gemini CLI not found. To enable CLIsponsor for Gemini, install Gemini CLI and rerun: npx clisponsor@latest install");
@@ -521,24 +914,180 @@ function installAntigravity() {
521
914
  console.log("Antigravity CLI hook installed.");
522
915
  }
523
916
 
917
+ function installOpenCode() {
918
+ if (!commandExists("opencode")) {
919
+ console.log("OpenCode CLI not found. To enable CLIsponsor for OpenCode, install OpenCode CLI and rerun: npx clisponsor@latest install");
920
+ return;
921
+ }
922
+
923
+ const opencodeDir = path.join(CONFIG_DIR, "opencode");
924
+ fs.mkdirSync(opencodeDir, { recursive: true });
925
+ const stagedPlugin = path.join(opencodeDir, "clisponsor_opencode_plugin.js");
926
+ fs.writeFileSync(stagedPlugin, openCodePluginSource(), { mode: 0o644 });
927
+
928
+ const pluginsDir = path.join(openCodeConfigDir(), "plugins");
929
+ fs.mkdirSync(pluginsDir, { recursive: true });
930
+ const installedPlugin = path.join(pluginsDir, "clisponsor_opencode_plugin.js");
931
+ fs.copyFileSync(stagedPlugin, installedPlugin);
932
+ console.log(`Updated ${installedPlugin}`);
933
+ console.log("OpenCode CLI plugin installed.");
934
+ }
935
+
936
+ function installPi() {
937
+ if (!commandExists("pi")) {
938
+ console.log("Pi CLI not found. To enable CLIsponsor for Pi, install Pi Coding Agent and rerun: npx clisponsor@latest install");
939
+ return;
940
+ }
941
+
942
+ const piDir = path.join(CONFIG_DIR, "pi");
943
+ fs.mkdirSync(piDir, { recursive: true });
944
+ const stagedExtension = path.join(piDir, "clisponsor_pi_extension.ts");
945
+ fs.writeFileSync(stagedExtension, piExtensionSource(), { mode: 0o644 });
946
+
947
+ const extensionsDir = path.join(HOME, ".pi", "agent", "extensions");
948
+ fs.mkdirSync(extensionsDir, { recursive: true });
949
+ const installedExtension = path.join(extensionsDir, "clisponsor_pi_extension.ts");
950
+ fs.copyFileSync(stagedExtension, installedExtension);
951
+ console.log(`Updated ${installedExtension}`);
952
+ console.log("Pi CLI extension installed.");
953
+ }
954
+
955
+ function installCopilot() {
956
+ if (!commandExists("copilot")) {
957
+ console.log("GitHub Copilot CLI not found. To enable CLIsponsor for GitHub Copilot CLI, install Copilot CLI and rerun: npx clisponsor@latest install");
958
+ return;
959
+ }
960
+
961
+ const copilotDir = path.join(CONFIG_DIR, "copilot");
962
+ fs.mkdirSync(copilotDir, { recursive: true });
963
+ const hookPath = path.join(copilotDir, "clisponsor_copilot_hook.mjs");
964
+ fs.writeFileSync(hookPath, copilotHookSource(), { mode: 0o755 });
965
+
966
+ const hooksPath = path.join(copilotHome(), "hooks", "clisponsor.json");
967
+ writeJson(hooksPath, {
968
+ version: 1,
969
+ hooks: {
970
+ sessionStart: [
971
+ {
972
+ type: "command",
973
+ command: `node ${JSON.stringify(hookPath)} sessionStart`,
974
+ timeoutSec: 5,
975
+ },
976
+ ],
977
+ userPromptSubmitted: [
978
+ {
979
+ type: "command",
980
+ command: `node ${JSON.stringify(hookPath)} userPromptSubmitted`,
981
+ timeoutSec: 5,
982
+ },
983
+ ],
984
+ agentStop: [
985
+ {
986
+ type: "command",
987
+ command: `node ${JSON.stringify(hookPath)} agentStop`,
988
+ timeoutSec: 5,
989
+ },
990
+ ],
991
+ },
992
+ });
993
+ console.log(`Updated ${hooksPath}`);
994
+ console.log("GitHub Copilot CLI hook installed.");
995
+ }
996
+
997
+ function installQwen() {
998
+ if (!commandExists("qwen")) {
999
+ console.log("Qwen Code CLI not found. To enable CLIsponsor for Qwen Code, install Qwen Code and rerun: npx clisponsor@latest install");
1000
+ return;
1001
+ }
1002
+
1003
+ const qwenDir = path.join(CONFIG_DIR, "qwen");
1004
+ fs.mkdirSync(qwenDir, { recursive: true });
1005
+ const hookPath = path.join(qwenDir, "clisponsor_qwen_hook.mjs");
1006
+ fs.writeFileSync(hookPath, agentHookSource("QwenCode"), { mode: 0o755 });
1007
+
1008
+ const settingsPath = path.join(qwenHome(), "settings.json");
1009
+ const settings = readEditableJson(settingsPath, {});
1010
+ addQwenCommandHook(settings, "SessionStart", `node ${JSON.stringify(hookPath)} SessionStart`);
1011
+ addQwenCommandHook(settings, "UserPromptSubmit", `node ${JSON.stringify(hookPath)} UserPromptSubmit`);
1012
+ addQwenCommandHook(settings, "Stop", `node ${JSON.stringify(hookPath)} Stop`);
1013
+ writeJson(settingsPath, settings);
1014
+ console.log(`Updated ${settingsPath}`);
1015
+ console.log("Qwen Code CLI hook installed.");
1016
+ }
1017
+
1018
+ function installTargets() {
1019
+ return [
1020
+ { target: "codex", label: "Codex CLI", command: "codex", install: installCodex },
1021
+ { target: "claude", label: "Claude Code CLI", command: "claude", install: installClaude },
1022
+ { target: "gemini", label: "Gemini CLI", command: "gemini", install: installGemini },
1023
+ { target: "antigravity", aliases: ["agy"], label: "Antigravity CLI", command: "agy", install: installAntigravity },
1024
+ { target: "opencode", label: "OpenCode CLI", command: "opencode", install: installOpenCode },
1025
+ { target: "pi", label: "Pi Coding Agent", command: "pi", install: installPi },
1026
+ { target: "copilot", label: "GitHub Copilot CLI", command: "copilot", install: installCopilot },
1027
+ { target: "qwen", label: "Qwen Code", command: "qwen", install: installQwen },
1028
+ ];
1029
+ }
1030
+
1031
+ function targetNames() {
1032
+ return installTargets().flatMap((spec) => [spec.target, ...(spec.aliases || [])]);
1033
+ }
1034
+
1035
+ function findInstallTarget(target) {
1036
+ return installTargets().find((spec) => spec.target === target || (spec.aliases || []).includes(target));
1037
+ }
1038
+
1039
+ function renderInstallHeader(target) {
1040
+ const scope = target === "all" ? "supported CLIs" : findInstallTarget(target)?.label || target;
1041
+ console.log(bold("CLIsponsor installer"));
1042
+ console.log(dim(`Scanning ${scope} on this machine...`));
1043
+ console.log("");
1044
+ }
1045
+
1046
+ function renderInstallSummary(results) {
1047
+ const installed = results.filter((result) => result.status === "installed").length;
1048
+ const missing = results.filter((result) => result.status === "missing").length;
1049
+ const failed = results.filter((result) => result.status === "failed").length;
1050
+ console.log("");
1051
+ const summary = `${installed} installed, ${missing} not found${failed ? `, ${failed} failed` : ""}.`;
1052
+ console.log(failed ? red(summary) : green(summary));
1053
+ if (missing) {
1054
+ console.log(dim("Install missing CLIs and rerun: npx clisponsor@latest install"));
1055
+ }
1056
+ }
1057
+
1058
+ function installTarget(spec) {
1059
+ if (!commandExists(spec.command)) {
1060
+ installLine("✕", spec, "not found", red);
1061
+ return { target: spec.target, status: "missing" };
1062
+ }
1063
+
1064
+ const stopSpinner = startInstallSpinner(spec);
1065
+ try {
1066
+ withoutInstallLogs(spec.install);
1067
+ stopSpinner();
1068
+ installLine("✓", spec, "installed", green);
1069
+ return { target: spec.target, status: "installed" };
1070
+ } catch (error) {
1071
+ stopSpinner();
1072
+ installLine("!", spec, `failed: ${error.message}`, red);
1073
+ return { target: spec.target, status: "failed", error };
1074
+ }
1075
+ }
1076
+
524
1077
  function installAll() {
525
- installCodex();
526
- installClaude();
527
- installGemini();
528
- installAntigravity();
1078
+ return installTargets().map((spec) => installTarget(spec));
529
1079
  }
530
1080
 
531
1081
  function install() {
532
1082
  const target = process.argv[3] && !process.argv[3].startsWith("--") ? process.argv[3] : "all";
533
- if (!["all", "codex", "claude", "gemini", "antigravity", "agy"].includes(target)) {
534
- console.error("Unknown install target. Use: codex, claude, gemini, antigravity, or all.");
1083
+ if (!["all", ...targetNames()].includes(target)) {
1084
+ console.error("Unknown install target. Use: codex, claude, gemini, antigravity, opencode, pi, copilot, qwen, or all.");
535
1085
  process.exit(1);
536
1086
  }
537
- if (target === "all") installAll();
538
- else if (target === "codex") installCodex();
539
- else if (target === "claude") installClaude();
540
- else if (target === "gemini") installGemini();
541
- else installAntigravity();
1087
+ renderInstallHeader(target);
1088
+ const results = target === "all" ? installAll() : [installTarget(findInstallTarget(target))];
1089
+ renderInstallSummary(results);
1090
+ if (results.some((result) => result.status === "failed")) process.exit(1);
542
1091
  }
543
1092
 
544
1093
  function uninstallCodex() {
@@ -591,16 +1140,69 @@ function uninstallAntigravity() {
591
1140
  console.log("Removed Antigravity hook script.");
592
1141
  }
593
1142
 
1143
+ function uninstallOpenCode() {
1144
+ const installedPlugin = path.join(openCodeConfigDir(), "plugins", "clisponsor_opencode_plugin.js");
1145
+ if (fs.existsSync(installedPlugin)) {
1146
+ fs.rmSync(installedPlugin, { force: true });
1147
+ console.log(`Removed ${installedPlugin}`);
1148
+ } else {
1149
+ console.log("No CLIsponsor OpenCode plugin found.");
1150
+ }
1151
+ fs.rmSync(path.join(CONFIG_DIR, "opencode"), { recursive: true, force: true });
1152
+ }
1153
+
1154
+ function uninstallPi() {
1155
+ const installedExtension = path.join(HOME, ".pi", "agent", "extensions", "clisponsor_pi_extension.ts");
1156
+ if (fs.existsSync(installedExtension)) {
1157
+ fs.rmSync(installedExtension, { force: true });
1158
+ console.log(`Removed ${installedExtension}`);
1159
+ } else {
1160
+ console.log("No CLIsponsor Pi extension found.");
1161
+ }
1162
+ fs.rmSync(path.join(CONFIG_DIR, "pi"), { recursive: true, force: true });
1163
+ }
1164
+
1165
+ function uninstallCopilot() {
1166
+ const hooksPath = path.join(copilotHome(), "hooks", "clisponsor.json");
1167
+ if (fs.existsSync(hooksPath)) {
1168
+ fs.rmSync(hooksPath, { force: true });
1169
+ console.log(`Removed ${hooksPath}`);
1170
+ } else {
1171
+ console.log("No CLIsponsor GitHub Copilot CLI hook found.");
1172
+ }
1173
+ fs.rmSync(path.join(CONFIG_DIR, "copilot"), { recursive: true, force: true });
1174
+ }
1175
+
1176
+ function uninstallQwen() {
1177
+ const settingsPath = path.join(qwenHome(), "settings.json");
1178
+ const settings = readEditableJson(settingsPath, {});
1179
+ if (removeClaudeCommandHooks(settings)) {
1180
+ writeJson(settingsPath, settings);
1181
+ console.log(`Removed CLIsponsor hooks from ${settingsPath}`);
1182
+ } else {
1183
+ console.log("No CLIsponsor Qwen Code hooks found.");
1184
+ }
1185
+ fs.rmSync(path.join(CONFIG_DIR, "qwen", "clisponsor_qwen_hook.mjs"), { force: true });
1186
+ try {
1187
+ fs.rmdirSync(path.join(CONFIG_DIR, "qwen"));
1188
+ } catch {}
1189
+ console.log("Removed Qwen Code hook script.");
1190
+ }
1191
+
594
1192
  function uninstall() {
595
1193
  const target = process.argv[3] && !process.argv[3].startsWith("--") ? process.argv[3] : "all";
596
- if (!["all", "codex", "claude", "gemini", "antigravity", "agy"].includes(target)) {
597
- console.error("Unknown uninstall target. Use: codex, claude, gemini, antigravity, or all.");
1194
+ if (!["all", "codex", "claude", "gemini", "antigravity", "agy", "opencode", "pi", "copilot", "qwen"].includes(target)) {
1195
+ console.error("Unknown uninstall target. Use: codex, claude, gemini, antigravity, opencode, pi, copilot, qwen, or all.");
598
1196
  process.exit(1);
599
1197
  }
600
1198
  if (target === "all" || target === "codex") uninstallCodex();
601
1199
  if (target === "all" || target === "claude") uninstallClaude();
602
1200
  if (target === "all" || target === "gemini") uninstallGemini();
603
1201
  if (target === "all" || target === "antigravity" || target === "agy") uninstallAntigravity();
1202
+ if (target === "all" || target === "opencode") uninstallOpenCode();
1203
+ if (target === "all" || target === "pi") uninstallPi();
1204
+ if (target === "all" || target === "copilot") uninstallCopilot();
1205
+ if (target === "all" || target === "qwen") uninstallQwen();
604
1206
  if (hasFlag("--config")) {
605
1207
  fs.rmSync(CONFIG_PATH, { force: true });
606
1208
  console.log(`Removed ${CONFIG_PATH}`);
@@ -660,6 +1262,10 @@ async function doctor() {
660
1262
  claudeHookScript: fs.existsSync(path.join(CONFIG_DIR, "claude", "clisponsor_claude_hook.mjs")),
661
1263
  geminiHookScript: fs.existsSync(path.join(CONFIG_DIR, "gemini", "clisponsor_gemini_hook.mjs")),
662
1264
  antigravityHookScript: fs.existsSync(path.join(CONFIG_DIR, "antigravity", "clisponsor_antigravity_hook.mjs")),
1265
+ opencodePlugin: fs.existsSync(path.join(openCodeConfigDir(), "plugins", "clisponsor_opencode_plugin.js")),
1266
+ piExtension: fs.existsSync(path.join(HOME, ".pi", "agent", "extensions", "clisponsor_pi_extension.ts")),
1267
+ copilotHook: fs.existsSync(path.join(copilotHome(), "hooks", "clisponsor.json")),
1268
+ qwenHookScript: fs.existsSync(path.join(CONFIG_DIR, "qwen", "clisponsor_qwen_hook.mjs")),
663
1269
  },
664
1270
  network: {},
665
1271
  };
@@ -687,6 +1293,10 @@ async function doctor() {
687
1293
  console.log(`Claude hook script: ${diagnostics.installed.claudeHookScript ? "yes" : "no"}`);
688
1294
  console.log(`Gemini hook script: ${diagnostics.installed.geminiHookScript ? "yes" : "no"}`);
689
1295
  console.log(`Antigravity hook script: ${diagnostics.installed.antigravityHookScript ? "yes" : "no"}`);
1296
+ console.log(`OpenCode plugin: ${diagnostics.installed.opencodePlugin ? "yes" : "no"}`);
1297
+ console.log(`Pi extension: ${diagnostics.installed.piExtension ? "yes" : "no"}`);
1298
+ console.log(`GitHub Copilot CLI hook: ${diagnostics.installed.copilotHook ? "yes" : "no"}`);
1299
+ console.log(`Qwen Code hook script: ${diagnostics.installed.qwenHookScript ? "yes" : "no"}`);
690
1300
  if (skipNetwork) {
691
1301
  console.log("Network: skipped");
692
1302
  } else {
@@ -697,9 +1307,9 @@ async function doctor() {
697
1307
 
698
1308
  function help() {
699
1309
  console.log(`clisponsor commands:
700
- clisponsor install [all|codex|claude|gemini|antigravity]
1310
+ clisponsor install [all|codex|claude|gemini|antigravity|opencode|pi|copilot|qwen]
701
1311
  clisponsor login <email> [--label=<device-label>]
702
- clisponsor uninstall [all|codex|claude|gemini|antigravity] [--config]
1312
+ clisponsor uninstall [all|codex|claude|gemini|antigravity|opencode|pi|copilot|qwen] [--config]
703
1313
  clisponsor status
704
1314
  clisponsor doctor [--json] [--skip-network]
705
1315
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clisponsor",
3
- "version": "1.0.11",
4
- "description": "CLIsponsor installer for Codex, Claude Code, and Gemini sponsored CLI placements.",
3
+ "version": "1.0.16",
4
+ "description": "CLIsponsor installer for Codex, Claude Code, Gemini, Antigravity, OpenCode, Pi, GitHub Copilot CLI, and Qwen Code sponsored CLI placements.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20"