fullstackgtm 0.44.0 → 0.46.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 (149) hide show
  1. package/CHANGELOG.md +293 -1
  2. package/INSTALL_FOR_AGENTS.md +13 -11
  3. package/README.md +45 -24
  4. package/dist/audit.d.ts +3 -1
  5. package/dist/audit.js +29 -2
  6. package/dist/backfill.d.ts +86 -0
  7. package/dist/backfill.js +251 -0
  8. package/dist/bin.js +4 -1
  9. package/dist/cli/audit.d.ts +15 -0
  10. package/dist/cli/audit.js +392 -0
  11. package/dist/cli/auth.d.ts +82 -0
  12. package/dist/cli/auth.js +607 -0
  13. package/dist/cli/backfill.d.ts +1 -0
  14. package/dist/cli/backfill.js +125 -0
  15. package/dist/cli/backfillRuns.d.ts +1 -0
  16. package/dist/cli/backfillRuns.js +187 -0
  17. package/dist/cli/call.d.ts +1 -0
  18. package/dist/cli/call.js +387 -0
  19. package/dist/cli/capabilities.d.ts +30 -0
  20. package/dist/cli/capabilities.js +197 -0
  21. package/dist/cli/draft.d.ts +7 -0
  22. package/dist/cli/draft.js +87 -0
  23. package/dist/cli/enrich.d.ts +8 -0
  24. package/dist/cli/enrich.js +806 -0
  25. package/dist/cli/fix.d.ts +33 -0
  26. package/dist/cli/fix.js +346 -0
  27. package/dist/cli/help.d.ts +25 -0
  28. package/dist/cli/help.js +646 -0
  29. package/dist/cli/icp.d.ts +7 -0
  30. package/dist/cli/icp.js +229 -0
  31. package/dist/cli/init.d.ts +7 -0
  32. package/dist/cli/init.js +58 -0
  33. package/dist/cli/market.d.ts +1 -0
  34. package/dist/cli/market.js +391 -0
  35. package/dist/cli/plans.d.ts +5 -0
  36. package/dist/cli/plans.js +456 -0
  37. package/dist/cli/schedule.d.ts +8 -0
  38. package/dist/cli/schedule.js +479 -0
  39. package/dist/cli/shared.d.ts +71 -0
  40. package/dist/cli/shared.js +342 -0
  41. package/dist/cli/signals.d.ts +7 -0
  42. package/dist/cli/signals.js +412 -0
  43. package/dist/cli/suggest.d.ts +49 -0
  44. package/dist/cli/suggest.js +135 -0
  45. package/dist/cli/tam.d.ts +9 -0
  46. package/dist/cli/tam.js +387 -0
  47. package/dist/cli/ui.d.ts +142 -0
  48. package/dist/cli/ui.js +427 -0
  49. package/dist/cli.d.ts +1 -57
  50. package/dist/cli.js +123 -5157
  51. package/dist/connector.d.ts +23 -0
  52. package/dist/connector.js +47 -0
  53. package/dist/connectors/hubspot.d.ts +10 -1
  54. package/dist/connectors/hubspot.js +244 -10
  55. package/dist/connectors/hubspotAuth.js +5 -1
  56. package/dist/connectors/linkedin.js +14 -14
  57. package/dist/connectors/salesforce.d.ts +10 -1
  58. package/dist/connectors/salesforce.js +39 -6
  59. package/dist/connectors/signalSources.js +7 -59
  60. package/dist/connectors/stripe.d.ts +37 -0
  61. package/dist/connectors/stripe.js +103 -31
  62. package/dist/enrich.d.ts +7 -0
  63. package/dist/enrich.js +7 -0
  64. package/dist/health.d.ts +11 -69
  65. package/dist/health.js +4 -134
  66. package/dist/healthScore.d.ts +71 -0
  67. package/dist/healthScore.js +143 -0
  68. package/dist/icp.d.ts +15 -11
  69. package/dist/icp.js +19 -36
  70. package/dist/index.d.ts +3 -1
  71. package/dist/index.js +3 -1
  72. package/dist/judge.d.ts +2 -0
  73. package/dist/judge.js +6 -0
  74. package/dist/llm.d.ts +29 -0
  75. package/dist/llm.js +206 -0
  76. package/dist/market.d.ts +39 -1
  77. package/dist/market.js +116 -44
  78. package/dist/marketClassify.d.ts +11 -1
  79. package/dist/marketClassify.js +17 -2
  80. package/dist/marketTaxonomy.d.ts +6 -1
  81. package/dist/marketTaxonomy.js +4 -2
  82. package/dist/mcp-bin.js +31 -2
  83. package/dist/mcp.js +372 -166
  84. package/dist/progress.d.ts +96 -0
  85. package/dist/progress.js +142 -0
  86. package/dist/runReport.d.ts +24 -0
  87. package/dist/runReport.js +139 -4
  88. package/dist/schedule.d.ts +80 -2
  89. package/dist/schedule.js +254 -1
  90. package/dist/spoolFiles.d.ts +44 -0
  91. package/dist/spoolFiles.js +114 -0
  92. package/dist/types.d.ts +29 -1
  93. package/docs/api.md +75 -8
  94. package/docs/architecture.md +2 -0
  95. package/docs/linkedin-connector-spec.md +1 -1
  96. package/docs/signal-spool-format.md +13 -0
  97. package/llms.txt +18 -9
  98. package/package.json +10 -3
  99. package/skills/fullstackgtm/SKILL.md +2 -1
  100. package/src/audit.ts +27 -1
  101. package/src/backfill.ts +340 -0
  102. package/src/bin.ts +5 -1
  103. package/src/cli/audit.ts +447 -0
  104. package/src/cli/auth.ts +669 -0
  105. package/src/cli/backfill.ts +156 -0
  106. package/src/cli/backfillRuns.ts +198 -0
  107. package/src/cli/call.ts +414 -0
  108. package/src/cli/capabilities.ts +215 -0
  109. package/src/cli/draft.ts +101 -0
  110. package/src/cli/enrich.ts +908 -0
  111. package/src/cli/fix.ts +373 -0
  112. package/src/cli/help.ts +702 -0
  113. package/src/cli/icp.ts +265 -0
  114. package/src/cli/init.ts +65 -0
  115. package/src/cli/market.ts +423 -0
  116. package/src/cli/plans.ts +524 -0
  117. package/src/cli/schedule.ts +526 -0
  118. package/src/cli/shared.ts +392 -0
  119. package/src/cli/signals.ts +434 -0
  120. package/src/cli/suggest.ts +151 -0
  121. package/src/cli/tam.ts +435 -0
  122. package/src/cli/ui.ts +497 -0
  123. package/src/cli.ts +122 -5855
  124. package/src/connector.ts +66 -0
  125. package/src/connectors/hubspot.ts +273 -9
  126. package/src/connectors/hubspotAuth.ts +5 -1
  127. package/src/connectors/linkedin.ts +14 -14
  128. package/src/connectors/salesforce.ts +51 -3
  129. package/src/connectors/signalSources.ts +7 -56
  130. package/src/connectors/stripe.ts +154 -34
  131. package/src/enrich.ts +13 -0
  132. package/src/health.ts +14 -213
  133. package/src/healthScore.ts +223 -0
  134. package/src/icp.ts +19 -35
  135. package/src/index.ts +28 -1
  136. package/src/judge.ts +7 -0
  137. package/src/llm.ts +239 -0
  138. package/src/market.ts +157 -44
  139. package/src/marketClassify.ts +26 -2
  140. package/src/marketTaxonomy.ts +10 -2
  141. package/src/mcp-bin.ts +34 -2
  142. package/src/mcp.ts +270 -63
  143. package/src/progress.ts +197 -0
  144. package/src/runReport.ts +159 -4
  145. package/src/schedule.ts +310 -3
  146. package/src/spoolFiles.ts +116 -0
  147. package/src/types.ts +32 -2
  148. package/docs/dx-punch-list.md +0 -87
  149. package/docs/roadmap-to-1.0.md +0 -211
