backlinkflow 0.1.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/CREDITS.md +63 -0
- package/LICENSE +21 -0
- package/README.md +320 -0
- package/bin/linkflow.js +22 -0
- package/data/directories.yaml +7175 -0
- package/dist/ai.js +100 -0
- package/dist/ai.js.map +7 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +7 -0
- package/dist/database.js +66 -0
- package/dist/database.js.map +7 -0
- package/dist/directories.yaml +7175 -0
- package/dist/engine/adapters.js +90 -0
- package/dist/engine/adapters.js.map +7 -0
- package/dist/engine/browser.js +59 -0
- package/dist/engine/browser.js.map +7 -0
- package/dist/engine/fields.js +58 -0
- package/dist/engine/fields.js.map +7 -0
- package/dist/engine/submit.js +90 -0
- package/dist/engine/submit.js.map +7 -0
- package/dist/index.js +273 -0
- package/dist/index.js.map +7 -0
- package/dist/payload.js +63 -0
- package/dist/payload.js.map +7 -0
- package/dist/report.js +42 -0
- package/dist/report.js.map +7 -0
- package/dist/tracker.js +46 -0
- package/dist/tracker.js.map +7 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +7 -0
- package/llms.txt +13 -0
- package/package.json +50 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["#!/usr/bin/env node\n/**\n * LinkFlow \u2014 zero-cost backlink & directory submission engine.\n *\n * Commands:\n * linkflow list [--category X] [--limit N] List directories in the database\n * linkflow search <query> Search directories\n * linkflow submit <site-url> [--dry-run] [--limit N] [--category X] Generate payloads + submit plan\n * linkflow payload <site-url> [--directory X] Generate AI-tailored payload only (no submission)\n * linkflow status Show submission tracker summary\n * linkflow report Regenerate report from tracker\n * linkflow stats Database stats\n * linkflow db:review Flag DB quality issues (dead links, homepage-as-submit)\n * linkflow db:regenerate Rebuild DB from source lists (scripts/regenerate-db.py)\n * linkflow init Print config template\n */\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { loadDirectories, searchDirectories, categories, dbStats } from './database.js';\nimport { loadConfig, hasAiConfig, printConfigSummary } from './config.js';\nimport { resetAiCallCount, getAiCallCount } from './ai.js';\nimport { generatePayload } from './payload.js';\nimport { loadTracker, alreadySubmitted, recordSubmission, trackerSummary } from './tracker.js';\nimport { writeReport, reportPath } from './report.js';\nimport { submitOne } from './engine/submit.js';\n\nconst rawArgs = process.argv.slice(2);\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst VERB = rawArgs[0] && !rawArgs[0].startsWith('--') ? rawArgs[0] : 'list';\nconst VERB_ARG = rawArgs[1] && !rawArgs[1].startsWith('--') ? rawArgs[1] : null;\n\nconst flag = (name: string): string | undefined => {\n const i = rawArgs.indexOf(name);\n return i !== -1 ? rawArgs[i + 1] : undefined;\n};\nconst has = (name: string): boolean => rawArgs.includes(name);\n\nconst DRY_RUN = has('--dry-run');\nconst LIMIT = parseInt(flag('--limit') || '0') || 0;\nconst CONFIG_FILE = flag('--config');\n\nfunction formatDir(d: any, i: number): string {\n return `${String(i + 1).padStart(3)}. ${d.name.padEnd(28)} [${d.category.padEnd(22)}] auto=${d.auto} ${d.dr ? `DR${d.dr}` : ''} ${d.status && d.status !== 'active' ? `(${d.status})` : ''}`;\n}\n\nasync function cmdList(): Promise<void> {\n const cat = flag('--category');\n const all = loadDirectories();\n const filtered = cat ? all.filter((d) => d.category === cat) : all;\n console.log(`\\nLinkFlow directory database: ${all.length} sites\\n`);\n for (const [i, d] of filtered.entries()) {\n if (LIMIT && i >= LIMIT) break;\n console.log(formatDir(d, i));\n }\n console.log(`\\n Categories: ${categories().join(', ')}`);\n}\n\nasync function cmdSearch(): Promise<void> {\n const q = VERB_ARG || '';\n const results = searchDirectories(q);\n console.log(`\\nSearch \"${q}\": ${results.length} matches\\n`);\n for (const [i, d] of results.entries()) {\n if (LIMIT && i >= LIMIT) break;\n console.log(formatDir(d, i));\n console.log(` ${d.submitUrl}`);\n }\n}\n\nasync function cmdSubmit(): Promise<void> {\n const siteUrl = VERB_ARG;\n if (!siteUrl) {\n console.log('Usage: linkflow submit <site-url> [--dry-run] [--limit N] [--category X] [--go]');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n console.log('\\nLinkFlow submit');\n console.log('\u2500'.repeat(50));\n printConfigSummary(cfg);\n\n if (!hasAiConfig(cfg)) {\n console.log(' \u26A0\uFE0F No AI config (.env.local AI_BASE_URL/AI_API_KEY) \u2014 using template payloads.');\n }\n\n const cat = flag('--category');\n const targets = loadDirectories().filter((d) => {\n if (cat && d.category !== cat) return false;\n return d.auto === 'yes' || d.auto === 'manual'; // skip dead/paid-only\n });\n const selected = LIMIT ? targets.slice(0, LIMIT) : targets;\n\n console.log(`\\n Target directories: ${selected.length} (auto+manual, ${cat || 'all categories'})\\n`);\n resetAiCallCount();\n\n const records = loadTracker();\n let submitted = 0, skipped = 0, failed = 0;\n\n for (const [i, dir] of selected.entries()) {\n if (alreadySubmitted(siteUrl, dir.name)) {\n console.log(` [${i + 1}/${selected.length}] \u23ED\uFE0F ${dir.name} \u2014 already submitted (tracked)`);\n skipped++;\n continue;\n }\n const payload = await generatePayload(dir, cfg);\n console.log(` [${i + 1}/${selected.length}] \uD83D\uDE80 ${dir.name}`);\n console.log(` tagline: ${payload.tagline.slice(0, 80)}`);\n console.log(` submit: ${dir.submitUrl}`);\n\n if (has('--go')) {\n // REAL automation via Playwright\n const result = await submitOne(dir, payload, {\n siteUrl,\n proofDir: '.linkflow/proofs',\n });\n console.log(` \u2192 ${result.status}: ${result.note || ''}${result.proof ? ` (proof: ${result.proof})` : ''}`);\n if (result.status === 'submitted') submitted++;\n else if (result.status === 'failed') failed++;\n\n // pacing between submissions (respect per-day limit)\n const perDay = cfg.pacing?.perDay ?? 10;\n if (i + 1 >= perDay && i + 1 < selected.length) {\n console.log(` \u23F8\uFE0F Daily pacing limit (${perDay}) reached \u2014 stopping.`);\n break;\n }\n if (i + 1 < selected.length) {\n const pause = (cfg.pacing?.minSeconds ?? 60) * 1000;\n console.log(` \u23F3 pacing ${pause / 1000}s before next\u2026`);\n await new Promise((r) => setTimeout(r, pause));\n }\n } else {\n // plan-only mode (v0.1 behavior)\n console.log(` desc: ${payload.description.slice(0, 100)}`);\n if (!DRY_RUN) {\n recordSubmission({\n site: siteUrl,\n directory: dir.name,\n status: 'pending',\n submittedAt: new Date().toISOString(),\n url: dir.submitUrl,\n notes: 'planned \u2014 run with --go for automation',\n });\n submitted++;\n }\n }\n }\n\n console.log(`\\n Done: ${submitted} submitted/planned, ${skipped} skipped, ${failed} failed. AI calls: ${getAiCallCount()}`);\n\n if (!DRY_RUN) {\n const all = loadTracker();\n writeReport(siteUrl, all.filter((r) => r.site === siteUrl));\n const { md } = reportPath();\n console.log(` Report: ${md}`);\n } else {\n console.log(' (dry-run \u2014 nothing recorded)');\n }\n}\n\nasync function cmdPayload(): Promise<void> {\n const siteUrl = VERB_ARG;\n if (!siteUrl) {\n console.log('Usage: linkflow payload <site-url> [--directory X]');\n process.exit(1);\n }\n const cfg = loadConfig(CONFIG_FILE);\n const dirName = flag('--directory');\n const dirs = dirName ? loadDirectories().filter((d) => d.name.toLowerCase().includes(dirName.toLowerCase())) : loadDirectories();\n if (!dirs.length) {\n console.log(' No matching directory.');\n return;\n }\n for (const dir of dirs.slice(0, 3)) {\n const payload = await generatePayload(dir, cfg);\n console.log(`\\n=== ${dir.name} ===`);\n console.log(JSON.stringify(payload, null, 2));\n }\n}\n\nfunction cmdStatus(): void {\n const s = trackerSummary();\n console.log('\\nLinkFlow tracker');\n console.log('\u2500'.repeat(50));\n console.log(` Total records: ${s.total}`);\n for (const [k, v] of Object.entries(s.byStatus)) console.log(` ${k}: ${v}`);\n if (s.sites.length) console.log(` Sites: ${s.sites.join(', ')}`);\n}\n\nfunction cmdReport(): void {\n const cfg = loadConfig();\n const site = VERB_ARG || cfg.siteUrl || 'all';\n const all = loadTracker();\n const filtered = site === 'all' ? all : all.filter((r) => r.site === site);\n writeReport(site, filtered);\n const { md } = reportPath();\n console.log(`\\n Report written: ${md}`);\n}\n\nfunction cmdStats(): void {\n const s = dbStats();\n console.log('\\nLinkFlow database stats');\n console.log('\u2500'.repeat(50));\n console.log(` Total directories: ${s.total}`);\n for (const [k, v] of Object.entries(s.byCategory)) console.log(` ${k}: ${v}`);\n console.log(` Auto-submittable: ${s.auto}`);\n}\n\nfunction cmdInit(): void {\n const tpl = {\n siteName: 'My Product',\n siteUrl: 'https://example.com',\n siteDescription: 'A short, honest description of what it is and who it is for.',\n tags: ['saas', 'devtools'],\n contentDomain: 'SaaS product',\n writingSample: 'Paste 2-3 sentences in your site voice here.',\n ai: {\n provider: 'openai',\n baseUrl: 'http://192.168.0.254:20128/v1',\n apiKey: 'your-omniroute-key',\n model: 'auto/best-free',\n maxCallsPerRun: 20,\n },\n pacing: { minSeconds: 60, perDay: 10 },\n };\n const p = path.join(process.cwd(), 'linkflow.config.json');\n if (fs.existsSync(p)) {\n console.log(` ${p} already exists \u2014 not overwriting.`);\n } else {\n fs.writeFileSync(p, JSON.stringify(tpl, null, 2));\n console.log(` Created ${p}`);\n }\n}\n\nasync function cmdDbReview(): Promise<void> {\n const { execSync } = await import('child_process');\n const script = path.join(__dirname, '..', 'scripts', 'review-db.py');\n if (!fs.existsSync(script)) {\n console.log(' review-db.py not found \u2014 run from repo root.');\n return;\n }\n try {\n const out = execSync(`python3 \"${script}\"`, { encoding: 'utf8' });\n console.log(out);\n } catch (err) {\n console.log(' Review failed:', (err as Error).message);\n }\n}\n\nasync function cmdDbRegenerate(): Promise<void> {\n const { execSync } = await import('child_process');\n const script = path.join(__dirname, '..', 'scripts', 'regenerate-db.py');\n if (!fs.existsSync(script)) {\n console.log(' regenerate-db.py not found \u2014 run from repo root.');\n return;\n }\n try {\n const out = execSync(`python3 \"${script}\"`, { encoding: 'utf8' });\n console.log(out);\n } catch (err) {\n console.log(' Regenerate failed:', (err as Error).message);\n }\n}\n\nasync function main(): Promise<void> {\n switch (VERB) {\n case 'list': await cmdList(); break;\n case 'search': await cmdSearch(); break;\n case 'submit': await cmdSubmit(); break;\n case 'payload': await cmdPayload(); break;\n case 'status': cmdStatus(); break;\n case 'report': cmdReport(); break;\n case 'stats': cmdStats(); break;\n case 'db:review': await cmdDbReview(); break;\n case 'db:regenerate': await cmdDbRegenerate(); break;\n case 'init': cmdInit(); break;\n default:\n console.log(`Unknown command: ${VERB}\\nRun 'linkflow' with: list | search | submit | payload | status | report | stats | db:review | db:regenerate | init`);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n console.error('LinkFlow error:', err);\n process.exit(1);\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/payload.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { aiChatWithRetry } from "./ai.js";
|
|
2
|
+
function slugify(s) {
|
|
3
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4
|
+
}
|
|
5
|
+
function templatePayload(dir, cfg) {
|
|
6
|
+
const desc = cfg.siteDescription || `${cfg.siteName} \u2014 ${cfg.contentDomain || "a product"}.`;
|
|
7
|
+
return {
|
|
8
|
+
directory: dir.name,
|
|
9
|
+
name: cfg.siteName,
|
|
10
|
+
tagline: desc.split(".")[0].slice(0, 60),
|
|
11
|
+
description: desc,
|
|
12
|
+
category: dir.category,
|
|
13
|
+
website: cfg.siteUrl,
|
|
14
|
+
fields: {
|
|
15
|
+
url: cfg.siteUrl,
|
|
16
|
+
name: cfg.siteName,
|
|
17
|
+
description: desc
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function aiPayload(dir, cfg) {
|
|
22
|
+
const sys = `You are a launch-copy specialist. Write a concise, honest submission for a directory. Output ONLY valid JSON with keys: name, tagline, description, category. Description: 2-3 sentences, no hype, no buzzwords, include what it is and who it's for.`;
|
|
23
|
+
const prompt = [
|
|
24
|
+
`Directory: ${dir.name} (${dir.category})`,
|
|
25
|
+
`Submit URL: ${dir.submitUrl}`,
|
|
26
|
+
`Site: ${cfg.siteName}`,
|
|
27
|
+
`URL: ${cfg.siteUrl}`,
|
|
28
|
+
`About: ${cfg.siteDescription || "(none provided)"}`,
|
|
29
|
+
cfg.tags?.length ? `Tags: ${cfg.tags.join(", ")}` : "",
|
|
30
|
+
cfg.writingSample ? `Writing sample (match this voice):
|
|
31
|
+
${cfg.writingSample.slice(0, 500)}` : ""
|
|
32
|
+
].filter(Boolean).join("\n");
|
|
33
|
+
const raw = await aiChatWithRetry(prompt, `payload-${slugify(dir.name)}`, {
|
|
34
|
+
system: sys,
|
|
35
|
+
maxTokens: 400
|
|
36
|
+
});
|
|
37
|
+
if (!raw) return null;
|
|
38
|
+
try {
|
|
39
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
40
|
+
if (!m) return null;
|
|
41
|
+
const data = JSON.parse(m[0]);
|
|
42
|
+
return {
|
|
43
|
+
directory: dir.name,
|
|
44
|
+
name: data.name || cfg.siteName,
|
|
45
|
+
tagline: data.tagline || "",
|
|
46
|
+
description: data.description || "",
|
|
47
|
+
category: data.category || dir.category,
|
|
48
|
+
website: cfg.siteUrl
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function generatePayload(dir, cfg) {
|
|
55
|
+
const ai = await aiPayload(dir, cfg);
|
|
56
|
+
return ai || templatePayload(dir, cfg);
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
aiPayload,
|
|
60
|
+
generatePayload,
|
|
61
|
+
templatePayload
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=payload.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/payload.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LinkFlow \u2014 payload generator.\n * Produces per-directory submission payloads (tagline, description, category) \u2014\n * AI-tailored when configured, template-based otherwise (zero-cost fallback).\n */\nimport type { DirectoryEntry, LinkFlowConfig, Payload } from './types.js';\nimport { aiChatWithRetry } from './ai.js';\n\nfunction slugify(s: string): string {\n return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');\n}\n\n/** Template fallback \u2014 no AI call needed. */\nexport function templatePayload(dir: DirectoryEntry, cfg: LinkFlowConfig): Payload {\n const desc = cfg.siteDescription || `${cfg.siteName} \u2014 ${cfg.contentDomain || 'a product'}.`;\n return {\n directory: dir.name,\n name: cfg.siteName,\n tagline: desc.split('.')[0].slice(0, 60),\n description: desc,\n category: dir.category,\n website: cfg.siteUrl,\n fields: {\n url: cfg.siteUrl,\n name: cfg.siteName,\n description: desc,\n },\n };\n}\n\n/** AI-tailored payload \u2014 uses the site's writing sample for voice consistency. */\nexport async function aiPayload(dir: DirectoryEntry, cfg: LinkFlowConfig): Promise<Payload | null> {\n const sys = `You are a launch-copy specialist. Write a concise, honest submission for a directory. Output ONLY valid JSON with keys: name, tagline, description, category. Description: 2-3 sentences, no hype, no buzzwords, include what it is and who it's for.`;\n\n const prompt = [\n `Directory: ${dir.name} (${dir.category})`,\n `Submit URL: ${dir.submitUrl}`,\n `Site: ${cfg.siteName}`,\n `URL: ${cfg.siteUrl}`,\n `About: ${cfg.siteDescription || '(none provided)'}`,\n cfg.tags?.length ? `Tags: ${cfg.tags.join(', ')}` : '',\n cfg.writingSample ? `Writing sample (match this voice):\\n${cfg.writingSample.slice(0, 500)}` : '',\n ].filter(Boolean).join('\\n');\n\n const raw = await aiChatWithRetry(prompt, `payload-${slugify(dir.name)}`, {\n system: sys,\n maxTokens: 400,\n });\n if (!raw) return null;\n\n try {\n const m = raw.match(/\\{[\\s\\S]*\\}/);\n if (!m) return null;\n const data = JSON.parse(m[0]);\n return {\n directory: dir.name,\n name: data.name || cfg.siteName,\n tagline: data.tagline || '',\n description: data.description || '',\n category: data.category || dir.category,\n website: cfg.siteUrl,\n };\n } catch {\n return null;\n }\n}\n\nexport async function generatePayload(dir: DirectoryEntry, cfg: LinkFlowConfig): Promise<Payload> {\n const ai = await aiPayload(dir, cfg);\n return ai || templatePayload(dir, cfg);\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,uBAAuB;AAEhC,SAAS,QAAQ,GAAmB;AAClC,SAAO,EAAE,YAAY,EAAE,QAAQ,eAAe,GAAG,EAAE,QAAQ,YAAY,EAAE;AAC3E;AAGO,SAAS,gBAAgB,KAAqB,KAA8B;AACjF,QAAM,OAAO,IAAI,mBAAmB,GAAG,IAAI,QAAQ,WAAM,IAAI,iBAAiB,WAAW;AACzF,SAAO;AAAA,IACL,WAAW,IAAI;AAAA,IACf,MAAM,IAAI;AAAA,IACV,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE;AAAA,IACvC,aAAa;AAAA,IACb,UAAU,IAAI;AAAA,IACd,SAAS,IAAI;AAAA,IACb,QAAQ;AAAA,MACN,KAAK,IAAI;AAAA,MACT,MAAM,IAAI;AAAA,MACV,aAAa;AAAA,IACf;AAAA,EACF;AACF;AAGA,eAAsB,UAAU,KAAqB,KAA8C;AACjG,QAAM,MAAM;AAEZ,QAAM,SAAS;AAAA,IACb,cAAc,IAAI,IAAI,KAAK,IAAI,QAAQ;AAAA,IACvC,eAAe,IAAI,SAAS;AAAA,IAC5B,SAAS,IAAI,QAAQ;AAAA,IACrB,QAAQ,IAAI,OAAO;AAAA,IACnB,UAAU,IAAI,mBAAmB,iBAAiB;AAAA,IAClD,IAAI,MAAM,SAAS,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK;AAAA,IACpD,IAAI,gBAAgB;AAAA,EAAuC,IAAI,cAAc,MAAM,GAAG,GAAG,CAAC,KAAK;AAAA,EACjG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;AAE3B,QAAM,MAAM,MAAM,gBAAgB,QAAQ,WAAW,QAAQ,IAAI,IAAI,CAAC,IAAI;AAAA,IACxE,QAAQ;AAAA,IACR,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI;AACF,UAAM,IAAI,IAAI,MAAM,aAAa;AACjC,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC;AAC5B,WAAO;AAAA,MACL,WAAW,IAAI;AAAA,MACf,MAAM,KAAK,QAAQ,IAAI;AAAA,MACvB,SAAS,KAAK,WAAW;AAAA,MACzB,aAAa,KAAK,eAAe;AAAA,MACjC,UAAU,KAAK,YAAY,IAAI;AAAA,MAC/B,SAAS,IAAI;AAAA,IACf;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,gBAAgB,KAAqB,KAAuC;AAChG,QAAM,KAAK,MAAM,UAAU,KAAK,GAAG;AACnC,SAAO,MAAM,gBAAgB,KAAK,GAAG;AACvC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const DATA_DIR = path.join(process.cwd(), ".linkflow");
|
|
4
|
+
const REPORT_MD = path.join(DATA_DIR, "report.md");
|
|
5
|
+
const REPORT_JSON = path.join(DATA_DIR, "report.json");
|
|
6
|
+
function writeReport(site, records) {
|
|
7
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
8
|
+
const byStatus = records.reduce((acc, r) => {
|
|
9
|
+
acc[r.status] = (acc[r.status] || 0) + 1;
|
|
10
|
+
return acc;
|
|
11
|
+
}, {});
|
|
12
|
+
const lines = [
|
|
13
|
+
`# LinkFlow Report \u2014 ${site}`,
|
|
14
|
+
"",
|
|
15
|
+
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
16
|
+
"",
|
|
17
|
+
`## Summary`,
|
|
18
|
+
"",
|
|
19
|
+
`| Status | Count |`,
|
|
20
|
+
`|---|---|`,
|
|
21
|
+
...Object.entries(byStatus).map(([k, v]) => `| ${k} | ${v} |`),
|
|
22
|
+
"",
|
|
23
|
+
`## Submissions`,
|
|
24
|
+
"",
|
|
25
|
+
`| Directory | Status | Submitted | URL | Proof |`,
|
|
26
|
+
`|---|---|---|---|---|`,
|
|
27
|
+
...records.map(
|
|
28
|
+
(r) => `| ${r.directory} | ${r.status} | ${r.submittedAt} | ${r.url || "\u2014"} | ${r.proof || "\u2014"} |`
|
|
29
|
+
),
|
|
30
|
+
""
|
|
31
|
+
];
|
|
32
|
+
fs.writeFileSync(REPORT_MD, lines.join("\n"));
|
|
33
|
+
fs.writeFileSync(REPORT_JSON, JSON.stringify({ site, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), records }, null, 2));
|
|
34
|
+
}
|
|
35
|
+
function reportPath() {
|
|
36
|
+
return { md: REPORT_MD, json: REPORT_JSON };
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
reportPath,
|
|
40
|
+
writeReport
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/report.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LinkFlow \u2014 proof-of-submission report generator.\n * Produces .linkflow/report.md + .linkflow/report.json (Submitator's \"detailed report\" feature, open-source).\n */\nimport fs from 'fs';\nimport path from 'path';\nimport type { SubmissionRecord } from './types.js';\n\nconst DATA_DIR = path.join(process.cwd(), '.linkflow');\nconst REPORT_MD = path.join(DATA_DIR, 'report.md');\nconst REPORT_JSON = path.join(DATA_DIR, 'report.json');\n\nexport function writeReport(site: string, records: SubmissionRecord[]): void {\n fs.mkdirSync(DATA_DIR, { recursive: true });\n\n const byStatus = records.reduce<Record<string, number>>((acc, r) => {\n acc[r.status] = (acc[r.status] || 0) + 1;\n return acc;\n }, {});\n\n const lines: string[] = [\n `# LinkFlow Report \u2014 ${site}`,\n '',\n `Generated: ${new Date().toISOString()}`,\n '',\n `## Summary`,\n '',\n `| Status | Count |`,\n `|---|---|`,\n ...Object.entries(byStatus).map(([k, v]) => `| ${k} | ${v} |`),\n '',\n `## Submissions`,\n '',\n `| Directory | Status | Submitted | URL | Proof |`,\n `|---|---|---|---|---|`,\n ...records.map(\n (r) =>\n `| ${r.directory} | ${r.status} | ${r.submittedAt} | ${r.url || '\u2014'} | ${r.proof || '\u2014'} |`\n ),\n '',\n ];\n\n fs.writeFileSync(REPORT_MD, lines.join('\\n'));\n fs.writeFileSync(REPORT_JSON, JSON.stringify({ site, generatedAt: new Date().toISOString(), records }, null, 2));\n}\n\nexport function reportPath(): { md: string; json: string } {\n return { md: REPORT_MD, json: REPORT_JSON };\n}\n"],
|
|
5
|
+
"mappings": "AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,MAAM,WAAW,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW;AACrD,MAAM,YAAY,KAAK,KAAK,UAAU,WAAW;AACjD,MAAM,cAAc,KAAK,KAAK,UAAU,aAAa;AAE9C,SAAS,YAAY,MAAc,SAAmC;AAC3E,KAAG,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAE1C,QAAM,WAAW,QAAQ,OAA+B,CAAC,KAAK,MAAM;AAClE,QAAI,EAAE,MAAM,KAAK,IAAI,EAAE,MAAM,KAAK,KAAK;AACvC,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,QAAkB;AAAA,IACtB,4BAAuB,IAAI;AAAA,IAC3B;AAAA,IACA,eAAc,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,IACtC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG,OAAO,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI;AAAA,IAC7D;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG,QAAQ;AAAA,MACT,CAAC,MACC,KAAK,EAAE,SAAS,MAAM,EAAE,MAAM,MAAM,EAAE,WAAW,MAAM,EAAE,OAAO,QAAG,MAAM,EAAE,SAAS,QAAG;AAAA,IAC3F;AAAA,IACA;AAAA,EACF;AAEA,KAAG,cAAc,WAAW,MAAM,KAAK,IAAI,CAAC;AAC5C,KAAG,cAAc,aAAa,KAAK,UAAU,EAAE,MAAM,cAAa,oBAAI,KAAK,GAAE,YAAY,GAAG,QAAQ,GAAG,MAAM,CAAC,CAAC;AACjH;AAEO,SAAS,aAA2C;AACzD,SAAO,EAAE,IAAI,WAAW,MAAM,YAAY;AAC5C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const DATA_DIR = path.join(process.cwd(), ".linkflow");
|
|
4
|
+
const TRACKER_PATH = path.join(DATA_DIR, "tracker.json");
|
|
5
|
+
function loadTracker() {
|
|
6
|
+
if (!fs.existsSync(TRACKER_PATH)) return [];
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(fs.readFileSync(TRACKER_PATH, "utf8"));
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function saveTracker(records) {
|
|
14
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
15
|
+
fs.writeFileSync(TRACKER_PATH, JSON.stringify(records, null, 2));
|
|
16
|
+
}
|
|
17
|
+
function alreadySubmitted(site, directory) {
|
|
18
|
+
return loadTracker().some(
|
|
19
|
+
(r) => r.site === site && r.directory === directory && r.status !== "failed"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function recordSubmission(rec) {
|
|
23
|
+
const records = loadTracker();
|
|
24
|
+
const idx = records.findIndex((r) => r.site === rec.site && r.directory === rec.directory);
|
|
25
|
+
if (idx !== -1) records[idx] = rec;
|
|
26
|
+
else records.push(rec);
|
|
27
|
+
saveTracker(records);
|
|
28
|
+
}
|
|
29
|
+
function trackerSummary() {
|
|
30
|
+
const records = loadTracker();
|
|
31
|
+
const byStatus = {};
|
|
32
|
+
for (const r of records) byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
|
33
|
+
return {
|
|
34
|
+
total: records.length,
|
|
35
|
+
byStatus,
|
|
36
|
+
sites: [...new Set(records.map((r) => r.site))]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
alreadySubmitted,
|
|
41
|
+
loadTracker,
|
|
42
|
+
recordSubmission,
|
|
43
|
+
saveTracker,
|
|
44
|
+
trackerSummary
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=tracker.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/tracker.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LinkFlow \u2014 submission tracker.\n * Persists submission history to .linkflow/tracker.json (gitignored).\n */\nimport fs from 'fs';\nimport path from 'path';\nimport type { SubmissionRecord } from './types.js';\n\nconst DATA_DIR = path.join(process.cwd(), '.linkflow');\nconst TRACKER_PATH = path.join(DATA_DIR, 'tracker.json');\n\nexport function loadTracker(): SubmissionRecord[] {\n if (!fs.existsSync(TRACKER_PATH)) return [];\n try {\n return JSON.parse(fs.readFileSync(TRACKER_PATH, 'utf8'));\n } catch {\n return [];\n }\n}\n\nexport function saveTracker(records: SubmissionRecord[]): void {\n fs.mkdirSync(DATA_DIR, { recursive: true });\n fs.writeFileSync(TRACKER_PATH, JSON.stringify(records, null, 2));\n}\n\nexport function alreadySubmitted(site: string, directory: string): boolean {\n return loadTracker().some(\n (r) => r.site === site && r.directory === directory && r.status !== 'failed'\n );\n}\n\nexport function recordSubmission(rec: SubmissionRecord): void {\n const records = loadTracker();\n // upsert: replace any existing record for same site+directory\n const idx = records.findIndex((r) => r.site === rec.site && r.directory === rec.directory);\n if (idx !== -1) records[idx] = rec;\n else records.push(rec);\n saveTracker(records);\n}\n\nexport function trackerSummary(): { total: number; byStatus: Record<string, number>; sites: string[] } {\n const records = loadTracker();\n const byStatus: Record<string, number> = {};\n for (const r of records) byStatus[r.status] = (byStatus[r.status] || 0) + 1;\n return {\n total: records.length,\n byStatus,\n sites: [...new Set(records.map((r) => r.site))],\n };\n}\n"],
|
|
5
|
+
"mappings": "AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,MAAM,WAAW,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW;AACrD,MAAM,eAAe,KAAK,KAAK,UAAU,cAAc;AAEhD,SAAS,cAAkC;AAChD,MAAI,CAAC,GAAG,WAAW,YAAY,EAAG,QAAO,CAAC;AAC1C,MAAI;AACF,WAAO,KAAK,MAAM,GAAG,aAAa,cAAc,MAAM,CAAC;AAAA,EACzD,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAEO,SAAS,YAAY,SAAmC;AAC7D,KAAG,UAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AAC1C,KAAG,cAAc,cAAc,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AACjE;AAEO,SAAS,iBAAiB,MAAc,WAA4B;AACzE,SAAO,YAAY,EAAE;AAAA,IACnB,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,cAAc,aAAa,EAAE,WAAW;AAAA,EACtE;AACF;AAEO,SAAS,iBAAiB,KAA6B;AAC5D,QAAM,UAAU,YAAY;AAE5B,QAAM,MAAM,QAAQ,UAAU,CAAC,MAAM,EAAE,SAAS,IAAI,QAAQ,EAAE,cAAc,IAAI,SAAS;AACzF,MAAI,QAAQ,GAAI,SAAQ,GAAG,IAAI;AAAA,MAC1B,SAAQ,KAAK,GAAG;AACrB,cAAY,OAAO;AACrB;AAEO,SAAS,iBAAuF;AACrG,QAAM,UAAU,YAAY;AAC5B,QAAM,WAAmC,CAAC;AAC1C,aAAW,KAAK,QAAS,UAAS,EAAE,MAAM,KAAK,SAAS,EAAE,MAAM,KAAK,KAAK;AAC1E,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf;AAAA,IACA,OAAO,CAAC,GAAG,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAAA,EAChD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
package/llms.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# LinkFlow
|
|
2
|
+
|
|
3
|
+
Zero-cost backlink & directory submission engine for indie hackers.
|
|
4
|
+
|
|
5
|
+
## Core files
|
|
6
|
+
- `src/index.ts` — CLI entry (list, search, submit, payload, status, report, stats, init)
|
|
7
|
+
- `src/database.ts` — directories.yaml loader + search
|
|
8
|
+
- `src/payload.ts` — AI/template payload generator
|
|
9
|
+
- `src/ai.ts` — OpenAI-compatible AI client (OmniRoute-ready, SSE-tolerant)
|
|
10
|
+
- `src/tracker.ts` — submission tracker (.linkflow/tracker.json)
|
|
11
|
+
- `src/report.ts` — proof-of-submission report (.linkflow/report.md)
|
|
12
|
+
- `data/directories.yaml` — 258-site vetted directory database
|
|
13
|
+
- `linkflow.config.json` — site + AI config
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backlinkflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LinkFlow — zero-cost backlink & directory submission engine: vetted directory database, AI-tailored submission payloads, tracker, and proof-of-submission reports.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Sankalp Singh",
|
|
8
|
+
"bin": {
|
|
9
|
+
"linkflow": "bin/linkflow.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./*": "./dist/*.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"bin",
|
|
19
|
+
"data/directories.yaml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"CREDITS.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"llms.txt"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node build.mjs",
|
|
27
|
+
"dev": "node build.mjs --watch",
|
|
28
|
+
"linkflow": "node bin/linkflow.js",
|
|
29
|
+
"linkflow:dry": "node bin/linkflow.js submit --dry-run",
|
|
30
|
+
"test": "node tests/run.js"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"seo",
|
|
37
|
+
"backlinks",
|
|
38
|
+
"directories",
|
|
39
|
+
"link-building",
|
|
40
|
+
"startup",
|
|
41
|
+
"launch",
|
|
42
|
+
"automation",
|
|
43
|
+
"ai"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"esbuild": "^0.28.2",
|
|
47
|
+
"js-yaml": "^4.3.2",
|
|
48
|
+
"playwright": "^1.62.1"
|
|
49
|
+
}
|
|
50
|
+
}
|