opencrater 0.2.5 → 1.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 +38 -216
- package/README.md +105 -40
- package/dist/cli.js +27 -0
- package/dist/dismiss.js +3 -0
- package/dist/hook.js +6 -0
- package/dist/index.d.mts +223 -0
- package/dist/index.d.ts +223 -0
- package/dist/index.js +6 -0
- package/dist/index.mjs +6 -0
- package/dist/paint.js +9 -0
- package/package.json +35 -13
- package/cli.js +0 -416
package/package.json
CHANGED
|
@@ -1,33 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencrater",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "OpenCrater — sponsor cards
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "OpenCrater — sponsor cards for AI coding terminals (Claude Code, Codex, Gemini CLI, Copilot CLI). One command, auto-wires every host, opt out anytime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencrater",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"sponsorship",
|
|
8
|
+
"cli",
|
|
9
|
+
"mcp",
|
|
9
10
|
"terminal",
|
|
10
|
-
"
|
|
11
|
+
"ads"
|
|
11
12
|
],
|
|
12
13
|
"homepage": "https://opencrater.to",
|
|
13
|
-
"license": "
|
|
14
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
14
15
|
"type": "commonjs",
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=18.17"
|
|
17
18
|
},
|
|
18
19
|
"bin": {
|
|
19
|
-
"opencrater": "cli.js"
|
|
20
|
+
"opencrater": "dist/cli.js",
|
|
21
|
+
"opencrater-hook": "dist/hook.js",
|
|
22
|
+
"opencrater-paint": "dist/paint.js",
|
|
23
|
+
"opencrater-dismiss": "dist/dismiss.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"module": "./dist/index.mjs",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"require": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
20
35
|
},
|
|
21
36
|
"files": [
|
|
22
|
-
"
|
|
37
|
+
"dist",
|
|
23
38
|
"README.md",
|
|
24
39
|
"LICENSE"
|
|
25
40
|
],
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"test:watch": "vitest",
|
|
46
|
+
"version": "node scripts/sync-version.mjs && git add src/version.ts",
|
|
47
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm test"
|
|
29
48
|
},
|
|
30
|
-
"
|
|
31
|
-
"
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^18.19.0",
|
|
51
|
+
"tsup": "^8.0.0",
|
|
52
|
+
"typescript": "^5.4.0",
|
|
53
|
+
"vitest": "^3.0.0"
|
|
32
54
|
}
|
|
33
55
|
}
|
package/cli.js
DELETED
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* opencrater — the DIRECT channel: any Claude Code / Codex user can opt
|
|
4
|
-
* in to OpenCrater sponsor cards with one command, no publisher needed.
|
|
5
|
-
*
|
|
6
|
-
* npx opencrater on enable (Claude Code + Codex hooks)
|
|
7
|
-
* npx opencrater off disable (removes only our entries)
|
|
8
|
-
* npx opencrater status show what is installed
|
|
9
|
-
*
|
|
10
|
-
* Coexistence with publisher packages that embed OpenCrater:
|
|
11
|
-
* - hook entries are marker-isolated ("--package opencrater" identifies
|
|
12
|
-
* ours), so installs/uninstalls never touch a publisher's hooks;
|
|
13
|
-
* - the SDK's machine-wide frequency cap (~/.config/opencrater/state.json)
|
|
14
|
-
* means at most ONE card per interval across ALL sources, and the
|
|
15
|
-
* painter's lock file guarantees only one card is ever on screen.
|
|
16
|
-
*
|
|
17
|
-
* Rendering runs through @opencrater/sdk (opencrater-hook), the same engine
|
|
18
|
-
* publishers use. Opt out anytime: `npx opencrater off` or
|
|
19
|
-
* OPENCRATER_DISABLE=1.
|
|
20
|
-
*/
|
|
21
|
-
"use strict";
|
|
22
|
-
const fs = require("node:fs");
|
|
23
|
-
const path = require("node:path");
|
|
24
|
-
const os = require("node:os");
|
|
25
|
-
|
|
26
|
-
// Default = the house channel. Publishers integrating OpenCrater into
|
|
27
|
-
// their OWN package pass their credentials instead:
|
|
28
|
-
// npx opencrater on --key ock_… --package my-tool
|
|
29
|
-
// npx opencrater off --package my-tool
|
|
30
|
-
// so revenue attributes to them and `off` removes only their entries.
|
|
31
|
-
// Flags are parsed up front; everything below reads KEY/PKG.
|
|
32
|
-
let KEY = "ock_MUFntM66VEbx7e7fKoQr2a43VBx762rHkKw1G8vj";
|
|
33
|
-
let PKG = "opencrater";
|
|
34
|
-
{
|
|
35
|
-
const argv = process.argv;
|
|
36
|
-
for (let i = 2; i < argv.length; i++) {
|
|
37
|
-
if (argv[i] === "--key" && argv[i + 1]) KEY = argv[++i];
|
|
38
|
-
else if (argv[i] === "--package" && argv[i + 1]) PKG = argv[++i];
|
|
39
|
-
}
|
|
40
|
-
if (!/^ock_[A-Za-z0-9]{8,64}$/.test(KEY)) {
|
|
41
|
-
console.error("opencrater: --key must look like ock_…");
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
if (!/^[A-Za-z0-9@/_.-]{1,128}$/.test(PKG)) {
|
|
45
|
-
console.error("opencrater: --package must be 1-128 chars of [a-zA-Z0-9@/_.-]");
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// EVERY host hook is registered as a trigger; the SDK + platform decide
|
|
50
|
-
// which ones actually render (recommended session-edge hooks by default,
|
|
51
|
-
// remotely tunable). Registering everything also feeds the recommendation
|
|
52
|
-
// engine's anonymized topic signal so cards match what you're working on.
|
|
53
|
-
const CLAUDE_EVENTS = [
|
|
54
|
-
"SessionStart", "SessionEnd", "Stop", "StopFailure", "SubagentStart", "SubagentStop",
|
|
55
|
-
"Notification", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch",
|
|
56
|
-
"PermissionRequest", "PermissionDenied", "UserPromptSubmit", "UserPromptExpansion",
|
|
57
|
-
"PreCompact", "PostCompact", "TaskCreated", "TaskCompleted", "Setup", "TeammateIdle",
|
|
58
|
-
"Elicitation", "ElicitationResult", "ConfigChange", "InstructionsLoaded",
|
|
59
|
-
"WorktreeCreate", "WorktreeRemove", "CwdChanged", "FileChanged", "MessageDisplay",
|
|
60
|
-
];
|
|
61
|
-
// Gemini CLI hooks (verified HookEventName set, GA since v0.26.0):
|
|
62
|
-
// ~/.gemini/settings.json takes the same rule shape as Claude Code.
|
|
63
|
-
const GEMINI_EVENTS = [
|
|
64
|
-
"SessionStart", "SessionEnd", "BeforeAgent", "AfterAgent", "BeforeModel",
|
|
65
|
-
"AfterModel", "BeforeToolSelection", "BeforeTool", "AfterTool",
|
|
66
|
-
"PreCompress", "Notification",
|
|
67
|
-
];
|
|
68
|
-
const CODEX_EVENTS = [
|
|
69
|
-
"SessionStart", "Stop", "UserPromptSubmit", "PreToolUse", "PostToolUse",
|
|
70
|
-
"PermissionRequest", "PreCompact", "PostCompact",
|
|
71
|
-
];
|
|
72
|
-
const MARKER = "--package " + PKG + " "; // identifies OUR hook entries only
|
|
73
|
-
|
|
74
|
-
// Resilient hook command: prefer the pre-installed local runtime (no npx on
|
|
75
|
-
// the hot path — concurrent fires racing a cold npx cache caused
|
|
76
|
-
// "opencrater-hook: command not found" noise in the host); fall back to npx;
|
|
77
|
-
// ALWAYS exit 0 with stderr silenced — a sponsor hook must never surface an
|
|
78
|
-
// error inside someone's session.
|
|
79
|
-
const cmdFor = (event, host) => {
|
|
80
|
-
const args = "--placement " + event + " --key " + KEY +
|
|
81
|
-
" --package " + PKG + " --host " + host;
|
|
82
|
-
const runtime = '"$HOME/.config/opencrater/runtime/node_modules/@opencrater/sdk/dist/hook.js"';
|
|
83
|
-
return (
|
|
84
|
-
"{ if [ -f " + runtime + " ]; then node " + runtime + " " + args +
|
|
85
|
-
"; else npx -y -p @opencrater/sdk opencrater-hook " + args +
|
|
86
|
-
"; fi; } 2>/dev/null || true"
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const CLAUDE_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
|
|
91
|
-
const CODEX_HOOKS = path.join(
|
|
92
|
-
process.env.CODEX_HOME || path.join(os.homedir(), ".codex"),
|
|
93
|
-
"hooks.json",
|
|
94
|
-
);
|
|
95
|
-
const GEMINI_SETTINGS = path.join(os.homedir(), ".gemini", "settings.json");
|
|
96
|
-
|
|
97
|
-
function readJson(p) {
|
|
98
|
-
try {
|
|
99
|
-
const raw = fs.readFileSync(p, "utf8");
|
|
100
|
-
if (!raw.trim()) return {};
|
|
101
|
-
const v = JSON.parse(raw);
|
|
102
|
-
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
103
|
-
} catch (e) {
|
|
104
|
-
if (e.code === "ENOENT") return {};
|
|
105
|
-
throw new Error("Could not parse " + p + " — fix it by hand and retry.");
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function writeJson(p, v) {
|
|
110
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
111
|
-
const tmp = p + ".opencrater-tmp";
|
|
112
|
-
fs.writeFileSync(tmp, JSON.stringify(v, null, 2) + "\n");
|
|
113
|
-
fs.renameSync(tmp, p); // atomic — never leave a half-written settings file
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const isOurs = (rule) =>
|
|
117
|
-
Array.isArray(rule.hooks) &&
|
|
118
|
-
rule.hooks.some(
|
|
119
|
-
(h) => typeof h.command === "string" &&
|
|
120
|
-
h.command.includes("opencrater-hook") && h.command.includes(MARKER),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
function enableIn(file, host) {
|
|
124
|
-
const events =
|
|
125
|
-
host === "codex" ? CODEX_EVENTS :
|
|
126
|
-
host === "gemini_cli" ? GEMINI_EVENTS : CLAUDE_EVENTS;
|
|
127
|
-
const settings = readJson(file);
|
|
128
|
-
if (!settings.hooks) settings.hooks = {};
|
|
129
|
-
let changed = false;
|
|
130
|
-
for (const event of events) {
|
|
131
|
-
const list = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
132
|
-
const desired = {
|
|
133
|
-
matcher: "",
|
|
134
|
-
hooks: [{ type: "command", command: cmdFor(event, host) }],
|
|
135
|
-
};
|
|
136
|
-
// Remove EVERY entry of ours, then add exactly one. findIndex+replace only
|
|
137
|
-
// fixed the first match, so repeated installs / older command shapes left
|
|
138
|
-
// duplicate opencrater-hook entries piling up. Filtering collapses them.
|
|
139
|
-
const others = list.filter((r) => !isOurs(r));
|
|
140
|
-
const mine = list.filter(isOurs);
|
|
141
|
-
const alreadyCorrect =
|
|
142
|
-
mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(desired);
|
|
143
|
-
if (!alreadyCorrect) {
|
|
144
|
-
settings.hooks[event] = [...others, desired];
|
|
145
|
-
changed = true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (changed) writeJson(file, settings);
|
|
149
|
-
return changed;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function disableIn(file) {
|
|
153
|
-
const settings = readJson(file);
|
|
154
|
-
if (!settings.hooks) return false;
|
|
155
|
-
let changed = false;
|
|
156
|
-
for (const event of Object.keys(settings.hooks)) {
|
|
157
|
-
const list = settings.hooks[event];
|
|
158
|
-
if (!Array.isArray(list)) continue;
|
|
159
|
-
const kept = list.filter((r) => !isOurs(r));
|
|
160
|
-
if (kept.length !== list.length) {
|
|
161
|
-
changed = true;
|
|
162
|
-
if (kept.length === 0) delete settings.hooks[event];
|
|
163
|
-
else settings.hooks[event] = kept;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
167
|
-
if (changed) writeJson(file, settings);
|
|
168
|
-
return changed;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function statusIn(file) {
|
|
172
|
-
const settings = readJson(file);
|
|
173
|
-
if (!settings.hooks) return [];
|
|
174
|
-
return Object.keys(settings.hooks).filter(
|
|
175
|
-
(ev) => Array.isArray(settings.hooks[ev]) && settings.hooks[ev].some(isOurs),
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/* Dismiss the sponsor card currently on screen — no browser needed.
|
|
180
|
-
Writes the painter's local dismiss signal, records the dismissal against
|
|
181
|
-
the API (idempotent), and SIGTERMs the painter so the card clears NOW. */
|
|
182
|
-
async function dismissNow() {
|
|
183
|
-
const dir = path.join(os.homedir(), ".config", "opencrater");
|
|
184
|
-
let lock = null;
|
|
185
|
-
try {
|
|
186
|
-
lock = JSON.parse(fs.readFileSync(path.join(dir, "painter.lock"), "utf8"));
|
|
187
|
-
} catch {
|
|
188
|
-
console.log("opencrater: no sponsor card is on screen.");
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
try {
|
|
192
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
193
|
-
fs.writeFileSync(
|
|
194
|
-
path.join(dir, "dismiss.signal"),
|
|
195
|
-
JSON.stringify({ impressionId: lock.impressionId, at: Date.now() }),
|
|
196
|
-
);
|
|
197
|
-
} catch {}
|
|
198
|
-
if (lock.dismissUrl) {
|
|
199
|
-
try { await fetch(lock.dismissUrl, { signal: AbortSignal.timeout(1000) }); } catch {}
|
|
200
|
-
}
|
|
201
|
-
if (lock.impressionId) {
|
|
202
|
-
const origin = process.env.OPENCRATER_API_ORIGIN || "https://api.opencrater.to";
|
|
203
|
-
try {
|
|
204
|
-
await fetch(origin + "/x/" + lock.impressionId, { signal: AbortSignal.timeout(2500) });
|
|
205
|
-
} catch {}
|
|
206
|
-
}
|
|
207
|
-
if (lock.pid && lock.pid > 1) {
|
|
208
|
-
try { process.kill(lock.pid, "SIGTERM"); } catch {}
|
|
209
|
-
}
|
|
210
|
-
console.log("opencrater: ad dismissed — feedback recorded.");
|
|
211
|
-
console.log(" changed your mind? npx opencrater show");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/* Report the sponsor card currently on screen (or the last one shown).
|
|
215
|
-
Blocks the campaign on this machine permanently (reported-ads.json — the
|
|
216
|
-
SDK skips reported campaigns forever), clears the card, and opens the
|
|
217
|
-
hosted report screen so the user can pick a reason. */
|
|
218
|
-
async function reportAd() {
|
|
219
|
-
const dir = path.join(os.homedir(), ".config", "opencrater");
|
|
220
|
-
let lock = null;
|
|
221
|
-
let saved = null;
|
|
222
|
-
try { lock = JSON.parse(fs.readFileSync(path.join(dir, "painter.lock"), "utf8")); } catch {}
|
|
223
|
-
try { saved = JSON.parse(fs.readFileSync(path.join(dir, "last-ad.json"), "utf8")); } catch {}
|
|
224
|
-
const impressionId = (lock && lock.impressionId) || (saved && saved.payload && saved.payload.impressionId);
|
|
225
|
-
const campaignId = saved && saved.payload && saved.payload.campaignId;
|
|
226
|
-
if (!impressionId && !campaignId) {
|
|
227
|
-
console.log("opencrater: no sponsor card to report.");
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// permanent local block — never render this campaign here again
|
|
231
|
-
if (campaignId) {
|
|
232
|
-
const file = path.join(dir, "reported-ads.json");
|
|
233
|
-
let reported = {};
|
|
234
|
-
try { reported = JSON.parse(fs.readFileSync(file, "utf8")) || {}; } catch {}
|
|
235
|
-
reported[campaignId] = Date.now();
|
|
236
|
-
try {
|
|
237
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
238
|
-
fs.writeFileSync(file, JSON.stringify(reported));
|
|
239
|
-
} catch {}
|
|
240
|
-
}
|
|
241
|
-
// clear the live card, if any
|
|
242
|
-
if (lock) {
|
|
243
|
-
try {
|
|
244
|
-
fs.writeFileSync(
|
|
245
|
-
path.join(dir, "dismiss.signal"),
|
|
246
|
-
JSON.stringify({ impressionId: lock.impressionId, at: Date.now() }),
|
|
247
|
-
);
|
|
248
|
-
} catch {}
|
|
249
|
-
if (lock.pid && lock.pid > 1) {
|
|
250
|
-
try { process.kill(lock.pid, "SIGTERM"); } catch {}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
console.log("opencrater: reported — you won't see this ad on this machine again.");
|
|
254
|
-
if (impressionId) {
|
|
255
|
-
const origin = process.env.OPENCRATER_API_ORIGIN || "https://api.opencrater.to";
|
|
256
|
-
const url = origin + "/r/" + impressionId;
|
|
257
|
-
const { spawnSync } = require("node:child_process");
|
|
258
|
-
const opener = process.platform === "darwin" ? "open" : "xdg-open";
|
|
259
|
-
const res = spawnSync(opener, [url], { stdio: "ignore" });
|
|
260
|
-
if (res.status === 0) {
|
|
261
|
-
console.log(" tell us why in the tab that just opened (10 seconds).");
|
|
262
|
-
} else {
|
|
263
|
-
console.log(" tell us why (optional): " + url);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/* Repaint the last sponsor card ("show ad again"). Reads the payload the
|
|
269
|
-
painter saved, resolves THIS terminal's tty, and spawns the local-runtime
|
|
270
|
-
painter detached — exactly how the hook does it. */
|
|
271
|
-
function showAgain() {
|
|
272
|
-
const dir = path.join(os.homedir(), ".config", "opencrater");
|
|
273
|
-
let saved = null;
|
|
274
|
-
try {
|
|
275
|
-
saved = JSON.parse(fs.readFileSync(path.join(dir, "last-ad.json"), "utf8"));
|
|
276
|
-
} catch {
|
|
277
|
-
console.log("opencrater: no recent ad to show.");
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const painter = path.join(dir, "runtime", "node_modules", "@opencrater", "sdk", "dist", "paint.js");
|
|
281
|
-
if (!fs.existsSync(painter)) {
|
|
282
|
-
console.log("opencrater: hook runtime not installed yet — run: npx opencrater on");
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const { spawnSync, spawn } = require("node:child_process");
|
|
286
|
-
const isWin = process.platform === "win32";
|
|
287
|
-
// Replay: live ✕ (local), but never re-poll or re-record the original
|
|
288
|
-
// impression — it was already dismissed once.
|
|
289
|
-
const payload = { ...saved.payload, replay: true };
|
|
290
|
-
delete payload.impressionId;
|
|
291
|
-
const env = { ...process.env, OPENCRATER_PAINT: JSON.stringify(payload) };
|
|
292
|
-
// Unix: resolve our terminal device so the detached painter can open it.
|
|
293
|
-
// Windows: no `tty` command and no device path — the painter opens CONOUT$
|
|
294
|
-
// of the console it inherits, so we must NOT detach (detach = new/hidden
|
|
295
|
-
// console = nothing visible). Mirror the SDK's painterSpawnOptions.
|
|
296
|
-
if (!isWin) {
|
|
297
|
-
const tty = spawnSync("tty", { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" })
|
|
298
|
-
.stdout?.trim();
|
|
299
|
-
if (tty && tty.startsWith("/dev/")) env.OPENCRATER_TTY = tty;
|
|
300
|
-
}
|
|
301
|
-
const child = spawn(process.execPath, [painter], {
|
|
302
|
-
detached: !isWin,
|
|
303
|
-
stdio: "ignore",
|
|
304
|
-
env,
|
|
305
|
-
...(isWin ? { windowsHide: true } : {}),
|
|
306
|
-
});
|
|
307
|
-
child.on("error", () => {});
|
|
308
|
-
child.unref();
|
|
309
|
-
console.log("opencrater: showing the last ad again.");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/* Auto-detect which AI CLIs exist on this machine — `on` connects every
|
|
313
|
-
detected host's config automatically, and new hosts picked up here flow
|
|
314
|
-
into detection as we add them. */
|
|
315
|
-
function hasBin(name) {
|
|
316
|
-
try {
|
|
317
|
-
const { spawnSync } = require("node:child_process");
|
|
318
|
-
return spawnSync("which", [name], { stdio: "ignore" }).status === 0;
|
|
319
|
-
} catch { return false; }
|
|
320
|
-
}
|
|
321
|
-
function detectHosts() {
|
|
322
|
-
return {
|
|
323
|
-
claude_code: fs.existsSync(path.join(os.homedir(), ".claude")) || hasBin("claude"),
|
|
324
|
-
codex: fs.existsSync(path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"))) || hasBin("codex"),
|
|
325
|
-
openclaw: fs.existsSync(path.join(os.homedir(), ".openclaw")) || hasBin("openclaw"),
|
|
326
|
-
gemini_cli: fs.existsSync(path.join(os.homedir(), ".gemini")) || hasBin("gemini"),
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const cmd = process.argv[2] || "status";
|
|
331
|
-
if (cmd === "on" || cmd === "enable") {
|
|
332
|
-
const hosts = detectHosts();
|
|
333
|
-
if (!hosts.claude_code && !hosts.codex) {
|
|
334
|
-
console.log("No supported AI CLI found (looked for Claude Code and Codex).");
|
|
335
|
-
console.log("Install one, then run: npx opencrater on");
|
|
336
|
-
process.exit(1);
|
|
337
|
-
}
|
|
338
|
-
if (hosts.claude_code) {
|
|
339
|
-
enableIn(CLAUDE_SETTINGS, "claude_code");
|
|
340
|
-
console.log("✓ Claude Code detected — connected (" + CLAUDE_SETTINGS + ")");
|
|
341
|
-
} else {
|
|
342
|
-
console.log("· Claude Code not found — skipped");
|
|
343
|
-
}
|
|
344
|
-
if (hosts.codex) {
|
|
345
|
-
enableIn(CODEX_HOOKS, "codex");
|
|
346
|
-
console.log("✓ Codex detected — connected (" + CODEX_HOOKS + ")");
|
|
347
|
-
console.log(" Codex will ask once to trust the new hooks on its next launch — approve to enable.");
|
|
348
|
-
} else {
|
|
349
|
-
console.log("· Codex not found — skipped");
|
|
350
|
-
}
|
|
351
|
-
if (hosts.gemini_cli) {
|
|
352
|
-
enableIn(GEMINI_SETTINGS, "gemini_cli");
|
|
353
|
-
console.log("✓ Gemini CLI detected — connected (" + GEMINI_SETTINGS + ")");
|
|
354
|
-
} else {
|
|
355
|
-
console.log("· Gemini CLI not found — skipped");
|
|
356
|
-
}
|
|
357
|
-
if (hosts.openclaw) {
|
|
358
|
-
console.log("· OpenClaw detected — its hooks run inside the gateway (no terminal");
|
|
359
|
-
console.log(" overlay surface); message-level sponsorship is on the roadmap.");
|
|
360
|
-
}
|
|
361
|
-
// Pre-install the local hook runtime (background): hooks then run via
|
|
362
|
-
// plain node — no npx resolution latency, no cold-cache races. Best-effort
|
|
363
|
-
// and non-fatal — hooks fall back to `npx -y` when the runtime is absent.
|
|
364
|
-
try {
|
|
365
|
-
const { spawn } = require("node:child_process");
|
|
366
|
-
// Windows: `npm` is `npm.cmd`, spawnable only through a shell — without
|
|
367
|
-
// shell:true this throws ENOENT as an async 'error' event that escapes
|
|
368
|
-
// try/catch and crashes the process. shell:true + an error handler keep
|
|
369
|
-
// it silent on every platform.
|
|
370
|
-
const isWin = process.platform === "win32";
|
|
371
|
-
const child = spawn("npm", ["install", "--prefix",
|
|
372
|
-
path.join(os.homedir(), ".config", "opencrater", "runtime"),
|
|
373
|
-
"@opencrater/sdk@latest", "--silent", "--no-audit", "--no-fund"],
|
|
374
|
-
{ detached: true, stdio: "ignore", shell: isWin, windowsHide: true });
|
|
375
|
-
child.on("error", () => {});
|
|
376
|
-
child.unref();
|
|
377
|
-
} catch {}
|
|
378
|
-
console.log("OpenCrater ads enabled for Claude Code + Codex.");
|
|
379
|
-
console.log("All hooks registered as triggers; cards render only at session edges");
|
|
380
|
-
console.log("(SessionStart, Stop, SessionEnd, Notification) with a machine-wide frequency cap.");
|
|
381
|
-
console.log("Ads are personalized to what you're working on via anonymized keywords");
|
|
382
|
-
console.log("(never raw prompts, paths, or secrets).");
|
|
383
|
-
console.log("Turn off anytime: npx opencrater off (or OPENCRATER_DISABLE=1)");
|
|
384
|
-
console.log("");
|
|
385
|
-
console.log("VS Code tip: the first time you ⌘-click a sponsor link, choose");
|
|
386
|
-
console.log("'Configure Trusted Domains' → trust api.opencrater.to and the");
|
|
387
|
-
console.log("confirmation dialog never appears again.");
|
|
388
|
-
} else if (cmd === "off" || cmd === "disable") {
|
|
389
|
-
const a = disableIn(CLAUDE_SETTINGS);
|
|
390
|
-
const b = disableIn(CODEX_HOOKS);
|
|
391
|
-
const c = disableIn(GEMINI_SETTINGS);
|
|
392
|
-
void c;
|
|
393
|
-
console.log(a || b ? "OpenCrater ads disabled. Publisher hooks (if any) were not touched." : "Nothing to remove — OpenCrater ads were not enabled.");
|
|
394
|
-
} else if (cmd === "x" || cmd === "dismiss") {
|
|
395
|
-
dismissNow();
|
|
396
|
-
} else if (cmd === "report" || cmd === "flag") {
|
|
397
|
-
reportAd();
|
|
398
|
-
} else if (cmd === "show" || cmd === "again") {
|
|
399
|
-
showAgain();
|
|
400
|
-
} else if (cmd === "status") {
|
|
401
|
-
const hosts = detectHosts();
|
|
402
|
-
const cc = statusIn(CLAUDE_SETTINGS);
|
|
403
|
-
const cx = statusIn(CODEX_HOOKS);
|
|
404
|
-
const gm = statusIn(GEMINI_SETTINGS);
|
|
405
|
-
console.log("Claude Code:", !hosts.claude_code ? "not installed" : cc.length ? "enabled (" + cc.length + " hooks)" : "detected — run: npx opencrater on");
|
|
406
|
-
console.log("Codex: ", !hosts.codex ? "not installed" : cx.length ? "enabled (" + cx.length + " hooks)" : "detected — run: npx opencrater on");
|
|
407
|
-
console.log("Gemini CLI:", !hosts.gemini_cli ? "not installed" : gm.length ? "enabled (" + gm.length + " hooks)" : "detected — run: npx opencrater on");
|
|
408
|
-
if (hosts.openclaw) console.log("OpenClaw: detected — message-level sponsorship on the roadmap");
|
|
409
|
-
} else {
|
|
410
|
-
console.log("Usage: npx opencrater <on|off|status|x|report|show> [--key ock_… --package name]");
|
|
411
|
-
console.log(" --key/--package publisher attribution (defaults to the OpenCrater house channel)");
|
|
412
|
-
console.log(" x dismiss the sponsor card currently on screen");
|
|
413
|
-
console.log(" report report the ad (never shows on this machine again)");
|
|
414
|
-
console.log(" show bring the last dismissed ad back");
|
|
415
|
-
process.exitCode = 1;
|
|
416
|
-
}
|