fullstackgtm 0.43.0 → 0.45.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +378 -1
  2. package/README.md +37 -14
  3. package/dist/audit.d.ts +3 -1
  4. package/dist/audit.js +29 -2
  5. package/dist/cli/audit.d.ts +15 -0
  6. package/dist/cli/audit.js +392 -0
  7. package/dist/cli/auth.d.ts +80 -0
  8. package/dist/cli/auth.js +500 -0
  9. package/dist/cli/call.d.ts +1 -0
  10. package/dist/cli/call.js +373 -0
  11. package/dist/cli/capabilities.d.ts +30 -0
  12. package/dist/cli/capabilities.js +197 -0
  13. package/dist/cli/draft.d.ts +7 -0
  14. package/dist/cli/draft.js +87 -0
  15. package/dist/cli/enrich.d.ts +8 -0
  16. package/dist/cli/enrich.js +788 -0
  17. package/dist/cli/fix.d.ts +33 -0
  18. package/dist/cli/fix.js +344 -0
  19. package/dist/cli/help.d.ts +25 -0
  20. package/dist/cli/help.js +609 -0
  21. package/dist/cli/icp.d.ts +7 -0
  22. package/dist/cli/icp.js +229 -0
  23. package/dist/cli/init.d.ts +7 -0
  24. package/dist/cli/init.js +58 -0
  25. package/dist/cli/market.d.ts +1 -0
  26. package/dist/cli/market.js +391 -0
  27. package/dist/cli/plans.d.ts +5 -0
  28. package/dist/cli/plans.js +454 -0
  29. package/dist/cli/schedule.d.ts +8 -0
  30. package/dist/cli/schedule.js +479 -0
  31. package/dist/cli/shared.d.ts +70 -0
  32. package/dist/cli/shared.js +331 -0
  33. package/dist/cli/signals.d.ts +7 -0
  34. package/dist/cli/signals.js +412 -0
  35. package/dist/cli/suggest.d.ts +49 -0
  36. package/dist/cli/suggest.js +135 -0
  37. package/dist/cli/tam.d.ts +9 -0
  38. package/dist/cli/tam.js +387 -0
  39. package/dist/cli/ui.d.ts +121 -0
  40. package/dist/cli/ui.js +375 -0
  41. package/dist/cli.d.ts +1 -57
  42. package/dist/cli.js +104 -4556
  43. package/dist/connector.d.ts +15 -0
  44. package/dist/connector.js +35 -0
  45. package/dist/connectors/hubspot.d.ts +3 -1
  46. package/dist/connectors/hubspot.js +10 -3
  47. package/dist/connectors/outboxChannel.d.ts +64 -0
  48. package/dist/connectors/outboxChannel.js +170 -0
  49. package/dist/connectors/prospectSources.d.ts +22 -0
  50. package/dist/connectors/prospectSources.js +43 -0
  51. package/dist/connectors/salesforce.d.ts +3 -1
  52. package/dist/connectors/salesforce.js +12 -5
  53. package/dist/connectors/signalSources.d.ts +105 -0
  54. package/dist/connectors/signalSources.js +264 -0
  55. package/dist/connectors/theirstack.d.ts +84 -0
  56. package/dist/connectors/theirstack.js +125 -0
  57. package/dist/icp.d.ts +52 -4
  58. package/dist/icp.js +112 -35
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +1 -0
  61. package/dist/init.js +3 -0
  62. package/dist/judge.d.ts +2 -0
  63. package/dist/judge.js +6 -0
  64. package/dist/judgeEval.d.ts +7 -0
  65. package/dist/judgeEval.js +8 -1
  66. package/dist/marketClassify.d.ts +2 -0
  67. package/dist/marketClassify.js +7 -1
  68. package/dist/mcp-bin.js +2 -2
  69. package/dist/mcp.js +259 -166
  70. package/dist/runReport.d.ts +26 -0
  71. package/dist/runReport.js +15 -0
  72. package/dist/schedule.d.ts +80 -2
  73. package/dist/schedule.js +272 -5
  74. package/dist/signals.d.ts +54 -0
  75. package/dist/signals.js +64 -0
  76. package/dist/spoolFiles.d.ts +44 -0
  77. package/dist/spoolFiles.js +114 -0
  78. package/dist/tam.d.ts +225 -0
  79. package/dist/tam.js +470 -0
  80. package/dist/types.d.ts +11 -0
  81. package/docs/api.md +91 -11
  82. package/docs/outbox-format.md +92 -0
  83. package/docs/recipes.md +37 -0
  84. package/docs/roadmap-to-1.0.md +31 -1
  85. package/docs/signal-spool-format.md +175 -0
  86. package/docs/tam.md +195 -0
  87. package/llms.txt +83 -11
  88. package/package.json +1 -1
  89. package/skills/fullstackgtm/SKILL.md +4 -3
  90. package/src/audit.ts +27 -1
  91. package/src/cli/audit.ts +447 -0
  92. package/src/cli/auth.ts +549 -0
  93. package/src/cli/call.ts +398 -0
  94. package/src/cli/capabilities.ts +215 -0
  95. package/src/cli/draft.ts +101 -0
  96. package/src/cli/enrich.ts +885 -0
  97. package/src/cli/fix.ts +372 -0
  98. package/src/cli/help.ts +664 -0
  99. package/src/cli/icp.ts +265 -0
  100. package/src/cli/init.ts +65 -0
  101. package/src/cli/market.ts +423 -0
  102. package/src/cli/plans.ts +523 -0
  103. package/src/cli/schedule.ts +526 -0
  104. package/src/cli/shared.ts +375 -0
  105. package/src/cli/signals.ts +434 -0
  106. package/src/cli/suggest.ts +151 -0
  107. package/src/cli/tam.ts +435 -0
  108. package/src/cli/ui.ts +426 -0
  109. package/src/cli.ts +103 -5170
  110. package/src/connector.ts +46 -0
  111. package/src/connectors/hubspot.ts +14 -2
  112. package/src/connectors/outboxChannel.ts +202 -0
  113. package/src/connectors/prospectSources.ts +57 -0
  114. package/src/connectors/salesforce.ts +18 -2
  115. package/src/connectors/signalSources.ts +314 -0
  116. package/src/connectors/theirstack.ts +170 -0
  117. package/src/icp.ts +120 -34
  118. package/src/index.ts +32 -0
  119. package/src/init.ts +3 -0
  120. package/src/judge.ts +7 -0
  121. package/src/judgeEval.ts +8 -1
  122. package/src/marketClassify.ts +8 -1
  123. package/src/mcp-bin.ts +2 -2
  124. package/src/mcp.ts +130 -57
  125. package/src/runReport.ts +39 -0
  126. package/src/schedule.ts +330 -7
  127. package/src/signals.ts +90 -0
  128. package/src/spoolFiles.ts +116 -0
  129. package/src/tam.ts +654 -0
  130. package/src/types.ts +12 -0
