fullstackgtm 0.52.2 → 0.52.4
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 +15 -0
- package/dist/cli/icp.js +5 -1
- package/dist/icpDerive.js +1 -1
- package/dist/llm.d.ts +3 -0
- package/dist/llm.js +26 -3
- package/package.json +1 -1
- package/src/cli/icp.ts +3 -1
- package/src/icpDerive.ts +1 -1
- package/src/llm.ts +27 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](https://github.com/fullst
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.52.4] — 2026-07-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Saving an interactively reviewed website-derived ICP no longer prints a
|
|
15
|
+
duplicate copy of the card below the file-written confirmation.
|
|
16
|
+
|
|
17
|
+
## [0.52.3] — 2026-07-11
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- OpenRouter ICP derivation replaces numeric reasoning-token heartbeats with
|
|
22
|
+
safe, subject-aware progress summaries inferred transiently from the model's
|
|
23
|
+
reasoning stream; raw chain-of-thought is never displayed or retained.
|
|
24
|
+
|
|
10
25
|
## [0.52.2] — 2026-07-11
|
|
11
26
|
|
|
12
27
|
### Fixed
|
package/dist/cli/icp.js
CHANGED
|
@@ -225,6 +225,7 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
225
225
|
finally {
|
|
226
226
|
status.done();
|
|
227
227
|
}
|
|
228
|
+
const reviewedInteractively = !rest.includes("--json") && !rest.includes("--no-interactive") && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
228
229
|
if (!rest.includes("--json") && !rest.includes("--no-interactive")) {
|
|
229
230
|
const reviewed = await reviewDerivedIcp(derived);
|
|
230
231
|
if (!reviewed)
|
|
@@ -237,7 +238,10 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
237
238
|
writeFileSync(path, `${JSON.stringify(derived.icp, null, 2)}\n`);
|
|
238
239
|
console.error(`Wrote reviewed ICP to ${path}. Next: fullstackgtm enrich acquire --source clay --icp ${path}`);
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
+
if (rest.includes("--json"))
|
|
242
|
+
console.log(JSON.stringify(derived, null, 2));
|
|
243
|
+
else if (!reviewedInteractively)
|
|
244
|
+
console.log(renderDerivedIcp(derived));
|
|
241
245
|
return;
|
|
242
246
|
}
|
|
243
247
|
if (sub === "set") {
|
package/dist/icpDerive.js
CHANGED
|
@@ -98,7 +98,7 @@ DOMAIN: ${target.domain}
|
|
|
98
98
|
model,
|
|
99
99
|
...(isOpenRouter ? {
|
|
100
100
|
onReasoningSummary: (summary) => args.onProgress?.({ stage: "model", message: `${model} reasoning summary: ${summary.slice(0, 240)}` }),
|
|
101
|
-
|
|
101
|
+
onReasoningPhase: (phase) => args.onProgress?.({ stage: "model", message: `${model}: ${phase}` }),
|
|
102
102
|
} : {}),
|
|
103
103
|
});
|
|
104
104
|
const companyName = typeof raw.companyName === "string" ? raw.companyName.trim().slice(0, 100) : target.domain;
|
package/dist/llm.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ export type LlmCallOptions = {
|
|
|
40
40
|
* deliberately not exposed; only provider-authored summaries and activity. */
|
|
41
41
|
onReasoningSummary?: (summary: string) => void;
|
|
42
42
|
onReasoningActivity?: (receivedCharacters: number) => void;
|
|
43
|
+
/** Safe, coarse phase inferred transiently from reasoning text. The raw text
|
|
44
|
+
* is never passed to callers, logged, or retained after the request. */
|
|
45
|
+
onReasoningPhase?: (phase: string) => void;
|
|
43
46
|
};
|
|
44
47
|
export type LlmExtractedInsight = ExtractedCallInsight & {
|
|
45
48
|
owner?: string;
|
package/dist/llm.js
CHANGED
|
@@ -315,7 +315,7 @@ export async function forcedToolCall(prompt, toolName, schema, model, options) {
|
|
|
315
315
|
tools: [{ type: "function", function: { name: toolName, parameters: schema } }],
|
|
316
316
|
tool_choice: { type: "function", function: { name: toolName } },
|
|
317
317
|
};
|
|
318
|
-
if (options.onReasoningSummary || options.onReasoningActivity) {
|
|
318
|
+
if (options.onReasoningSummary || options.onReasoningActivity || options.onReasoningPhase) {
|
|
319
319
|
return streamOpenAiToolCall(fetchImpl, openaiUrl, requestBody, options);
|
|
320
320
|
}
|
|
321
321
|
const response = await llmFetch(fetchImpl, openaiUrl, {
|
|
@@ -352,7 +352,9 @@ async function streamOpenAiToolCall(fetchImpl, url, body, options) {
|
|
|
352
352
|
let argumentsJson = "";
|
|
353
353
|
let reasoningCharacters = 0;
|
|
354
354
|
let lastActivityReport = 0;
|
|
355
|
+
let recentReasoning = "";
|
|
355
356
|
const seenSummaries = new Set();
|
|
357
|
+
const seenPhases = new Set();
|
|
356
358
|
while (true) {
|
|
357
359
|
const { value, done } = await reader.read();
|
|
358
360
|
buffer += decoder.decode(value, { stream: !done });
|
|
@@ -373,9 +375,18 @@ async function streamOpenAiToolCall(fetchImpl, url, body, options) {
|
|
|
373
375
|
}
|
|
374
376
|
const delta = chunk.choices?.[0]?.delta;
|
|
375
377
|
argumentsJson += delta?.tool_calls?.[0]?.function?.arguments ?? "";
|
|
376
|
-
|
|
378
|
+
const detailText = (delta?.reasoning_details ?? []).filter((detail) => detail.type === "reasoning.text").map((detail) => detail.text ?? "").join("");
|
|
379
|
+
const reasoningText = delta?.reasoning || detailText;
|
|
380
|
+
reasoningCharacters += reasoningText.length;
|
|
381
|
+
if (reasoningText) {
|
|
382
|
+
recentReasoning = `${recentReasoning}${reasoningText}`.slice(-600);
|
|
383
|
+
const phase = reasoningPhase(recentReasoning);
|
|
384
|
+
if (phase && !seenPhases.has(phase)) {
|
|
385
|
+
seenPhases.add(phase);
|
|
386
|
+
options.onReasoningPhase?.(phase);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
377
389
|
for (const detail of delta?.reasoning_details ?? []) {
|
|
378
|
-
reasoningCharacters += detail.text?.length ?? detail.summary?.length ?? 0;
|
|
379
390
|
if (detail.type !== "reasoning.summary" || !detail.summary)
|
|
380
391
|
continue;
|
|
381
392
|
const summary = detail.summary.replace(/\s+/g, " ").trim();
|
|
@@ -396,6 +407,18 @@ async function streamOpenAiToolCall(fetchImpl, url, body, options) {
|
|
|
396
407
|
throw new Error("OpenAI-compatible model returned no streamed tool call — try again or a different --model.");
|
|
397
408
|
return JSON.parse(argumentsJson);
|
|
398
409
|
}
|
|
410
|
+
function reasoningPhase(text) {
|
|
411
|
+
const normalized = text.toLowerCase();
|
|
412
|
+
const phases = [
|
|
413
|
+
[/quote|evidence|verbatim|source text|supporting claim/, "checking candidate evidence against website quotes"],
|
|
414
|
+
[/buyer|persona|job title|seniority|department|decision.?maker/, "resolving likely buyer roles, departments, and seniority"],
|
|
415
|
+
[/industr|employee|company size|firmographic|geograph|target account/, "narrowing target-account industries, size, and geography"],
|
|
416
|
+
[/intent|trigger|timing|why now|signal/, "identifying intent and timing signals"],
|
|
417
|
+
[/offer|product|service|value proposition|capabilit/, "interpreting the company offer and value proposition"],
|
|
418
|
+
[/schema|structured|json|field|assemble|final/, "assembling the structured ICP response"],
|
|
419
|
+
];
|
|
420
|
+
return phases.find(([pattern]) => pattern.test(normalized))?.[1];
|
|
421
|
+
}
|
|
399
422
|
async function llmFetch(fetchImpl, url, init) {
|
|
400
423
|
let response;
|
|
401
424
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullstackgtm",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.4",
|
|
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 LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
|
package/src/cli/icp.ts
CHANGED
|
@@ -216,6 +216,7 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
216
216
|
} finally {
|
|
217
217
|
status.done();
|
|
218
218
|
}
|
|
219
|
+
const reviewedInteractively = !rest.includes("--json") && !rest.includes("--no-interactive") && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
219
220
|
if (!rest.includes("--json") && !rest.includes("--no-interactive")) {
|
|
220
221
|
const reviewed = await reviewDerivedIcp(derived);
|
|
221
222
|
if (!reviewed) throw new Error("ICP review cancelled; no file was written.");
|
|
@@ -227,7 +228,8 @@ ops. Develop one by interview, then \`enrich acquire\` picks up ./icp.json.
|
|
|
227
228
|
writeFileSync(path, `${JSON.stringify(derived.icp, null, 2)}\n`);
|
|
228
229
|
console.error(`Wrote reviewed ICP to ${path}. Next: fullstackgtm enrich acquire --source clay --icp ${path}`);
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
+
if (rest.includes("--json")) console.log(JSON.stringify(derived, null, 2));
|
|
232
|
+
else if (!reviewedInteractively) console.log(renderDerivedIcp(derived));
|
|
231
233
|
return;
|
|
232
234
|
}
|
|
233
235
|
if (sub === "set") {
|
package/src/icpDerive.ts
CHANGED
|
@@ -112,7 +112,7 @@ DOMAIN: ${target.domain}
|
|
|
112
112
|
model,
|
|
113
113
|
...(isOpenRouter ? {
|
|
114
114
|
onReasoningSummary: (summary: string) => args.onProgress?.({ stage: "model", message: `${model} reasoning summary: ${summary.slice(0, 240)}` }),
|
|
115
|
-
|
|
115
|
+
onReasoningPhase: (phase: string) => args.onProgress?.({ stage: "model", message: `${model}: ${phase}` }),
|
|
116
116
|
} : {}),
|
|
117
117
|
}) as Record<string, unknown>;
|
|
118
118
|
const companyName = typeof raw.companyName === "string" ? raw.companyName.trim().slice(0, 100) : target.domain;
|
package/src/llm.ts
CHANGED
|
@@ -123,6 +123,9 @@ export type LlmCallOptions = {
|
|
|
123
123
|
* deliberately not exposed; only provider-authored summaries and activity. */
|
|
124
124
|
onReasoningSummary?: (summary: string) => void;
|
|
125
125
|
onReasoningActivity?: (receivedCharacters: number) => void;
|
|
126
|
+
/** Safe, coarse phase inferred transiently from reasoning text. The raw text
|
|
127
|
+
* is never passed to callers, logged, or retained after the request. */
|
|
128
|
+
onReasoningPhase?: (phase: string) => void;
|
|
126
129
|
};
|
|
127
130
|
|
|
128
131
|
export type LlmExtractedInsight = ExtractedCallInsight & {
|
|
@@ -458,7 +461,7 @@ export async function forcedToolCall(
|
|
|
458
461
|
tools: [{ type: "function", function: { name: toolName, parameters: schema } }],
|
|
459
462
|
tool_choice: { type: "function", function: { name: toolName } },
|
|
460
463
|
};
|
|
461
|
-
if (options.onReasoningSummary || options.onReasoningActivity) {
|
|
464
|
+
if (options.onReasoningSummary || options.onReasoningActivity || options.onReasoningPhase) {
|
|
462
465
|
return streamOpenAiToolCall(fetchImpl, openaiUrl, requestBody, options);
|
|
463
466
|
}
|
|
464
467
|
const response = await llmFetch(fetchImpl, openaiUrl, {
|
|
@@ -497,7 +500,9 @@ async function streamOpenAiToolCall(
|
|
|
497
500
|
let argumentsJson = "";
|
|
498
501
|
let reasoningCharacters = 0;
|
|
499
502
|
let lastActivityReport = 0;
|
|
503
|
+
let recentReasoning = "";
|
|
500
504
|
const seenSummaries = new Set<string>();
|
|
505
|
+
const seenPhases = new Set<string>();
|
|
501
506
|
while (true) {
|
|
502
507
|
const { value, done } = await reader.read();
|
|
503
508
|
buffer += decoder.decode(value, { stream: !done });
|
|
@@ -511,9 +516,15 @@ async function streamOpenAiToolCall(
|
|
|
511
516
|
try { chunk = JSON.parse(data); } catch { continue; }
|
|
512
517
|
const delta = chunk.choices?.[0]?.delta;
|
|
513
518
|
argumentsJson += delta?.tool_calls?.[0]?.function?.arguments ?? "";
|
|
514
|
-
|
|
519
|
+
const detailText = (delta?.reasoning_details ?? []).filter((detail) => detail.type === "reasoning.text").map((detail) => detail.text ?? "").join("");
|
|
520
|
+
const reasoningText = delta?.reasoning || detailText;
|
|
521
|
+
reasoningCharacters += reasoningText.length;
|
|
522
|
+
if (reasoningText) {
|
|
523
|
+
recentReasoning = `${recentReasoning}${reasoningText}`.slice(-600);
|
|
524
|
+
const phase = reasoningPhase(recentReasoning);
|
|
525
|
+
if (phase && !seenPhases.has(phase)) { seenPhases.add(phase); options.onReasoningPhase?.(phase); }
|
|
526
|
+
}
|
|
515
527
|
for (const detail of delta?.reasoning_details ?? []) {
|
|
516
|
-
reasoningCharacters += detail.text?.length ?? detail.summary?.length ?? 0;
|
|
517
528
|
if (detail.type !== "reasoning.summary" || !detail.summary) continue;
|
|
518
529
|
const summary = detail.summary.replace(/\s+/g, " ").trim();
|
|
519
530
|
if (summary && !seenSummaries.has(summary)) { seenSummaries.add(summary); options.onReasoningSummary?.(summary); }
|
|
@@ -529,6 +540,19 @@ async function streamOpenAiToolCall(
|
|
|
529
540
|
return JSON.parse(argumentsJson);
|
|
530
541
|
}
|
|
531
542
|
|
|
543
|
+
function reasoningPhase(text: string): string | undefined {
|
|
544
|
+
const normalized = text.toLowerCase();
|
|
545
|
+
const phases: Array<[RegExp, string]> = [
|
|
546
|
+
[/quote|evidence|verbatim|source text|supporting claim/, "checking candidate evidence against website quotes"],
|
|
547
|
+
[/buyer|persona|job title|seniority|department|decision.?maker/, "resolving likely buyer roles, departments, and seniority"],
|
|
548
|
+
[/industr|employee|company size|firmographic|geograph|target account/, "narrowing target-account industries, size, and geography"],
|
|
549
|
+
[/intent|trigger|timing|why now|signal/, "identifying intent and timing signals"],
|
|
550
|
+
[/offer|product|service|value proposition|capabilit/, "interpreting the company offer and value proposition"],
|
|
551
|
+
[/schema|structured|json|field|assemble|final/, "assembling the structured ICP response"],
|
|
552
|
+
];
|
|
553
|
+
return phases.find(([pattern]) => pattern.test(normalized))?.[1];
|
|
554
|
+
}
|
|
555
|
+
|
|
532
556
|
async function llmFetch(fetchImpl: typeof fetch, url: string, init: RequestInit): Promise<unknown> {
|
|
533
557
|
let response: Response;
|
|
534
558
|
try {
|