fullstackgtm 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +185 -1
  2. package/README.md +19 -7
  3. package/dist/audit.d.ts +3 -1
  4. package/dist/audit.js +29 -2
  5. package/dist/cli/audit.d.ts +15 -0
  6. package/dist/cli/audit.js +392 -0
  7. package/dist/cli/auth.d.ts +80 -0
  8. package/dist/cli/auth.js +500 -0
  9. package/dist/cli/call.d.ts +1 -0
  10. package/dist/cli/call.js +373 -0
  11. package/dist/cli/capabilities.d.ts +30 -0
  12. package/dist/cli/capabilities.js +197 -0
  13. package/dist/cli/draft.d.ts +7 -0
  14. package/dist/cli/draft.js +87 -0
  15. package/dist/cli/enrich.d.ts +8 -0
  16. package/dist/cli/enrich.js +788 -0
  17. package/dist/cli/fix.d.ts +33 -0
  18. package/dist/cli/fix.js +344 -0
  19. package/dist/cli/help.d.ts +25 -0
  20. package/dist/cli/help.js +609 -0
  21. package/dist/cli/icp.d.ts +7 -0
  22. package/dist/cli/icp.js +229 -0
  23. package/dist/cli/init.d.ts +7 -0
  24. package/dist/cli/init.js +58 -0
  25. package/dist/cli/market.d.ts +1 -0
  26. package/dist/cli/market.js +391 -0
  27. package/dist/cli/plans.d.ts +5 -0
  28. package/dist/cli/plans.js +454 -0
  29. package/dist/cli/schedule.d.ts +8 -0
  30. package/dist/cli/schedule.js +479 -0
  31. package/dist/cli/shared.d.ts +70 -0
  32. package/dist/cli/shared.js +331 -0
  33. package/dist/cli/signals.d.ts +7 -0
  34. package/dist/cli/signals.js +412 -0
  35. package/dist/cli/suggest.d.ts +49 -0
  36. package/dist/cli/suggest.js +135 -0
  37. package/dist/cli/tam.d.ts +9 -0
  38. package/dist/cli/tam.js +387 -0
  39. package/dist/cli/ui.d.ts +121 -0
  40. package/dist/cli/ui.js +375 -0
  41. package/dist/cli.d.ts +1 -57
  42. package/dist/cli.js +100 -5130
  43. package/dist/connector.d.ts +15 -0
  44. package/dist/connector.js +35 -0
  45. package/dist/connectors/hubspot.d.ts +3 -1
  46. package/dist/connectors/hubspot.js +10 -3
  47. package/dist/connectors/salesforce.d.ts +3 -1
  48. package/dist/connectors/salesforce.js +12 -5
  49. package/dist/connectors/signalSources.js +7 -59
  50. package/dist/icp.d.ts +15 -11
  51. package/dist/icp.js +19 -36
  52. package/dist/judge.d.ts +2 -0
  53. package/dist/judge.js +6 -0
  54. package/dist/marketClassify.d.ts +2 -0
  55. package/dist/marketClassify.js +7 -1
  56. package/dist/mcp-bin.js +2 -2
  57. package/dist/mcp.js +259 -166
  58. package/dist/schedule.d.ts +80 -2
  59. package/dist/schedule.js +254 -1
  60. package/dist/spoolFiles.d.ts +44 -0
  61. package/dist/spoolFiles.js +114 -0
  62. package/dist/types.d.ts +11 -0
  63. package/docs/api.md +73 -7
  64. package/docs/signal-spool-format.md +13 -0
  65. package/llms.txt +15 -6
  66. package/package.json +1 -1
  67. package/skills/fullstackgtm/SKILL.md +1 -1
  68. package/src/audit.ts +27 -1
  69. package/src/cli/audit.ts +447 -0
  70. package/src/cli/auth.ts +549 -0
  71. package/src/cli/call.ts +398 -0
  72. package/src/cli/capabilities.ts +215 -0
  73. package/src/cli/draft.ts +101 -0
  74. package/src/cli/enrich.ts +885 -0
  75. package/src/cli/fix.ts +372 -0
  76. package/src/cli/help.ts +664 -0
  77. package/src/cli/icp.ts +265 -0
  78. package/src/cli/init.ts +65 -0
  79. package/src/cli/market.ts +423 -0
  80. package/src/cli/plans.ts +523 -0
  81. package/src/cli/schedule.ts +526 -0
  82. package/src/cli/shared.ts +375 -0
  83. package/src/cli/signals.ts +434 -0
  84. package/src/cli/suggest.ts +151 -0
  85. package/src/cli/tam.ts +435 -0
  86. package/src/cli/ui.ts +426 -0
  87. package/src/cli.ts +99 -5829
  88. package/src/connector.ts +46 -0
  89. package/src/connectors/hubspot.ts +14 -2
  90. package/src/connectors/salesforce.ts +18 -2
  91. package/src/connectors/signalSources.ts +7 -56
  92. package/src/icp.ts +19 -35
  93. package/src/judge.ts +7 -0
  94. package/src/marketClassify.ts +8 -1
  95. package/src/mcp-bin.ts +2 -2
  96. package/src/mcp.ts +130 -57
  97. package/src/schedule.ts +310 -3
  98. package/src/spoolFiles.ts +116 -0
  99. package/src/types.ts +12 -0
