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
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CashClaw SEO Auditor - Technical Audit Script
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node audit.js --url "https://example.com" [--output audit.json]
|
|
8
|
+
*
|
|
9
|
+
* Fetches a URL, parses the HTML, and generates structured audit data
|
|
10
|
+
* covering meta tags, headings, images, links, and performance signals.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { argv, exit } = require("process");
|
|
14
|
+
const { writeFileSync } = require("fs");
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Argument parsing
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
function parseArgs() {
|
|
21
|
+
const args = {};
|
|
22
|
+
for (let i = 2; i < argv.length; i++) {
|
|
23
|
+
if (argv[i] === "--url" && argv[i + 1]) {
|
|
24
|
+
args.url = argv[++i];
|
|
25
|
+
} else if (argv[i] === "--output" && argv[i + 1]) {
|
|
26
|
+
args.output = argv[++i];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!args.url) {
|
|
30
|
+
console.error("Usage: node audit.js --url <URL> [--output <file.json>]");
|
|
31
|
+
exit(1);
|
|
32
|
+
}
|
|
33
|
+
return args;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// HTML helpers (lightweight, no external deps)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function extractTag(html, tag) {
|
|
41
|
+
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi");
|
|
42
|
+
const matches = [];
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = re.exec(html)) !== null) matches.push(m[1].trim());
|
|
45
|
+
return matches;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractMeta(html, nameOrProperty) {
|
|
49
|
+
// <meta name="..." content="..."> or <meta property="..." content="...">
|
|
50
|
+
const re = new RegExp(
|
|
51
|
+
`<meta[^>]*(?:name|property)=["']${nameOrProperty}["'][^>]*content=["']([^"']*)["']`,
|
|
52
|
+
"i"
|
|
53
|
+
);
|
|
54
|
+
const alt = new RegExp(
|
|
55
|
+
`<meta[^>]*content=["']([^"']*)["'][^>]*(?:name|property)=["']${nameOrProperty}["']`,
|
|
56
|
+
"i"
|
|
57
|
+
);
|
|
58
|
+
const m = html.match(re) || html.match(alt);
|
|
59
|
+
return m ? m[1] : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractTagAttr(html, tag, attr) {
|
|
63
|
+
const re = new RegExp(`<${tag}[^>]*${attr}=["']([^"']*)["']`, "gi");
|
|
64
|
+
const results = [];
|
|
65
|
+
let m;
|
|
66
|
+
while ((m = re.exec(html)) !== null) results.push(m[1]);
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractLinks(html) {
|
|
71
|
+
const re = /<a[^>]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
|
72
|
+
const links = [];
|
|
73
|
+
let m;
|
|
74
|
+
while ((m = re.exec(html)) !== null) {
|
|
75
|
+
links.push({ href: m[1], text: m[2].replace(/<[^>]*>/g, "").trim() });
|
|
76
|
+
}
|
|
77
|
+
return links;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractImages(html) {
|
|
81
|
+
const re = /<img[^>]*>/gi;
|
|
82
|
+
const images = [];
|
|
83
|
+
let m;
|
|
84
|
+
while ((m = re.exec(html)) !== null) {
|
|
85
|
+
const tag = m[0];
|
|
86
|
+
const src = (tag.match(/src=["']([^"']*)["']/i) || [])[1] || "";
|
|
87
|
+
const alt = (tag.match(/alt=["']([^"']*)["']/i) || [])[1] || "";
|
|
88
|
+
const loading = (tag.match(/loading=["']([^"']*)["']/i) || [])[1] || "";
|
|
89
|
+
images.push({ src, alt, loading });
|
|
90
|
+
}
|
|
91
|
+
return images;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function wordCount(html) {
|
|
95
|
+
const text = html
|
|
96
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
97
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
98
|
+
.replace(/<[^>]*>/g, " ")
|
|
99
|
+
.replace(/\s+/g, " ")
|
|
100
|
+
.trim();
|
|
101
|
+
return text ? text.split(" ").length : 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Audit checks
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
function check(name, pass, value, note) {
|
|
109
|
+
return {
|
|
110
|
+
name,
|
|
111
|
+
status: pass === null ? "N/A" : pass ? "PASS" : "FAIL",
|
|
112
|
+
value,
|
|
113
|
+
note: note || "",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function auditTechnical(url, html, headers, timing) {
|
|
118
|
+
const results = [];
|
|
119
|
+
|
|
120
|
+
// HTTPS
|
|
121
|
+
results.push(
|
|
122
|
+
check("HTTPS enabled", url.startsWith("https"), url)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Response time
|
|
126
|
+
results.push(
|
|
127
|
+
check(
|
|
128
|
+
"Server response time",
|
|
129
|
+
timing < 500,
|
|
130
|
+
`${timing}ms`,
|
|
131
|
+
timing < 500 ? "Good" : "Slow - target < 500ms"
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Viewport meta
|
|
136
|
+
const hasViewport = /meta[^>]*name=["']viewport["']/i.test(html);
|
|
137
|
+
results.push(check("Mobile viewport meta", hasViewport, hasViewport));
|
|
138
|
+
|
|
139
|
+
// Canonical
|
|
140
|
+
const canonical = (html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']*)["']/i) || [])[1];
|
|
141
|
+
results.push(check("Canonical tag", !!canonical, canonical || "Missing"));
|
|
142
|
+
|
|
143
|
+
// Language
|
|
144
|
+
const lang = (html.match(/<html[^>]*lang=["']([^"']*)["']/i) || [])[1];
|
|
145
|
+
results.push(check("HTML lang attribute", !!lang, lang || "Missing"));
|
|
146
|
+
|
|
147
|
+
// Charset
|
|
148
|
+
const hasCharset = /meta[^>]*charset/i.test(html);
|
|
149
|
+
results.push(check("Charset declaration", hasCharset, hasCharset));
|
|
150
|
+
|
|
151
|
+
// Compression
|
|
152
|
+
const encoding = headers["content-encoding"] || "";
|
|
153
|
+
const compressed = /gzip|br|deflate/i.test(encoding);
|
|
154
|
+
results.push(check("Compression (gzip/br)", compressed, encoding || "None"));
|
|
155
|
+
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function auditOnPage(html) {
|
|
160
|
+
const results = [];
|
|
161
|
+
|
|
162
|
+
// Title
|
|
163
|
+
const titles = extractTag(html, "title");
|
|
164
|
+
const title = titles[0] || "";
|
|
165
|
+
results.push(
|
|
166
|
+
check(
|
|
167
|
+
"Title tag exists",
|
|
168
|
+
titles.length === 1 && title.length > 0,
|
|
169
|
+
title || "Missing",
|
|
170
|
+
title ? `${title.length} chars` : "No title tag found"
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
results.push(
|
|
174
|
+
check(
|
|
175
|
+
"Title length (50-60 chars)",
|
|
176
|
+
title.length >= 50 && title.length <= 60,
|
|
177
|
+
`${title.length} chars`,
|
|
178
|
+
title.length < 50 ? "Too short" : title.length > 60 ? "Too long" : "Good"
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// Meta description
|
|
183
|
+
const desc = extractMeta(html, "description") || "";
|
|
184
|
+
results.push(
|
|
185
|
+
check("Meta description exists", desc.length > 0, desc || "Missing")
|
|
186
|
+
);
|
|
187
|
+
results.push(
|
|
188
|
+
check(
|
|
189
|
+
"Meta description length (150-160)",
|
|
190
|
+
desc.length >= 150 && desc.length <= 160,
|
|
191
|
+
`${desc.length} chars`
|
|
192
|
+
)
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Headings
|
|
196
|
+
const h1s = extractTag(html, "h1");
|
|
197
|
+
results.push(
|
|
198
|
+
check("H1 tag exists and unique", h1s.length === 1, `${h1s.length} H1 tags found`)
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const h2s = extractTag(html, "h2");
|
|
202
|
+
results.push(
|
|
203
|
+
check("H2 tags present", h2s.length > 0, `${h2s.length} H2 tags`)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Images
|
|
207
|
+
const images = extractImages(html);
|
|
208
|
+
const imagesWithAlt = images.filter((img) => img.alt.length > 0);
|
|
209
|
+
const allHaveAlt = images.length === 0 || imagesWithAlt.length === images.length;
|
|
210
|
+
results.push(
|
|
211
|
+
check(
|
|
212
|
+
"Images have alt text",
|
|
213
|
+
allHaveAlt,
|
|
214
|
+
`${imagesWithAlt.length}/${images.length} have alt`,
|
|
215
|
+
allHaveAlt ? "Good" : "Add alt text to all images"
|
|
216
|
+
)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const lazyLoaded = images.filter((img) => img.loading === "lazy");
|
|
220
|
+
results.push(
|
|
221
|
+
check(
|
|
222
|
+
"Images use lazy loading",
|
|
223
|
+
images.length === 0 || lazyLoaded.length > 0,
|
|
224
|
+
`${lazyLoaded.length}/${images.length} lazy`
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Open Graph
|
|
229
|
+
const ogTitle = extractMeta(html, "og:title");
|
|
230
|
+
const ogDesc = extractMeta(html, "og:description");
|
|
231
|
+
const ogImage = extractMeta(html, "og:image");
|
|
232
|
+
results.push(
|
|
233
|
+
check("Open Graph tags", !!(ogTitle && ogDesc && ogImage), {
|
|
234
|
+
title: ogTitle || "Missing",
|
|
235
|
+
description: ogDesc || "Missing",
|
|
236
|
+
image: ogImage || "Missing",
|
|
237
|
+
})
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Twitter Card
|
|
241
|
+
const twCard = extractMeta(html, "twitter:card");
|
|
242
|
+
results.push(check("Twitter Card meta", !!twCard, twCard || "Missing"));
|
|
243
|
+
|
|
244
|
+
// Word count
|
|
245
|
+
const wc = wordCount(html);
|
|
246
|
+
results.push(
|
|
247
|
+
check("Content length (300+ words)", wc >= 300, `${wc} words`)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Favicon
|
|
251
|
+
const hasFavicon = /<link[^>]*rel=["'](?:shortcut )?icon["']/i.test(html);
|
|
252
|
+
results.push(check("Favicon configured", hasFavicon, hasFavicon));
|
|
253
|
+
|
|
254
|
+
return results;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function auditLinks(html, baseUrl) {
|
|
258
|
+
const links = extractLinks(html);
|
|
259
|
+
const internal = links.filter((l) => {
|
|
260
|
+
try {
|
|
261
|
+
const u = new URL(l.href, baseUrl);
|
|
262
|
+
return u.hostname === new URL(baseUrl).hostname;
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
const external = links.filter((l) => !internal.includes(l));
|
|
268
|
+
const emptyAnchors = links.filter((l) => l.text.length === 0);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
total: links.length,
|
|
272
|
+
internal: internal.length,
|
|
273
|
+
external: external.length,
|
|
274
|
+
emptyAnchors: emptyAnchors.length,
|
|
275
|
+
links: links.slice(0, 50), // cap at 50 for output size
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Score calculation
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
function calcScore(checks) {
|
|
284
|
+
let total = 0;
|
|
285
|
+
let passed = 0;
|
|
286
|
+
for (const c of checks) {
|
|
287
|
+
if (c.status === "N/A") continue;
|
|
288
|
+
total++;
|
|
289
|
+
if (c.status === "PASS") passed++;
|
|
290
|
+
}
|
|
291
|
+
return total === 0 ? 100 : Math.round((passed / total) * 100);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function grade(score) {
|
|
295
|
+
if (score >= 90) return "A";
|
|
296
|
+
if (score >= 80) return "B";
|
|
297
|
+
if (score >= 70) return "C";
|
|
298
|
+
if (score >= 60) return "D";
|
|
299
|
+
return "F";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Main
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
async function main() {
|
|
307
|
+
const args = parseArgs();
|
|
308
|
+
const url = args.url;
|
|
309
|
+
|
|
310
|
+
console.log(`\n CashClaw SEO Auditor`);
|
|
311
|
+
console.log(` Auditing: ${url}\n`);
|
|
312
|
+
|
|
313
|
+
const startTime = Date.now();
|
|
314
|
+
let html, headers, status;
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const response = await fetch(url, {
|
|
318
|
+
headers: {
|
|
319
|
+
"User-Agent":
|
|
320
|
+
"CashClawBot/1.0 (+https://cashclaw.ai) Mozilla/5.0 compatible",
|
|
321
|
+
Accept: "text/html,application/xhtml+xml",
|
|
322
|
+
},
|
|
323
|
+
redirect: "follow",
|
|
324
|
+
});
|
|
325
|
+
status = response.status;
|
|
326
|
+
headers = Object.fromEntries(response.headers.entries());
|
|
327
|
+
html = await response.text();
|
|
328
|
+
} catch (err) {
|
|
329
|
+
console.error(` Failed to fetch ${url}: ${err.message}`);
|
|
330
|
+
exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const timing = Date.now() - startTime;
|
|
334
|
+
|
|
335
|
+
console.log(` Status: ${status}`);
|
|
336
|
+
console.log(` Response time: ${timing}ms`);
|
|
337
|
+
console.log(` Page size: ${(html.length / 1024).toFixed(1)}KB\n`);
|
|
338
|
+
|
|
339
|
+
// Run audits
|
|
340
|
+
const technical = auditTechnical(url, html, headers, timing);
|
|
341
|
+
const onPage = auditOnPage(html);
|
|
342
|
+
const linkData = auditLinks(html, url);
|
|
343
|
+
|
|
344
|
+
const allChecks = [...technical, ...onPage];
|
|
345
|
+
const overallScore = calcScore(allChecks);
|
|
346
|
+
const technicalScore = calcScore(technical);
|
|
347
|
+
const onPageScore = calcScore(onPage);
|
|
348
|
+
|
|
349
|
+
const audit = {
|
|
350
|
+
url,
|
|
351
|
+
audited_at: new Date().toISOString(),
|
|
352
|
+
response: {
|
|
353
|
+
status,
|
|
354
|
+
timing_ms: timing,
|
|
355
|
+
page_size_bytes: html.length,
|
|
356
|
+
content_type: headers["content-type"] || "",
|
|
357
|
+
},
|
|
358
|
+
scores: {
|
|
359
|
+
overall: overallScore,
|
|
360
|
+
overall_grade: grade(overallScore),
|
|
361
|
+
technical: technicalScore,
|
|
362
|
+
technical_grade: grade(technicalScore),
|
|
363
|
+
on_page: onPageScore,
|
|
364
|
+
on_page_grade: grade(onPageScore),
|
|
365
|
+
},
|
|
366
|
+
technical,
|
|
367
|
+
on_page: onPage,
|
|
368
|
+
links: linkData,
|
|
369
|
+
images: extractImages(html).length,
|
|
370
|
+
word_count: wordCount(html),
|
|
371
|
+
headings: {
|
|
372
|
+
h1: extractTag(html, "h1"),
|
|
373
|
+
h2: extractTag(html, "h2"),
|
|
374
|
+
h3: extractTag(html, "h3"),
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Output
|
|
379
|
+
const json = JSON.stringify(audit, null, 2);
|
|
380
|
+
|
|
381
|
+
if (args.output) {
|
|
382
|
+
writeFileSync(args.output, json, "utf-8");
|
|
383
|
+
console.log(` Report saved to: ${args.output}`);
|
|
384
|
+
} else {
|
|
385
|
+
console.log(json);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Summary
|
|
389
|
+
console.log(`\n ---- Score Summary ----`);
|
|
390
|
+
console.log(` Overall: ${overallScore}/100 (${grade(overallScore)})`);
|
|
391
|
+
console.log(` Technical: ${technicalScore}/100 (${grade(technicalScore)})`);
|
|
392
|
+
console.log(` On-Page: ${onPageScore}/100 (${grade(onPageScore)})`);
|
|
393
|
+
console.log(
|
|
394
|
+
` Issues: ${allChecks.filter((c) => c.status === "FAIL").length} failures, ${allChecks.filter((c) => c.status === "PASS").length} passed\n`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
main().catch((err) => {
|
|
399
|
+
console.error(`Fatal error: ${err.message}`);
|
|
400
|
+
exit(1);
|
|
401
|
+
});
|