@hasna/microservices 0.0.9 → 0.0.11
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/bin/index.js +236 -36
- package/bin/mcp.js +153 -4
- package/dist/index.js +120 -3
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-leads/package.json +27 -0
- package/microservices/microservice-leads/src/cli/index.ts +596 -0
- package/microservices/microservice-leads/src/db/database.ts +93 -0
- package/microservices/microservice-leads/src/db/leads.ts +520 -0
- package/microservices/microservice-leads/src/db/lists.ts +151 -0
- package/microservices/microservice-leads/src/db/migrations.ts +93 -0
- package/microservices/microservice-leads/src/index.ts +65 -0
- package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
- package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
- package/microservices/microservice-leads/src/mcp/index.ts +533 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- package/package.json +1 -1
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import {
|
|
6
|
+
createLead,
|
|
7
|
+
getLead,
|
|
8
|
+
listLeads,
|
|
9
|
+
updateLead,
|
|
10
|
+
deleteLead,
|
|
11
|
+
searchLeads,
|
|
12
|
+
bulkImportLeads,
|
|
13
|
+
exportLeads,
|
|
14
|
+
addActivity,
|
|
15
|
+
getActivities,
|
|
16
|
+
getLeadTimeline,
|
|
17
|
+
getLeadStats,
|
|
18
|
+
getPipeline,
|
|
19
|
+
deduplicateLeads,
|
|
20
|
+
mergeLeads,
|
|
21
|
+
} from "../db/leads.js";
|
|
22
|
+
import {
|
|
23
|
+
createList,
|
|
24
|
+
listLists,
|
|
25
|
+
getListMembers,
|
|
26
|
+
addToList,
|
|
27
|
+
removeFromList,
|
|
28
|
+
deleteList,
|
|
29
|
+
} from "../db/lists.js";
|
|
30
|
+
import { enrichLead, bulkEnrich } from "../lib/enrichment.js";
|
|
31
|
+
import { scoreLead, autoScoreAll, getScoreDistribution } from "../lib/scoring.js";
|
|
32
|
+
|
|
33
|
+
const program = new Command();
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.name("microservice-leads")
|
|
37
|
+
.description("Lead generation, storage, scoring, and data enrichment microservice")
|
|
38
|
+
.version("0.0.1");
|
|
39
|
+
|
|
40
|
+
// --- Lead CRUD ---
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command("add")
|
|
44
|
+
.description("Add a new lead")
|
|
45
|
+
.option("--name <name>", "Lead name")
|
|
46
|
+
.option("--email <email>", "Email address")
|
|
47
|
+
.option("--phone <phone>", "Phone number")
|
|
48
|
+
.option("--company <company>", "Company name")
|
|
49
|
+
.option("--title <title>", "Job title")
|
|
50
|
+
.option("--website <url>", "Website URL")
|
|
51
|
+
.option("--linkedin <url>", "LinkedIn URL")
|
|
52
|
+
.option("--source <source>", "Lead source", "manual")
|
|
53
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
54
|
+
.option("--notes <notes>", "Notes")
|
|
55
|
+
.option("--json", "Output as JSON", false)
|
|
56
|
+
.action((opts) => {
|
|
57
|
+
const lead = createLead({
|
|
58
|
+
name: opts.name,
|
|
59
|
+
email: opts.email,
|
|
60
|
+
phone: opts.phone,
|
|
61
|
+
company: opts.company,
|
|
62
|
+
title: opts.title,
|
|
63
|
+
website: opts.website,
|
|
64
|
+
linkedin_url: opts.linkedin,
|
|
65
|
+
source: opts.source,
|
|
66
|
+
tags: opts.tags ? opts.tags.split(",").map((t: string) => t.trim()) : undefined,
|
|
67
|
+
notes: opts.notes,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (opts.json) {
|
|
71
|
+
console.log(JSON.stringify(lead, null, 2));
|
|
72
|
+
} else {
|
|
73
|
+
console.log(`Created lead: ${lead.name || lead.email || lead.id} (${lead.id})`);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.command("get")
|
|
79
|
+
.description("Get a lead by ID")
|
|
80
|
+
.argument("<id>", "Lead ID")
|
|
81
|
+
.option("--json", "Output as JSON", false)
|
|
82
|
+
.action((id, opts) => {
|
|
83
|
+
const lead = getLead(id);
|
|
84
|
+
if (!lead) {
|
|
85
|
+
console.error(`Lead '${id}' not found.`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (opts.json) {
|
|
90
|
+
console.log(JSON.stringify(lead, null, 2));
|
|
91
|
+
} else {
|
|
92
|
+
console.log(`${lead.name || "(no name)"}`);
|
|
93
|
+
if (lead.email) console.log(` Email: ${lead.email}`);
|
|
94
|
+
if (lead.phone) console.log(` Phone: ${lead.phone}`);
|
|
95
|
+
if (lead.company) console.log(` Company: ${lead.company}`);
|
|
96
|
+
if (lead.title) console.log(` Title: ${lead.title}`);
|
|
97
|
+
console.log(` Status: ${lead.status}`);
|
|
98
|
+
console.log(` Score: ${lead.score}`);
|
|
99
|
+
if (lead.tags.length) console.log(` Tags: ${lead.tags.join(", ")}`);
|
|
100
|
+
if (lead.notes) console.log(` Notes: ${lead.notes}`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command("list")
|
|
106
|
+
.description("List leads")
|
|
107
|
+
.option("--status <status>", "Filter by status")
|
|
108
|
+
.option("--source <source>", "Filter by source")
|
|
109
|
+
.option("--score-min <n>", "Minimum score")
|
|
110
|
+
.option("--score-max <n>", "Maximum score")
|
|
111
|
+
.option("--enriched", "Only enriched leads")
|
|
112
|
+
.option("--limit <n>", "Limit results")
|
|
113
|
+
.option("--json", "Output as JSON", false)
|
|
114
|
+
.action((opts) => {
|
|
115
|
+
const leads = listLeads({
|
|
116
|
+
status: opts.status,
|
|
117
|
+
source: opts.source,
|
|
118
|
+
score_min: opts.scoreMin ? parseInt(opts.scoreMin) : undefined,
|
|
119
|
+
score_max: opts.scoreMax ? parseInt(opts.scoreMax) : undefined,
|
|
120
|
+
enriched: opts.enriched ? true : undefined,
|
|
121
|
+
limit: opts.limit ? parseInt(opts.limit) : undefined,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (opts.json) {
|
|
125
|
+
console.log(JSON.stringify(leads, null, 2));
|
|
126
|
+
} else {
|
|
127
|
+
if (leads.length === 0) {
|
|
128
|
+
console.log("No leads found.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const l of leads) {
|
|
132
|
+
const email = l.email ? ` <${l.email}>` : "";
|
|
133
|
+
const score = ` [score: ${l.score}]`;
|
|
134
|
+
console.log(` ${l.name || "(no name)"}${email} — ${l.status}${score}`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`\n${leads.length} lead(s)`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command("update")
|
|
142
|
+
.description("Update a lead")
|
|
143
|
+
.argument("<id>", "Lead ID")
|
|
144
|
+
.option("--name <name>", "Name")
|
|
145
|
+
.option("--email <email>", "Email")
|
|
146
|
+
.option("--phone <phone>", "Phone")
|
|
147
|
+
.option("--company <company>", "Company")
|
|
148
|
+
.option("--title <title>", "Title")
|
|
149
|
+
.option("--website <url>", "Website")
|
|
150
|
+
.option("--linkedin <url>", "LinkedIn URL")
|
|
151
|
+
.option("--source <source>", "Source")
|
|
152
|
+
.option("--status <status>", "Status")
|
|
153
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
154
|
+
.option("--notes <notes>", "Notes")
|
|
155
|
+
.option("--json", "Output as JSON", false)
|
|
156
|
+
.action((id, opts) => {
|
|
157
|
+
const input: Record<string, unknown> = {};
|
|
158
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
159
|
+
if (opts.email !== undefined) input.email = opts.email;
|
|
160
|
+
if (opts.phone !== undefined) input.phone = opts.phone;
|
|
161
|
+
if (opts.company !== undefined) input.company = opts.company;
|
|
162
|
+
if (opts.title !== undefined) input.title = opts.title;
|
|
163
|
+
if (opts.website !== undefined) input.website = opts.website;
|
|
164
|
+
if (opts.linkedin !== undefined) input.linkedin_url = opts.linkedin;
|
|
165
|
+
if (opts.source !== undefined) input.source = opts.source;
|
|
166
|
+
if (opts.status !== undefined) input.status = opts.status;
|
|
167
|
+
if (opts.tags !== undefined) input.tags = opts.tags.split(",").map((t: string) => t.trim());
|
|
168
|
+
if (opts.notes !== undefined) input.notes = opts.notes;
|
|
169
|
+
|
|
170
|
+
const lead = updateLead(id, input);
|
|
171
|
+
if (!lead) {
|
|
172
|
+
console.error(`Lead '${id}' not found.`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (opts.json) {
|
|
177
|
+
console.log(JSON.stringify(lead, null, 2));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(`Updated: ${lead.name || lead.email || lead.id}`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
program
|
|
184
|
+
.command("delete")
|
|
185
|
+
.description("Delete a lead")
|
|
186
|
+
.argument("<id>", "Lead ID")
|
|
187
|
+
.action((id) => {
|
|
188
|
+
const deleted = deleteLead(id);
|
|
189
|
+
if (deleted) {
|
|
190
|
+
console.log(`Deleted lead ${id}`);
|
|
191
|
+
} else {
|
|
192
|
+
console.error(`Lead '${id}' not found.`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.command("search")
|
|
199
|
+
.description("Search leads by name, email, or company")
|
|
200
|
+
.argument("<query>", "Search term")
|
|
201
|
+
.option("--json", "Output as JSON", false)
|
|
202
|
+
.action((query, opts) => {
|
|
203
|
+
const results = searchLeads(query);
|
|
204
|
+
|
|
205
|
+
if (opts.json) {
|
|
206
|
+
console.log(JSON.stringify(results, null, 2));
|
|
207
|
+
} else {
|
|
208
|
+
if (results.length === 0) {
|
|
209
|
+
console.log(`No leads matching "${query}".`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const l of results) {
|
|
213
|
+
console.log(` ${l.name || "(no name)"} ${l.email ? `<${l.email}>` : ""} — ${l.status}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// --- Import/Export ---
|
|
219
|
+
|
|
220
|
+
program
|
|
221
|
+
.command("import")
|
|
222
|
+
.description("Import leads from a CSV file")
|
|
223
|
+
.requiredOption("--file <path>", "CSV file path")
|
|
224
|
+
.option("--enrich", "Enrich after import", false)
|
|
225
|
+
.option("--json", "Output as JSON", false)
|
|
226
|
+
.action((opts) => {
|
|
227
|
+
const content = readFileSync(opts.file, "utf-8");
|
|
228
|
+
const lines = content.trim().split("\n");
|
|
229
|
+
if (lines.length < 2) {
|
|
230
|
+
console.error("CSV file must have a header row and at least one data row.");
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
|
235
|
+
const data = lines.slice(1).map((line) => {
|
|
236
|
+
const values = line.split(",").map((v) => v.trim().replace(/^"|"$/g, ""));
|
|
237
|
+
const row: Record<string, string> = {};
|
|
238
|
+
headers.forEach((h, i) => { row[h] = values[i] || ""; });
|
|
239
|
+
return {
|
|
240
|
+
name: row["name"] || undefined,
|
|
241
|
+
email: row["email"] || undefined,
|
|
242
|
+
phone: row["phone"] || undefined,
|
|
243
|
+
company: row["company"] || undefined,
|
|
244
|
+
title: row["title"] || undefined,
|
|
245
|
+
website: row["website"] || undefined,
|
|
246
|
+
linkedin_url: row["linkedin_url"] || row["linkedin"] || undefined,
|
|
247
|
+
source: row["source"] || "csv_import",
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const result = bulkImportLeads(data);
|
|
252
|
+
|
|
253
|
+
if (opts.enrich) {
|
|
254
|
+
// Enrich all newly imported leads
|
|
255
|
+
const leads = listLeads({ source: "csv_import", enriched: false });
|
|
256
|
+
const enrichResult = bulkEnrich(leads.map((l) => l.id));
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
console.log(JSON.stringify({ ...result, enrichment: enrichResult }, null, 2));
|
|
259
|
+
} else {
|
|
260
|
+
console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}, Errors: ${result.errors.length}`);
|
|
261
|
+
console.log(`Enriched: ${enrichResult.enriched}, Failed: ${enrichResult.failed}`);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
if (opts.json) {
|
|
265
|
+
console.log(JSON.stringify(result, null, 2));
|
|
266
|
+
} else {
|
|
267
|
+
console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}, Errors: ${result.errors.length}`);
|
|
268
|
+
if (result.errors.length > 0) {
|
|
269
|
+
for (const err of result.errors) console.error(` ${err}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
program
|
|
276
|
+
.command("export")
|
|
277
|
+
.description("Export leads")
|
|
278
|
+
.option("--format <format>", "Export format (csv or json)", "json")
|
|
279
|
+
.option("--status <status>", "Filter by status")
|
|
280
|
+
.option("--json", "Force JSON format", false)
|
|
281
|
+
.action((opts) => {
|
|
282
|
+
const format = opts.json ? "json" : (opts.format as "csv" | "json");
|
|
283
|
+
const output = exportLeads(format, { status: opts.status });
|
|
284
|
+
console.log(output);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// --- Enrichment ---
|
|
288
|
+
|
|
289
|
+
program
|
|
290
|
+
.command("enrich")
|
|
291
|
+
.description("Enrich a lead by ID")
|
|
292
|
+
.argument("<id>", "Lead ID")
|
|
293
|
+
.option("--json", "Output as JSON", false)
|
|
294
|
+
.action((id, opts) => {
|
|
295
|
+
const lead = enrichLead(id);
|
|
296
|
+
if (!lead) {
|
|
297
|
+
console.error(`Lead '${id}' not found.`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (opts.json) {
|
|
302
|
+
console.log(JSON.stringify(lead, null, 2));
|
|
303
|
+
} else {
|
|
304
|
+
console.log(`Enriched: ${lead.name || lead.email || lead.id}`);
|
|
305
|
+
if (lead.company) console.log(` Company: ${lead.company}`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
program
|
|
310
|
+
.command("enrich-all")
|
|
311
|
+
.description("Enrich all un-enriched leads")
|
|
312
|
+
.option("--limit <n>", "Limit number of leads to enrich")
|
|
313
|
+
.option("--json", "Output as JSON", false)
|
|
314
|
+
.action((opts) => {
|
|
315
|
+
const leads = listLeads({ enriched: false, limit: opts.limit ? parseInt(opts.limit) : undefined });
|
|
316
|
+
const result = bulkEnrich(leads.map((l) => l.id));
|
|
317
|
+
|
|
318
|
+
if (opts.json) {
|
|
319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
} else {
|
|
321
|
+
console.log(`Enriched: ${result.enriched}, Failed: ${result.failed}`);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// --- Scoring ---
|
|
326
|
+
|
|
327
|
+
program
|
|
328
|
+
.command("score")
|
|
329
|
+
.description("Score a lead by ID")
|
|
330
|
+
.argument("<id>", "Lead ID")
|
|
331
|
+
.option("--json", "Output as JSON", false)
|
|
332
|
+
.action((id, opts) => {
|
|
333
|
+
const result = scoreLead(id);
|
|
334
|
+
if (!result) {
|
|
335
|
+
console.error(`Lead '${id}' not found.`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (opts.json) {
|
|
340
|
+
console.log(JSON.stringify(result, null, 2));
|
|
341
|
+
} else {
|
|
342
|
+
console.log(`Score: ${result.score}/100`);
|
|
343
|
+
console.log(`Reason: ${result.reason}`);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
program
|
|
348
|
+
.command("score-all")
|
|
349
|
+
.description("Auto-score all leads with score=0")
|
|
350
|
+
.option("--json", "Output as JSON", false)
|
|
351
|
+
.action((opts) => {
|
|
352
|
+
const result = autoScoreAll();
|
|
353
|
+
if (opts.json) {
|
|
354
|
+
console.log(JSON.stringify(result, null, 2));
|
|
355
|
+
} else {
|
|
356
|
+
console.log(`Scored ${result.scored} of ${result.total} unscored leads`);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// --- Pipeline & Stats ---
|
|
361
|
+
|
|
362
|
+
program
|
|
363
|
+
.command("pipeline")
|
|
364
|
+
.description("Show lead pipeline funnel")
|
|
365
|
+
.option("--json", "Output as JSON", false)
|
|
366
|
+
.action((opts) => {
|
|
367
|
+
const pipeline = getPipeline();
|
|
368
|
+
|
|
369
|
+
if (opts.json) {
|
|
370
|
+
console.log(JSON.stringify(pipeline, null, 2));
|
|
371
|
+
} else {
|
|
372
|
+
console.log("Lead Pipeline:");
|
|
373
|
+
for (const stage of pipeline) {
|
|
374
|
+
const bar = "█".repeat(Math.max(1, Math.round(stage.pct / 5)));
|
|
375
|
+
console.log(` ${stage.status.padEnd(14)} ${String(stage.count).padStart(4)} ${stage.pct}% ${bar}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
program
|
|
381
|
+
.command("stats")
|
|
382
|
+
.description("Show lead statistics")
|
|
383
|
+
.option("--json", "Output as JSON", false)
|
|
384
|
+
.action((opts) => {
|
|
385
|
+
const stats = getLeadStats();
|
|
386
|
+
|
|
387
|
+
if (opts.json) {
|
|
388
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
389
|
+
} else {
|
|
390
|
+
console.log(`Total leads: ${stats.total}`);
|
|
391
|
+
console.log(`Average score: ${stats.avg_score}`);
|
|
392
|
+
console.log(`Conversion rate: ${stats.conversion_rate}%`);
|
|
393
|
+
console.log("\nBy status:");
|
|
394
|
+
for (const [status, count] of Object.entries(stats.by_status)) {
|
|
395
|
+
console.log(` ${status}: ${count}`);
|
|
396
|
+
}
|
|
397
|
+
console.log("\nBy source:");
|
|
398
|
+
for (const [source, count] of Object.entries(stats.by_source)) {
|
|
399
|
+
console.log(` ${source}: ${count}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// --- Activity ---
|
|
405
|
+
|
|
406
|
+
program
|
|
407
|
+
.command("activity")
|
|
408
|
+
.description("Show activity timeline for a lead")
|
|
409
|
+
.argument("<id>", "Lead ID")
|
|
410
|
+
.option("--limit <n>", "Limit results")
|
|
411
|
+
.option("--json", "Output as JSON", false)
|
|
412
|
+
.action((id, opts) => {
|
|
413
|
+
const activities = opts.limit
|
|
414
|
+
? getActivities(id, parseInt(opts.limit))
|
|
415
|
+
: getLeadTimeline(id);
|
|
416
|
+
|
|
417
|
+
if (opts.json) {
|
|
418
|
+
console.log(JSON.stringify(activities, null, 2));
|
|
419
|
+
} else {
|
|
420
|
+
if (activities.length === 0) {
|
|
421
|
+
console.log("No activities found.");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
for (const a of activities) {
|
|
425
|
+
console.log(` [${a.created_at}] ${a.type}: ${a.description || "(no description)"}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// --- Dedup & Merge ---
|
|
431
|
+
|
|
432
|
+
program
|
|
433
|
+
.command("dedup")
|
|
434
|
+
.description("Find duplicate leads by email")
|
|
435
|
+
.option("--json", "Output as JSON", false)
|
|
436
|
+
.action((opts) => {
|
|
437
|
+
const pairs = deduplicateLeads();
|
|
438
|
+
|
|
439
|
+
if (opts.json) {
|
|
440
|
+
console.log(JSON.stringify(pairs, null, 2));
|
|
441
|
+
} else {
|
|
442
|
+
if (pairs.length === 0) {
|
|
443
|
+
console.log("No duplicates found.");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
console.log(`Found ${pairs.length} duplicate pair(s):`);
|
|
447
|
+
for (const p of pairs) {
|
|
448
|
+
console.log(` ${p.email}: ${p.lead1.id} vs ${p.lead2.id}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
program
|
|
454
|
+
.command("merge")
|
|
455
|
+
.description("Merge two leads (keep first, merge second into it)")
|
|
456
|
+
.argument("<keep-id>", "Lead ID to keep")
|
|
457
|
+
.argument("<merge-id>", "Lead ID to merge and delete")
|
|
458
|
+
.option("--json", "Output as JSON", false)
|
|
459
|
+
.action((keepId, mergeId, opts) => {
|
|
460
|
+
const result = mergeLeads(keepId, mergeId);
|
|
461
|
+
if (!result) {
|
|
462
|
+
console.error("One or both leads not found.");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (opts.json) {
|
|
467
|
+
console.log(JSON.stringify(result, null, 2));
|
|
468
|
+
} else {
|
|
469
|
+
console.log(`Merged lead ${mergeId} into ${keepId}`);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// --- Convert ---
|
|
474
|
+
|
|
475
|
+
program
|
|
476
|
+
.command("convert")
|
|
477
|
+
.description("Mark a lead as converted")
|
|
478
|
+
.argument("<id>", "Lead ID")
|
|
479
|
+
.option("--json", "Output as JSON", false)
|
|
480
|
+
.action((id, opts) => {
|
|
481
|
+
const lead = updateLead(id, { status: "converted" });
|
|
482
|
+
if (!lead) {
|
|
483
|
+
console.error(`Lead '${id}' not found.`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
addActivity(id, "status_change", "Lead converted");
|
|
487
|
+
|
|
488
|
+
if (opts.json) {
|
|
489
|
+
console.log(JSON.stringify(lead, null, 2));
|
|
490
|
+
} else {
|
|
491
|
+
console.log(`Converted: ${lead.name || lead.email || lead.id}`);
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// --- Lists ---
|
|
496
|
+
|
|
497
|
+
const listCmd = program
|
|
498
|
+
.command("list-cmd")
|
|
499
|
+
.alias("lists")
|
|
500
|
+
.description("Lead list management");
|
|
501
|
+
|
|
502
|
+
listCmd
|
|
503
|
+
.command("create")
|
|
504
|
+
.description("Create a lead list")
|
|
505
|
+
.requiredOption("--name <name>", "List name")
|
|
506
|
+
.option("--description <desc>", "Description")
|
|
507
|
+
.option("--filter <query>", "Smart filter query (e.g. 'status=qualified AND score>=50')")
|
|
508
|
+
.option("--json", "Output as JSON", false)
|
|
509
|
+
.action((opts) => {
|
|
510
|
+
const list = createList({
|
|
511
|
+
name: opts.name,
|
|
512
|
+
description: opts.description,
|
|
513
|
+
filter_query: opts.filter,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
if (opts.json) {
|
|
517
|
+
console.log(JSON.stringify(list, null, 2));
|
|
518
|
+
} else {
|
|
519
|
+
console.log(`Created list: ${list.name} (${list.id})`);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
listCmd
|
|
524
|
+
.command("list")
|
|
525
|
+
.description("List all lead lists")
|
|
526
|
+
.option("--json", "Output as JSON", false)
|
|
527
|
+
.action((opts) => {
|
|
528
|
+
const lists = listLists();
|
|
529
|
+
|
|
530
|
+
if (opts.json) {
|
|
531
|
+
console.log(JSON.stringify(lists, null, 2));
|
|
532
|
+
} else {
|
|
533
|
+
if (lists.length === 0) {
|
|
534
|
+
console.log("No lists found.");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
for (const l of lists) {
|
|
538
|
+
const filter = l.filter_query ? ` [smart: ${l.filter_query}]` : "";
|
|
539
|
+
console.log(` ${l.name}${filter} (${l.id})`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
listCmd
|
|
545
|
+
.command("members")
|
|
546
|
+
.description("Show members of a list")
|
|
547
|
+
.argument("<list-id>", "List ID")
|
|
548
|
+
.option("--json", "Output as JSON", false)
|
|
549
|
+
.action((listId, opts) => {
|
|
550
|
+
const members = getListMembers(listId);
|
|
551
|
+
|
|
552
|
+
if (opts.json) {
|
|
553
|
+
console.log(JSON.stringify(members, null, 2));
|
|
554
|
+
} else {
|
|
555
|
+
if (members.length === 0) {
|
|
556
|
+
console.log("No members in this list.");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
for (const m of members) {
|
|
560
|
+
console.log(` ${m.name || "(no name)"} ${m.email ? `<${m.email}>` : ""}`);
|
|
561
|
+
}
|
|
562
|
+
console.log(`\n${members.length} member(s)`);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
listCmd
|
|
567
|
+
.command("add")
|
|
568
|
+
.description("Add a lead to a list")
|
|
569
|
+
.requiredOption("--list <id>", "List ID")
|
|
570
|
+
.requiredOption("--lead <id>", "Lead ID")
|
|
571
|
+
.action((opts) => {
|
|
572
|
+
const added = addToList(opts.list, opts.lead);
|
|
573
|
+
if (added) {
|
|
574
|
+
console.log(`Added lead ${opts.lead} to list ${opts.list}`);
|
|
575
|
+
} else {
|
|
576
|
+
console.error("Failed to add lead to list.");
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
listCmd
|
|
582
|
+
.command("remove")
|
|
583
|
+
.description("Remove a lead from a list")
|
|
584
|
+
.requiredOption("--list <id>", "List ID")
|
|
585
|
+
.requiredOption("--lead <id>", "Lead ID")
|
|
586
|
+
.action((opts) => {
|
|
587
|
+
const removed = removeFromList(opts.list, opts.lead);
|
|
588
|
+
if (removed) {
|
|
589
|
+
console.log(`Removed lead ${opts.lead} from list ${opts.list}`);
|
|
590
|
+
} else {
|
|
591
|
+
console.error("Lead not found in list.");
|
|
592
|
+
process.exit(1);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection for microservice-leads
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { MIGRATIONS } from "./migrations.js";
|
|
9
|
+
|
|
10
|
+
let _db: Database | null = null;
|
|
11
|
+
|
|
12
|
+
function getDbPath(): string {
|
|
13
|
+
// Environment variable override
|
|
14
|
+
if (process.env["MICROSERVICES_DIR"]) {
|
|
15
|
+
return join(process.env["MICROSERVICES_DIR"], "microservice-leads", "data.db");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for .microservices in current or parent directories
|
|
19
|
+
let dir = resolve(process.cwd());
|
|
20
|
+
while (true) {
|
|
21
|
+
const candidate = join(dir, ".microservices", "microservice-leads", "data.db");
|
|
22
|
+
const msDir = join(dir, ".microservices");
|
|
23
|
+
if (existsSync(msDir)) return candidate;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Global fallback
|
|
30
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
31
|
+
return join(home, ".microservices", "microservice-leads", "data.db");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(filePath: string): void {
|
|
35
|
+
const dir = dirname(resolve(filePath));
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getDatabase(): Database {
|
|
42
|
+
if (_db) return _db;
|
|
43
|
+
|
|
44
|
+
const dbPath = getDbPath();
|
|
45
|
+
ensureDir(dbPath);
|
|
46
|
+
|
|
47
|
+
_db = new Database(dbPath);
|
|
48
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
49
|
+
_db.exec("PRAGMA foreign_keys = ON");
|
|
50
|
+
|
|
51
|
+
// Create migrations table
|
|
52
|
+
_db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
54
|
+
id INTEGER PRIMARY KEY,
|
|
55
|
+
name TEXT NOT NULL,
|
|
56
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
// Apply pending migrations
|
|
61
|
+
const applied = _db
|
|
62
|
+
.query("SELECT id FROM _migrations ORDER BY id")
|
|
63
|
+
.all() as { id: number }[];
|
|
64
|
+
const appliedIds = new Set(applied.map((r) => r.id));
|
|
65
|
+
|
|
66
|
+
for (const migration of MIGRATIONS) {
|
|
67
|
+
if (appliedIds.has(migration.id)) continue;
|
|
68
|
+
|
|
69
|
+
_db.exec("BEGIN");
|
|
70
|
+
try {
|
|
71
|
+
_db.exec(migration.sql);
|
|
72
|
+
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(
|
|
73
|
+
migration.id,
|
|
74
|
+
migration.name
|
|
75
|
+
);
|
|
76
|
+
_db.exec("COMMIT");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
_db.exec("ROLLBACK");
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return _db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function closeDatabase(): void {
|
|
89
|
+
if (_db) {
|
|
90
|
+
_db.close();
|
|
91
|
+
_db = null;
|
|
92
|
+
}
|
|
93
|
+
}
|