indiecrm-cli 0.1.0 → 0.2.1
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/CHANGELOG.md +447 -0
- package/CODE_OF_CONDUCT.md +35 -0
- package/CONTRIBUTING.md +7 -0
- package/README.md +14 -2
- package/RESEARCH.md +346 -0
- package/SECURITY.md +35 -0
- package/dist/affiliate-copy.js +49 -0
- package/dist/auth.js +453 -0
- package/dist/bigquery.js +199 -0
- package/dist/chrome-browser.js +231 -0
- package/dist/cli.js +19652 -0
- package/dist/company-identity-review.js +78 -0
- package/dist/company-leads.js +422 -0
- package/dist/company-recovery.js +84 -0
- package/dist/deel-outreach.js +469 -0
- package/dist/deel-salesnav.js +368 -0
- package/dist/direct-path.js +326 -0
- package/dist/domain.js +53 -0
- package/dist/domainfinder.js +764 -0
- package/dist/engine.js +216 -0
- package/dist/historical-queries.js +189 -0
- package/dist/hunter-emailfinder.js +252 -0
- package/dist/icp-templates.js +171 -0
- package/dist/indiecrm/commands.js +108 -0
- package/dist/indiecrm-cli.js +2 -104
- package/dist/instantly.js +136 -0
- package/dist/io.js +21 -0
- package/dist/leadlists-funnel.js +148 -0
- package/dist/linkedin-companies.js +562 -0
- package/dist/linkedin-product-details.js +1203 -0
- package/dist/linkedin-product-search.js +1081 -0
- package/dist/linkedin-products.js +786 -0
- package/dist/linkedin-session-contracts.js +3 -0
- package/dist/linkedin-session.js +846 -0
- package/dist/providers.js +1 -0
- package/dist/research-browser-preference.js +37 -0
- package/dist/sales-navigator.js +1231 -0
- package/dist/salesnav-backfill.js +710 -0
- package/dist/sample-data.js +34 -0
- package/dist/session-recovery.js +62 -0
- package/dist/vendor/salesprompter-shared/extension-session-contracts.js +29 -0
- package/dist/vendor/salesprompter-shared/linkedin-session.js +22 -0
- package/dist/vendor/salesprompter-shared/phantombuster-contracts.js +16 -0
- package/dist/vendor/salesprompter-shared/session-vault-contracts.js +17 -0
- package/package.json +73 -14
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const SAMPLE_COMPANIES = [
|
|
2
|
+
{ companyName: "Northstar Freight", domain: "northstarfreight.com", industry: "Logistics", region: "North America", employeeCount: 180 },
|
|
3
|
+
{ companyName: "Brightpath Health", domain: "brightpathhealth.io", industry: "Healthcare", region: "Europe", employeeCount: 320 },
|
|
4
|
+
{ companyName: "ForgeOps Cloud", domain: "forgeopscloud.dev", industry: "Software", region: "North America", employeeCount: 85 },
|
|
5
|
+
{ companyName: "Summit Retail Group", domain: "summitretailgroup.com", industry: "Retail", region: "Europe", employeeCount: 540 },
|
|
6
|
+
{ companyName: "Atlas Industrial", domain: "atlasindustrial.co", industry: "Manufacturing", region: "North America", employeeCount: 260 },
|
|
7
|
+
{ companyName: "Meridian Finance", domain: "meridianfinance.ai", industry: "Financial Services", region: "Europe", employeeCount: 140 }
|
|
8
|
+
];
|
|
9
|
+
export const SAMPLE_CONTACTS = [
|
|
10
|
+
{ contactName: "Avery Chen", title: "VP Sales" },
|
|
11
|
+
{ contactName: "Jordan Patel", title: "Head of Revenue Operations" },
|
|
12
|
+
{ contactName: "Taylor Morgan", title: "Director of Growth" },
|
|
13
|
+
{ contactName: "Morgan Diaz", title: "Chief Revenue Officer" },
|
|
14
|
+
{ contactName: "Cameron Lee", title: "Sales Operations Manager" },
|
|
15
|
+
{ contactName: "Riley Brooks", title: "Demand Generation Lead" }
|
|
16
|
+
];
|
|
17
|
+
export const SAMPLE_SIGNALS = [
|
|
18
|
+
"hiring sales reps",
|
|
19
|
+
"recent funding",
|
|
20
|
+
"expanding into new regions",
|
|
21
|
+
"using fragmented sales tooling",
|
|
22
|
+
"growing outbound team",
|
|
23
|
+
"launching new product line"
|
|
24
|
+
];
|
|
25
|
+
export const SAMPLE_TECH = [
|
|
26
|
+
"HubSpot",
|
|
27
|
+
"Salesforce",
|
|
28
|
+
"Apollo",
|
|
29
|
+
"Instantly",
|
|
30
|
+
"Outreach",
|
|
31
|
+
"Clay",
|
|
32
|
+
"Segment",
|
|
33
|
+
"PostHog"
|
|
34
|
+
];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function isFreshSessionForIdentity(current, next) {
|
|
2
|
+
return current === null || next.identity === current.identity && (next.cookie !== current.cookie || next.csrfToken !== current.csrfToken);
|
|
3
|
+
}
|
|
4
|
+
/** Poll only the configured credential source. No provider requests or identity rotation. */
|
|
5
|
+
export async function waitForFreshSession(options) {
|
|
6
|
+
const now = options.now ?? Date.now;
|
|
7
|
+
const sleep = options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
8
|
+
const deadline = now() + options.waitMs;
|
|
9
|
+
let nextNotice = now();
|
|
10
|
+
while (true) {
|
|
11
|
+
const value = await options.read();
|
|
12
|
+
if (value !== null && options.accept(value))
|
|
13
|
+
return value;
|
|
14
|
+
const remaining = deadline - now();
|
|
15
|
+
if (remaining <= 0)
|
|
16
|
+
return null;
|
|
17
|
+
if (now() >= nextNotice) {
|
|
18
|
+
options.onWaiting?.();
|
|
19
|
+
nextNotice = now() + 30000;
|
|
20
|
+
}
|
|
21
|
+
await sleep(Math.min(5000, remaining));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Delay credential access until collection really needs it (cached runs need none). */
|
|
25
|
+
export function createLazySessionRecovery(load, options) {
|
|
26
|
+
let runner;
|
|
27
|
+
return async (collect) => {
|
|
28
|
+
runner ??= createSessionRecovery({ ...options, initial: await load() });
|
|
29
|
+
return runner(collect);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** One credential refresh per run; never rotate on rate limits or other failures. */
|
|
33
|
+
export function createSessionRecovery(options) {
|
|
34
|
+
let current = options.initial;
|
|
35
|
+
let attempted = false;
|
|
36
|
+
const unavailable = () => new Error("Sales Navigator rejected the available LinkedIn session. Completed company searches are preserved. Reconnect the Salesprompter extension, or rerun with --browser-relay-port and a signed-in browser worker. Signing into Salesprompter alone does not refresh LinkedIn.");
|
|
37
|
+
return async function run(collect) {
|
|
38
|
+
try {
|
|
39
|
+
return await collect(current);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!options.isAuthError(error))
|
|
43
|
+
throw error;
|
|
44
|
+
if (attempted)
|
|
45
|
+
throw unavailable();
|
|
46
|
+
attempted = true;
|
|
47
|
+
const next = await options.refresh();
|
|
48
|
+
if (!next || options.sameSession(current, next))
|
|
49
|
+
throw unavailable();
|
|
50
|
+
current = next;
|
|
51
|
+
await options.beforeRetry();
|
|
52
|
+
try {
|
|
53
|
+
return await collect(current);
|
|
54
|
+
}
|
|
55
|
+
catch (retryError) {
|
|
56
|
+
if (options.isAuthError(retryError))
|
|
57
|
+
throw unavailable();
|
|
58
|
+
throw retryError;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const linkedInCapturedSessionSchema = z.object({
|
|
3
|
+
sessionCookie: z.string().min(1),
|
|
4
|
+
csrfToken: z.string().min(1),
|
|
5
|
+
linkedInIdentity: z.string().min(1),
|
|
6
|
+
userAgent: z.string().min(1),
|
|
7
|
+
requestId: z.string().min(1).nullable().optional(),
|
|
8
|
+
timestamp: z.string().min(1).nullable().optional(),
|
|
9
|
+
extractedFrom: z.string().min(1).nullable().optional(),
|
|
10
|
+
});
|
|
11
|
+
export const extensionSessionSyncPayloadSchema = z.object({
|
|
12
|
+
sessionCookie: z.string().min(1),
|
|
13
|
+
csrfToken: z.string().min(1),
|
|
14
|
+
linkedInIdentity: z.string().min(1),
|
|
15
|
+
userAgent: z.string().min(1),
|
|
16
|
+
requestId: z.string().min(1).nullable().optional(),
|
|
17
|
+
userEmail: z.string().email().nullable().optional(),
|
|
18
|
+
userFullName: z.string().min(1).nullable().optional(),
|
|
19
|
+
userHandle: z.string().min(1).nullable().optional(),
|
|
20
|
+
});
|
|
21
|
+
export const extensionSessionSyncSuccessSchema = z.object({
|
|
22
|
+
success: z.literal(true),
|
|
23
|
+
result: z.unknown().nullable().optional(),
|
|
24
|
+
persisted: z.boolean().nullable().optional(),
|
|
25
|
+
storageReady: z.boolean().nullable().optional(),
|
|
26
|
+
});
|
|
27
|
+
export const extensionSessionSyncErrorSchema = z.object({
|
|
28
|
+
error: z.string().min(1),
|
|
29
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const DEFAULT_LINKEDIN_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36';
|
|
2
|
+
function normalizeWhitespace(value) {
|
|
3
|
+
return (value ?? '').replace(/\s+/g, ' ').trim();
|
|
4
|
+
}
|
|
5
|
+
export function extractLiAtCookieValue(sessionCookie) {
|
|
6
|
+
const trimmed = normalizeWhitespace(sessionCookie);
|
|
7
|
+
if (!trimmed) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const liAtMatch = trimmed.match(/(?:^|[;\s])li_at="?([^";]+)"?/i);
|
|
11
|
+
if (liAtMatch?.[1]) {
|
|
12
|
+
return liAtMatch[1];
|
|
13
|
+
}
|
|
14
|
+
return trimmed.startsWith('li_at=') ? trimmed.slice('li_at='.length) : trimmed;
|
|
15
|
+
}
|
|
16
|
+
export function normalizeLiAtCookie(sessionCookie) {
|
|
17
|
+
return extractLiAtCookieValue(sessionCookie);
|
|
18
|
+
}
|
|
19
|
+
export function formatLiAtCookieHeader(sessionCookie) {
|
|
20
|
+
const normalized = normalizeLiAtCookie(sessionCookie);
|
|
21
|
+
return normalized ? `li_at=${normalized}` : null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const DEFAULT_PHANTOMBUSTER_API_BASE = 'https://api.phantombuster.com/api/v2';
|
|
3
|
+
export const DEFAULT_LINKEDIN_COMPANY_SCRAPER_AGENT_ID = '3415615142546934';
|
|
4
|
+
export const LINKEDIN_COMPANY_SCRAPER_COLUMN_NAME = 'Url';
|
|
5
|
+
export const linkedInCompanyScraperLaunchOptionsSchema = z.object({
|
|
6
|
+
spreadsheetUrl: z.string().url(),
|
|
7
|
+
columnName: z.string().min(1).nullable().optional(),
|
|
8
|
+
webhookUrl: z.string().url().nullable().optional(),
|
|
9
|
+
sessionCookie: z.string().min(1).nullable().optional(),
|
|
10
|
+
userAgent: z.string().min(1).nullable().optional(),
|
|
11
|
+
companiesPerLaunch: z.number().int().positive().nullable().optional(),
|
|
12
|
+
delayBetween: z.number().nonnegative().nullable().optional(),
|
|
13
|
+
});
|
|
14
|
+
export const phantombusterLaunchResponseSchema = z.object({
|
|
15
|
+
containerId: z.string().min(1),
|
|
16
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const linkedInSessionSelectionSourceSchema = z.enum([
|
|
3
|
+
'cli_linkedin_company_backfill',
|
|
4
|
+
'cli_linkedin_company_backfill_preflight',
|
|
5
|
+
'cli_linkedin_company_backfill_invalid_session',
|
|
6
|
+
'cli_linkedin_company_backfill_decrypt_failure',
|
|
7
|
+
'extension_user_profile',
|
|
8
|
+
'extension_user_profile_li_at_fallback',
|
|
9
|
+
'operator_env_seed',
|
|
10
|
+
'pipedream_raw_manual_seed',
|
|
11
|
+
]);
|
|
12
|
+
export const linkedInSessionInactiveReasonSchema = z.enum([
|
|
13
|
+
'linkedin_session_invalid',
|
|
14
|
+
'decrypt_failed',
|
|
15
|
+
'excluded_linkedin_session_identity',
|
|
16
|
+
'quarantined_browser_unverified',
|
|
17
|
+
]);
|
package/package.json
CHANGED
|
@@ -1,29 +1,88 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indiecrm-cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "IndieCRM CLI for CRM workspaces and Salesprompter research workflows",
|
|
5
5
|
"author": "Daniel Sinewe <hello@danielsinewe.com>",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
7
|
+
"bin": {
|
|
8
|
+
"indiecrm": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"RESEARCH.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"CONTRIBUTING.md",
|
|
16
|
+
"CODE_OF_CONDUCT.md",
|
|
17
|
+
"SECURITY.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
9
20
|
"scripts": {
|
|
10
|
-
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.
|
|
11
|
-
"
|
|
12
|
-
"
|
|
21
|
+
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json",
|
|
22
|
+
"benchmark:linkedin:direct": "node ./scripts/benchmark-linkedin-direct-strategies.mjs",
|
|
23
|
+
"build:docs:site": "node ./scripts/build-docs-site.mjs",
|
|
24
|
+
"check": "tsc --noEmit -p tsconfig.json",
|
|
25
|
+
"supabase:migrations:check": "node ./scripts/check-app-supabase-migrations.mjs",
|
|
26
|
+
"supabase:migrations:sync": "node ./scripts/sync-app-supabase-migrations.mjs",
|
|
27
|
+
"shared:sync": "node ./scripts/sync-shared.mjs",
|
|
28
|
+
"docs:dev": "mint dev",
|
|
29
|
+
"docs:broken-links": "mint broken-links",
|
|
30
|
+
"docs:a11y": "mint a11y",
|
|
31
|
+
"start": "node ./dist/cli.js",
|
|
32
|
+
"test": "npm run build && tsc -p tsconfig.test.json && node --test dist-tests/tests/**/*.test.js && node --test tests/indiecrm-cli.test.mjs && node --test tests/migration.test.mjs",
|
|
33
|
+
"verify:all": "npm run check && npm test && npm run build:docs:site && npm run docs:broken-links && npm run docs:a11y",
|
|
34
|
+
"vercel-build": "npm run build:docs:site"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.12.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
13
41
|
},
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
42
|
+
"keywords": [
|
|
43
|
+
"indiecrm",
|
|
44
|
+
"cli",
|
|
45
|
+
"crm",
|
|
46
|
+
"sales",
|
|
47
|
+
"mcp",
|
|
48
|
+
"salesprompter",
|
|
49
|
+
"lead-generation",
|
|
50
|
+
"enrichment",
|
|
51
|
+
"scoring",
|
|
52
|
+
"sync",
|
|
53
|
+
"workflow",
|
|
54
|
+
"automation",
|
|
55
|
+
"json",
|
|
56
|
+
"tooling"
|
|
57
|
+
],
|
|
17
58
|
"homepage": "https://indiecrm.app",
|
|
18
|
-
"repository": {
|
|
19
|
-
|
|
59
|
+
"repository": {
|
|
60
|
+
"type": "git",
|
|
61
|
+
"url": "git+https://github.com/danielsinewe/indiecrm-cli.git"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/danielsinewe/indiecrm-cli/issues"
|
|
65
|
+
},
|
|
20
66
|
"license": "MIT",
|
|
21
67
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"
|
|
68
|
+
"@google-cloud/bigquery": "^8.1.1",
|
|
69
|
+
"@supabase/supabase-js": "^2.99.1",
|
|
70
|
+
"@types/pg": "^8.20.0",
|
|
71
|
+
"cheerio": "^1.2.0",
|
|
72
|
+
"commander": "^14.0.1",
|
|
73
|
+
"pg": "^8.21.0",
|
|
74
|
+
"puppeteer-core": "^25.10.0",
|
|
75
|
+
"zod": "^4.1.5",
|
|
76
|
+
"@modelcontextprotocol/client": "^2.1.0"
|
|
24
77
|
},
|
|
25
78
|
"devDependencies": {
|
|
26
79
|
"@types/node": "^24.3.0",
|
|
80
|
+
"gray-matter": "^4.0.3",
|
|
81
|
+
"marked": "^16.3.0",
|
|
82
|
+
"mint": "^4.2.216",
|
|
27
83
|
"typescript": "^5.9.2"
|
|
84
|
+
},
|
|
85
|
+
"overrides": {
|
|
86
|
+
"undici": "^7.24.0"
|
|
28
87
|
}
|
|
29
88
|
}
|