fullstackgtm 0.47.0 → 0.48.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 +38 -6
- package/INSTALL_FOR_AGENTS.md +3 -3
- package/README.md +16 -8
- package/dist/audit.js +7 -0
- package/dist/backfill.js +2 -16
- package/dist/cli/fix.d.ts +3 -0
- package/dist/cli/fix.js +70 -1
- package/dist/cli/help.d.ts +6 -0
- package/dist/cli/help.js +76 -2
- package/dist/cli/suggest.d.ts +2 -1
- package/dist/cli/suggest.js +49 -3
- package/dist/cli.js +40 -15
- package/dist/connectors/hubspot.d.ts +2 -1
- package/dist/connectors/salesforce.d.ts +2 -1
- package/dist/connectors/signalSources.js +2 -11
- package/dist/format.js +31 -5
- package/dist/freeEmailDomains.d.ts +1 -0
- package/dist/freeEmailDomains.js +14 -0
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -1
- package/dist/mcp.js +19 -15
- package/dist/relationships.d.ts +39 -0
- package/dist/relationships.js +101 -0
- package/dist/route.d.ts +46 -0
- package/dist/route.js +228 -0
- package/dist/rules.d.ts +1 -0
- package/dist/rules.js +172 -12
- package/dist/types.d.ts +15 -0
- package/docs/api.md +20 -3
- package/docs/architecture.md +5 -2
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +2 -2
- package/src/audit.ts +7 -0
- package/src/backfill.ts +2 -17
- package/src/cli/fix.ts +68 -1
- package/src/cli/help.ts +83 -2
- package/src/cli/suggest.ts +58 -4
- package/src/cli.ts +38 -13
- package/src/connectors/hubspot.ts +3 -1
- package/src/connectors/salesforce.ts +3 -1
- package/src/connectors/signalSources.ts +2 -12
- package/src/format.ts +33 -5
- package/src/freeEmailDomains.ts +14 -0
- package/src/hierarchy.ts +185 -0
- package/src/index.ts +25 -0
- package/src/mcp.ts +22 -16
- package/src/relationships.ts +115 -0
- package/src/route.ts +278 -0
- package/src/rules.ts +192 -12
- package/src/types.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,10 +3,42 @@
|
|
|
3
3
|
All notable changes to the `fullstackgtm` package are documented here.
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
-
The path to 1.0 is planned in
|
|
6
|
+
The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md).
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.48.0] — 2026-07-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added `GtmAuditRule.emitsOperations` and `isFindingsOnlyRule` so hosts can derive findings-only rule behavior from the package registry instead of duplicating rule IDs.
|
|
15
|
+
- **Route, hierarchy, and relationships open surfaces.** `route leads` builds governed lead-to-account/owner patch plans; `hierarchy report` and `relationships account` provide read-only planning reports for account trees and stakeholder maps.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **CLI human path now uses progressive disclosure by default.** Bare
|
|
20
|
+
`fullstackgtm`/`--help` shows a compact lifecycle-grouped command map instead
|
|
21
|
+
of the full flag wall; `help <command>` and `<command> --help` show focused
|
|
22
|
+
per-command guidance; `help --full` (including `help <command> --full`) keeps
|
|
23
|
+
the complete reference available. `audit` now defaults to a summary view with
|
|
24
|
+
`--full` for per-operation detail and prints next-step guidance on stderr for
|
|
25
|
+
human runs while keeping `--json` machine-clean. The dry-run footer now frames
|
|
26
|
+
approval/write safety as invariants rather than prototype copy.
|
|
27
|
+
- **Patch-plan assumptions and open questions are canonical.** Plan exports and
|
|
28
|
+
markdown use `assumptions` plus `openQuestions`; the duplicate
|
|
29
|
+
`decisionPoints` alias was removed.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Route plan safety and batching.** Owner routing recognizes both canonical
|
|
34
|
+
user ids and provider CRM owner ids, skips name-only account matches as
|
|
35
|
+
ambiguous, avoids self-conflicting sibling preconditions, groups only contacts
|
|
36
|
+
with multiple operations, and uses one shared free-email domain list.
|
|
37
|
+
- **Hierarchy and relationships correctness.** Hierarchy reports dedupe duplicate
|
|
38
|
+
account ids, avoid common public-suffix parent guesses, and rely on a single
|
|
39
|
+
cycle-dedupe pass. Relationship `--domain` selectors normalize domains and
|
|
40
|
+
markdown preserves zero-value deal amounts.
|
|
41
|
+
|
|
10
42
|
## [0.47.0] — 2026-07-06
|
|
11
43
|
|
|
12
44
|
### Added
|
|
@@ -117,7 +149,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
|
|
|
117
149
|
doctor, rules, dedupe, resolve, snapshot, and error paths) and the public
|
|
118
150
|
import surface of `cli.ts` is unchanged. Profiling method, opportunity
|
|
119
151
|
matrix, and honest non-wins are documented in
|
|
120
|
-
[docs/perf-notes.md](
|
|
152
|
+
[docs/perf-notes.md](https://github.com/fullstackgtm/core/blob/main/docs/perf-notes.md).
|
|
121
153
|
|
|
122
154
|
## [0.45.0] — 2026-07-02
|
|
123
155
|
|
|
@@ -455,7 +487,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
|
|
|
455
487
|
Library: `connectors/signalSources.ts` (`listSignalSources`, `getSignalSource`,
|
|
456
488
|
`fileSource`, `serpapiNewsSource`, `hubspotFormsSource`), `stagedRowToSignal` +
|
|
457
489
|
`StagedSignalRow` from `signals.ts`. Design:
|
|
458
|
-
[docs/spec-connectors-signals-outbound.md](
|
|
490
|
+
[docs/spec-connectors-signals-outbound.md](https://github.com/fullstackgtm/core/blob/main/docs/spec-connectors-signals-outbound.md)
|
|
459
491
|
(the connector taxonomy + the Phase 2/3 webhook-spool and governed-channel plan).
|
|
460
492
|
- **Webhook spool format + conventional landing zone (signals Phase 2).**
|
|
461
493
|
`--connector file` with no path now reads the conventional spool directory
|
|
@@ -844,7 +876,7 @@ The path to 1.0 is planned in the roadmap doc in the repository.
|
|
|
844
876
|
you approve specific operations and run `apply`… These safety invariants are
|
|
845
877
|
not beta."), matching the README and agent skill.
|
|
846
878
|
|
|
847
|
-
See [docs/dx-punch-list.md](
|
|
879
|
+
See [docs/dx-punch-list.md](https://github.com/fullstackgtm/core/blob/main/docs/dx-punch-list.md) for the audit that drove this.
|
|
848
880
|
|
|
849
881
|
## [0.34.0] — 2026-06-18
|
|
850
882
|
|
|
@@ -1935,7 +1967,7 @@ milestones, not a frozen public contract. 0.10.0 continues the 0.x line
|
|
|
1935
1967
|
1.2.2 plus the status/docs corrections below. `fullstackgtm@1.2.1` and
|
|
1936
1968
|
`@1.2.2` were briefly on npm (2026-06-10) and were unpublished the same
|
|
1937
1969
|
week; those version numbers are permanently burned per npm policy. The real
|
|
1938
|
-
1.0 will be declared via [docs/roadmap-to-1.0.md](
|
|
1970
|
+
1.0 will be declared via [docs/roadmap-to-1.0.md](https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md)
|
|
1939
1971
|
once the API surface has survived external usage.
|
|
1940
1972
|
|
|
1941
1973
|
### Changed
|
|
@@ -2230,7 +2262,7 @@ stability commitment attached.
|
|
|
2230
2262
|
|
|
2231
2263
|
- The hosted app's Convex plan tables are the second `PlanStore`
|
|
2232
2264
|
implementation target; unifying them onto these types is the remaining
|
|
2233
|
-
0.3.x work (see docs/roadmap-to-1.0.md).
|
|
2265
|
+
0.3.x work (see https://github.com/fullstackgtm/core/blob/main/docs/roadmap-to-1.0.md).
|
|
2234
2266
|
|
|
2235
2267
|
## [0.2.0] — Unreleased
|
|
2236
2268
|
|
package/INSTALL_FOR_AGENTS.md
CHANGED
|
@@ -33,7 +33,7 @@ machine — that is normal and does not block the next step.
|
|
|
33
33
|
fullstackgtm audit --demo --json
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Expect a JSON patch plan with `dryRun: true` and
|
|
36
|
+
Expect a JSON patch plan with `dryRun: true` and 110 findings/110 operations over a generated,
|
|
37
37
|
deliberately messy CRM. Deterministic per seed: `--seed 7` is the default, so
|
|
38
38
|
two runs produce identical finding and operation ids. Exit code 0.
|
|
39
39
|
|
|
@@ -131,8 +131,8 @@ the server resolves them from there (peer-dependency semantics) — so this
|
|
|
131
131
|
works from inside existing projects too.
|
|
132
132
|
|
|
133
133
|
Tools exposed over stdio — read-only: `fullstackgtm_audit`,
|
|
134
|
-
`
|
|
135
|
-
`fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
|
|
134
|
+
`fullstackgtm_capabilities`, `fullstackgtm_rules`, `fullstackgtm_suggest`,
|
|
135
|
+
`fullstackgtm_call_parse`, `fullstackgtm_resolve`, `fullstackgtm_market_worksheet`. Gated:
|
|
136
136
|
`fullstackgtm_apply` (requires explicit `approvedOperationIds`),
|
|
137
137
|
`fullstackgtm_market_observe` (every quoted span is verified against the
|
|
138
138
|
stored captures before anything is appended).
|
package/README.md
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/fullstackgtm/core/actions/workflows/ci.yml) [](https://www.npmjs.com/package/fullstackgtm) [](./LICENSE)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Guardrails for AI agents working in your CRM.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Let Claude or Codex loose on Salesforce or HubSpot and it will make predictable mistakes — duplicates, bad batch edits, silent overwrites. fullstackgtm is the wrapper that knows those mistakes: every change becomes a reviewable plan before anything is written.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Instead of overwriting records, an agent using fullstackgtm sets out a **patch plan**: what it wants to change, field by field, with before/after values and the reason. You review it, roll it out incrementally (approve 10 operations, then 100, then all), and every write records the before value — so a mistake is visible and reversible, not silently absorbed. Think `terraform plan` for your CRM.
|
|
10
|
+
|
|
11
|
+
**What it catches:** duplicates, orphaned records, bad batch edits, stale deals, missing owners — deterministic rules, not LLM guesses. Works as a CLI, a library, or an MCP server your agent calls directly.
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Open source (Apache-2.0), zero runtime dependencies. Beta (0.x): APIs may shift before 1.0; the safety invariants never do. Connectors: HubSpot, Salesforce (read/write), Stripe (read-only).
|
|
12
14
|
|
|
13
15
|
## Install
|
|
14
16
|
|
|
15
17
|
```bash
|
|
18
|
+
npx fullstackgtm audit --demo # zero-install try-it path
|
|
16
19
|
npm install fullstackgtm # library + CLI in a project
|
|
17
|
-
npx fullstackgtm audit --demo # or zero-install via npx
|
|
18
20
|
npm install github:fullstackgtm/core # or straight from this repo (project-local)
|
|
19
21
|
npx github:fullstackgtm/core audit --demo # zero-install from the repo
|
|
20
22
|
```
|
|
@@ -315,7 +317,7 @@ fullstackgtm diff --before old.json --after new.json --fail-on-new-findings
|
|
|
315
317
|
- `--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.
|
|
316
318
|
- Exit codes: `0` success, `1` error, `2` findings at/above `--fail-on`.
|
|
317
319
|
|
|
318
|
-
"Built for agents" is measured, not asserted: a 1,904-run benchmark (17 CRM-operations scenarios × 3 tool-surface arms × up to 4 trials, across ten models from six vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface matching or beating raw CRM-API access on completion-under-policy for every model tested — strictly better in nine of ten — and the effect is vendor-independent. Full matrix and methodology: [the leaderboard](
|
|
320
|
+
"Built for agents" is measured, not asserted: a 1,904-run benchmark (17 CRM-operations scenarios × 3 tool-surface arms × up to 4 trials, across ten models from six vendors, deterministic graders over final CRM state, τ-bench-style pass^k) shows the gated CLI surface matching or beating raw CRM-API access on completion-under-policy for every model tested — strictly better in nine of ten — and the effect is vendor-independent. Full matrix and methodology: [the leaderboard](https://github.com/fullstackgtm/core/blob/main/evals/crm/leaderboard/RESULTS.md).
|
|
319
321
|
|
|
320
322
|
Caveat: the leaderboard documents the Opus reduced protocol and mixed-version Informed arms; treat the headline as a pointer to those exact RESULTS.md conditions.
|
|
321
323
|
|
|
@@ -533,9 +535,9 @@ Or configure any MCP client (Cursor, Claude Desktop, …) with:
|
|
|
533
535
|
}
|
|
534
536
|
```
|
|
535
537
|
|
|
536
|
-
|
|
538
|
+
Nine tools are exposed over stdio.
|
|
537
539
|
|
|
538
|
-
**Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_rules` (rule discovery), `fullstackgtm_suggest` (deterministic placeholder values with confidence + reasons), `fullstackgtm_call_parse` (transcripts → provenance-marked segments, insights, and evidence), `fullstackgtm_resolve` (the create gate: exists / ambiguous / safe_to_create), and `fullstackgtm_market_worksheet` (the classification packet for one vendor: claims, judging rules, captured page texts).
|
|
540
|
+
**Read-only:** `fullstackgtm_audit` (sample, demo, file, or live provider sources with optional rule scoping), `fullstackgtm_capabilities` (server/tool capability manifest), `fullstackgtm_rules` (rule discovery), `fullstackgtm_suggest` (deterministic placeholder values with confidence + reasons), `fullstackgtm_call_parse` (transcripts → provenance-marked segments, insights, and evidence), `fullstackgtm_resolve` (the create gate: exists / ambiguous / safe_to_create), and `fullstackgtm_market_worksheet` (the classification packet for one vendor: claims, judging rules, captured page texts).
|
|
539
541
|
|
|
540
542
|
**Gated:** `fullstackgtm_apply` (requires explicit `approvedOperationIds`; placeholders still need value overrides) and `fullstackgtm_market_observe` (verifies every quoted span against the stored captures before appending — nothing is stored unless the whole set passes).
|
|
541
543
|
|
|
@@ -547,6 +549,12 @@ Tokens stored via `fullstackgtm login` are picked up automatically — the env v
|
|
|
547
549
|
2. Every proposed write is a typed patch operation with before/after values, a reason, and a risk level.
|
|
548
550
|
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.
|
|
549
551
|
|
|
552
|
+
## License & boundary
|
|
553
|
+
|
|
554
|
+
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.
|
|
555
|
+
|
|
556
|
+
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 safety invariants (read-only audits, approval-gated writes, placeholder refusal) are not beta and do not change.
|
|
557
|
+
|
|
550
558
|
## Development
|
|
551
559
|
|
|
552
560
|
```bash
|
package/dist/audit.js
CHANGED
|
@@ -33,6 +33,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
33
33
|
const context = { snapshot, policy, index: buildSnapshotIndex(snapshot) };
|
|
34
34
|
const findings = [];
|
|
35
35
|
const operations = [];
|
|
36
|
+
const assumptionsById = new Map();
|
|
36
37
|
for (const rule of rules) {
|
|
37
38
|
try {
|
|
38
39
|
onRule?.(rule.id, "start");
|
|
@@ -43,6 +44,10 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
43
44
|
const result = rule.evaluate(context);
|
|
44
45
|
findings.push(...result.findings);
|
|
45
46
|
operations.push(...result.operations);
|
|
47
|
+
for (const assumption of result.assumptions ?? []) {
|
|
48
|
+
if (!assumptionsById.has(assumption.id))
|
|
49
|
+
assumptionsById.set(assumption.id, assumption);
|
|
50
|
+
}
|
|
46
51
|
try {
|
|
47
52
|
onRule?.(rule.id, "done", result.findings.length);
|
|
48
53
|
}
|
|
@@ -50,6 +55,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
50
55
|
// progress is presentation-only
|
|
51
56
|
}
|
|
52
57
|
}
|
|
58
|
+
const assumptions = Array.from(assumptionsById.values());
|
|
53
59
|
const evidence = buildEvidence(snapshot, findings, policy.today);
|
|
54
60
|
const pipelineFindings = buildPipelineFindings(findings, operations, evidence, policy.today);
|
|
55
61
|
linkOperationContext(operations, findings, evidence);
|
|
@@ -63,6 +69,7 @@ export function auditSnapshot(snapshot, policy = defaultPolicy(), rules = builti
|
|
|
63
69
|
findings,
|
|
64
70
|
pipelineFindings,
|
|
65
71
|
evidence,
|
|
72
|
+
assumptions: assumptions.length > 0 ? assumptions : undefined,
|
|
66
73
|
operations,
|
|
67
74
|
};
|
|
68
75
|
}
|
package/dist/backfill.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* connector re-resolves each invoice id (resolve-first) and creates only on
|
|
12
12
|
* a confirmed miss.
|
|
13
13
|
*/
|
|
14
|
+
import { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
|
|
14
15
|
import { normalizeDomain } from "./merge.js";
|
|
15
16
|
// Mirrors stableHash in rules.ts (FNV-1a); duplicated to keep backfill.ts
|
|
16
17
|
// importable without pulling the audit engine (the enrich.ts precedent).
|
|
@@ -23,21 +24,6 @@ function fnv1a(value) {
|
|
|
23
24
|
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
24
25
|
}
|
|
25
26
|
export const DEFAULT_BACKFILL_MATCH_PROPERTY = "stripe_invoice_id";
|
|
26
|
-
// Freemail domains never become a company domain: stamping "gmail.com" on an
|
|
27
|
-
// account (or resolving a company BY gmail.com) would collapse unrelated
|
|
28
|
-
// customers onto one record. Mirrors FREE_MAIL in connectors/signalSources.ts
|
|
29
|
-
// (duplicated to keep backfill.ts importable without connector code, the
|
|
30
|
-
// fnv1a precedent above).
|
|
31
|
-
const FREE_MAIL = new Set([
|
|
32
|
-
"gmail.com",
|
|
33
|
-
"yahoo.com",
|
|
34
|
-
"hotmail.com",
|
|
35
|
-
"outlook.com",
|
|
36
|
-
"icloud.com",
|
|
37
|
-
"aol.com",
|
|
38
|
-
"proton.me",
|
|
39
|
-
"protonmail.com",
|
|
40
|
-
]);
|
|
41
27
|
/** `Invoice <number || id> — <customer name>` (the plan-time dedupe name). */
|
|
42
28
|
function backfillDealName(invoice, companyName) {
|
|
43
29
|
return `Invoice ${invoice.number ?? invoice.id} — ${invoice.customerName ?? companyName}`;
|
|
@@ -102,7 +88,7 @@ export function buildStripeBackfillPlan(invoices, snapshot, opts = {}) {
|
|
|
102
88
|
let companyDomain = normalizeDomain(account?.domain);
|
|
103
89
|
let accountIsNew = false;
|
|
104
90
|
if (!account && createMissingAccounts) {
|
|
105
|
-
const usableDomain = domain && !
|
|
91
|
+
const usableDomain = domain && !FREE_EMAIL_DOMAINS.has(domain) ? domain : undefined;
|
|
106
92
|
const name = invoice.customerName?.trim() || usableDomain;
|
|
107
93
|
if (name) {
|
|
108
94
|
companyName = name;
|
package/dist/cli/fix.d.ts
CHANGED
|
@@ -31,3 +31,6 @@ export declare function reassignCommand(args: string[]): Promise<void>;
|
|
|
31
31
|
*/
|
|
32
32
|
export declare function fixCommand(args: string[]): Promise<void>;
|
|
33
33
|
export declare function suggest(args: string[]): Promise<void>;
|
|
34
|
+
export declare function routeCommand(args: string[]): Promise<void>;
|
|
35
|
+
export declare function hierarchyCommand(args: string[]): Promise<void>;
|
|
36
|
+
export declare function relationshipsCommand(args: string[]): Promise<void>;
|
package/dist/cli/fix.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { auditSnapshot, defaultPolicy } from "../audit.js";
|
|
5
5
|
import { loadConfig, mergePolicy, resolveConfiguredRules } from "../config.js";
|
|
@@ -7,6 +7,10 @@ import { applyPatchPlan } from "../connector.js";
|
|
|
7
7
|
import { patchPlanToMarkdown } from "../format.js";
|
|
8
8
|
import { createFilePlanStore } from "../planStore.js";
|
|
9
9
|
import { resolveRecord } from "../resolve.js";
|
|
10
|
+
import { parseAssignmentPolicy } from "../assign.js";
|
|
11
|
+
import { buildLeadRoutePlan } from "../route.js";
|
|
12
|
+
import { accountHierarchyToMarkdown, buildAccountHierarchy } from "../hierarchy.js";
|
|
13
|
+
import { buildRelationshipMap, relationshipMapToMarkdown } from "../relationships.js";
|
|
10
14
|
import { buildBulkUpdatePlan } from "../bulkUpdate.js";
|
|
11
15
|
import { buildDedupePlan } from "../dedupe.js";
|
|
12
16
|
import { buildReassignPlans } from "../reassign.js";
|
|
@@ -344,3 +348,68 @@ function snapshotSourceHint(args) {
|
|
|
344
348
|
return `--input ${input} `;
|
|
345
349
|
return "";
|
|
346
350
|
}
|
|
351
|
+
function parseJsonOrFile(value) {
|
|
352
|
+
const candidate = resolve(process.cwd(), value);
|
|
353
|
+
const text = existsSync(candidate) ? readFileSync(candidate, "utf8") : value;
|
|
354
|
+
return JSON.parse(text);
|
|
355
|
+
}
|
|
356
|
+
export async function routeCommand(args) {
|
|
357
|
+
const [subcommand, ...rest] = args;
|
|
358
|
+
if (subcommand !== "leads") {
|
|
359
|
+
throw new Error("Usage: fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save] [--json] [--out <path>]");
|
|
360
|
+
}
|
|
361
|
+
const match = option(rest, "--match") ?? "domain";
|
|
362
|
+
if (!["domain", "company", "both"].includes(match))
|
|
363
|
+
throw new Error("--match must be domain, company, or both");
|
|
364
|
+
const policyRaw = option(rest, "--policy");
|
|
365
|
+
const snapshot = await readSnapshot(rest);
|
|
366
|
+
const result = buildLeadRoutePlan(snapshot, {
|
|
367
|
+
matchByDomain: match === "domain" || match === "both",
|
|
368
|
+
matchByCompanyName: match === "company" || match === "both",
|
|
369
|
+
inheritAccountOwner: !rest.includes("--no-inherit-owner"),
|
|
370
|
+
reassignExistingOwner: rest.includes("--reassign-owned"),
|
|
371
|
+
assignmentPolicy: policyRaw ? parseAssignmentPolicy(parseJsonOrFile(policyRaw)) : undefined,
|
|
372
|
+
maxOperations: numericOption(rest, "--max-operations"),
|
|
373
|
+
reason: option(rest, "--reason") ?? undefined,
|
|
374
|
+
});
|
|
375
|
+
if (rest.includes("--json")) {
|
|
376
|
+
const out = option(rest, "--out");
|
|
377
|
+
if (out)
|
|
378
|
+
writeFileSync(resolve(process.cwd(), out), `${JSON.stringify(result.plan, null, 2)}\n`);
|
|
379
|
+
if (saveRequested(rest))
|
|
380
|
+
await createFilePlanStore().save(result.plan);
|
|
381
|
+
console.error(`Route dry-run: ${JSON.stringify(result.counts)}`);
|
|
382
|
+
console.log(JSON.stringify({ counts: result.counts, plan: result.plan }, null, 2));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
console.error(`Route dry-run: ${result.counts.linkOperations} link(s), ${result.counts.ownerOperations} owner assignment(s), ${result.counts.ambiguousAccountMatches} ambiguous match(es).`);
|
|
386
|
+
await emitPlan(result.plan, rest);
|
|
387
|
+
}
|
|
388
|
+
export async function hierarchyCommand(args) {
|
|
389
|
+
const [subcommand, ...rest] = args;
|
|
390
|
+
if (subcommand !== "report")
|
|
391
|
+
throw new Error("Usage: fullstackgtm hierarchy report [source options] [--json|--out <path>]");
|
|
392
|
+
const snapshot = await readSnapshot(rest);
|
|
393
|
+
const report = buildAccountHierarchy(snapshot);
|
|
394
|
+
const out = option(rest, "--out");
|
|
395
|
+
const rendered = rest.includes("--json") ? `${JSON.stringify(report, null, 2)}\n` : `${accountHierarchyToMarkdown(report)}\n`;
|
|
396
|
+
if (out)
|
|
397
|
+
writeFileSync(resolve(process.cwd(), out), rendered);
|
|
398
|
+
console.log(rendered.trimEnd());
|
|
399
|
+
}
|
|
400
|
+
export async function relationshipsCommand(args) {
|
|
401
|
+
const [subcommand, ...rest] = args;
|
|
402
|
+
if (subcommand !== "account")
|
|
403
|
+
throw new Error("Usage: fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]");
|
|
404
|
+
const accountId = option(rest, "--account-id") ?? undefined;
|
|
405
|
+
const domain = option(rest, "--domain") ?? undefined;
|
|
406
|
+
if (!accountId && !domain)
|
|
407
|
+
throw new Error("relationships account needs --account-id <id> or --domain <domain>");
|
|
408
|
+
const snapshot = await readSnapshot(rest);
|
|
409
|
+
const map = buildRelationshipMap(snapshot, { accountId, domain });
|
|
410
|
+
const out = option(rest, "--out");
|
|
411
|
+
const rendered = rest.includes("--json") ? `${JSON.stringify(map, null, 2)}\n` : `${relationshipMapToMarkdown(map)}\n`;
|
|
412
|
+
if (out)
|
|
413
|
+
writeFileSync(resolve(process.cwd(), out), rendered);
|
|
414
|
+
console.log(rendered.trimEnd());
|
|
415
|
+
}
|
package/dist/cli/help.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export type HelpEntry = {
|
|
|
10
10
|
};
|
|
11
11
|
export declare const HELP: Record<string, HelpEntry>;
|
|
12
12
|
export declare const BESPOKE_HELP: string[];
|
|
13
|
+
export declare const GLOBAL_FLAGS: string[];
|
|
14
|
+
export declare const GLOBAL_SHORT_FLAGS: string[];
|
|
15
|
+
export declare const SOURCE_FLAGS: string[];
|
|
16
|
+
export declare const AUDIT_FLAGS: string[];
|
|
17
|
+
export declare const COMMAND_FLAGS: Record<string, string[]>;
|
|
18
|
+
export declare const FLAGS_WITH_VALUES: Set<string>;
|
|
13
19
|
export declare function shortUsage(): string;
|
|
14
20
|
/**
|
|
15
21
|
* Interactive-terminal styling for the short front door: a dimmed one-line
|
package/dist/cli/help.js
CHANGED
|
@@ -46,6 +46,12 @@ Usage:
|
|
|
46
46
|
fullstackgtm resolve <account|contact|deal> [--name N] [--domain D] [--email E] [--account-id A] [source options] [--json]
|
|
47
47
|
the create gate: exit 0 = safe to create, exit 2 = match
|
|
48
48
|
found (exists/ambiguous) — call before ANY record creation
|
|
49
|
+
fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save|--json|--out <path>]
|
|
50
|
+
lead-to-account matching + owner routing as a governed plan
|
|
51
|
+
fullstackgtm hierarchy report [source options] [--json|--out <path>]
|
|
52
|
+
account hierarchy view from native parents + subdomains
|
|
53
|
+
fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]
|
|
54
|
+
relationship map from contacts, deals, and activity evidence
|
|
49
55
|
fullstackgtm market init --category <name> start a market map: vendors + claim taxonomy as reviewable config
|
|
50
56
|
fullstackgtm market capture [--config <path>] [--run <label>]
|
|
51
57
|
fullstackgtm market classify [--run <label>] [--vendor <id>] [--model m] [--out <path>]
|
|
@@ -386,7 +392,28 @@ export const HELP = {
|
|
|
386
392
|
detail: "Prevention, not cleanup. Call before ANY record creation — a sync job, webhook, agent, or script. Returns exists/ambiguous/safe_to_create with matches and reasons; exit 2 = do not create.",
|
|
387
393
|
seeAlso: ["dedupe", "audit"],
|
|
388
394
|
},
|
|
395
|
+
hierarchy: {
|
|
396
|
+
summary: "account hierarchy report (native parents + subdomain inference)",
|
|
397
|
+
phase: "Prevent",
|
|
398
|
+
synopsis: ["fullstackgtm hierarchy report [source options] [--json|--out <path>]"],
|
|
399
|
+
detail: "Builds a report-only account tree from provider-native parent ids (when present in raw payloads) and deterministic subdomain inference. It surfaces duplicate-domain and ambiguous-parent conflicts instead of guessing writes.",
|
|
400
|
+
seeAlso: ["route", "relationships"],
|
|
401
|
+
},
|
|
402
|
+
relationships: {
|
|
403
|
+
summary: "relationship map for an account from contacts/deals/activity evidence",
|
|
404
|
+
phase: "Prevent",
|
|
405
|
+
synopsis: ["fullstackgtm relationships account (--account-id <id>|--domain <d>) [source options] [--json|--out <path>]"],
|
|
406
|
+
detail: "Builds a stakeholder map with inferred buyer roles, sentiment from activity subjects, open deals, and missing-role gaps. Read-only evidence surface; no CRM writes.",
|
|
407
|
+
seeAlso: ["call", "route"],
|
|
408
|
+
},
|
|
389
409
|
// Remediate — governed writes (all produce plans; nothing writes outside approve → apply)
|
|
410
|
+
route: {
|
|
411
|
+
summary: "lead-to-account matching and owner routing plan",
|
|
412
|
+
phase: "Remediate",
|
|
413
|
+
synopsis: ["fullstackgtm route leads [source options] [--match domain|company|both] [--no-inherit-owner] [--reassign-owned] [--policy <json|path>] [--save|--json|--out <path>]"],
|
|
414
|
+
detail: "Matches contact-shaped leads to accounts by domain and/or company name, inherits account owners (or applies an assignment policy), and emits every change as a dry-run patch plan for approve → apply. Ambiguous matches are surfaced, never guessed.",
|
|
415
|
+
seeAlso: ["resolve", "reassign"],
|
|
416
|
+
},
|
|
390
417
|
fix: {
|
|
391
418
|
summary: "one-shot composite: audit one rule → suggest → approve → apply",
|
|
392
419
|
phase: "Remediate",
|
|
@@ -559,15 +586,62 @@ export const HELP = {
|
|
|
559
586
|
// Verbs that print their own richer multi-subcommand help; runCli routes their
|
|
560
587
|
// `--help` to themselves, so commandHelp() only renders these via `help <verb>`.
|
|
561
588
|
export const BESPOKE_HELP = ["init", "call", "market", "tam", "enrich", "bulk-update", "schedule", "signals", "icp", "draft"];
|
|
589
|
+
export const GLOBAL_FLAGS = ["--help", "--full"];
|
|
590
|
+
export const GLOBAL_SHORT_FLAGS = ["-h"];
|
|
591
|
+
export const SOURCE_FLAGS = ["--provider", "--token-env", "--input", "--demo", "--sample", "--seed", "--today"];
|
|
592
|
+
export const AUDIT_FLAGS = ["--config", "--rules", "--stale-days", "--fail-on", "--save", "--dry-run", "--json", "--out", "--full"];
|
|
593
|
+
// Complete per-command flag registry used by runCli's fail-closed flag
|
|
594
|
+
// validation. Keep this next to HELP so focused help, machine capabilities,
|
|
595
|
+
// and the parser safety gate have one command inventory to reconcile against.
|
|
596
|
+
export const COMMAND_FLAGS = {
|
|
597
|
+
init: ["--source", "--provider", "--out", "--force"],
|
|
598
|
+
login: ["--via", "--hosted", "--private-token", "--token", "--key", "--api-key", "--client-id", "--client-secret", "--scopes", "--port", "--oauth", "--device", "--instance-url", "--login-url", "--no-validate"],
|
|
599
|
+
logout: [],
|
|
600
|
+
doctor: ["--json"],
|
|
601
|
+
capabilities: ["--json"],
|
|
602
|
+
"robot-docs": [],
|
|
603
|
+
profiles: ["--json"],
|
|
604
|
+
health: ["--json"],
|
|
605
|
+
snapshot: [...SOURCE_FLAGS, "--since", "--out", "--archive"],
|
|
606
|
+
audit: [...SOURCE_FLAGS, ...AUDIT_FLAGS],
|
|
607
|
+
report: [...SOURCE_FLAGS, ...AUDIT_FLAGS, "--plan", "--client", "--title", "--prepared-by", "--format", "--max-examples"],
|
|
608
|
+
diff: ["--before", "--after", "--config", "--stale-days", "--today", "--json", "--fail-on-new-findings"],
|
|
609
|
+
rules: ["--config", "--json"],
|
|
610
|
+
resolve: [...SOURCE_FLAGS, "--name", "--domain", "--email", "--account-id", "--json"],
|
|
611
|
+
hierarchy: [...SOURCE_FLAGS, "--json", "--out"],
|
|
612
|
+
relationships: [...SOURCE_FLAGS, "--account-id", "--domain", "--json", "--out"],
|
|
613
|
+
route: [...SOURCE_FLAGS, "--match", "--no-inherit-owner", "--reassign-owned", "--policy", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
614
|
+
fix: [...SOURCE_FLAGS, "--config", "--rule", "--provider", "--min-confidence", "--include-creates", "--yes", "--confirm", "--dry-run"],
|
|
615
|
+
"bulk-update": [...SOURCE_FLAGS, "--where", "--set", "--guard", "--require", "--create-task", "--archive", "--force-archive-duplicates", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
616
|
+
dedupe: [...SOURCE_FLAGS, "--key", "--keep", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
617
|
+
reassign: [...SOURCE_FLAGS, "--from", "--assign-unowned", "--to", "--objects", "--where", "--except-deal-stage", "--include-closed-deals", "--reason", "--max-operations", "--save", "--dry-run", "--json", "--out"],
|
|
618
|
+
backfill: [...SOURCE_FLAGS, "--since", "--pipeline", "--match-property", "--skip-unmatched", "--save", "--dry-run", "--json"],
|
|
619
|
+
enrich: [...SOURCE_FLAGS, "--config", "--source", "--icp", "--input", "--provider", "--stale-days", "--assign-owner", "--objects", "--max", "--staged-run", "--run-label", "--label", "--runs", "--save", "--dry-run", "--json", "--out"],
|
|
620
|
+
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"],
|
|
621
|
+
suggest: [...SOURCE_FLAGS, "--plan-id", "--plan", "--json", "--out"],
|
|
622
|
+
plans: ["--status", "--operations", "--value", "--values-from", "--min-confidence", "--include-creates", "--json"],
|
|
623
|
+
apply: ["--plan", "--plan-id", "--provider", "--channel", "--token-env", "--approve", "--value", "--json", "--config"],
|
|
624
|
+
"audit-log": ["--in", "--out", "--json"],
|
|
625
|
+
merge: ["--input", "--out", "--json"],
|
|
626
|
+
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"],
|
|
627
|
+
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"],
|
|
628
|
+
icp: [...SOURCE_FLAGS, "--name", "--signals-from", "--with-history", "--prompt", "--min-score", "--from-judge", "--golden", "--against-outcomes", "--min-accuracy", "--model", "--label", "--save", "--json", "--out"],
|
|
629
|
+
signals: ["--bucket", "--source", "--connector", "--connector-opt", "--watchlist", "--keywords", "--from", "--label", "--since", "--account", "--contact", "--unjudged", "--touch", "--result", "--save", "--json", "--explain"],
|
|
630
|
+
draft: ["--from-judge", "--min-score", "--prompt", "--model", "--channel", "--save", "--dry-run", "--json", "--out"],
|
|
631
|
+
schedule: ["--cron", "--label", "--provider", "--trigger", "--runs", "--json"],
|
|
632
|
+
};
|
|
633
|
+
export const FLAGS_WITH_VALUES = new Set([
|
|
634
|
+
"--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", "--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", "--scopes", "--seed", "--set", "--signals-from", "--since", "--snapshot", "--source", "--staged-run", "--stale-days", "--status", "--task-account", "--task-deal", "--title", "--to", "--today", "--token", "--token-env", "--touch", "--transcript", "--trigger", "--usd-per-credit", "--value", "--values-from", "--vendor", "--via", "--watchlist", "--where",
|
|
635
|
+
]);
|
|
562
636
|
// Lifecycle-grouped front door. One line per verb, organized by the
|
|
563
637
|
// Prevent→Detect→Remediate→Verify loop so a new user sees ~6 jobs, not 22 verbs.
|
|
564
638
|
export function shortUsage() {
|
|
565
639
|
const groups = [
|
|
566
640
|
["Get started", ["init"]],
|
|
567
641
|
["Setup & health", ["login", "logout", "doctor", "capabilities", "robot-docs", "profiles", "health"]],
|
|
568
|
-
["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules"]],
|
|
642
|
+
["Detect — read-only", ["audit", "report", "snapshot", "diff", "rules", "hierarchy", "relationships"]],
|
|
569
643
|
["Prevent — gate writes", ["resolve"]],
|
|
570
|
-
["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "enrich", "backfill"]],
|
|
644
|
+
["Remediate — governed writes", ["fix", "bulk-update", "dedupe", "reassign", "route", "enrich", "backfill"]],
|
|
571
645
|
["Calls → evidence", ["call"]],
|
|
572
646
|
["Govern — the plan/apply spine", ["suggest", "plans", "apply", "audit-log", "merge"]],
|
|
573
647
|
["Market intelligence", ["market", "tam"]],
|
package/dist/cli/suggest.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function documentedFlags(): Set<string>;
|
|
|
10
10
|
export type FlagTypo = {
|
|
11
11
|
given: string;
|
|
12
12
|
/** Human-readable correction, e.g. `--json` or `--rules missing-deal-owner`. */
|
|
13
|
-
suggestion: string;
|
|
13
|
+
suggestion: string | null;
|
|
14
14
|
/** The argv tokens that replace `given` in the corrected command. */
|
|
15
15
|
replacement: string[];
|
|
16
16
|
};
|
|
@@ -29,6 +29,7 @@ export type FlagTypo = {
|
|
|
29
29
|
* --jsn` exited 0 and printed markdown where the agent asked for JSON.
|
|
30
30
|
*/
|
|
31
31
|
export declare function detectFlagTypo(args: string[]): FlagTypo | null;
|
|
32
|
+
export declare function detectUnknownFlag(command: string, args: string[]): FlagTypo | null;
|
|
32
33
|
/** Nearest known command, derived from the same HELP table. */
|
|
33
34
|
export declare function suggestCommand(command: string): string | null;
|
|
34
35
|
/**
|
package/dist/cli/suggest.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// (exit 1) that prints the exact corrected command for the agent to run
|
|
12
12
|
// itself; this holds uniformly for read and write verbs, so a typo can never
|
|
13
13
|
// silently change what a write-shaped invocation stages.
|
|
14
|
-
import { HELP, usage } from "./help.js";
|
|
14
|
+
import { COMMAND_FLAGS, FLAGS_WITH_VALUES, GLOBAL_FLAGS, GLOBAL_SHORT_FLAGS, HELP, usage, } from "./help.js";
|
|
15
15
|
export function levenshtein(a, b) {
|
|
16
16
|
const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
17
17
|
for (let i = 0; i <= a.length; i += 1)
|
|
@@ -96,6 +96,52 @@ export function detectFlagTypo(args) {
|
|
|
96
96
|
}
|
|
97
97
|
return null;
|
|
98
98
|
}
|
|
99
|
+
function isFlagShaped(token) {
|
|
100
|
+
return token !== "-" && token.startsWith("-");
|
|
101
|
+
}
|
|
102
|
+
function suggestCommandFlag(unknown, allowed) {
|
|
103
|
+
const candidates = [...allowed].filter((flag) => flag.startsWith("--"));
|
|
104
|
+
const prefix = candidates
|
|
105
|
+
.filter((flag) => unknown.startsWith(`${flag}-`) || flag.startsWith(unknown))
|
|
106
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
107
|
+
if (prefix)
|
|
108
|
+
return prefix;
|
|
109
|
+
return nearest(unknown.toLowerCase().replace(/_/g, "-"), candidates, 3);
|
|
110
|
+
}
|
|
111
|
+
export function detectUnknownFlag(command, args) {
|
|
112
|
+
const commandFlags = COMMAND_FLAGS[command];
|
|
113
|
+
if (!commandFlags)
|
|
114
|
+
return null;
|
|
115
|
+
const allowed = new Set([...GLOBAL_FLAGS, ...commandFlags]);
|
|
116
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
117
|
+
const token = args[index];
|
|
118
|
+
if (token === "--")
|
|
119
|
+
break;
|
|
120
|
+
if (GLOBAL_SHORT_FLAGS.includes(token))
|
|
121
|
+
continue;
|
|
122
|
+
if (!token.startsWith("-"))
|
|
123
|
+
continue;
|
|
124
|
+
if (!token.startsWith("--")) {
|
|
125
|
+
return { given: token, suggestion: null, replacement: [] };
|
|
126
|
+
}
|
|
127
|
+
const equalsIndex = token.indexOf("=");
|
|
128
|
+
const flag = equalsIndex === -1 ? token : token.slice(0, equalsIndex);
|
|
129
|
+
if (allowed.has(flag)) {
|
|
130
|
+
if (equalsIndex !== -1) {
|
|
131
|
+
const value = token.slice(equalsIndex + 1);
|
|
132
|
+
const replacement = value === "" ? [flag] : [flag, value];
|
|
133
|
+
return { given: token, suggestion: replacement.join(" "), replacement };
|
|
134
|
+
}
|
|
135
|
+
if (FLAGS_WITH_VALUES.has(flag) && args[index + 1] !== undefined && !isFlagShaped(args[index + 1])) {
|
|
136
|
+
index += 1;
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const suggestion = suggestCommandFlag(flag, allowed);
|
|
141
|
+
return { given: flag, suggestion, replacement: suggestion ? [suggestion] : [] };
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
99
145
|
/** Nearest known command, derived from the same HELP table. */
|
|
100
146
|
export function suggestCommand(command) {
|
|
101
147
|
if (!command)
|
|
@@ -127,8 +173,8 @@ export function unknownFlagEnvelope(command, args, typo) {
|
|
|
127
173
|
code: "UNKNOWN_FLAG",
|
|
128
174
|
message: `Unknown flag: ${typo.given}`,
|
|
129
175
|
hints: [
|
|
130
|
-
`Did you mean: ${typo.suggestion}
|
|
131
|
-
`Try: ${correctedCommand(command, args, typo)}
|
|
176
|
+
...(typo.suggestion ? [`Did you mean: ${typo.suggestion}`] : []),
|
|
177
|
+
...(typo.replacement.length > 0 ? [`Try: ${correctedCommand(command, args, typo)}`] : []),
|
|
132
178
|
],
|
|
133
179
|
},
|
|
134
180
|
};
|