cashclaw 1.0.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 +21 -0
- package/README.md +281 -0
- package/bin/cashclaw.js +2 -0
- package/missions/blog-post-1500.json +21 -0
- package/missions/blog-post-500.json +19 -0
- package/missions/lead-list-50.json +20 -0
- package/missions/seo-audit-basic.json +19 -0
- package/missions/seo-audit-pro.json +23 -0
- package/missions/social-media-weekly.json +19 -0
- package/missions/whatsapp-setup.json +22 -0
- package/package.json +45 -0
- package/skills/cashclaw-content-writer/SKILL.md +245 -0
- package/skills/cashclaw-core/SKILL.md +251 -0
- package/skills/cashclaw-invoicer/SKILL.md +395 -0
- package/skills/cashclaw-invoicer/scripts/stripe-ops.js +441 -0
- package/skills/cashclaw-lead-generator/SKILL.md +246 -0
- package/skills/cashclaw-lead-generator/scripts/scraper.js +356 -0
- package/skills/cashclaw-seo-auditor/SKILL.md +240 -0
- package/skills/cashclaw-seo-auditor/scripts/audit.js +401 -0
- package/skills/cashclaw-social-media/SKILL.md +374 -0
- package/skills/cashclaw-whatsapp-manager/SKILL.md +357 -0
- package/src/cli/commands/dashboard.js +72 -0
- package/src/cli/commands/init.js +290 -0
- package/src/cli/commands/status.js +174 -0
- package/src/cli/index.js +496 -0
- package/src/cli/utils/banner.js +44 -0
- package/src/cli/utils/config.js +170 -0
- package/src/dashboard/public/app.js +329 -0
- package/src/dashboard/public/index.html +139 -0
- package/src/dashboard/public/style.css +464 -0
- package/src/dashboard/server.js +224 -0
- package/src/engine/earnings-tracker.js +184 -0
- package/src/engine/mission-runner.js +224 -0
- package/src/engine/scheduler.js +139 -0
- package/src/integrations/hyrve-bridge.js +213 -0
- package/src/integrations/openclaw-bridge.js +207 -0
- package/src/integrations/stripe-connect.js +204 -0
- package/templates/config.default.json +83 -0
- package/templates/invoice.html +260 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { showBanner, showMiniBanner } from './utils/banner.js';
|
|
4
|
+
import { loadConfig, saveConfig } from './utils/config.js';
|
|
5
|
+
import { runInit } from './commands/init.js';
|
|
6
|
+
import { runStatus } from './commands/status.js';
|
|
7
|
+
import { runDashboard } from './commands/dashboard.js';
|
|
8
|
+
import { listMissions, createMission, startMission, completeMission, cancelMission, getMission } from '../engine/mission-runner.js';
|
|
9
|
+
import { getTotal, getMonthly, getWeekly, getToday, getHistory, getByService } from '../engine/earnings-tracker.js';
|
|
10
|
+
import { listInstalledSkills, listAvailableSkills, installSkills } from '../integrations/openclaw-bridge.js';
|
|
11
|
+
import Table from 'cli-table3';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const orange = chalk.hex('#FF6B35');
|
|
20
|
+
const green = chalk.hex('#16C784');
|
|
21
|
+
const dim = chalk.dim;
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name('cashclaw')
|
|
27
|
+
.description('Turn your OpenClaw AI agent into a freelance business')
|
|
28
|
+
.version('1.0.0', '-v, --version');
|
|
29
|
+
|
|
30
|
+
// ─── cashclaw init ─────────────────────────────────────────────────────
|
|
31
|
+
program
|
|
32
|
+
.command('init')
|
|
33
|
+
.description('Interactive setup wizard to configure your CashClaw agent')
|
|
34
|
+
.action(async () => {
|
|
35
|
+
showBanner();
|
|
36
|
+
await runInit();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── cashclaw status ───────────────────────────────────────────────────
|
|
40
|
+
program
|
|
41
|
+
.command('status')
|
|
42
|
+
.description('Show agent status, services, earnings, and skills')
|
|
43
|
+
.action(async () => {
|
|
44
|
+
await runStatus();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── cashclaw dashboard ────────────────────────────────────────────────
|
|
48
|
+
program
|
|
49
|
+
.command('dashboard')
|
|
50
|
+
.description('Launch the web dashboard')
|
|
51
|
+
.option('-p, --port <number>', 'Port number', parseInt)
|
|
52
|
+
.option('--no-open', 'Don\'t auto-open browser')
|
|
53
|
+
.action(async (options) => {
|
|
54
|
+
await runDashboard(options);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── cashclaw missions ────────────────────────────────────────────────
|
|
58
|
+
const missionsCmd = program
|
|
59
|
+
.command('missions')
|
|
60
|
+
.description('Manage client missions');
|
|
61
|
+
|
|
62
|
+
missionsCmd
|
|
63
|
+
.command('list')
|
|
64
|
+
.description('List all missions')
|
|
65
|
+
.option('-s, --status <status>', 'Filter by status (created|in_progress|completed|cancelled)')
|
|
66
|
+
.action(async (options) => {
|
|
67
|
+
showMiniBanner();
|
|
68
|
+
const missions = await listMissions(options.status || null);
|
|
69
|
+
|
|
70
|
+
if (missions.length === 0) {
|
|
71
|
+
console.log(dim(' No missions found.\n'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const table = new Table({
|
|
76
|
+
head: [dim('ID'), dim('Name'), dim('Status'), dim('Price'), dim('Client'), dim('Created')],
|
|
77
|
+
colWidths: [10, 22, 14, 10, 16, 14],
|
|
78
|
+
style: { head: [] },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
for (const m of missions) {
|
|
82
|
+
const statusColor = {
|
|
83
|
+
created: chalk.blue,
|
|
84
|
+
in_progress: chalk.yellow,
|
|
85
|
+
completed: green,
|
|
86
|
+
cancelled: chalk.red,
|
|
87
|
+
paid: green.bold,
|
|
88
|
+
}[m.status] || dim;
|
|
89
|
+
|
|
90
|
+
table.push([
|
|
91
|
+
m.id.slice(0, 8),
|
|
92
|
+
m.name,
|
|
93
|
+
statusColor(m.status),
|
|
94
|
+
`$${m.price_usd}`,
|
|
95
|
+
m.client?.name || '-',
|
|
96
|
+
new Date(m.created_at).toLocaleDateString(),
|
|
97
|
+
]);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(table.toString());
|
|
101
|
+
console.log(dim(`\n Total: ${missions.length} mission(s)\n`));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
missionsCmd
|
|
105
|
+
.command('create <template>')
|
|
106
|
+
.description('Create a new mission from a template')
|
|
107
|
+
.option('-c, --client <name>', 'Client name')
|
|
108
|
+
.option('-e, --email <email>', 'Client email')
|
|
109
|
+
.action(async (templateName, options) => {
|
|
110
|
+
showMiniBanner();
|
|
111
|
+
|
|
112
|
+
// Load template from missions/ directory
|
|
113
|
+
const templatesDir = path.resolve(__dirname, '../../missions');
|
|
114
|
+
const templateFile = path.join(templatesDir, `${templateName}.json`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const exists = await fs.pathExists(templateFile);
|
|
118
|
+
if (!exists) {
|
|
119
|
+
// List available templates
|
|
120
|
+
const files = await fs.readdir(templatesDir);
|
|
121
|
+
const templates = files.filter((f) => f.endsWith('.json')).map((f) => f.replace('.json', ''));
|
|
122
|
+
console.log(chalk.red(` Template "${templateName}" not found.\n`));
|
|
123
|
+
console.log(dim(' Available templates:'));
|
|
124
|
+
for (const t of templates) {
|
|
125
|
+
console.log(` - ${t}`);
|
|
126
|
+
}
|
|
127
|
+
console.log();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const template = await fs.readJson(templateFile);
|
|
132
|
+
const mission = await createMission(template, {
|
|
133
|
+
name: options.client || 'Walk-in Client',
|
|
134
|
+
email: options.email || '',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
console.log(green.bold(' Mission created!\n'));
|
|
138
|
+
console.log(` ${orange('ID:')} ${mission.id}`);
|
|
139
|
+
console.log(` ${orange('Name:')} ${mission.name}`);
|
|
140
|
+
console.log(` ${orange('Price:')} $${mission.price_usd}`);
|
|
141
|
+
console.log(` ${orange('Client:')} ${mission.client.name}`);
|
|
142
|
+
console.log(` ${orange('Status:')} ${mission.status}\n`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(chalk.red(` Error: ${err.message}\n`));
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
missionsCmd
|
|
149
|
+
.command('start <id>')
|
|
150
|
+
.description('Start a mission')
|
|
151
|
+
.action(async (id) => {
|
|
152
|
+
showMiniBanner();
|
|
153
|
+
try {
|
|
154
|
+
// Support short IDs by finding the full ID
|
|
155
|
+
const fullId = await resolveShortId(id);
|
|
156
|
+
const mission = await startMission(fullId);
|
|
157
|
+
console.log(green(` Mission "${mission.name}" started.\n`));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error(chalk.red(` Error: ${err.message}\n`));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
missionsCmd
|
|
164
|
+
.command('complete <id>')
|
|
165
|
+
.description('Mark a mission as completed')
|
|
166
|
+
.action(async (id) => {
|
|
167
|
+
showMiniBanner();
|
|
168
|
+
try {
|
|
169
|
+
const fullId = await resolveShortId(id);
|
|
170
|
+
const mission = await completeMission(fullId);
|
|
171
|
+
console.log(green(` Mission "${mission.name}" completed!\n`));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(chalk.red(` Error: ${err.message}\n`));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
missionsCmd
|
|
178
|
+
.command('cancel <id>')
|
|
179
|
+
.description('Cancel a mission')
|
|
180
|
+
.action(async (id) => {
|
|
181
|
+
showMiniBanner();
|
|
182
|
+
try {
|
|
183
|
+
const fullId = await resolveShortId(id);
|
|
184
|
+
const mission = await cancelMission(fullId);
|
|
185
|
+
console.log(chalk.yellow(` Mission "${mission.name}" cancelled.\n`));
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error(chalk.red(` Error: ${err.message}\n`));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
missionsCmd
|
|
192
|
+
.command('show <id>')
|
|
193
|
+
.description('Show mission details')
|
|
194
|
+
.action(async (id) => {
|
|
195
|
+
showMiniBanner();
|
|
196
|
+
try {
|
|
197
|
+
const fullId = await resolveShortId(id);
|
|
198
|
+
const mission = await getMission(fullId);
|
|
199
|
+
if (!mission) {
|
|
200
|
+
console.log(chalk.red(` Mission not found: ${id}\n`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log(orange.bold(` ${mission.name}\n`));
|
|
205
|
+
console.log(` ${dim('ID:')} ${mission.id}`);
|
|
206
|
+
console.log(` ${dim('Template:')} ${mission.template}`);
|
|
207
|
+
console.log(` ${dim('Service:')} ${mission.service_type}`);
|
|
208
|
+
console.log(` ${dim('Tier:')} ${mission.tier}`);
|
|
209
|
+
console.log(` ${dim('Price:')} $${mission.price_usd}`);
|
|
210
|
+
console.log(` ${dim('Status:')} ${mission.status}`);
|
|
211
|
+
console.log(` ${dim('Client:')} ${mission.client?.name || '-'} (${mission.client?.email || '-'})`);
|
|
212
|
+
console.log(` ${dim('Created:')} ${mission.created_at}`);
|
|
213
|
+
if (mission.started_at) console.log(` ${dim('Started:')} ${mission.started_at}`);
|
|
214
|
+
if (mission.completed_at) console.log(` ${dim('Completed:')} ${mission.completed_at}`);
|
|
215
|
+
|
|
216
|
+
if (mission.steps?.length > 0) {
|
|
217
|
+
console.log(`\n ${dim('Steps:')}`);
|
|
218
|
+
for (const step of mission.steps) {
|
|
219
|
+
const icon = step.status === 'completed' ? green('done') : dim('pending');
|
|
220
|
+
console.log(` ${step.index + 1}. ${step.description} [${icon}]`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (mission.deliverables?.length > 0) {
|
|
225
|
+
console.log(`\n ${dim('Deliverables:')}`);
|
|
226
|
+
for (const d of mission.deliverables) {
|
|
227
|
+
console.log(` - ${d}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
console.log();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(chalk.red(` Error: ${err.message}\n`));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Default action for missions (no subcommand) = list
|
|
237
|
+
missionsCmd.action(async () => {
|
|
238
|
+
showMiniBanner();
|
|
239
|
+
const missions = await listMissions();
|
|
240
|
+
if (missions.length === 0) {
|
|
241
|
+
console.log(dim(' No missions yet. Create one with:\n'));
|
|
242
|
+
console.log(` ${chalk.bold('cashclaw missions create seo-audit-basic --client "John Doe"')}\n`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const table = new Table({
|
|
247
|
+
head: [dim('ID'), dim('Name'), dim('Status'), dim('Price'), dim('Client')],
|
|
248
|
+
colWidths: [10, 24, 14, 10, 18],
|
|
249
|
+
style: { head: [] },
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
for (const m of missions.slice(0, 10)) {
|
|
253
|
+
const statusColor = {
|
|
254
|
+
created: chalk.blue,
|
|
255
|
+
in_progress: chalk.yellow,
|
|
256
|
+
completed: green,
|
|
257
|
+
cancelled: chalk.red,
|
|
258
|
+
paid: green.bold,
|
|
259
|
+
}[m.status] || dim;
|
|
260
|
+
|
|
261
|
+
table.push([
|
|
262
|
+
m.id.slice(0, 8),
|
|
263
|
+
m.name,
|
|
264
|
+
statusColor(m.status),
|
|
265
|
+
`$${m.price_usd}`,
|
|
266
|
+
m.client?.name || '-',
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(table.toString());
|
|
271
|
+
if (missions.length > 10) {
|
|
272
|
+
console.log(dim(`\n ... and ${missions.length - 10} more. Use "cashclaw missions list" for all.\n`));
|
|
273
|
+
}
|
|
274
|
+
console.log();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// ─── cashclaw earnings ─────────────────────────────────────────────────
|
|
278
|
+
program
|
|
279
|
+
.command('earnings')
|
|
280
|
+
.description('Show earnings summary and history')
|
|
281
|
+
.option('-n, --limit <number>', 'Number of recent entries to show', parseInt, 10)
|
|
282
|
+
.action(async (options) => {
|
|
283
|
+
showMiniBanner();
|
|
284
|
+
|
|
285
|
+
const config = await loadConfig();
|
|
286
|
+
const currencySymbol = { USD: '$', EUR: '€', GBP: '£', TRY: '₺' }[config.agent?.currency] || '$';
|
|
287
|
+
|
|
288
|
+
const [total, monthly, weekly, today, history, byService] = await Promise.all([
|
|
289
|
+
getTotal(),
|
|
290
|
+
getMonthly(),
|
|
291
|
+
getWeekly(),
|
|
292
|
+
getToday(),
|
|
293
|
+
getHistory(options.limit),
|
|
294
|
+
getByService(),
|
|
295
|
+
]);
|
|
296
|
+
|
|
297
|
+
console.log(orange.bold(' Earnings Summary\n'));
|
|
298
|
+
|
|
299
|
+
const summaryTable = new Table({
|
|
300
|
+
chars: { top: '', 'top-mid': '', 'top-left': ' ', 'top-right': '',
|
|
301
|
+
bottom: '', 'bottom-mid': '', 'bottom-left': ' ', 'bottom-right': '',
|
|
302
|
+
left: ' ', 'left-mid': ' ', mid: '', 'mid-mid': '',
|
|
303
|
+
right: '', 'right-mid': '', middle: ' ' },
|
|
304
|
+
colWidths: [18, 22],
|
|
305
|
+
style: { 'padding-left': 0, 'padding-right': 0 },
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
summaryTable.push(
|
|
309
|
+
[dim('All Time'), green.bold(`${currencySymbol}${total.toFixed(2)}`)],
|
|
310
|
+
[dim('This Month'), `${currencySymbol}${monthly.total.toFixed(2)} (${monthly.count} jobs)`],
|
|
311
|
+
[dim('This Week'), `${currencySymbol}${weekly.total.toFixed(2)} (${weekly.count} jobs)`],
|
|
312
|
+
[dim('Today'), `${currencySymbol}${today.total.toFixed(2)} (${today.count} jobs)`],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
console.log(summaryTable.toString());
|
|
316
|
+
|
|
317
|
+
// By service breakdown
|
|
318
|
+
const serviceKeys = Object.keys(byService);
|
|
319
|
+
if (serviceKeys.length > 0) {
|
|
320
|
+
console.log(orange('\n By Service\n'));
|
|
321
|
+
for (const [svc, data] of Object.entries(byService)) {
|
|
322
|
+
console.log(` ${dim(svc.padEnd(22))} ${currencySymbol}${data.total.toFixed(2)} (${data.count} jobs)`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Recent history
|
|
327
|
+
if (history.length > 0) {
|
|
328
|
+
console.log(orange('\n Recent Earnings\n'));
|
|
329
|
+
|
|
330
|
+
const histTable = new Table({
|
|
331
|
+
head: [dim('Date'), dim('Service'), dim('Amount'), dim('Client')],
|
|
332
|
+
colWidths: [14, 20, 12, 18],
|
|
333
|
+
style: { head: [] },
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
for (const e of history) {
|
|
337
|
+
histTable.push([
|
|
338
|
+
new Date(e.recorded_at).toLocaleDateString(),
|
|
339
|
+
e.service_type,
|
|
340
|
+
green(`${currencySymbol}${e.amount.toFixed(2)}`),
|
|
341
|
+
e.client_name || '-',
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log(histTable.toString());
|
|
346
|
+
} else {
|
|
347
|
+
console.log(dim('\n No earnings recorded yet.\n'));
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ─── cashclaw config ───────────────────────────────────────────────────
|
|
353
|
+
const configCmd = program
|
|
354
|
+
.command('config')
|
|
355
|
+
.description('View or update configuration');
|
|
356
|
+
|
|
357
|
+
configCmd
|
|
358
|
+
.command('show')
|
|
359
|
+
.description('Show current configuration')
|
|
360
|
+
.action(async () => {
|
|
361
|
+
showMiniBanner();
|
|
362
|
+
const config = await loadConfig();
|
|
363
|
+
console.log(JSON.stringify(config, null, 2));
|
|
364
|
+
console.log();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
configCmd
|
|
368
|
+
.command('set <key> <value>')
|
|
369
|
+
.description('Set a configuration value (dot notation: agent.name)')
|
|
370
|
+
.action(async (key, value) => {
|
|
371
|
+
showMiniBanner();
|
|
372
|
+
const config = await loadConfig();
|
|
373
|
+
|
|
374
|
+
// Parse dot notation: "stripe.secret_key" -> config.stripe.secret_key
|
|
375
|
+
const keys = key.split('.');
|
|
376
|
+
let obj = config;
|
|
377
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
378
|
+
if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') {
|
|
379
|
+
obj[keys[i]] = {};
|
|
380
|
+
}
|
|
381
|
+
obj = obj[keys[i]];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Try to parse as number or boolean
|
|
385
|
+
let parsedValue = value;
|
|
386
|
+
if (value === 'true') parsedValue = true;
|
|
387
|
+
else if (value === 'false') parsedValue = false;
|
|
388
|
+
else if (!isNaN(Number(value)) && value.trim() !== '') parsedValue = Number(value);
|
|
389
|
+
|
|
390
|
+
obj[keys[keys.length - 1]] = parsedValue;
|
|
391
|
+
await saveConfig(config);
|
|
392
|
+
|
|
393
|
+
console.log(green(` ${key} = ${JSON.stringify(parsedValue)}\n`));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
configCmd
|
|
397
|
+
.command('get <key>')
|
|
398
|
+
.description('Get a configuration value')
|
|
399
|
+
.action(async (key) => {
|
|
400
|
+
showMiniBanner();
|
|
401
|
+
const config = await loadConfig();
|
|
402
|
+
const keys = key.split('.');
|
|
403
|
+
let value = config;
|
|
404
|
+
for (const k of keys) {
|
|
405
|
+
if (value === undefined || value === null) break;
|
|
406
|
+
value = value[k];
|
|
407
|
+
}
|
|
408
|
+
if (value === undefined) {
|
|
409
|
+
console.log(chalk.yellow(` Key "${key}" not found.\n`));
|
|
410
|
+
} else {
|
|
411
|
+
console.log(` ${orange(key)} = ${JSON.stringify(value, null, 2)}\n`);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
configCmd.action(async () => {
|
|
416
|
+
showMiniBanner();
|
|
417
|
+
const config = await loadConfig();
|
|
418
|
+
console.log(orange.bold(' Configuration\n'));
|
|
419
|
+
console.log(` ${dim('Agent:')} ${config.agent.name}`);
|
|
420
|
+
console.log(` ${dim('Currency:')} ${config.agent.currency}`);
|
|
421
|
+
console.log(` ${dim('Stripe:')} ${config.stripe.connected ? green('connected') : chalk.yellow('not set')}`);
|
|
422
|
+
console.log(` ${dim('Port:')} ${config.server.port}`);
|
|
423
|
+
console.log();
|
|
424
|
+
console.log(dim(' Use "cashclaw config show" for full config'));
|
|
425
|
+
console.log(dim(' Use "cashclaw config set <key> <value>" to update\n'));
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ─── cashclaw skills ───────────────────────────────────────────────────
|
|
429
|
+
program
|
|
430
|
+
.command('skills')
|
|
431
|
+
.description('List and manage OpenClaw skills')
|
|
432
|
+
.option('-i, --install', 'Install all available skills')
|
|
433
|
+
.action(async (options) => {
|
|
434
|
+
showMiniBanner();
|
|
435
|
+
const config = await loadConfig();
|
|
436
|
+
|
|
437
|
+
const available = await listAvailableSkills();
|
|
438
|
+
const installed = await listInstalledSkills(config.openclaw?.skills_dir);
|
|
439
|
+
|
|
440
|
+
console.log(orange.bold(' CashClaw Skills\n'));
|
|
441
|
+
|
|
442
|
+
if (available.length === 0) {
|
|
443
|
+
console.log(dim(' No skills found in package.\n'));
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
for (const skill of available) {
|
|
448
|
+
const isInstalled = installed.includes(skill.name);
|
|
449
|
+
const icon = isInstalled ? green('installed') : dim('available');
|
|
450
|
+
console.log(` ${skill.name.padEnd(30)} [${icon}]`);
|
|
451
|
+
}
|
|
452
|
+
console.log();
|
|
453
|
+
|
|
454
|
+
if (options.install) {
|
|
455
|
+
const skillNames = available.map((s) => s.name);
|
|
456
|
+
console.log(dim(` Installing ${skillNames.length} skills...\n`));
|
|
457
|
+
|
|
458
|
+
const result = await installSkills(skillNames, config.openclaw?.skills_dir);
|
|
459
|
+
|
|
460
|
+
if (result.installed.length > 0) {
|
|
461
|
+
for (const name of result.installed) {
|
|
462
|
+
console.log(green(` + ${name}`));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (result.failed.length > 0) {
|
|
466
|
+
for (const f of result.failed) {
|
|
467
|
+
console.log(chalk.yellow(` ! ${f.name}: ${f.error}`));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
console.log(`\n ${result.message}\n`);
|
|
471
|
+
} else {
|
|
472
|
+
console.log(dim(' Run "cashclaw skills --install" to install all.\n'));
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// ─── Default action (no command) ───────────────────────────────────────
|
|
477
|
+
program.action(() => {
|
|
478
|
+
showBanner();
|
|
479
|
+
program.outputHelp();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Resolve a short ID (first 8 chars) to a full mission UUID.
|
|
484
|
+
*/
|
|
485
|
+
async function resolveShortId(shortId) {
|
|
486
|
+
if (shortId.length >= 32) return shortId; // Already full UUID
|
|
487
|
+
|
|
488
|
+
const missions = await listMissions();
|
|
489
|
+
const match = missions.find((m) => m.id.startsWith(shortId));
|
|
490
|
+
if (!match) {
|
|
491
|
+
throw new Error(`No mission found matching "${shortId}"`);
|
|
492
|
+
}
|
|
493
|
+
return match.id;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
program.parse();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import boxen from 'boxen';
|
|
3
|
+
|
|
4
|
+
const orange = chalk.hex('#FF6B35');
|
|
5
|
+
const green = chalk.hex('#16C784');
|
|
6
|
+
const dim = chalk.dim;
|
|
7
|
+
|
|
8
|
+
const LOGO = `
|
|
9
|
+
___ _ ___ _
|
|
10
|
+
/ __\\__ _ ___| |__ / __\\ | __ ___ __
|
|
11
|
+
/ / / _\` / __| '_ \\/ / | |/ _\` \\ \\ /\\ / /
|
|
12
|
+
/ /__| (_| \\__ \\ | | / /___| | (_| |\\ V V /
|
|
13
|
+
\\____/\\__,_|___/_| |_\\____/|_|\\__,_| \\_/\\_/
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export function showBanner() {
|
|
17
|
+
const logoColored = orange.bold(LOGO);
|
|
18
|
+
const version = dim('v1.0.0');
|
|
19
|
+
const tagline = green('Turn your AI agent into a freelance business');
|
|
20
|
+
const site = dim('https://cashclawai.com');
|
|
21
|
+
|
|
22
|
+
const content = `${logoColored}
|
|
23
|
+
${tagline} ${version}
|
|
24
|
+
${site}`;
|
|
25
|
+
|
|
26
|
+
const banner = boxen(content, {
|
|
27
|
+
padding: { top: 0, bottom: 1, left: 2, right: 2 },
|
|
28
|
+
borderStyle: 'round',
|
|
29
|
+
borderColor: '#FF6B35',
|
|
30
|
+
dimBorder: false,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log(banner);
|
|
34
|
+
console.log();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function showMiniBanner() {
|
|
38
|
+
console.log(
|
|
39
|
+
orange.bold('\n CashClaw') +
|
|
40
|
+
dim(' v1.0.0') +
|
|
41
|
+
green(' | ') +
|
|
42
|
+
dim('cashclawai.com\n')
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the path to the CashClaw config directory: ~/.cashclaw/
|
|
7
|
+
*/
|
|
8
|
+
export function getConfigDir() {
|
|
9
|
+
return path.join(os.homedir(), '.cashclaw');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the path to the main config file: ~/.cashclaw/config.json
|
|
14
|
+
*/
|
|
15
|
+
export function getConfigPath() {
|
|
16
|
+
return path.join(getConfigDir(), 'config.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ensures ~/.cashclaw/ and its subdirectories exist.
|
|
21
|
+
*/
|
|
22
|
+
export async function ensureConfigDir() {
|
|
23
|
+
const configDir = getConfigDir();
|
|
24
|
+
await fs.ensureDir(configDir);
|
|
25
|
+
await fs.ensureDir(path.join(configDir, 'missions'));
|
|
26
|
+
return configDir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the full default configuration with realistic pricing.
|
|
31
|
+
*/
|
|
32
|
+
export function getDefaultConfig() {
|
|
33
|
+
return {
|
|
34
|
+
agent: {
|
|
35
|
+
name: 'MyCashClaw',
|
|
36
|
+
owner: '',
|
|
37
|
+
email: '',
|
|
38
|
+
currency: 'USD',
|
|
39
|
+
created_at: new Date().toISOString(),
|
|
40
|
+
},
|
|
41
|
+
stripe: {
|
|
42
|
+
secret_key: '',
|
|
43
|
+
connected: false,
|
|
44
|
+
mode: 'test',
|
|
45
|
+
},
|
|
46
|
+
server: {
|
|
47
|
+
port: 3847,
|
|
48
|
+
host: 'localhost',
|
|
49
|
+
},
|
|
50
|
+
services: {
|
|
51
|
+
seo_audit: {
|
|
52
|
+
enabled: false,
|
|
53
|
+
pricing: {
|
|
54
|
+
basic: 9,
|
|
55
|
+
standard: 29,
|
|
56
|
+
pro: 59,
|
|
57
|
+
},
|
|
58
|
+
description: 'Automated SEO audits with actionable recommendations',
|
|
59
|
+
},
|
|
60
|
+
content_writing: {
|
|
61
|
+
enabled: false,
|
|
62
|
+
pricing: {
|
|
63
|
+
post_500: 5,
|
|
64
|
+
post_1500: 12,
|
|
65
|
+
newsletter: 9,
|
|
66
|
+
},
|
|
67
|
+
description: 'AI-powered blog posts, articles, and newsletters',
|
|
68
|
+
},
|
|
69
|
+
lead_generation: {
|
|
70
|
+
enabled: false,
|
|
71
|
+
pricing: {
|
|
72
|
+
starter_25: 9,
|
|
73
|
+
standard_50: 15,
|
|
74
|
+
pro_100: 25,
|
|
75
|
+
},
|
|
76
|
+
description: 'Targeted lead lists with contact info and scoring',
|
|
77
|
+
},
|
|
78
|
+
whatsapp_management: {
|
|
79
|
+
enabled: false,
|
|
80
|
+
pricing: {
|
|
81
|
+
setup: 19,
|
|
82
|
+
monthly: 49,
|
|
83
|
+
},
|
|
84
|
+
description: 'WhatsApp Business setup and automated responses',
|
|
85
|
+
},
|
|
86
|
+
social_media: {
|
|
87
|
+
enabled: false,
|
|
88
|
+
pricing: {
|
|
89
|
+
weekly_1: 9,
|
|
90
|
+
weekly_3: 19,
|
|
91
|
+
monthly_full: 49,
|
|
92
|
+
},
|
|
93
|
+
description: 'Social media content creation and scheduling',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
hyrve: {
|
|
97
|
+
api_url: 'https://api.hyrveai.com/v1',
|
|
98
|
+
registered: false,
|
|
99
|
+
agent_id: '',
|
|
100
|
+
},
|
|
101
|
+
openclaw: {
|
|
102
|
+
workspace: '',
|
|
103
|
+
skills_dir: '',
|
|
104
|
+
auto_detected: false,
|
|
105
|
+
},
|
|
106
|
+
heartbeat: {
|
|
107
|
+
enabled: false,
|
|
108
|
+
interval_ms: 60000,
|
|
109
|
+
},
|
|
110
|
+
stats: {
|
|
111
|
+
total_missions: 0,
|
|
112
|
+
completed_missions: 0,
|
|
113
|
+
total_earned: 0,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Loads config from ~/.cashclaw/config.json.
|
|
120
|
+
* Returns default config if file does not exist.
|
|
121
|
+
*/
|
|
122
|
+
export async function loadConfig() {
|
|
123
|
+
const configPath = getConfigPath();
|
|
124
|
+
try {
|
|
125
|
+
const exists = await fs.pathExists(configPath);
|
|
126
|
+
if (!exists) {
|
|
127
|
+
return getDefaultConfig();
|
|
128
|
+
}
|
|
129
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
130
|
+
const loaded = JSON.parse(raw);
|
|
131
|
+
// Merge with defaults to fill in any missing keys
|
|
132
|
+
const defaults = getDefaultConfig();
|
|
133
|
+
return deepMerge(defaults, loaded);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`Warning: Could not read config at ${configPath}: ${err.message}`);
|
|
136
|
+
return getDefaultConfig();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Saves config object to ~/.cashclaw/config.json.
|
|
142
|
+
*/
|
|
143
|
+
export async function saveConfig(config) {
|
|
144
|
+
await ensureConfigDir();
|
|
145
|
+
const configPath = getConfigPath();
|
|
146
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
147
|
+
return configPath;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Deep merge: target is base, source overrides.
|
|
152
|
+
*/
|
|
153
|
+
function deepMerge(target, source) {
|
|
154
|
+
const result = { ...target };
|
|
155
|
+
for (const key of Object.keys(source)) {
|
|
156
|
+
if (
|
|
157
|
+
source[key] &&
|
|
158
|
+
typeof source[key] === 'object' &&
|
|
159
|
+
!Array.isArray(source[key]) &&
|
|
160
|
+
target[key] &&
|
|
161
|
+
typeof target[key] === 'object' &&
|
|
162
|
+
!Array.isArray(target[key])
|
|
163
|
+
) {
|
|
164
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
165
|
+
} else {
|
|
166
|
+
result[key] = source[key];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|