fullstackgtm 0.14.0 → 0.14.1

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 CHANGED
@@ -5,6 +5,26 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project adheres to [Semantic Versioning](https://semver.org/).
6
6
  The path to 1.0 is planned in [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md).
7
7
 
8
+ ## [0.14.1] — 2026-06-11
9
+
10
+ Fixes from the 0.14.0 journey verification (4 agents, 21 checks).
11
+
12
+ ### Fixed
13
+
14
+ - **`call score` no longer suggests a flag it rejects**: the keyless error
15
+ said "pass --deterministic" but score has no non-LLM mode — its error now
16
+ says exactly that. Rubric files are also validated *before* the
17
+ credential check (keyless users see rubric errors), and a non-JSON rubric
18
+ names the file and the expected shape instead of a raw parse error.
19
+ - **NDJSON rows carry `extractor`** — LLM and deterministic rows were
20
+ per-row indistinguishable in warehouse loads, contradicting the
21
+ provenance claim. (Docs wording was right at the parse-result and
22
+ evidence level; now it is true per-row too.)
23
+ - **`call … --help` prints help** instead of being shadowed by argument
24
+ and credential checks.
25
+ - `login anthropic|openai` gets the same explicit argv-secret rejection
26
+ (`--token`/`--key`/`--api-key`) as the CRM providers.
27
+
8
28
  ## [0.14.0] — 2026-06-11
9
29
 
10
30
  LLM-powered call intelligence, bring-your-own-key. The dry-run replications
package/dist/cli.js CHANGED
@@ -418,6 +418,17 @@ function parseValueOverrides(args) {
418
418
  }
419
419
  async function callCommand(args) {
420
420
  const [subcommand, ...rest] = args;
421
+ if (args.includes("--help") || args.includes("-h")) {
422
+ console.log(`call parse --transcript <file> [--title t] [--source s] [--model m] [--deterministic] [--json|--ndjson] [--out <path>]
423
+ call score --transcript <file>|--call <parsed.json> [--rubric <rubric.json>] [--model m] [--json|--out <path>]
424
+ call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
425
+ call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
426
+
427
+ parse/score default to LLM extraction (Anthropic or OpenAI key via env,
428
+ \`login anthropic|openai\`, or a one-time prompt). parse --deterministic is
429
+ the free keyword baseline; score always needs a key (scoring is LLM work).`);
430
+ return;
431
+ }
421
432
  const loadParsedCall = async () => {
422
433
  const callPath = option(rest, "--call");
423
434
  if (callPath) {
@@ -462,6 +473,7 @@ async function callCommand(args) {
462
473
  call_id: parsed.id,
463
474
  call_title: parsed.title ?? null,
464
475
  source_system: parsed.sourceSystem,
476
+ extractor: parsed.extractor,
465
477
  type: insight.type,
466
478
  title: insight.title,
467
479
  text: insight.text,
@@ -534,11 +546,19 @@ async function callCommand(args) {
534
546
  return;
535
547
  }
536
548
  if (subcommand === "score") {
537
- const credential = await requireLlmCredential();
549
+ // Rubric problems surface before any credential or API work.
538
550
  const rubricPath = option(rest, "--rubric");
539
- const rubric = rubricPath
540
- ? parseRubric(readFileSync(resolve(process.cwd(), rubricPath), "utf8"))
541
- : DEFAULT_RUBRIC;
551
+ let rubric = DEFAULT_RUBRIC;
552
+ if (rubricPath) {
553
+ const rubricRaw = readFileSync(resolve(process.cwd(), rubricPath), "utf8");
554
+ try {
555
+ rubric = parseRubric(rubricRaw);
556
+ }
557
+ catch (error) {
558
+ throw new Error(`${rubricPath} is not a valid rubric: ${error instanceof Error ? error.message : String(error)} Expected JSON like { "scale": 5, "dimensions": [{ "name": "...", "weight": 1, "rubric": "..." }] }.`);
559
+ }
560
+ }
561
+ const credential = await requireLlmCredential("score");
542
562
  const transcriptPath = option(rest, "--transcript");
543
563
  let transcriptText;
544
564
  let title = option(rest, "--title") ?? undefined;
@@ -577,12 +597,14 @@ async function callCommand(args) {
577
597
  * TTY a missing key is captured once (validated, stored 0600 like provider
578
598
  * logins). Non-interactive contexts get an actionable error instead.
579
599
  */
580
- async function requireLlmCredential() {
600
+ async function requireLlmCredential(command = "parse") {
581
601
  const resolved = resolveLlmCredential();
582
602
  if (resolved)
583
603
  return resolved;
604
+ // Scoring is inherently LLM work — there is no keyword fallback to suggest.
605
+ const fallbackHint = command === "parse" ? ", or pass --deterministic for the free keyword baseline" : " (call score has no non-LLM mode)";
584
606
  if (!process.stdin.isTTY) {
585
- throw new Error("LLM extraction needs an API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, run `echo \"$KEY\" | fullstackgtm login anthropic` (or `login openai`) once, or pass --deterministic for the free keyword baseline.");
607
+ throw new Error(`LLM ${command === "score" ? "scoring" : "extraction"} needs an API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or run \`echo "$KEY" | fullstackgtm login anthropic\` (or \`login openai\`) once${fallbackHint}.`);
586
608
  }
587
609
  console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
588
610
  console.error(`Paste it once; it is validated and stored at ${credentialsPath()} (file mode 0600), like CRM logins.`);
@@ -1177,6 +1199,7 @@ async function login(args) {
1177
1199
  return;
1178
1200
  }
1179
1201
  if (provider === "anthropic" || provider === "openai") {
1202
+ rejectArgvSecret(args, "--token", "--key", "--api-key");
1180
1203
  const key = await readSecret(`${provider} API key (${provider === "anthropic" ? "sk-ant-..." : "sk-..."})`);
1181
1204
  if (!key)
1182
1205
  throw new Error(`No ${provider} key provided.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Full Stack GTM",
package/src/cli.ts CHANGED
@@ -490,6 +490,17 @@ function parseValueOverrides(args: string[]) {
490
490
 
491
491
  async function callCommand(args: string[]) {
492
492
  const [subcommand, ...rest] = args;
493
+ if (args.includes("--help") || args.includes("-h")) {
494
+ console.log(`call parse --transcript <file> [--title t] [--source s] [--model m] [--deterministic] [--json|--ndjson] [--out <path>]
495
+ call score --transcript <file>|--call <parsed.json> [--rubric <rubric.json>] [--model m] [--json|--out <path>]
496
+ call link --attendees <a@x.com,...> | --domain <x.com> [source options] [--json]
497
+ call plan --transcript <file>|--call <parsed.json> --deal <id> [source options] [--save|--json]
498
+
499
+ parse/score default to LLM extraction (Anthropic or OpenAI key via env,
500
+ \`login anthropic|openai\`, or a one-time prompt). parse --deterministic is
501
+ the free keyword baseline; score always needs a key (scoring is LLM work).`);
502
+ return;
503
+ }
493
504
 
494
505
  const loadParsedCall = async (): Promise<ParsedCall> => {
495
506
  const callPath = option(rest, "--call");
@@ -535,6 +546,7 @@ async function callCommand(args: string[]) {
535
546
  call_id: parsed.id,
536
547
  call_title: parsed.title ?? null,
537
548
  source_system: parsed.sourceSystem,
549
+ extractor: parsed.extractor,
538
550
  type: insight.type,
539
551
  title: insight.title,
540
552
  text: insight.text,
@@ -609,11 +621,20 @@ async function callCommand(args: string[]) {
609
621
  }
610
622
 
611
623
  if (subcommand === "score") {
612
- const credential = await requireLlmCredential();
624
+ // Rubric problems surface before any credential or API work.
613
625
  const rubricPath = option(rest, "--rubric");
614
- const rubric = rubricPath
615
- ? parseRubric(readFileSync(resolve(process.cwd(), rubricPath), "utf8"))
616
- : DEFAULT_RUBRIC;
626
+ let rubric = DEFAULT_RUBRIC;
627
+ if (rubricPath) {
628
+ const rubricRaw = readFileSync(resolve(process.cwd(), rubricPath), "utf8");
629
+ try {
630
+ rubric = parseRubric(rubricRaw);
631
+ } catch (error) {
632
+ throw new Error(
633
+ `${rubricPath} is not a valid rubric: ${error instanceof Error ? error.message : String(error)} Expected JSON like { "scale": 5, "dimensions": [{ "name": "...", "weight": 1, "rubric": "..." }] }.`,
634
+ );
635
+ }
636
+ }
637
+ const credential = await requireLlmCredential("score");
617
638
  const transcriptPath = option(rest, "--transcript");
618
639
  let transcriptText: string;
619
640
  let title = option(rest, "--title") ?? undefined;
@@ -651,12 +672,15 @@ async function callCommand(args: string[]) {
651
672
  * TTY a missing key is captured once (validated, stored 0600 like provider
652
673
  * logins). Non-interactive contexts get an actionable error instead.
653
674
  */
654
- async function requireLlmCredential(): Promise<{ provider: LlmProvider; apiKey: string }> {
675
+ async function requireLlmCredential(command: "parse" | "score" = "parse"): Promise<{ provider: LlmProvider; apiKey: string }> {
655
676
  const resolved = resolveLlmCredential();
656
677
  if (resolved) return resolved;
678
+ // Scoring is inherently LLM work — there is no keyword fallback to suggest.
679
+ const fallbackHint =
680
+ command === "parse" ? ", or pass --deterministic for the free keyword baseline" : " (call score has no non-LLM mode)";
657
681
  if (!process.stdin.isTTY) {
658
682
  throw new Error(
659
- "LLM extraction needs an API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, run `echo \"$KEY\" | fullstackgtm login anthropic` (or `login openai`) once, or pass --deterministic for the free keyword baseline.",
683
+ `LLM ${command === "score" ? "scoring" : "extraction"} needs an API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or run \`echo "$KEY" | fullstackgtm login anthropic\` (or \`login openai\`) once${fallbackHint}.`,
660
684
  );
661
685
  }
662
686
  console.error("LLM parsing needs an API key (Anthropic or OpenAI) — yours, used directly with the provider.");
@@ -1325,6 +1349,7 @@ async function login(args: string[]) {
1325
1349
  return;
1326
1350
  }
1327
1351
  if (provider === "anthropic" || provider === "openai") {
1352
+ rejectArgvSecret(args, "--token", "--key", "--api-key");
1328
1353
  const key = await readSecret(`${provider} API key (${provider === "anthropic" ? "sk-ant-..." : "sk-..."})`);
1329
1354
  if (!key) throw new Error(`No ${provider} key provided.`);
1330
1355
  if (!args.includes("--no-validate")) {