fullstackgtm 0.10.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 (77) hide show
  1. package/CHANGELOG.md +381 -0
  2. package/INSTALL_FOR_AGENTS.md +87 -0
  3. package/LICENSE +202 -0
  4. package/README.md +230 -0
  5. package/dist/audit.d.ts +7 -0
  6. package/dist/audit.js +202 -0
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +6 -0
  9. package/dist/cli.d.ts +38 -0
  10. package/dist/cli.js +915 -0
  11. package/dist/config.d.ts +36 -0
  12. package/dist/config.js +85 -0
  13. package/dist/connector.d.ts +30 -0
  14. package/dist/connector.js +94 -0
  15. package/dist/connectors/hubspot.d.ts +20 -0
  16. package/dist/connectors/hubspot.js +409 -0
  17. package/dist/connectors/hubspotAuth.d.ts +42 -0
  18. package/dist/connectors/hubspotAuth.js +189 -0
  19. package/dist/connectors/salesforce.d.ts +26 -0
  20. package/dist/connectors/salesforce.js +318 -0
  21. package/dist/connectors/salesforceAuth.d.ts +44 -0
  22. package/dist/connectors/salesforceAuth.js +120 -0
  23. package/dist/connectors/stripe.d.ts +27 -0
  24. package/dist/connectors/stripe.js +176 -0
  25. package/dist/credentials.d.ts +75 -0
  26. package/dist/credentials.js +197 -0
  27. package/dist/demo.d.ts +20 -0
  28. package/dist/demo.js +169 -0
  29. package/dist/diff.d.ts +46 -0
  30. package/dist/diff.js +107 -0
  31. package/dist/format.d.ts +3 -0
  32. package/dist/format.js +109 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.js +17 -0
  35. package/dist/mappings.d.ts +8 -0
  36. package/dist/mappings.js +123 -0
  37. package/dist/mcp-bin.d.ts +2 -0
  38. package/dist/mcp-bin.js +33 -0
  39. package/dist/mcp.d.ts +1 -0
  40. package/dist/mcp.js +140 -0
  41. package/dist/merge.d.ts +48 -0
  42. package/dist/merge.js +145 -0
  43. package/dist/planStore.d.ts +31 -0
  44. package/dist/planStore.js +116 -0
  45. package/dist/rules.d.ts +24 -0
  46. package/dist/rules.js +512 -0
  47. package/dist/sampleData.d.ts +2 -0
  48. package/dist/sampleData.js +115 -0
  49. package/dist/types.d.ts +294 -0
  50. package/dist/types.js +8 -0
  51. package/docs/api.md +72 -0
  52. package/docs/roadmap-to-1.0.md +121 -0
  53. package/llms.txt +25 -0
  54. package/package.json +76 -0
  55. package/src/audit.ts +242 -0
  56. package/src/bin.ts +7 -0
  57. package/src/cli.ts +1042 -0
  58. package/src/config.ts +113 -0
  59. package/src/connector.ts +140 -0
  60. package/src/connectors/hubspot.ts +528 -0
  61. package/src/connectors/hubspotAuth.ts +246 -0
  62. package/src/connectors/salesforce.ts +420 -0
  63. package/src/connectors/salesforceAuth.ts +167 -0
  64. package/src/connectors/stripe.ts +215 -0
  65. package/src/credentials.ts +282 -0
  66. package/src/demo.ts +200 -0
  67. package/src/diff.ts +158 -0
  68. package/src/format.ts +162 -0
  69. package/src/index.ts +129 -0
  70. package/src/mappings.ts +157 -0
  71. package/src/mcp-bin.ts +32 -0
  72. package/src/mcp.ts +185 -0
  73. package/src/merge.ts +235 -0
  74. package/src/planStore.ts +155 -0
  75. package/src/rules.ts +539 -0
  76. package/src/sampleData.ts +117 -0
  77. package/src/types.ts +372 -0
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # fullstackgtm
2
+
3
+ **Plan/apply for your GTM stack.** An open-source framework for managing disparate go-to-market data spread across third-party systems: a canonical CRM/GTM data model, deterministic hygiene audits, reviewable dry-run patch plans, and approval-gated write-back to providers.
4
+
5
+ Think `terraform plan` for your CRM: agents and scripts may *read* everything, but every proposed change becomes a typed patch operation — object, field, before, after, reason, risk — that a human approves before any provider write happens.
6
+
7
+ Licensed under [Apache-2.0](./LICENSE). The boundary is deliberate and stable: the framework, CLI, and MCP server are open source; the hosted Full Stack GTM application (dashboard, sync backend, broker service, team workflows) is a separate, proprietary product built on top of this package. Features never move from open to closed. See [CONTRIBUTING.md](./CONTRIBUTING.md) for how development and mirroring work.
8
+
9
+ **Status: beta (0.x).** The surfaces in [docs/api.md](./docs/api.md) — the canonical model, rule interface, plan/apply contract, connector contract, merge/diff, config, CLI, and MCP tools — are settling but may still break in minor releases until 1.0; the path there is [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md). The safety invariants (read-only audits, approval-gated writes, placeholder refusal) are not beta and do not change. Connectors: HubSpot (read/write), Salesforce (read/write), Stripe (read-only billing).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install fullstackgtm # library + CLI in a project
15
+ npx fullstackgtm audit --demo # or zero-install via npx
16
+ npm install github:fullstackgtm/core # or straight from this repo (project-local)
17
+ npx github:fullstackgtm/core audit --demo # zero-install from the repo
18
+ ```
19
+
20
+ (Global `npm install -g` from a git URL is unreliable on npm 11 — it symlinks into npm's temp cache. Use the registry for global installs, or the project-local/npx forms above.)
21
+
22
+ Requires Node 20+. The core has zero runtime dependencies; only the MCP server entrypoint uses the optional peers `@modelcontextprotocol/sdk` and `zod`.
23
+
24
+ ```bash
25
+ npx fullstackgtm doctor # verify the install: node version, credentials, MCP peers, next step
26
+ ```
27
+
28
+ Installing for an AI agent? Hand it [INSTALL_FOR_AGENTS.md](./INSTALL_FOR_AGENTS.md) — a deterministic install-and-verify script with expected outputs. A documentation map lives in [llms.txt](./llms.txt).
29
+
30
+ ## Five-minute loop
31
+
32
+ ```bash
33
+ # 0. No credentials? Try it on a realistic, deliberately messy demo CRM
34
+ npx fullstackgtm audit --demo
35
+
36
+ # 1. Audit your real HubSpot portal (private app token or OAuth access token)
37
+ HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm audit --provider hubspot --out plan.json
38
+
39
+ # 2. Review plan.json, then apply ONLY the operations you approve
40
+ HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm apply \
41
+ --plan plan.json --provider hubspot \
42
+ --approve op_abc123,op_def456 \
43
+ --value op_def456=2026-09-30
44
+ ```
45
+
46
+ Nothing is ever written without an explicit `--approve`. Operations whose value is a human decision (`requires_human_*` placeholders, e.g. which owner to assign) are refused unless you supply a concrete `--value` override.
47
+
48
+ ## Built for agents (and the RevOps humans they work for)
49
+
50
+ Every command is designed to compose in an agent loop — deterministic output, machine-readable everywhere, meaningful exit codes:
51
+
52
+ ```bash
53
+ # Discover what the auditor checks
54
+ fullstackgtm rules --json
55
+
56
+ # Fetch once (expensive), audit offline as many times as you like (cheap)
57
+ fullstackgtm snapshot --provider hubspot --out snap.json
58
+ fullstackgtm audit --input snap.json --json
59
+ fullstackgtm audit --input snap.json --rules stale-deal --stale-days 45 --json
60
+
61
+ # Gate a nightly CI job or agent run on hygiene: exit 2 if findings ≥ threshold
62
+ fullstackgtm audit --provider hubspot --fail-on warning
63
+ ```
64
+
65
+ - Finding and operation ids are **stable hashes** of rule + record, so two runs over the same data produce identical ids — agents can diff plans, track findings across runs, and approve operations by id without re-parsing.
66
+ - `--demo` (with `--seed`) generates a realistic mid-market CRM with injected real-world failure modes — departed owners, unlinked deals, orphan accounts, stale pipeline — so agents and CI can exercise the full snapshot → audit → apply pipeline with zero credentials.
67
+ - Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
68
+
69
+ ## Authentication: CLI-first, browser only at the consent moment
70
+
71
+ Credential resolution is a ladder — the first rung that yields a token wins:
72
+
73
+ 1. **`--token-env <NAME>`** — explicit env var for one invocation (agent sandboxes, scripts)
74
+ 2. **`HUBSPOT_ACCESS_TOKEN`** — ambient env (CI)
75
+ 3. **Stored login** — `fullstackgtm login hubspot`, kept in `~/.fullstackgtm/credentials.json` (0600; override location with `FSGTM_HOME`)
76
+ 4. **Broker pairing** — `fullstackgtm login --via <hosted url>`: the team's deployment holds the CRM credentials; the CLI holds only a revocable pairing token
77
+
78
+ ### Teams: auth once, point every CLI at the stored sync credentials
79
+
80
+ ```bash
81
+ fullstackgtm login --via https://gtm.yourco.com
82
+ # Pairing code: ABCD-2345
83
+ # Approve this CLI in your dashboard: https://gtm.yourco.com/dashboard/cli-auth?code=ABCD-2345
84
+ ```
85
+
86
+ An admin connects HubSpot **once** in the hosted dashboard (the org's OAuth tokens live encrypted in the deployment). Pairing a CLI is a device-flow handshake: the CLI prints a code, an admin or manager approves it in the dashboard, and the CLI receives a long-lived broker token (stored hashed server-side, revocable per CLI). From then on, every provider command silently exchanges the broker token for a short-lived CRM access token minted from the org's stored sync credentials — and inherits the org's field mappings. No one pastes CRM tokens; revoking a laptop is one row.
87
+
88
+ ### Individuals: no deployment needed
89
+
90
+ ```bash
91
+ # HubSpot, zero web flow: paste a private app token once (validated, then stored)
92
+ fullstackgtm login hubspot
93
+
94
+ # HubSpot, bring-your-own-app OAuth. The browser is used exactly once — the
95
+ # consent grant — captured on a 127.0.0.1 loopback (RFC 8252); the CLI
96
+ # exchanges the code itself and refreshes silently from then on.
97
+ fullstackgtm login hubspot --oauth --client-id <id> --client-secret <secret>
98
+ # (register http://localhost:8763/callback as a redirect URL on your app)
99
+
100
+ # Salesforce: native device flow — confirm a code on any device, no localhost
101
+ # server, no client secret, silent refresh. Needs a Connected App consumer key
102
+ # with device flow enabled.
103
+ fullstackgtm login salesforce --device --client-id <consumer key>
104
+ # ...or a session token directly:
105
+ fullstackgtm login salesforce --token <t> --instance-url https://yourorg.my.salesforce.com
106
+
107
+ fullstackgtm logout hubspot # or: salesforce | broker
108
+ ```
109
+
110
+ A direct `login hubspot` always wins over a broker pairing, so an operator can override the team default. HubSpot does not support the device-authorization grant or secretless public clients, which is why the bring-your-own-app OAuth path requires client credentials; they are stored locally for silent refresh, the same model as `gcloud` and `aws` CLI profiles.
111
+
112
+ ## Concepts
113
+
114
+ | Concept | What it is |
115
+ |---|---|
116
+ | **Canonical snapshot** | Provider-independent view of users, accounts, contacts, deals, activities. Records carry `identities` — `(provider, externalId)` claims — so the same real-world entity can be tracked across several systems. |
117
+ | **Audit rule** | A deterministic function `(context) => { findings, operations }`. Five built-ins cover orphan accounts, ownerless deals, unlinked deals, past close dates, and stale pipeline. Write your own in ~10 lines. |
118
+ | **Patch plan** | The dry-run output of an audit: findings plus typed patch operations with before/after values, reasons, risk levels, and approval flags. Always a proposal, never a mutation. |
119
+ | **Connector** | A provider adapter: `fetchSnapshot()` for reads, optional `applyOperation()` for writes. HubSpot and Salesforce reference connectors ship in the package; connectors never drop records they can't fully resolve — the audit flags them instead. |
120
+ | **Patch plan run** | The audit record of one apply attempt: per-operation applied/failed/skipped results. |
121
+
122
+ ## Write a custom rule
123
+
124
+ ```ts
125
+ import {
126
+ auditSnapshot, auditFindingId, builtinAuditRules, defaultPolicy,
127
+ type GtmAuditRule,
128
+ } from "fullstackgtm";
129
+
130
+ const missingAmount: GtmAuditRule = {
131
+ id: "missing-deal-amount",
132
+ title: "Deal has no amount",
133
+ description: "Amountless deals make forecast coverage meaningless.",
134
+ evaluate: ({ snapshot }) => ({
135
+ findings: snapshot.deals
136
+ .filter((deal) => !deal.amount)
137
+ .map((deal) => ({
138
+ id: auditFindingId("missing-deal-amount", deal.id),
139
+ objectType: "deal", objectId: deal.id,
140
+ ruleId: "missing-deal-amount",
141
+ title: "Deal has no amount", severity: "warning",
142
+ summary: `${deal.name} has no amount.`,
143
+ recommendation: "Set an amount or close the deal out.",
144
+ })),
145
+ operations: [],
146
+ }),
147
+ };
148
+
149
+ const plan = auditSnapshot(snapshot, defaultPolicy(), [...builtinAuditRules, missingAmount]);
150
+ ```
151
+
152
+ ## Use a connector programmatically
153
+
154
+ ```ts
155
+ import { applyPatchPlan, auditSnapshot, createHubspotConnector } from "fullstackgtm";
156
+
157
+ const hubspot = createHubspotConnector({
158
+ getAccessToken: () => process.env.HUBSPOT_ACCESS_TOKEN!,
159
+ });
160
+
161
+ const snapshot = await hubspot.fetchSnapshot();
162
+ const plan = auditSnapshot(snapshot);
163
+
164
+ // Later, after human review:
165
+ const run = await applyPatchPlan(hubspot, plan, {
166
+ approvedOperationIds: ["op_abc123"],
167
+ valueOverrides: { op_abc123: "9001" },
168
+ });
169
+ ```
170
+
171
+ Implementing a new provider means implementing one type:
172
+
173
+ ```ts
174
+ import type { GtmConnector } from "fullstackgtm";
175
+
176
+ const myConnector: GtmConnector = {
177
+ provider: "my-crm",
178
+ fetchSnapshot: async () => ({ /* canonical snapshot */ }),
179
+ applyOperation: async (operation) => ({ operationId: operation.id, status: "applied" }),
180
+ };
181
+ ```
182
+
183
+ ## MCP server
184
+
185
+ The MCP entrypoint needs the optional peer dependencies `@modelcontextprotocol/sdk` and `zod` — plain `npx fullstackgtm-mcp` won't install optional peers, so pull them in explicitly:
186
+
187
+ ```bash
188
+ # In a project
189
+ npm install fullstackgtm @modelcontextprotocol/sdk zod
190
+ HUBSPOT_ACCESS_TOKEN=pat-... npx fullstackgtm-mcp
191
+
192
+ # Zero-install
193
+ npx -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp
194
+ ```
195
+
196
+ Add it to Claude Code in one command:
197
+
198
+ ```bash
199
+ claude mcp add fullstackgtm -e HUBSPOT_ACCESS_TOKEN=pat-... -- npx -y -p fullstackgtm -p @modelcontextprotocol/sdk -p zod fullstackgtm-mcp
200
+ ```
201
+
202
+ Or configure any MCP client (Cursor, Claude Desktop, …) with:
203
+
204
+ ```json
205
+ {
206
+ "mcpServers": {
207
+ "fullstackgtm": {
208
+ "command": "npx",
209
+ "args": ["-y", "-p", "fullstackgtm", "-p", "@modelcontextprotocol/sdk", "-p", "zod", "fullstackgtm-mcp"],
210
+ "env": { "HUBSPOT_ACCESS_TOKEN": "pat-..." }
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ Exposes `fullstackgtm_audit` (read-only; sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_rules` (rule discovery), and `fullstackgtm_apply` (requires explicit `approvedOperationIds`) over stdio. Tokens stored via `fullstackgtm login` are picked up automatically — the env var is only needed when no stored login exists.
217
+
218
+ ## Safety model
219
+
220
+ 1. Reads are safe by default; audits never mutate anything.
221
+ 2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
222
+ 3. `applyPatchPlan` enforces the contract for all connectors: only explicitly approved operation ids are written, placeholders require concrete override values, and every attempt produces a per-operation result record.
223
+
224
+ ## Development
225
+
226
+ ```bash
227
+ npm run build # compiles src/ to dist/ (tsc, type declarations included)
228
+ ```
229
+
230
+ Tests live in the repository root `tests/` directory and run with `npm test`.
@@ -0,0 +1,7 @@
1
+ import type { CanonicalGtmSnapshot, GtmAuditRule, GtmPolicy, PatchPlan } from "./types.ts";
2
+ export declare function defaultPolicy(today?: string): GtmPolicy;
3
+ /**
4
+ * Run every rule over the snapshot and collect the results into a single
5
+ * dry-run patch plan. Pass custom rules to extend or replace the built-ins.
6
+ */
7
+ export declare function auditSnapshot(snapshot: CanonicalGtmSnapshot, policy?: GtmPolicy, rules?: GtmAuditRule[]): PatchPlan;
package/dist/audit.js ADDED
@@ -0,0 +1,202 @@
1
+ import { buildSnapshotIndex, builtinAuditRules, stableHash } from "./rules.js";
2
+ const DEFAULT_POLICY = {
3
+ staleDealDays: 30,
4
+ requireDealOwner: true,
5
+ requireAccountForDeal: true,
6
+ };
7
+ export function defaultPolicy(today = new Date().toISOString().slice(0, 10)) {
8
+ return {
9
+ ...DEFAULT_POLICY,
10
+ today,
11
+ };
12
+ }
13
+ /**
14
+ * Run every rule over the snapshot and collect the results into a single
15
+ * dry-run patch plan. Pass custom rules to extend or replace the built-ins.
16
+ */
17
+ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builtinAuditRules) {
18
+ const context = { snapshot, policy, index: buildSnapshotIndex(snapshot) };
19
+ const findings = [];
20
+ const operations = [];
21
+ for (const rule of rules) {
22
+ const result = rule.evaluate(context);
23
+ findings.push(...result.findings);
24
+ operations.push(...result.operations);
25
+ }
26
+ const evidence = buildEvidence(snapshot, findings, policy.today);
27
+ const pipelineFindings = buildPipelineFindings(findings, operations, evidence, policy.today);
28
+ linkOperationContext(operations, findings, evidence);
29
+ return {
30
+ id: `patch_plan_${stableHash(`${snapshot.provider}:${snapshot.generatedAt}:${findings.length}:${operations.length}`)}`,
31
+ title: "GTM hygiene audit patch plan",
32
+ createdAt: new Date().toISOString(),
33
+ status: operations.length > 0 ? "needs_approval" : "draft",
34
+ dryRun: true,
35
+ summary: `${findings.length} findings and ${operations.length} proposed dry-run operations.`,
36
+ findings,
37
+ pipelineFindings,
38
+ evidence,
39
+ operations,
40
+ };
41
+ }
42
+ function daysBetween(start, end) {
43
+ const startMs = Date.parse(start);
44
+ const endMs = Date.parse(end);
45
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs))
46
+ return 0;
47
+ return Math.floor((endMs - startMs) / 86_400_000);
48
+ }
49
+ function buildEvidence(snapshot, findings, today) {
50
+ const recordsByKey = buildRecordIndex(snapshot);
51
+ return findings.map((finding) => {
52
+ const source = recordsByKey.get(`${finding.objectType}:${finding.objectId}`);
53
+ const provider = source && "provider" in source && typeof source.provider === "string"
54
+ ? source.provider
55
+ : snapshot.provider;
56
+ const lastSyncAt = source && "lastSyncAt" in source && typeof source.lastSyncAt === "string"
57
+ ? source.lastSyncAt
58
+ : undefined;
59
+ const lastActivityAt = source && "lastActivityAt" in source && typeof source.lastActivityAt === "string"
60
+ ? source.lastActivityAt
61
+ : undefined;
62
+ const sourceObjectId = source && "crmId" in source && typeof source.crmId === "string"
63
+ ? source.crmId
64
+ : finding.objectId;
65
+ return {
66
+ id: evidenceId(finding.ruleId, finding.objectId),
67
+ sourceSystem: sourceSystem(provider),
68
+ sourceObjectType: finding.objectType,
69
+ sourceObjectId,
70
+ objectType: finding.objectType,
71
+ objectId: finding.objectId,
72
+ title: finding.title,
73
+ text: evidenceText(finding, source),
74
+ observedAt: lastActivityAt ?? lastSyncAt,
75
+ capturedAt: snapshot.generatedAt,
76
+ freshness: freshness(lastSyncAt ?? lastActivityAt, today),
77
+ metadata: {
78
+ ruleId: finding.ruleId,
79
+ generatedAt: snapshot.generatedAt,
80
+ },
81
+ };
82
+ });
83
+ }
84
+ function buildPipelineFindings(findings, operations, evidence, today) {
85
+ const operationsById = new Map(operations.map((operation) => [operation.id, operation]));
86
+ const evidenceById = new Map(evidence.map((item) => [item.id, item]));
87
+ return findings
88
+ .map((finding) => {
89
+ const type = finding.type ?? findingType(finding.ruleId);
90
+ if (!type)
91
+ return null;
92
+ const operation = operationsById.get(operationIdFromFindingId(finding.id));
93
+ const evidenceItem = evidenceById.get(evidenceIdFromFindingId(finding.id));
94
+ const evidenceIds = evidenceItem ? [evidenceItem.id] : [];
95
+ return {
96
+ id: finding.id,
97
+ type,
98
+ objectType: finding.objectType,
99
+ objectId: finding.objectId,
100
+ severity: finding.severity,
101
+ status: operation ? "planned" : "open",
102
+ title: finding.title,
103
+ summary: finding.summary,
104
+ recommendation: finding.recommendation,
105
+ evidenceIds,
106
+ currentCrmValue: finding.currentCrmValue ?? operation?.beforeValue,
107
+ proposedValue: finding.proposedValue ?? operation?.afterValue,
108
+ freshness: finding.freshness ?? evidenceItem?.freshness ?? freshness(undefined, today),
109
+ patchEligibility: {
110
+ eligible: Boolean(operation),
111
+ operation: operation?.operation ?? "none",
112
+ field: operation?.field,
113
+ reason: operation
114
+ ? "A typed patch operation can be reviewed before writeback."
115
+ : "No supported patch operation exists yet.",
116
+ approvalRequired: operation?.approvalRequired ?? true,
117
+ },
118
+ };
119
+ })
120
+ .filter((finding) => finding !== null);
121
+ }
122
+ function linkOperationContext(operations, findings, evidence) {
123
+ const findingIdsByOperationId = new Map();
124
+ for (const finding of findings) {
125
+ const operationIdForFinding = operationIdFromFindingId(finding.id);
126
+ const ids = findingIdsByOperationId.get(operationIdForFinding) ?? [];
127
+ ids.push(finding.id);
128
+ findingIdsByOperationId.set(operationIdForFinding, ids);
129
+ }
130
+ const evidenceIdSet = new Set(evidence.map((item) => item.id));
131
+ for (const operation of operations) {
132
+ operation.findingIds = findingIdsByOperationId.get(operation.id) ?? [];
133
+ operation.evidenceIds = operation.findingIds
134
+ .map((findingId) => evidenceIdFromFindingId(findingId))
135
+ .filter((id) => evidenceIdSet.has(id));
136
+ operation.verification = {
137
+ status: "not_started",
138
+ expectedValue: operation.afterValue,
139
+ auditText: "Dry-run audit only; verification starts after an approved provider/local write.",
140
+ };
141
+ }
142
+ }
143
+ function operationIdFromFindingId(findingId) {
144
+ return findingId.replace(/^finding_/, "op_");
145
+ }
146
+ function evidenceIdFromFindingId(findingId) {
147
+ return findingId.replace(/^finding_/, "ev_");
148
+ }
149
+ function buildRecordIndex(snapshot) {
150
+ const recordsByKey = new Map();
151
+ for (const row of snapshot.accounts)
152
+ recordsByKey.set(`account:${row.id}`, row);
153
+ for (const row of snapshot.contacts)
154
+ recordsByKey.set(`contact:${row.id}`, row);
155
+ for (const row of snapshot.deals)
156
+ recordsByKey.set(`deal:${row.id}`, row);
157
+ for (const row of snapshot.users)
158
+ recordsByKey.set(`user:${row.id}`, row);
159
+ for (const row of snapshot.activities)
160
+ recordsByKey.set(`activity:${row.id}`, row);
161
+ return recordsByKey;
162
+ }
163
+ function evidenceText(finding, source) {
164
+ const name = source && typeof source === "object" && "name" in source ? String(source.name) : finding.objectId;
165
+ return `${finding.ruleId}: ${name}. ${finding.summary}`;
166
+ }
167
+ function sourceSystem(value) {
168
+ if (value === "salesforce" ||
169
+ value === "hubspot" ||
170
+ value === "gong" ||
171
+ value === "chorus" ||
172
+ value === "fathom" ||
173
+ value === "manual" ||
174
+ value === "csv" ||
175
+ value === "mock") {
176
+ return value;
177
+ }
178
+ return "unknown";
179
+ }
180
+ function freshness(sourceUpdatedAt, today) {
181
+ const ageDays = sourceUpdatedAt ? daysBetween(sourceUpdatedAt, today) : undefined;
182
+ return {
183
+ state: ageDays === undefined ? "unknown" : ageDays > 30 ? "stale" : "fresh",
184
+ checkedAt: today,
185
+ sourceUpdatedAt,
186
+ ageDays,
187
+ };
188
+ }
189
+ function findingType(ruleId) {
190
+ if (ruleId === "missing-deal-owner")
191
+ return "deal_missing_owner";
192
+ if (ruleId === "missing-deal-account")
193
+ return "deal_missing_account";
194
+ if (ruleId === "past-close-date")
195
+ return "deal_past_close_date";
196
+ if (ruleId === "stale-deal")
197
+ return "deal_stale_activity";
198
+ return undefined;
199
+ }
200
+ function evidenceId(ruleId, objectId) {
201
+ return `ev_${stableHash(`${ruleId}:${objectId}`)}`;
202
+ }
package/dist/bin.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cli.js";
3
+ runCli(process.argv.slice(2)).catch((error) => {
4
+ console.error(error instanceof Error ? error.message : String(error));
5
+ process.exitCode = 1;
6
+ });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ type ProviderDoctorStatus = {
2
+ source: "env" | "stored" | "broker" | "none";
3
+ detail: string;
4
+ };
5
+ export declare function doctorReport(env?: Record<string, string | undefined>): {
6
+ package: {
7
+ name: string;
8
+ version: string;
9
+ };
10
+ node: {
11
+ version: string;
12
+ ok: boolean;
13
+ required: string;
14
+ };
15
+ credentialStore: {
16
+ path: string;
17
+ exists: boolean;
18
+ };
19
+ config: {
20
+ path: string;
21
+ exists: boolean;
22
+ };
23
+ providers: Record<string, ProviderDoctorStatus>;
24
+ broker: {
25
+ paired: boolean;
26
+ baseUrl: string;
27
+ } | {
28
+ paired: boolean;
29
+ baseUrl?: undefined;
30
+ };
31
+ mcp: {
32
+ peersInstalled: boolean;
33
+ missing: string[];
34
+ };
35
+ nextSteps: string[];
36
+ };
37
+ export declare function runCli(argv: string[]): Promise<void>;
38
+ export {};