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 +20 -0
- package/dist/cli.js +29 -6
- package/package.json +1 -1
- package/src/cli.ts +31 -6
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
|
-
|
|
549
|
+
// Rubric problems surface before any credential or API work.
|
|
538
550
|
const rubricPath = option(rest, "--rubric");
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
624
|
+
// Rubric problems surface before any credential or API work.
|
|
613
625
|
const rubricPath = option(rest, "--rubric");
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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")) {
|