clawmoney 0.17.45 → 0.17.47
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/dist/commands/relay.js +8 -6
- package/dist/relay/pricing.js +7 -0
- package/dist/relay/provider.js +35 -5
- package/dist/relay/upstream/chatgpt-web.d.ts +51 -0
- package/dist/relay/upstream/chatgpt-web.js +114 -0
- package/dist/task/daemon.js +93 -5
- package/dist/task/preflight.d.ts +47 -0
- package/dist/task/preflight.js +515 -0
- package/dist/task/skills/codex/_bnbot.js +3 -0
- package/dist/task/skills/index.d.ts +16 -0
- package/dist/task/skills/index.js +59 -1
- package/dist/task/skills/opencli/_bnbot.d.ts +5 -0
- package/dist/task/skills/opencli/_bnbot.js +21 -0
- package/dist/task/skills/opencli/linkedin-salesnav.d.ts +7 -0
- package/dist/task/skills/opencli/linkedin-salesnav.js +36 -0
- package/dist/task/skills/opencli/platforms.d.ts +4 -0
- package/dist/task/skills/opencli/platforms.js +28 -20
- package/dist/task/skills/opencli/scrapers.d.ts +6 -0
- package/dist/task/skills/opencli/scrapers.js +37 -0
- package/dist/task/skills/x/_mapper.d.ts +2 -0
- package/dist/task/skills/x/_mapper.js +6 -2
- package/package.json +1 -1
package/dist/commands/relay.js
CHANGED
|
@@ -11,7 +11,7 @@ const LOG_FILE = join(homedir(), ".clawmoney", "relay.log");
|
|
|
11
11
|
export async function relayRegisterCommand(options) {
|
|
12
12
|
const config = requireConfig();
|
|
13
13
|
// Validate CLI type
|
|
14
|
-
const validClis = ["claude", "codex", "gemini", "antigravity"];
|
|
14
|
+
const validClis = ["claude", "codex", "gemini", "antigravity", "chatgpt-web"];
|
|
15
15
|
if (!validClis.includes(options.cli)) {
|
|
16
16
|
console.error(chalk.red(`Invalid CLI type "${options.cli}". Must be one of: ${validClis.join(", ")}`));
|
|
17
17
|
process.exit(1);
|
|
@@ -36,14 +36,16 @@ export async function relayRegisterCommand(options) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
else {
|
|
39
|
-
|
|
39
|
+
// chatgpt-web drives the browser via opencli — there's no "chatgpt-web" binary.
|
|
40
|
+
const probeBin = options.cli === "chatgpt-web" ? "opencli" : options.cli;
|
|
41
|
+
const spinner = ora(`Checking if ${probeBin} is installed...`).start();
|
|
40
42
|
try {
|
|
41
|
-
execSync(`which ${
|
|
42
|
-
spinner.succeed(`${
|
|
43
|
+
execSync(`which ${probeBin}`, { stdio: "pipe" });
|
|
44
|
+
spinner.succeed(`${probeBin} is available`);
|
|
43
45
|
}
|
|
44
46
|
catch {
|
|
45
|
-
spinner.fail(chalk.red(`${
|
|
46
|
-
console.log(chalk.dim(` Make sure ${
|
|
47
|
+
spinner.fail(chalk.red(`${probeBin} is not installed or not in PATH`));
|
|
48
|
+
console.log(chalk.dim(` Make sure ${probeBin} CLI is installed and accessible.`));
|
|
47
49
|
process.exit(1);
|
|
48
50
|
}
|
|
49
51
|
}
|
package/dist/relay/pricing.js
CHANGED
|
@@ -32,6 +32,13 @@ export const API_PRICES = {
|
|
|
32
32
|
// 5 / 5.1 / 5.2-codex families were fully removed that day. Anything
|
|
33
33
|
// below this comment that's deprecated was removed from the CLI-side
|
|
34
34
|
// pricing table so `modelsForCli("codex")` no longer offers them.
|
|
35
|
+
//
|
|
36
|
+
// gpt-5.5 — current Codex CLI default (config.toml `model = "gpt-5.5"`)
|
|
37
|
+
// after the mid-2026 bump. Upgraded ChatGPT accounts now 404 the older
|
|
38
|
+
// 5.4/5.3-codex/5.2 ids with "not supported when using Codex with a
|
|
39
|
+
// ChatGPT account", so gpt-5.5 must be offered. Priced at the 5.4 tier
|
|
40
|
+
// pending an official LiteLLM entry.
|
|
41
|
+
"gpt-5.5": { input: 2.50, output: 15 },
|
|
35
42
|
"gpt-5.4": { input: 2.50, output: 15 },
|
|
36
43
|
"gpt-5.4-mini": { input: 0.75, output: 4.50 },
|
|
37
44
|
"gpt-5.3-codex": { input: 1.75, output: 14 },
|
package/dist/relay/provider.js
CHANGED
|
@@ -6,6 +6,7 @@ import { RelayWsClient } from "./ws-client.js";
|
|
|
6
6
|
import { callClaudeApi, callClaudeApiPassthrough, preflightClaudeApi, getRateGuardSnapshot as getClaudeRateGuardSnapshot, } from "./upstream/claude-api.js";
|
|
7
7
|
import { callCodexApi, callCodexApiPassthrough, preflightCodexApi, getRateGuardSnapshot as getCodexRateGuardSnapshot, } from "./upstream/codex-api.js";
|
|
8
8
|
import { callGeminiApi, preflightGeminiApi, getGeminiRateGuardSnapshot, } from "./upstream/gemini-api.js";
|
|
9
|
+
import { callChatGPTWeb } from "./upstream/chatgpt-web.js";
|
|
9
10
|
import { callAntigravityApi, preflightAntigravityApi, getAntigravityRateGuardSnapshot, } from "./upstream/antigravity-api.js";
|
|
10
11
|
import { callMinimaxApi, preflightMinimaxApi, getMinimaxRateGuardSnapshot, } from "./upstream/minimax-api.js";
|
|
11
12
|
import { callKimiCodingApi, preflightKimiCodingApi, getKimiCodingRateGuardSnapshot, } from "./upstream/kimi-coding-api.js";
|
|
@@ -221,6 +222,11 @@ function messagesToPrompt(messages) {
|
|
|
221
222
|
const AUTH_ERROR_THRESHOLD = 3;
|
|
222
223
|
const consecutiveAuthErrorsByCli = new Map();
|
|
223
224
|
const cliAuthDisabled = new Set();
|
|
225
|
+
// chatgpt-web: buyer sessions we've already opened a ChatGPT tab for. First
|
|
226
|
+
// turn opens a temporary chat; later turns continue in the same tab so context
|
|
227
|
+
// accumulates. Keyed by cli_session_id (falls back to request_id for stateless
|
|
228
|
+
// single-shots, which just get a fresh temporary chat each time).
|
|
229
|
+
const chatgptWebSessions = new Set();
|
|
224
230
|
const AUTH_BROKEN_PATTERNS = [
|
|
225
231
|
// Anthropic 403: OAuth authentication is currently not allowed for
|
|
226
232
|
// this organization. The new prod signal from 2026-04-15 incident.
|
|
@@ -313,11 +319,15 @@ async function executeRelayRequest(request, config, sendChunk) {
|
|
|
313
319
|
// antigravity → daily-cloudcode-pa). Each handler has its own
|
|
314
320
|
// fingerprint file and rate-guard instance.
|
|
315
321
|
if (cliType === "codex") {
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
322
|
+
// Passthrough ONLY when the Hub forwarded a real Responses API body —
|
|
323
|
+
// i.e. it carries an `input` array (the /v1/responses drop-in path).
|
|
324
|
+
// The OpenAI-compat /v1/chat/completions path forwards a body with
|
|
325
|
+
// `messages` (no `input`), so fall through to template mode, which
|
|
326
|
+
// builds a Responses request from the flattened `prompt`. Without this
|
|
327
|
+
// guard, chat/completions hit passthrough and 400'd with
|
|
328
|
+
// "Passthrough body missing `input` array".
|
|
329
|
+
const pb = request.passthrough_body;
|
|
330
|
+
if (pb && Array.isArray(pb.input) && pb.input.length > 0) {
|
|
321
331
|
parsed = await callCodexApiPassthrough({
|
|
322
332
|
clientBody: request.passthrough_body,
|
|
323
333
|
model,
|
|
@@ -340,6 +350,26 @@ async function executeRelayRequest(request, config, sendChunk) {
|
|
|
340
350
|
maxTokens: max_budget_usd ? undefined : 8192,
|
|
341
351
|
});
|
|
342
352
|
}
|
|
353
|
+
else if (cliType === "chatgpt-web") {
|
|
354
|
+
// Web send/read path — drive chatgpt.com via opencli (temporary chat)
|
|
355
|
+
// instead of the reverse-proxy API. Real-browser route, un-bannable.
|
|
356
|
+
// Per-buyer tab keyed by cli_session_id: first turn opens a temporary
|
|
357
|
+
// chat, later turns continue in the same tab so context accumulates.
|
|
358
|
+
const webKey = cliSessionId || request_id;
|
|
359
|
+
const seenBefore = chatgptWebSessions.has(webKey);
|
|
360
|
+
chatgptWebSessions.add(webKey);
|
|
361
|
+
// Image turn when the buyer asks for an image model (gpt-image-*) — that
|
|
362
|
+
// forces a regular chat + image grab. Text models stay on temporary chat.
|
|
363
|
+
const wantsImage = /image/i.test(model);
|
|
364
|
+
parsed = await callChatGPTWeb({
|
|
365
|
+
prompt,
|
|
366
|
+
model,
|
|
367
|
+
timeout: wantsImage ? 200 : 120,
|
|
368
|
+
sessionKey: webKey,
|
|
369
|
+
continueChat: seenBefore,
|
|
370
|
+
imageOut: wantsImage,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
343
373
|
else if (cliType === "antigravity") {
|
|
344
374
|
parsed = await callAntigravityApi({
|
|
345
375
|
prompt,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chatgpt-web upstream — serve a relay request by driving the ChatGPT *website*
|
|
3
|
+
* (not the reverse-proxy API). Sends the prompt into a ChatGPT temporary chat
|
|
4
|
+
* via opencli's CDP browser automation, waits for the reply, reads it back.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists alongside the codex reverse-proxy path:
|
|
7
|
+
* - It's a real person typing in chatgpt.com → OpenAI can't tell it apart from
|
|
8
|
+
* normal use → effectively un-bannable (the reverse-proxy路径 mimics the
|
|
9
|
+
* Codex CLI fingerprint, which is safe but not bullet-proof at scale).
|
|
10
|
+
* - Temporary chat → no history pollution, clean per-session isolation, not
|
|
11
|
+
* used for training.
|
|
12
|
+
* - Trade-off: slower (seconds, UI render) and not token-streamed. Fine for
|
|
13
|
+
* conversation; the buyer still gets a standard reply.
|
|
14
|
+
*
|
|
15
|
+
* Requires the provider machine to have `@jackwener/opencli` installed (with the
|
|
16
|
+
* `chatgpt ask --temporary` support) and a logged-in ChatGPT session in the
|
|
17
|
+
* opencli-controlled Chrome. Point OPENCLI_BIN at a custom binary for local
|
|
18
|
+
* source runs.
|
|
19
|
+
*/
|
|
20
|
+
import type { ParsedOutput } from "../types.js";
|
|
21
|
+
export interface ChatGPTWebOptions {
|
|
22
|
+
prompt: string;
|
|
23
|
+
model: string;
|
|
24
|
+
/** Max seconds to wait for the assistant reply. */
|
|
25
|
+
timeout?: number;
|
|
26
|
+
/** Continue an existing /c/<id> conversation (multi-turn, non-temporary). */
|
|
27
|
+
conversationId?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Continue the CURRENT chat in place (no --new/--temporary). Used for
|
|
30
|
+
* stateful multi-turn inside a temporary chat: the first turn opens the
|
|
31
|
+
* temporary chat, later turns just send into the same tab so ChatGPT
|
|
32
|
+
* accumulates context. Temporary chats have no /c/<id> to resume by URL,
|
|
33
|
+
* so this in-place continuation is the only way to keep their context.
|
|
34
|
+
*/
|
|
35
|
+
continueChat?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Per-buyer browser session key → its own ChatGPT tab. Same key reuses the
|
|
38
|
+
* same tab (multi-turn context); different keys run concurrently in separate
|
|
39
|
+
* tabs. Maps to opencli `--session <key>`.
|
|
40
|
+
*/
|
|
41
|
+
sessionKey?: string;
|
|
42
|
+
/**
|
|
43
|
+
* This turn is expected to produce an image (generate/edit). Forces a regular
|
|
44
|
+
* chat (ChatGPT temporary chat BLOCKS image generation) and waits for + grabs
|
|
45
|
+
* the image. Maps to opencli `--image-out` + `--new` (first turn).
|
|
46
|
+
*/
|
|
47
|
+
imageOut?: boolean;
|
|
48
|
+
/** Local image paths to upload before the prompt (edit the buyer's own image). */
|
|
49
|
+
imagePaths?: string[];
|
|
50
|
+
}
|
|
51
|
+
export declare function callChatGPTWeb(opts: ChatGPTWebOptions): Promise<ParsedOutput>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chatgpt-web upstream — serve a relay request by driving the ChatGPT *website*
|
|
3
|
+
* (not the reverse-proxy API). Sends the prompt into a ChatGPT temporary chat
|
|
4
|
+
* via opencli's CDP browser automation, waits for the reply, reads it back.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists alongside the codex reverse-proxy path:
|
|
7
|
+
* - It's a real person typing in chatgpt.com → OpenAI can't tell it apart from
|
|
8
|
+
* normal use → effectively un-bannable (the reverse-proxy路径 mimics the
|
|
9
|
+
* Codex CLI fingerprint, which is safe but not bullet-proof at scale).
|
|
10
|
+
* - Temporary chat → no history pollution, clean per-session isolation, not
|
|
11
|
+
* used for training.
|
|
12
|
+
* - Trade-off: slower (seconds, UI render) and not token-streamed. Fine for
|
|
13
|
+
* conversation; the buyer still gets a standard reply.
|
|
14
|
+
*
|
|
15
|
+
* Requires the provider machine to have `@jackwener/opencli` installed (with the
|
|
16
|
+
* `chatgpt ask --temporary` support) and a logged-in ChatGPT session in the
|
|
17
|
+
* opencli-controlled Chrome. Point OPENCLI_BIN at a custom binary for local
|
|
18
|
+
* source runs.
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
21
|
+
import { promisify } from "node:util";
|
|
22
|
+
const execFileP = promisify(execFile);
|
|
23
|
+
const OPENCLI_BIN = process.env.OPENCLI_BIN || "opencli";
|
|
24
|
+
/** Roughly 4 chars per token — web send/read has no real token counts, so we
|
|
25
|
+
* estimate for billing parity with the API paths. */
|
|
26
|
+
function estimateTokens(text) {
|
|
27
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
28
|
+
}
|
|
29
|
+
export async function callChatGPTWeb(opts) {
|
|
30
|
+
const timeout = opts.timeout ?? 120;
|
|
31
|
+
const args = ["chatgpt", "ask", opts.prompt, "--timeout", String(timeout), "-f", "json"];
|
|
32
|
+
// Turn routing:
|
|
33
|
+
// - continueChat → no flag; opencli's ensureOnChatGPT stays on the current
|
|
34
|
+
// page, so a temporary chat continues in place and keeps its context.
|
|
35
|
+
// - conversationId → resume a saved /c/<id> conversation.
|
|
36
|
+
// - otherwise → open a fresh temporary chat (first turn / single-shot).
|
|
37
|
+
if (opts.continueChat) {
|
|
38
|
+
// intentionally no flag — continue the current chat in place.
|
|
39
|
+
}
|
|
40
|
+
else if (opts.conversationId) {
|
|
41
|
+
args.push("--conversation", opts.conversationId);
|
|
42
|
+
}
|
|
43
|
+
else if (opts.imageOut) {
|
|
44
|
+
// Image generation is blocked in temporary chats → use a regular new chat.
|
|
45
|
+
args.push("--new");
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
args.push("--temporary");
|
|
49
|
+
}
|
|
50
|
+
if (opts.imageOut) {
|
|
51
|
+
args.push("--image-out");
|
|
52
|
+
}
|
|
53
|
+
if (opts.imagePaths?.length) {
|
|
54
|
+
args.push("--image", opts.imagePaths.join(","));
|
|
55
|
+
}
|
|
56
|
+
// Per-buyer tab isolation: same key → same tab (context kept), different
|
|
57
|
+
// keys → concurrent tabs.
|
|
58
|
+
if (opts.sessionKey) {
|
|
59
|
+
args.push("--session", opts.sessionKey);
|
|
60
|
+
}
|
|
61
|
+
let stdout = "";
|
|
62
|
+
try {
|
|
63
|
+
const result = await execFileP(OPENCLI_BIN, args, {
|
|
64
|
+
timeout: (timeout + 30) * 1000,
|
|
65
|
+
maxBuffer: 96 * 1024 * 1024, // base64 images can be several MB each
|
|
66
|
+
env: { ...process.env },
|
|
67
|
+
});
|
|
68
|
+
stdout = result.stdout;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const e = err;
|
|
72
|
+
const tail = `${e.stdout ?? ""}${e.stderr ?? ""}`.slice(0, 400);
|
|
73
|
+
throw new Error(`opencli chatgpt ask failed: ${e.message ?? String(err)} ${tail}`);
|
|
74
|
+
}
|
|
75
|
+
// opencli `-f json` prints a JSON array; tolerate any leading banner lines.
|
|
76
|
+
const start = stdout.indexOf("[");
|
|
77
|
+
if (start < 0) {
|
|
78
|
+
throw new Error(`opencli chatgpt ask: no JSON in output: ${stdout.slice(0, 300)}`);
|
|
79
|
+
}
|
|
80
|
+
let rows;
|
|
81
|
+
try {
|
|
82
|
+
rows = JSON.parse(stdout.slice(start));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
throw new Error(`opencli chatgpt ask: bad JSON: ${stdout.slice(start, start + 300)}`);
|
|
86
|
+
}
|
|
87
|
+
const response = String(rows?.[0]?.response ?? "").trim();
|
|
88
|
+
const images = Array.isArray(rows?.[0]?.images) ? rows[0].images : [];
|
|
89
|
+
if (!response && !images.length) {
|
|
90
|
+
throw new Error(`opencli chatgpt ask empty (stdout=${stdout.length}b, keys=[${Object.keys(rows?.[0] ?? {}).join(",")}], imagesType=${typeof rows?.[0]?.images})`);
|
|
91
|
+
}
|
|
92
|
+
// Carry images back inside the chat.completion content as markdown data
|
|
93
|
+
// URLs — the buyer's front-end renders them directly. (Image turns often
|
|
94
|
+
// have little/no text, so the image IS the answer.)
|
|
95
|
+
let text = response;
|
|
96
|
+
if (images.length) {
|
|
97
|
+
const md = images.map((img, i) => ``).join("\n");
|
|
98
|
+
text = text ? `${text}\n\n${md}` : md;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
text,
|
|
102
|
+
sessionId: String(rows?.[0]?.conversationId ?? ""),
|
|
103
|
+
usage: {
|
|
104
|
+
input_tokens: estimateTokens(opts.prompt),
|
|
105
|
+
// Don't token-count the base64 image (it'd be ~375k "tokens"); bill each
|
|
106
|
+
// generated image as a flat token block instead.
|
|
107
|
+
output_tokens: estimateTokens(response) + images.length * 800,
|
|
108
|
+
cache_creation_tokens: 0,
|
|
109
|
+
cache_read_tokens: 0,
|
|
110
|
+
},
|
|
111
|
+
model: opts.model,
|
|
112
|
+
costUsd: 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/dist/task/daemon.js
CHANGED
|
@@ -24,6 +24,7 @@ import { readFileSync, writeFileSync, unlinkSync, existsSync, appendFileSync, mk
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
27
28
|
import YAML from "yaml";
|
|
28
29
|
const TASK_LOG_FILE = join(homedir(), ".clawmoney", "task.log");
|
|
29
30
|
function tsLine(level, msg) {
|
|
@@ -47,10 +48,12 @@ function installFileLogger() {
|
|
|
47
48
|
console.error = (...args) => logToFile("ERROR", ...args);
|
|
48
49
|
}
|
|
49
50
|
import { TaskWsClient } from "./ws-client.js";
|
|
50
|
-
import { getSkill, listSkills } from "./skills/index.js";
|
|
51
|
+
import { getSkill, listSkills, defaultAdvertiseSkills } from "./skills/index.js";
|
|
52
|
+
import { runPreflight, writePreflightReport } from "./preflight.js";
|
|
51
53
|
const CONFIG_DIR = join(homedir(), ".clawmoney");
|
|
52
54
|
const CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
|
|
53
55
|
const PID_FILE = join(CONFIG_DIR, "task.pid");
|
|
56
|
+
const TASK_STATE_FILE = join(CONFIG_DIR, "task-state.json");
|
|
54
57
|
function loadYamlConfig() {
|
|
55
58
|
if (!existsSync(CONFIG_FILE))
|
|
56
59
|
return {};
|
|
@@ -75,7 +78,10 @@ function loadConfig() {
|
|
|
75
78
|
.split(",")
|
|
76
79
|
.map((s) => s.trim())
|
|
77
80
|
.filter(Boolean);
|
|
78
|
-
|
|
81
|
+
// Default advertise set excludes skills the SpareAPI backend now fetches
|
|
82
|
+
// directly (YC / IndieHackers / Hacker News); an explicit SKILLS= can still
|
|
83
|
+
// opt into them since `supported` below is the full registry.
|
|
84
|
+
const skills = requested.length > 0 ? requested : defaultAdvertiseSkills();
|
|
79
85
|
// Sanity-check: drop anything we can't actually serve so we don't
|
|
80
86
|
// advertise dead skills to the hub.
|
|
81
87
|
const supported = new Set(listSkills());
|
|
@@ -128,6 +134,26 @@ function removePid() {
|
|
|
128
134
|
// ignore
|
|
129
135
|
}
|
|
130
136
|
}
|
|
137
|
+
// Lifecycle phase the desktop app reads to show "service running, checking
|
|
138
|
+
// logins…" during the (possibly slow first-run) preflight, vs "online" once
|
|
139
|
+
// the hub connection is up. Distinct from the pid file, which only says the
|
|
140
|
+
// process exists.
|
|
141
|
+
function writeTaskState(phase) {
|
|
142
|
+
try {
|
|
143
|
+
writeFileSync(TASK_STATE_FILE, JSON.stringify({ phase, pid: process.pid, ts: new Date().toISOString() }), "utf-8");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* best effort */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function clearTaskState() {
|
|
150
|
+
try {
|
|
151
|
+
unlinkSync(TASK_STATE_FILE);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// ignore
|
|
155
|
+
}
|
|
156
|
+
}
|
|
131
157
|
async function handleTaskRequest(ws, req) {
|
|
132
158
|
const startedAtMs = Date.now();
|
|
133
159
|
const handler = getSkill(req.skill_id);
|
|
@@ -186,10 +212,47 @@ async function handleTaskRequest(ws, req) {
|
|
|
186
212
|
clearTimeout(timer);
|
|
187
213
|
}
|
|
188
214
|
}
|
|
189
|
-
function
|
|
215
|
+
function applySystemProxy() {
|
|
216
|
+
// bnbot's publicScrapers honor https_proxy/http_proxy/all_proxy to reach
|
|
217
|
+
// censored public APIs (wiki/google/...). The daemon's own WS uses the
|
|
218
|
+
// `ws` lib which ignores these env vars, so this only routes child bnbot
|
|
219
|
+
// fetches through the proxy. Auto-detect the macOS system proxy when the
|
|
220
|
+
// operator hasn't set one explicitly.
|
|
221
|
+
if (process.env.https_proxy || process.env.HTTPS_PROXY ||
|
|
222
|
+
process.env.all_proxy || process.env.ALL_PROXY) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (process.platform !== "darwin")
|
|
226
|
+
return;
|
|
227
|
+
try {
|
|
228
|
+
const out = execFileSync("scutil", ["--proxy"], { encoding: "utf8", timeout: 3000 });
|
|
229
|
+
const get = (key) => out.match(new RegExp(`\\b${key}\\s*:\\s*(\\S+)`))?.[1];
|
|
230
|
+
let url;
|
|
231
|
+
if (get("HTTPSEnable") === "1" && get("HTTPSProxy")) {
|
|
232
|
+
url = `http://${get("HTTPSProxy")}:${get("HTTPSPort") ?? "0"}`;
|
|
233
|
+
}
|
|
234
|
+
else if (get("HTTPEnable") === "1" && get("HTTPProxy")) {
|
|
235
|
+
url = `http://${get("HTTPProxy")}:${get("HTTPPort") ?? "0"}`;
|
|
236
|
+
}
|
|
237
|
+
else if (get("SOCKSEnable") === "1" && get("SOCKSProxy")) {
|
|
238
|
+
url = `socks5://${get("SOCKSProxy")}:${get("SOCKSPort") ?? "0"}`;
|
|
239
|
+
}
|
|
240
|
+
if (url) {
|
|
241
|
+
process.env.https_proxy = url;
|
|
242
|
+
process.env.http_proxy = url;
|
|
243
|
+
process.env.all_proxy = url;
|
|
244
|
+
console.log(`[task] auto-detected system proxy ${url} (routing skill fetches through it)`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
console.error(`[task] system proxy detection skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function main() {
|
|
190
252
|
// Daemon was started as a script (stdio:"ignore"); from here on, all
|
|
191
253
|
// log lines should land in ~/.clawmoney/task.log.
|
|
192
254
|
installFileLogger();
|
|
255
|
+
applySystemProxy();
|
|
193
256
|
const existing = readTaskPid();
|
|
194
257
|
if (existing !== null && isPidAlive(existing)) {
|
|
195
258
|
console.error(`[task] already running (PID ${existing})`);
|
|
@@ -200,12 +263,32 @@ function main() {
|
|
|
200
263
|
console.error("[task] api_key missing — set API_KEY env or run `clawmoney setup`");
|
|
201
264
|
process.exit(1);
|
|
202
265
|
}
|
|
266
|
+
// Mark the service up the moment config checks out — BEFORE the (possibly
|
|
267
|
+
// slow first-run) preflight. Two reasons: the app can show "service running,
|
|
268
|
+
// checking logins…" instead of "not started", and the app's self-heal keys
|
|
269
|
+
// off the pid file, so writing it now stops it from re-spawning us mid-probe.
|
|
270
|
+
writePid();
|
|
271
|
+
writeTaskState("probing");
|
|
272
|
+
// Preflight: probe each platform's external dependency and drop the ones
|
|
273
|
+
// that would fail every task (Codex not installed, X not logged in, the
|
|
274
|
+
// Chrome extension down, …). Writes ~/.clawmoney/preflight.json so the
|
|
275
|
+
// desktop app can surface a home-screen notice. Re-runs every start, so
|
|
276
|
+
// fixing the dependency recovers the platform on the next boot.
|
|
277
|
+
console.log(`[task] preflight on ${config.skills.length} skills…`);
|
|
278
|
+
const { skills: keptSkills, report } = await runPreflight(config.skills);
|
|
279
|
+
writePreflightReport(report);
|
|
280
|
+
config.skills = keptSkills;
|
|
281
|
+
if (report.summary.droppedSkills > 0) {
|
|
282
|
+
console.warn(`[task] preflight dropped ${report.summary.droppedSkills} skill(s) across ` +
|
|
283
|
+
`${report.summary.failed} platform(s): ${report.dropped.join(", ")}`);
|
|
284
|
+
}
|
|
203
285
|
console.log(`[task] starting daemon hub=${config.hub_url} skills=[${config.skills.join(",")}]`);
|
|
204
286
|
if (config.skills.length === 0) {
|
|
205
287
|
console.error("[task] no skills to advertise — refusing to start");
|
|
288
|
+
removePid();
|
|
289
|
+
clearTaskState();
|
|
206
290
|
process.exit(1);
|
|
207
291
|
}
|
|
208
|
-
writePid();
|
|
209
292
|
// Belt-and-suspenders: even if every other handle (WS, heartbeat,
|
|
210
293
|
// reconnect timer) somehow goes away simultaneously, this interval
|
|
211
294
|
// is unref-less and ref-counted into the event loop, so the daemon
|
|
@@ -225,6 +308,7 @@ function main() {
|
|
|
225
308
|
switch (frame.event) {
|
|
226
309
|
case "connected":
|
|
227
310
|
console.log(`[task] connected agent_id=${frame.agent_id} name="${frame.agent_name}"`);
|
|
311
|
+
writeTaskState("online");
|
|
228
312
|
break;
|
|
229
313
|
case "task_request":
|
|
230
314
|
void handleTaskRequest(ws, frame);
|
|
@@ -241,6 +325,7 @@ function main() {
|
|
|
241
325
|
console.log(`[task] ${signal} — shutting down`);
|
|
242
326
|
ws.stop();
|
|
243
327
|
removePid();
|
|
328
|
+
clearTaskState();
|
|
244
329
|
setTimeout(() => process.exit(0), 200);
|
|
245
330
|
};
|
|
246
331
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
@@ -251,5 +336,8 @@ function main() {
|
|
|
251
336
|
// the module is imported (e.g. by src/commands/task.ts which only wants
|
|
252
337
|
// to use readTaskPid / isPidAlive helpers).
|
|
253
338
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
254
|
-
main()
|
|
339
|
+
main().catch((err) => {
|
|
340
|
+
console.error(`[task] fatal: ${err instanceof Error ? err.stack : String(err)}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
255
343
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type PreflightStatus = "ok" | "failed" | "unknown";
|
|
2
|
+
export interface PlatformPreflight {
|
|
3
|
+
/** Display name surfaced in the app notice. */
|
|
4
|
+
label: string;
|
|
5
|
+
/** "local-app" | "web-login" | "social-login" — drives the app hint copy. */
|
|
6
|
+
category: string;
|
|
7
|
+
status: PreflightStatus;
|
|
8
|
+
/** How many advertised skills sit under this platform prefix. */
|
|
9
|
+
skills: number;
|
|
10
|
+
/** Human-readable failure reason (only when failed/unknown). */
|
|
11
|
+
reason?: string;
|
|
12
|
+
/** Actionable fix shown to the operator. */
|
|
13
|
+
hint?: string;
|
|
14
|
+
/** false = nothing the operator can do (e.g. upstream app dropped the
|
|
15
|
+
* capability); the app keeps it off the notice banner. Default true. */
|
|
16
|
+
actionable?: boolean;
|
|
17
|
+
/** Login page the app can open for the operator to fix a logged-out
|
|
18
|
+
* browser platform in one click. */
|
|
19
|
+
loginUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface PreflightReport {
|
|
22
|
+
ts: string;
|
|
23
|
+
/** false when any probed platform failed. */
|
|
24
|
+
ok: boolean;
|
|
25
|
+
summary: {
|
|
26
|
+
checked: number;
|
|
27
|
+
failed: number;
|
|
28
|
+
droppedSkills: number;
|
|
29
|
+
};
|
|
30
|
+
/** Keyed by platform prefix (the segment before the first dot in a skill_id). */
|
|
31
|
+
platforms: Record<string, PlatformPreflight>;
|
|
32
|
+
/** skill_ids removed from the advertise set. */
|
|
33
|
+
dropped: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface PreflightOutcome {
|
|
36
|
+
/** Skills that passed (or were never probed) — the advertise set. */
|
|
37
|
+
skills: string[];
|
|
38
|
+
report: PreflightReport;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Probe every platform that has a registered dependency, drop the failures
|
|
42
|
+
* from the advertise set, and build the report. `unknown` verdicts are
|
|
43
|
+
* kept advertising (conservative) but recorded so the app can hint.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runPreflight(skills: string[]): Promise<PreflightOutcome>;
|
|
46
|
+
/** Persist the verdict for the desktop app to read on its next dashboard load. */
|
|
47
|
+
export declare function writePreflightReport(report: PreflightReport): void;
|