fullstackgtm 0.50.0 → 0.51.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 +45 -0
- package/README.md +7 -0
- package/dist/cli/audit.js +6 -1
- package/dist/cli/auth.js +49 -4
- package/dist/cli/backfill.js +4 -0
- package/dist/cli/call.js +28 -6
- package/dist/cli/draft.js +11 -1
- package/dist/cli/enrich.js +124 -57
- package/dist/cli/fix.js +6 -3
- package/dist/cli/help.js +31 -28
- package/dist/cli/icp.js +15 -1
- package/dist/cli/init.js +3 -3
- package/dist/cli/market.js +14 -1
- package/dist/cli/planOutput.d.ts +5 -0
- package/dist/cli/planOutput.js +27 -0
- package/dist/cli/plans.js +184 -33
- package/dist/cli/schedule.js +22 -11
- package/dist/cli/signals.js +22 -3
- package/dist/cli/tam.d.ts +1 -1
- package/dist/cli/tam.js +14 -2
- package/dist/cli/ui.js +14 -6
- package/dist/connectors/clay.d.ts +33 -0
- package/dist/connectors/clay.js +123 -0
- package/dist/connectors/hubspot.d.ts +4 -0
- package/dist/connectors/hubspot.js +36 -18
- package/dist/connectors/prospectSources.d.ts +12 -0
- package/dist/connectors/prospectSources.js +1 -1
- package/dist/contactProviders.d.ts +44 -0
- package/dist/contactProviders.js +100 -0
- package/dist/enrich.d.ts +7 -1
- package/dist/enrich.js +46 -1
- package/dist/hostedPatchPlan.js +6 -1
- package/dist/icp.d.ts +2 -0
- package/dist/icp.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.js +1 -1
- package/docs/api.md +18 -1
- package/package.json +1 -1
- package/src/cli/audit.ts +5 -1
- package/src/cli/auth.ts +46 -4
- package/src/cli/backfill.ts +3 -0
- package/src/cli/call.ts +25 -6
- package/src/cli/draft.ts +9 -1
- package/src/cli/enrich.ts +132 -56
- package/src/cli/fix.ts +5 -3
- package/src/cli/help.ts +31 -28
- package/src/cli/icp.ts +13 -1
- package/src/cli/init.ts +3 -3
- package/src/cli/market.ts +12 -1
- package/src/cli/planOutput.ts +30 -0
- package/src/cli/plans.ts +174 -29
- package/src/cli/schedule.ts +21 -15
- package/src/cli/signals.ts +22 -3
- package/src/cli/tam.ts +12 -3
- package/src/cli/ui.ts +15 -6
- package/src/connectors/clay.ts +155 -0
- package/src/connectors/hubspot.ts +41 -17
- package/src/connectors/prospectSources.ts +7 -1
- package/src/contactProviders.ts +141 -0
- package/src/enrich.ts +51 -2
- package/src/hostedPatchPlan.ts +6 -1
- package/src/icp.ts +51 -0
- package/src/index.ts +25 -0
- package/src/init.ts +2 -2
package/dist/cli/help.js
CHANGED
|
@@ -16,13 +16,16 @@ Usage:
|
|
|
16
16
|
fullstackgtm login salesforce --instance-url <url> [--no-validate] advanced: paste access token
|
|
17
17
|
fullstackgtm login stripe [--no-validate]
|
|
18
18
|
fullstackgtm login anthropic | openai store an LLM API key for call parse/score
|
|
19
|
-
fullstackgtm login apollo
|
|
19
|
+
fullstackgtm login apollo | clay store a data-provider Public API key
|
|
20
|
+
fullstackgtm login pipe0 | explorium | theirstack store a discovery-provider key (theirstack = technographic TAM)
|
|
21
|
+
fullstackgtm login heyreach store a HeyReach key for enrich acquire --source linkedin
|
|
22
|
+
fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|heyreach|broker>
|
|
20
23
|
|
|
21
24
|
Secrets (tokens, client secrets) are NEVER passed as flags — they leak via
|
|
22
25
|
the process list and shell history. Pipe them on stdin or enter them at the
|
|
23
26
|
interactive prompt:
|
|
24
27
|
echo "$HUBSPOT_TOKEN" | fullstackgtm login hubspot --private-token
|
|
25
|
-
fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
28
|
+
fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
26
29
|
cold start: scaffold icp.json + enrich.config.json + a
|
|
27
30
|
PLAYBOOK wired for this workspace (the CLI ships primitives,
|
|
28
31
|
your agent is the orchestrator — see docs/recipes.md)
|
|
@@ -72,7 +75,7 @@ Usage:
|
|
|
72
75
|
fullstackgtm tam estimate [--name <n>] [--icp <path>] (--accounts <n> | --source theirstack|explorium) (--acv <annual-usd> | --acv-from-crm --deal-period monthly|quarterly|annual) [--acv-basis account|buyer] [--buyers-per-account <n>] [--cross-checks <file.json>] [source options] [--json]
|
|
73
76
|
fullstackgtm tam status [--name <n>] <source options> [--save] [--json]
|
|
74
77
|
fullstackgtm tam report [--name <n>] [--out <path>]
|
|
75
|
-
fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--label <l>]
|
|
78
|
+
fullstackgtm tam populate [--name <n>] --cron "<expr>" [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--label <l>]
|
|
76
79
|
size the reachable market FROM the ICP (a real account count ×
|
|
77
80
|
ACV; buyers/account = the contact population target), then fill
|
|
78
81
|
it: populate schedules plan-only enrich acquire --save (apply
|
|
@@ -176,11 +179,11 @@ Usage:
|
|
|
176
179
|
fullstackgtm suggest --plan-id <id> | --plan <path> [source options] [--json] [--out <path>]
|
|
177
180
|
derive values for requires_human_* placeholders
|
|
178
181
|
from snapshot evidence, with confidence + reasons
|
|
179
|
-
fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>
|
|
182
|
+
fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>
|
|
180
183
|
fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]
|
|
181
184
|
fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low] [--include-creates]
|
|
182
185
|
fullstackgtm plans recover <id> --acknowledge-uncertain-writes
|
|
183
|
-
fullstackgtm apply --plan-id <id> --provider <name>
|
|
186
|
+
fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]
|
|
184
187
|
fullstackgtm apply --plan-id <id> --channel outbox (render approved openers; transmits nothing)
|
|
185
188
|
fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [options]
|
|
186
189
|
fullstackgtm audit-log export [--out <path>] | verify --in <path> tamper-evident apply-run record
|
|
@@ -276,7 +279,7 @@ export const HELP = {
|
|
|
276
279
|
summary: "scaffold a workspace (icp.json + enrich.config.json + PLAYBOOK)",
|
|
277
280
|
phase: "Setup",
|
|
278
281
|
synopsis: [
|
|
279
|
-
"fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
|
|
282
|
+
"fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]",
|
|
280
283
|
],
|
|
281
284
|
detail: "Cold start: writes a starter ICP, an acquire-ready enrich.config.json (with a visible assign seam so leads are never ownerless), and a PLAYBOOK wired with the cold-start + outbound-loop recipes for this workspace. Pure file-writer — no network, keeps existing files unless --force. The CLI ships governed primitives; your coding agent is the orchestrator (see docs/recipes.md).",
|
|
282
285
|
seeAlso: ["icp", "enrich", "signals"],
|
|
@@ -289,7 +292,7 @@ export const HELP = {
|
|
|
289
292
|
"fullstackgtm login hubspot | salesforce hosted browser OAuth (default)",
|
|
290
293
|
"fullstackgtm login --via <hosted url> pair with a team deployment",
|
|
291
294
|
"fullstackgtm login stripe",
|
|
292
|
-
"fullstackgtm login anthropic | openai | apollo",
|
|
295
|
+
"fullstackgtm login anthropic | openai | apollo | clay",
|
|
293
296
|
],
|
|
294
297
|
detail: "HubSpot/Salesforce use hosted browser OAuth by default; BYO app/token paths are advanced. Secrets are NEVER passed as flags — pipe on stdin or enter at the prompt: `echo \"$TOKEN\" | fullstackgtm login hubspot --private-token`.",
|
|
295
298
|
seeAlso: ["doctor", "logout", "profiles"],
|
|
@@ -297,7 +300,7 @@ export const HELP = {
|
|
|
297
300
|
logout: {
|
|
298
301
|
summary: "remove stored credentials for a provider",
|
|
299
302
|
phase: "Setup",
|
|
300
|
-
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|pipe0|explorium|broker>"],
|
|
303
|
+
synopsis: ["fullstackgtm logout <hubspot|salesforce|stripe|anthropic|openai|apollo|clay|pipe0|explorium|broker>"],
|
|
301
304
|
seeAlso: ["login", "doctor"],
|
|
302
305
|
},
|
|
303
306
|
doctor: {
|
|
@@ -502,7 +505,7 @@ export const HELP = {
|
|
|
502
505
|
summary: "replicated plan lifecycle: list / show / sync / approve / reject",
|
|
503
506
|
phase: "Govern",
|
|
504
507
|
synopsis: [
|
|
505
|
-
"fullstackgtm plans list [--status <s>] | show <id> | sync | reject <id>",
|
|
508
|
+
"fullstackgtm plans list [--status <s>] | show <id> [--verbose] | sync | reject <id>",
|
|
506
509
|
"fullstackgtm plans approve <id> --operations <ids|all> [--value <opId>=<v>]",
|
|
507
510
|
"fullstackgtm plans approve <id> --values-from <suggestions.json> [--min-confidence high|low]",
|
|
508
511
|
"fullstackgtm plans recover <id> --acknowledge-uncertain-writes",
|
|
@@ -514,11 +517,11 @@ export const HELP = {
|
|
|
514
517
|
summary: "write ONLY explicitly approved operations to a provider",
|
|
515
518
|
phase: "Govern / Verify",
|
|
516
519
|
synopsis: [
|
|
517
|
-
"fullstackgtm apply --plan-id <id> --provider <name>",
|
|
520
|
+
"fullstackgtm apply --plan-id <id> --provider <name> [--verbose|--json]",
|
|
518
521
|
"fullstackgtm apply --plan-id <id> --channel outbox (render approved drafted openers to the outbox; transmits nothing)",
|
|
519
522
|
"fullstackgtm apply --plan <path> --provider <name> --approve <ids|all> [--value <opId>=<v>]",
|
|
520
523
|
],
|
|
521
|
-
detail: "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
|
|
524
|
+
detail: "The CLI CRM-write verb. Writes only operations approved locally or imported from the shared hosted replica, with an online execution claim when available, stable operation ids, compare-and-set, resolve-before-create, and readback. Default output is a compact outcome card; --verbose prints the full run document and --json preserves structured output. Hosted may execute the same synchronized plan when it has a compatible CRM connection; its exact operation receipts are imported on the next CLI check-in. Never writes requires_human_* placeholders without a --value override.",
|
|
522
525
|
seeAlso: ["plans", "suggest", "audit-log"],
|
|
523
526
|
},
|
|
524
527
|
"audit-log": {
|
|
@@ -597,7 +600,7 @@ export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "schedul
|
|
|
597
600
|
export const GLOBAL_FLAGS = ["--help", "--full"];
|
|
598
601
|
export const GLOBAL_SHORT_FLAGS = ["-h"];
|
|
599
602
|
export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
|
|
600
|
-
export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
|
|
603
|
+
export const AUDIT_FLAGS = ["--config", "--allow-plugins", "--no-plugins", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full", "--verbose"];
|
|
601
604
|
// Complete per-command flag registry used by runCli's fail-closed flag
|
|
602
605
|
// validation. Keep this next to HELP so focused help, machine capabilities,
|
|
603
606
|
// and the parser safety gate have one command inventory to reconcile against.
|
|
@@ -605,7 +608,7 @@ export const COMMAND_FLAGS = {
|
|
|
605
608
|
init: ["--source", "--provider", "--out", "--force"],
|
|
606
609
|
login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
|
|
607
610
|
logout: [],
|
|
608
|
-
doctor: ["--json"],
|
|
611
|
+
doctor: ["--verbose", "--json"],
|
|
609
612
|
capabilities: ["--json"],
|
|
610
613
|
"robot-docs": [],
|
|
611
614
|
profiles: ["--json"],
|
|
@@ -619,24 +622,24 @@ export const COMMAND_FLAGS = {
|
|
|
619
622
|
hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
|
|
620
623
|
relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
|
|
621
624
|
route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
622
|
-
fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
|
|
623
|
-
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
624
|
-
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
625
|
-
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
626
|
-
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
|
|
627
|
-
enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
|
|
628
|
-
call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
|
|
625
|
+
fix: [...SOURCE_FLAGS, "--config", "--allow-plugins", "--no-plugins", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run", "--verbose"],
|
|
626
|
+
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
627
|
+
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
628
|
+
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
629
|
+
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--verbose", "--json"],
|
|
630
|
+
enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--list", "--stale-days", "--assign-owner", "--objects", "--max", "--scan-limit", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
631
|
+
call: [...SOURCE_FLAGS, "--transcript", "--title", "--source", "--model", "--deterministic", "--heuristics", "--llm", "--verbose", "--json", "--ndjson", "--out", "--call", "--call-type", "--rubric", "--list", "--list-rubrics", "--attendees", "--domain", "--deal", "--save", "--dry-run"],
|
|
629
632
|
suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
|
|
630
|
-
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--json"],
|
|
631
|
-
apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
|
|
633
|
+
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--acknowledge-uncertain-writes", "--verbose", "--json"],
|
|
634
|
+
apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--verbose", "--json", "--config"],
|
|
632
635
|
"audit-log": ["--in", "--out", "--json"],
|
|
633
636
|
merge: ["--input", "--out", "--json"],
|
|
634
|
-
market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--json", "--out"],
|
|
635
|
-
tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--json", "--out"],
|
|
636
|
-
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
|
|
637
|
-
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
|
|
638
|
-
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
|
|
639
|
-
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--json"],
|
|
637
|
+
market: ["--category", "--config", "--vendor", "--snapshot", "--task-account", "--task-deal", "--capture-run", "--run", "--prior-run", "--diff", "--from", "--calls", "--anchor", "--min-mentions", "--max-claims", "--promote-lift", "--model", "--format", "--auto", "--unverified", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
638
|
+
tam: [...SOURCE_FLAGS, "--source", "--name", "--icp", "--accounts", "--acv", "--acv-basis", "--acv-from-crm", "--deal-period", "--buyers-per-account", "--cross-checks", "--max", "--max-credits", "--usd-per-credit", "--dry-run", "--confirm", "--cron", "--label", "--save", "--verbose", "--json", "--out"],
|
|
639
|
+
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--verbose", "--json", "--out"],
|
|
640
|
+
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--verbose", "--json", "--explain"],
|
|
641
|
+
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--verbose", "--json", "--out"],
|
|
642
|
+
schedule: ["--cron", "--label", "--provider", "--trigger", "--timer", "--runs", "--verbose", "--json"],
|
|
640
643
|
};
|
|
641
644
|
export const FLAGS_WITH_VALUES = new Set([
|
|
642
645
|
"--account", "--account-id", "--accounts", "--acv", "--acv-basis", "--after", "--anchor", "--api-key", "--approve", "--archive", "--assign-owner", "--attendees", "--before", "--bucket", "--buyers-per-account", "--call", "--call-type", "--calls", "--capture-run", "--category", "--channel", "--client", "--client-id", "--client-secret", "--config", "--connector", "--connector-opt", "--contact", "--cron", "--cross-checks", "--deal", "--deal-period", "--diff", "--domain", "--email", "--except-deal-stage", "--format", "--from", "--from-judge", "--golden", "--guard", "--icp", "--in", "--input", "--instance-url", "--keep", "--key", "--keywords", "--label", "--list", "--login-url", "--match", "--match-property", "--max", "--max-claims", "--max-credits", "--max-examples", "--max-operations", "--min-accuracy", "--min-confidence", "--min-mentions", "--min-score", "--model", "--name", "--objects", "--operations", "--out", "--pipeline", "--plan", "--plan-id", "--policy", "--port", "--prepared-by", "--prior-run", "--profile", "--promote-lift", "--prompt", "--provider", "--reason", "--require", "--result", "--rubric", "--rule", "--rules", "--run", "--run-label", "--runs", "--scan-limit", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--timer", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
|
package/dist/cli/icp.js
CHANGED
|
@@ -9,6 +9,20 @@ import { DEFAULT_GOLDEN_NOW_ISO, DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, defau
|
|
|
9
9
|
import { loadIcp, numericOption, option, readSnapshot, resolveLlmBaseUrls, saveRequested } from "./shared.js";
|
|
10
10
|
import { createStatusLine } from "./ui.js";
|
|
11
11
|
import { unknownSubcommandError } from "./suggest.js";
|
|
12
|
+
function renderJudgeDecisions(decisions) {
|
|
13
|
+
if (decisions.length === 0)
|
|
14
|
+
return "No accounts cleared the score threshold.";
|
|
15
|
+
const lines = [`ICP decisions (${decisions.length})`, ""];
|
|
16
|
+
for (const decision of decisions) {
|
|
17
|
+
const target = decision.contact?.email || decision.contact?.title;
|
|
18
|
+
lines.push(`${decision.decision.toUpperCase().padEnd(7)} ${String(decision.score).padStart(3)} ${decision.accountDomain}${target ? ` · ${target}` : ""}`);
|
|
19
|
+
if (decision.whyNow)
|
|
20
|
+
lines.push(` ${decision.whyNow.replace(/\s+/g, " ").trim()}`);
|
|
21
|
+
if (decision.play)
|
|
22
|
+
lines.push(` Next: ${decision.play.replace(/\s+/g, " ").trim()}`);
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
12
26
|
/**
|
|
13
27
|
* `icp` — develop and inspect the Ideal Customer Profile that targets acquire.
|
|
14
28
|
* The CLI can't run AskUserQuestion itself; `icp interview` emits the question
|
|
@@ -133,7 +147,7 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
133
147
|
}
|
|
134
148
|
decisions = decisions.filter((d) => d.score >= minScore);
|
|
135
149
|
// 5) Ranked decisions to stdout (JSON), guidance to stderr.
|
|
136
|
-
console.log(JSON.stringify(decisions, null, 2));
|
|
150
|
+
console.log(rest.includes("--json") || rest.includes("--verbose") ? JSON.stringify(decisions, null, 2) : renderJudgeDecisions(decisions));
|
|
137
151
|
console.error(`Judged ${unjudged.length} signal(s) across ${new Set(decisions.map((d) => d.accountDomain)).size} account(s): ` +
|
|
138
152
|
`${decisions.filter((d) => d.decision === "send").length} send, ` +
|
|
139
153
|
`${decisions.filter((d) => d.decision === "nurture").length} nurture, ` +
|
package/dist/cli/init.js
CHANGED
|
@@ -13,7 +13,7 @@ import { option } from "./shared.js";
|
|
|
13
13
|
export function initCommand(args) {
|
|
14
14
|
if (args.includes("--help") || args.includes("-h")) {
|
|
15
15
|
console.log(`Usage:
|
|
16
|
-
fullstackgtm init [--source pipe0|explorium|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
16
|
+
fullstackgtm init [--source pipe0|explorium|clay|linkedin] [--provider hubspot|salesforce] [--out <dir>] [--force]
|
|
17
17
|
|
|
18
18
|
Scaffolds a workspace so the first acquire/signals/judge/draft commands work:
|
|
19
19
|
icp.json starter ICP (edit, or rebuild with \`icp interview\`)
|
|
@@ -25,8 +25,8 @@ docs/recipes.md for the full play set. Existing files are kept unless --force.`)
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
const source = (option(args, "--source") ?? "pipe0");
|
|
28
|
-
if (!["pipe0", "explorium", "linkedin"].includes(source)) {
|
|
29
|
-
throw new Error(`init: --source must be pipe0|explorium|linkedin (got "${source}")`);
|
|
28
|
+
if (!["pipe0", "explorium", "clay", "linkedin"].includes(source)) {
|
|
29
|
+
throw new Error(`init: --source must be pipe0|explorium|clay|linkedin (got "${source}")`);
|
|
30
30
|
}
|
|
31
31
|
const provider = (option(args, "--provider") ?? "hubspot");
|
|
32
32
|
if (!["hubspot", "salesforce"].includes(provider)) {
|
package/dist/cli/market.js
CHANGED
|
@@ -481,9 +481,22 @@ Rules:
|
|
|
481
481
|
writeFileSync(resolve(process.cwd(), outPath), output);
|
|
482
482
|
console.log(`Wrote ${outPath}`);
|
|
483
483
|
}
|
|
484
|
-
else {
|
|
484
|
+
else if (rest.includes("--verbose") || option(rest, "--format")) {
|
|
485
485
|
console.log(output);
|
|
486
486
|
}
|
|
487
|
+
else {
|
|
488
|
+
const fronts = computeFrontStates(config, set);
|
|
489
|
+
const counts = new Map();
|
|
490
|
+
for (const front of fronts)
|
|
491
|
+
counts.set(front.state, (counts.get(front.state) ?? 0) + 1);
|
|
492
|
+
console.log(`${config.category} market · ${set.runLabel}`);
|
|
493
|
+
console.log(`${fronts.length} claim fronts · ${["open", "contested", "owned", "saturated", "vacant"].filter((state) => counts.has(state)).map((state) => `${counts.get(state)} ${state}`).join(" · ")}`);
|
|
494
|
+
for (const front of fronts) {
|
|
495
|
+
const owner = front.loudVendorIds.length ? ` · ${front.loudVendorIds.join(", ")}` : "";
|
|
496
|
+
console.log(`\n${front.state.toUpperCase()} ${front.claimId}${owner}`);
|
|
497
|
+
}
|
|
498
|
+
console.log("\nUse --verbose for the full evidence report, or --out <path> to write it.");
|
|
499
|
+
}
|
|
487
500
|
return;
|
|
488
501
|
}
|
|
489
502
|
if (subcommand === "axes") {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { box, colorEnabled, paint, truncateToWidth } from "./ui.js";
|
|
2
|
+
export function compactPlan(plan, options = {}) {
|
|
3
|
+
const counts = new Map();
|
|
4
|
+
for (const operation of plan.operations) {
|
|
5
|
+
const action = operation.operation || operation.field || "change";
|
|
6
|
+
counts.set(action, (counts.get(action) ?? 0) + 1);
|
|
7
|
+
}
|
|
8
|
+
const mix = [...counts]
|
|
9
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
10
|
+
.slice(0, 4)
|
|
11
|
+
.map(([action, count]) => `${count} ${action}`)
|
|
12
|
+
.join(" · ");
|
|
13
|
+
const lines = [
|
|
14
|
+
truncateToWidth(plan.title, 100),
|
|
15
|
+
`Status ${options.saved ? "SAVED · NEEDS APPROVAL" : "DRY RUN · NOTHING WRITTEN"}`,
|
|
16
|
+
truncateToWidth(`Scope ${plan.operations.length} operation${plan.operations.length === 1 ? "" : "s"}${mix ? ` · ${mix}` : ""}`, 100),
|
|
17
|
+
truncateToWidth(`Effect ${plan.summary}`, 100),
|
|
18
|
+
options.saved ? `Next fullstackgtm plans show ${plan.id}` : "Next re-run with --save to stage this plan for approval",
|
|
19
|
+
];
|
|
20
|
+
return [
|
|
21
|
+
...box(lines, paint(colorEnabled(process.stdout)), "Plan preview"),
|
|
22
|
+
`Details: re-run with --verbose${options.saved ? `; JSON: fullstackgtm plans show ${plan.id} --json` : "; machine output: --json"}`,
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
export function verbosePlanRequested(args) {
|
|
26
|
+
return args.includes("--verbose") || args.includes("--full");
|
|
27
|
+
}
|
package/dist/cli/plans.js
CHANGED
|
@@ -17,7 +17,7 @@ import { progressReporter, reportCounts } from "../runReport.js";
|
|
|
17
17
|
import { claimHostedPlanApply, reconcileHostedPatchPlan, releaseHostedPlanApply, reportHostedPlanLifecycle, uploadHostedPatchPlan } from "../hostedPatchPlan.js";
|
|
18
18
|
import { APPLY_STAGES, composeListeners, createProgressEmitter } from "../progress.js";
|
|
19
19
|
import { connectorFor, isOptionValue, numericOption, option, repeatedOption, selectedRules } from "./shared.js";
|
|
20
|
-
import { colorEnabled, createProgressRenderer, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
|
|
20
|
+
import { box, colorEnabled, createProgressRenderer, createStatusLine, formatDuration, paint, planStatusWord, stylizePlanMarkdown, table, truncateToWidth } from "./ui.js";
|
|
21
21
|
import { unknownSubcommandError } from "./suggest.js";
|
|
22
22
|
function parseValueOverrides(args) {
|
|
23
23
|
const valueOverrides = {};
|
|
@@ -29,6 +29,119 @@ function parseValueOverrides(args) {
|
|
|
29
29
|
}
|
|
30
30
|
return valueOverrides;
|
|
31
31
|
}
|
|
32
|
+
function recordHeadline(operation) {
|
|
33
|
+
const after = operation.afterValue && typeof operation.afterValue === "object" && !Array.isArray(operation.afterValue)
|
|
34
|
+
? operation.afterValue : {};
|
|
35
|
+
const properties = after.properties && typeof after.properties === "object" && !Array.isArray(after.properties)
|
|
36
|
+
? after.properties : {};
|
|
37
|
+
const first = typeof properties.firstname === "string" ? properties.firstname : "";
|
|
38
|
+
const last = typeof properties.lastname === "string" ? properties.lastname : "";
|
|
39
|
+
const company = typeof properties.company === "string" ? properties.company
|
|
40
|
+
: typeof after.associateCompanyName === "string" ? after.associateCompanyName : undefined;
|
|
41
|
+
const title = [first, last].filter(Boolean).join(" ") || company || operation.objectId || operation.id;
|
|
42
|
+
const role = typeof properties.jobtitle === "string" ? properties.jobtitle : undefined;
|
|
43
|
+
return { title, ...(role ? { subtitle: role } : {}), ...(company ? { company } : {}) };
|
|
44
|
+
}
|
|
45
|
+
function planEffect(stored) {
|
|
46
|
+
const total = stored.plan.operations.length;
|
|
47
|
+
const approved = stored.approvedOperationIds.length;
|
|
48
|
+
if (stored.status === "applied") {
|
|
49
|
+
const results = stored.runs.at(-1)?.results ?? [];
|
|
50
|
+
const applied = results.filter((result) => result.status === "applied").length;
|
|
51
|
+
const skipped = results.filter((result) => result.status === "skipped").length;
|
|
52
|
+
return `Completed ${applied} operation${applied === 1 ? "" : "s"}${skipped ? `; ${skipped} skipped` : ""}. No further writes will run.`;
|
|
53
|
+
}
|
|
54
|
+
if (stored.status === "approved")
|
|
55
|
+
return `Apply will execute ${approved} selected operation${approved === 1 ? "" : "s"}; ${total - approved} will not run.`;
|
|
56
|
+
if (stored.status === "rejected")
|
|
57
|
+
return "Rejected. No provider writes can run.";
|
|
58
|
+
return `No provider writes can run until operations are approved (0 of ${total} selected).`;
|
|
59
|
+
}
|
|
60
|
+
function printPlanCards(stored, hostedUrl) {
|
|
61
|
+
const p = paint(colorEnabled(process.stdout));
|
|
62
|
+
const total = stored.plan.operations.length;
|
|
63
|
+
const approved = new Set(stored.approvedOperationIds);
|
|
64
|
+
const width = Math.max(56, Math.min(100, (process.stdout.columns ?? 100) - 4));
|
|
65
|
+
const fit = (value) => truncateToWidth(value, width);
|
|
66
|
+
const summary = [
|
|
67
|
+
fit(stored.plan.title),
|
|
68
|
+
`Status ${stored.status.toUpperCase().replaceAll("_", " ")}`,
|
|
69
|
+
`Selection ${approved.size} of ${total} operation${total === 1 ? "" : "s"} approved`,
|
|
70
|
+
`Runs ${stored.runs.length}`,
|
|
71
|
+
fit(`Effect ${planEffect(stored)}`),
|
|
72
|
+
];
|
|
73
|
+
console.log(box(summary, p, "Plan").join("\n"));
|
|
74
|
+
if (hostedUrl)
|
|
75
|
+
console.log(`Hosted: ${hostedUrl}`);
|
|
76
|
+
console.log("\nOperations");
|
|
77
|
+
let operationIndex = 0;
|
|
78
|
+
const latestResults = new Map((stored.runs.at(-1)?.results ?? []).map((result) => [result.operationId, result.status]));
|
|
79
|
+
for (const operation of stored.plan.operations) {
|
|
80
|
+
const selected = approved.has(operation.id);
|
|
81
|
+
const resultStatus = latestResults.get(operation.id);
|
|
82
|
+
const headline = recordHeadline(operation);
|
|
83
|
+
const action = operation.operation === "create_record"
|
|
84
|
+
? `Create ${operation.objectType}${headline.company ? ` and resolve/link ${headline.company}` : ""}`
|
|
85
|
+
: `${operation.operation.replaceAll("_", " ")} ${operation.objectType}`;
|
|
86
|
+
const lines = [
|
|
87
|
+
fit(headline.title),
|
|
88
|
+
...(headline.subtitle || headline.company
|
|
89
|
+
? [fit([headline.subtitle, headline.company].filter(Boolean).join(" · "))]
|
|
90
|
+
: []),
|
|
91
|
+
fit(`Action ${action}`),
|
|
92
|
+
`Operation ${operation.id}`,
|
|
93
|
+
];
|
|
94
|
+
if (operationIndex++ > 0)
|
|
95
|
+
console.log("");
|
|
96
|
+
const state = resultStatus === "applied" ? "✓ APPLIED"
|
|
97
|
+
: resultStatus && selected ? `! ${resultStatus.toUpperCase()}`
|
|
98
|
+
: selected ? "✓ APPROVED" : stored.status === "applied" ? "○ EXCLUDED" : "○ NOT APPROVED";
|
|
99
|
+
console.log(box(lines, p, state).join("\n"));
|
|
100
|
+
}
|
|
101
|
+
if (stored.status === "approved") {
|
|
102
|
+
console.log(`\nNext: fullstackgtm apply --plan-id ${stored.plan.id} --provider <hubspot|salesforce>`);
|
|
103
|
+
}
|
|
104
|
+
console.log(`Details: fullstackgtm plans show ${stored.plan.id} --verbose`);
|
|
105
|
+
}
|
|
106
|
+
function printApplyCards(run, plan, approvedOperationIds, planIdStored) {
|
|
107
|
+
const p = paint(colorEnabled(process.stdout));
|
|
108
|
+
const approved = new Set(approvedOperationIds);
|
|
109
|
+
const operations = new Map(plan.operations.map((operation) => [operation.id, operation]));
|
|
110
|
+
const counts = { applied: 0, skipped: 0, failed: 0, conflict: 0 };
|
|
111
|
+
for (const result of run.results)
|
|
112
|
+
counts[result.status] += 1;
|
|
113
|
+
const elapsed = Math.max(0, Date.parse(run.finishedAt) - Date.parse(run.startedAt));
|
|
114
|
+
const summary = [
|
|
115
|
+
`Status ${run.status.toUpperCase()}`,
|
|
116
|
+
`Provider ${run.provider}`,
|
|
117
|
+
`Outcome ${counts.applied} applied · ${run.results.length - approved.size} excluded · ${counts.failed + counts.conflict} failed/conflicted`,
|
|
118
|
+
`Duration ${formatDuration(elapsed)}`,
|
|
119
|
+
...(planIdStored ? ["Receipt recorded locally; hosted reconciliation requested"] : []),
|
|
120
|
+
];
|
|
121
|
+
console.log(box(summary, p, "Apply result").join("\n"));
|
|
122
|
+
console.log("\nOperations");
|
|
123
|
+
let index = 0;
|
|
124
|
+
for (const result of run.results) {
|
|
125
|
+
const operation = operations.get(result.operationId);
|
|
126
|
+
if (!operation)
|
|
127
|
+
continue;
|
|
128
|
+
const selected = approved.has(result.operationId);
|
|
129
|
+
const headline = recordHeadline(operation);
|
|
130
|
+
const state = !selected ? "○ EXCLUDED" : result.status === "applied" ? "✓ APPLIED" : `! ${result.status.toUpperCase()}`;
|
|
131
|
+
const lines = [
|
|
132
|
+
headline.title,
|
|
133
|
+
...(headline.subtitle || headline.company ? [[headline.subtitle, headline.company].filter(Boolean).join(" · ")] : []),
|
|
134
|
+
`Operation ${result.operationId}`,
|
|
135
|
+
...(result.detail ? [truncateToWidth(`Result ${result.detail}`, 100)] : []),
|
|
136
|
+
];
|
|
137
|
+
if (index++ > 0)
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(box(lines, p, state).join("\n"));
|
|
140
|
+
}
|
|
141
|
+
if (planIdStored)
|
|
142
|
+
console.log(`\nInspect: fullstackgtm plans show ${run.planId}`);
|
|
143
|
+
console.log("Details: rerun with --verbose; machine output: --json");
|
|
144
|
+
}
|
|
32
145
|
function tryLoadAcquireConfig(args) {
|
|
33
146
|
try {
|
|
34
147
|
const path = resolve(process.cwd(), option(args, "--config") ?? ENRICH_CONFIG_FILE_NAME);
|
|
@@ -322,9 +435,12 @@ export async function apply(args) {
|
|
|
322
435
|
if (args.includes("--json")) {
|
|
323
436
|
console.log(JSON.stringify(run, null, 2));
|
|
324
437
|
}
|
|
325
|
-
else {
|
|
438
|
+
else if (args.includes("--verbose")) {
|
|
326
439
|
console.log(formatPatchPlanRun(run));
|
|
327
440
|
}
|
|
441
|
+
else {
|
|
442
|
+
printApplyCards(run, plan, approvedOperationIds, Boolean(planId && store));
|
|
443
|
+
}
|
|
328
444
|
if (run.status === "failed")
|
|
329
445
|
process.exitCode = 1;
|
|
330
446
|
}
|
|
@@ -422,31 +538,48 @@ export async function plansCommand(args) {
|
|
|
422
538
|
}
|
|
423
539
|
const p = paint(colorEnabled(process.stdout));
|
|
424
540
|
if (p.enabled) {
|
|
425
|
-
|
|
426
|
-
|
|
541
|
+
const statusCounts = new Map();
|
|
542
|
+
for (const stored of plans)
|
|
543
|
+
statusCounts.set(stored.status, (statusCounts.get(stored.status) ?? 0) + 1);
|
|
544
|
+
const awaiting = plans.filter((stored) => stored.status === "needs_approval").length;
|
|
545
|
+
const approved = plans.filter((stored) => stored.status === "approved").length;
|
|
546
|
+
console.log(box([
|
|
547
|
+
`${plans.length} plan${plans.length === 1 ? "" : "s"} · ${awaiting} awaiting approval · ${approved} ready to apply`,
|
|
548
|
+
[...statusCounts.entries()].map(([name, count]) => `${name.replaceAll("_", " ")}: ${count}`).join(" · "),
|
|
549
|
+
], p, "Change queue").join("\n"));
|
|
550
|
+
console.log("");
|
|
551
|
+
// Interactive terminals get an aligned decision table; the plain
|
|
552
|
+
// per-line format below remains stable for pipes and scripts.
|
|
427
553
|
const rows = plans.map((stored) => [
|
|
428
|
-
stored.plan.id,
|
|
429
554
|
stored.status,
|
|
430
|
-
|
|
555
|
+
stored.plan.title,
|
|
556
|
+
`${stored.approvedOperationIds.length}/${stored.plan.operations.length} selected`,
|
|
431
557
|
`${stored.runs.length} run${stored.runs.length === 1 ? "" : "s"}`,
|
|
432
|
-
stored.plan.
|
|
558
|
+
stored.plan.id,
|
|
433
559
|
]);
|
|
434
560
|
// Long summaries would wrap and break the table's alignment. The summary
|
|
435
561
|
// is the LAST column: cap it to what's left of the terminal width after
|
|
436
562
|
// the fixed columns and their two-space gutters (floor of 24 so narrow
|
|
437
563
|
// terminals still show something useful).
|
|
438
564
|
const columns = process.stdout.columns ?? 80;
|
|
439
|
-
const fixedWidth = [0,
|
|
440
|
-
const
|
|
565
|
+
const fixedWidth = [0, 2, 3, 4].reduce((sum, index) => sum + Math.max(...rows.map((row) => row[index].length)), 0) + 2 * 4;
|
|
566
|
+
const titleWidth = Math.max(24, columns - fixedWidth);
|
|
441
567
|
for (const row of rows)
|
|
442
|
-
row[
|
|
568
|
+
row[1] = truncateToWidth(row[1], titleWidth);
|
|
443
569
|
const statusPainter = (cell) => {
|
|
444
570
|
const painted = planStatusWord(cell.trimEnd(), p);
|
|
445
571
|
return painted + cell.slice(cell.trimEnd().length);
|
|
446
572
|
};
|
|
447
|
-
for (const line of table(rows, [
|
|
573
|
+
for (const line of table(rows, [statusPainter, null, p.dim, p.dim, p.dim])) {
|
|
448
574
|
console.log(line);
|
|
449
575
|
}
|
|
576
|
+
const next = plans.find((stored) => stored.status === "approved") ?? plans.find((stored) => stored.status === "needs_approval");
|
|
577
|
+
if (next) {
|
|
578
|
+
const command = next.status === "approved"
|
|
579
|
+
? `fullstackgtm apply --plan-id ${next.plan.id} --provider <name>`
|
|
580
|
+
: `fullstackgtm plans show ${next.plan.id}`;
|
|
581
|
+
console.log(`\nNext: ${command}`);
|
|
582
|
+
}
|
|
450
583
|
return;
|
|
451
584
|
}
|
|
452
585
|
for (const stored of plans) {
|
|
@@ -473,16 +606,11 @@ export async function plansCommand(args) {
|
|
|
473
606
|
console.log(JSON.stringify(stored, null, 2));
|
|
474
607
|
return;
|
|
475
608
|
}
|
|
476
|
-
const showPaint = paint(colorEnabled(process.stdout));
|
|
477
|
-
console.log(`Status: ${planStatusWord(stored.status, showPaint)}`);
|
|
478
|
-
console.log(`Approved operations: ${stored.approvedOperationIds.join(", ") || "none"}`);
|
|
479
|
-
console.log(`Runs: ${stored.runs.length}`);
|
|
480
609
|
const mirror = await uploadHostedPatchPlan(stored);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (stored.applyAttempts?.length) {
|
|
610
|
+
printPlanCards(stored, mirror.status === "saved" ? mirror.state.url : undefined);
|
|
611
|
+
if (mirror.status === "unavailable" || mirror.status === "conflict")
|
|
612
|
+
console.log(`Hosted sync pending: ${mirror.reason}`);
|
|
613
|
+
if (rest.includes("--verbose") && stored.applyAttempts?.length) {
|
|
486
614
|
console.log("Apply attempts:");
|
|
487
615
|
for (const attempt of stored.applyAttempts) {
|
|
488
616
|
console.log(` ${attempt.id} ${attempt.status} ${attempt.provider} via ${attempt.source} ${attempt.claimedAt}`);
|
|
@@ -490,8 +618,11 @@ export async function plansCommand(args) {
|
|
|
490
618
|
console.log(` ${attempt.note}`);
|
|
491
619
|
}
|
|
492
620
|
}
|
|
493
|
-
|
|
494
|
-
|
|
621
|
+
if (rest.includes("--verbose")) {
|
|
622
|
+
const showPaint = paint(colorEnabled(process.stdout));
|
|
623
|
+
console.log("\nFull plan document\n");
|
|
624
|
+
console.log(stylizePlanMarkdown(patchPlanToMarkdown(stored.plan), showPaint));
|
|
625
|
+
}
|
|
495
626
|
return;
|
|
496
627
|
}
|
|
497
628
|
if (subcommand === "approve") {
|
|
@@ -578,19 +709,39 @@ export async function plansCommand(args) {
|
|
|
578
709
|
if (subcommand === "sync") {
|
|
579
710
|
const plans = await store.list();
|
|
580
711
|
let updated = 0;
|
|
712
|
+
let completed = 0;
|
|
581
713
|
const warnings = [];
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
714
|
+
const status = createStatusLine();
|
|
715
|
+
status.set(`Synchronizing plan replicas 0/${plans.length}`);
|
|
716
|
+
// A workspace can accumulate hundreds of local plans. Reconcile with a
|
|
717
|
+
// small worker pool so one network round-trip per plan does not turn into
|
|
718
|
+
// a minutes-long silent serial wait, without stampeding the hosted API.
|
|
719
|
+
let next = 0;
|
|
720
|
+
const worker = async () => {
|
|
721
|
+
while (next < plans.length) {
|
|
722
|
+
const stored = plans[next++];
|
|
723
|
+
const result = await reconcileHostedPatchPlan(store, stored);
|
|
724
|
+
if (result.status === "updated")
|
|
725
|
+
updated += 1;
|
|
726
|
+
else if (result.status === "conflict" || result.status === "unavailable")
|
|
727
|
+
warnings.push(`${stored.plan.id}: ${result.reason}`);
|
|
728
|
+
// Historical local-only plans have no remote row to receive a receipt.
|
|
729
|
+
// `backfill runs` is the explicit import path for that old history.
|
|
730
|
+
if (stored.status === "applied" && result.status !== "missing" && (stored.applyAttempts?.length ?? 0) > 0) {
|
|
731
|
+
const hostedClaimId = [...(stored.applyAttempts ?? [])].reverse().find((attempt) => attempt.hostedClaimId)?.hostedClaimId;
|
|
732
|
+
const pushed = await reportHostedPlanLifecycle(stored, { claimId: hostedClaimId });
|
|
733
|
+
if (pushed.status === "conflict" || pushed.status === "unavailable")
|
|
734
|
+
warnings.push(`${stored.plan.id} receipt: ${pushed.reason}`);
|
|
735
|
+
}
|
|
736
|
+
completed += 1;
|
|
737
|
+
status.set(`Synchronizing plan replicas ${completed}/${plans.length}`);
|
|
593
738
|
}
|
|
739
|
+
};
|
|
740
|
+
try {
|
|
741
|
+
await Promise.all(Array.from({ length: Math.min(6, plans.length) }, () => worker()));
|
|
742
|
+
}
|
|
743
|
+
finally {
|
|
744
|
+
status.done();
|
|
594
745
|
}
|
|
595
746
|
console.log(`Plan replicas synchronized: ${updated} updated, ${plans.length - updated} unchanged.`);
|
|
596
747
|
for (const warning of warnings)
|