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,184 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import dayjs from 'dayjs';
5
+ import isoWeek from 'dayjs/plugin/isoWeek.js';
6
+
7
+ dayjs.extend(isoWeek);
8
+
9
+ const EARNINGS_FILE = path.join(os.homedir(), '.cashclaw', 'earnings.jsonl');
10
+
11
+ /**
12
+ * Ensure the earnings file parent directory exists.
13
+ */
14
+ async function ensureEarningsFile() {
15
+ await fs.ensureDir(path.dirname(EARNINGS_FILE));
16
+ const exists = await fs.pathExists(EARNINGS_FILE);
17
+ if (!exists) {
18
+ await fs.writeFile(EARNINGS_FILE, '', 'utf-8');
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Record a new earning entry.
24
+ * @param {object} data - { mission_id, service_type, amount, currency, client_name, client_email, description, payment_id }
25
+ * @returns {object} The recorded earning entry
26
+ */
27
+ export async function recordEarning(data) {
28
+ await ensureEarningsFile();
29
+
30
+ const entry = {
31
+ id: `earn_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
32
+ mission_id: data.mission_id || null,
33
+ service_type: data.service_type || 'general',
34
+ amount: data.amount || 0,
35
+ currency: data.currency || 'USD',
36
+ client_name: data.client_name || 'Unknown',
37
+ client_email: data.client_email || '',
38
+ description: data.description || '',
39
+ payment_id: data.payment_id || null,
40
+ recorded_at: dayjs().toISOString(),
41
+ };
42
+
43
+ const line = JSON.stringify(entry) + '\n';
44
+ await fs.appendFile(EARNINGS_FILE, line, 'utf-8');
45
+
46
+ return entry;
47
+ }
48
+
49
+ /**
50
+ * Read all earning entries from the JSONL file.
51
+ */
52
+ async function readAllEarnings() {
53
+ await ensureEarningsFile();
54
+
55
+ const content = await fs.readFile(EARNINGS_FILE, 'utf-8');
56
+ const lines = content.trim().split('\n').filter(Boolean);
57
+
58
+ const earnings = [];
59
+ for (const line of lines) {
60
+ try {
61
+ earnings.push(JSON.parse(line));
62
+ } catch (err) {
63
+ // Skip malformed lines
64
+ continue;
65
+ }
66
+ }
67
+
68
+ // Sort by recorded_at descending
69
+ earnings.sort((a, b) => new Date(b.recorded_at) - new Date(a.recorded_at));
70
+ return earnings;
71
+ }
72
+
73
+ /**
74
+ * Get the total of all earnings in USD.
75
+ */
76
+ export async function getTotal() {
77
+ const earnings = await readAllEarnings();
78
+ return earnings.reduce((sum, e) => sum + (e.amount || 0), 0);
79
+ }
80
+
81
+ /**
82
+ * Get earnings for the current month.
83
+ */
84
+ export async function getMonthly() {
85
+ const earnings = await readAllEarnings();
86
+ const startOfMonth = dayjs().startOf('month');
87
+
88
+ const monthly = earnings.filter((e) =>
89
+ dayjs(e.recorded_at).isAfter(startOfMonth) || dayjs(e.recorded_at).isSame(startOfMonth)
90
+ );
91
+
92
+ return {
93
+ total: monthly.reduce((sum, e) => sum + (e.amount || 0), 0),
94
+ count: monthly.length,
95
+ entries: monthly,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Get earnings for the current week (ISO week, Mon-Sun).
101
+ */
102
+ export async function getWeekly() {
103
+ const earnings = await readAllEarnings();
104
+ const startOfWeek = dayjs().startOf('isoWeek');
105
+
106
+ const weekly = earnings.filter((e) =>
107
+ dayjs(e.recorded_at).isAfter(startOfWeek) || dayjs(e.recorded_at).isSame(startOfWeek)
108
+ );
109
+
110
+ return {
111
+ total: weekly.reduce((sum, e) => sum + (e.amount || 0), 0),
112
+ count: weekly.length,
113
+ entries: weekly,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Get earnings for today.
119
+ */
120
+ export async function getToday() {
121
+ const earnings = await readAllEarnings();
122
+ const startOfDay = dayjs().startOf('day');
123
+
124
+ const today = earnings.filter((e) =>
125
+ dayjs(e.recorded_at).isAfter(startOfDay) || dayjs(e.recorded_at).isSame(startOfDay)
126
+ );
127
+
128
+ return {
129
+ total: today.reduce((sum, e) => sum + (e.amount || 0), 0),
130
+ count: today.length,
131
+ entries: today,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Get earning history (most recent first).
137
+ * @param {number} limit - Max entries to return (default 50)
138
+ */
139
+ export async function getHistory(limit = 50) {
140
+ const earnings = await readAllEarnings();
141
+ return earnings.slice(0, limit);
142
+ }
143
+
144
+ /**
145
+ * Get a breakdown of earnings by service type.
146
+ */
147
+ export async function getByService() {
148
+ const earnings = await readAllEarnings();
149
+ const byService = {};
150
+
151
+ for (const e of earnings) {
152
+ const key = e.service_type || 'general';
153
+ if (!byService[key]) {
154
+ byService[key] = { total: 0, count: 0 };
155
+ }
156
+ byService[key].total += e.amount || 0;
157
+ byService[key].count += 1;
158
+ }
159
+
160
+ return byService;
161
+ }
162
+
163
+ /**
164
+ * Get daily totals for the last N days (for charting).
165
+ * @param {number} days - Number of days to look back (default 30)
166
+ */
167
+ export async function getDailyTotals(days = 30) {
168
+ const earnings = await readAllEarnings();
169
+ const result = [];
170
+
171
+ for (let i = days - 1; i >= 0; i--) {
172
+ const date = dayjs().subtract(i, 'day').format('YYYY-MM-DD');
173
+ const dayEarnings = earnings.filter(
174
+ (e) => dayjs(e.recorded_at).format('YYYY-MM-DD') === date
175
+ );
176
+ result.push({
177
+ date,
178
+ total: dayEarnings.reduce((sum, e) => sum + (e.amount || 0), 0),
179
+ count: dayEarnings.length,
180
+ });
181
+ }
182
+
183
+ return result;
184
+ }
@@ -0,0 +1,224 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import dayjs from 'dayjs';
6
+
7
+ const MISSIONS_DIR = path.join(os.homedir(), '.cashclaw', 'missions');
8
+
9
+ /**
10
+ * Ensure the missions directory exists.
11
+ */
12
+ async function ensureMissionsDir() {
13
+ await fs.ensureDir(MISSIONS_DIR);
14
+ }
15
+
16
+ /**
17
+ * Create a new mission from a template and client info.
18
+ * @param {object} template - Mission template (from missions/*.json)
19
+ * @param {object} client - Client info { name, email, notes }
20
+ * @returns {object} The created mission object
21
+ */
22
+ export async function createMission(template, client = {}) {
23
+ await ensureMissionsDir();
24
+
25
+ const id = uuidv4();
26
+ const now = dayjs().toISOString();
27
+
28
+ const mission = {
29
+ id,
30
+ template: template.template || 'custom',
31
+ service_type: template.service_type || 'general',
32
+ tier: template.tier || 'basic',
33
+ name: template.name || 'Untitled Mission',
34
+ description: template.description || '',
35
+ price_usd: template.default_price_usd || 0,
36
+ estimated_hours: template.estimated_hours || 1,
37
+ skills_required: template.skills_required || [],
38
+ deliverables: template.deliverables || [],
39
+ steps: (template.steps || []).map((step, i) => ({
40
+ index: i,
41
+ description: step,
42
+ status: 'pending',
43
+ completed_at: null,
44
+ })),
45
+ client: {
46
+ name: client.name || 'Unknown',
47
+ email: client.email || '',
48
+ notes: client.notes || '',
49
+ },
50
+ status: 'created', // created -> in_progress -> completed -> paid | cancelled
51
+ payment: {
52
+ status: 'unpaid', // unpaid -> pending -> paid
53
+ stripe_payment_id: null,
54
+ payment_link: null,
55
+ paid_at: null,
56
+ },
57
+ created_at: now,
58
+ started_at: null,
59
+ completed_at: null,
60
+ updated_at: now,
61
+ };
62
+
63
+ const missionPath = path.join(MISSIONS_DIR, `${id}.json`);
64
+ await fs.writeJson(missionPath, mission, { spaces: 2 });
65
+
66
+ return mission;
67
+ }
68
+
69
+ /**
70
+ * Start a mission by ID - sets status to in_progress.
71
+ */
72
+ export async function startMission(id) {
73
+ const mission = await getMission(id);
74
+ if (!mission) {
75
+ throw new Error(`Mission not found: ${id}`);
76
+ }
77
+ if (mission.status !== 'created') {
78
+ throw new Error(`Mission ${id} cannot be started (status: ${mission.status})`);
79
+ }
80
+
81
+ mission.status = 'in_progress';
82
+ mission.started_at = dayjs().toISOString();
83
+ mission.updated_at = dayjs().toISOString();
84
+
85
+ const missionPath = path.join(MISSIONS_DIR, `${id}.json`);
86
+ await fs.writeJson(missionPath, mission, { spaces: 2 });
87
+
88
+ return mission;
89
+ }
90
+
91
+ /**
92
+ * Complete a mission by ID - sets status to completed.
93
+ */
94
+ export async function completeMission(id) {
95
+ const mission = await getMission(id);
96
+ if (!mission) {
97
+ throw new Error(`Mission not found: ${id}`);
98
+ }
99
+ if (mission.status !== 'in_progress') {
100
+ throw new Error(`Mission ${id} cannot be completed (status: ${mission.status})`);
101
+ }
102
+
103
+ mission.status = 'completed';
104
+ mission.completed_at = dayjs().toISOString();
105
+ mission.updated_at = dayjs().toISOString();
106
+
107
+ // Mark all steps as completed
108
+ for (const step of mission.steps) {
109
+ if (step.status === 'pending') {
110
+ step.status = 'completed';
111
+ step.completed_at = dayjs().toISOString();
112
+ }
113
+ }
114
+
115
+ const missionPath = path.join(MISSIONS_DIR, `${id}.json`);
116
+ await fs.writeJson(missionPath, mission, { spaces: 2 });
117
+
118
+ return mission;
119
+ }
120
+
121
+ /**
122
+ * Cancel a mission by ID.
123
+ */
124
+ export async function cancelMission(id) {
125
+ const mission = await getMission(id);
126
+ if (!mission) {
127
+ throw new Error(`Mission not found: ${id}`);
128
+ }
129
+ if (mission.status === 'paid') {
130
+ throw new Error(`Mission ${id} is already paid and cannot be cancelled`);
131
+ }
132
+
133
+ mission.status = 'cancelled';
134
+ mission.updated_at = dayjs().toISOString();
135
+
136
+ const missionPath = path.join(MISSIONS_DIR, `${id}.json`);
137
+ await fs.writeJson(missionPath, mission, { spaces: 2 });
138
+
139
+ return mission;
140
+ }
141
+
142
+ /**
143
+ * Update mission step status.
144
+ */
145
+ export async function updateMissionStep(id, stepIndex, status) {
146
+ const mission = await getMission(id);
147
+ if (!mission) {
148
+ throw new Error(`Mission not found: ${id}`);
149
+ }
150
+ if (!mission.steps[stepIndex]) {
151
+ throw new Error(`Step ${stepIndex} not found in mission ${id}`);
152
+ }
153
+
154
+ mission.steps[stepIndex].status = status;
155
+ if (status === 'completed') {
156
+ mission.steps[stepIndex].completed_at = dayjs().toISOString();
157
+ }
158
+ mission.updated_at = dayjs().toISOString();
159
+
160
+ const missionPath = path.join(MISSIONS_DIR, `${id}.json`);
161
+ await fs.writeJson(missionPath, mission, { spaces: 2 });
162
+
163
+ return mission;
164
+ }
165
+
166
+ /**
167
+ * List all missions, optionally filtered by status.
168
+ */
169
+ export async function listMissions(statusFilter = null) {
170
+ await ensureMissionsDir();
171
+
172
+ const files = await fs.readdir(MISSIONS_DIR);
173
+ const jsonFiles = files.filter((f) => f.endsWith('.json'));
174
+
175
+ const missions = [];
176
+ for (const file of jsonFiles) {
177
+ try {
178
+ const mission = await fs.readJson(path.join(MISSIONS_DIR, file));
179
+ if (!statusFilter || mission.status === statusFilter) {
180
+ missions.push(mission);
181
+ }
182
+ } catch (err) {
183
+ // Skip corrupted files
184
+ continue;
185
+ }
186
+ }
187
+
188
+ // Sort by created_at descending
189
+ missions.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
190
+ return missions;
191
+ }
192
+
193
+ /**
194
+ * Get a single mission by ID.
195
+ */
196
+ export async function getMission(id) {
197
+ const missionPath = path.join(MISSIONS_DIR, `${id}.json`);
198
+ try {
199
+ const exists = await fs.pathExists(missionPath);
200
+ if (!exists) return null;
201
+ return await fs.readJson(missionPath);
202
+ } catch (err) {
203
+ return null;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Get mission statistics.
209
+ */
210
+ export async function getMissionStats() {
211
+ const all = await listMissions();
212
+ return {
213
+ total: all.length,
214
+ created: all.filter((m) => m.status === 'created').length,
215
+ in_progress: all.filter((m) => m.status === 'in_progress').length,
216
+ completed: all.filter((m) => m.status === 'completed').length,
217
+ cancelled: all.filter((m) => m.status === 'cancelled').length,
218
+ paid: all.filter((m) => m.status === 'paid').length,
219
+ total_value: all.reduce((sum, m) => sum + (m.price_usd || 0), 0),
220
+ completed_value: all
221
+ .filter((m) => m.status === 'completed' || m.status === 'paid')
222
+ .reduce((sum, m) => sum + (m.price_usd || 0), 0),
223
+ };
224
+ }
@@ -0,0 +1,139 @@
1
+ import { listMissions } from './mission-runner.js';
2
+ import { loadConfig } from '../cli/utils/config.js';
3
+
4
+ let heartbeatTimer = null;
5
+ let isRunning = false;
6
+
7
+ /**
8
+ * Start the heartbeat scheduler that periodically checks for
9
+ * pending missions and unpaid invoices.
10
+ * @param {number} intervalMs - Interval in milliseconds (default: 60000)
11
+ */
12
+ export function startHeartbeat(intervalMs = 60000) {
13
+ if (isRunning) {
14
+ console.log('[scheduler] Heartbeat already running.');
15
+ return;
16
+ }
17
+
18
+ isRunning = true;
19
+ console.log(`[scheduler] Heartbeat started (interval: ${intervalMs}ms)`);
20
+
21
+ // Run immediately on start
22
+ tick();
23
+
24
+ heartbeatTimer = setInterval(tick, intervalMs);
25
+ }
26
+
27
+ /**
28
+ * Stop the heartbeat scheduler.
29
+ */
30
+ export function stopHeartbeat() {
31
+ if (heartbeatTimer) {
32
+ clearInterval(heartbeatTimer);
33
+ heartbeatTimer = null;
34
+ }
35
+ isRunning = false;
36
+ console.log('[scheduler] Heartbeat stopped.');
37
+ }
38
+
39
+ /**
40
+ * Single tick of the heartbeat: check pending work.
41
+ */
42
+ async function tick() {
43
+ try {
44
+ await checkPendingMissions();
45
+ await checkUnpaidInvoices();
46
+ } catch (err) {
47
+ console.error(`[scheduler] Heartbeat tick error: ${err.message}`);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check for missions that are created but not started,
53
+ * or in_progress missions that may need attention.
54
+ * @returns {object} Summary of pending missions
55
+ */
56
+ export async function checkPendingMissions() {
57
+ try {
58
+ const created = await listMissions('created');
59
+ const inProgress = await listMissions('in_progress');
60
+
61
+ const summary = {
62
+ pending_start: created.length,
63
+ in_progress: inProgress.length,
64
+ missions_needing_attention: [],
65
+ };
66
+
67
+ // Flag missions that have been in_progress for more than 24 hours
68
+ const now = Date.now();
69
+ for (const mission of inProgress) {
70
+ const startedAt = new Date(mission.started_at).getTime();
71
+ const hoursElapsed = (now - startedAt) / (1000 * 60 * 60);
72
+ if (hoursElapsed > mission.estimated_hours * 2) {
73
+ summary.missions_needing_attention.push({
74
+ id: mission.id,
75
+ name: mission.name,
76
+ hours_elapsed: Math.round(hoursElapsed * 10) / 10,
77
+ estimated_hours: mission.estimated_hours,
78
+ });
79
+ }
80
+ }
81
+
82
+ if (summary.pending_start > 0 || summary.missions_needing_attention.length > 0) {
83
+ console.log(
84
+ `[scheduler] Pending: ${summary.pending_start} to start, ` +
85
+ `${summary.in_progress} in progress, ` +
86
+ `${summary.missions_needing_attention.length} overdue`
87
+ );
88
+ }
89
+
90
+ return summary;
91
+ } catch (err) {
92
+ console.error(`[scheduler] Error checking pending missions: ${err.message}`);
93
+ return { pending_start: 0, in_progress: 0, missions_needing_attention: [] };
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Check for completed missions that haven't been paid yet.
99
+ * @returns {object} Summary of unpaid invoices
100
+ */
101
+ export async function checkUnpaidInvoices() {
102
+ try {
103
+ const completed = await listMissions('completed');
104
+
105
+ const unpaid = completed.filter(
106
+ (m) => m.payment && m.payment.status === 'unpaid'
107
+ );
108
+
109
+ const summary = {
110
+ unpaid_count: unpaid.length,
111
+ unpaid_total: unpaid.reduce((sum, m) => sum + (m.price_usd || 0), 0),
112
+ unpaid_missions: unpaid.map((m) => ({
113
+ id: m.id,
114
+ name: m.name,
115
+ amount: m.price_usd,
116
+ client: m.client?.name || 'Unknown',
117
+ completed_at: m.completed_at,
118
+ })),
119
+ };
120
+
121
+ if (summary.unpaid_count > 0) {
122
+ console.log(
123
+ `[scheduler] Unpaid invoices: ${summary.unpaid_count} ($${summary.unpaid_total})`
124
+ );
125
+ }
126
+
127
+ return summary;
128
+ } catch (err) {
129
+ console.error(`[scheduler] Error checking unpaid invoices: ${err.message}`);
130
+ return { unpaid_count: 0, unpaid_total: 0, unpaid_missions: [] };
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Check if heartbeat is currently running.
136
+ */
137
+ export function isHeartbeatRunning() {
138
+ return isRunning;
139
+ }