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/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@ import { activeProfile, listProfiles, setActiveProfile } from "./credentials.js"
|
|
|
2
2
|
import { capabilitiesCommand, printCommandHelpJson, robotDocsCommand, unknownCommandEnvelope } from "./cli/capabilities.js";
|
|
3
3
|
import { BESPOKE_HELP, commandHelp, HELP, shortUsage, stylizeShortUsage, usage } from "./cli/help.js";
|
|
4
4
|
import { readPackageInfo } from "./cli/shared.js";
|
|
5
|
-
import { correctedCommand, detectFlagTypo, suggestCommand, unknownFlagEnvelope } from "./cli/suggest.js";
|
|
5
|
+
import { correctedCommand, detectFlagTypo, detectUnknownFlag, suggestCommand, unknownFlagEnvelope } from "./cli/suggest.js";
|
|
6
6
|
import { colorEnabled, paint } from "./cli/ui.js";
|
|
7
7
|
// Verb modules load lazily inside their dispatch branch below. The dispatcher
|
|
8
8
|
// used to import all of them eagerly, so `--version` compiled the full
|
|
@@ -56,16 +56,22 @@ export async function runCli(argv) {
|
|
|
56
56
|
}
|
|
57
57
|
if (command === "help") {
|
|
58
58
|
const [topic] = args;
|
|
59
|
-
if (topic && topic
|
|
59
|
+
if (topic && !topic.startsWith("-") && args.includes("--json")) {
|
|
60
60
|
// `help <cmd> --json` → machine-readable help derived from the same
|
|
61
|
-
// HELP entry the plain text renders from.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
// HELP entry the plain text renders from. JSON takes precedence over
|
|
62
|
+
// --full so automated callers get a stable shape.
|
|
63
|
+
printCommandHelpJson(topic);
|
|
64
|
+
}
|
|
65
|
+
else if (args.includes("--full")) {
|
|
66
|
+
// --full is the global escape hatch to the complete reference, even
|
|
67
|
+
// when a topic is given (help <cmd> --full), unless --json is requested.
|
|
68
|
+
console.log(usage());
|
|
69
|
+
}
|
|
70
|
+
else if (topic && !topic.startsWith("-")) {
|
|
71
|
+
console.log(commandHelp(topic));
|
|
66
72
|
}
|
|
67
73
|
else {
|
|
68
|
-
console.log(
|
|
74
|
+
console.log(styledShortUsage());
|
|
69
75
|
}
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
@@ -90,20 +96,27 @@ export async function runCli(argv) {
|
|
|
90
96
|
}
|
|
91
97
|
// Flag typos used to be silently dropped by the per-verb parsers —
|
|
92
98
|
// `audit --demo --jsn` exited 0 and printed markdown where the agent asked
|
|
93
|
-
// for JSON.
|
|
94
|
-
// help.ts
|
|
95
|
-
// auto-execute the correction, so a typo can never
|
|
96
|
-
// write-shaped invocation stages.
|
|
99
|
+
// for JSON. Every flag-shaped token is now checked against the per-command
|
|
100
|
+
// registry in help.ts, so unknown flags fail closed instead of being ignored.
|
|
101
|
+
// Suggest-only: never auto-execute the correction, so a typo can never
|
|
102
|
+
// silently change what a write-shaped invocation stages.
|
|
97
103
|
if (command in HELP) {
|
|
98
|
-
const typo =
|
|
104
|
+
const typo = detectUnknownFlag(command, args);
|
|
99
105
|
if (typo) {
|
|
100
106
|
if (args.includes("--json")) {
|
|
101
107
|
console.log(JSON.stringify(unknownFlagEnvelope(command, args, typo), null, 2));
|
|
102
108
|
}
|
|
103
109
|
else {
|
|
110
|
+
console.error(`Unknown flag for ${command}: ${typo.given}`);
|
|
111
|
+
// Keep the old near-miss shape too; existing agents key off it.
|
|
104
112
|
console.error(`Unknown flag: ${typo.given}`);
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
if (typo.suggestion) {
|
|
114
|
+
console.error(`Did you mean ${typo.suggestion}?`);
|
|
115
|
+
console.error(`Did you mean: ${typo.suggestion}`);
|
|
116
|
+
if (typo.replacement.length > 0) {
|
|
117
|
+
console.error(`Try: ${correctedCommand(command, args, typo)}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
107
120
|
}
|
|
108
121
|
process.exitCode = 1;
|
|
109
122
|
return;
|
|
@@ -161,6 +174,18 @@ export async function runCli(argv) {
|
|
|
161
174
|
await (await import("./cli/fix.js")).resolveCommand(args);
|
|
162
175
|
return;
|
|
163
176
|
}
|
|
177
|
+
if (command === "route") {
|
|
178
|
+
await (await import("./cli/fix.js")).routeCommand(args);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (command === "hierarchy") {
|
|
182
|
+
await (await import("./cli/fix.js")).hierarchyCommand(args);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (command === "relationships") {
|
|
186
|
+
await (await import("./cli/fix.js")).relationshipsCommand(args);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
164
189
|
if (command === "bulk-update") {
|
|
165
190
|
await (await import("./cli/fix.js")).bulkUpdateCommand(args);
|
|
166
191
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FieldMappings } from "../mappings.ts";
|
|
2
2
|
import type { GtmConnector, SnapshotProgress } from "../types.ts";
|
|
3
3
|
import { type ProgressEmitter } from "../progress.ts";
|
|
4
|
+
export type HubspotWritableConnector = GtmConnector & Required<Pick<GtmConnector, "applyOperation" | "readField">>;
|
|
4
5
|
export type HubspotConnectorOptions = {
|
|
5
6
|
/** Returns a HubSpot access token (private app token or OAuth access token). */
|
|
6
7
|
getAccessToken: () => string | Promise<string>;
|
|
@@ -26,4 +27,4 @@ export type HubspotConnectorOptions = {
|
|
|
26
27
|
* amountless deals — so audit rules can surface the gaps instead of hiding
|
|
27
28
|
* them.
|
|
28
29
|
*/
|
|
29
|
-
export declare function createHubspotConnector(options: HubspotConnectorOptions):
|
|
30
|
+
export declare function createHubspotConnector(options: HubspotConnectorOptions): HubspotWritableConnector;
|
|
@@ -6,6 +6,7 @@ export type SalesforceConnection = {
|
|
|
6
6
|
/** e.g. https://yourorg.my.salesforce.com */
|
|
7
7
|
instanceUrl: string;
|
|
8
8
|
};
|
|
9
|
+
export type SalesforceWritableConnector = GtmConnector & Required<Pick<GtmConnector, "applyOperation" | "readField">>;
|
|
9
10
|
export type SalesforceConnectorOptions = {
|
|
10
11
|
/** Returns an access token plus the instance URL it belongs to. */
|
|
11
12
|
getConnection: () => SalesforceConnection | Promise<SalesforceConnection>;
|
|
@@ -32,4 +33,4 @@ export type SalesforceConnectorOptions = {
|
|
|
32
33
|
* surface the gaps). Probabilities are normalized to 0..1 to match the
|
|
33
34
|
* canonical model.
|
|
34
35
|
*/
|
|
35
|
-
export declare function createSalesforceConnector(options: SalesforceConnectorOptions):
|
|
36
|
+
export declare function createSalesforceConnector(options: SalesforceConnectorOptions): SalesforceWritableConnector;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { readFileSync, statSync } from "node:fs";
|
|
22
22
|
import { resolve } from "node:path";
|
|
23
|
+
import { FREE_EMAIL_DOMAINS } from "../freeEmailDomains.js";
|
|
23
24
|
import { SIGNAL_BUCKETS } from "../signals.js";
|
|
24
25
|
import { parseSpoolText, spoolFilesIn } from "../spoolFiles.js";
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
@@ -243,22 +244,12 @@ function fieldValue(values, name) {
|
|
|
243
244
|
}
|
|
244
245
|
return "";
|
|
245
246
|
}
|
|
246
|
-
const FREE_MAIL = new Set([
|
|
247
|
-
"gmail.com",
|
|
248
|
-
"yahoo.com",
|
|
249
|
-
"hotmail.com",
|
|
250
|
-
"outlook.com",
|
|
251
|
-
"icloud.com",
|
|
252
|
-
"aol.com",
|
|
253
|
-
"proton.me",
|
|
254
|
-
"protonmail.com",
|
|
255
|
-
]);
|
|
256
247
|
/** Company domain from an email, or "" for free-mail / no email. */
|
|
257
248
|
function corporateDomain(email) {
|
|
258
249
|
if (!email.includes("@"))
|
|
259
250
|
return "";
|
|
260
251
|
const domain = email.split("@").at(-1).trim().toLowerCase();
|
|
261
|
-
if (!domain ||
|
|
252
|
+
if (!domain || FREE_EMAIL_DOMAINS.has(domain))
|
|
262
253
|
return "";
|
|
263
254
|
return domain;
|
|
264
255
|
}
|
package/dist/format.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// `summary: true` renders
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// where you approve specific operations — keep every operation's detail.
|
|
1
|
+
// `summary: true` renders the compact audit view: header, findings summary,
|
|
2
|
+
// assumptions/open questions, and an operation count. Defaults to the full view
|
|
3
|
+
// so write-preview callers (bulk-update, fix, dedupe, plans show, MCP) — where
|
|
4
|
+
// you approve specific operations — keep every operation's detail.
|
|
6
5
|
export function patchPlanToMarkdown(plan, opts = {}) {
|
|
7
6
|
const lines = [
|
|
8
7
|
`# ${plan.title}`,
|
|
@@ -25,6 +24,7 @@ export function patchPlanToMarkdown(plan, opts = {}) {
|
|
|
25
24
|
});
|
|
26
25
|
lines.push("");
|
|
27
26
|
}
|
|
27
|
+
appendAssumptionsAndDecisionPoints(lines, plan);
|
|
28
28
|
if (opts.summary) {
|
|
29
29
|
const ops = plan.operations.length;
|
|
30
30
|
lines.push(ops === 0
|
|
@@ -96,6 +96,32 @@ export function formatPatchPlanRun(run) {
|
|
|
96
96
|
}
|
|
97
97
|
return `${lines.join("\n")}\n`;
|
|
98
98
|
}
|
|
99
|
+
function appendAssumptionsAndDecisionPoints(lines, plan) {
|
|
100
|
+
const assumptions = [...(plan.assumptions ?? [])].sort((left, right) => {
|
|
101
|
+
if (left.confidence === "guess" && right.confidence !== "guess")
|
|
102
|
+
return -1;
|
|
103
|
+
if (left.confidence !== "guess" && right.confidence === "guess")
|
|
104
|
+
return 1;
|
|
105
|
+
return 0;
|
|
106
|
+
});
|
|
107
|
+
const openQuestions = plan.openQuestions ?? [];
|
|
108
|
+
if (assumptions.length === 0 && openQuestions.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
lines.push("## Assumptions & decision points", "");
|
|
111
|
+
for (const assumption of assumptions) {
|
|
112
|
+
const warning = assumption.confidence === "guess" ? "⚠️ " : "";
|
|
113
|
+
lines.push(`- ${warning}${assumption.text}${assumption.source ? ` _(source: ${assumption.source})_` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
if (openQuestions.length > 0) {
|
|
116
|
+
if (assumptions.length > 0)
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push("### Open questions", "");
|
|
119
|
+
for (const question of openQuestions) {
|
|
120
|
+
lines.push(`- ${question}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
99
125
|
function formatValue(value) {
|
|
100
126
|
if (value === undefined || value === null || value === "")
|
|
101
127
|
return "unset";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const FREE_EMAIL_DOMAINS: Set<string>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Shared free-mail domains that should never be treated as company domains.
|
|
2
|
+
export const FREE_EMAIL_DOMAINS = new Set([
|
|
3
|
+
"gmail.com",
|
|
4
|
+
"googlemail.com",
|
|
5
|
+
"yahoo.com",
|
|
6
|
+
"outlook.com",
|
|
7
|
+
"hotmail.com",
|
|
8
|
+
"live.com",
|
|
9
|
+
"icloud.com",
|
|
10
|
+
"me.com",
|
|
11
|
+
"aol.com",
|
|
12
|
+
"proton.me",
|
|
13
|
+
"protonmail.com",
|
|
14
|
+
]);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CanonicalGtmSnapshot } from "./types.ts";
|
|
2
|
+
export type AccountHierarchyNode = {
|
|
3
|
+
accountId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
domain?: string;
|
|
6
|
+
parentAccountId?: string;
|
|
7
|
+
parentReason?: string;
|
|
8
|
+
children: AccountHierarchyNode[];
|
|
9
|
+
};
|
|
10
|
+
export type AccountHierarchyConflict = {
|
|
11
|
+
type: "duplicate_domain" | "ambiguous_parent" | "cycle";
|
|
12
|
+
accountIds: string[];
|
|
13
|
+
key: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
};
|
|
16
|
+
export type AccountHierarchyReport = {
|
|
17
|
+
generatedAt: string;
|
|
18
|
+
roots: AccountHierarchyNode[];
|
|
19
|
+
orphanAccounts: string[];
|
|
20
|
+
conflicts: AccountHierarchyConflict[];
|
|
21
|
+
counts: {
|
|
22
|
+
accounts: number;
|
|
23
|
+
roots: number;
|
|
24
|
+
linkedChildren: number;
|
|
25
|
+
conflicts: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export declare function buildAccountHierarchy(snapshot: CanonicalGtmSnapshot): AccountHierarchyReport;
|
|
29
|
+
export declare function accountHierarchyToMarkdown(report: AccountHierarchyReport): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { normalizeDomain } from "./merge.js";
|
|
2
|
+
function rawString(account, keys) {
|
|
3
|
+
if (!account.raw || typeof account.raw !== "object")
|
|
4
|
+
return undefined;
|
|
5
|
+
const raw = account.raw;
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = raw[key];
|
|
8
|
+
if (typeof value === "string" && value.trim())
|
|
9
|
+
return value.trim();
|
|
10
|
+
if (value && typeof value === "object") {
|
|
11
|
+
const nested = value;
|
|
12
|
+
for (const nestedKey of ["id", "value", "name"]) {
|
|
13
|
+
if (typeof nested[nestedKey] === "string" && String(nested[nestedKey]).trim())
|
|
14
|
+
return String(nested[nestedKey]).trim();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
function parentHint(account) {
|
|
21
|
+
return rawString(account, ["parentAccountId", "parent_account_id", "ParentId", "parentCompanyId", "hs_parent_company_id"]);
|
|
22
|
+
}
|
|
23
|
+
function cloneNode(account) {
|
|
24
|
+
return { accountId: String(account.id), name: account.name, ...(account.domain ? { domain: account.domain } : {}), children: [] };
|
|
25
|
+
}
|
|
26
|
+
function looksLikePublicSuffix(domain) {
|
|
27
|
+
const parts = domain.split(".");
|
|
28
|
+
if (parts.length !== 2)
|
|
29
|
+
return false;
|
|
30
|
+
const [label, tld] = parts;
|
|
31
|
+
return tld.length === 2 && ["ac", "co", "com", "edu", "gov", "net", "org"].includes(label);
|
|
32
|
+
}
|
|
33
|
+
function domainParent(childDomain, domains) {
|
|
34
|
+
const parts = childDomain.split(".");
|
|
35
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
36
|
+
const candidate = parts.slice(i).join(".");
|
|
37
|
+
if (candidate.split(".").length < 2 || looksLikePublicSuffix(candidate))
|
|
38
|
+
continue;
|
|
39
|
+
const unique = new Map((domains.get(candidate) ?? []).map((account) => [String(account.id), account]));
|
|
40
|
+
if (unique.size > 1)
|
|
41
|
+
return "ambiguous";
|
|
42
|
+
if (unique.size === 1)
|
|
43
|
+
return [...unique.values()][0];
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function detectCycles(parentByChild) {
|
|
48
|
+
const cyclic = new Set();
|
|
49
|
+
const cycles = [];
|
|
50
|
+
const emitted = new Set();
|
|
51
|
+
for (const start of parentByChild.keys()) {
|
|
52
|
+
const path = [];
|
|
53
|
+
const indexById = new Map();
|
|
54
|
+
let cursor = start;
|
|
55
|
+
while (cursor && parentByChild.has(cursor)) {
|
|
56
|
+
const seenAt = indexById.get(cursor);
|
|
57
|
+
if (seenAt !== undefined) {
|
|
58
|
+
const cycle = path.slice(seenAt);
|
|
59
|
+
for (const id of cycle)
|
|
60
|
+
cyclic.add(id);
|
|
61
|
+
const key = [...cycle].sort().join("|");
|
|
62
|
+
if (!emitted.has(key)) {
|
|
63
|
+
emitted.add(key);
|
|
64
|
+
cycles.push(cycle);
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
indexById.set(cursor, path.length);
|
|
69
|
+
path.push(cursor);
|
|
70
|
+
cursor = parentByChild.get(cursor)?.parentId;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { accountIds: cyclic, cycles };
|
|
74
|
+
}
|
|
75
|
+
export function buildAccountHierarchy(snapshot) {
|
|
76
|
+
const byId = new Map(snapshot.accounts.map((account) => [String(account.id), account]));
|
|
77
|
+
const accounts = [...byId.values()];
|
|
78
|
+
const domains = new Map();
|
|
79
|
+
for (const account of accounts) {
|
|
80
|
+
const domain = normalizeDomain(account.domain);
|
|
81
|
+
if (domain)
|
|
82
|
+
domains.set(domain, [...(domains.get(domain) ?? []), account]);
|
|
83
|
+
}
|
|
84
|
+
const conflicts = [];
|
|
85
|
+
for (const [domain, accounts] of domains) {
|
|
86
|
+
const ids = [...new Set(accounts.map((account) => String(account.id)))];
|
|
87
|
+
if (ids.length > 1)
|
|
88
|
+
conflicts.push({ type: "duplicate_domain", accountIds: ids.sort(), key: domain, detail: `${ids.length} accounts share domain ${domain}; hierarchy inference will not choose between them.` });
|
|
89
|
+
}
|
|
90
|
+
const nodes = new Map(accounts.map((account) => [String(account.id), cloneNode(account)]));
|
|
91
|
+
const parentByChild = new Map();
|
|
92
|
+
for (const account of accounts) {
|
|
93
|
+
const id = String(account.id);
|
|
94
|
+
const hint = parentHint(account);
|
|
95
|
+
if (hint && byId.has(hint) && hint !== id) {
|
|
96
|
+
parentByChild.set(id, { parentId: hint, reason: "provider-parent" });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const domain = normalizeDomain(account.domain);
|
|
100
|
+
if (!domain)
|
|
101
|
+
continue;
|
|
102
|
+
const inferred = domainParent(domain, domains);
|
|
103
|
+
if (inferred === "ambiguous")
|
|
104
|
+
conflicts.push({ type: "ambiguous_parent", accountIds: [id], key: domain, detail: `Multiple possible parent accounts found for ${domain}; leaving account as a root.` });
|
|
105
|
+
else if (inferred && String(inferred.id) !== id)
|
|
106
|
+
parentByChild.set(id, { parentId: String(inferred.id), reason: "domain-subdomain" });
|
|
107
|
+
}
|
|
108
|
+
const cyclicAccounts = detectCycles(parentByChild);
|
|
109
|
+
for (const cycle of cyclicAccounts.cycles) {
|
|
110
|
+
const ordered = [...cycle].sort();
|
|
111
|
+
const key = ordered.join(" -> ");
|
|
112
|
+
conflicts.push({
|
|
113
|
+
type: "cycle",
|
|
114
|
+
accountIds: ordered,
|
|
115
|
+
key,
|
|
116
|
+
detail: `Cyclic parent hints detected among ${ordered.join(", ")}; leaving those accounts as roots instead of hiding them in a loop.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
for (const accountId of cyclicAccounts.accountIds)
|
|
120
|
+
parentByChild.delete(accountId);
|
|
121
|
+
const roots = [];
|
|
122
|
+
const orphanAccounts = [];
|
|
123
|
+
const linkedChildren = new Set();
|
|
124
|
+
for (const account of accounts) {
|
|
125
|
+
const id = String(account.id);
|
|
126
|
+
const node = nodes.get(id);
|
|
127
|
+
const parent = parentByChild.get(id);
|
|
128
|
+
if (parent && nodes.has(parent.parentId)) {
|
|
129
|
+
node.parentAccountId = parent.parentId;
|
|
130
|
+
node.parentReason = parent.reason;
|
|
131
|
+
nodes.get(parent.parentId).children.push(node);
|
|
132
|
+
linkedChildren.add(id);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
roots.push(node);
|
|
136
|
+
if (!account.domain && !parentHint(account))
|
|
137
|
+
orphanAccounts.push(id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const sortTree = (list) => {
|
|
141
|
+
list.sort((a, b) => a.name.localeCompare(b.name) || a.accountId.localeCompare(b.accountId));
|
|
142
|
+
for (const node of list)
|
|
143
|
+
sortTree(node.children);
|
|
144
|
+
};
|
|
145
|
+
sortTree(roots);
|
|
146
|
+
return { generatedAt: snapshot.generatedAt, roots, orphanAccounts: orphanAccounts.sort(), conflicts, counts: { accounts: accounts.length, roots: roots.length, linkedChildren: linkedChildren.size, conflicts: conflicts.length } };
|
|
147
|
+
}
|
|
148
|
+
export function accountHierarchyToMarkdown(report) {
|
|
149
|
+
const lines = ["# Account hierarchy", "", `${report.counts.accounts} account(s), ${report.counts.roots} root(s), ${report.counts.linkedChildren} inferred child link(s), ${report.counts.conflicts} conflict(s).`, ""];
|
|
150
|
+
const walk = (nodes, depth) => {
|
|
151
|
+
for (const node of nodes) {
|
|
152
|
+
const suffix = [node.domain, node.parentReason ? `via ${node.parentReason}` : undefined].filter(Boolean).join("; ");
|
|
153
|
+
lines.push(`${" ".repeat(depth)}- ${node.name} (${node.accountId}${suffix ? ` — ${suffix}` : ""})`);
|
|
154
|
+
walk(node.children, depth + 1);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
walk(report.roots, 0);
|
|
158
|
+
if (report.conflicts.length) {
|
|
159
|
+
lines.push("", "## Conflicts");
|
|
160
|
+
for (const conflict of report.conflicts)
|
|
161
|
+
lines.push(`- ${conflict.type} ${conflict.key}: ${conflict.detail}`);
|
|
162
|
+
}
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,9 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, type Assign
|
|
|
3
3
|
export { buildBulkUpdatePlan, isFilterableField, parseWhere, type BulkUpdateOptions } from "./bulkUpdate.ts";
|
|
4
4
|
export { buildDedupePlan, dedupeKey, type DedupeOptions } from "./dedupe.ts";
|
|
5
5
|
export { buildReassignPlans, type ReassignObjectType, type ReassignOptions } from "./reassign.ts";
|
|
6
|
+
export { buildLeadRoutePlan, type LeadRouteCounts, type LeadRouteOptions, type LeadRouteResult, } from "./route.ts";
|
|
7
|
+
export { accountHierarchyToMarkdown, buildAccountHierarchy, type AccountHierarchyConflict, type AccountHierarchyNode, type AccountHierarchyReport, } from "./hierarchy.ts";
|
|
8
|
+
export { buildRelationshipMap, relationshipMapToMarkdown, type RelationshipMap, type StakeholderNode, type StakeholderRole, type StakeholderSentiment, } from "./relationships.ts";
|
|
6
9
|
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, type FullstackgtmConfig, type LoadedConfig, } from "./config.ts";
|
|
7
10
|
export { applyPatchPlan, type ApplyPatchPlanOptions } from "./connector.ts";
|
|
8
11
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, type ProgressEmitter, type ProgressEvent, type ProgressListener, type ProgressSnapshot, } from "./progress.ts";
|
|
@@ -23,11 +26,12 @@ export { mergeSnapshots, type MergeConflict, type MergeMatch, type MergeReport,
|
|
|
23
26
|
export { createFilePlanStore, type PlanStore, type StoredPlan } from "./planStore.ts";
|
|
24
27
|
export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, type ApprovalVerification, } from "./integrity.ts";
|
|
25
28
|
export { buildAuditLog, verifyAuditLog, type AuditLogEntry, type AuditLogExport, type AuditLogVerification, } from "./auditLog.ts";
|
|
29
|
+
export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.ts";
|
|
26
30
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.ts";
|
|
27
31
|
export { computeHealth, summarizeHealth, healthToMarkdown, type HealthEntry, type HealthRollup, type HealthRuleDelta, } from "./health.ts";
|
|
28
32
|
export { auditReportToHtml, auditReportToMarkdown, type ReportOptions } from "./report.ts";
|
|
29
33
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, type CrmObjectType, type FieldMappings, } from "./mappings.ts";
|
|
30
|
-
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
34
|
+
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.ts";
|
|
31
35
|
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, type CallDealSuggestion, type CallInsightType, type ExtractedCallInsight, type ParsedCall, type ParsedTranscriptSegment, } from "./calls.ts";
|
|
32
36
|
export { sampleSnapshot } from "./sampleData.ts";
|
|
33
37
|
export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, type CallScorecard, type LlmCallClassification, type LlmCredential, type LlmExtractedInsight, type LlmProvider, type Rubric, type RubricDimension, type ScoreBand, type ScoredDimension, } from "./llm.ts";
|
|
@@ -48,4 +52,4 @@ export { fetchAtsJobs, snippetFor, type AtsBoardSource, type AtsJob, } from "./c
|
|
|
48
52
|
export { scoreBand, judgeRunId, normalizeSpan, isGroundedSpan, scoreAccount, accountRecentlyTouched, bestContactForAccount, deterministicDecision, deterministicWhyNow, JUDGE_SCHEMA, DEFAULT_JUDGE_PROMPT, judgeDecisionLlm, judgeSignals, judgeDir, createFileJudgeStore, type JudgeDecisionKind, type JudgeDecision, type JudgeRun, type JudgeBand, type AccountScore, type JudgeStore, } from "./judge.ts";
|
|
49
53
|
export { draft, authorOpeners, draftOpenerLlm, deterministicOpener, creditedTriggerSignal, draftEvidence, sanitizeOpener, firstLine, firstLineGroundedInTrigger, hasBannedGreeting, isStaleTrigger, draftOperationId, draftEvidenceId, DRAFT_CHANNELS, DRAFT_SCHEMA, DEFAULT_DRAFT_PROMPT, DEFAULT_FRESHNESS_DAYS, type Draft, type RejectedDraft, type DraftResult, type DraftChannel, } from "./draft.ts";
|
|
50
54
|
export { DEFAULT_GOLDEN_SET, DEFAULT_MIN_ACCURACY, HOT_SCORE, defaultJudgeFn, gradeAgainstOutcomes, gradeJudge, parseGoldenSet, type Confusion, type EvalJudgeFn, type GoldenHistory, type GoldenRow, type GradeResult, type OutcomeCalibration, } from "./judgeEval.ts";
|
|
51
|
-
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
|
55
|
+
export type { ApprovalStatus, AuditFinding, AuditFindingSeverity, CanonicalAccount, CanonicalActivity, CanonicalContact, CanonicalDeal, CanonicalGtmSnapshot, CanonicalUser, CrmProvider, GtmAuditRule, GtmConnector, GtmEvidence, GtmEvidenceSourceSystem, GtmObjectType, GtmPolicy, GtmRuleContext, GtmRuleResult, GtmSnapshotIndex, PatchOperation, PatchOperationResult, PatchOperationType, PatchPlan, PatchPlanAssumption, PatchPlanAssumptionConfidence, PatchPlanRun, PatchPlanRunStatus, PatchVerification, PipelineFinding, PipelineFindingStatus, PipelineFindingType, ProviderIdentity, RiskLevel, SourceFreshness, } from "./types.ts";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@ export { matchesTerritory, parseAssignmentPolicy, resolveAssignment, } from "./a
|
|
|
3
3
|
export { buildBulkUpdatePlan, isFilterableField, parseWhere } from "./bulkUpdate.js";
|
|
4
4
|
export { buildDedupePlan, dedupeKey } from "./dedupe.js";
|
|
5
5
|
export { buildReassignPlans } from "./reassign.js";
|
|
6
|
+
export { buildLeadRoutePlan, } from "./route.js";
|
|
7
|
+
export { accountHierarchyToMarkdown, buildAccountHierarchy, } from "./hierarchy.js";
|
|
8
|
+
export { buildRelationshipMap, relationshipMapToMarkdown, } from "./relationships.js";
|
|
6
9
|
export { CONFIG_FILE_NAME, loadConfig, mergePolicy, resolveConfiguredRules, } from "./config.js";
|
|
7
10
|
export { applyPatchPlan } from "./connector.js";
|
|
8
11
|
export { APPLY_STAGES, BACKFILL_STRIPE_STAGES, composeListeners, createProgressEmitter, CRM_SYNC_STAGES, nullProgressEmitter, SNAPSHOT_PULL_STAGES, STRIPE_SNAPSHOT_STAGES, } from "./progress.js";
|
|
@@ -23,11 +26,12 @@ export { mergeSnapshots, } from "./merge.js";
|
|
|
23
26
|
export { createFilePlanStore } from "./planStore.js";
|
|
24
27
|
export { computeApprovalDigests, loadOrCreateSigningKey, loadSigningKey, signApproval, verifyApprovalDigests, } from "./integrity.js";
|
|
25
28
|
export { buildAuditLog, verifyAuditLog, } from "./auditLog.js";
|
|
29
|
+
export { FREE_EMAIL_DOMAINS } from "./freeEmailDomains.js";
|
|
26
30
|
export { formatPatchPlanRun, patchPlanToMarkdown } from "./format.js";
|
|
27
31
|
export { computeHealth, summarizeHealth, healthToMarkdown, } from "./health.js";
|
|
28
32
|
export { auditReportToHtml, auditReportToMarkdown } from "./report.js";
|
|
29
33
|
export { HUBSPOT_DEFAULT_FIELD_MAPPINGS, SALESFORCE_DEFAULT_FIELD_MAPPINGS, mappedField, mappedFields, normalizeFieldMappings, readMappedValue, } from "./mappings.js";
|
|
30
|
-
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
34
|
+
export { accountSingleSourceRule, activeDealAccountWithoutContactsRule, auditFindingId, buildSnapshotIndex, builtinAuditRules, closingSoonInactiveRule, isFindingsOnlyRule, duplicateAccountDomainRule, duplicateContactEmailRule, duplicateOpenDealRule, missingDealAccountRule, missingDealAmountRule, missingDealOwnerRule, orphanAccountRule, pastCloseDateRule, patchOperationId, provenanceSummary, requiresHumanInput, staleDealRule, } from "./rules.js";
|
|
31
35
|
export { extractCallInsights, normalizeTranscript, parseCall, parseTranscript, suggestCallDeal, summarizeInsights, } from "./calls.js";
|
|
32
36
|
export { sampleSnapshot } from "./sampleData.js";
|
|
33
37
|
export { DEFAULT_MODELS, DEFAULT_RUBRIC, classifyCallLlm, detectProviderFromKey, extractInsightsLlm, forcedToolCall, parseRubric, resolveLlmCredential, scoreCallLlm, validateLlmKey, } from "./llm.js";
|
package/dist/mcp.js
CHANGED
|
@@ -129,6 +129,9 @@ function packageVersion() {
|
|
|
129
129
|
return "0.0.0";
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
function strictSchema(shape) {
|
|
133
|
+
return z.object(shape).strict();
|
|
134
|
+
}
|
|
132
135
|
// Single registration table. startMcpServer() registers exactly this list,
|
|
133
136
|
// and fullstackgtm_capabilities reports its names from the same array — the
|
|
134
137
|
// advertised inventory cannot drift from what is actually registered.
|
|
@@ -143,7 +146,7 @@ const toolDefinitions = [
|
|
|
143
146
|
description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
|
|
144
147
|
"Sources: the realistic zero-credential demo CRM (provider: \"demo\" — richest test data), " +
|
|
145
148
|
"the minimal sample dataset, a snapshot file, or a live provider.",
|
|
146
|
-
inputSchema: {
|
|
149
|
+
inputSchema: strictSchema({
|
|
147
150
|
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
148
151
|
inputPath: z.string().optional(),
|
|
149
152
|
configPath: z.string().optional(),
|
|
@@ -151,7 +154,7 @@ const toolDefinitions = [
|
|
|
151
154
|
output: z.enum(["json", "markdown"]).optional(),
|
|
152
155
|
today: z.string().optional(),
|
|
153
156
|
staleDealDays: z.number().int().positive().optional(),
|
|
154
|
-
},
|
|
157
|
+
}),
|
|
155
158
|
},
|
|
156
159
|
handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
|
|
157
160
|
const loaded = configPath ? loadConfig(configPath) : null;
|
|
@@ -176,11 +179,11 @@ const toolDefinitions = [
|
|
|
176
179
|
description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
|
|
177
180
|
"evidence (account-name matching, contact associations), with confidence levels and " +
|
|
178
181
|
"reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
|
|
179
|
-
inputSchema: {
|
|
182
|
+
inputSchema: strictSchema({
|
|
180
183
|
planPath: z.string(),
|
|
181
184
|
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
182
185
|
inputPath: z.string().optional(),
|
|
183
|
-
},
|
|
186
|
+
}),
|
|
184
187
|
},
|
|
185
188
|
handler: async ({ planPath, provider, inputPath }) => {
|
|
186
189
|
const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
|
|
@@ -198,14 +201,14 @@ const toolDefinitions = [
|
|
|
198
201
|
"uses LLM extraction when an Anthropic/OpenAI key is configured in the server " +
|
|
199
202
|
"environment or credential store, else the free deterministic keyword baseline; " +
|
|
200
203
|
"'llm' and 'deterministic' force either. Read-only; every insight is provenance-marked.",
|
|
201
|
-
inputSchema: {
|
|
204
|
+
inputSchema: strictSchema({
|
|
202
205
|
transcript: z.string().optional(),
|
|
203
206
|
transcriptPath: z.string().optional(),
|
|
204
207
|
title: z.string().optional(),
|
|
205
208
|
source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
|
|
206
209
|
extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
|
|
207
210
|
model: z.string().optional(),
|
|
208
|
-
},
|
|
211
|
+
}),
|
|
209
212
|
},
|
|
210
213
|
handler: async ({ transcript, transcriptPath, title, source, extractor, model }) => {
|
|
211
214
|
const raw = transcript ??
|
|
@@ -238,7 +241,7 @@ const toolDefinitions = [
|
|
|
238
241
|
"(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
|
|
239
242
|
"identity keys as the audit/merge engines (account domain, contact email, open-deal " +
|
|
240
243
|
"key). Read-only. Never create on 'exists' or 'ambiguous'.",
|
|
241
|
-
inputSchema: {
|
|
244
|
+
inputSchema: strictSchema({
|
|
242
245
|
objectType: z.enum(["account", "contact", "deal"]),
|
|
243
246
|
name: z.string().optional(),
|
|
244
247
|
domain: z.string().optional(),
|
|
@@ -246,7 +249,7 @@ const toolDefinitions = [
|
|
|
246
249
|
accountId: z.string().optional(),
|
|
247
250
|
provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
|
|
248
251
|
inputPath: z.string().optional(),
|
|
249
|
-
},
|
|
252
|
+
}),
|
|
250
253
|
},
|
|
251
254
|
handler: async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
|
|
252
255
|
const snapshot = await readSnapshot(provider, inputPath);
|
|
@@ -259,7 +262,7 @@ const toolDefinitions = [
|
|
|
259
262
|
config: {
|
|
260
263
|
title: "List Audit Rules",
|
|
261
264
|
description: "List the built-in deterministic audit rules with ids and descriptions.",
|
|
262
|
-
inputSchema: {},
|
|
265
|
+
inputSchema: strictSchema({}),
|
|
263
266
|
},
|
|
264
267
|
handler: async () => {
|
|
265
268
|
return content(builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })));
|
|
@@ -277,13 +280,13 @@ const toolDefinitions = [
|
|
|
277
280
|
"the store's HMAC approval digests — ids not approved with `plans approve` are " +
|
|
278
281
|
"refused — and the run is recorded onto the stored plan so `plans show` and " +
|
|
279
282
|
"`audit-log export` include it.",
|
|
280
|
-
inputSchema: {
|
|
283
|
+
inputSchema: strictSchema({
|
|
281
284
|
provider: z.enum(["hubspot", "salesforce"]),
|
|
282
285
|
planPath: z.string(),
|
|
283
286
|
approvedOperationIds: z.array(z.string()).min(1),
|
|
284
287
|
valueOverrides: z.record(z.string(), z.string()).optional(),
|
|
285
288
|
output: z.enum(["json", "markdown"]).optional(),
|
|
286
|
-
},
|
|
289
|
+
}),
|
|
287
290
|
},
|
|
288
291
|
handler: async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
|
|
289
292
|
// The file may be a raw PatchPlan (audit --out) or a StoredPlan envelope
|
|
@@ -400,11 +403,11 @@ const toolDefinitions = [
|
|
|
400
403
|
"and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
|
|
401
404
|
"ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
|
|
402
405
|
"character against the captures, so never paraphrase.",
|
|
403
|
-
inputSchema: {
|
|
406
|
+
inputSchema: strictSchema({
|
|
404
407
|
vendorId: z.string(),
|
|
405
408
|
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
406
409
|
captureRun: z.string().optional(),
|
|
407
|
-
},
|
|
410
|
+
}),
|
|
408
411
|
},
|
|
409
412
|
handler: async ({ vendorId, configPath, captureRun }) => {
|
|
410
413
|
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
@@ -420,10 +423,10 @@ const toolDefinitions = [
|
|
|
420
423
|
"Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
|
|
421
424
|
"span against the stored capture it cites. Returns problems if rejected; nothing is " +
|
|
422
425
|
"stored unless the whole set passes. Observations are append-only — use a new runLabel.",
|
|
423
|
-
inputSchema: {
|
|
426
|
+
inputSchema: strictSchema({
|
|
424
427
|
observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
|
|
425
428
|
configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
|
|
426
|
-
},
|
|
429
|
+
}),
|
|
427
430
|
},
|
|
428
431
|
handler: async ({ observationsPath, configPath }) => {
|
|
429
432
|
const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
|
|
@@ -454,6 +457,7 @@ toolDefinitions.unshift({
|
|
|
454
457
|
description: "Machine-readable contract for this MCP server: the tool inventory (derived from the " +
|
|
455
458
|
"server's own registration table, with per-tool CRM-write flags), safety defaults, and " +
|
|
456
459
|
"the CLI entrypoints for everything the tools don't cover.",
|
|
460
|
+
inputSchema: strictSchema({}),
|
|
457
461
|
},
|
|
458
462
|
handler: async () => content({
|
|
459
463
|
ok: true,
|