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
package/dist/ai.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { loadConfig } from "./config.js";
|
|
2
|
+
let _callCount = 0;
|
|
3
|
+
function resetAiCallCount() {
|
|
4
|
+
_callCount = 0;
|
|
5
|
+
}
|
|
6
|
+
function getAiCallCount() {
|
|
7
|
+
return _callCount;
|
|
8
|
+
}
|
|
9
|
+
async function aiChat(prompt, task, opts) {
|
|
10
|
+
const cfg = loadConfig();
|
|
11
|
+
const max = cfg.ai?.maxCallsPerRun ?? 20;
|
|
12
|
+
if (_callCount >= max) {
|
|
13
|
+
console.log(` \u26A0\uFE0F AI budget: ${_callCount}/${max} calls used \u2014 skipping ${task}`);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const baseUrl = (cfg.ai?.baseUrl || "").replace(/\/+$/, "");
|
|
17
|
+
const apiKey = cfg.ai?.apiKey || "sk-local";
|
|
18
|
+
const model = cfg.ai?.model || "auto/best-free";
|
|
19
|
+
if (!baseUrl) {
|
|
20
|
+
console.log(" \u26A0\uFE0F No AI base URL configured \u2014 using template payload (no AI call).");
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const messages = [];
|
|
24
|
+
if (opts?.system) messages.push({ role: "system", content: opts.system });
|
|
25
|
+
messages.push({ role: "user", content: prompt });
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
32
|
+
"Accept": "application/json"
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify({
|
|
35
|
+
model,
|
|
36
|
+
messages,
|
|
37
|
+
max_tokens: opts?.maxTokens ?? 600,
|
|
38
|
+
temperature: 0.7,
|
|
39
|
+
stream: false
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const body = await res.text().catch(() => "");
|
|
44
|
+
console.log(` \u26A0\uFE0F AI error ${res.status}: ${body.slice(0, 200)}`);
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const text = await res.text();
|
|
48
|
+
let data = null;
|
|
49
|
+
const trimmed = text.trim();
|
|
50
|
+
if (trimmed.startsWith("{")) {
|
|
51
|
+
data = JSON.parse(trimmed);
|
|
52
|
+
} else if (trimmed.includes("data:")) {
|
|
53
|
+
for (const line of trimmed.split("\n")) {
|
|
54
|
+
if (!line.startsWith("data:")) continue;
|
|
55
|
+
const payload = line.slice(5).trim();
|
|
56
|
+
if (!payload || payload === "[DONE]") continue;
|
|
57
|
+
try {
|
|
58
|
+
const chunk = JSON.parse(payload);
|
|
59
|
+
if (chunk?.choices?.[0]?.delta?.content) {
|
|
60
|
+
data = chunk;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
if (chunk?.choices?.[0]?.message?.content) {
|
|
64
|
+
data = chunk;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error("Unexpected response format");
|
|
72
|
+
}
|
|
73
|
+
const content = data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.delta?.content ?? null;
|
|
74
|
+
if (!content) {
|
|
75
|
+
console.log(" \u26A0\uFE0F AI returned no content");
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
_callCount++;
|
|
79
|
+
return content;
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.log(` \u26A0\uFE0F AI request failed: ${err.message}`);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function aiChatWithRetry(prompt, task, opts) {
|
|
86
|
+
const retries = opts?.retries ?? 2;
|
|
87
|
+
for (let i = 0; i <= retries; i++) {
|
|
88
|
+
const out = await aiChat(prompt, task, opts);
|
|
89
|
+
if (out) return out;
|
|
90
|
+
if (i < retries) await new Promise((r) => setTimeout(r, 1e3 * (i + 1)));
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
aiChat,
|
|
96
|
+
aiChatWithRetry,
|
|
97
|
+
getAiCallCount,
|
|
98
|
+
resetAiCallCount
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=ai.js.map
|
package/dist/ai.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/ai.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LinkFlow \u2014 AI provider (OmniRoute-compatible OpenAI endpoint, zero-cost default).\n *\n * Matches the memory note: IBE enrichment + seoflow use OmniRoute auto/best-free via\n * openai-compat (AI_PROVIDER=openai, BASE_URL=.../v1). LinkFlow reuses the same env\n * conventions so it works with the user's existing OmniRoute setup.\n *\n * Env vars (all optional, from .env.local):\n * AI_PROVIDER openai (default) | any\n * AI_BASE_URL e.g. http://192.168.0.254:20128/v1 (OmniRoute)\n * AI_API_KEY API key\n * AI_MODEL model name override\n */\nimport { loadConfig } from './config.js';\n\ninterface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nlet _callCount = 0;\nexport function resetAiCallCount(): void { _callCount = 0; }\nexport function getAiCallCount(): number { return _callCount; }\n\n/**\n * Chat completion against an OpenAI-compatible endpoint.\n * Returns text or null on failure.\n */\nexport async function aiChat(prompt: string, task: string, opts?: { system?: string; maxTokens?: number }): Promise<string | null> {\n const cfg = loadConfig();\n const max = cfg.ai?.maxCallsPerRun ?? 20;\n if (_callCount >= max) {\n console.log(` \u26A0\uFE0F AI budget: ${_callCount}/${max} calls used \u2014 skipping ${task}`);\n return null;\n }\n\n const baseUrl = (cfg.ai?.baseUrl || '').replace(/\\/+$/, '');\n const apiKey = cfg.ai?.apiKey || 'sk-local';\n const model = cfg.ai?.model || 'auto/best-free';\n\n if (!baseUrl) {\n console.log(' \u26A0\uFE0F No AI base URL configured \u2014 using template payload (no AI call).');\n return null;\n }\n\n const messages: ChatMessage[] = [];\n if (opts?.system) messages.push({ role: 'system', content: opts.system });\n messages.push({ role: 'user', content: prompt });\n\n try {\n const res = await fetch(`${baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${apiKey}`,\n 'Accept': 'application/json',\n },\n body: JSON.stringify({\n model,\n messages,\n max_tokens: opts?.maxTokens ?? 600,\n temperature: 0.7,\n stream: false,\n }),\n });\n if (!res.ok) {\n const body = await res.text().catch(() => '');\n console.log(` \u26A0\uFE0F AI error ${res.status}: ${body.slice(0, 200)}`);\n return null;\n }\n const text = await res.text();\n\n // Some providers (OmniRoute) stream SSE even when stream:false is requested.\n // Parse either a plain JSON body or an SSE stream of `data: {...}` lines.\n let data: any = null;\n const trimmed = text.trim();\n if (trimmed.startsWith('{')) {\n data = JSON.parse(trimmed);\n } else if (trimmed.includes('data:')) {\n // SSE stream \u2014 join the JSON payloads (the last `data: [DONE]`-style chunk\n // is a sentinel; use the first chunk with a real choice).\n for (const line of trimmed.split('\\n')) {\n if (!line.startsWith('data:')) continue;\n const payload = line.slice(5).trim();\n if (!payload || payload === '[DONE]') continue;\n try {\n const chunk = JSON.parse(payload);\n if (chunk?.choices?.[0]?.delta?.content) {\n data = chunk;\n break;\n }\n if (chunk?.choices?.[0]?.message?.content) {\n data = chunk;\n break;\n }\n } catch {\n /* skip malformed chunk */\n }\n }\n } else {\n throw new Error('Unexpected response format');\n }\n\n const content = data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.delta?.content ?? null;\n if (!content) {\n console.log(' \u26A0\uFE0F AI returned no content');\n return null;\n }\n _callCount++;\n return content;\n } catch (err) {\n console.log(` \u26A0\uFE0F AI request failed: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** Retry wrapper for transient failures. */\nexport async function aiChatWithRetry(prompt: string, task: string, opts?: { system?: string; maxTokens?: number; retries?: number }): Promise<string | null> {\n const retries = opts?.retries ?? 2;\n for (let i = 0; i <= retries; i++) {\n const out = await aiChat(prompt, task, opts);\n if (out) return out;\n if (i < retries) await new Promise((r) => setTimeout(r, 1000 * (i + 1)));\n }\n return null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
function loadEnvLocal() {
|
|
4
|
+
const envPath = path.join(process.cwd(), ".env.local");
|
|
5
|
+
if (!fs.existsSync(envPath)) return {};
|
|
6
|
+
const out = {};
|
|
7
|
+
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
|
|
8
|
+
const m = line.match(/^\s*([A-Z_0-9]+)\s*=\s*(.*)\s*$/);
|
|
9
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
function loadConfig(configPathOverride) {
|
|
14
|
+
const configPath = configPathOverride || path.join(process.cwd(), "linkflow.config.json");
|
|
15
|
+
let cfg = {};
|
|
16
|
+
if (fs.existsSync(configPath)) {
|
|
17
|
+
cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
18
|
+
}
|
|
19
|
+
const env = loadEnvLocal();
|
|
20
|
+
return {
|
|
21
|
+
...cfg,
|
|
22
|
+
ai: {
|
|
23
|
+
provider: env.AI_PROVIDER || cfg.ai?.provider,
|
|
24
|
+
baseUrl: env.AI_BASE_URL || env.OMNITROUTE_BASE_URL || cfg.ai?.baseUrl,
|
|
25
|
+
apiKey: env.AI_API_KEY || env.OMNITROUTE_API_KEY || cfg.ai?.apiKey,
|
|
26
|
+
model: env.AI_MODEL || cfg.ai?.model,
|
|
27
|
+
maxCallsPerRun: cfg.ai?.maxCallsPerRun ?? 20
|
|
28
|
+
},
|
|
29
|
+
pacing: cfg.pacing || { minSeconds: 60, perDay: 10 }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function hasAiConfig(cfg) {
|
|
33
|
+
return Boolean(cfg.ai?.baseUrl && cfg.ai?.apiKey);
|
|
34
|
+
}
|
|
35
|
+
function printConfigSummary(cfg) {
|
|
36
|
+
console.log(" site:", cfg.siteName || "(unset)");
|
|
37
|
+
console.log(" url:", cfg.siteUrl || "(unset)");
|
|
38
|
+
console.log(" ai provider:", cfg.ai?.provider || "(auto)");
|
|
39
|
+
if (cfg.ai?.baseUrl) console.log(" ai baseUrl:", cfg.ai.baseUrl);
|
|
40
|
+
console.log(" pacing:", `${cfg.pacing?.perDay ?? 10}/day, ${cfg.pacing?.minSeconds ?? 60}s apart`);
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
hasAiConfig,
|
|
44
|
+
loadConfig,
|
|
45
|
+
printConfigSummary
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/config.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LinkFlow \u2014 config loader.\n * Reads linkflow.config.json + .env.local (same conventions as seoflow).\n */\nimport fs from 'fs';\nimport path from 'path';\nimport type { LinkFlowConfig } from './types.js';\n\nfunction loadEnvLocal(): Record<string, string> {\n const envPath = path.join(process.cwd(), '.env.local');\n if (!fs.existsSync(envPath)) return {};\n const out: Record<string, string> = {};\n for (const line of fs.readFileSync(envPath, 'utf8').split('\\n')) {\n const m = line.match(/^\\s*([A-Z_0-9]+)\\s*=\\s*(.*)\\s*$/);\n if (m) out[m[1]] = m[2].replace(/^[\"']|[\"']$/g, '');\n }\n return out;\n}\n\nexport function loadConfig(configPathOverride?: string): LinkFlowConfig {\n const configPath = configPathOverride || path.join(process.cwd(), 'linkflow.config.json');\n let cfg: Partial<LinkFlowConfig> = {};\n if (fs.existsSync(configPath)) {\n cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n }\n\n const env = loadEnvLocal();\n // AI config: explicit env vars override config file\n return {\n ...cfg,\n ai: {\n provider: env.AI_PROVIDER || cfg.ai?.provider,\n baseUrl: env.AI_BASE_URL || env.OMNITROUTE_BASE_URL || cfg.ai?.baseUrl,\n apiKey: env.AI_API_KEY || env.OMNITROUTE_API_KEY || cfg.ai?.apiKey,\n model: env.AI_MODEL || cfg.ai?.model,\n maxCallsPerRun: cfg.ai?.maxCallsPerRun ?? 20,\n },\n pacing: cfg.pacing || { minSeconds: 60, perDay: 10 },\n } as LinkFlowConfig;\n}\n\nexport function hasAiConfig(cfg: LinkFlowConfig): boolean {\n return Boolean(cfg.ai?.baseUrl && cfg.ai?.apiKey);\n}\n\nexport function printConfigSummary(cfg: LinkFlowConfig): void {\n console.log(' site:', cfg.siteName || '(unset)');\n console.log(' url:', cfg.siteUrl || '(unset)');\n console.log(' ai provider:', cfg.ai?.provider || '(auto)');\n if (cfg.ai?.baseUrl) console.log(' ai baseUrl:', cfg.ai.baseUrl);\n console.log(' pacing:', `${cfg.pacing?.perDay ?? 10}/day, ${cfg.pacing?.minSeconds ?? 60}s apart`);\n}\n"],
|
|
5
|
+
"mappings": "AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,SAAS,eAAuC;AAC9C,QAAM,UAAU,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AACrD,MAAI,CAAC,GAAG,WAAW,OAAO,EAAG,QAAO,CAAC;AACrC,QAAM,MAA8B,CAAC;AACrC,aAAW,QAAQ,GAAG,aAAa,SAAS,MAAM,EAAE,MAAM,IAAI,GAAG;AAC/D,UAAM,IAAI,KAAK,MAAM,iCAAiC;AACtD,QAAI,EAAG,KAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,QAAQ,gBAAgB,EAAE;AAAA,EACpD;AACA,SAAO;AACT;AAEO,SAAS,WAAW,oBAA6C;AACtE,QAAM,aAAa,sBAAsB,KAAK,KAAK,QAAQ,IAAI,GAAG,sBAAsB;AACxF,MAAI,MAA+B,CAAC;AACpC,MAAI,GAAG,WAAW,UAAU,GAAG;AAC7B,UAAM,KAAK,MAAM,GAAG,aAAa,YAAY,MAAM,CAAC;AAAA,EACtD;AAEA,QAAM,MAAM,aAAa;AAEzB,SAAO;AAAA,IACL,GAAG;AAAA,IACH,IAAI;AAAA,MACF,UAAU,IAAI,eAAe,IAAI,IAAI;AAAA,MACrC,SAAS,IAAI,eAAe,IAAI,uBAAuB,IAAI,IAAI;AAAA,MAC/D,QAAQ,IAAI,cAAc,IAAI,sBAAsB,IAAI,IAAI;AAAA,MAC5D,OAAO,IAAI,YAAY,IAAI,IAAI;AAAA,MAC/B,gBAAgB,IAAI,IAAI,kBAAkB;AAAA,IAC5C;AAAA,IACA,QAAQ,IAAI,UAAU,EAAE,YAAY,IAAI,QAAQ,GAAG;AAAA,EACrD;AACF;AAEO,SAAS,YAAY,KAA8B;AACxD,SAAO,QAAQ,IAAI,IAAI,WAAW,IAAI,IAAI,MAAM;AAClD;AAEO,SAAS,mBAAmB,KAA2B;AAC5D,UAAQ,IAAI,WAAW,IAAI,YAAY,SAAS;AAChD,UAAQ,IAAI,UAAU,IAAI,WAAW,SAAS;AAC9C,UAAQ,IAAI,kBAAkB,IAAI,IAAI,YAAY,QAAQ;AAC1D,MAAI,IAAI,IAAI,QAAS,SAAQ,IAAI,iBAAiB,IAAI,GAAG,OAAO;AAChE,UAAQ,IAAI,aAAa,GAAG,IAAI,QAAQ,UAAU,EAAE,SAAS,IAAI,QAAQ,cAAc,EAAE,SAAS;AACpG;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/database.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
let _cache = null;
|
|
7
|
+
function resolveDbPath() {
|
|
8
|
+
const candidates = [
|
|
9
|
+
path.join(process.cwd(), "data", "directories.yaml"),
|
|
10
|
+
path.join(__dirname, "directories.yaml"),
|
|
11
|
+
path.join(__dirname, "..", "data", "directories.yaml"),
|
|
12
|
+
path.join(process.cwd(), "node_modules", "linkflow", "data", "directories.yaml")
|
|
13
|
+
];
|
|
14
|
+
for (const p of candidates) {
|
|
15
|
+
if (fs.existsSync(p)) return p;
|
|
16
|
+
}
|
|
17
|
+
throw new Error("LinkFlow: directories.yaml not found. Run from project root or install the package.");
|
|
18
|
+
}
|
|
19
|
+
function loadDirectories() {
|
|
20
|
+
if (_cache) return _cache;
|
|
21
|
+
const raw = fs.readFileSync(resolveDbPath(), "utf8");
|
|
22
|
+
const doc = yaml.load(raw);
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const [category, entries] of Object.entries(doc)) {
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
out.push({ ...e, category });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
_cache = out;
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
function findDirectory(nameOrUrl) {
|
|
33
|
+
const q = nameOrUrl.toLowerCase();
|
|
34
|
+
return loadDirectories().find(
|
|
35
|
+
(d) => d.name.toLowerCase() === q || d.submitUrl.toLowerCase().includes(q) || q.includes(d.submitUrl.toLowerCase())
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
function searchDirectories(query, category) {
|
|
39
|
+
const q = query.toLowerCase();
|
|
40
|
+
return loadDirectories().filter((d) => {
|
|
41
|
+
if (category && d.category !== category) return false;
|
|
42
|
+
if (!q) return true;
|
|
43
|
+
return d.name.toLowerCase().includes(q) || (d.notes || "").toLowerCase().includes(q) || d.submitUrl.toLowerCase().includes(q);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function categories() {
|
|
47
|
+
return [...new Set(loadDirectories().map((d) => d.category))];
|
|
48
|
+
}
|
|
49
|
+
function dbStats() {
|
|
50
|
+
const all = loadDirectories();
|
|
51
|
+
const byCategory = {};
|
|
52
|
+
let auto = 0;
|
|
53
|
+
for (const d of all) {
|
|
54
|
+
byCategory[d.category] = (byCategory[d.category] || 0) + 1;
|
|
55
|
+
if (d.auto === "yes") auto++;
|
|
56
|
+
}
|
|
57
|
+
return { total: all.length, byCategory, auto };
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
categories,
|
|
61
|
+
dbStats,
|
|
62
|
+
findDirectory,
|
|
63
|
+
loadDirectories,
|
|
64
|
+
searchDirectories
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/database.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * LinkFlow \u2014 directory database loader.\n * Loads data/directories.yaml (or the bundled copy in dist/).\n */\nimport fs from 'fs';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport yaml from 'js-yaml';\nimport type { DirectoryEntry } from './types.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nlet _cache: DirectoryEntry[] | null = null;\n\n/** Resolve the directories.yaml path, preferring the source then the bundled copy. */\nfunction resolveDbPath(): string {\n const candidates = [\n path.join(process.cwd(), 'data', 'directories.yaml'),\n path.join(__dirname, 'directories.yaml'),\n path.join(__dirname, '..', 'data', 'directories.yaml'),\n path.join(process.cwd(), 'node_modules', 'linkflow', 'data', 'directories.yaml'),\n ];\n for (const p of candidates) {\n if (fs.existsSync(p)) return p;\n }\n throw new Error('LinkFlow: directories.yaml not found. Run from project root or install the package.');\n}\n\nexport function loadDirectories(): DirectoryEntry[] {\n if (_cache) return _cache;\n const raw = fs.readFileSync(resolveDbPath(), 'utf8');\n const doc = yaml.load(raw) as Record<string, DirectoryEntry[]>;\n const out: DirectoryEntry[] = [];\n for (const [category, entries] of Object.entries(doc)) {\n for (const e of entries) {\n out.push({ ...e, category });\n }\n }\n _cache = out;\n return out;\n}\n\nexport function findDirectory(nameOrUrl: string): DirectoryEntry | undefined {\n const q = nameOrUrl.toLowerCase();\n return loadDirectories().find(\n (d) =>\n d.name.toLowerCase() === q ||\n d.submitUrl.toLowerCase().includes(q) ||\n q.includes(d.submitUrl.toLowerCase())\n );\n}\n\nexport function searchDirectories(query: string, category?: string): DirectoryEntry[] {\n const q = query.toLowerCase();\n return loadDirectories().filter((d) => {\n if (category && d.category !== category) return false;\n if (!q) return true;\n return (\n d.name.toLowerCase().includes(q) ||\n (d.notes || '').toLowerCase().includes(q) ||\n d.submitUrl.toLowerCase().includes(q)\n );\n });\n}\n\nexport function categories(): string[] {\n return [...new Set(loadDirectories().map((d) => d.category))];\n}\n\nexport function dbStats(): { total: number; byCategory: Record<string, number>; auto: number } {\n const all = loadDirectories();\n const byCategory: Record<string, number> = {};\n let auto = 0;\n for (const d of all) {\n byCategory[d.category] = (byCategory[d.category] || 0) + 1;\n if (d.auto === 'yes') auto++;\n }\n return { total: all.length, byCategory, auto };\n}\n"],
|
|
5
|
+
"mappings": "AAIA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AAGjB,MAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAE7D,IAAI,SAAkC;AAGtC,SAAS,gBAAwB;AAC/B,QAAM,aAAa;AAAA,IACjB,KAAK,KAAK,QAAQ,IAAI,GAAG,QAAQ,kBAAkB;AAAA,IACnD,KAAK,KAAK,WAAW,kBAAkB;AAAA,IACvC,KAAK,KAAK,WAAW,MAAM,QAAQ,kBAAkB;AAAA,IACrD,KAAK,KAAK,QAAQ,IAAI,GAAG,gBAAgB,YAAY,QAAQ,kBAAkB;AAAA,EACjF;AACA,aAAW,KAAK,YAAY;AAC1B,QAAI,GAAG,WAAW,CAAC,EAAG,QAAO;AAAA,EAC/B;AACA,QAAM,IAAI,MAAM,qFAAqF;AACvG;AAEO,SAAS,kBAAoC;AAClD,MAAI,OAAQ,QAAO;AACnB,QAAM,MAAM,GAAG,aAAa,cAAc,GAAG,MAAM;AACnD,QAAM,MAAM,KAAK,KAAK,GAAG;AACzB,QAAM,MAAwB,CAAC;AAC/B,aAAW,CAAC,UAAU,OAAO,KAAK,OAAO,QAAQ,GAAG,GAAG;AACrD,eAAW,KAAK,SAAS;AACvB,UAAI,KAAK,EAAE,GAAG,GAAG,SAAS,CAAC;AAAA,IAC7B;AAAA,EACF;AACA,WAAS;AACT,SAAO;AACT;AAEO,SAAS,cAAc,WAA+C;AAC3E,QAAM,IAAI,UAAU,YAAY;AAChC,SAAO,gBAAgB,EAAE;AAAA,IACvB,CAAC,MACC,EAAE,KAAK,YAAY,MAAM,KACzB,EAAE,UAAU,YAAY,EAAE,SAAS,CAAC,KACpC,EAAE,SAAS,EAAE,UAAU,YAAY,CAAC;AAAA,EACxC;AACF;AAEO,SAAS,kBAAkB,OAAe,UAAqC;AACpF,QAAM,IAAI,MAAM,YAAY;AAC5B,SAAO,gBAAgB,EAAE,OAAO,CAAC,MAAM;AACrC,QAAI,YAAY,EAAE,aAAa,SAAU,QAAO;AAChD,QAAI,CAAC,EAAG,QAAO;AACf,WACE,EAAE,KAAK,YAAY,EAAE,SAAS,CAAC,MAC9B,EAAE,SAAS,IAAI,YAAY,EAAE,SAAS,CAAC,KACxC,EAAE,UAAU,YAAY,EAAE,SAAS,CAAC;AAAA,EAExC,CAAC;AACH;AAEO,SAAS,aAAuB;AACrC,SAAO,CAAC,GAAG,IAAI,IAAI,gBAAgB,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAC9D;AAEO,SAAS,UAA+E;AAC7F,QAAM,MAAM,gBAAgB;AAC5B,QAAM,aAAqC,CAAC;AAC5C,MAAI,OAAO;AACX,aAAW,KAAK,KAAK;AACnB,eAAW,EAAE,QAAQ,KAAK,WAAW,EAAE,QAAQ,KAAK,KAAK;AACzD,QAAI,EAAE,SAAS,MAAO;AAAA,EACxB;AACA,SAAO,EAAE,OAAO,IAAI,QAAQ,YAAY,KAAK;AAC/C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|