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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cashclaw.js +2 -0
  4. package/missions/blog-post-1500.json +21 -0
  5. package/missions/blog-post-500.json +19 -0
  6. package/missions/lead-list-50.json +20 -0
  7. package/missions/seo-audit-basic.json +19 -0
  8. package/missions/seo-audit-pro.json +23 -0
  9. package/missions/social-media-weekly.json +19 -0
  10. package/missions/whatsapp-setup.json +22 -0
  11. package/package.json +45 -0
  12. package/skills/cashclaw-content-writer/SKILL.md +245 -0
  13. package/skills/cashclaw-core/SKILL.md +251 -0
  14. package/skills/cashclaw-invoicer/SKILL.md +395 -0
  15. package/skills/cashclaw-invoicer/scripts/stripe-ops.js +441 -0
  16. package/skills/cashclaw-lead-generator/SKILL.md +246 -0
  17. package/skills/cashclaw-lead-generator/scripts/scraper.js +356 -0
  18. package/skills/cashclaw-seo-auditor/SKILL.md +240 -0
  19. package/skills/cashclaw-seo-auditor/scripts/audit.js +401 -0
  20. package/skills/cashclaw-social-media/SKILL.md +374 -0
  21. package/skills/cashclaw-whatsapp-manager/SKILL.md +357 -0
  22. package/src/cli/commands/dashboard.js +72 -0
  23. package/src/cli/commands/init.js +290 -0
  24. package/src/cli/commands/status.js +174 -0
  25. package/src/cli/index.js +496 -0
  26. package/src/cli/utils/banner.js +44 -0
  27. package/src/cli/utils/config.js +170 -0
  28. package/src/dashboard/public/app.js +329 -0
  29. package/src/dashboard/public/index.html +139 -0
  30. package/src/dashboard/public/style.css +464 -0
  31. package/src/dashboard/server.js +224 -0
  32. package/src/engine/earnings-tracker.js +184 -0
  33. package/src/engine/mission-runner.js +224 -0
  34. package/src/engine/scheduler.js +139 -0
  35. package/src/integrations/hyrve-bridge.js +213 -0
  36. package/src/integrations/openclaw-bridge.js +207 -0
  37. package/src/integrations/stripe-connect.js +204 -0
  38. package/templates/config.default.json +83 -0
  39. package/templates/invoice.html +260 -0
@@ -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
+ }