fullstackgtm 0.49.0 → 0.50.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/CHANGELOG.md +53 -0
- package/DATA-FLOWS.md +1 -0
- package/README.md +41 -0
- package/dist/acquireCheckpoint.d.ts +33 -0
- package/dist/acquireCheckpoint.js +149 -0
- package/dist/acquireLinkedIn.d.ts +2 -0
- package/dist/acquireLinkedIn.js +1 -1
- package/dist/cli/enrich.js +283 -79
- package/dist/cli/help.js +13 -9
- package/dist/cli/plans.js +100 -6
- package/dist/cli/shared.d.ts +3 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli/ui.d.ts +10 -5
- package/dist/cli/ui.js +16 -5
- package/dist/connectors/linkedin.d.ts +2 -0
- package/dist/connectors/linkedin.js +5 -0
- package/dist/connectors/prospectSources.d.ts +23 -0
- package/dist/connectors/prospectSources.js +21 -3
- package/dist/enrich.d.ts +44 -1
- package/dist/hostedAcquireCheckpoint.d.ts +43 -0
- package/dist/hostedAcquireCheckpoint.js +129 -0
- package/dist/hostedPatchPlan.d.ts +87 -0
- package/dist/hostedPatchPlan.js +270 -0
- package/dist/icp.js +13 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/planStore.d.ts +14 -0
- package/dist/planStore.js +73 -0
- package/dist/progress.d.ts +2 -2
- package/dist/progress.js +2 -2
- package/docs/api.md +24 -0
- package/docs/architecture.md +9 -0
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +1 -1
- package/src/acquireCheckpoint.ts +186 -0
- package/src/acquireLinkedIn.ts +3 -1
- package/src/cli/enrich.ts +322 -78
- package/src/cli/help.ts +14 -9
- package/src/cli/plans.ts +95 -6
- package/src/cli/shared.ts +2 -1
- package/src/cli/ui.ts +18 -9
- package/src/connectors/linkedin.ts +6 -0
- package/src/connectors/prospectSources.ts +46 -4
- package/src/enrich.ts +47 -1
- package/src/hostedAcquireCheckpoint.ts +156 -0
- package/src/hostedPatchPlan.ts +286 -0
- package/src/icp.ts +14 -2
- package/src/index.ts +20 -0
- package/src/planStore.ts +87 -0
- package/src/progress.ts +2 -2
|
@@ -63,7 +63,7 @@ credentials AND stored plans per client org.
|
|
|
63
63
|
| `backfill stripe\|runs [--since <iso>] [--pipeline <id\|label>]` | Paid Stripe invoices → proposed closed-won deals (amount = invoice total, close date = paid date, company matched by billing-email domain then name); deduped by a `stripe_invoice_id` deal property re-resolved at apply, so re-running never double-creates; a customer the CRM doesn't know gets a proposed account create in the same plan (freemail domains never used; `--skip-unmatched` = report-only); `--save` → approve → `apply`. `backfill runs` replays LOCAL run history + health timeline to the paired hosted app (idempotent; `--dry-run` to preview) |
|
|
64
64
|
| `call parse\|score\|link\|plan` | Transcripts → evidence-quoted insights, rubric scorecards, deal linking, governed next-step writes |
|
|
65
65
|
| `enrich append\|refresh\|ingest\|status` | Governed enrichment (Apollo pull / Clay ingest), fill-blanks-only plans |
|
|
66
|
-
| `enrich acquire [--source explorium\|pipe0\|linkedin]` | Net-new ICP-targeted lead gen:
|
|
66
|
+
| `enrich acquire [--source explorium\|pipe0\|linkedin] [--max <new>] [--scan-limit <raw>]` | Net-new ICP-targeted lead gen: independent per-provider/list/query checkpoints traverse full audiences instead of rereading page one; paired CLIs CAS-sync opaque, non-PII continuation to the hosted org; resolve-first deduped `create_record` plans, meter-capped and owner-stamped; `enrich status --runs` exposes funnel + continuation; never auto-writes |
|
|
67
67
|
| `signals fetch\|list\|outcome\|weights` | Detect-side timing layer: capture fresh buying triggers into a profile-scoped ledger (free Greenhouse/Lever/Ashby ATS in the box; `--from` staged files; `--connector file\|serpapi-news\|hubspot-forms` to pull connected platforms / read the webhook spool `<home>/signals/spool`, secrets via the credential ladder), ranked by learned weights. Writes NOTHING to the CRM; `outcome --account <domain> --contact <id> --result …` re-weights which triggers earn a touch |
|
|
68
68
|
| `icp interview\|set\|show\|judge\|eval` | Build the ICP, then `judge` ranks fresh signals into send/nurture/skip — pass a snapshot (`--provider`/`--input`) and each decision resolves its CRM target (`accountId` + the `contact`/`contacts` to reach). `eval` is the calibration gate (exit 2 below the bar) |
|
|
69
69
|
| `draft [--from-judge latest] [--channel email\|linkedin\|task]` | One trigger-grounded opener per hot account → a governed `create_task` targeting the resolved `contact.id` (or `accountId`); rejects a domain-only decision ("acquire it first"). **Never sends** — after `plans approve`, `apply --provider <crm>` logs it as a CRM task, or `apply --channel outbox` renders it to `<home>/signals/outbox/<channel>.jsonl` for a downstream sender to drain; the CLI transmits nothing |
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { credentialsDir, ensureSecureHomeDir } from "./credentials.ts";
|
|
5
|
+
import {
|
|
6
|
+
assertNoSymlinkComponents,
|
|
7
|
+
readSecureRegularFile,
|
|
8
|
+
writeSecureFileAtomic,
|
|
9
|
+
} from "./secureFile.ts";
|
|
10
|
+
|
|
11
|
+
/** The complete identity of one independently traversable provider audience. */
|
|
12
|
+
export type AcquireCheckpointKey = {
|
|
13
|
+
provider: string;
|
|
14
|
+
source: string;
|
|
15
|
+
listId: string | null;
|
|
16
|
+
queryFingerprint: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type AcquireContinuation = {
|
|
20
|
+
cursor?: string | null;
|
|
21
|
+
offset?: number | null;
|
|
22
|
+
exhausted: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Versioned wire format shared by local persistence and hosted sync. */
|
|
26
|
+
export type AcquireCheckpoint = {
|
|
27
|
+
version: 1;
|
|
28
|
+
id: string;
|
|
29
|
+
key: AcquireCheckpointKey;
|
|
30
|
+
continuation: AcquireContinuation;
|
|
31
|
+
updatedAt: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const COMPONENT_MAX = 512;
|
|
35
|
+
|
|
36
|
+
function requireComponent(value: unknown, name: string): string {
|
|
37
|
+
if (typeof value !== "string" || value.length === 0 || value.length > COMPONENT_MAX || /[\u0000-\u001f]/.test(value)) {
|
|
38
|
+
throw new Error(`Invalid acquisition checkpoint ${name}`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function validateAcquireCheckpointKey(value: unknown): AcquireCheckpointKey {
|
|
44
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
45
|
+
throw new Error("Invalid acquisition checkpoint key");
|
|
46
|
+
}
|
|
47
|
+
const candidate = value as Partial<AcquireCheckpointKey>;
|
|
48
|
+
const listId = candidate.listId;
|
|
49
|
+
if (listId !== null && listId !== undefined) requireComponent(listId, "listId");
|
|
50
|
+
return {
|
|
51
|
+
provider: requireComponent(candidate.provider, "provider"),
|
|
52
|
+
source: requireComponent(candidate.source, "source"),
|
|
53
|
+
listId: listId ?? null,
|
|
54
|
+
queryFingerprint: requireComponent(candidate.queryFingerprint, "queryFingerprint"),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Stable, non-secret filename/key suitable for both filesystem and hosted records. */
|
|
59
|
+
export function acquireCheckpointId(key: AcquireCheckpointKey): string {
|
|
60
|
+
const valid = validateAcquireCheckpointKey(key);
|
|
61
|
+
const canonical = JSON.stringify([valid.provider, valid.source, valid.listId, valid.queryFingerprint]);
|
|
62
|
+
return `acqcp_${createHash("sha256").update(canonical).digest("hex")}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateContinuation(value: unknown): AcquireContinuation {
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
67
|
+
throw new Error("Invalid acquisition checkpoint continuation");
|
|
68
|
+
}
|
|
69
|
+
const candidate = value as Partial<AcquireContinuation>;
|
|
70
|
+
if (typeof candidate.exhausted !== "boolean") throw new Error("Invalid acquisition checkpoint exhausted flag");
|
|
71
|
+
if (candidate.cursor !== undefined && candidate.cursor !== null) requireComponent(candidate.cursor, "cursor");
|
|
72
|
+
if (
|
|
73
|
+
candidate.offset !== undefined && candidate.offset !== null &&
|
|
74
|
+
(!Number.isSafeInteger(candidate.offset) || candidate.offset < 0)
|
|
75
|
+
) {
|
|
76
|
+
throw new Error("Invalid acquisition checkpoint offset");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...(candidate.cursor !== undefined ? { cursor: candidate.cursor } : {}),
|
|
80
|
+
...(candidate.offset !== undefined ? { offset: candidate.offset } : {}),
|
|
81
|
+
exhausted: candidate.exhausted,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateAcquireCheckpoint(value: unknown): AcquireCheckpoint {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
87
|
+
throw new Error("Invalid acquisition checkpoint");
|
|
88
|
+
}
|
|
89
|
+
const candidate = value as Partial<AcquireCheckpoint>;
|
|
90
|
+
if (candidate.version !== 1) throw new Error(`Unsupported acquisition checkpoint version: ${String(candidate.version)}`);
|
|
91
|
+
const key = validateAcquireCheckpointKey(candidate.key);
|
|
92
|
+
const id = acquireCheckpointId(key);
|
|
93
|
+
if (candidate.id !== id) throw new Error("Acquisition checkpoint id does not match its key");
|
|
94
|
+
if (typeof candidate.updatedAt !== "string" || !Number.isFinite(Date.parse(candidate.updatedAt))) {
|
|
95
|
+
throw new Error("Invalid acquisition checkpoint updatedAt");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
version: 1,
|
|
99
|
+
id,
|
|
100
|
+
key,
|
|
101
|
+
continuation: validateContinuation(candidate.continuation),
|
|
102
|
+
updatedAt: candidate.updatedAt,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function acquireCheckpointsDir(baseDir?: string): string {
|
|
107
|
+
return join(baseDir ?? credentialsDir(), "acquire", "checkpoints");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface AcquireCheckpointStore {
|
|
111
|
+
get(key: AcquireCheckpointKey): Promise<AcquireCheckpoint | null>;
|
|
112
|
+
put(key: AcquireCheckpointKey, continuation: AcquireContinuation, updatedAt?: Date): Promise<AcquireCheckpoint>;
|
|
113
|
+
/** Import a validated local/hosted record without changing its timestamp. */
|
|
114
|
+
putRecord(record: AcquireCheckpoint): Promise<AcquireCheckpoint>;
|
|
115
|
+
list(): Promise<AcquireCheckpoint[]>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function createFileAcquireCheckpointStore(baseDir?: string): AcquireCheckpointStore {
|
|
119
|
+
const directory = acquireCheckpointsDir(baseDir);
|
|
120
|
+
|
|
121
|
+
function fileForId(id: string): string {
|
|
122
|
+
return join(directory, `${id}.json`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readId(id: string): AcquireCheckpoint | null {
|
|
126
|
+
try {
|
|
127
|
+
return validateAcquireCheckpoint(JSON.parse(readSecureRegularFile(fileForId(id), { tightenMode: 0o600 })));
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ensureDirectory(): void {
|
|
135
|
+
if (baseDir === undefined) ensureSecureHomeDir();
|
|
136
|
+
// Tighten both managed levels. mkdir's mode does not affect a directory
|
|
137
|
+
// restored or pre-created with broader permissions.
|
|
138
|
+
const levels = [join(baseDir ?? credentialsDir(), "acquire"), directory];
|
|
139
|
+
for (const level of levels) {
|
|
140
|
+
assertNoSymlinkComponents(level);
|
|
141
|
+
mkdirSync(level, { recursive: true, mode: 0o700 });
|
|
142
|
+
assertNoSymlinkComponents(level);
|
|
143
|
+
try { chmodSync(level, 0o700); } catch { /* chmod is unavailable on some filesystems. */ }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function writeRecord(value: AcquireCheckpoint): AcquireCheckpoint {
|
|
148
|
+
const record = validateAcquireCheckpoint(value);
|
|
149
|
+
ensureDirectory();
|
|
150
|
+
writeSecureFileAtomic(fileForId(record.id), `${JSON.stringify(record, null, 2)}\n`);
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
async get(key) {
|
|
156
|
+
return readId(acquireCheckpointId(key));
|
|
157
|
+
},
|
|
158
|
+
async put(key, continuation, updatedAt = new Date()) {
|
|
159
|
+
const validKey = validateAcquireCheckpointKey(key);
|
|
160
|
+
return writeRecord({
|
|
161
|
+
version: 1,
|
|
162
|
+
id: acquireCheckpointId(validKey),
|
|
163
|
+
key: validKey,
|
|
164
|
+
continuation: validateContinuation(continuation),
|
|
165
|
+
updatedAt: updatedAt.toISOString(),
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
async putRecord(record) {
|
|
169
|
+
return writeRecord(record);
|
|
170
|
+
},
|
|
171
|
+
async list() {
|
|
172
|
+
let names: string[];
|
|
173
|
+
try {
|
|
174
|
+
assertNoSymlinkComponents(directory);
|
|
175
|
+
names = readdirSync(directory).filter((name) => /^acqcp_[a-f0-9]{64}\.json$/.test(name));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
return names
|
|
181
|
+
.map((name) => readId(name.slice(0, -5)))
|
|
182
|
+
.filter((record): record is AcquireCheckpoint => record !== null)
|
|
183
|
+
.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
package/src/acquireLinkedIn.ts
CHANGED
|
@@ -49,6 +49,8 @@ export type DiscoverLinkedInOptions = {
|
|
|
49
49
|
sourceId?: string;
|
|
50
50
|
/** Hard cap on prospects pulled. */
|
|
51
51
|
max?: number;
|
|
52
|
+
/** Opaque provider continuation cursor. */
|
|
53
|
+
cursor?: string;
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
/**
|
|
@@ -60,7 +62,7 @@ export async function discoverLinkedInProspects(
|
|
|
60
62
|
provider: LinkedInProvider,
|
|
61
63
|
options: DiscoverLinkedInOptions = {},
|
|
62
64
|
): Promise<Prospect[]> {
|
|
63
|
-
const raw = await provider.fetchProspects({ sourceId: options.sourceId, max: options.max });
|
|
65
|
+
const raw = await provider.fetchProspects({ sourceId: options.sourceId, max: options.max, cursor: options.cursor });
|
|
64
66
|
return raw.map(linkedInProspectToProspect);
|
|
65
67
|
}
|
|
66
68
|
|