fullstackgtm 0.43.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.
- package/CHANGELOG.md +378 -1
- package/README.md +37 -14
- package/dist/audit.d.ts +3 -1
- package/dist/audit.js +29 -2
- package/dist/cli/audit.d.ts +15 -0
- package/dist/cli/audit.js +392 -0
- package/dist/cli/auth.d.ts +80 -0
- package/dist/cli/auth.js +500 -0
- package/dist/cli/call.d.ts +1 -0
- package/dist/cli/call.js +373 -0
- package/dist/cli/capabilities.d.ts +30 -0
- package/dist/cli/capabilities.js +197 -0
- package/dist/cli/draft.d.ts +7 -0
- package/dist/cli/draft.js +87 -0
- package/dist/cli/enrich.d.ts +8 -0
- package/dist/cli/enrich.js +788 -0
- package/dist/cli/fix.d.ts +33 -0
- package/dist/cli/fix.js +344 -0
- package/dist/cli/help.d.ts +25 -0
- package/dist/cli/help.js +609 -0
- package/dist/cli/icp.d.ts +7 -0
- package/dist/cli/icp.js +229 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +58 -0
- package/dist/cli/market.d.ts +1 -0
- package/dist/cli/market.js +391 -0
- package/dist/cli/plans.d.ts +5 -0
- package/dist/cli/plans.js +454 -0
- package/dist/cli/schedule.d.ts +8 -0
- package/dist/cli/schedule.js +479 -0
- package/dist/cli/shared.d.ts +70 -0
- package/dist/cli/shared.js +331 -0
- package/dist/cli/signals.d.ts +7 -0
- package/dist/cli/signals.js +412 -0
- package/dist/cli/suggest.d.ts +49 -0
- package/dist/cli/suggest.js +135 -0
- package/dist/cli/tam.d.ts +9 -0
- package/dist/cli/tam.js +387 -0
- package/dist/cli/ui.d.ts +121 -0
- package/dist/cli/ui.js +375 -0
- package/dist/cli.d.ts +1 -57
- package/dist/cli.js +104 -4556
- package/dist/connector.d.ts +15 -0
- package/dist/connector.js +35 -0
- package/dist/connectors/hubspot.d.ts +3 -1
- package/dist/connectors/hubspot.js +10 -3
- package/dist/connectors/outboxChannel.d.ts +64 -0
- package/dist/connectors/outboxChannel.js +170 -0
- package/dist/connectors/prospectSources.d.ts +22 -0
- package/dist/connectors/prospectSources.js +43 -0
- package/dist/connectors/salesforce.d.ts +3 -1
- package/dist/connectors/salesforce.js +12 -5
- package/dist/connectors/signalSources.d.ts +105 -0
- package/dist/connectors/signalSources.js +264 -0
- package/dist/connectors/theirstack.d.ts +84 -0
- package/dist/connectors/theirstack.js +125 -0
- package/dist/icp.d.ts +52 -4
- package/dist/icp.js +112 -35
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/init.js +3 -0
- package/dist/judge.d.ts +2 -0
- package/dist/judge.js +6 -0
- package/dist/judgeEval.d.ts +7 -0
- package/dist/judgeEval.js +8 -1
- package/dist/marketClassify.d.ts +2 -0
- package/dist/marketClassify.js +7 -1
- package/dist/mcp-bin.js +2 -2
- package/dist/mcp.js +259 -166
- package/dist/runReport.d.ts +26 -0
- package/dist/runReport.js +15 -0
- package/dist/schedule.d.ts +80 -2
- package/dist/schedule.js +272 -5
- package/dist/signals.d.ts +54 -0
- package/dist/signals.js +64 -0
- package/dist/spoolFiles.d.ts +44 -0
- package/dist/spoolFiles.js +114 -0
- package/dist/tam.d.ts +225 -0
- package/dist/tam.js +470 -0
- package/dist/types.d.ts +11 -0
- package/docs/api.md +91 -11
- package/docs/outbox-format.md +92 -0
- package/docs/recipes.md +37 -0
- package/docs/roadmap-to-1.0.md +31 -1
- package/docs/signal-spool-format.md +175 -0
- package/docs/tam.md +195 -0
- package/llms.txt +83 -11
- package/package.json +1 -1
- package/skills/fullstackgtm/SKILL.md +4 -3
- package/src/audit.ts +27 -1
- package/src/cli/audit.ts +447 -0
- package/src/cli/auth.ts +549 -0
- package/src/cli/call.ts +398 -0
- package/src/cli/capabilities.ts +215 -0
- package/src/cli/draft.ts +101 -0
- package/src/cli/enrich.ts +885 -0
- package/src/cli/fix.ts +372 -0
- package/src/cli/help.ts +664 -0
- package/src/cli/icp.ts +265 -0
- package/src/cli/init.ts +65 -0
- package/src/cli/market.ts +423 -0
- package/src/cli/plans.ts +523 -0
- package/src/cli/schedule.ts +526 -0
- package/src/cli/shared.ts +375 -0
- package/src/cli/signals.ts +434 -0
- package/src/cli/suggest.ts +151 -0
- package/src/cli/tam.ts +435 -0
- package/src/cli/ui.ts +426 -0
- package/src/cli.ts +103 -5170
- package/src/connector.ts +46 -0
- package/src/connectors/hubspot.ts +14 -2
- package/src/connectors/outboxChannel.ts +202 -0
- package/src/connectors/prospectSources.ts +57 -0
- package/src/connectors/salesforce.ts +18 -2
- package/src/connectors/signalSources.ts +314 -0
- package/src/connectors/theirstack.ts +170 -0
- package/src/icp.ts +120 -34
- package/src/index.ts +32 -0
- package/src/init.ts +3 -0
- package/src/judge.ts +7 -0
- package/src/judgeEval.ts +8 -1
- package/src/marketClassify.ts +8 -1
- package/src/mcp-bin.ts +2 -2
- package/src/mcp.ts +130 -57
- package/src/runReport.ts +39 -0
- package/src/schedule.ts +330 -7
- package/src/signals.ts +90 -0
- package/src/spoolFiles.ts +116 -0
- package/src/tam.ts +654 -0
- package/src/types.ts +12 -0
package/src/cli/call.ts
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Extracted verbatim from src/cli.ts (mechanical split, no behavior change).
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { patchPlanToMarkdown } from "../format.ts";
|
|
6
|
+
import { createFilePlanStore } from "../planStore.ts";
|
|
7
|
+
import { normalizeTranscript, parseCall, suggestCallDeal, type ExtractedCallInsight, type ParsedCall } from "../calls.ts";
|
|
8
|
+
import { classifyCall, rubricForCallType, rubricPresets, CALL_TYPES, CALL_TYPE_IDS, type CallType } from "../callTypes.ts";
|
|
9
|
+
import { DEFAULT_RUBRIC, classifyCallLlm, extractInsightsLlm, parseRubric, resolveLlmCredential, scoreCallLlm, type CallScorecard, type Rubric } from "../llm.ts";
|
|
10
|
+
import type { PatchPlan } from "../types.ts";
|
|
11
|
+
import { option, readSnapshot, requireLlmCredential, saveRequested } from "./shared.ts";
|
|
12
|
+
import { startElapsedStatus } from "./ui.ts";
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export async function callCommand(args: string[]) {
|
|
16
|
+
const [subcommand, ...rest] = args;
|
|
17
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
18
|
+
console.log(`call parse --transcript <file> [--title t] [--source s] [--model m] [--deterministic] [--json|--ndjson] [--out <path>]
|
|
19
|
+
call classify --transcript <file>|--call <parsed.json> [--llm] [--deterministic] [--json] [--list]
|
|
20
|
+
call score --transcript <file>|--call <parsed.json> [--call-type <t>] [--rubric <rubric.json>] [--model m] [--json|--out <path>] [--list-rubrics]
|
|
21
|
+
call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
|
|
22
|
+
call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
|
|
23
|
+
|
|
24
|
+
classify picks the call type (deterministic signals; --llm for a model tiebreak).
|
|
25
|
+
score auto-selects the type-specific rubric from that classification unless you
|
|
26
|
+
pass --call-type or --rubric. Call types: ${CALL_TYPE_IDS.join(", ")}.
|
|
27
|
+
|
|
28
|
+
parse/score default to LLM extraction (Anthropic or OpenAI key via env,
|
|
29
|
+
\`login anthropic|openai\`, or a one-time prompt). parse --deterministic is the
|
|
30
|
+
free keyword baseline and classify --deterministic needs no key.
|
|
31
|
+
score always needs a key (scoring is LLM work).`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const loadParsedCall = async (): Promise<ParsedCall> => {
|
|
36
|
+
const callPath = option(rest, "--call");
|
|
37
|
+
if (callPath) {
|
|
38
|
+
return JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8")) as ParsedCall;
|
|
39
|
+
}
|
|
40
|
+
const transcriptPath = option(rest, "--transcript");
|
|
41
|
+
if (!transcriptPath) throw new Error(`call ${subcommand} requires --transcript <file> or --call <parsed.json>`);
|
|
42
|
+
const raw = readFileSync(resolve(process.cwd(), transcriptPath), "utf8");
|
|
43
|
+
const source = option(rest, "--source") as ParsedCall["sourceSystem"] | undefined;
|
|
44
|
+
const base = {
|
|
45
|
+
title: option(rest, "--title") ?? undefined,
|
|
46
|
+
sourceSystem: source,
|
|
47
|
+
capturedAt: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
if (rest.includes("--deterministic")) {
|
|
50
|
+
return parseCall(raw, base);
|
|
51
|
+
}
|
|
52
|
+
// LLM extraction is the default: bring-your-own-key (Anthropic or OpenAI).
|
|
53
|
+
const credential = await requireLlmCredential();
|
|
54
|
+
const normalized = normalizeTranscript(raw);
|
|
55
|
+
// Elapsed-time spinner on interactive terminals while the model works.
|
|
56
|
+
const wait = startElapsedStatus(
|
|
57
|
+
(elapsed) => `Extracting insights · ${credential.provider} · ${elapsed}`,
|
|
58
|
+
);
|
|
59
|
+
let extraction: Awaited<ReturnType<typeof extractInsightsLlm>>;
|
|
60
|
+
try {
|
|
61
|
+
extraction = await extractInsightsLlm(normalized, {
|
|
62
|
+
...credential,
|
|
63
|
+
model: option(rest, "--model") ?? undefined,
|
|
64
|
+
title: base.title,
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
wait.done();
|
|
68
|
+
}
|
|
69
|
+
const { insights, model } = extraction;
|
|
70
|
+
return parseCall(raw, {
|
|
71
|
+
...base,
|
|
72
|
+
insights,
|
|
73
|
+
extractor: `llm:${credential.provider}:${model}`,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Reconstruct plain transcript text from either a --transcript file (any
|
|
78
|
+
// dialect, normalized) or a parsed --call JSON. Shared by classify + score.
|
|
79
|
+
const loadTranscriptText = (): string => {
|
|
80
|
+
const transcriptPath = option(rest, "--transcript");
|
|
81
|
+
if (transcriptPath) {
|
|
82
|
+
return normalizeTranscript(readFileSync(resolve(process.cwd(), transcriptPath), "utf8"));
|
|
83
|
+
}
|
|
84
|
+
const callPath = option(rest, "--call");
|
|
85
|
+
if (!callPath) throw new Error(`call ${subcommand} requires --transcript <file> or --call <parsed.json>`);
|
|
86
|
+
const parsed = JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8")) as ParsedCall;
|
|
87
|
+
return parsed.segments.map((s) => (s.speaker ? `${s.speaker}: ${s.text}` : s.text)).join("\n");
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (subcommand === "classify") {
|
|
91
|
+
if (rest.includes("--list")) {
|
|
92
|
+
const lines = CALL_TYPES.map((d) => `${d.id.padEnd(22)} ${d.name} — ${d.definition}`);
|
|
93
|
+
console.log(rest.includes("--json") ? JSON.stringify(CALL_TYPES, null, 2) : lines.join("\n"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const transcriptText = loadTranscriptText();
|
|
97
|
+
const title = option(rest, "--title") ?? undefined;
|
|
98
|
+
const deterministic = classifyCall(transcriptText);
|
|
99
|
+
// LLM tiebreak: explicit --llm, or auto when the deterministic pass is unsure
|
|
100
|
+
// and a key is available (never required — deterministic always answers).
|
|
101
|
+
const wantLlm = rest.includes("--llm") || (!rest.includes("--deterministic") && deterministic.confidence !== "high" && Boolean(resolveLlmCredential()));
|
|
102
|
+
let result: {
|
|
103
|
+
type: CallType;
|
|
104
|
+
confidence: string;
|
|
105
|
+
reason: string;
|
|
106
|
+
method: string;
|
|
107
|
+
candidates?: typeof deterministic.candidates;
|
|
108
|
+
model?: string;
|
|
109
|
+
} = deterministic;
|
|
110
|
+
if (wantLlm) {
|
|
111
|
+
const credential = await requireLlmCredential("score");
|
|
112
|
+
const llm = await classifyCallLlm(transcriptText, CALL_TYPES, {
|
|
113
|
+
...credential,
|
|
114
|
+
model: option(rest, "--model") ?? undefined,
|
|
115
|
+
title,
|
|
116
|
+
});
|
|
117
|
+
result = { type: llm.type, confidence: "high", reason: llm.reason, method: "llm", model: llm.model };
|
|
118
|
+
}
|
|
119
|
+
if (rest.includes("--json")) {
|
|
120
|
+
console.log(JSON.stringify(result, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const def = CALL_TYPES.find((d) => d.id === result.type);
|
|
124
|
+
console.log(`Call type: ${def?.name ?? result.type} (${result.type})`);
|
|
125
|
+
console.log(`Confidence: ${result.confidence} · via ${result.method}${result.model ? ` (${result.model})` : ""}`);
|
|
126
|
+
console.log(`Why: ${result.reason}`);
|
|
127
|
+
if (result.method === "deterministic" && deterministic.candidates.length > 1) {
|
|
128
|
+
const others = deterministic.candidates.slice(1, 4).map((c) => `${c.type} (${c.score})`).join(", ");
|
|
129
|
+
console.log(`Other matches: ${others}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`\nScore it with this rubric: fullstackgtm call score ${option(rest, "--transcript") ? `--transcript ${option(rest, "--transcript")}` : `--call ${option(rest, "--call")}`} --call-type ${result.type}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (subcommand === "parse") {
|
|
136
|
+
const parsed = await loadParsedCall();
|
|
137
|
+
const outPath = option(rest, "--out");
|
|
138
|
+
if (outPath) writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(parsed, null, 2)}\n`);
|
|
139
|
+
if (rest.includes("--ndjson")) {
|
|
140
|
+
// One flat row per insight — warehouse-friendly (e.g. Snowflake COPY).
|
|
141
|
+
for (const insight of parsed.insights) {
|
|
142
|
+
console.log(
|
|
143
|
+
JSON.stringify({
|
|
144
|
+
call_id: parsed.id,
|
|
145
|
+
call_title: parsed.title ?? null,
|
|
146
|
+
source_system: parsed.sourceSystem,
|
|
147
|
+
extractor: parsed.extractor,
|
|
148
|
+
type: insight.type,
|
|
149
|
+
title: insight.title,
|
|
150
|
+
text: insight.text,
|
|
151
|
+
evidence: insight.evidence,
|
|
152
|
+
speaker: insight.speaker ?? null,
|
|
153
|
+
confidence: insight.confidence,
|
|
154
|
+
importance: insight.importance,
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (rest.includes("--json") || outPath) {
|
|
161
|
+
if (!outPath) console.log(JSON.stringify(parsed, null, 2));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
console.log(`Call ${parsed.id}${parsed.title ? ` — ${parsed.title}` : ""} (${parsed.sourceSystem})`);
|
|
165
|
+
console.log(`${parsed.segments.length} segments · ${parsed.insights.length} insights (${parsed.summary.highImportance} high-importance)\n`);
|
|
166
|
+
for (const insight of parsed.insights) {
|
|
167
|
+
console.log(`[${insight.type}] (importance ${insight.importance}) ${insight.text}`);
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (subcommand === "link") {
|
|
173
|
+
const attendees = option(rest, "--attendees");
|
|
174
|
+
const domain = option(rest, "--domain");
|
|
175
|
+
if (!attendees && !domain) throw new Error("call link requires --attendees <emails,comma-separated> and/or --domain <example.com>");
|
|
176
|
+
const snapshot = await readSnapshot(rest);
|
|
177
|
+
const suggestion = suggestCallDeal(snapshot, {
|
|
178
|
+
attendeeEmails: attendees?.split(",").map((e) => e.trim()).filter(Boolean),
|
|
179
|
+
domain: domain ?? undefined,
|
|
180
|
+
});
|
|
181
|
+
if (rest.includes("--json")) {
|
|
182
|
+
console.log(JSON.stringify(suggestion, null, 2));
|
|
183
|
+
} else {
|
|
184
|
+
const marker = suggestion.confidence === "high" ? "✓" : suggestion.confidence === "low" ? "~" : "✗";
|
|
185
|
+
console.log(`${marker} [${suggestion.confidence}] ${suggestion.dealId ?? "no match"}${suggestion.dealName ? ` — ${suggestion.dealName}` : ""}`);
|
|
186
|
+
console.log(` ${suggestion.reason}`);
|
|
187
|
+
}
|
|
188
|
+
if (suggestion.confidence === "none") process.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (subcommand === "plan") {
|
|
193
|
+
const dealId = option(rest, "--deal");
|
|
194
|
+
if (!dealId) throw new Error("call plan requires --deal <dealId> (use `call link` to find it)");
|
|
195
|
+
const parsed = await loadParsedCall();
|
|
196
|
+
const snapshot = await readSnapshot(rest);
|
|
197
|
+
const deal = snapshot.deals.find((row) => row.id === dealId);
|
|
198
|
+
if (!deal) throw new Error(`Deal ${dealId} is not in the snapshot — check the id or the snapshot source.`);
|
|
199
|
+
|
|
200
|
+
const nextSteps = parsed.insights.filter((insight) => insight.type === "next_step");
|
|
201
|
+
if (nextSteps.length === 0) {
|
|
202
|
+
console.log("No next-step insights in this call — nothing to plan. (Other insight types are evidence, not writes.)");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const [top, ...others] = nextSteps;
|
|
206
|
+
const proposed = top.text.trim().slice(0, 255);
|
|
207
|
+
const current = deal.nextStep?.trim() ?? "";
|
|
208
|
+
const plan = buildCallPlan(parsed, deal, proposed, current, others.slice(0, 3));
|
|
209
|
+
|
|
210
|
+
if (saveRequested(rest)) {
|
|
211
|
+
await createFilePlanStore().save(plan);
|
|
212
|
+
console.log(
|
|
213
|
+
`Saved plan ${plan.id}. Review with \`fullstackgtm plans show ${plan.id}\`, approve with \`fullstackgtm plans approve ${plan.id} --operations <ids|all>\`, then \`fullstackgtm apply --plan-id ${plan.id} --provider <name>\`.`,
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
console.log(rest.includes("--json") ? JSON.stringify(plan, null, 2) : patchPlanToMarkdown(plan));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (subcommand === "score") {
|
|
222
|
+
if (rest.includes("--list-rubrics")) {
|
|
223
|
+
console.log(JSON.stringify(rubricPresets(), null, 2));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// Explicit-rubric problems surface before any credential or API work.
|
|
227
|
+
const rubricPath = option(rest, "--rubric");
|
|
228
|
+
let rubric: Rubric | undefined;
|
|
229
|
+
if (rubricPath) {
|
|
230
|
+
const rubricRaw = readFileSync(resolve(process.cwd(), rubricPath), "utf8");
|
|
231
|
+
try {
|
|
232
|
+
rubric = parseRubric(rubricRaw);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`${rubricPath} is not a valid rubric: ${error instanceof Error ? error.message : String(error)} Expected JSON like { "scale": 5, "dimensions": [{ "name": "...", "weight": 1, "rubric": "..." }] }.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const callTypeOpt = option(rest, "--call-type") as CallType | undefined;
|
|
240
|
+
if (callTypeOpt && !CALL_TYPE_IDS.includes(callTypeOpt)) {
|
|
241
|
+
throw new Error(`Unknown --call-type "${callTypeOpt}". One of: ${CALL_TYPE_IDS.join(", ")}.`);
|
|
242
|
+
}
|
|
243
|
+
const credential = await requireLlmCredential("score");
|
|
244
|
+
const transcriptPath = option(rest, "--transcript");
|
|
245
|
+
let transcriptText: string;
|
|
246
|
+
let title = option(rest, "--title") ?? undefined;
|
|
247
|
+
if (transcriptPath) {
|
|
248
|
+
transcriptText = normalizeTranscript(readFileSync(resolve(process.cwd(), transcriptPath), "utf8"));
|
|
249
|
+
} else {
|
|
250
|
+
const callPath = option(rest, "--call");
|
|
251
|
+
if (!callPath) throw new Error("call score requires --transcript <file> or --call <parsed.json>");
|
|
252
|
+
const parsed = JSON.parse(readFileSync(resolve(process.cwd(), callPath), "utf8")) as ParsedCall;
|
|
253
|
+
transcriptText = parsed.segments
|
|
254
|
+
.map((segment) => (segment.speaker ? `${segment.speaker}: ${segment.text}` : segment.text))
|
|
255
|
+
.join("\n");
|
|
256
|
+
title = title ?? parsed.title;
|
|
257
|
+
}
|
|
258
|
+
// Rubric selection: explicit --rubric wins, then --call-type, else the
|
|
259
|
+
// deterministic classifier picks the type-specific preset. No generic
|
|
260
|
+
// discovery rubric silently applied to a renewal anymore.
|
|
261
|
+
if (!rubric) {
|
|
262
|
+
const type = callTypeOpt ?? classifyCall(transcriptText).type;
|
|
263
|
+
rubric = rubricForCallType(type, DEFAULT_RUBRIC);
|
|
264
|
+
if (!rest.includes("--json")) {
|
|
265
|
+
const how = callTypeOpt ? `--call-type ${callTypeOpt}` : `auto-classified as ${type}`;
|
|
266
|
+
console.error(`Scoring with the "${rubric.name ?? "Generic"}" rubric (${how}). Override with --rubric <file> or --call-type <type>.`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const scoreWait = startElapsedStatus(
|
|
270
|
+
(elapsed) => `Scoring against "${rubric?.name ?? "Generic"}" · ${credential.provider} · ${elapsed}`,
|
|
271
|
+
);
|
|
272
|
+
let scorecard: CallScorecard;
|
|
273
|
+
try {
|
|
274
|
+
scorecard = await scoreCallLlm(transcriptText, rubric, {
|
|
275
|
+
...credential,
|
|
276
|
+
model: option(rest, "--model") ?? undefined,
|
|
277
|
+
title,
|
|
278
|
+
});
|
|
279
|
+
} finally {
|
|
280
|
+
scoreWait.done();
|
|
281
|
+
}
|
|
282
|
+
const outPath = option(rest, "--out");
|
|
283
|
+
if (outPath) writeFileSync(resolve(process.cwd(), outPath), `${JSON.stringify(scorecard, null, 2)}\n`);
|
|
284
|
+
if (rest.includes("--json")) {
|
|
285
|
+
console.log(JSON.stringify(scorecard, null, 2));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
console.log(renderScorecard(scorecard, title));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw new Error(`call supports: parse, classify, link, plan, score (got ${subcommand ?? "nothing"})`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function renderScorecard(scorecard: CallScorecard, title?: string): string {
|
|
296
|
+
const bandText = scorecard.band ? ` — ${scorecard.band.label}` : "";
|
|
297
|
+
const rubricLine = scorecard.rubricName ? `Rubric: ${scorecard.rubricName}${scorecard.callType ? ` (${scorecard.callType})` : ""}` : "";
|
|
298
|
+
const lines = [
|
|
299
|
+
`# Coaching Scorecard${title ? ` — ${title}` : ""}`,
|
|
300
|
+
"",
|
|
301
|
+
`**Overall: ${scorecard.overallScore}/${scorecard.scale}${bandText}** (model: ${scorecard.model})`,
|
|
302
|
+
...(scorecard.band?.meaning ? [`> ${scorecard.band.meaning}`] : []),
|
|
303
|
+
...(rubricLine ? ["", `_${rubricLine}_`] : []),
|
|
304
|
+
"",
|
|
305
|
+
"| Dimension | Score | | Coaching note |",
|
|
306
|
+
"| --- | --- | --- | --- |",
|
|
307
|
+
];
|
|
308
|
+
for (const dim of scorecard.dimensions) {
|
|
309
|
+
const filled = Math.round((dim.score / dim.maxScore) * 5);
|
|
310
|
+
const bar = "█".repeat(filled) + "░".repeat(5 - filled);
|
|
311
|
+
lines.push(`| ${dim.name} | ${dim.score}/${dim.maxScore} | ${bar} | ${dim.coachingNote} |`);
|
|
312
|
+
}
|
|
313
|
+
if (scorecard.highlights.length) {
|
|
314
|
+
lines.push("", "**Highlights**");
|
|
315
|
+
for (const h of scorecard.highlights) lines.push(`- ${h}`);
|
|
316
|
+
}
|
|
317
|
+
if (scorecard.missedItems.length) {
|
|
318
|
+
lines.push("", "**Missed**");
|
|
319
|
+
for (const m of scorecard.missedItems) lines.push(`- ${m}`);
|
|
320
|
+
}
|
|
321
|
+
return lines.join("\n");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildCallPlan(
|
|
325
|
+
parsed: ParsedCall,
|
|
326
|
+
deal: { id: string; name: string; nextStep?: string },
|
|
327
|
+
proposed: string,
|
|
328
|
+
current: string,
|
|
329
|
+
extraNextSteps: ExtractedCallInsight[],
|
|
330
|
+
): PatchPlan {
|
|
331
|
+
const findings: PatchPlan["findings"] = [];
|
|
332
|
+
const operations: PatchPlan["operations"] = [];
|
|
333
|
+
const nextStepEvidence = parsed.evidence.filter(
|
|
334
|
+
(item) => (item.metadata as { insightType?: string } | undefined)?.insightType === "next_step",
|
|
335
|
+
);
|
|
336
|
+
const evidenceIds = nextStepEvidence.map((item) => item.id);
|
|
337
|
+
|
|
338
|
+
if (current.toLowerCase() !== proposed.toLowerCase()) {
|
|
339
|
+
findings.push({
|
|
340
|
+
id: `finding_${parsed.id.replace(/^call_/, "")}_${deal.id}`,
|
|
341
|
+
objectType: "deal",
|
|
342
|
+
objectId: deal.id,
|
|
343
|
+
ruleId: "call-next-step-not-reflected-in-crm",
|
|
344
|
+
type: "call_next_step_not_reflected_in_crm",
|
|
345
|
+
title: "Call agreed a next step the CRM does not reflect",
|
|
346
|
+
severity: "warning",
|
|
347
|
+
summary: current
|
|
348
|
+
? `The call produced "${proposed}" but ${deal.name}'s next step still reads "${current}".`
|
|
349
|
+
: `The call produced "${proposed}" but ${deal.name} has no next step set.`,
|
|
350
|
+
recommendation: "Review the evidence and approve the next-step update.",
|
|
351
|
+
evidenceIds,
|
|
352
|
+
currentCrmValue: current || null,
|
|
353
|
+
proposedValue: proposed,
|
|
354
|
+
});
|
|
355
|
+
operations.push({
|
|
356
|
+
id: `op_${parsed.id.replace(/^call_/, "")}_next`,
|
|
357
|
+
objectType: "deal",
|
|
358
|
+
objectId: deal.id,
|
|
359
|
+
operation: "set_field",
|
|
360
|
+
field: "nextStep",
|
|
361
|
+
beforeValue: current || null,
|
|
362
|
+
afterValue: proposed,
|
|
363
|
+
reason: `Call evidence: ${nextStepEvidence[0]?.text.slice(0, 200) ?? proposed}`,
|
|
364
|
+
sourceRuleOrPolicy: "call_intelligence.next_step",
|
|
365
|
+
riskLevel: "high",
|
|
366
|
+
approvalRequired: true,
|
|
367
|
+
rollback: "Restore the previous deal next step (the before value) if the update is wrong.",
|
|
368
|
+
evidenceIds,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
for (const [index, extra] of extraNextSteps.entries()) {
|
|
372
|
+
operations.push({
|
|
373
|
+
id: `op_${parsed.id.replace(/^call_/, "")}_task${index}`,
|
|
374
|
+
objectType: "deal",
|
|
375
|
+
objectId: deal.id,
|
|
376
|
+
operation: "create_task",
|
|
377
|
+
field: "follow_up_task",
|
|
378
|
+
beforeValue: null,
|
|
379
|
+
afterValue: extra.text.trim().slice(0, 255),
|
|
380
|
+
reason: `Additional commitment from the call: ${extra.evidence.slice(0, 160)}`,
|
|
381
|
+
sourceRuleOrPolicy: "call_intelligence.follow_up",
|
|
382
|
+
riskLevel: "low",
|
|
383
|
+
approvalRequired: true,
|
|
384
|
+
rollback: "Close or delete the created task.",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
id: `patch_plan_${parsed.id.replace(/^call_/, "")}${deal.id.slice(-4)}`,
|
|
389
|
+
title: `Call evidence plan${parsed.title ? ` — ${parsed.title}` : ""} → ${deal.name}`,
|
|
390
|
+
createdAt: new Date().toISOString(),
|
|
391
|
+
status: "needs_approval",
|
|
392
|
+
dryRun: true,
|
|
393
|
+
summary: `${findings.length} finding(s) and ${operations.length} proposed operation(s) from call ${parsed.id}.`,
|
|
394
|
+
findings,
|
|
395
|
+
evidence: parsed.evidence,
|
|
396
|
+
operations,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Machine-readable contract surfaces: `capabilities`, `robot-docs`, and the
|
|
2
|
+
// `--help --json` payloads.
|
|
3
|
+
//
|
|
4
|
+
// DESIGN RULE — every list in these payloads is DERIVED from an existing
|
|
5
|
+
// single source of truth (the HELP table in help.ts, BESPOKE_HELP,
|
|
6
|
+
// readPackageInfo(), the shipped SKILL.md), never hand-maintained in
|
|
7
|
+
// parallel. Hand-written parallel lists were the core defect of the first
|
|
8
|
+
// attempt at this surface (PR #156): its `mutates` list omitted `merge`, its
|
|
9
|
+
// `dryRunOnly` list omitted snapshot/health/suggest, and its MCP tool list
|
|
10
|
+
// reported 4 of 8+ tools.
|
|
11
|
+
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { BESPOKE_HELP, HELP } from "./help.ts";
|
|
14
|
+
import { readPackageInfo } from "./shared.ts";
|
|
15
|
+
import { suggestCommand } from "./suggest.ts";
|
|
16
|
+
|
|
17
|
+
export type CommandAccess = "read-only" | "write-shaped";
|
|
18
|
+
|
|
19
|
+
// Lifecycle phase → access shape. "write-shaped" means the verb participates
|
|
20
|
+
// in the write path (emits dry-run patch plans, applies approved operations,
|
|
21
|
+
// or installs schedule entries); "read-only" verbs never stage or perform a
|
|
22
|
+
// write. Derived from each HELP entry's `phase`, with the deviations from
|
|
23
|
+
// the naive phase mapping documented inline.
|
|
24
|
+
const PHASE_ACCESS: Record<string, CommandAccess> = {
|
|
25
|
+
Setup: "read-only",
|
|
26
|
+
Detect: "read-only",
|
|
27
|
+
// Prevent gates writes but performs none: its one verb, `resolve`, only
|
|
28
|
+
// reads the snapshot and exits 0/2 — the CALLER decides whether to create.
|
|
29
|
+
Prevent: "read-only",
|
|
30
|
+
Remediate: "write-shaped",
|
|
31
|
+
Govern: "write-shaped",
|
|
32
|
+
"Govern / Verify": "write-shaped",
|
|
33
|
+
// Verify inspects the tamper-evident record of past applies (`audit-log`
|
|
34
|
+
// export/verify); it writes nothing.
|
|
35
|
+
Verify: "read-only",
|
|
36
|
+
// Continuous = `schedule`: installs crontab entries — a local write with
|
|
37
|
+
// ongoing side effects, and `apply --plan-id <id>` is schedulable.
|
|
38
|
+
Continuous: "write-shaped",
|
|
39
|
+
// Intelligence (`market`, `tam`) reads public pages / the CRM and writes
|
|
40
|
+
// only the local workspace — except `tam populate`, overridden below.
|
|
41
|
+
Intelligence: "read-only",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Per-verb overrides for commands whose phase would misclassify them.
|
|
45
|
+
const ACCESS_OVERRIDES: Record<string, CommandAccess> = {
|
|
46
|
+
// `suggest` sits on the Govern spine but is explicitly read-only: it
|
|
47
|
+
// derives values for requires_human_* placeholders from snapshot evidence
|
|
48
|
+
// and writes nothing — approval happens later in `plans approve`.
|
|
49
|
+
suggest: "read-only",
|
|
50
|
+
// `tam populate` installs recurring schedule entries (by delegating to the
|
|
51
|
+
// schedule layer) — the same "ongoing side effects" that make `schedule`
|
|
52
|
+
// write-shaped. An agent trusting this contract must not treat `tam` as
|
|
53
|
+
// inert.
|
|
54
|
+
tam: "write-shaped",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function commandAccess(name: string): CommandAccess {
|
|
58
|
+
const override = ACCESS_OVERRIDES[name];
|
|
59
|
+
if (override) return override;
|
|
60
|
+
const entry = HELP[name];
|
|
61
|
+
const access = entry ? PHASE_ACCESS[entry.phase] : undefined;
|
|
62
|
+
if (!access) {
|
|
63
|
+
// Fail loudly instead of guessing: a new phase value (or a command
|
|
64
|
+
// missing from HELP) must be classified deliberately, not defaulted.
|
|
65
|
+
throw new Error(
|
|
66
|
+
`No access mapping for command "${name}" (phase: ${entry?.phase ?? "not in HELP"}). ` +
|
|
67
|
+
"Add the phase to PHASE_ACCESS or an explicit ACCESS_OVERRIDES entry in src/cli/capabilities.ts.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return access;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Quoted from docs/api.md ("Exit codes: `0` success · `1` error · `2`
|
|
74
|
+
// findings/regressions at the requested gate (`--fail-on`,
|
|
75
|
+
// `--fail-on-new-findings`)"), plus the gate-shaped exit-2 semantics the
|
|
76
|
+
// help table documents for `resolve` and `icp eval`.
|
|
77
|
+
const EXIT_CODES = {
|
|
78
|
+
"0": "success",
|
|
79
|
+
"1": "error",
|
|
80
|
+
"2": "findings/regressions at the requested gate (--fail-on, --fail-on-new-findings); resolve: match found (exists/ambiguous); icp eval: accuracy below --min-accuracy",
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
83
|
+
// Quoted from the Safety section of the full help reference (help.ts usage()).
|
|
84
|
+
const SAFETY =
|
|
85
|
+
"Audits are read-only. Apply writes only operations you explicitly approve, " +
|
|
86
|
+
"and never writes requires_human_* placeholders without a --value override.";
|
|
87
|
+
|
|
88
|
+
function commandRecords() {
|
|
89
|
+
return Object.entries(HELP).map(([name, entry]) => ({
|
|
90
|
+
name,
|
|
91
|
+
summary: entry.summary,
|
|
92
|
+
phase: entry.phase,
|
|
93
|
+
access: commandAccess(name),
|
|
94
|
+
bespokeHelp: BESPOKE_HELP.includes(name),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function capabilitiesCommand(_args: string[]) {
|
|
99
|
+
const payload = {
|
|
100
|
+
ok: true,
|
|
101
|
+
tool: "fullstackgtm",
|
|
102
|
+
version: readPackageInfo().version,
|
|
103
|
+
contract: {
|
|
104
|
+
jsonFlag: "--json",
|
|
105
|
+
planFirst: true,
|
|
106
|
+
writePath:
|
|
107
|
+
"write-shaped verbs emit dry-run patch plans; a CRM write happens only through `apply` on operations approved via `plans approve` (or an explicit --approve list)",
|
|
108
|
+
safety: SAFETY,
|
|
109
|
+
exitCodes: EXIT_CODES,
|
|
110
|
+
},
|
|
111
|
+
commands: commandRecords(),
|
|
112
|
+
help: {
|
|
113
|
+
perCommand: "npx fullstackgtm <command> --help [--json]",
|
|
114
|
+
full: "npx fullstackgtm help --full",
|
|
115
|
+
agentGuide: "npx fullstackgtm robot-docs",
|
|
116
|
+
},
|
|
117
|
+
mcp: {
|
|
118
|
+
command: "npx -y fullstackgtm-mcp",
|
|
119
|
+
capabilitiesTool: "fullstackgtm_capabilities",
|
|
120
|
+
note: "the MCP tool inventory is self-reported by the fullstackgtm_capabilities tool, derived from the server's own registration table",
|
|
121
|
+
},
|
|
122
|
+
examples: [
|
|
123
|
+
"npx fullstackgtm capabilities --json",
|
|
124
|
+
"npx fullstackgtm audit --demo --json",
|
|
125
|
+
"npx fullstackgtm plans list --json",
|
|
126
|
+
"npx fullstackgtm doctor --json",
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function stripFrontmatter(markdown: string) {
|
|
133
|
+
if (!markdown.startsWith("---\n")) return markdown;
|
|
134
|
+
const end = markdown.indexOf("\n---\n", 4);
|
|
135
|
+
if (end === -1) return markdown;
|
|
136
|
+
return markdown.slice(end + "\n---\n".length);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function robotDocsCommand(args: string[]) {
|
|
140
|
+
const topic = args.find((arg) => !arg.startsWith("-"));
|
|
141
|
+
if (topic && topic !== "guide") {
|
|
142
|
+
console.error(`Unknown robot-docs topic: ${topic}. Run \`fullstackgtm robot-docs\` for the agent guide.`);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// The package ships exactly one maintained agent guide —
|
|
147
|
+
// skills/fullstackgtm/SKILL.md (in the npm tarball via the `files` field).
|
|
148
|
+
// Print its body instead of maintaining a second hand-written guide here.
|
|
149
|
+
// Resolved relative to the package root, same hop as readPackageInfo().
|
|
150
|
+
let raw: string;
|
|
151
|
+
try {
|
|
152
|
+
raw = readFileSync(new URL("../../skills/fullstackgtm/SKILL.md", import.meta.url), "utf8");
|
|
153
|
+
} catch {
|
|
154
|
+
console.error(
|
|
155
|
+
"robot-docs: the packaged agent guide (skills/fullstackgtm/SKILL.md) is missing from this install. " +
|
|
156
|
+
"Reinstall the package, or read it at https://github.com/fullstackgtm/core/blob/main/packages/fullstackgtm/skills/fullstackgtm/SKILL.md",
|
|
157
|
+
);
|
|
158
|
+
process.exitCode = 1;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log(stripFrontmatter(raw).trim());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── `--help --json` / `help <cmd> --json` ──────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export function commandHelpJsonPayload(command: string) {
|
|
167
|
+
const entry = HELP[command];
|
|
168
|
+
if (!entry) return null;
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
command,
|
|
172
|
+
summary: entry.summary,
|
|
173
|
+
phase: entry.phase,
|
|
174
|
+
access: commandAccess(command),
|
|
175
|
+
synopsis: entry.synopsis,
|
|
176
|
+
detail: entry.detail ?? null,
|
|
177
|
+
options: (entry.options ?? []).map(([flag, description]) => ({ flag, description })),
|
|
178
|
+
seeAlso: entry.seeAlso ?? [],
|
|
179
|
+
// Bespoke verbs keep a richer plain-text subcommand reference of their own.
|
|
180
|
+
bespokeHelp: BESPOKE_HELP.includes(command),
|
|
181
|
+
fullReference: "npx fullstackgtm help --full",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Nearest-command lookup lives in suggest.ts alongside the flag-typo
|
|
186
|
+
// handler; re-exported here so existing importers keep working.
|
|
187
|
+
export { suggestCommand } from "./suggest.ts";
|
|
188
|
+
|
|
189
|
+
export function unknownCommandEnvelope(command: string) {
|
|
190
|
+
const suggestion = suggestCommand(command);
|
|
191
|
+
return {
|
|
192
|
+
ok: false as const,
|
|
193
|
+
error: {
|
|
194
|
+
code: "UNKNOWN_COMMAND" as const,
|
|
195
|
+
message: `Unknown command: ${command}`,
|
|
196
|
+
hints: [
|
|
197
|
+
suggestion
|
|
198
|
+
? `Did you mean: npx fullstackgtm ${suggestion}`
|
|
199
|
+
: "Run `npx fullstackgtm capabilities --json` to list supported commands.",
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Print machine-readable help for one command; unknown commands get the
|
|
206
|
+
// structured UNKNOWN_COMMAND envelope (exit 1) instead of a text wall.
|
|
207
|
+
export function printCommandHelpJson(command: string) {
|
|
208
|
+
const payload = commandHelpJsonPayload(command);
|
|
209
|
+
if (!payload) {
|
|
210
|
+
console.log(JSON.stringify(unknownCommandEnvelope(command), null, 2));
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
215
|
+
}
|