clawmoney 0.7.2 → 0.8.1

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.
@@ -16,9 +16,9 @@ function truncate(str, maxLen) {
16
16
  return '-';
17
17
  return str.length > maxLen ? str.slice(0, maxLen - 1) + '...' : str;
18
18
  }
19
- function printBoostTable(tasks) {
19
+ function printEngageTable(tasks) {
20
20
  if (tasks.length === 0) {
21
- console.log(chalk.dim(' No boost tasks found.'));
21
+ console.log(chalk.dim(' No engage tasks found.'));
22
22
  return;
23
23
  }
24
24
  // Header
@@ -34,9 +34,9 @@ function printBoostTable(tasks) {
34
34
  console.log(` ${chalk.cyan(id.padEnd(8))} ${title.padEnd(30)} ${chalk.green(reward.padEnd(10))} ${budget.padEnd(10)} ${joined.padEnd(8)} ${status.padEnd(10)}`);
35
35
  }
36
36
  }
37
- function printHireTable(tasks) {
37
+ function printPromoteTable(tasks) {
38
38
  if (tasks.length === 0) {
39
- console.log(chalk.dim(' No hire tasks found.'));
39
+ console.log(chalk.dim(' No promote tasks found.'));
40
40
  return;
41
41
  }
42
42
  // Header
@@ -55,46 +55,46 @@ function printHireTable(tasks) {
55
55
  export async function browseCommand(options) {
56
56
  const config = loadConfig();
57
57
  const apiKey = config?.api_key;
58
- const taskType = options.type || 'boost';
58
+ const taskType = options.type || 'engage';
59
59
  const status = options.status || 'active';
60
60
  const limit = parseInt(options.limit || '10', 10);
61
61
  console.log('');
62
- if (taskType === 'hire' || taskType === 'all') {
63
- const hireSpinner = ora('Fetching hire tasks...').start();
62
+ if (taskType === 'promote' || taskType === 'all') {
63
+ const promoteSpinner = ora('Fetching promote tasks...').start();
64
64
  try {
65
- const resp = await apiGet(`/api/v1/hire/?status=${status}&sort_by=total_budget&sort_order=desc&limit=${limit}`, apiKey);
65
+ const resp = await apiGet(`/api/v1/promote/?status=${status}&sort_by=total_budget&sort_order=desc&limit=${limit}`, apiKey);
66
66
  if (!resp.ok) {
67
- hireSpinner.fail(`Failed to fetch hire tasks (${resp.status})`);
67
+ promoteSpinner.fail(`Failed to fetch promote tasks (${resp.status})`);
68
68
  }
69
69
  else {
70
70
  const body = resp.data;
71
71
  const tasks = (body.data || (Array.isArray(body) ? body : []));
72
- hireSpinner.succeed(`Hire Tasks (${tasks.length})`);
73
- printHireTable(tasks);
72
+ promoteSpinner.succeed(`Promote Tasks (${tasks.length})`);
73
+ printPromoteTable(tasks);
74
74
  }
75
75
  }
76
76
  catch (err) {
77
- hireSpinner.fail('Failed to fetch hire tasks');
77
+ promoteSpinner.fail('Failed to fetch promote tasks');
78
78
  console.error(chalk.red(err.message));
79
79
  }
80
80
  console.log('');
81
81
  }
82
- if (taskType === 'boost' || taskType === 'all') {
83
- const boostSpinner = ora('Fetching boost tasks...').start();
82
+ if (taskType === 'engage' || taskType === 'all') {
83
+ const engageSpinner = ora('Fetching engage tasks...').start();
84
84
  try {
85
- const resp = await apiGet(`/api/v1/boost/?status=${status}&limit=${limit}`, apiKey);
85
+ const resp = await apiGet(`/api/v1/engage/?status=${status}&limit=${limit}`, apiKey);
86
86
  if (!resp.ok) {
87
- boostSpinner.fail(`Failed to fetch boost tasks (${resp.status})`);
87
+ engageSpinner.fail(`Failed to fetch engage tasks (${resp.status})`);
88
88
  }
89
89
  else {
90
90
  const body = resp.data;
91
91
  const tasks = (body.data || (Array.isArray(body) ? body : []));
92
- boostSpinner.succeed(`Boost Tasks (${tasks.length})`);
93
- printBoostTable(tasks);
92
+ engageSpinner.succeed(`Engage Tasks (${tasks.length})`);
93
+ printEngageTable(tasks);
94
94
  }
95
95
  }
96
96
  catch (err) {
97
- boostSpinner.fail('Failed to fetch boost tasks');
97
+ engageSpinner.fail('Failed to fetch engage tasks');
98
98
  console.error(chalk.red(err.message));
99
99
  }
100
100
  console.log('');
@@ -0,0 +1,14 @@
1
+ export declare function hubStartCommand(options: {
2
+ cli?: string;
3
+ }): Promise<void>;
4
+ export declare function hubStopCommand(): Promise<void>;
5
+ export declare function hubStatusCommand(): Promise<void>;
6
+ interface RegisterOptions {
7
+ name: string;
8
+ category: string;
9
+ description: string;
10
+ price: string;
11
+ }
12
+ export declare function hubRegisterCommand(options: RegisterOptions): Promise<void>;
13
+ export declare function hubSkillsCommand(): Promise<void>;
14
+ export {};
@@ -0,0 +1,164 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import chalk from "chalk";
5
+ import ora from "ora";
6
+ import { requireConfig } from "../utils/config.js";
7
+ import { apiGet, apiPost } from "../utils/api.js";
8
+ import { readPid, isPidAlive, removePid } from "../hub/provider.js";
9
+ const LOG_FILE = join(homedir(), ".clawmoney", "provider.log");
10
+ // ── hub start ──
11
+ export async function hubStartCommand(options) {
12
+ const config = requireConfig();
13
+ // Check if already running
14
+ const existingPid = readPid();
15
+ if (existingPid && isPidAlive(existingPid)) {
16
+ console.log(chalk.yellow(`Hub Provider is already running (PID ${existingPid}). Use "clawmoney hub stop" first.`));
17
+ return;
18
+ }
19
+ const spinner = ora("Starting Hub Provider...").start();
20
+ try {
21
+ // Spawn the daemon process using node with a special env flag
22
+ // The daemon will re-import provider.ts and call runProvider()
23
+ const daemonScript = join(import.meta.url.replace("file://", "").replace(/\/commands\/hub\.js$/, ""), "hub", "daemon.js");
24
+ const args = [daemonScript];
25
+ if (options.cli) {
26
+ args.push("--cli", options.cli);
27
+ }
28
+ const child = spawn(process.execPath, args, {
29
+ stdio: "ignore",
30
+ detached: true,
31
+ env: {
32
+ ...process.env,
33
+ CLAWMONEY_DAEMON: "1",
34
+ },
35
+ });
36
+ child.unref();
37
+ // Give the daemon a moment to start and write PID
38
+ await new Promise((resolve) => setTimeout(resolve, 1000));
39
+ const pid = readPid();
40
+ if (pid && isPidAlive(pid)) {
41
+ spinner.succeed(chalk.green(`Hub Provider started (PID ${pid})`));
42
+ console.log(chalk.dim(` Log file: ${LOG_FILE}`));
43
+ console.log(chalk.dim(` CLI command: ${options.cli || "claude"}`));
44
+ console.log(chalk.dim(` API key: ${config.api_key.slice(0, 8)}...`));
45
+ }
46
+ else {
47
+ spinner.fail(chalk.red("Failed to start Hub Provider. Check logs at: " + LOG_FILE));
48
+ process.exit(1);
49
+ }
50
+ }
51
+ catch (err) {
52
+ spinner.fail(chalk.red("Failed to start Hub Provider"));
53
+ throw err;
54
+ }
55
+ }
56
+ // ── hub stop ──
57
+ export async function hubStopCommand() {
58
+ const pid = readPid();
59
+ if (!pid) {
60
+ console.log(chalk.dim("Hub Provider is not running (no PID file)."));
61
+ return;
62
+ }
63
+ if (!isPidAlive(pid)) {
64
+ console.log(chalk.dim(`Hub Provider PID ${pid} is not alive. Cleaning up PID file.`));
65
+ removePid();
66
+ return;
67
+ }
68
+ try {
69
+ process.kill(pid, "SIGTERM");
70
+ console.log(chalk.green(`Hub Provider stopped (PID ${pid}).`));
71
+ }
72
+ catch (err) {
73
+ console.error(chalk.red(`Failed to stop process ${pid}:`), err.message);
74
+ }
75
+ // Wait briefly for cleanup, then ensure PID file is removed
76
+ await new Promise((resolve) => setTimeout(resolve, 500));
77
+ removePid();
78
+ }
79
+ // ── hub status ──
80
+ export async function hubStatusCommand() {
81
+ const pid = readPid();
82
+ if (!pid) {
83
+ console.log(chalk.dim("Hub Provider is not running."));
84
+ return;
85
+ }
86
+ if (isPidAlive(pid)) {
87
+ console.log(chalk.green(`Hub Provider is running (PID ${pid}).`));
88
+ console.log(chalk.dim(` Log file: ${LOG_FILE}`));
89
+ }
90
+ else {
91
+ console.log(chalk.yellow(`Hub Provider PID ${pid} is not alive (stale PID file).`));
92
+ removePid();
93
+ }
94
+ }
95
+ export async function hubRegisterCommand(options) {
96
+ const config = requireConfig();
97
+ const price = parseFloat(options.price);
98
+ if (isNaN(price) || price < 0) {
99
+ console.error(chalk.red("Invalid price. Must be a non-negative number."));
100
+ process.exit(1);
101
+ }
102
+ const spinner = ora("Registering skill...").start();
103
+ try {
104
+ const resp = await apiPost("/api/v1/hub/skills", {
105
+ name: options.name,
106
+ category: options.category,
107
+ description: options.description,
108
+ price_per_call: price,
109
+ }, config.api_key);
110
+ if (!resp.ok) {
111
+ const detail = resp.data && typeof resp.data === "object" && "detail" in resp.data
112
+ ? resp.data.detail
113
+ : JSON.stringify(resp.data);
114
+ spinner.fail(chalk.red(`Failed to register skill (${resp.status}): ${detail}`));
115
+ process.exit(1);
116
+ }
117
+ spinner.succeed(chalk.green("Skill registered successfully!"));
118
+ console.log("");
119
+ console.log(` ${chalk.bold("Name:")} ${options.name}`);
120
+ console.log(` ${chalk.bold("Category:")} ${options.category}`);
121
+ console.log(` ${chalk.bold("Price:")} $${price.toFixed(2)}/call`);
122
+ }
123
+ catch (err) {
124
+ spinner.fail(chalk.red("Failed to register skill"));
125
+ throw err;
126
+ }
127
+ }
128
+ export async function hubSkillsCommand() {
129
+ const config = requireConfig();
130
+ const spinner = ora("Fetching skills...").start();
131
+ try {
132
+ const resp = await apiGet("/api/v1/hub/skills/mine", config.api_key);
133
+ if (!resp.ok) {
134
+ spinner.fail(chalk.red(`Failed to fetch skills (${resp.status})`));
135
+ process.exit(1);
136
+ }
137
+ const skills = Array.isArray(resp.data)
138
+ ? resp.data
139
+ : resp.data.data ?? [];
140
+ spinner.succeed(`My Skills (${skills.length})`);
141
+ console.log("");
142
+ if (skills.length === 0) {
143
+ console.log(chalk.dim(' No skills registered. Use "clawmoney hub register" to add one.'));
144
+ return;
145
+ }
146
+ // Table header
147
+ console.log(chalk.bold(` ${"Name".padEnd(20)} ${"Category".padEnd(20)} ${"Price".padEnd(10)} ${"Calls".padEnd(8)} ${"Status".padEnd(10)}`));
148
+ console.log(chalk.dim(" " + "-".repeat(70)));
149
+ for (const skill of skills) {
150
+ const name = (skill.name ?? "-").slice(0, 19);
151
+ const category = (skill.category ?? "-").slice(0, 19);
152
+ const price = skill.price_per_call !== undefined
153
+ ? `$${skill.price_per_call.toFixed(2)}`
154
+ : "-";
155
+ const calls = String(skill.call_count ?? "-");
156
+ const status = skill.status ?? "-";
157
+ console.log(` ${chalk.cyan(name.padEnd(20))} ${category.padEnd(20)} ${chalk.green(price.padEnd(10))} ${calls.padEnd(8)} ${status.padEnd(10)}`);
158
+ }
159
+ }
160
+ catch (err) {
161
+ spinner.fail(chalk.red("Failed to fetch skills"));
162
+ throw err;
163
+ }
164
+ }
@@ -0,0 +1,14 @@
1
+ interface SubmitOptions {
2
+ url: string;
3
+ platform?: string;
4
+ text?: string;
5
+ }
6
+ interface VerifyOptions {
7
+ witness?: boolean;
8
+ relevance: string;
9
+ quality: string;
10
+ vote?: string;
11
+ }
12
+ export declare function promoteSubmitCommand(taskId: string, options: SubmitOptions): Promise<void>;
13
+ export declare function promoteVerifyCommand(submissionId: string, options: VerifyOptions): Promise<void>;
14
+ export {};
@@ -0,0 +1,169 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { apiGet, apiPost } from '../utils/api.js';
4
+ import { awalExec } from '../utils/awal.js';
5
+ import { requireConfig } from '../utils/config.js';
6
+ export async function promoteSubmitCommand(taskId, options) {
7
+ const config = requireConfig();
8
+ // 自动检测平台(从 task 获取)
9
+ let platform = options.platform;
10
+ if (!platform) {
11
+ try {
12
+ const taskResp = await apiGet(`/api/v1/promote/${taskId}`, config.api_key);
13
+ if (taskResp.ok && taskResp.data.platform) {
14
+ platform = taskResp.data.platform;
15
+ }
16
+ }
17
+ catch { /* ignore */ }
18
+ }
19
+ if (!platform)
20
+ platform = 'twitter';
21
+ console.log('');
22
+ const spinner = ora(`Submitting proof for task ${taskId.slice(0, 8)}...`).start();
23
+ try {
24
+ const body = {
25
+ platform,
26
+ proof_url: options.url,
27
+ };
28
+ if (options.text) {
29
+ body.content_text = options.text;
30
+ }
31
+ const resp = await apiPost(`/api/v1/promote/${taskId}/submit`, body, config.api_key);
32
+ if (!resp.ok) {
33
+ spinner.fail('Submission failed');
34
+ const detail = typeof resp.data === 'object' ? (resp.data.detail || JSON.stringify(resp.data)) : String(resp.data);
35
+ console.error(chalk.red(` ${detail}`));
36
+ return;
37
+ }
38
+ spinner.succeed('Proof submitted');
39
+ if (resp.data.id) {
40
+ console.log(chalk.dim(` Submission ID: ${resp.data.id}`));
41
+ }
42
+ }
43
+ catch (err) {
44
+ spinner.fail('Submission failed');
45
+ console.error(chalk.red(err.message));
46
+ }
47
+ console.log('');
48
+ }
49
+ export async function promoteVerifyCommand(submissionId, options) {
50
+ const config = requireConfig();
51
+ console.log('');
52
+ if (options.witness) {
53
+ // Get submission to extract proof_url
54
+ const subSpinner = ora('Fetching submission...').start();
55
+ let proofUrl = '';
56
+ try {
57
+ // Try to get submission details - submissionId might be used directly
58
+ const resp = await apiGet(`/api/v1/promote/submissions/${submissionId}`, config.api_key);
59
+ if (resp.ok && resp.data.proof_url) {
60
+ proofUrl = resp.data.proof_url;
61
+ subSpinner.succeed(`Proof URL: ${proofUrl}`);
62
+ }
63
+ else {
64
+ subSpinner.warn('Could not fetch submission, will need tweet ID');
65
+ }
66
+ }
67
+ catch {
68
+ subSpinner.warn('Could not fetch submission');
69
+ }
70
+ // Extract tweet ID
71
+ let tweetId = '';
72
+ if (proofUrl) {
73
+ const match = proofUrl.match(/status\/(\d+)/);
74
+ if (match)
75
+ tweetId = match[1];
76
+ }
77
+ if (!tweetId) {
78
+ console.error(chalk.red(' Could not extract tweet ID from proof URL'));
79
+ return;
80
+ }
81
+ // Fetch witness proof via x402
82
+ const witnessSpinner = ora('Fetching witness proof via x402 ($0.01)...').start();
83
+ let witnessData;
84
+ try {
85
+ witnessData = await awalExec([
86
+ 'x402', 'pay', `https://witness.bnbot.ai/x/${tweetId}`,
87
+ ]);
88
+ witnessSpinner.succeed('Witness proof obtained');
89
+ }
90
+ catch (err) {
91
+ witnessSpinner.fail('Witness fetch failed');
92
+ console.error(chalk.red(err.message));
93
+ return;
94
+ }
95
+ // Parse witness response — awalExec wraps: { success, data: { status, data: { code, data: {...}, proof: {...} } } }
96
+ const proof = witnessData?.data?.data?.proof || witnessData?.data?.proof || witnessData?.proof;
97
+ if (!proof) {
98
+ console.error(chalk.red(' No proof in witness response'));
99
+ console.log(chalk.dim(` Raw: ${JSON.stringify(witnessData).slice(0, 200)}`));
100
+ return;
101
+ }
102
+ // Submit witness verification
103
+ const vote = options.vote || 'approve';
104
+ const relevanceScore = parseInt(options.relevance, 10);
105
+ const qualityScore = parseInt(options.quality, 10);
106
+ const verifySpinner = ora(`Submitting witness verification (${vote}, R:${relevanceScore} Q:${qualityScore})...`).start();
107
+ try {
108
+ const resp = await apiPost(`/api/v1/promote/submissions/${submissionId}/verify`, {
109
+ vote,
110
+ relevance_score: relevanceScore,
111
+ quality_score: qualityScore,
112
+ tweet_proof: {
113
+ payload: proof.payload,
114
+ signature: proof.signature,
115
+ signer: proof.signer,
116
+ timestamp: proof.timestamp,
117
+ },
118
+ }, config.api_key);
119
+ if (!resp.ok) {
120
+ verifySpinner.fail('Verification failed');
121
+ const detail = typeof resp.data === 'object' ? (resp.data.detail || JSON.stringify(resp.data)) : String(resp.data);
122
+ console.error(chalk.red(` ${detail}`));
123
+ }
124
+ else {
125
+ verifySpinner.succeed('Witness verification submitted');
126
+ if (resp.data.id) {
127
+ console.log(chalk.dim(` Verification ID: ${resp.data.id}`));
128
+ }
129
+ }
130
+ }
131
+ catch (err) {
132
+ verifySpinner.fail('Verification failed');
133
+ console.error(chalk.red(err.message));
134
+ }
135
+ }
136
+ else {
137
+ // Manual verification
138
+ const vote = options.vote || 'approve';
139
+ const relevanceScore = parseInt(options.relevance, 10);
140
+ const qualityScore = parseInt(options.quality, 10);
141
+ const spinner = ora(`Submitting manual verification (${vote}, R:${relevanceScore} Q:${qualityScore})...`).start();
142
+ try {
143
+ const resp = await apiPost(`/api/v1/promote/submissions/${submissionId}/verify`, {
144
+ vote,
145
+ relevance_score: relevanceScore,
146
+ quality_score: qualityScore,
147
+ views: 0,
148
+ likes: 0,
149
+ comments: 0,
150
+ }, config.api_key);
151
+ if (!resp.ok) {
152
+ spinner.fail('Verification failed');
153
+ const detail = typeof resp.data === 'object' ? (resp.data.detail || JSON.stringify(resp.data)) : String(resp.data);
154
+ console.error(chalk.red(` ${detail}`));
155
+ }
156
+ else {
157
+ spinner.succeed('Manual verification submitted');
158
+ if (resp.data.id) {
159
+ console.log(chalk.dim(` Verification ID: ${resp.data.id}`));
160
+ }
161
+ }
162
+ }
163
+ catch (err) {
164
+ spinner.fail('Verification failed');
165
+ console.error(chalk.red(err.message));
166
+ }
167
+ }
168
+ console.log('');
169
+ }
@@ -203,6 +203,6 @@ export async function setupCommand() {
203
203
  console.log(` Next steps:`);
204
204
  console.log(` ${chalk.cyan('clawmoney browse')} Browse available tasks`);
205
205
  console.log(` ${chalk.cyan('clawmoney wallet balance')} Check your wallet balance`);
206
- console.log(` ${chalk.cyan('clawmoney hire submit')} Submit a task proof`);
206
+ console.log(` ${chalk.cyan('clawmoney promote submit')} Submit a task proof`);
207
207
  console.log('');
208
208
  }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Daemon entry point for the Hub Provider.
4
+ * This file is spawned as a detached child process by `clawmoney hub start`.
5
+ * It runs the provider main loop (WS + Poller + Executor).
6
+ */
7
+ export {};
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Daemon entry point for the Hub Provider.
4
+ * This file is spawned as a detached child process by `clawmoney hub start`.
5
+ * It runs the provider main loop (WS + Poller + Executor).
6
+ */
7
+ import { runProvider } from "./provider.js";
8
+ // Parse CLI args passed from the parent
9
+ let cliCommand;
10
+ const args = process.argv.slice(2);
11
+ for (let i = 0; i < args.length; i++) {
12
+ if (args[i] === "--cli" && args[i + 1]) {
13
+ cliCommand = args[i + 1];
14
+ i++;
15
+ }
16
+ }
17
+ // Run the provider (this blocks until shutdown signal)
18
+ runProvider(cliCommand);
@@ -0,0 +1,4 @@
1
+ export declare function isProcessed(orderId: string): boolean;
2
+ export declare function markProcessed(orderId: string): void;
3
+ export declare function startDedup(): void;
4
+ export declare function stopDedup(): void;
@@ -0,0 +1,35 @@
1
+ import { logger } from "./logger.js";
2
+ const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
3
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
4
+ const seen = new Map();
5
+ let cleanupTimer = null;
6
+ export function isProcessed(orderId) {
7
+ return seen.has(orderId);
8
+ }
9
+ export function markProcessed(orderId) {
10
+ seen.set(orderId, Date.now());
11
+ }
12
+ function cleanup() {
13
+ const cutoff = Date.now() - TTL_MS;
14
+ let removed = 0;
15
+ for (const [id, ts] of seen) {
16
+ if (ts < cutoff) {
17
+ seen.delete(id);
18
+ removed++;
19
+ }
20
+ }
21
+ if (removed > 0) {
22
+ logger.info(`Dedup cleanup: removed ${removed} stale entries, ${seen.size} remaining`);
23
+ }
24
+ }
25
+ export function startDedup() {
26
+ cleanupTimer = setInterval(cleanup, CLEANUP_INTERVAL_MS);
27
+ cleanupTimer.unref();
28
+ }
29
+ export function stopDedup() {
30
+ if (cleanupTimer) {
31
+ clearInterval(cleanupTimer);
32
+ cleanupTimer = null;
33
+ }
34
+ seen.clear();
35
+ }
@@ -0,0 +1,13 @@
1
+ import type { ProviderConfig, ServiceCallEvent, TestCallEvent, DeliverEvent, TestResponseEvent } from "./types.js";
2
+ type SendFn = (event: DeliverEvent | TestResponseEvent) => boolean;
3
+ export declare class Executor {
4
+ private config;
5
+ private send;
6
+ private activeTasks;
7
+ constructor(config: ProviderConfig, send: SendFn);
8
+ get activeCount(): number;
9
+ handleServiceCall(call: ServiceCallEvent): void;
10
+ handleTestCall(call: TestCallEvent): void;
11
+ private executeTask;
12
+ }
13
+ export {};