clawmoney 0.15.9 → 0.15.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function relaySetupCommand(): Promise<void>;
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { intro, outro, multiselect, confirm, text, spinner, isCancel, cancel, log, } from "@clack/prompts";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { apiPost } from "../utils/api.js";
|
|
8
|
+
import { requireConfig } from "../utils/config.js";
|
|
9
|
+
import { API_PRICES, RELAY_DISCOUNT, PLATFORM_FEE } from "../relay/pricing.js";
|
|
10
|
+
// ── Per-cli_type model catalogs ──
|
|
11
|
+
//
|
|
12
|
+
// `RECOMMENDED_MODELS` is what gets registered when the user picks "all
|
|
13
|
+
// recommended" — it's a curated subset of API_PRICES that maps to the
|
|
14
|
+
// models a typical end-user actually wants to expose. Old / preview /
|
|
15
|
+
// niche models are intentionally excluded from the default; the user
|
|
16
|
+
// can pick them via "select individually" if needed.
|
|
17
|
+
//
|
|
18
|
+
// `modelsForCli` returns the full set per cli_type drawn directly from
|
|
19
|
+
// API_PRICES so every priced model is available in the manual picker.
|
|
20
|
+
const RECOMMENDED_MODELS = {
|
|
21
|
+
claude: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
|
|
22
|
+
codex: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex"],
|
|
23
|
+
antigravity: [
|
|
24
|
+
"antigravity-gemini-3-pro",
|
|
25
|
+
"antigravity-claude-opus-4-6",
|
|
26
|
+
"antigravity-claude-sonnet-4-6",
|
|
27
|
+
],
|
|
28
|
+
gemini: ["gemini-2.5-pro", "gemini-2.5-flash"],
|
|
29
|
+
};
|
|
30
|
+
function modelsForCli(cli) {
|
|
31
|
+
const all = Object.keys(API_PRICES);
|
|
32
|
+
if (cli === "claude") {
|
|
33
|
+
return all.filter((m) => m.startsWith("claude-"));
|
|
34
|
+
}
|
|
35
|
+
if (cli === "codex") {
|
|
36
|
+
return all.filter((m) => m.startsWith("gpt-") || m.startsWith("o3") || m.startsWith("o4"));
|
|
37
|
+
}
|
|
38
|
+
if (cli === "antigravity") {
|
|
39
|
+
return all.filter((m) => m.startsWith("antigravity-"));
|
|
40
|
+
}
|
|
41
|
+
if (cli === "gemini") {
|
|
42
|
+
// Exclude antigravity-prefixed gemini variants — those are served by
|
|
43
|
+
// the antigravity cli_type, not the standalone gemini cli_type.
|
|
44
|
+
return all.filter((m) => m.startsWith("gemini-") && !m.startsWith("antigravity-"));
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
function detectInstalledClis() {
|
|
49
|
+
const results = [];
|
|
50
|
+
// Binary-based CLIs: probe `which`. We intentionally don't try to
|
|
51
|
+
// validate OAuth state here — the daemon's preflight does that on
|
|
52
|
+
// first start, and probing OAuth from a sync setup wizard would be
|
|
53
|
+
// brittle (keychain prompts, refresh-token races, etc).
|
|
54
|
+
const binaries = [
|
|
55
|
+
{ cli: "claude", bin: "claude" },
|
|
56
|
+
{ cli: "codex", bin: "codex" },
|
|
57
|
+
{ cli: "gemini", bin: "gemini" },
|
|
58
|
+
];
|
|
59
|
+
for (const { cli, bin } of binaries) {
|
|
60
|
+
let installed = false;
|
|
61
|
+
try {
|
|
62
|
+
execSync(`command -v ${bin}`, { stdio: "pipe" });
|
|
63
|
+
installed = true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
installed = false;
|
|
67
|
+
}
|
|
68
|
+
results.push({
|
|
69
|
+
cli,
|
|
70
|
+
available: installed,
|
|
71
|
+
hint: installed
|
|
72
|
+
? "binary in PATH (login state will be validated when daemon starts)"
|
|
73
|
+
: `${bin} not found in PATH`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Antigravity is OAuth-file based — there's no `antigravity` binary
|
|
77
|
+
// installed locally. We check for the OAuth credentials file that
|
|
78
|
+
// `clawmoney antigravity login` writes.
|
|
79
|
+
const antigravityFile = join(homedir(), ".clawmoney", "antigravity-accounts.json");
|
|
80
|
+
const antigravityAvailable = existsSync(antigravityFile);
|
|
81
|
+
results.push({
|
|
82
|
+
cli: "antigravity",
|
|
83
|
+
available: antigravityAvailable,
|
|
84
|
+
hint: antigravityAvailable
|
|
85
|
+
? "Google OAuth file present"
|
|
86
|
+
: "no Google OAuth linked (run `clawmoney antigravity login` first)",
|
|
87
|
+
});
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
// ── Helpers ──
|
|
91
|
+
function formatPrice(input, output) {
|
|
92
|
+
return `$${input}/$${output} per 1M`;
|
|
93
|
+
}
|
|
94
|
+
function formatBuyerPrice(input, output) {
|
|
95
|
+
const buyerInput = (input * RELAY_DISCOUNT).toFixed(3);
|
|
96
|
+
const buyerOutput = (output * RELAY_DISCOUNT).toFixed(3);
|
|
97
|
+
return `$${buyerInput}/$${buyerOutput} per 1M (after ${Math.round((1 - RELAY_DISCOUNT) * 100)}% relay discount)`;
|
|
98
|
+
}
|
|
99
|
+
// ── Main command ──
|
|
100
|
+
export async function relaySetupCommand() {
|
|
101
|
+
const config = requireConfig();
|
|
102
|
+
intro(chalk.cyan(" ClawMoney Relay Setup "));
|
|
103
|
+
log.message("Sell your idle Claude Max / ChatGPT Pro / Google subscription capacity to other AI agents.");
|
|
104
|
+
// ── Step 1: detect installed CLIs ──
|
|
105
|
+
const detectSpin = spinner();
|
|
106
|
+
detectSpin.start("Scanning for installed CLI clients...");
|
|
107
|
+
const detected = detectInstalledClis();
|
|
108
|
+
detectSpin.stop("Scan complete");
|
|
109
|
+
for (const d of detected) {
|
|
110
|
+
if (d.available) {
|
|
111
|
+
log.success(`${chalk.bold(d.cli.padEnd(12))} ${chalk.dim(d.hint)}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
log.warn(`${chalk.bold(d.cli.padEnd(12))} ${chalk.dim(d.hint)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const available = detected.filter((d) => d.available);
|
|
118
|
+
if (available.length === 0) {
|
|
119
|
+
log.error("No supported CLI clients found locally. Install at least one of: " +
|
|
120
|
+
chalk.cyan("claude, codex, gemini") +
|
|
121
|
+
" — or run `clawmoney antigravity login` to link a Google account.");
|
|
122
|
+
cancel("Setup aborted");
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// ── Step 2: pick which families to register ──
|
|
126
|
+
const familyChoice = await multiselect({
|
|
127
|
+
message: "Which CLI families do you want to provide? (space to toggle, enter to confirm)",
|
|
128
|
+
options: available.map((d) => ({
|
|
129
|
+
value: d.cli,
|
|
130
|
+
label: d.cli,
|
|
131
|
+
hint: d.hint,
|
|
132
|
+
})),
|
|
133
|
+
initialValues: available.map((d) => d.cli),
|
|
134
|
+
required: true,
|
|
135
|
+
});
|
|
136
|
+
if (isCancel(familyChoice)) {
|
|
137
|
+
cancel("Setup cancelled");
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
const selectedClis = familyChoice;
|
|
141
|
+
const registrations = [];
|
|
142
|
+
for (const cli of selectedClis) {
|
|
143
|
+
const allModels = modelsForCli(cli);
|
|
144
|
+
const recommended = (RECOMMENDED_MODELS[cli] ?? []).filter((m) => allModels.includes(m));
|
|
145
|
+
if (allModels.length === 0) {
|
|
146
|
+
log.warn(`${cli}: no models found in pricing table — skipping`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
log.step(`${chalk.bold(cli)}: choose models`);
|
|
150
|
+
const useRecommended = await confirm({
|
|
151
|
+
message: `Register the ${recommended.length} recommended ${cli} models? (${recommended.join(", ")})`,
|
|
152
|
+
initialValue: true,
|
|
153
|
+
});
|
|
154
|
+
if (isCancel(useRecommended)) {
|
|
155
|
+
cancel("Setup cancelled");
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
let chosen;
|
|
159
|
+
if (useRecommended) {
|
|
160
|
+
chosen = recommended;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const picked = await multiselect({
|
|
164
|
+
message: `Pick ${cli} models to register:`,
|
|
165
|
+
options: allModels.map((m) => {
|
|
166
|
+
const p = API_PRICES[m];
|
|
167
|
+
return {
|
|
168
|
+
value: m,
|
|
169
|
+
label: m,
|
|
170
|
+
hint: formatPrice(p.input, p.output),
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
initialValues: recommended,
|
|
174
|
+
required: true,
|
|
175
|
+
});
|
|
176
|
+
if (isCancel(picked)) {
|
|
177
|
+
cancel("Setup cancelled");
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
chosen = picked;
|
|
181
|
+
}
|
|
182
|
+
for (const model of chosen) {
|
|
183
|
+
const p = API_PRICES[model];
|
|
184
|
+
registrations.push({
|
|
185
|
+
cli,
|
|
186
|
+
model,
|
|
187
|
+
input: p.input,
|
|
188
|
+
output: p.output,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (registrations.length === 0) {
|
|
193
|
+
cancel("No models selected — nothing to register");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
// ── Step 4: global concurrency + daily budget ──
|
|
197
|
+
const concurrencyAns = await text({
|
|
198
|
+
message: "Concurrency cap per provider? (1-5 recommended; higher looks less like a single power user to upstream fingerprint detection)",
|
|
199
|
+
placeholder: "5",
|
|
200
|
+
defaultValue: "5",
|
|
201
|
+
validate: (v) => {
|
|
202
|
+
const n = parseInt(v || "5", 10);
|
|
203
|
+
if (Number.isNaN(n) || n < 1 || n > 20) {
|
|
204
|
+
return "Must be a number between 1 and 20";
|
|
205
|
+
}
|
|
206
|
+
return undefined;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
if (isCancel(concurrencyAns)) {
|
|
210
|
+
cancel("Setup cancelled");
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|
|
213
|
+
const dailyLimitAns = await text({
|
|
214
|
+
message: "Daily API spend cap per provider in USD? (the daemon stops accepting requests when exceeded)",
|
|
215
|
+
placeholder: "15",
|
|
216
|
+
defaultValue: "15",
|
|
217
|
+
validate: (v) => {
|
|
218
|
+
const n = parseFloat(v || "15");
|
|
219
|
+
if (Number.isNaN(n) || n < 0) {
|
|
220
|
+
return "Must be a non-negative number";
|
|
221
|
+
}
|
|
222
|
+
return undefined;
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
if (isCancel(dailyLimitAns)) {
|
|
226
|
+
cancel("Setup cancelled");
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
const concurrency = parseInt(concurrencyAns, 10);
|
|
230
|
+
const dailyLimit = parseFloat(dailyLimitAns);
|
|
231
|
+
// ── Step 5: confirmation summary ──
|
|
232
|
+
log.step(chalk.bold("Summary"));
|
|
233
|
+
for (const r of registrations) {
|
|
234
|
+
log.message(` ${chalk.cyan(r.cli + "/" + r.model).padEnd(50)} ${chalk.dim(formatBuyerPrice(r.input, r.output))}`);
|
|
235
|
+
}
|
|
236
|
+
log.message(chalk.dim(` ${registrations.length} providers · concurrency=${concurrency} · daily_limit=$${dailyLimit}`));
|
|
237
|
+
log.message(chalk.dim(` You earn ~${Math.round((1 - PLATFORM_FEE) * 100)}% of what buyers pay (after platform fee)`));
|
|
238
|
+
const proceed = await confirm({
|
|
239
|
+
message: `Register all ${registrations.length} providers now?`,
|
|
240
|
+
initialValue: true,
|
|
241
|
+
});
|
|
242
|
+
if (isCancel(proceed) || !proceed) {
|
|
243
|
+
cancel("Setup cancelled");
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
// ── Step 6: register each (idempotent — "already registered" counts as success) ──
|
|
247
|
+
let succeeded = 0;
|
|
248
|
+
let failed = 0;
|
|
249
|
+
const failures = [];
|
|
250
|
+
for (const r of registrations) {
|
|
251
|
+
const regSpin = spinner();
|
|
252
|
+
regSpin.start(`Registering ${r.cli}/${r.model}...`);
|
|
253
|
+
try {
|
|
254
|
+
const body = {
|
|
255
|
+
cli_type: r.cli,
|
|
256
|
+
model: r.model,
|
|
257
|
+
mode: "chat",
|
|
258
|
+
concurrency,
|
|
259
|
+
daily_limit_usd: dailyLimit,
|
|
260
|
+
price_input_per_m: r.input,
|
|
261
|
+
price_output_per_m: r.output,
|
|
262
|
+
};
|
|
263
|
+
const resp = await apiPost("/api/v1/relay/providers", body, config.api_key);
|
|
264
|
+
if (resp.ok) {
|
|
265
|
+
regSpin.stop(`${chalk.green("✓")} ${r.cli}/${r.model}`);
|
|
266
|
+
succeeded++;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
|
|
270
|
+
? resp.data.detail
|
|
271
|
+
: resp.data;
|
|
272
|
+
const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
273
|
+
// Already-registered is a soft success — idempotent re-run.
|
|
274
|
+
if (detail.includes("Already registered")) {
|
|
275
|
+
regSpin.stop(`${chalk.yellow("~")} ${r.cli}/${r.model} ${chalk.dim("(already registered, no change)")}`);
|
|
276
|
+
succeeded++;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
regSpin.stop(`${chalk.red("✗")} ${r.cli}/${r.model} ${chalk.dim("(" + detail.slice(0, 80) + ")")}`);
|
|
280
|
+
failed++;
|
|
281
|
+
failures.push({ cli: r.cli, model: r.model, error: detail });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
const msg = err.message;
|
|
287
|
+
regSpin.stop(`${chalk.red("✗")} ${r.cli}/${r.model} ${chalk.dim("(" + msg + ")")}`);
|
|
288
|
+
failed++;
|
|
289
|
+
failures.push({ cli: r.cli, model: r.model, error: msg });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── Step 7: next steps ──
|
|
293
|
+
log.step(chalk.bold("Done"));
|
|
294
|
+
log.message(`${chalk.green(succeeded.toString())} providers registered`);
|
|
295
|
+
if (failed > 0) {
|
|
296
|
+
log.warn(`${failed} registrations failed`);
|
|
297
|
+
for (const f of failures) {
|
|
298
|
+
log.message(chalk.dim(` ${f.cli}/${f.model}: ${f.error.slice(0, 120)}`));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
log.message("");
|
|
302
|
+
log.message(chalk.bold("Next steps"));
|
|
303
|
+
log.message(" Start the daemon for each cli_type you registered:");
|
|
304
|
+
for (const cli of selectedClis) {
|
|
305
|
+
log.message(` ${chalk.cyan(`clawmoney relay start --cli ${cli}`)}`);
|
|
306
|
+
}
|
|
307
|
+
log.message("");
|
|
308
|
+
log.message(" Useful follow-up commands:");
|
|
309
|
+
log.message(` ${chalk.cyan("clawmoney relay status")} # check daemon health + provider list`);
|
|
310
|
+
log.message(` ${chalk.cyan("clawmoney relay credits")} # check earnings + payout balance`);
|
|
311
|
+
log.message(` ${chalk.cyan("clawmoney relay stop")} # stop the daemon`);
|
|
312
|
+
outro(chalk.green("Setup complete"));
|
|
313
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -458,6 +458,19 @@ gig
|
|
|
458
458
|
const relay = program
|
|
459
459
|
.command('relay')
|
|
460
460
|
.description('Relay marketplace: sell idle AI subscription capacity');
|
|
461
|
+
relay
|
|
462
|
+
.command('setup')
|
|
463
|
+
.description('Interactive: detect installed CLIs, pick models, register all in one go (recommended for first-time setup)')
|
|
464
|
+
.action(async () => {
|
|
465
|
+
try {
|
|
466
|
+
const { relaySetupCommand } = await import('./commands/relay-setup.js');
|
|
467
|
+
await relaySetupCommand();
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
console.error(err.message);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
461
474
|
relay
|
|
462
475
|
.command('register')
|
|
463
476
|
.description('Register as a relay provider')
|
|
@@ -937,41 +937,115 @@ function mergeBetas(required, clientBeta) {
|
|
|
937
937
|
}
|
|
938
938
|
return out.join(",");
|
|
939
939
|
}
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
// match the account's pinned UA (syncBillingHeaderVersion, gateway_billing_header.go).
|
|
940
|
+
// Ensure a passthrough body carries the full Claude Code fingerprint
|
|
941
|
+
// shell that Anthropic's OAuth-endpoint validator expects. Called from
|
|
942
|
+
// doCallClaudeApiPassthrough as the last body-munging step before the
|
|
943
|
+
// HTTP request goes out.
|
|
945
944
|
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
945
|
+
// The three fingerprint-sensitive fields that MUST be present on every
|
|
946
|
+
// real Claude Code /v1/messages request:
|
|
947
|
+
//
|
|
948
|
+
// 1. system[0] = {type:"text", text:"x-anthropic-billing-header: ..."}
|
|
949
|
+
// — always the FIRST block. Contains cc_version.<FP3> where FP3 is
|
|
950
|
+
// SHA256(SALT + chars_from_first_user_msg + cli_version).hex[:3].
|
|
951
|
+
// 2. system[i] = {type:"text", text:"You are a Claude agent, built on
|
|
952
|
+
// Anthropic's Claude Agent SDK..."} with cache_control ephemeral.
|
|
953
|
+
// — the template-mode "CC identity marker" that passes the dice-
|
|
954
|
+
// coefficient validator.
|
|
955
|
+
// 3. thinking: {type, budget_tokens?} — on Claude 4+ models real CLI
|
|
956
|
+
// always sends this; zero-thinking accounts stand out.
|
|
957
|
+
// 4. tools: array (empty [] is fine) — real CLI always sends the
|
|
958
|
+
// field, missing means the request shape doesn't match.
|
|
959
|
+
//
|
|
960
|
+
// For real Claude Code / anthropic SDK clients that already send a full
|
|
961
|
+
// body (via /v1/messages passthrough path), every check here no-ops —
|
|
962
|
+
// the body is already in CC shape and we don't touch it.
|
|
963
|
+
//
|
|
964
|
+
// For OpenAI-SDK-style clients going through /v1/chat/completions (the
|
|
965
|
+
// Hub's chat→anthropic converter produces a minimal body), we augment
|
|
966
|
+
// with the missing shell fields so the outbound request is
|
|
967
|
+
// indistinguishable from a real CC request that happens to have a
|
|
968
|
+
// user-provided system prompt and no local tools.
|
|
969
|
+
//
|
|
970
|
+
// All buyer content (messages, their own system text, their own tools,
|
|
971
|
+
// thinking config if they sent one) is preserved.
|
|
972
|
+
function ensureClaudeCodeShell(body, fingerprint) {
|
|
973
|
+
// ── Normalize system to an array of content blocks ──
|
|
974
|
+
if (!Array.isArray(body.system)) {
|
|
975
|
+
if (typeof body.system === "string" && body.system.length > 0) {
|
|
976
|
+
// String-shaped system (anthropic SDK convenience form) →
|
|
977
|
+
// wrap in a single text block so we can prepend.
|
|
978
|
+
body.system = [{ type: "text", text: body.system }];
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
body.system = [];
|
|
982
|
+
}
|
|
983
|
+
}
|
|
954
984
|
const system = body.system;
|
|
955
|
-
|
|
956
|
-
|
|
985
|
+
// ── Detect CC identity marker anywhere in system ──
|
|
986
|
+
const hasCcMarker = system.some((b) => b &&
|
|
987
|
+
typeof b === "object" &&
|
|
988
|
+
b.type === "text" &&
|
|
989
|
+
typeof b.text === "string" &&
|
|
990
|
+
b.text.includes(CLAUDE_CODE_SYSTEM_PROMPT_LEAD));
|
|
991
|
+
// ── Detect billing header in system[0] ──
|
|
957
992
|
const firstBlock = system[0];
|
|
958
|
-
|
|
959
|
-
typeof firstBlock
|
|
960
|
-
firstBlock.type
|
|
961
|
-
typeof firstBlock.text
|
|
962
|
-
|
|
993
|
+
const hasBillingHeaderFirst = !!firstBlock &&
|
|
994
|
+
typeof firstBlock === "object" &&
|
|
995
|
+
firstBlock.type === "text" &&
|
|
996
|
+
typeof firstBlock.text === "string" &&
|
|
997
|
+
firstBlock.text.startsWith("x-anthropic-billing-header:");
|
|
998
|
+
// ── Build the attribution header (always recompute so cc_version + FP3
|
|
999
|
+
// match OUR fingerprint and the buyer's actual first user message) ──
|
|
1000
|
+
const firstUserMsg = extractFirstUserMessageText(body.messages);
|
|
1001
|
+
const freshHeader = buildClaudeAttributionHeader(firstUserMsg, fingerprint.cc_version, fingerprint.cc_entrypoint);
|
|
1002
|
+
// ── Inject CC marker if missing ──
|
|
1003
|
+
// Position: right after the billing header slot (idx 1), or right
|
|
1004
|
+
// after any buyer-prefixed system blocks (at head) if we're also
|
|
1005
|
+
// inserting the billing header.
|
|
1006
|
+
if (!hasCcMarker) {
|
|
1007
|
+
const markerBlock = {
|
|
1008
|
+
type: "text",
|
|
1009
|
+
text: `${CLAUDE_CODE_SYSTEM_PROMPT_LEAD}\n\n${RELAY_INSTRUCTIONS}`,
|
|
1010
|
+
cache_control: { type: "ephemeral" },
|
|
1011
|
+
};
|
|
1012
|
+
// Insert at index 1 (slot after billing header), or 0 if we'll be
|
|
1013
|
+
// unshifting the billing header next.
|
|
1014
|
+
if (hasBillingHeaderFirst) {
|
|
1015
|
+
system.splice(1, 0, markerBlock);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
system.unshift(markerBlock);
|
|
1019
|
+
}
|
|
963
1020
|
}
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
//
|
|
967
|
-
//
|
|
968
|
-
//
|
|
969
|
-
|
|
970
|
-
|
|
1021
|
+
// ── Update or inject billing header at index 0 ──
|
|
1022
|
+
if (hasBillingHeaderFirst) {
|
|
1023
|
+
// Rewrite in place so cc_version reflects OUR fingerprint, not the
|
|
1024
|
+
// buyer's original (which might have been from a different CLI
|
|
1025
|
+
// version than our pinned fingerprint).
|
|
1026
|
+
firstBlock.text = freshHeader;
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
system.unshift({ type: "text", text: freshHeader });
|
|
1030
|
+
}
|
|
1031
|
+
// ── Ensure tools array exists ──
|
|
1032
|
+
if (!Array.isArray(body.tools)) {
|
|
1033
|
+
body.tools = [];
|
|
1034
|
+
}
|
|
1035
|
+
// ── Inject thinking config if missing ──
|
|
1036
|
+
// Real CLI always sends this for Claude 4+ models; zero-thinking
|
|
1037
|
+
// accounts are a relay-farm tell. pickClaudeThinkingConfig picks the
|
|
1038
|
+
// right shape (adaptive for 4-6, enabled-with-budget for 4-5/haiku).
|
|
1039
|
+
if (!body.thinking || typeof body.thinking !== "object") {
|
|
1040
|
+
const rawMaxTokens = typeof body.max_tokens === "number" && body.max_tokens > 0
|
|
1041
|
+
? body.max_tokens
|
|
1042
|
+
: 4096;
|
|
1043
|
+
const { config, adjustedMaxTokens } = pickClaudeThinkingConfig(body.model ?? "", rawMaxTokens);
|
|
1044
|
+
if (config) {
|
|
1045
|
+
body.thinking = config;
|
|
1046
|
+
body.max_tokens = adjustedMaxTokens;
|
|
1047
|
+
}
|
|
971
1048
|
}
|
|
972
|
-
const firstUserMsg = extractFirstUserMessageText(body.messages);
|
|
973
|
-
const newHeader = buildClaudeAttributionHeader(firstUserMsg, fingerprint.cc_version, fingerprint.cc_entrypoint);
|
|
974
|
-
firstBlock.text = newHeader;
|
|
975
1049
|
}
|
|
976
1050
|
// Walk system text blocks and rewrite third-party identity sentences
|
|
977
1051
|
// (OpenCode, etc.) to the Claude Code banner. sub2api does the same thing
|
|
@@ -1025,7 +1099,7 @@ async function doCallClaudeApiPassthrough(opts) {
|
|
|
1025
1099
|
// Sanitize system: replace third-party identity sentences + sync
|
|
1026
1100
|
// billing header cc_version to match our pinned CLI version.
|
|
1027
1101
|
sanitizePassthroughSystemArray(body);
|
|
1028
|
-
|
|
1102
|
+
ensureClaudeCodeShell(body, fp);
|
|
1029
1103
|
// Clamp thinking.budget_tokens to Anthropic's minimum so buyer-chosen
|
|
1030
1104
|
// small budgets don't 400. If max_tokens < budget_tokens + 1, bump
|
|
1031
1105
|
// max_tokens too so the request stays valid.
|
|
@@ -77,9 +77,17 @@ const MAX_TRANSIENT_RETRIES = 2;
|
|
|
77
77
|
// Codex responses on small prompts come back in <10s; we give a generous
|
|
78
78
|
// ceiling to tolerate slow tokens without hanging the daemon forever.
|
|
79
79
|
const WS_OVERALL_TIMEOUT_MS = 180 * 1000;
|
|
80
|
-
// Default
|
|
81
|
-
//
|
|
80
|
+
// Default instructions for Codex template mode. Template mode flattens
|
|
81
|
+
// messages into a single prompt and drops buyer's tools — the "plain
|
|
82
|
+
// text only" hint aligns model behavior with what template can actually
|
|
83
|
+
// deliver.
|
|
82
84
|
const RELAY_INSTRUCTIONS = "You are a helpful AI assistant operating in relay mode. Respond to the user's message with plain text only. Be concise.";
|
|
85
|
+
// Neutral fallback for Codex passthrough mode when the buyer did NOT
|
|
86
|
+
// supply their own instructions. Unlike the template-mode string, this
|
|
87
|
+
// one does NOT forbid tool use — if the buyer sent a tools array we
|
|
88
|
+
// want the model to use them. Kept intentionally vague so it doesn't
|
|
89
|
+
// bias the model's behavior when the buyer's intent is unspecified.
|
|
90
|
+
const CODEX_PASSTHROUGH_FALLBACK_INSTRUCTIONS = "You are a helpful coding assistant. Use the available tools when appropriate to answer the user.";
|
|
83
91
|
// ── Proxy ──
|
|
84
92
|
//
|
|
85
93
|
// We configure the global undici dispatcher for the OAuth refresh fetch()
|
|
@@ -1051,10 +1059,13 @@ function buildCodexPassthroughFrame(clientBody, model, fingerprint, sessionId, t
|
|
|
1051
1059
|
if (frame.parallel_tool_calls === undefined) {
|
|
1052
1060
|
frame.parallel_tool_calls = false;
|
|
1053
1061
|
}
|
|
1054
|
-
// Instructions: if buyer didn't send one, fall back to
|
|
1055
|
-
//
|
|
1062
|
+
// Instructions: if buyer didn't send one, fall back to a neutral
|
|
1063
|
+
// tool-friendly default so the model still has guidance while not
|
|
1064
|
+
// forbidding tool use (unlike template mode's RELAY_INSTRUCTIONS,
|
|
1065
|
+
// which says "plain text only" — wrong fit for passthrough where
|
|
1066
|
+
// buyer's tools should actually be used).
|
|
1056
1067
|
if (typeof frame.instructions !== "string" || !frame.instructions) {
|
|
1057
|
-
frame.instructions =
|
|
1068
|
+
frame.instructions = CODEX_PASSTHROUGH_FALLBACK_INSTRUCTIONS;
|
|
1058
1069
|
}
|
|
1059
1070
|
if (warmup) {
|
|
1060
1071
|
// Real CLI's prewarm flow: first frame of each turn has generate:false.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmoney",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.11",
|
|
4
4
|
"description": "ClawMoney CLI -- Earn rewards with your AI agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"awal": "^2.2.0",
|
|
22
22
|
"@bnbot/cli": "^0.3.0",
|
|
23
|
+
"@clack/prompts": "^0.7.0",
|
|
23
24
|
"chalk": "^5.3.0",
|
|
24
25
|
"commander": "^12.0.0",
|
|
25
26
|
"ora": "^8.0.0",
|