package/dist/mcp.js CHANGED
@@ -42,6 +42,8 @@ const { McpServer } = await importPeer("@modelcontextprotocol/sdk/server/mcp.js"
42
42
  const { StdioServerTransport } = await importPeer("@modelcontextprotocol/sdk/server/stdio.js");
43
43
  const { z } = await importPeer("zod/v4");
44
44
  import { auditSnapshot, defaultPolicy } from "./audit.js";
45
+ import { assertCanonicalSnapshot } from "./cli/shared.js";
46
+ import { nearest } from "./cli/suggest.js";
45
47
  import { loadConfig, mergePolicy, resolveConfiguredRules } from "./config.js";
46
48
  import { applyPatchPlan } from "./connector.js";
47
49
  import { createHubspotConnector } from "./connectors/hubspot.js";
@@ -101,7 +103,9 @@ async function connectorFor(provider) {
101
103
  }
102
104
  return createStripeConnector({ getApiKey: () => key });
103
105
  }
104
- throw new Error(`Unknown provider: ${provider}. Supported providers: hubspot, salesforce, stripe`);
106
+ const providerSuggestion = nearest(provider.toLowerCase(), ["hubspot", "salesforce", "stripe"], 3);
107
+ throw new Error(`Unknown provider: ${provider}.${providerSuggestion ? ` Did you mean ${providerSuggestion}?` : ""} ` +
108
+ "Supported providers: hubspot, salesforce, stripe");
105
109
  }
106
110
  async function readSnapshot(provider, inputPath) {
107
111
  if (provider === "demo")
@@ -112,7 +116,7 @@ async function readSnapshot(provider, inputPath) {
112
116
  }
113
117
  if (!inputPath)
114
118
  return sampleSnapshot;
115
- return JSON.parse(readFileSync(resolve(process.cwd(), inputPath), "utf8"));
119
+ return assertCanonicalSnapshot(JSON.parse(readFileSync(resolve(process.cwd(), inputPath), "utf8")), inputPath);
116
120
  }