@@ -0,0 +1,392 @@
1
+ // Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { auditSnapshot, defaultPolicy } from "../audit.js";
5
+ import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
6
+ import { getCredential, resolveHubspotConnection } from "../credentials.js";
7
+ import { patchPlanToMarkdown } from "../format.js";
8
+ import { appendHealthEntry, computeHealth, healthToMarkdown, readHealthTimeline, saveWorkspaceSnapshot, summarizeHealth, activeWorkspaceProfile } from "../health.js";
9
+ import { createFilePlanStore } from "../planStore.js";
10
+ import { auditReportToHtml, auditReportToMarkdown } from "../report.js";
11
+ import { reportCounts, reportCrm, reportFindings } from "../runReport.js";
12
+ import { connectorFor, numericOption, option, readSnapshot, saveRequested, selectedRules } from "./shared.js";
13
+ import { colorEnabled, createChecklist, formatCount, paint, scoreColor, sparkline, stylizePlanMarkdown, table } from "./ui.js";
14
+ const SEVERITY_RANK = {
15
+ info: 0,
16
+ warning: 1,
17
+ critical: 2,
18
+ };
19
+ function failOnThreshold(args) {
20
+ const value = option(args, "--fail-on");
21
+ if (!value)
22
+ return null;
23
+ if (value !== "info" && value !== "warning" && value !== "critical") {
24
+ throw new Error("--fail-on must be one of: info, warning, critical");
25
+ }
26
+ return value;
27
+ }
28
+ export async function snapshotCommand(args) {
29
+ const since = option(args, "--since");
30
+ let snapshot;
31
+ if (since) {
32
+ const provider = option(args, "--provider");
33
+ if (!provider)
34
+ throw new Error("--since requires --provider <name>");
35
+ const connector = await connectorFor(provider, args);
36
+ if (!connector.fetchChanges) {
37
+ throw new Error(`The ${provider} connector does not support incremental fetch.`);
38
+ }
39
+ snapshot = await connector.fetchChanges(since);
40
+ }
41
+ else {
42
+ snapshot = await readSnapshot(args);
43
+ }
44
+ const serialized = `${JSON.stringify(snapshot, null, 2)}\n`;
45
+ const archive = option(args, "--archive");
46
+ if (archive) {
47
+ const dir = resolve(process.cwd(), archive);
48
+ const { mkdirSync } = await import("node:fs");
49
+ mkdirSync(dir, { recursive: true });
50
+ const fileName = `${snapshot.provider}-${snapshot.generatedAt.replace(/[:.]/g, "-")}.json`;
51
+ const path = resolve(dir, fileName);
52
+ writeFileSync(path, serialized);
53
+ console.log(`Archived snapshot to ${path}`);
54
+ return;
55
+ }
56
+ const out = option(args, "--out");
57
+ if (out) {
58
+ writeFileSync(resolve(process.cwd(), out), serialized);
59
+ console.log(`Wrote snapshot (${snapshot.users.length} users, ${snapshot.accounts.length} accounts, ` +
60
+ `${snapshot.contacts.length} contacts, ${snapshot.deals.length} deals, ` +
61
+ `${snapshot.activities.length} activities) to ${out}`);
62
+ }
63
+ else {
64
+ console.log(serialized.trimEnd());
65
+ }
66
+ }
67
+ // #2 (dx-punch-list): every read verb points forward. `audit` is the keystone —
68
+ // `audit --demo` is the most-run first command and used to dead-end on a blank
69
+ // line. Context-aware guidance mirrors doctor's "Next step" and fix's chaining,
70
+ // stepping the user along Detect → Govern → apply. Printed to stderr so stdout
71
+ // stays clean for pipes/--out; suppressed under --json (machine/agent context).
72
+ function auditNextStep(args, plan) {
73
+ const provider = option(args, "--provider");
74
+ const usingLiveData = Boolean(provider) || Boolean(option(args, "--input"));
75
+ const saved = saveRequested(args);
76
+ const count = plan.findings.length;
77
+ if (count === 0) {
78
+ return [
79
+ "✓ No findings — this snapshot is clean. Nothing to apply.",
80
+ " Keep it clean: gate new records with `fullstackgtm resolve`,",
81
+ " and schedule a recurring check with `fullstackgtm schedule add ...`.",
82
+ ].join("\n");
83
+ }
84
+ const head = `${count} finding${count === 1 ? "" : "s"} — a dry-run plan. Nothing was written.`;
85
+ if (!usingLiveData) {
86
+ return [
87
+ head,
88
+ "Next:",
89
+ " • Client-ready writeup: fullstackgtm report --demo",
90
+ " • Run on your CRM: fullstackgtm login hubspot → fullstackgtm audit --provider hubspot --save",
91
+ ].join("\n");
92
+ }
93
+ if (!saved) {
94
+ return [
95
+ head,
96
+ "Next: persist it with --save, then suggest → approve → apply:",
97
+ ` fullstackgtm audit --provider ${provider ?? "<name>"} --save`,
98
+ ].join("\n");
99
+ }
100
+ // --save already confirmed the plan id above; chain the governed spine.
101
+ const providerFlag = provider ? ` --provider ${provider}` : "";
102
+ return [
103
+ head,
104
+ "Next: derive values, approve the safe ones, apply:",
105
+ ` fullstackgtm suggest --plan-id ${plan.id}${providerFlag} --out suggestions.json`,
106
+ ` fullstackgtm plans approve ${plan.id} --values-from suggestions.json`,
107
+ ` fullstackgtm apply --plan-id ${plan.id}${providerFlag}`,
108
+ ].join("\n");
109
+ }
110
+ /**
111
+ * Resolve the live HubSpot account's record-URL base (e.g.
112
+ * `https://app-na2.hubspot.com/contacts/<portalId>/record`) and report it on the
113
+ * run so the dashboard can deep-link findings. Best-effort: any failure is
114
+ * swallowed — deep-links are a nicety, never a reason to fail the audit.
115
+ */
116
+ async function reportHubspotDeepLinkBase() {
117
+ try {
118
+ const connection = await resolveHubspotConnection();
119
+ if (!connection?.accessToken)
120
+ return;
121
+ const response = await fetch("https://api.hubapi.com/account-info/v3/details", {
122
+ headers: { Authorization: `Bearer ${connection.accessToken}` },
123
+ });
124
+ if (!response.ok)
125
+ return;
126
+ const info = (await response.json());
127
+ if (!info.portalId || !info.uiDomain)
128
+ return;
129
+ reportCrm({
130
+ provider: "hubspot",
131
+ recordUrlBase: `https://${info.uiDomain}/contacts/${info.portalId}/record`,
132
+ });
133
+ }
134
+ catch {
135
+ // deep-link base is best-effort
136
+ }
137
+ }
138
+ /**
139
+ * When paired (a broker credential exists), hand the local HubSpot token to the
140
+ * hosted deployment so its Integrations page shows HubSpot connected and the
141
+ * backend can sync on its own. The token lands in the same place a manual
142
+ * in-app connect would (encrypted on the org's integration). Best-effort:
143
+ * never blocks or fails the audit.
144
+ */
145
+ async function registerHubspotWithBroker() {
146
+ try {
147
+ const broker = getCredential("broker");
148
+ if (!broker?.baseUrl || !broker.accessToken)
149
+ return; // only when paired
150
+ const connection = await resolveHubspotConnection();
151
+ if (!connection?.accessToken)
152
+ return;
153
+ const response = await fetch(`${broker.baseUrl.replace(/\/+$/, "")}/api/cli/integration`, {
154
+ method: "POST",
155
+ headers: {
156
+ Authorization: `Bearer ${broker.accessToken}`,
157
+ "Content-Type": "application/json",
158
+ },
159
+ body: JSON.stringify({ provider: "hubspot", token: connection.accessToken }),
160
+ signal: AbortSignal.timeout(8000),
161
+ });
162
+ if (response.ok) {
163
+ console.error(`Registered HubSpot with ${broker.baseUrl} — it now shows on the dashboard's Integrations page.`);
164
+ }
165
+ }
166
+ catch {
167
+ // registering the connection is best-effort; never affect the audit
168
+ }
169
+ }
170
+ export async function audit(args) {
171
+ const threshold = failOnThreshold(args);
172
+ const loaded = loadConfig(option(args, "--config") ?? undefined);
173
+ const rules = selectedRules(args, await resolveConfiguredRules(loaded));
174
+ const snapshot = await readSnapshot(args);
175
+ const policy = mergePolicy(defaultPolicy(), loaded?.config);
176
+ const today = option(args, "--today");
177
+ if (today)
178
+ policy.today = today;
179
+ const staleDealDays = numericOption(args, "--stale-days");
180
+ if (staleDealDays !== undefined)
181
+ policy.staleDealDays = staleDealDays;
182
+ // Interactive terminals watch the rule registry fill in (○ → spinner → ✓,
183
+ // with per-rule finding counts) on stderr; the board erases itself before
184
+ // the plan renders. Piped/CI/agent runs get an inert no-op.
185
+ const board = createChecklist(rules.map((rule) => ({ id: rule.id, label: rule.id })));
186
+ let plan;
187
+ try {
188
+ plan = auditSnapshot(snapshot, policy, rules, board.active
189
+ ? (ruleId, phase, count) => board.update(ruleId, phase === "start" ? "running" : "ok", phase === "done" ? `${count} finding${count === 1 ? "" : "s"}` : undefined)
190
+ : undefined);
191
+ }
192
+ finally {
193
+ board.done();
194
+ }
195
+ reportCounts({
196
+ findings: plan.findings.length,
197
+ critical: plan.findings.filter((f) => f.severity === "critical").length,
198
+ warning: plan.findings.filter((f) => f.severity === "warning").length,
199
+ });
200
+ // Row-level detail for the hosted dashboard: IDs + issue type only (no field
201
+ // values). Enrich each finding with its proposed op's field/operation when one
202
+ // is linked, so the dashboard can show "what" and "where" without "the value".
203
+ const opByFinding = new Map();
204
+ for (const op of plan.operations ?? []) {
205
+ for (const findingId of op.findingIds ?? []) {
206
+ if (!opByFinding.has(findingId)) {
207
+ opByFinding.set(findingId, { field: op.field, operation: op.operation });
208
+ }
209
+ }
210
+ }
211
+ reportFindings(plan.findings.map((f) => ({
212
+ objectType: f.objectType,
213
+ objectId: f.objectId,
214
+ severity: f.severity,
215
+ ruleId: f.ruleId,
216
+ ...opByFinding.get(f.id),
217
+ })));
218
+ // When auditing a live HubSpot, emit the account's record-URL base so the
219
+ // hosted dashboard can deep-link each finding to the real CRM record. Best
220
+ // effort and HubSpot-only for now; never blocks or fails the audit.
221
+ if (option(args, "--provider") === "hubspot") {
222
+ await reportHubspotDeepLinkBase();
223
+ await registerHubspotWithBroker();
224
+ }
225
+ const out = option(args, "--out");
226
+ if (out) {
227
+ writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(plan, null, 2)}\n`);
228
+ }
229
+ if (saveRequested(args)) {
230
+ await createFilePlanStore().save(plan);
231
+ // Engagement workspace: stamp the per-profile health timeline + snapshot so
232
+ // the org accrues a continuous record from the verb people already run.
233
+ // Honor --today (audit is deterministic under it); fall back to wall-clock.
234
+ const health = computeHealth(plan, snapshot, today ?? new Date().toISOString());
235
+ appendHealthEntry(health);
236
+ saveWorkspaceSnapshot(plan.id, snapshot);
237
+ console.error(`Saved plan ${plan.id}. Health ${health.score}/100 recorded (\`fullstackgtm health\`). ` +
238
+ `Review with \`fullstackgtm plans show ${plan.id}\`, approve with \`fullstackgtm plans approve ${plan.id} --operations <ids|all>\`.`);
239
+ }
240
+ if (args.includes("--json")) {
241
+ console.log(JSON.stringify(plan, null, 2));
242
+ }
243
+ else {
244
+ // Default to the summary view (rule table + counts); the full per-operation
245
+ // dump is opt-in via --full or, for a deliverable, `report`. (dx-punch-list #3)
246
+ // Interactive terminals get the styled rendering; piped output is unchanged.
247
+ console.log(stylizePlanMarkdown(patchPlanToMarkdown(plan, { summary: !args.includes("--full") }), paint(colorEnabled(process.stdout))));
248
+ console.error(`\n${auditNextStep(args, plan)}`);
249
+ }
250
+ if (threshold &&
251
+ plan.findings.some((finding) => SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold])) {
252
+ process.exitCode = 2;
253
+ }
254
+ }
255
+ /**
256
+ * Roll up the active profile's health timeline (accrued by `audit --save`):
257
+ * current deterministic score, change since the last audit, and per-rule
258
+ * deltas. Read-only — it only reads `health.jsonl`, never re-audits.
259
+ */
260
+ export function healthCommand(args) {
261
+ const profile = activeWorkspaceProfile();
262
+ const rollup = summarizeHealth(readHealthTimeline(), profile);
263
+ if (!rollup) {
264
+ if (args.includes("--json")) {
265
+ console.log(JSON.stringify({ profile, auditCount: 0 }, null, 2));
266
+ }
267
+ else {
268
+ console.log(`No audits recorded yet for profile "${profile}".\n` +
269
+ "Start the timeline: `fullstackgtm audit --provider <name> --save`" +
270
+ (profile === "default" ? "" : ` --profile ${profile}`) +
271
+ ".");
272
+ }
273
+ return;
274
+ }
275
+ if (args.includes("--json")) {
276
+ console.log(JSON.stringify(rollup, null, 2));
277
+ return;
278
+ }
279
+ const p = paint(colorEnabled(process.stdout));
280
+ // The markdown rollup is a deliverable — piped/redirected output stays
281
+ // byte-identical to the pre-TUI CLI. Interactive terminals get the styled view.
282
+ console.log(p.enabled ? renderHealthRich(rollup, p) : healthToMarkdown(rollup));
283
+ }
284
+ /** Interactive-terminal rendering of the health rollup: the markdown's facts, styled. */
285
+ function renderHealthRich(rollup, p) {
286
+ const { current, scoreDelta } = rollup;
287
+ const deltaText = scoreDelta === null
288
+ ? p.dim("(first audit — no prior reading)")
289
+ : `${scoreDelta === 0 ? p.dim("·") : scoreDelta > 0 ? p.green("▲") : p.red("▼")} ${scoreDelta >= 0 ? p.green(`+${scoreDelta}`) : p.red(`${scoreDelta}`)} ${p.dim("since the previous audit")}`;
290
+ const trend = rollup.history.length > 1
291
+ ? ` ${p.cyan(sparkline(rollup.history.map((point) => point.score)))}`
292
+ : "";
293
+ const severity = (count, label, painter) => count > 0 ? painter(`${count} ${label}`) : p.dim(`${count} ${label}`);
294
+ const lines = [
295
+ p.bold(`GTM health — profile ${rollup.profile}`),
296
+ "",
297
+ `Score ${p.bold(scoreColor(current.score, p))}${p.dim("/100")} ${deltaText}${trend}`,
298
+ p.dim(`${rollup.auditCount} audit(s), ${shortDay(rollup.first)} → ${shortDay(rollup.latest)}`),
299
+ `Findings: ${formatCount(current.findings)} (${severity(current.severityCounts.critical, "critical", p.red)}, ${severity(current.severityCounts.warning, "warning", p.yellow)}, ${severity(current.severityCounts.info, "info", p.dim)})`,
300
+ "",
301
+ // Widths are measured on raw cell text, so cells stay unstyled here; the
302
+ // header row is dimmed as a whole line after layout.
303
+ ...table([
304
+ ["", "type", "score", "findings", "records"],
305
+ ...["account", "contact", "deal"].map((type) => {
306
+ const bucket = current.byObjectType[type];
307
+ return ["", type, `${bucket.score}/100`, formatCount(bucket.findings), formatCount(bucket.records)];
308
+ }),
309
+ ]).map((row, index) => (index === 0 ? p.dim(row) : row)),
310
+ ];
311
+ if (rollup.ruleDeltas.length > 0) {
312
+ // More findings = worse, so a positive delta paints red. The delta is the
313
+ // LAST column: ANSI codes there never skew the padding of columns before it.
314
+ const deltaCell = (delta) => delta === 0 ? p.dim("·") : delta > 0 ? p.red(`+${delta}`) : p.green(`${delta}`);
315
+ lines.push("", p.dim("By rule (Δ since previous audit)"), ...table(rollup.ruleDeltas.map((rule) => ["", rule.ruleId, formatCount(rule.current), deltaCell(rule.delta)])));
316
+ }
317
+ return lines.join("\n");
318
+ }
319
+ /** 2026-06-12T08:00:00Z → "2026-06-12". */
320
+ function shortDay(at) {
321
+ return at.slice(0, 10);
322
+ }
323
+ /**
324
+ * Render an audit as a client-facing deliverable. Same sources and audit
325
+ * options as `audit`; `--plan` instead renders an existing plan JSON without
326
+ * re-fetching (useful for a plan produced earlier or by another machine).
327
+ */
328
+ export async function reportCommand(args) {
329
+ const loaded = loadConfig(option(args, "--config") ?? undefined);
330
+ const configuredRules = await resolveConfiguredRules(loaded);
331
+ let plan;
332
+ let snapshot;
333
+ const planPath = option(args, "--plan");
334
+ if (planPath) {
335
+ plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
336
+ const input = option(args, "--input");
337
+ if (input) {
338
+ snapshot = JSON.parse(readFileSync(resolve(process.cwd(), input), "utf8"));
339
+ }
340
+ }
341
+ else {
342
+ snapshot = await readSnapshot(args);
343
+ const policy = mergePolicy(defaultPolicy(), loaded?.config);
344
+ const today = option(args, "--today");
345
+ if (today)
346
+ policy.today = today;
347
+ const staleDealDays = numericOption(args, "--stale-days");
348
+ if (staleDealDays !== undefined)
349
+ policy.staleDealDays = staleDealDays;
350
+ plan = auditSnapshot(snapshot, policy, selectedRules(args, configuredRules));
351
+ }
352
+ const reportOptions = {
353
+ title: option(args, "--title") ?? undefined,
354
+ clientName: option(args, "--client") ?? undefined,
355
+ preparedBy: option(args, "--prepared-by") ?? undefined,
356
+ date: option(args, "--today") ?? undefined,
357
+ maxExamplesPerRule: numericOption(args, "--max-examples"),
358
+ rules: configuredRules,
359
+ snapshot,
360
+ };
361
+ const out = option(args, "--out");
362
+ const format = option(args, "--format") ?? (out?.endsWith(".html") ? "html" : "markdown");
363
+ if (format !== "markdown" && format !== "html") {
364
+ throw new Error("--format must be markdown or html");
365
+ }
366
+ const rendered = format === "html"
367
+ ? auditReportToHtml(plan, reportOptions)
368
+ : auditReportToMarkdown(plan, reportOptions);
369
+ if (out) {
370
+ writeFileSync(resolve(process.cwd(), out), rendered);
371
+ console.log(`Wrote ${format} report (${plan.findings.length} findings) to ${out}`);
372
+ }
373
+ else {
374
+ console.log(rendered.trimEnd());
375
+ }
376
+ }
377
+ export async function rulesCommand(args) {
378
+ const loaded = loadConfig(option(args, "--config") ?? undefined);
379
+ const rules = await resolveConfiguredRules(loaded);
380
+ if (args.includes("--json")) {
381
+ console.log(JSON.stringify(rules.map(({ id, title, description, category }) => ({
382
+ id,
383
+ title,
384
+ description,
385
+ category: category ?? "uncategorized",
386
+ })), null, 2));
387
+ return;
388
+ }
389
+ for (const rule of rules) {
390
+ console.log(`${rule.id} [${rule.category ?? "uncategorized"}]\n ${rule.title}\n ${rule.description}\n`);
391
+ }
392
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The broker channel carries a long-lived pairing bearer and receives freshly
3
+ * minted live-CRM tokens, so it must be TLS unless it's an explicit localhost
4
+ * dev target. Refuse http:// (and non-http schemes) otherwise — single-quote
5
+ * shell escaping does nothing for a token sent in cleartext.
6
+ */
7
+ export declare function assertSecureBrokerUrl(raw: string): URL;
8
+ type CrmProvider = "hubspot" | "salesforce";
9
+ export declare function hostedProviderLogin(provider: CrmProvider, baseUrl: string): Promise<void>;
10
+ export declare function login(args: string[]): Promise<void>;
11
+ export declare function logout(args: string[]): void;
12
+ type ProviderDoctorStatus = {
13
+ source: "env" | "stored" | "broker" | "none";
14
+ detail: string;
15
+ };
16
+ export declare function doctorReport(env?: Record<string, string | undefined>): {
17
+ package: {
18
+ name: string;
19
+ version: string;
20
+ };
21
+ node: {
22
+ version: string;
23
+ ok: boolean;
24
+ required: string;
25
+ };
26
+ profile: string;
27
+ credentialStore: {
28
+ path: string;
29
+ exists: boolean;
30
+ };
31
+ config: {
32
+ path: string;
33
+ exists: boolean;
34
+ };
35
+ providers: Record<string, ProviderDoctorStatus>;
36
+ broker: {
37
+ paired: boolean;
38
+ baseUrl: string;
39
+ } | {
40
+ paired: boolean;
41
+ baseUrl?: undefined;
42
+ };
43
+ llm: {
44
+ configured: boolean;
45
+ provider: import("../llm.ts").LlmProvider;
46
+ source: "stored" | "env";
47
+ detail?: undefined;
48
+ } | {
49
+ configured: boolean;
50
+ detail: string;
51
+ provider?: undefined;
52
+ source?: undefined;
53
+ };
54
+ mcp: {
55
+ peersInstalled: boolean;
56
+ missing: string[];
57
+ };
58
+ nextSteps: string[];
59
+ };
60
+ export type WorkspaceDoctor = {
61
+ profile: string;
62
+ healthScore: number | null;
63
+ scoreDelta: number | null;
64
+ lastAuditAt: string | null;
65
+ auditCount: number;
66
+ pendingPlans: Array<{
67
+ id: string;
68
+ summary: string;
69
+ operations: number;
70
+ approved: number;
71
+ }>;
72
+ };
73
+ /**
74
+ * The workspace slice of doctor: current health + plans awaiting approval.
75
+ * Folded into doctor (rather than adding a separate triage verb) so a single
76
+ * call returns environment + workspace state + copy-pasteable next commands —
77
+ * previously an agent needed three round-trips (doctor, health, plans list)
78
+ * to orient itself.
79
+ */
80
+ export declare function workspaceDoctor(): Promise<WorkspaceDoctor>;
81
+ export declare function doctorCommand(args: string[]): Promise<void>;
82
+ export {};