@@ -0,0 +1,331 @@
1
+ // Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { createHubspotConnector } from "../connectors/hubspot.js";
5
+ import { createSalesforceConnector } from "../connectors/salesforce.js";
6
+ import { createStripeConnector } from "../connectors/stripe.js";
7
+ import { credentialsPath, getCredential, resolveHubspotConnection, resolveSalesforceConnection, storeCredential } from "../credentials.js";
8
+ import { generateDemoSnapshot } from "../demo.js";
9
+ import { builtinAuditRules } from "../rules.js";
10
+ import { sampleSnapshot } from "../sampleData.js";
11
+ import { detectProviderFromKey, resolveLlmCredential, validateLlmKey } from "../llm.js";
12
+ import { parseIcp } from "../icp.js";
13
+ import { nearest } from "./suggest.js";
14
+ import { createStatusLine, formatCount, formatDuration } from "./ui.js";
15
+ export function option(args, name) {
16
+ const index = args.indexOf(name);
17
+ if (index === -1)
18
+ return null;
19
+ return args[index + 1] ?? null;
20
+ }
21
+ export function repeatedOption(args, name) {
22
+ const values = [];
23
+ for (let index = 0; index < args.length; index += 1) {
24
+ if (args[index] === name && args[index + 1] !== undefined) {
25
+ values.push(args[index + 1]);
26
+ }
27
+ }
28
+ return values;
29
+ }
30
+ /**
31
+ * Additive alias: `--dry-run` asserts the preview default. Every verb that
32
+ * honors it is already preview-by-default (plans are dry-run; nothing writes
33
+ * to a CRM without approve → apply) — the flag additionally suppresses
34
+ * `--save` (and the `fix --yes` write stage) so the invocation can never
35
+ * persist a plan/run record or reach a write. Omitting it changes nothing:
36
+ * defaults stay exactly as before.
37
+ */
38
+ export function dryRunRequested(args) {
39
+ return args.includes("--dry-run");
40
+ }
41
+ let dryRunOverridesSaveNoted = false;
42
+ /** `--save`, unless `--dry-run` locks the preview default (see dryRunRequested). */
43
+ export function saveRequested(args) {
44
+ const save = args.includes("--save");
45
+ if (save && dryRunRequested(args)) {
46
+ if (!dryRunOverridesSaveNoted) {
47
+ console.error("(--dry-run overrides --save: preview only, nothing persisted)");
48
+ dryRunOverridesSaveNoted = true;
49
+ }
50
+ return false;
51
+ }
52
+ return save;
53
+ }
54
+ /**
55
+ * Additive alias: `--confirm` is accepted wherever an ad-hoc confirmation
56
+ * flag already gates a write stage (e.g. `fix --yes`). The old flags keep
57
+ * working; `--dry-run` wins over both.
58
+ */
59
+ export function confirmRequested(args, ...legacyAliases) {
60
+ if (dryRunRequested(args))
61
+ return false;
62
+ return ["--confirm", ...legacyAliases].some((flag) => args.includes(flag));
63
+ }
64
+ export function numericOption(args, name) {
65
+ const value = option(args, name);
66
+ if (value === null)
67
+ return undefined;
68
+ const parsed = Number(value);
69
+ if (!Number.isFinite(parsed))
70
+ throw new Error(`${name} must be a number`);
71
+ return parsed;
72
+ }
73
+ async function hubspotConnection(args) {
74
+ const tokenEnv = option(args, "--token-env");
75
+ if (tokenEnv) {
76
+ const token = process.env[tokenEnv];
77
+ if (!token)
78
+ throw new Error(`${tokenEnv} is not set.`);
79
+ return { accessToken: token };
80
+ }
81
+ if (process.env.HUBSPOT_ACCESS_TOKEN) {
82
+ return { accessToken: process.env.HUBSPOT_ACCESS_TOKEN };
83
+ }
84
+ const stored = await resolveHubspotConnection();
85
+ if (stored)
86
+ return stored;
87
+ throw new Error("No HubSpot credentials. Run `fullstackgtm login hubspot`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set HUBSPOT_ACCESS_TOKEN. (`fullstackgtm doctor` shows credential status; see the README's \"Connect your CRM\" section for the private-app scopes to grant.)");
88
+ }
89
+ async function salesforceConnection() {
90
+ if (process.env.SALESFORCE_ACCESS_TOKEN && process.env.SALESFORCE_INSTANCE_URL) {
91
+ return {
92
+ accessToken: process.env.SALESFORCE_ACCESS_TOKEN,
93
+ instanceUrl: process.env.SALESFORCE_INSTANCE_URL,
94
+ };
95
+ }
96
+ const stored = await resolveSalesforceConnection();
97
+ if (stored)
98
+ return stored;
99
+ throw new Error("No Salesforce credentials. Run `fullstackgtm login salesforce`, pair with a hosted deployment via `fullstackgtm login --via <url>`, or set SALESFORCE_ACCESS_TOKEN and SALESFORCE_INSTANCE_URL. (`fullstackgtm doctor` shows credential status; device-flow login needs an admin-created Connected App — see the README's \"Connect your CRM\" section.)");
100
+ }
101
+ export async function connectorFor(provider, args, onProgress) {
102
+ if (provider === "hubspot") {
103
+ const connection = await hubspotConnection(args);
104
+ return createHubspotConnector({
105
+ getAccessToken: () => connection.accessToken,
106
+ fieldMappings: connection.fieldMappings ?? undefined,
107
+ // Point at a mock/proxy HubSpot (tests, evals, request-recording).
108
+ apiBaseUrl: process.env.HUBSPOT_API_BASE_URL,
109
+ onProgress,
110
+ });
111
+ }
112
+ if (provider === "salesforce") {
113
+ const connection = await salesforceConnection();
114
+ return createSalesforceConnector({
115
+ getConnection: () => connection,
116
+ fieldMappings: connection.fieldMappings ?? undefined,
117
+ onProgress,
118
+ });
119
+ }
120
+ if (provider === "stripe") {
121
+ const key = process.env.STRIPE_SECRET_KEY ?? getCredential("stripe")?.accessToken;
122
+ if (!key) {
123
+ throw new Error("No Stripe credentials. Run `echo \"$STRIPE_KEY\" | fullstackgtm login stripe` or set STRIPE_SECRET_KEY. A restricted key with read access to Customers and Subscriptions is enough. (`fullstackgtm doctor` shows credential status.)");
124
+ }
125
+ return createStripeConnector({ getApiKey: () => key });
126
+ }
127
+ const providerSuggestion = nearest(provider.toLowerCase(), ["hubspot", "salesforce", "stripe"], 3);
128
+ throw new Error(`Unknown provider: ${provider}.${providerSuggestion ? ` Did you mean ${providerSuggestion}?` : ""} ` +
129
+ "Supported providers: hubspot, salesforce, stripe");
130
+ }
131
+ export async function readSnapshot(args) {
132
+ const provider = option(args, "--provider");
133
+ if (provider) {
134
+ // Live pulls can page for minutes with nothing on screen. On an
135
+ // interactive terminal, a stderr status line shows the running tally;
136
+ // piped/CI/agent runs get an inert no-op (zero bytes written).
137
+ const status = createStatusLine();
138
+ const startedAt = Date.now();
139
+ const connector = await connectorFor(provider, args, status.active
140
+ ? (progress) => status.set(`Pulling ${progress.objectType}s from ${provider}… ${formatCount(progress.fetched)} fetched · ${formatDuration(Date.now() - startedAt)}`)
141
+ : undefined);
142
+ try {
143
+ return await connector.fetchSnapshot();
144
+ }
145
+ finally {
146
+ status.done();
147
+ }
148
+ }
149
+ if (args.includes("--demo")) {
150
+ return generateDemoSnapshot({
151
+ seed: numericOption(args, "--seed"),
152
+ today: option(args, "--today") ?? undefined,
153
+ });
154
+ }
155
+ if (args.includes("--sample"))
156
+ return sampleSnapshot;
157
+ const input = option(args, "--input");
158
+ if (!input) {
159
+ throw new Error("No data source. Pass one of: --provider <hubspot|salesforce|stripe> (live, read-only), " +
160
+ "--input <snapshot.json> (a saved snapshot), --demo (a generated messy CRM), or " +
161
+ "--sample (the tiny built-in fixture). Refusing to silently audit sample data.");
162
+ }
163
+ const path = resolve(process.cwd(), input);
164
+ let parsed;
165
+ try {
166
+ parsed = JSON.parse(readFileSync(path, "utf8"));
167
+ }
168
+ catch (error) {
169
+ const reason = error instanceof Error ? error.message : String(error);
170
+ throw new Error(`--input ${input} could not be read as JSON: ${reason}`);
171
+ }
172
+ return assertCanonicalSnapshot(parsed, input);
173
+ }
174
+ /**
175
+ * Validate that an --input file actually has the canonical snapshot shape
176
+ * (the JSON `snapshot --out` writes) instead of blindly casting — a plan or
177
+ * arbitrary JSON passed here used to surface as a confusing crash deep in a
178
+ * rule, or worse, an empty "clean" audit.
179
+ */
180
+ export function assertCanonicalSnapshot(parsed, source) {
181
+ const expected = "expected the shape written by `fullstackgtm snapshot --out <path>`: " +
182
+ "{ generatedAt, provider, users[], accounts[], contacts[], deals[], activities[] }";
183
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
184
+ throw new Error(`--input ${source} is not a canonical GTM snapshot (top level is not an object); ${expected}.`);
185
+ }
186
+ const record = parsed;
187
+ const arrayFields = ["users", "accounts", "contacts", "deals", "activities"];
188
+ const problems = arrayFields
189
+ .filter((field) => !Array.isArray(record[field]))
190
+ .map((field) => (record[field] === undefined ? `missing "${field}"` : `"${field}" is not an array`));
191
+ if (problems.length > 0) {
192
+ throw new Error(`--input ${source} is not a canonical GTM snapshot (${problems.join(", ")}); ${expected}.`);
193
+ }
194
+ return parsed;
195
+ }
196
+ export function selectedRules(args, baseRules = builtinAuditRules) {
197
+ const requested = option(args, "--rules");
198
+ if (!requested)
199
+ return baseRules;
200
+ const ids = requested.split(",").map((id) => id.trim()).filter(Boolean);
201
+ const known = new Map(baseRules.map((rule) => [rule.id, rule]));
202
+ const rules = ids.map((id) => {
203
+ const rule = known.get(id);
204
+ if (!rule) {
205
+ throw new Error(`Unknown rule: ${id}. Available rules: ${baseRules.map((r) => r.id).join(", ")}`);
206
+ }
207
+ return rule;
208
+ });
209
+ return rules;
210
+ }
211
+ /**
212
+ * First-touch key onboarding: env vars win, then the credential store; on a
213
+ * TTY a missing key is captured once (validated, stored 0600 like provider
214
+ * logins). Non-interactive contexts get an actionable error instead.
215
+ */
216
+ export async function requireLlmCredential(command = "parse") {
217
+ // Base-URL overrides are resolved here (not inside forcedToolCall) so the LLM
218
+ // seam stays pure/injectable. Spread into LlmCallOptions at every call site
219
+ // via `...credential`; unset env leaves the upstream defaults untouched.
220
+ const baseUrls = resolveLlmBaseUrls();
221
+ const resolved = resolveLlmCredential();
222
+ if (resolved)
223
+ return { ...resolved, ...baseUrls };
224
+ // Scoring is inherently LLM work — there is no keyword fallback to suggest.
225
+ const fallbackHint = command === "parse"
226
+ ? ", or pass --deterministic for the free keyword baseline"
227
+ : command === "score"
228
+ ? " (call score has no non-LLM mode)"
229
+ : ", or classify by hand: `market worksheet --vendor <id>` then `market observe --from`";
230
+ const work = command === "score" ? "scoring" : command === "parse" ? "extraction" : "classification";
231
+ if (!process.stdin.isTTY) {
232
+ throw new Error(`LLM ${work} needs an API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or run \`echo "$KEY" | fullstackgtm login anthropic\` (or \`login openai\`) once${fallbackHint}.`);
233
+ }
234
+ console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
235
+ console.error(`Paste it once; it is validated and stored at ${credentialsPath()} (file mode 0600), like CRM logins.`);
236
+ console.error("(Alternatives: set ANTHROPIC_API_KEY / OPENAI_API_KEY, or pass --deterministic for the free keyword baseline.)\n");
237
+ const apiKey = await readSecret("API key (sk-ant-... or sk-...): ");
238
+ const provider = detectProviderFromKey(apiKey);
239
+ const validation = await validateLlmKey(provider, apiKey);
240
+ if (!validation.ok)
241
+ throw new Error(`${provider} rejected the key: ${validation.detail}`);
242
+ const now = new Date().toISOString();
243
+ storeCredential(provider, { kind: "api_key", accessToken: apiKey, createdAt: now, updatedAt: now });
244
+ console.error(`Stored ${provider} key (${validation.detail}). Future runs use it automatically; remove with \`fullstackgtm logout ${provider}\`.\n`);
245
+ return { provider, apiKey, ...baseUrls };
246
+ }
247
+ /**
248
+ * Read optional LLM base-URL overrides from env so the package can run against
249
+ * an Anthropic/OpenAI-compatible gateway (GLM-5.2, z.ai, Ollama) without code
250
+ * changes. Returns only the keys that are set; an empty object when neither is,
251
+ * so spreading into LlmCallOptions is a no-op by default.
252
+ */
253
+ export function resolveLlmBaseUrls(env = process.env) {
254
+ const out = {};
255
+ if (env.ANTHROPIC_API_BASE_URL)
256
+ out.anthropicBaseUrl = env.ANTHROPIC_API_BASE_URL;
257
+ if (env.OPENAI_API_BASE_URL)
258
+ out.openaiBaseUrl = env.OPENAI_API_BASE_URL;
259
+ return out;
260
+ }
261
+ /** Load the active ICP: --icp <path>, else ./icp.json. Undefined if none. */
262
+ export function loadIcp(args) {
263
+ const explicit = option(args, "--icp");
264
+ const path = resolve(process.cwd(), explicit ?? "icp.json");
265
+ if (!existsSync(path)) {
266
+ if (explicit)
267
+ throw new Error(`--icp ${explicit}: file not found`);
268
+ return undefined;
269
+ }
270
+ return parseIcp(readFileSync(path, "utf8"));
271
+ }
272
+ /**
273
+ * Read a secret without exposing it on the command line. Secrets passed as
274
+ * argv leak through the process list (`ps`), `/proc`, and shell history, so
275
+ * the CLI never accepts them as flags. Instead:
276
+ * - non-interactive: pipe it on stdin (docker `--password-stdin` style),
277
+ * e.g. `echo "$TOKEN" | fullstackgtm login hubspot`
278
+ * - interactive: prompted on the TTY with the input muted
279
+ */
280
+ export async function readSecret(label) {
281
+ if (!process.stdin.isTTY) {
282
+ const chunks = [];
283
+ for await (const chunk of process.stdin) {
284
+ chunks.push(chunk);
285
+ }
286
+ const piped = Buffer.concat(chunks).toString("utf8").trim();
287
+ if (!piped) {
288
+ throw new Error(`No secret provided. Pipe it on stdin (e.g. \`echo "$SECRET" | fullstackgtm login ...\`) ` +
289
+ `or run interactively. Secrets are never accepted as CLI arguments — they leak via the ` +
290
+ `process list and shell history.`);
291
+ }
292
+ // Multi-line stdin (rare) — take the first non-empty line.
293
+ return piped.split(/\r?\n/)[0].trim();
294
+ }
295
+ const readline = await import("node:readline");
296
+ return new Promise((resolveSecret) => {
297
+ const rl = readline.createInterface({
298
+ input: process.stdin,
299
+ output: process.stderr,
300
+ terminal: true,
301
+ });
302
+ let muted = false;
303
+ const mutable = rl;
304
+ mutable._writeToOutput = (value) => {
305
+ if (!muted)
306
+ process.stderr.write(value);
307
+ };
308
+ rl.question(`${label}: `, (answer) => {
309
+ rl.close();
310
+ process.stderr.write("\n");
311
+ resolveSecret(answer.trim());
312
+ });
313
+ muted = true;
314
+ });
315
+ }
316
+ export function isOptionValue(args, arg) {
317
+ const index = args.indexOf(arg);
318
+ return index > 0 && args[index - 1].startsWith("--");
319
+ }
320
+ export function readPackageInfo() {
321
+ try {
322
+ // This file lives one level deeper than the old src/cli.ts, so the hop to
323
+ // the package root is ../.. (was ..). Same target file as before the split.
324
+ const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
325
+ const parsed = JSON.parse(raw);
326
+ return { name: parsed.name ?? "fullstackgtm", version: parsed.version ?? "unknown" };
327
+ }
328
+ catch {
329
+ return { name: "fullstackgtm", version: "unknown" };
330
+ }
331
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `signals` — detect fresh buying triggers (ATS job-board scrapes + staged
3
+ * funding/company/social ingest), rank them, and persist a local signal ledger.
4
+ * READ-ONLY re: CRM: NEVER emits a PatchPlan, never calls a recording connector.
5
+ * `--save` persists only to the signal store, not a plan.
6
+ */
7
+ export declare function signalsCommand(args: string[]): Promise<void>;