117
121
  function packageVersion() {
118
122
  try {
@@ -123,179 +127,268 @@ function packageVersion() {
123
127
  return "0.0.0";
124
128
  }
125
129
  }
126
- export async function startMcpServer() {
127
- const server = new McpServer({
128
- name: "fullstackgtm",
129
- version: packageVersion(),
130
- });
131
- server.registerTool("fullstackgtm_audit", {
132
- title: "GTM Ops Audit",
133
- description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
134
- "Sources: the realistic zero-credential demo CRM (provider: \"demo\" — richest test data), " +
135
- "the minimal sample dataset, a snapshot file, or a live provider.",
136
- inputSchema: {
137
- provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
138
- inputPath: z.string().optional(),
139
- configPath: z.string().optional(),
140
- rules: z.array(z.string()).optional(),
141
- output: z.enum(["json", "markdown"]).optional(),
142
- today: z.string().optional(),
143
- staleDealDays: z.number().int().positive().optional(),
130
+ // Single registration table. startMcpServer() registers exactly this list,
131
+ // and fullstackgtm_capabilities reports its names from the same array — the
132
+ // advertised inventory cannot drift from what is actually registered.
133
+ // (A hand-written parallel tool list was a core defect of the first pass at
134
+ // a capabilities tool, which reported 4 of the 8 registered tools.)
135
+ const toolDefinitions = [
136
+ {
137
+ name: "fullstackgtm_audit",
138
+ writesCrm: false,
139
+ config: {
140
+ title: "GTM Ops Audit",
141
+ description: "Run a dry-run GTM hygiene audit and return a reviewable patch plan. " +
142
+ "Sources: the realistic zero-credential demo CRM (provider: \"demo\" — richest test data), " +
143
+ "the minimal sample dataset, a snapshot file, or a live provider.",
144
+ inputSchema: {
145
+ provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
146
+ inputPath: z.string().optional(),
147
+ configPath: z.string().optional(),
148
+ rules: z.array(z.string()).optional(),
149
+ output: z.enum(["json", "markdown"]).optional(),
150
+ today: z.string().optional(),
151
+ staleDealDays: z.number().int().positive().optional(),
152
+ },
144
153
  },
145
- }, async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
146
- const loaded = configPath ? loadConfig(configPath) : null;
147
- const policy = mergePolicy(defaultPolicy(today), loaded?.config);
148
- if (today)
149
- policy.today = today;
150
- if (staleDealDays !== undefined)
151
- policy.staleDealDays = staleDealDays;
152
- const ruleSet = await resolveConfiguredRules(loaded);
153
- const selected = rules?.length
154
- ? ruleSet.filter((rule) => rules.includes(rule.id))
155
- : ruleSet;
156
- const plan = auditSnapshot(await readSnapshot(provider, inputPath), policy, selected);
157
- return content(output === "markdown" ? patchPlanToMarkdown(plan) : plan);
158
- });
159
- server.registerTool("fullstackgtm_suggest", {
160
- title: "Suggest Placeholder Values",
161
- description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
162
- "evidence (account-name matching, contact associations), with confidence levels and " +
163
- "reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
164
- inputSchema: {
165
- planPath: z.string(),
166
- provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
167
- inputPath: z.string().optional(),
154
+ handler: async ({ provider, inputPath, configPath, rules, output, today, staleDealDays }) => {
155
+ const loaded = configPath ? loadConfig(configPath) : null;
156
+ const policy = mergePolicy(defaultPolicy(today), loaded?.config);
157
+ if (today)
158
+ policy.today = today;
159
+ if (staleDealDays !== undefined)
160
+ policy.staleDealDays = staleDealDays;
161
+ const ruleSet = await resolveConfiguredRules(loaded);
162
+ const selected = rules?.length
163
+ ? ruleSet.filter((rule) => rules.includes(rule.id))
164
+ : ruleSet;
165
+ const plan = auditSnapshot(await readSnapshot(provider, inputPath), policy, selected);
166
+ return content(output === "markdown" ? patchPlanToMarkdown(plan) : plan);
168
167
  },
169
- }, async ({ planPath, provider, inputPath }) => {
170
- const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
171
- const snapshot = await readSnapshot(provider, inputPath);
172
- return content({ suggestions: suggestValues(plan, snapshot) });
173
- });
174
- server.registerTool("fullstackgtm_call_parse", {
175
- title: "Parse Call Transcript",
176
- description: "Parse a call transcript (Speaker:/[Speaker]: lines or Granola utterance JSON) into " +
177
- "canonical segments, insights, and GtmEvidence records. extractor: 'auto' (default) " +
178
- "uses LLM extraction when an Anthropic/OpenAI key is configured in the server " +
179
- "environment or credential store, else the free deterministic keyword baseline; " +
180
- "'llm' and 'deterministic' force either. Read-only; every insight is provenance-marked.",
181
- inputSchema: {
182
- transcript: z.string().optional(),
183
- transcriptPath: z.string().optional(),
184
- title: z.string().optional(),
185
- source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
186
- extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
187
- model: z.string().optional(),
168
+ },
169
+ {
170
+ name: "fullstackgtm_suggest",
171
+ writesCrm: false,
172
+ config: {
173
+ title: "Suggest Placeholder Values",
174
+ description: "Derive values for a plan's requires_human_* placeholder operations from snapshot " +
175
+ "evidence (account-name matching, contact associations), with confidence levels and " +
176
+ "reasons. Read-only; feed accepted values into fullstackgtm_apply's valueOverrides.",
177
+ inputSchema: {
178
+ planPath: z.string(),
179
+ provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
180
+ inputPath: z.string().optional(),
181
+ },
188
182
  },
189
- }, async ({ transcript, transcriptPath, title, source, extractor, model }) => {
190
- const raw = transcript ??
191
- (transcriptPath ? readFileSync(resolve(process.cwd(), transcriptPath), "utf8") : null);
192
- if (!raw)
193
- throw new Error("Provide transcript (text) or transcriptPath (file).");
194
- const mode = extractor ?? "auto";
195
- const credential = mode === "deterministic" ? null : resolveLlmCredential();
196
- if (mode === "llm" && !credential) {
197
- throw new Error("extractor 'llm' needs an API key: set ANTHROPIC_API_KEY or OPENAI_API_KEY in the MCP server environment, or store one with `fullstackgtm login anthropic|openai`.");
198
- }
199
- if (credential) {
200
- const normalized = normalizeTranscript(raw);
201
- const { insights, model: used } = await extractInsightsLlm(normalized, {
202
- ...credential,
203
- model,
204
- title,
205
- });
206
- return content(parseCall(raw, { title, sourceSystem: source, insights, extractor: `llm:${credential.provider}:${used}` }));
207
- }
208
- return content(parseCall(raw, { title, sourceSystem: source }));
209
- });
210
- server.registerTool("fullstackgtm_resolve", {
211
- title: "Resolve Record (create gate)",
212
- description: "Before creating a CRM record, check whether it already exists. Returns a verdict " +
213
- "(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
214
- "identity keys as the audit/merge engines (account domain, contact email, open-deal " +
215
- "key). Read-only. Never create on 'exists' or 'ambiguous'.",
216
- inputSchema: {
217
- objectType: z.enum(["account", "contact", "deal"]),
218
- name: z.string().optional(),
219
- domain: z.string().optional(),
220
- email: z.string().optional(),
221
- accountId: z.string().optional(),
222
- provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
223
- inputPath: z.string().optional(),
183
+ handler: async ({ planPath, provider, inputPath }) => {
184
+ const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
185
+ const snapshot = await readSnapshot(provider, inputPath);
186
+ return content({ suggestions: suggestValues(plan, snapshot) });
224
187
  },
225
- }, async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
226
- const snapshot = await readSnapshot(provider, inputPath);
227
- return content(resolveRecord(snapshot, { objectType, name, domain, email, accountId }));
228
- });
229
- server.registerTool("fullstackgtm_rules", {
230
- title: "List Audit Rules",
231
- description: "List the built-in deterministic audit rules with ids and descriptions.",
232
- inputSchema: {},
233
- }, async () => {
234
- return content(builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })));
235
- });
236
- server.registerTool("fullstackgtm_apply", {
237
- title: "Apply Approved Patch Operations",
238
- description: "Apply explicitly approved operations from a patch plan through a provider " +
239
- "connector. Operations not listed in approvedOperationIds are never written, " +
240
- "and requires_human_* placeholders need a value override.",
241
- inputSchema: {
242
- provider: z.enum(["hubspot", "salesforce"]),
243
- planPath: z.string(),
244
- approvedOperationIds: z.array(z.string()).min(1),
245
- valueOverrides: z.record(z.string(), z.string()).optional(),
246
- output: z.enum(["json", "markdown"]).optional(),
188
+ },
189
+ {
190
+ name: "fullstackgtm_call_parse",
191
+ writesCrm: false,
192
+ config: {
193
+ title: "Parse Call Transcript",
194
+ description: "Parse a call transcript (Speaker:/[Speaker]: lines or Granola utterance JSON) into " +
195
+ "canonical segments, insights, and GtmEvidence records. extractor: 'auto' (default) " +
196
+ "uses LLM extraction when an Anthropic/OpenAI key is configured in the server " +
197
+ "environment or credential store, else the free deterministic keyword baseline; " +
198
+ "'llm' and 'deterministic' force either. Read-only; every insight is provenance-marked.",
199
+ inputSchema: {
200
+ transcript: z.string().optional(),
201
+ transcriptPath: z.string().optional(),
202
+ title: z.string().optional(),
203
+ source: z.enum(["gong", "chorus", "fathom", "manual", "csv", "unknown"]).optional(),
204
+ extractor: z.enum(["auto", "llm", "deterministic"]).optional(),
205
+ model: z.string().optional(),
206
+ },
247
207
  },
248
- }, async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
249
- const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
250
- const run = await applyPatchPlan(await connectorFor(provider), plan, {
251
- approvedOperationIds,
252
- valueOverrides,
253
- });
254
- return content(output === "markdown" ? formatPatchPlanRun(run) : run);
255
- });
256
- server.registerTool("fullstackgtm_market_worksheet", {
257
- title: "Market Map Classification Worksheet",
258
- description: "Get everything needed to classify ONE vendor's messaging intensity for a market map: " +
259
- "the claim taxonomy with judging definitions, the surface rule, and the captured page " +
260
- "texts. Read each claim's definition, judge loud/quiet/absent from the page texts only, " +
261
- "and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
262
- "ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
263
- "character against the captures, so never paraphrase.",
264
- inputSchema: {
265
- vendorId: z.string(),
266
- configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
267
- captureRun: z.string().optional(),
208
+ handler: async ({ transcript, transcriptPath, title, source, extractor, model }) => {
209
+ const raw = transcript ??
210
+ (transcriptPath ? readFileSync(resolve(process.cwd(), transcriptPath), "utf8") : null);
211
+ if (!raw)
212
+ throw new Error("Provide transcript (text) or transcriptPath (file).");
213
+ const mode = extractor ?? "auto";
214
+ const credential = mode === "deterministic" ? null : resolveLlmCredential();
215
+ if (mode === "llm" && !credential) {
216
+ throw new Error("extractor 'llm' needs an API key: set ANTHROPIC_API_KEY or OPENAI_API_KEY in the MCP server environment, or store one with `fullstackgtm login anthropic|openai`.");
217
+ }
218
+ if (credential) {
219
+ const normalized = normalizeTranscript(raw);
220
+ const { insights, model: used } = await extractInsightsLlm(normalized, {
221
+ ...credential,
222
+ model,
223
+ title,
224
+ });
225
+ return content(parseCall(raw, { title, sourceSystem: source, insights, extractor: `llm:${credential.provider}:${used}` }));
226
+ }
227
+ return content(parseCall(raw, { title, sourceSystem: source }));
268
228
  },
269
- }, async ({ vendorId, configPath, captureRun }) => {
270
- const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
271
- return content(buildWorksheet(config, vendorId, { captureRun }));
272
- });
273
- server.registerTool("fullstackgtm_market_observe", {
274
- title: "Submit Market Map Observations",
275
- description: "Submit a complete ObservationSet (every vendor × claim cell) for a market map run. " +
276
- "Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
277
- "span against the stored capture it cites. Returns problems if rejected; nothing is " +
278
- "stored unless the whole set passes. Observations are append-only use a new runLabel.",
279
- inputSchema: {
280
- observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
281
- configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
229
+ },
230
+ {
231
+ name: "fullstackgtm_resolve",
232
+ writesCrm: false,
233
+ config: {
234
+ title: "Resolve Record (create gate)",
235
+ description: "Before creating a CRM record, check whether it already exists. Returns a verdict " +
236
+ "(exists | ambiguous | safe_to_create) with matches and a reason, using the same " +
237
+ "identity keys as the audit/merge engines (account domain, contact email, open-deal " +
238
+ "key). Read-only. Never create on 'exists' or 'ambiguous'.",
239
+ inputSchema: {
240
+ objectType: z.enum(["account", "contact", "deal"]),
241
+ name: z.string().optional(),
242
+ domain: z.string().optional(),
243
+ email: z.string().optional(),
244
+ accountId: z.string().optional(),
245
+ provider: z.enum(["sample", "demo", "hubspot", "salesforce", "stripe"]).optional(),
246
+ inputPath: z.string().optional(),
247
+ },
282
248
  },
283
- }, async ({ observationsPath, configPath }) => {
284
- const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
285
- const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8"));
286
- const problems = validateObservationSet(config, set);
287
- const failures = verifyEvidenceSpans(set.observations, loadCaptureTexts(config.category).textByHash);
288
- if (problems.length > 0 || failures.length > 0) {
289
- return content({
290
- accepted: false,
291
- problems,
292
- spanFailures: failures.map((failure) => `${failure.vendorId} × ${failure.claimId}: ${failure.problem}`),
249
+ handler: async ({ objectType, name, domain, email, accountId, provider, inputPath }) => {
250
+ const snapshot = await readSnapshot(provider, inputPath);
251
+ return content(resolveRecord(snapshot, { objectType, name, domain, email, accountId }));
252
+ },
253
+ },
254
+ {
255
+ name: "fullstackgtm_rules",
256
+ writesCrm: false,
257
+ config: {
258
+ title: "List Audit Rules",
259
+ description: "List the built-in deterministic audit rules with ids and descriptions.",
260
+ inputSchema: {},
261
+ },
262
+ handler: async () => {
263
+ return content(builtinAuditRules.map(({ id, title, description }) => ({ id, title, description })));
264
+ },
265
+ },
266
+ {
267
+ name: "fullstackgtm_apply",
268
+ writesCrm: true,
269
+ config: {
270
+ title: "Apply Approved Patch Operations",
271
+ description: "Apply explicitly approved operations from a patch plan through a provider " +
272
+ "connector. Operations not listed in approvedOperationIds are never written, " +
273
+ "and requires_human_* placeholders need a value override.",
274
+ inputSchema: {
275
+ provider: z.enum(["hubspot", "salesforce"]),
276
+ planPath: z.string(),
277
+ approvedOperationIds: z.array(z.string()).min(1),
278
+ valueOverrides: z.record(z.string(), z.string()).optional(),
279
+ output: z.enum(["json", "markdown"]).optional(),
280
+ },
281
+ },
282
+ handler: async ({ provider, planPath, approvedOperationIds, valueOverrides, output }) => {
283
+ const plan = JSON.parse(readFileSync(resolve(process.cwd(), planPath), "utf8"));
284
+ const run = await applyPatchPlan(await connectorFor(provider), plan, {
285
+ approvedOperationIds,
286
+ valueOverrides,
293
287
  });
294
- }
295
- await createFileObservationStore(config.category).append(set);
296
- const fronts = computeFrontStates(config, set);
297
- return content({ accepted: true, runLabel: set.runLabel, observations: set.observations.length, fronts });
288
+ return content(output === "markdown" ? formatPatchPlanRun(run) : run);
289
+ },
290
+ },
291
+ {
292
+ name: "fullstackgtm_market_worksheet",
293
+ writesCrm: false,
294
+ config: {
295
+ title: "Market Map Classification Worksheet",
296
+ description: "Get everything needed to classify ONE vendor's messaging intensity for a market map: " +
297
+ "the claim taxonomy with judging definitions, the surface rule, and the captured page " +
298
+ "texts. Read each claim's definition, judge loud/quiet/absent from the page texts only, " +
299
+ "and quote verbatim spans (≤300 chars) for every loud/quiet reading. Submit the full " +
300
+ "ObservationSet via fullstackgtm_market_observe — quotes are verified character-for-" +
301
+ "character against the captures, so never paraphrase.",
302
+ inputSchema: {
303
+ vendorId: z.string(),
304
+ configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
305
+ captureRun: z.string().optional(),
306
+ },
307
+ },
308
+ handler: async ({ vendorId, configPath, captureRun }) => {
309
+ const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
310
+ return content(buildWorksheet(config, vendorId, { captureRun }));
311
+ },
312
+ },
313
+ {
314
+ name: "fullstackgtm_market_observe",
315
+ writesCrm: false,
316
+ config: {
317
+ title: "Submit Market Map Observations",
318
+ description: "Submit a complete ObservationSet (every vendor × claim cell) for a market map run. " +
319
+ "Validates coverage, the verbatim-evidence rule, and mechanically verifies every quoted " +
320
+ "span against the stored capture it cites. Returns problems if rejected; nothing is " +
321
+ "stored unless the whole set passes. Observations are append-only — use a new runLabel.",
322
+ inputSchema: {
323
+ observationsPath: z.string().describe("Path to the ObservationSet JSON file"),
324
+ configPath: z.string().optional().describe("Path to market.config.json (default ./market.config.json)"),
325
+ },
326
+ },
327
+ handler: async ({ observationsPath, configPath }) => {
328
+ const config = loadMarketConfigOrHint(resolve(process.cwd(), configPath ?? "market.config.json"));
329
+ const set = JSON.parse(readFileSync(resolve(process.cwd(), observationsPath), "utf8"));
330
+ const problems = validateObservationSet(config, set);
331
+ const failures = verifyEvidenceSpans(set.observations, loadCaptureTexts(config.category).textByHash);
332
+ if (problems.length > 0 || failures.length > 0) {
333
+ return content({
334
+ accepted: false,
335
+ problems,
336
+ spanFailures: failures.map((failure) => `${failure.vendorId} × ${failure.claimId}: ${failure.problem}`),
337
+ });
338
+ }
339
+ await createFileObservationStore(config.category).append(set);
340
+ const fronts = computeFrontStates(config, set);
341
+ return content({ accepted: true, runLabel: set.runLabel, observations: set.observations.length, fronts });
342
+ },
343
+ },
344
+ ];
345
+ // Registered first so agents that list tools see the contract entry up top.
346
+ // Its payload closes over the registration table, so the reported inventory
347
+ // is derived, not hand-maintained.
348
+ toolDefinitions.unshift({
349
+ name: "fullstackgtm_capabilities",
350
+ writesCrm: false,
351
+ config: {
352
+ title: "FullStackGTM Capabilities",
353
+ description: "Machine-readable contract for this MCP server: the tool inventory (derived from the " +
354
+ "server's own registration table, with per-tool CRM-write flags), safety defaults, and " +
355
+ "the CLI entrypoints for everything the tools don't cover.",
356
+ },
357
+ handler: async () => content({
358
+ ok: true,
359
+ tool: "fullstackgtm",
360
+ version: packageVersion(),
361
+ planFirst: true,
362
+ mcpTools: toolDefinitions.map(({ name, writesCrm, config }) => ({
363
+ name,
364
+ title: config.title,
365
+ writesCrm,
366
+ })),
367
+ safety: "Only tools flagged writesCrm can reach a CRM; fullstackgtm_apply writes only operations " +
368
+ "listed in approvedOperationIds and never writes requires_human_* placeholders without a value override.",
369
+ server: { command: "npx -y fullstackgtm-mcp" },
370
+ cli: {
371
+ command: "npx fullstackgtm <command> [--json]",
372
+ capabilities: "npx fullstackgtm capabilities --json",
373
+ agentGuide: "npx fullstackgtm robot-docs",
374
+ },
375
+ examples: [
376
+ "npx -y fullstackgtm-mcp",
377
+ "npx fullstackgtm capabilities --json",
378
+ "npx fullstackgtm audit --demo --json",
379
+ ],
380
+ }),
381
+ });
382
+ export async function startMcpServer() {
383
+ const server = new McpServer({
384
+ name: "fullstackgtm",
385
+ version: packageVersion(),
298
386
  });
387
+ for (const tool of toolDefinitions) {
388
+ // The table is loosely typed so it can drive one loop; the SDK still
389
+ // validates every call against the zod inputSchema at runtime.
390
+ server.registerTool(tool.name, tool.config, tool.handler);
391
+ }
299
392
  const transport = new StdioServerTransport();
300
393
  await server.connect(transport);
301
394
  }
@@ -40,9 +40,11 @@ export type ScheduleRunRecord = {
40
40
  * recorded for visibility but nothing ran. Reasons: "plan_not_approved"
41
41
  * (scheduled apply against a plan that is not approved — there is no flag
42
42
  * that relaxes this), "not_schedulable" (the stored argv no longer passes
43
- * the allowlist, e.g. after a hand edit of schedules.json).
43
+ * the allowlist, e.g. after a hand edit of schedules.json),
44
+ * "duplicate_firing" (a cron-triggered run already recorded in this minute
45
+ * — launchd can double-trigger the Vixie dom+dow OR corner).
44
46
  */
45
- noopReason?: "plan_not_approved" | "not_schedulable";
47
+ noopReason?: "plan_not_approved" | "not_schedulable" | "duplicate_firing";
46
48
  };
47
49
  export declare function scheduleId(label: string, cron: string, argv: string[], createdAt: string): string;
48
50
  /**
@@ -158,3 +160,79 @@ export declare function renderManagedBlock(profile: string, entries: ScheduleEnt
158
160
  * Idempotent: re-applying the same block yields identical content.
159
161
  */
160
162
  export declare function replaceManagedBlock(existing: string, profile: string, block: string | null): string;
163
+ /**
164
+ * One launchd StartCalendarInterval dict: every present key must match (AND),
165
+ * a missing key is a wildcard, and an array of dicts fires when ANY dict
166
+ * matches. Weekday 0 is Sunday, as in cron.
167
+ */
168
+ export type LaunchdCalendarInterval = {
169
+ Minute?: number;
170
+ Hour?: number;
171
+ Day?: number;
172
+ Weekday?: number;
173
+ Month?: number;
174
+ };
175
+ /**
176
+ * Compile a cron expression to StartCalendarInterval dicts. Full-range fields
177
+ * (`*`, but also spellings like `0-59`) are omitted (launchd wildcard); the
178
+ * rest cross-product, capped so a dense expression cannot render a megabyte
179
+ * plist. Vixie day semantics: when day-of-month AND day-of-week are both
180
+ * restricted, either may match — launchd ANDs keys within one dict, so that
181
+ * becomes the union of a Day dict set and a Weekday dict set. A date matching
182
+ * both sets makes launchd trigger twice in the same minute; the run entry
183
+ * point drops the second via isDuplicateCronFiring.
184
+ */
185
+ export declare function cronToCalendarIntervals(cron: CronExpression, cap?: number): LaunchdCalendarInterval[];
186
+ /**
187
+ * True when a cron-triggered run was already recorded in `firedAt`'s minute.
188
+ * The `schedule run` entry point checks this for trigger:cron so a launchd
189
+ * double-trigger (dom+dow OR corner above) records a duplicate_firing no-op
190
+ * instead of running the command twice. Manual runs are never deduplicated.
191
+ */
192
+ export declare function isDuplicateCronFiring(runs: ScheduleRunRecord[], firedAt: string): boolean;
193
+ /** Reverse-DNS namespace for one profile's agents; install/uninstall own this prefix wholesale. */
194
+ export declare function launchdAgentPrefix(profile: string): string;
195
+ export declare function launchdLabel(profile: string, scheduleEntryId: string): string;
196
+ /**
197
+ * Render one LaunchAgent plist. ProgramArguments is an argv array — launchd
198
+ * execs it directly, no shell, so there is no quoting/injection surface at
199
+ * all (values are XML-escaped for the document, nothing more).
200
+ */
201
+ export declare function renderLaunchdPlist(options: {
202
+ label: string;
203
+ programArguments: string[];
204
+ calendarIntervals: LaunchdCalendarInterval[];
205
+ environment?: Record<string, string>;
206
+ standardOutPath?: string;
207
+ standardErrorPath?: string;
208
+ }): string;
209
+ export type LaunchdIo = {
210
+ /** Plist file names currently in the agents directory. */
211
+ list(): string[];
212
+ write(fileName: string, content: string): void;
213
+ remove(fileName: string): void;
214
+ /** Load one agent plist into the user's gui domain; throws on failure. */
215
+ bootstrap(fileName: string): void;
216
+ /** Best-effort unload by label; a label that is not loaded is not an error. */
217
+ bootout(label: string): void;
218
+ };
219
+ /**
220
+ * The real ~/Library/LaunchAgents + launchctl. Everything above takes a
221
+ * LaunchdIo so tests inject fakes and never touch the user's agents.
222
+ */
223
+ export declare function systemLaunchdIo(agentsDirOverride?: string): LaunchdIo;
224
+ /**
225
+ * Materialize enabled entries as one LaunchAgent each, replacing the
226
+ * profile's com.fullstackgtm.<profile>.* fleet wholesale (stale agents are
227
+ * booted out and deleted; plists outside the prefix are never touched).
228
+ * `cliArgv` is the argv-array analog of the crontab invocation line.
229
+ */
230
+ export declare function installLaunchdAgents(profile: string, entries: ScheduleEntry[], cliArgv: string[], io: LaunchdIo, options?: {
231
+ environment?: Record<string, string>;
232
+ logDir?: string;
233
+ }): {
234
+ installed: string[];
235
+ removed: string[];
236
+ };
237
+ /** Boot out and delete every agent in the profile's prefix; returns the removed labels. */
238
+ export declare function uninstallLaunchdAgents(profile: string, io: LaunchdIo): string[];