@sonnechasser/ntrp 0.3.4 → 0.3.5
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/dist/ai/guardrails-smoke.js +1696 -413
- package/dist/ai/guardrails-smoke.js.map +1 -1
- package/dist/conversation/deepdive-smoke.js +3010 -0
- package/dist/conversation/deepdive-smoke.js.map +1 -0
- package/dist/conversation/loop-guard-smoke.js +4116 -1401
- package/dist/conversation/loop-guard-smoke.js.map +1 -1
- package/dist/index.js +4356 -1576
- package/dist/index.js.map +1 -1
- package/dist/investigation/quality-eval-cli.js +2058 -775
- package/dist/investigation/quality-eval-cli.js.map +1 -1
- package/dist/investigation/verbosity-cli.js +2064 -781
- package/dist/investigation/verbosity-cli.js.map +1 -1
- package/dist/mcp/server.js +2058 -774
- package/dist/mcp/server.js.map +1 -1
- package/dist/services/exports-registry-smoke.js +858 -0
- package/dist/services/exports-registry-smoke.js.map +1 -0
- package/dist/services/transcript-smoke.js +170 -34
- package/dist/services/transcript-smoke.js.map +1 -1
- package/dist/strategist/strategist-smoke.js +970 -65
- package/dist/strategist/strategist-smoke.js.map +1 -1
- package/dist/whimsy/time-bank-smoke.js +4065 -1446
- package/dist/whimsy/time-bank-smoke.js.map +1 -1
- package/package.json +3 -1
package/dist/mcp/server.js
CHANGED
|
@@ -113,6 +113,259 @@ var init_formatters = __esm({
|
|
|
113
113
|
}
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
// src/config/store.ts
|
|
117
|
+
var store_exports = {};
|
|
118
|
+
__export(store_exports, {
|
|
119
|
+
deleteConfigValue: () => deleteConfigValue,
|
|
120
|
+
getConfigValue: () => getConfigValue,
|
|
121
|
+
getConfiguredAiInboxDir: () => getConfiguredAiInboxDir,
|
|
122
|
+
getExportsDir: () => getExportsDir,
|
|
123
|
+
getKnowledgeDir: () => getKnowledgeDir,
|
|
124
|
+
getMemoryDir: () => getMemoryDir,
|
|
125
|
+
getStrategiesDir: () => getStrategiesDir,
|
|
126
|
+
getWinsDir: () => getWinsDir,
|
|
127
|
+
loadConfig: () => loadConfig,
|
|
128
|
+
ntrpHome: () => ntrpHome,
|
|
129
|
+
resetConfigCache: () => resetConfigCache,
|
|
130
|
+
saveConfig: () => saveConfig,
|
|
131
|
+
setConfigValue: () => setConfigValue
|
|
132
|
+
});
|
|
133
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
134
|
+
import { homedir } from "os";
|
|
135
|
+
import { join, resolve } from "path";
|
|
136
|
+
function ntrpHome() {
|
|
137
|
+
return NTRP_DIR;
|
|
138
|
+
}
|
|
139
|
+
function ensureDir() {
|
|
140
|
+
if (!existsSync(NTRP_DIR)) {
|
|
141
|
+
mkdirSync(NTRP_DIR, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function loadConfig() {
|
|
145
|
+
if (cachedConfig) return cachedConfig;
|
|
146
|
+
ensureDir();
|
|
147
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
148
|
+
cachedConfig = {};
|
|
149
|
+
return cachedConfig;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
cachedConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
153
|
+
} catch {
|
|
154
|
+
cachedConfig = {};
|
|
155
|
+
}
|
|
156
|
+
return cachedConfig;
|
|
157
|
+
}
|
|
158
|
+
function saveConfig(config) {
|
|
159
|
+
ensureDir();
|
|
160
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
161
|
+
cachedConfig = config;
|
|
162
|
+
}
|
|
163
|
+
function resetConfigCache() {
|
|
164
|
+
cachedConfig = null;
|
|
165
|
+
}
|
|
166
|
+
function getConfigValue(key) {
|
|
167
|
+
if (key === "api-key") return loadConfig()["api-key"];
|
|
168
|
+
if (key === "license-key") return process.env.NTRP_LICENSE_KEY ?? loadConfig()["license-key"];
|
|
169
|
+
const config = loadConfig();
|
|
170
|
+
return config[key];
|
|
171
|
+
}
|
|
172
|
+
function setConfigValue(key, value) {
|
|
173
|
+
const config = loadConfig();
|
|
174
|
+
config[key] = value;
|
|
175
|
+
saveConfig(config);
|
|
176
|
+
}
|
|
177
|
+
function deleteConfigValue(key) {
|
|
178
|
+
const config = loadConfig();
|
|
179
|
+
delete config[key];
|
|
180
|
+
saveConfig(config);
|
|
181
|
+
}
|
|
182
|
+
function getExportsDir() {
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const dir = resolve(config["export-dir"] ?? join(NTRP_DIR, "exports"));
|
|
185
|
+
if (!existsSync(dir)) {
|
|
186
|
+
mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
return dir;
|
|
189
|
+
}
|
|
190
|
+
function getConfiguredAiInboxDir() {
|
|
191
|
+
const raw = loadConfig()["ai-inbox-dir"];
|
|
192
|
+
return raw ? resolve(raw) : null;
|
|
193
|
+
}
|
|
194
|
+
function getStrategiesDir() {
|
|
195
|
+
const dir = join(NTRP_DIR, "strategies");
|
|
196
|
+
if (!existsSync(dir)) {
|
|
197
|
+
mkdirSync(dir, { recursive: true });
|
|
198
|
+
writeFileSync(join(dir, "README.md"), `# Strategies
|
|
199
|
+
|
|
200
|
+
This directory holds your GTM strategy files. Each file describes a strategy you're executing.
|
|
201
|
+
|
|
202
|
+
## How to use
|
|
203
|
+
|
|
204
|
+
1. Create a markdown file for each active strategy (e.g., \`multi-thread-q2.md\`)
|
|
205
|
+
2. Describe the goal, target segment, and success criteria
|
|
206
|
+
3. Reference playbook plays that support this strategy
|
|
207
|
+
4. After diagnosis, check if vital signs improved in the targeted area
|
|
208
|
+
|
|
209
|
+
## Example
|
|
210
|
+
|
|
211
|
+
\`\`\`markdown
|
|
212
|
+
# Multi-Thread Enterprise Deals \u2014 Q2
|
|
213
|
+
|
|
214
|
+
**Goal:** Reduce single-threaded deals from 65% to under 30%
|
|
215
|
+
**Segment:** Enterprise accounts > $100K
|
|
216
|
+
**Play:** Multi-Thread Your Deals
|
|
217
|
+
**Success metric:** Thread depth score > 70
|
|
218
|
+
\`\`\`
|
|
219
|
+
`);
|
|
220
|
+
}
|
|
221
|
+
return dir;
|
|
222
|
+
}
|
|
223
|
+
function getMemoryDir() {
|
|
224
|
+
const dir = join(NTRP_DIR, "memory");
|
|
225
|
+
if (!existsSync(dir)) {
|
|
226
|
+
mkdirSync(dir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
return dir;
|
|
229
|
+
}
|
|
230
|
+
function getKnowledgeDir() {
|
|
231
|
+
const dir = join(NTRP_DIR, "knowledge");
|
|
232
|
+
if (!existsSync(dir)) {
|
|
233
|
+
mkdirSync(dir, { recursive: true });
|
|
234
|
+
writeFileSync(join(dir, "README.md"), `# Knowledge Packs
|
|
235
|
+
|
|
236
|
+
Drop case studies, GTM frameworks, benchmark reports, or playbooks here as
|
|
237
|
+
markdown, text, or PDF. NTRP ingests them with \`/knowledge add <file>\` and
|
|
238
|
+
references the most relevant passages during analysis \u2014 so the agent can learn
|
|
239
|
+
from work done outside this platform.
|
|
240
|
+
|
|
241
|
+
## How to use
|
|
242
|
+
|
|
243
|
+
1. Add a file: \`/knowledge add ~/Downloads/plg-benchmarks-2026.pdf\`
|
|
244
|
+
2. List what's indexed: \`/knowledge list\`
|
|
245
|
+
3. Ask a question \u2014 relevant passages are pulled in automatically.
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
return dir;
|
|
249
|
+
}
|
|
250
|
+
function getWinsDir() {
|
|
251
|
+
const dir = join(NTRP_DIR, "wins");
|
|
252
|
+
if (!existsSync(dir)) {
|
|
253
|
+
mkdirSync(dir, { recursive: true });
|
|
254
|
+
writeFileSync(join(dir, "README.md"), `# Wins
|
|
255
|
+
|
|
256
|
+
This directory logs outcomes when a strategy or play succeeds. Each win creates a record that future diagnoses can reference.
|
|
257
|
+
|
|
258
|
+
## How to use
|
|
259
|
+
|
|
260
|
+
1. After executing a play, log the result here (e.g., \`2026-04-clean-pipeline.md\`)
|
|
261
|
+
2. Include: what you did, what changed, before/after scores
|
|
262
|
+
3. Future AI findings will reference wins to track improvement over time
|
|
263
|
+
|
|
264
|
+
## Example
|
|
265
|
+
|
|
266
|
+
\`\`\`markdown
|
|
267
|
+
# Pipeline Cleanup \u2014 April 2026
|
|
268
|
+
|
|
269
|
+
**Play:** Clean Dead Pipeline
|
|
270
|
+
**Before:** Freshness 29/100, $3.1M stale pipeline
|
|
271
|
+
**After:** Freshness 72/100, removed 45 zombie deals
|
|
272
|
+
**Impact:** Forecast accuracy improved from 62% to 84%
|
|
273
|
+
\`\`\`
|
|
274
|
+
`);
|
|
275
|
+
}
|
|
276
|
+
return dir;
|
|
277
|
+
}
|
|
278
|
+
var NTRP_DIR, CONFIG_PATH, cachedConfig;
|
|
279
|
+
var init_store = __esm({
|
|
280
|
+
"src/config/store.ts"() {
|
|
281
|
+
"use strict";
|
|
282
|
+
NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
|
|
283
|
+
CONFIG_PATH = join(NTRP_DIR, "config.json");
|
|
284
|
+
cachedConfig = null;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// src/output/path-safety.ts
|
|
289
|
+
import { homedir as homedir2 } from "os";
|
|
290
|
+
import { resolve as resolve2, sep } from "path";
|
|
291
|
+
var NTRP_HOME;
|
|
292
|
+
var init_path_safety = __esm({
|
|
293
|
+
"src/output/path-safety.ts"() {
|
|
294
|
+
"use strict";
|
|
295
|
+
init_store();
|
|
296
|
+
NTRP_HOME = ntrpHome();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// src/services/exports-registry.ts
|
|
301
|
+
import {
|
|
302
|
+
appendFileSync,
|
|
303
|
+
copyFileSync,
|
|
304
|
+
cpSync,
|
|
305
|
+
existsSync as existsSync2,
|
|
306
|
+
mkdirSync as mkdirSync2,
|
|
307
|
+
readFileSync as readFileSync2,
|
|
308
|
+
readdirSync,
|
|
309
|
+
renameSync,
|
|
310
|
+
rmSync,
|
|
311
|
+
statSync,
|
|
312
|
+
writeFileSync as writeFileSync2
|
|
313
|
+
} from "fs";
|
|
314
|
+
import { basename, dirname, join as join2, resolve as resolve3, sep as sep2 } from "path";
|
|
315
|
+
import { randomUUID } from "crypto";
|
|
316
|
+
function ensureExportsLayout(root = getExportsDir()) {
|
|
317
|
+
mkdirSync2(root, { recursive: true });
|
|
318
|
+
mkdirSync2(join2(root, "latest"), { recursive: true });
|
|
319
|
+
for (const sub of KIND_DIRS) {
|
|
320
|
+
mkdirSync2(join2(root, sub), { recursive: true });
|
|
321
|
+
}
|
|
322
|
+
const readme = join2(root, "README.md");
|
|
323
|
+
if (!existsSync2(readme)) {
|
|
324
|
+
writeFileSync2(readme, ARCHIVE_README, "utf-8");
|
|
325
|
+
}
|
|
326
|
+
if (!existsSync2(join2(root, "INDEX.md"))) {
|
|
327
|
+
writeFileSync2(join2(root, "INDEX.md"), "# NTRP exports\n\n_No exports yet._\n", "utf-8");
|
|
328
|
+
}
|
|
329
|
+
if (!existsSync2(join2(root, "manifest.jsonl"))) {
|
|
330
|
+
writeFileSync2(join2(root, "manifest.jsonl"), "", "utf-8");
|
|
331
|
+
}
|
|
332
|
+
return root;
|
|
333
|
+
}
|
|
334
|
+
function getAiInboxDir() {
|
|
335
|
+
return getConfiguredAiInboxDir();
|
|
336
|
+
}
|
|
337
|
+
function archiveIndexPath() {
|
|
338
|
+
return join2(ensureExportsLayout(), "INDEX.md");
|
|
339
|
+
}
|
|
340
|
+
var KIND_DIRS, ARCHIVE_README;
|
|
341
|
+
var init_exports_registry = __esm({
|
|
342
|
+
"src/services/exports-registry.ts"() {
|
|
343
|
+
"use strict";
|
|
344
|
+
init_store();
|
|
345
|
+
init_path_safety();
|
|
346
|
+
KIND_DIRS = ["handoffs", "reports", "notes", "csv", "publish"];
|
|
347
|
+
ARCHIVE_README = `# NTRP exports archive
|
|
348
|
+
|
|
349
|
+
Handoffs, reports, notes, CSV receipts, and publish packages land here by kind:
|
|
350
|
+
|
|
351
|
+
- \`handoffs/\` \u2014 agent prompts (\`handoff-deck-*.md\`, \u2026)
|
|
352
|
+
- \`reports/\` \u2014 markdown reports
|
|
353
|
+
- \`notes/\` \u2014 Obsidian-style notes
|
|
354
|
+
- \`csv/\` \u2014 backmeup receipt folders
|
|
355
|
+
- \`publish/\` \u2014 repository export packages
|
|
356
|
+
- \`latest/\` \u2014 stable copies of the newest file per kind
|
|
357
|
+
|
|
358
|
+
\`INDEX.md\` is regenerated from \`manifest.jsonl\` on every write/move.
|
|
359
|
+
|
|
360
|
+
Point a desktop AI app at a dedicated inbox instead of this folder:
|
|
361
|
+
|
|
362
|
+
\`\`\`
|
|
363
|
+
/inbox set ~/Documents/Claude/ntrp-inbox
|
|
364
|
+
\`\`\`
|
|
365
|
+
`;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
116
369
|
// src/services/terminal-capture.ts
|
|
117
370
|
function redactSecrets(line) {
|
|
118
371
|
let out = line;
|
|
@@ -335,7 +588,7 @@ var init_terminal_capture = __esm({
|
|
|
335
588
|
});
|
|
336
589
|
|
|
337
590
|
// src/services/context-doc.ts
|
|
338
|
-
import { writeFileSync } from "fs";
|
|
591
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
339
592
|
function buildSessionContextDoc(file, opts = {}) {
|
|
340
593
|
const id = file.id;
|
|
341
594
|
const shortId = id.slice(-4);
|
|
@@ -447,6 +700,21 @@ function buildSessionContextDoc(file, opts = {}) {
|
|
|
447
700
|
lines.push("- None yet.");
|
|
448
701
|
}
|
|
449
702
|
lines.push("");
|
|
703
|
+
lines.push("## Exports");
|
|
704
|
+
lines.push("");
|
|
705
|
+
try {
|
|
706
|
+
lines.push(`- Archive index: \`${archiveIndexPath()}\``);
|
|
707
|
+
lines.push(`- Archive root: \`${getExportsDir()}\``);
|
|
708
|
+
const inbox = getAiInboxDir();
|
|
709
|
+
if (inbox) {
|
|
710
|
+
lines.push(`- AI inbox: \`${inbox}\` (open \`latest-handoff.md\` or \`INDEX.md\`)`);
|
|
711
|
+
} else {
|
|
712
|
+
lines.push("- AI inbox: unset \u2014 `/inbox set <folder>` for Claude Desktop");
|
|
713
|
+
}
|
|
714
|
+
} catch {
|
|
715
|
+
lines.push("- Export catalog unavailable.");
|
|
716
|
+
}
|
|
717
|
+
lines.push("");
|
|
450
718
|
lines.push(`## Conversation (${exchanges} exchange${exchanges === 1 ? "" : "s"})`);
|
|
451
719
|
lines.push("");
|
|
452
720
|
if (file.messages.length === 0) {
|
|
@@ -489,13 +757,13 @@ function writeSessionContextDoc(ctx) {
|
|
|
489
757
|
try {
|
|
490
758
|
const file = buildSessionFileSnapshot(ctx);
|
|
491
759
|
const doc = buildSessionContextDoc(file, { snapshot: ctx.snapshot.computeResult });
|
|
492
|
-
|
|
760
|
+
writeFileSync3(contextDocPathForSession(ctx.sessionId), doc);
|
|
493
761
|
} catch {
|
|
494
762
|
}
|
|
495
763
|
}
|
|
496
764
|
function writeContextDocForSessionFile(file, opts = {}) {
|
|
497
765
|
try {
|
|
498
|
-
|
|
766
|
+
writeFileSync3(contextDocPathForSession(file.id), buildSessionContextDoc(file, opts));
|
|
499
767
|
} catch {
|
|
500
768
|
}
|
|
501
769
|
}
|
|
@@ -505,18 +773,20 @@ var init_context_doc = __esm({
|
|
|
505
773
|
"use strict";
|
|
506
774
|
init_context2();
|
|
507
775
|
init_formatters();
|
|
776
|
+
init_store();
|
|
777
|
+
init_exports_registry();
|
|
508
778
|
init_terminal_capture();
|
|
509
779
|
AGENT_EXCERPT_CHARS = 400;
|
|
510
780
|
}
|
|
511
781
|
});
|
|
512
782
|
|
|
513
783
|
// src/services/transcript.ts
|
|
514
|
-
import { existsSync, readFileSync, writeFileSync as
|
|
515
|
-
import { join } from "path";
|
|
784
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
|
|
785
|
+
import { join as join3 } from "path";
|
|
516
786
|
function rebindSessionTranscript(ctx) {
|
|
517
787
|
if (!state || state.sessionId === ctx.sessionId) return;
|
|
518
|
-
const priorJson =
|
|
519
|
-
if (
|
|
788
|
+
const priorJson = join3(getSessionsDir(), `${state.sessionId}.json`);
|
|
789
|
+
if (existsSync3(priorJson)) {
|
|
520
790
|
finalizeCurrentFile("switched session");
|
|
521
791
|
} else {
|
|
522
792
|
discardSessionTranscript(state.sessionId);
|
|
@@ -530,16 +800,16 @@ function discardSessionTranscript(sessionId) {
|
|
|
530
800
|
clearFlushTimer();
|
|
531
801
|
}
|
|
532
802
|
try {
|
|
533
|
-
|
|
803
|
+
rmSync2(transcriptPathForSession(sessionId), { force: true });
|
|
534
804
|
} catch {
|
|
535
805
|
}
|
|
536
806
|
}
|
|
537
807
|
function createState(sessionId) {
|
|
538
808
|
const filePath = transcriptPathForSession(sessionId);
|
|
539
809
|
let base = "";
|
|
540
|
-
if (
|
|
810
|
+
if (existsSync3(filePath)) {
|
|
541
811
|
try {
|
|
542
|
-
base =
|
|
812
|
+
base = readFileSync3(filePath, "utf-8").trimEnd() + "\n";
|
|
543
813
|
} catch {
|
|
544
814
|
base = "";
|
|
545
815
|
}
|
|
@@ -599,7 +869,7 @@ function flushNow(closedNote) {
|
|
|
599
869
|
s.lastFlushMs = Date.now();
|
|
600
870
|
try {
|
|
601
871
|
getSessionsDir();
|
|
602
|
-
|
|
872
|
+
writeFileSync4(s.filePath, render(s, closedNote));
|
|
603
873
|
} catch {
|
|
604
874
|
}
|
|
605
875
|
}
|
|
@@ -638,12 +908,12 @@ __export(connection_exports, {
|
|
|
638
908
|
run: () => run,
|
|
639
909
|
setActiveDbPath: () => setActiveDbPath
|
|
640
910
|
});
|
|
641
|
-
import { mkdirSync, existsSync as
|
|
642
|
-
import { dirname, join as
|
|
643
|
-
function
|
|
644
|
-
const dir =
|
|
645
|
-
if (!
|
|
646
|
-
|
|
911
|
+
import { mkdirSync as mkdirSync3, existsSync as existsSync4, rmSync as rmSync3 } from "fs";
|
|
912
|
+
import { dirname as dirname2, join as join4, resolve as resolve4 } from "path";
|
|
913
|
+
function ensureDir2() {
|
|
914
|
+
const dir = dirname2(activeDbPath);
|
|
915
|
+
if (!existsSync4(dir)) {
|
|
916
|
+
mkdirSync3(dir, { recursive: true });
|
|
647
917
|
}
|
|
648
918
|
}
|
|
649
919
|
function getActiveDbPath() {
|
|
@@ -651,7 +921,7 @@ function getActiveDbPath() {
|
|
|
651
921
|
}
|
|
652
922
|
async function setActiveDbPath(path) {
|
|
653
923
|
if (DB_PATH_PINNED) return;
|
|
654
|
-
const resolved =
|
|
924
|
+
const resolved = resolve4(path);
|
|
655
925
|
if (resolved === activeDbPath) return;
|
|
656
926
|
await discardConnection();
|
|
657
927
|
activeDbPath = resolved;
|
|
@@ -671,7 +941,7 @@ async function getConnection() {
|
|
|
671
941
|
}
|
|
672
942
|
await discardConnection();
|
|
673
943
|
}
|
|
674
|
-
|
|
944
|
+
ensureDir2();
|
|
675
945
|
const duckdb = await loadDuckDB();
|
|
676
946
|
db = new duckdb.Database(activeDbPath);
|
|
677
947
|
conn = new duckdb.Connection(db);
|
|
@@ -689,20 +959,20 @@ function isClosedConnectionError(err) {
|
|
|
689
959
|
function closeConnection(c) {
|
|
690
960
|
const close2 = c.close;
|
|
691
961
|
if (typeof close2 !== "function") return Promise.resolve();
|
|
692
|
-
return new Promise((
|
|
962
|
+
return new Promise((resolve10) => {
|
|
693
963
|
try {
|
|
694
|
-
close2.call(c, () =>
|
|
964
|
+
close2.call(c, () => resolve10());
|
|
695
965
|
} catch {
|
|
696
|
-
|
|
966
|
+
resolve10();
|
|
697
967
|
}
|
|
698
968
|
});
|
|
699
969
|
}
|
|
700
970
|
function isConnectionAlive(c) {
|
|
701
|
-
return new Promise((
|
|
971
|
+
return new Promise((resolve10) => {
|
|
702
972
|
try {
|
|
703
|
-
c.all("SELECT 1", (err) =>
|
|
973
|
+
c.all("SELECT 1", (err) => resolve10(!err));
|
|
704
974
|
} catch {
|
|
705
|
-
|
|
975
|
+
resolve10(false);
|
|
706
976
|
}
|
|
707
977
|
});
|
|
708
978
|
}
|
|
@@ -716,8 +986,8 @@ async function discardConnection() {
|
|
|
716
986
|
await closeConnection(currentConn).catch(() => void 0);
|
|
717
987
|
}
|
|
718
988
|
if (currentDb) {
|
|
719
|
-
await new Promise((
|
|
720
|
-
currentDb.close(() =>
|
|
989
|
+
await new Promise((resolve10) => {
|
|
990
|
+
currentDb.close(() => resolve10());
|
|
721
991
|
}).catch(() => void 0);
|
|
722
992
|
}
|
|
723
993
|
}
|
|
@@ -732,10 +1002,10 @@ async function withReconnect(op) {
|
|
|
732
1002
|
}
|
|
733
1003
|
async function execAllOnce(sql, params) {
|
|
734
1004
|
const c = await getConnection();
|
|
735
|
-
return new Promise((
|
|
1005
|
+
return new Promise((resolve10, reject) => {
|
|
736
1006
|
const cb = (err, rows) => {
|
|
737
1007
|
if (err) reject(err);
|
|
738
|
-
else
|
|
1008
|
+
else resolve10(rows ?? []);
|
|
739
1009
|
};
|
|
740
1010
|
if (params.length > 0) {
|
|
741
1011
|
const stmt = c.prepare(sql);
|
|
@@ -750,18 +1020,18 @@ async function execAllOnce(sql, params) {
|
|
|
750
1020
|
}
|
|
751
1021
|
async function runOnce(sql, params = []) {
|
|
752
1022
|
const c = await getConnection();
|
|
753
|
-
return new Promise((
|
|
1023
|
+
return new Promise((resolve10, reject) => {
|
|
754
1024
|
if (params.length > 0) {
|
|
755
1025
|
const stmt = c.prepare(sql);
|
|
756
1026
|
stmt.run(...params, (err) => {
|
|
757
1027
|
stmt.finalize();
|
|
758
1028
|
if (err) reject(err);
|
|
759
|
-
else
|
|
1029
|
+
else resolve10();
|
|
760
1030
|
});
|
|
761
1031
|
} else {
|
|
762
1032
|
c.run(sql, (err) => {
|
|
763
1033
|
if (err) reject(err);
|
|
764
|
-
else
|
|
1034
|
+
else resolve10();
|
|
765
1035
|
});
|
|
766
1036
|
}
|
|
767
1037
|
});
|
|
@@ -781,15 +1051,15 @@ async function close() {
|
|
|
781
1051
|
async function recreateDatabaseFile() {
|
|
782
1052
|
await discardConnection();
|
|
783
1053
|
for (const path of [activeDbPath, `${activeDbPath}.wal`]) {
|
|
784
|
-
|
|
1054
|
+
rmSync3(path, { force: true });
|
|
785
1055
|
}
|
|
786
1056
|
}
|
|
787
|
-
var
|
|
1057
|
+
var NTRP_DIR2, DEFAULT_DB_PATH, DB_PATH_PINNED, activeDbPath, db, conn, duckdbModule, connectionGeneration, lastHealthCheckMs, HEALTH_CHECK_INTERVAL_MS;
|
|
788
1058
|
var init_connection = __esm({
|
|
789
1059
|
"src/db/connection.ts"() {
|
|
790
1060
|
"use strict";
|
|
791
|
-
|
|
792
|
-
DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ?
|
|
1061
|
+
NTRP_DIR2 = process.env.NTRP_HOME ? resolve4(process.env.NTRP_HOME) : join4(process.env.HOME ?? "", ".ntrp");
|
|
1062
|
+
DEFAULT_DB_PATH = process.env.NTRP_DB_PATH ? resolve4(process.env.NTRP_DB_PATH) : join4(NTRP_DIR2, "ntrp.duckdb");
|
|
793
1063
|
DB_PATH_PINNED = !!process.env.NTRP_DB_PATH;
|
|
794
1064
|
activeDbPath = DEFAULT_DB_PATH;
|
|
795
1065
|
db = null;
|
|
@@ -859,9 +1129,9 @@ __export(queries_exports, {
|
|
|
859
1129
|
upsertStrategy: () => upsertStrategy,
|
|
860
1130
|
uuid: () => uuid
|
|
861
1131
|
});
|
|
862
|
-
import { randomUUID } from "crypto";
|
|
1132
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
863
1133
|
function uuid() {
|
|
864
|
-
return
|
|
1134
|
+
return randomUUID2();
|
|
865
1135
|
}
|
|
866
1136
|
function now() {
|
|
867
1137
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1907,216 +2177,49 @@ CREATE INDEX IF NOT EXISTS idx_health_readings_batch ON health_readings(upload_b
|
|
|
1907
2177
|
}
|
|
1908
2178
|
});
|
|
1909
2179
|
|
|
1910
|
-
// src/config/
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
getKnowledgeDir: () => getKnowledgeDir,
|
|
1917
|
-
getMemoryDir: () => getMemoryDir,
|
|
1918
|
-
getStrategiesDir: () => getStrategiesDir,
|
|
1919
|
-
getWinsDir: () => getWinsDir,
|
|
1920
|
-
loadConfig: () => loadConfig,
|
|
1921
|
-
ntrpHome: () => ntrpHome,
|
|
1922
|
-
resetConfigCache: () => resetConfigCache,
|
|
1923
|
-
saveConfig: () => saveConfig,
|
|
1924
|
-
setConfigValue: () => setConfigValue
|
|
1925
|
-
});
|
|
1926
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
1927
|
-
import { homedir } from "os";
|
|
1928
|
-
import { join as join3, resolve as resolve2 } from "path";
|
|
1929
|
-
function ntrpHome() {
|
|
1930
|
-
return NTRP_DIR2;
|
|
2180
|
+
// src/config/install.ts
|
|
2181
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2182
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
|
|
2183
|
+
import { join as join5 } from "path";
|
|
2184
|
+
function installPath() {
|
|
2185
|
+
return join5(ntrpHome(), "install.json");
|
|
1931
2186
|
}
|
|
1932
|
-
function
|
|
1933
|
-
|
|
1934
|
-
|
|
2187
|
+
function ensureDir3() {
|
|
2188
|
+
const dir = ntrpHome();
|
|
2189
|
+
if (!existsSync5(dir)) {
|
|
2190
|
+
mkdirSync4(dir, { recursive: true });
|
|
1935
2191
|
}
|
|
1936
2192
|
}
|
|
1937
|
-
function
|
|
1938
|
-
if (
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
cachedConfig = {};
|
|
1942
|
-
return cachedConfig;
|
|
1943
|
-
}
|
|
1944
|
-
try {
|
|
1945
|
-
cachedConfig = JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
|
|
1946
|
-
} catch {
|
|
1947
|
-
cachedConfig = {};
|
|
1948
|
-
}
|
|
1949
|
-
return cachedConfig;
|
|
2193
|
+
function isValidInstall(value) {
|
|
2194
|
+
if (!value || typeof value !== "object") return false;
|
|
2195
|
+
const r = value;
|
|
2196
|
+
return r.schema_version === 1 && typeof r.install_id === "string" && r.install_id.length > 0 && typeof r.created_at === "string";
|
|
1950
2197
|
}
|
|
1951
|
-
function
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2198
|
+
function ensureInstall() {
|
|
2199
|
+
if (cachedInstall) return cachedInstall;
|
|
2200
|
+
const path = installPath();
|
|
2201
|
+
if (existsSync5(path)) {
|
|
2202
|
+
try {
|
|
2203
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
2204
|
+
if (isValidInstall(parsed)) {
|
|
2205
|
+
cachedInstall = parsed;
|
|
2206
|
+
return parsed;
|
|
2207
|
+
}
|
|
2208
|
+
} catch {
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
const record = {
|
|
2212
|
+
schema_version: 1,
|
|
2213
|
+
install_id: randomUUID3(),
|
|
2214
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2215
|
+
};
|
|
2216
|
+
ensureDir3();
|
|
2217
|
+
writeFileSync5(path, JSON.stringify(record, null, 2) + "\n");
|
|
2218
|
+
cachedInstall = record;
|
|
2219
|
+
return record;
|
|
1955
2220
|
}
|
|
1956
|
-
function
|
|
1957
|
-
|
|
1958
|
-
}
|
|
1959
|
-
function getConfigValue(key) {
|
|
1960
|
-
if (key === "api-key") return loadConfig()["api-key"];
|
|
1961
|
-
if (key === "license-key") return process.env.NTRP_LICENSE_KEY ?? loadConfig()["license-key"];
|
|
1962
|
-
const config = loadConfig();
|
|
1963
|
-
return config[key];
|
|
1964
|
-
}
|
|
1965
|
-
function setConfigValue(key, value) {
|
|
1966
|
-
const config = loadConfig();
|
|
1967
|
-
config[key] = value;
|
|
1968
|
-
saveConfig(config);
|
|
1969
|
-
}
|
|
1970
|
-
function deleteConfigValue(key) {
|
|
1971
|
-
const config = loadConfig();
|
|
1972
|
-
delete config[key];
|
|
1973
|
-
saveConfig(config);
|
|
1974
|
-
}
|
|
1975
|
-
function getExportsDir() {
|
|
1976
|
-
const config = loadConfig();
|
|
1977
|
-
const dir = resolve2(config["export-dir"] ?? join3(NTRP_DIR2, "exports"));
|
|
1978
|
-
if (!existsSync3(dir)) {
|
|
1979
|
-
mkdirSync2(dir, { recursive: true });
|
|
1980
|
-
}
|
|
1981
|
-
return dir;
|
|
1982
|
-
}
|
|
1983
|
-
function getStrategiesDir() {
|
|
1984
|
-
const dir = join3(NTRP_DIR2, "strategies");
|
|
1985
|
-
if (!existsSync3(dir)) {
|
|
1986
|
-
mkdirSync2(dir, { recursive: true });
|
|
1987
|
-
writeFileSync3(join3(dir, "README.md"), `# Strategies
|
|
1988
|
-
|
|
1989
|
-
This directory holds your GTM strategy files. Each file describes a strategy you're executing.
|
|
1990
|
-
|
|
1991
|
-
## How to use
|
|
1992
|
-
|
|
1993
|
-
1. Create a markdown file for each active strategy (e.g., \`multi-thread-q2.md\`)
|
|
1994
|
-
2. Describe the goal, target segment, and success criteria
|
|
1995
|
-
3. Reference playbook plays that support this strategy
|
|
1996
|
-
4. After diagnosis, check if vital signs improved in the targeted area
|
|
1997
|
-
|
|
1998
|
-
## Example
|
|
1999
|
-
|
|
2000
|
-
\`\`\`markdown
|
|
2001
|
-
# Multi-Thread Enterprise Deals \u2014 Q2
|
|
2002
|
-
|
|
2003
|
-
**Goal:** Reduce single-threaded deals from 65% to under 30%
|
|
2004
|
-
**Segment:** Enterprise accounts > $100K
|
|
2005
|
-
**Play:** Multi-Thread Your Deals
|
|
2006
|
-
**Success metric:** Thread depth score > 70
|
|
2007
|
-
\`\`\`
|
|
2008
|
-
`);
|
|
2009
|
-
}
|
|
2010
|
-
return dir;
|
|
2011
|
-
}
|
|
2012
|
-
function getMemoryDir() {
|
|
2013
|
-
const dir = join3(NTRP_DIR2, "memory");
|
|
2014
|
-
if (!existsSync3(dir)) {
|
|
2015
|
-
mkdirSync2(dir, { recursive: true });
|
|
2016
|
-
}
|
|
2017
|
-
return dir;
|
|
2018
|
-
}
|
|
2019
|
-
function getKnowledgeDir() {
|
|
2020
|
-
const dir = join3(NTRP_DIR2, "knowledge");
|
|
2021
|
-
if (!existsSync3(dir)) {
|
|
2022
|
-
mkdirSync2(dir, { recursive: true });
|
|
2023
|
-
writeFileSync3(join3(dir, "README.md"), `# Knowledge Packs
|
|
2024
|
-
|
|
2025
|
-
Drop case studies, GTM frameworks, benchmark reports, or playbooks here as
|
|
2026
|
-
markdown, text, or PDF. NTRP ingests them with \`/knowledge add <file>\` and
|
|
2027
|
-
references the most relevant passages during analysis \u2014 so the agent can learn
|
|
2028
|
-
from work done outside this platform.
|
|
2029
|
-
|
|
2030
|
-
## How to use
|
|
2031
|
-
|
|
2032
|
-
1. Add a file: \`/knowledge add ~/Downloads/plg-benchmarks-2026.pdf\`
|
|
2033
|
-
2. List what's indexed: \`/knowledge list\`
|
|
2034
|
-
3. Ask a question \u2014 relevant passages are pulled in automatically.
|
|
2035
|
-
`);
|
|
2036
|
-
}
|
|
2037
|
-
return dir;
|
|
2038
|
-
}
|
|
2039
|
-
function getWinsDir() {
|
|
2040
|
-
const dir = join3(NTRP_DIR2, "wins");
|
|
2041
|
-
if (!existsSync3(dir)) {
|
|
2042
|
-
mkdirSync2(dir, { recursive: true });
|
|
2043
|
-
writeFileSync3(join3(dir, "README.md"), `# Wins
|
|
2044
|
-
|
|
2045
|
-
This directory logs outcomes when a strategy or play succeeds. Each win creates a record that future diagnoses can reference.
|
|
2046
|
-
|
|
2047
|
-
## How to use
|
|
2048
|
-
|
|
2049
|
-
1. After executing a play, log the result here (e.g., \`2026-04-clean-pipeline.md\`)
|
|
2050
|
-
2. Include: what you did, what changed, before/after scores
|
|
2051
|
-
3. Future AI findings will reference wins to track improvement over time
|
|
2052
|
-
|
|
2053
|
-
## Example
|
|
2054
|
-
|
|
2055
|
-
\`\`\`markdown
|
|
2056
|
-
# Pipeline Cleanup \u2014 April 2026
|
|
2057
|
-
|
|
2058
|
-
**Play:** Clean Dead Pipeline
|
|
2059
|
-
**Before:** Freshness 29/100, $3.1M stale pipeline
|
|
2060
|
-
**After:** Freshness 72/100, removed 45 zombie deals
|
|
2061
|
-
**Impact:** Forecast accuracy improved from 62% to 84%
|
|
2062
|
-
\`\`\`
|
|
2063
|
-
`);
|
|
2064
|
-
}
|
|
2065
|
-
return dir;
|
|
2066
|
-
}
|
|
2067
|
-
var NTRP_DIR2, CONFIG_PATH, cachedConfig;
|
|
2068
|
-
var init_store = __esm({
|
|
2069
|
-
"src/config/store.ts"() {
|
|
2070
|
-
"use strict";
|
|
2071
|
-
NTRP_DIR2 = process.env.NTRP_HOME ? resolve2(process.env.NTRP_HOME) : join3(homedir(), ".ntrp");
|
|
2072
|
-
CONFIG_PATH = join3(NTRP_DIR2, "config.json");
|
|
2073
|
-
cachedConfig = null;
|
|
2074
|
-
}
|
|
2075
|
-
});
|
|
2076
|
-
|
|
2077
|
-
// src/config/install.ts
|
|
2078
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
2079
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
2080
|
-
import { join as join4 } from "path";
|
|
2081
|
-
function installPath() {
|
|
2082
|
-
return join4(ntrpHome(), "install.json");
|
|
2083
|
-
}
|
|
2084
|
-
function ensureDir3() {
|
|
2085
|
-
const dir = ntrpHome();
|
|
2086
|
-
if (!existsSync4(dir)) {
|
|
2087
|
-
mkdirSync3(dir, { recursive: true });
|
|
2088
|
-
}
|
|
2089
|
-
}
|
|
2090
|
-
function isValidInstall(value) {
|
|
2091
|
-
if (!value || typeof value !== "object") return false;
|
|
2092
|
-
const r = value;
|
|
2093
|
-
return r.schema_version === 1 && typeof r.install_id === "string" && r.install_id.length > 0 && typeof r.created_at === "string";
|
|
2094
|
-
}
|
|
2095
|
-
function ensureInstall() {
|
|
2096
|
-
if (cachedInstall) return cachedInstall;
|
|
2097
|
-
const path = installPath();
|
|
2098
|
-
if (existsSync4(path)) {
|
|
2099
|
-
try {
|
|
2100
|
-
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
2101
|
-
if (isValidInstall(parsed)) {
|
|
2102
|
-
cachedInstall = parsed;
|
|
2103
|
-
return parsed;
|
|
2104
|
-
}
|
|
2105
|
-
} catch {
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
const record = {
|
|
2109
|
-
schema_version: 1,
|
|
2110
|
-
install_id: randomUUID2(),
|
|
2111
|
-
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2112
|
-
};
|
|
2113
|
-
ensureDir3();
|
|
2114
|
-
writeFileSync4(path, JSON.stringify(record, null, 2) + "\n");
|
|
2115
|
-
cachedInstall = record;
|
|
2116
|
-
return record;
|
|
2117
|
-
}
|
|
2118
|
-
function getInstallId() {
|
|
2119
|
-
return ensureInstall().install_id;
|
|
2221
|
+
function getInstallId() {
|
|
2222
|
+
return ensureInstall().install_id;
|
|
2120
2223
|
}
|
|
2121
2224
|
var cachedInstall;
|
|
2122
2225
|
var init_install = __esm({
|
|
@@ -2128,16 +2231,16 @@ var init_install = __esm({
|
|
|
2128
2231
|
});
|
|
2129
2232
|
|
|
2130
2233
|
// src/config/progress-migrate.ts
|
|
2131
|
-
import { existsSync as
|
|
2132
|
-
import { join as
|
|
2234
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, renameSync as renameSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2235
|
+
import { join as join6 } from "path";
|
|
2133
2236
|
function legacyStatePath() {
|
|
2134
|
-
return
|
|
2237
|
+
return join6(ntrpHome(), "state.json");
|
|
2135
2238
|
}
|
|
2136
2239
|
function legacyStateBackupPath() {
|
|
2137
|
-
return
|
|
2240
|
+
return join6(ntrpHome(), "state.json.bak");
|
|
2138
2241
|
}
|
|
2139
2242
|
function progressPath() {
|
|
2140
|
-
return
|
|
2243
|
+
return join6(ntrpHome(), "progress.json");
|
|
2141
2244
|
}
|
|
2142
2245
|
function isValidLegacyState(value) {
|
|
2143
2246
|
if (!value || typeof value !== "object") return false;
|
|
@@ -2145,11 +2248,11 @@ function isValidLegacyState(value) {
|
|
|
2145
2248
|
return s.schema_version === 1 && typeof s.total_minutes_saved === "number" && Array.isArray(s.credits) && Array.isArray(s.milestones_unlocked);
|
|
2146
2249
|
}
|
|
2147
2250
|
function migrateLegacyStateIfNeeded(installId) {
|
|
2148
|
-
if (
|
|
2251
|
+
if (existsSync6(progressPath())) return null;
|
|
2149
2252
|
const legacyPath = legacyStatePath();
|
|
2150
|
-
if (!
|
|
2253
|
+
if (!existsSync6(legacyPath)) return null;
|
|
2151
2254
|
try {
|
|
2152
|
-
const parsed = JSON.parse(
|
|
2255
|
+
const parsed = JSON.parse(readFileSync5(legacyPath, "utf-8"));
|
|
2153
2256
|
if (!isValidLegacyState(parsed)) return null;
|
|
2154
2257
|
const { schema_version: _v, ...rest } = parsed;
|
|
2155
2258
|
const progress = {
|
|
@@ -2157,9 +2260,9 @@ function migrateLegacyStateIfNeeded(installId) {
|
|
|
2157
2260
|
schema_version: 2,
|
|
2158
2261
|
install_id: installId
|
|
2159
2262
|
};
|
|
2160
|
-
|
|
2263
|
+
writeFileSync6(progressPath(), JSON.stringify(progress, null, 2) + "\n");
|
|
2161
2264
|
try {
|
|
2162
|
-
|
|
2265
|
+
renameSync2(legacyPath, legacyStateBackupPath());
|
|
2163
2266
|
} catch {
|
|
2164
2267
|
}
|
|
2165
2268
|
return progress;
|
|
@@ -2264,15 +2367,15 @@ var init_usage_backfill = __esm({
|
|
|
2264
2367
|
});
|
|
2265
2368
|
|
|
2266
2369
|
// src/config/progress.ts
|
|
2267
|
-
import { existsSync as
|
|
2268
|
-
import { join as
|
|
2370
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "fs";
|
|
2371
|
+
import { join as join7 } from "path";
|
|
2269
2372
|
function progressPath2() {
|
|
2270
|
-
return
|
|
2373
|
+
return join7(ntrpHome(), "progress.json");
|
|
2271
2374
|
}
|
|
2272
2375
|
function ensureDir4() {
|
|
2273
2376
|
const dir = ntrpHome();
|
|
2274
|
-
if (!
|
|
2275
|
-
|
|
2377
|
+
if (!existsSync7(dir)) {
|
|
2378
|
+
mkdirSync5(dir, { recursive: true });
|
|
2276
2379
|
}
|
|
2277
2380
|
}
|
|
2278
2381
|
function emptyProgress(installId) {
|
|
@@ -2302,9 +2405,9 @@ function reconcileInstallId(state2) {
|
|
|
2302
2405
|
}
|
|
2303
2406
|
function readProgressFile() {
|
|
2304
2407
|
const path = progressPath2();
|
|
2305
|
-
if (!
|
|
2408
|
+
if (!existsSync7(path)) return { state: null, changed: false };
|
|
2306
2409
|
try {
|
|
2307
|
-
const parsed = JSON.parse(
|
|
2410
|
+
const parsed = JSON.parse(readFileSync6(path, "utf-8"));
|
|
2308
2411
|
if (!isValidProgress(parsed)) return { state: null, changed: false };
|
|
2309
2412
|
return reconcileInstallId(parsed);
|
|
2310
2413
|
} catch {
|
|
@@ -2345,7 +2448,7 @@ function saveProgress(state2) {
|
|
|
2345
2448
|
schema_version: 2,
|
|
2346
2449
|
install_id: getInstallId()
|
|
2347
2450
|
};
|
|
2348
|
-
|
|
2451
|
+
writeFileSync7(progressPath2(), JSON.stringify(next, null, 2) + "\n");
|
|
2349
2452
|
}
|
|
2350
2453
|
function appendCredit(state2, credit) {
|
|
2351
2454
|
const credits = [...state2.credits, credit];
|
|
@@ -3111,20 +3214,20 @@ var init_time_bank = __esm({
|
|
|
3111
3214
|
});
|
|
3112
3215
|
|
|
3113
3216
|
// src/ai/llm/providers.ts
|
|
3114
|
-
import { existsSync as
|
|
3115
|
-
import { join as
|
|
3217
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync8 } from "fs";
|
|
3218
|
+
import { join as join8 } from "path";
|
|
3116
3219
|
function providersPath() {
|
|
3117
|
-
return
|
|
3220
|
+
return join8(ntrpHome(), "providers.json");
|
|
3118
3221
|
}
|
|
3119
3222
|
function loadCustomProviders() {
|
|
3120
3223
|
if (cachedEntries) return cachedEntries;
|
|
3121
3224
|
const path = providersPath();
|
|
3122
|
-
if (!
|
|
3225
|
+
if (!existsSync8(path)) {
|
|
3123
3226
|
cachedEntries = [];
|
|
3124
3227
|
return cachedEntries;
|
|
3125
3228
|
}
|
|
3126
3229
|
try {
|
|
3127
|
-
const parsed = JSON.parse(
|
|
3230
|
+
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
3128
3231
|
cachedEntries = Array.isArray(parsed.providers) ? parsed.providers : [];
|
|
3129
3232
|
} catch {
|
|
3130
3233
|
cachedEntries = [];
|
|
@@ -3885,20 +3988,20 @@ var init_openai_compat = __esm({
|
|
|
3885
3988
|
});
|
|
3886
3989
|
|
|
3887
3990
|
// src/ai/llm/models-cache.ts
|
|
3888
|
-
import { existsSync as
|
|
3889
|
-
import { join as
|
|
3991
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
3992
|
+
import { join as join9 } from "path";
|
|
3890
3993
|
function cachePath() {
|
|
3891
|
-
return
|
|
3994
|
+
return join9(ntrpHome(), "models.json");
|
|
3892
3995
|
}
|
|
3893
3996
|
function loadFile() {
|
|
3894
3997
|
if (cached) return cached;
|
|
3895
3998
|
const path = cachePath();
|
|
3896
|
-
if (!
|
|
3999
|
+
if (!existsSync9(path)) {
|
|
3897
4000
|
cached = { version: 1, providers: {} };
|
|
3898
4001
|
return cached;
|
|
3899
4002
|
}
|
|
3900
4003
|
try {
|
|
3901
|
-
const parsed = JSON.parse(
|
|
4004
|
+
const parsed = JSON.parse(readFileSync8(path, "utf-8"));
|
|
3902
4005
|
cached = { version: 1, providers: parsed.providers ?? {} };
|
|
3903
4006
|
} catch {
|
|
3904
4007
|
cached = { version: 1, providers: {} };
|
|
@@ -3906,7 +4009,7 @@ function loadFile() {
|
|
|
3906
4009
|
return cached;
|
|
3907
4010
|
}
|
|
3908
4011
|
function saveFile(file) {
|
|
3909
|
-
|
|
4012
|
+
writeFileSync9(cachePath(), JSON.stringify(file, null, 2) + "\n");
|
|
3910
4013
|
cached = file;
|
|
3911
4014
|
}
|
|
3912
4015
|
function getProviderModels(provider) {
|
|
@@ -4067,10 +4170,10 @@ var init_catalog = __esm({
|
|
|
4067
4170
|
});
|
|
4068
4171
|
|
|
4069
4172
|
// src/ai/llm/http.ts
|
|
4070
|
-
import { readFileSync as
|
|
4173
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
4071
4174
|
function fixtureResponse(url, headers) {
|
|
4072
4175
|
try {
|
|
4073
|
-
const raw =
|
|
4176
|
+
const raw = readFileSync9(process.env.NTRP_LLM_HTTP_FIXTURE, "utf-8");
|
|
4074
4177
|
const entries = JSON.parse(raw);
|
|
4075
4178
|
const headerValues = Object.values(headers).join(" ");
|
|
4076
4179
|
for (const entry of entries) {
|
|
@@ -5038,24 +5141,24 @@ var init_errors2 = __esm({
|
|
|
5038
5141
|
|
|
5039
5142
|
// src/strategies/readers.ts
|
|
5040
5143
|
import { createHash } from "crypto";
|
|
5041
|
-
import { existsSync as
|
|
5042
|
-
import { extname, resolve as
|
|
5144
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
5145
|
+
import { extname, resolve as resolve5 } from "path";
|
|
5043
5146
|
import { parse as parseYaml } from "yaml";
|
|
5044
5147
|
import { PDFParse } from "pdf-parse";
|
|
5045
5148
|
async function readStrategyFile(pathOrDash) {
|
|
5046
5149
|
if (pathOrDash === "-") {
|
|
5047
|
-
const text2 =
|
|
5150
|
+
const text2 = readFileSync10(0, "utf-8");
|
|
5048
5151
|
return createDocument("stdin", null, text2, {});
|
|
5049
5152
|
}
|
|
5050
|
-
const sourcePath =
|
|
5051
|
-
if (!
|
|
5153
|
+
const sourcePath = resolve5(pathOrDash);
|
|
5154
|
+
if (!existsSync10(sourcePath)) {
|
|
5052
5155
|
throw new NtrpError("strategy_file_not_found", `Strategy file not found: ${pathOrDash}`, 2 /* Usage */);
|
|
5053
5156
|
}
|
|
5054
5157
|
const ext = extname(sourcePath).toLowerCase();
|
|
5055
5158
|
if (ext === ".pdf") {
|
|
5056
5159
|
return readPdf(sourcePath);
|
|
5057
5160
|
}
|
|
5058
|
-
const text =
|
|
5161
|
+
const text = readFileSync10(sourcePath, "utf-8");
|
|
5059
5162
|
if (ext === ".yaml" || ext === ".yml") {
|
|
5060
5163
|
const structured = parseStructuredYaml(text);
|
|
5061
5164
|
return createDocument("yaml", sourcePath, text, structured);
|
|
@@ -5070,7 +5173,7 @@ function readStrategyText(text) {
|
|
|
5070
5173
|
return createDocument("text", null, text, {});
|
|
5071
5174
|
}
|
|
5072
5175
|
async function readPdf(sourcePath) {
|
|
5073
|
-
const data =
|
|
5176
|
+
const data = readFileSync10(sourcePath);
|
|
5074
5177
|
const parser = new PDFParse({ data });
|
|
5075
5178
|
try {
|
|
5076
5179
|
const result = await parser.getText();
|
|
@@ -5117,17 +5220,17 @@ var init_readers = __esm({
|
|
|
5117
5220
|
});
|
|
5118
5221
|
|
|
5119
5222
|
// src/memory/knowledge.ts
|
|
5120
|
-
import { existsSync as
|
|
5121
|
-
import { join as
|
|
5122
|
-
import { randomUUID as
|
|
5223
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, appendFileSync as appendFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
5224
|
+
import { join as join10 } from "path";
|
|
5225
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5123
5226
|
function knowledgePath() {
|
|
5124
|
-
return
|
|
5227
|
+
return join10(getMemoryDir(), KNOWLEDGE_FILE);
|
|
5125
5228
|
}
|
|
5126
5229
|
function loadKnowledgeChunks() {
|
|
5127
5230
|
const path = knowledgePath();
|
|
5128
|
-
if (!
|
|
5231
|
+
if (!existsSync11(path)) return [];
|
|
5129
5232
|
const out = [];
|
|
5130
|
-
for (const line of
|
|
5233
|
+
for (const line of readFileSync11(path, "utf-8").split("\n")) {
|
|
5131
5234
|
const trimmed = line.trim();
|
|
5132
5235
|
if (!trimmed) continue;
|
|
5133
5236
|
try {
|
|
@@ -5160,16 +5263,16 @@ __export(playbook_exports, {
|
|
|
5160
5263
|
getPlaysForVitalSign: () => getPlaysForVitalSign,
|
|
5161
5264
|
matchTriggeredPlays: () => matchTriggeredPlays
|
|
5162
5265
|
});
|
|
5163
|
-
import { existsSync as
|
|
5164
|
-
import { join as
|
|
5266
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12, appendFileSync as appendFileSync3 } from "fs";
|
|
5267
|
+
import { join as join11 } from "path";
|
|
5165
5268
|
function playsPath() {
|
|
5166
|
-
return
|
|
5269
|
+
return join11(getMemoryDir(), PLAYS_FILE);
|
|
5167
5270
|
}
|
|
5168
5271
|
function getCustomPlays() {
|
|
5169
5272
|
const path = playsPath();
|
|
5170
|
-
if (!
|
|
5273
|
+
if (!existsSync12(path)) return [];
|
|
5171
5274
|
const out = [];
|
|
5172
|
-
for (const line of
|
|
5275
|
+
for (const line of readFileSync12(path, "utf-8").split("\n")) {
|
|
5173
5276
|
const trimmed = line.trim();
|
|
5174
5277
|
if (!trimmed) continue;
|
|
5175
5278
|
try {
|
|
@@ -5201,7 +5304,7 @@ function addCustomPlay(input) {
|
|
|
5201
5304
|
source: "learned"
|
|
5202
5305
|
};
|
|
5203
5306
|
try {
|
|
5204
|
-
|
|
5307
|
+
appendFileSync3(playsPath(), JSON.stringify(play) + "\n");
|
|
5205
5308
|
} catch {
|
|
5206
5309
|
}
|
|
5207
5310
|
return play;
|
|
@@ -5591,15 +5694,15 @@ JSON SHAPE:
|
|
|
5591
5694
|
});
|
|
5592
5695
|
|
|
5593
5696
|
// src/strategies/library.ts
|
|
5594
|
-
import { writeFileSync as
|
|
5595
|
-
import { join as
|
|
5697
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
5698
|
+
import { join as join12 } from "path";
|
|
5596
5699
|
import { stringify as stringifyYaml } from "yaml";
|
|
5597
5700
|
function strategyLibraryPath(slug) {
|
|
5598
|
-
return
|
|
5701
|
+
return join12(getStrategiesDir(), `${slug}.md`);
|
|
5599
5702
|
}
|
|
5600
5703
|
function writeStrategyMarkdown(strategy) {
|
|
5601
5704
|
const path = strategyLibraryPath(strategy.slug);
|
|
5602
|
-
|
|
5705
|
+
writeFileSync10(path, renderStrategyMarkdown(strategy), "utf-8");
|
|
5603
5706
|
return path;
|
|
5604
5707
|
}
|
|
5605
5708
|
function renderStrategyMarkdown(strategy) {
|
|
@@ -5723,12 +5826,12 @@ var init_library = __esm({
|
|
|
5723
5826
|
});
|
|
5724
5827
|
|
|
5725
5828
|
// src/strategies/connectors.ts
|
|
5726
|
-
import { readdirSync as
|
|
5727
|
-
import { homedir as
|
|
5728
|
-
import { basename, extname as extname2, join as
|
|
5829
|
+
import { readdirSync as readdirSync3, statSync as statSync2 } from "fs";
|
|
5830
|
+
import { homedir as homedir3 } from "os";
|
|
5831
|
+
import { basename as basename2, extname as extname2, join as join13, relative, resolve as resolve6, sep as sep3 } from "path";
|
|
5729
5832
|
function createLocalFolderConnector(options) {
|
|
5730
|
-
const rootPath =
|
|
5731
|
-
const name = options.name ?? (
|
|
5833
|
+
const rootPath = resolveUserPath2(options.rootPath);
|
|
5834
|
+
const name = options.name ?? (basename2(rootPath) || "local");
|
|
5732
5835
|
const includePatterns = normalizePatterns(options.includePatterns);
|
|
5733
5836
|
const excludePatterns = normalizePatterns(options.excludePatterns);
|
|
5734
5837
|
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
@@ -5767,8 +5870,8 @@ function createLocalFolderConnector(options) {
|
|
|
5767
5870
|
};
|
|
5768
5871
|
}
|
|
5769
5872
|
function walkLocalFolder(rootPath, currentPath, refs, opts) {
|
|
5770
|
-
for (const entry of
|
|
5771
|
-
const absolutePath =
|
|
5873
|
+
for (const entry of readdirSync3(currentPath, { withFileTypes: true })) {
|
|
5874
|
+
const absolutePath = join13(currentPath, entry.name);
|
|
5772
5875
|
const relativePath = normalizePath(relative(rootPath, absolutePath));
|
|
5773
5876
|
if (entry.isDirectory()) {
|
|
5774
5877
|
if (shouldSkipDirectory(entry.name) || matchesAny(relativePath, opts.excludePatterns)) continue;
|
|
@@ -5802,7 +5905,7 @@ function shouldSkipDirectory(name) {
|
|
|
5802
5905
|
}
|
|
5803
5906
|
function safeStat(path) {
|
|
5804
5907
|
try {
|
|
5805
|
-
return
|
|
5908
|
+
return statSync2(path);
|
|
5806
5909
|
} catch {
|
|
5807
5910
|
return null;
|
|
5808
5911
|
}
|
|
@@ -5816,7 +5919,7 @@ function matchesAny(relativePath, patterns) {
|
|
|
5816
5919
|
function matchesPattern(relativePath, pattern) {
|
|
5817
5920
|
const normalizedPath = normalizePath(relativePath);
|
|
5818
5921
|
const normalizedPattern = normalizePath(pattern);
|
|
5819
|
-
const base =
|
|
5922
|
+
const base = basename2(normalizedPath);
|
|
5820
5923
|
if (!normalizedPattern.includes("*")) {
|
|
5821
5924
|
return normalizedPath === normalizedPattern || normalizedPath.endsWith(`/${normalizedPattern}`) || normalizedPath.includes(normalizedPattern);
|
|
5822
5925
|
}
|
|
@@ -5828,12 +5931,12 @@ function wildcardToRegExp(pattern) {
|
|
|
5828
5931
|
return new RegExp(`^${escaped}$`, "i");
|
|
5829
5932
|
}
|
|
5830
5933
|
function normalizePath(path) {
|
|
5831
|
-
return path.split(
|
|
5934
|
+
return path.split(sep3).join("/");
|
|
5832
5935
|
}
|
|
5833
|
-
function
|
|
5834
|
-
if (path === "~") return
|
|
5835
|
-
if (path.startsWith("~/")) return
|
|
5836
|
-
return
|
|
5936
|
+
function resolveUserPath2(path) {
|
|
5937
|
+
if (path === "~") return homedir3();
|
|
5938
|
+
if (path.startsWith("~/")) return join13(homedir3(), path.slice(2));
|
|
5939
|
+
return resolve6(path);
|
|
5837
5940
|
}
|
|
5838
5941
|
var DEFAULT_MAX_FILES, DEFAULT_MAX_BYTES, SUPPORTED_EXTENSIONS, DEFAULT_EXCLUDED_DIRS;
|
|
5839
5942
|
var init_connectors = __esm({
|
|
@@ -6044,17 +6147,17 @@ __export(store_exports2, {
|
|
|
6044
6147
|
rewriteJsonl: () => rewriteJsonl,
|
|
6045
6148
|
scrubText: () => scrubText
|
|
6046
6149
|
});
|
|
6047
|
-
import { existsSync as
|
|
6048
|
-
import { join as
|
|
6049
|
-
import { randomUUID as
|
|
6150
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, appendFileSync as appendFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync11 } from "fs";
|
|
6151
|
+
import { join as join14 } from "path";
|
|
6152
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6050
6153
|
function memPath(file) {
|
|
6051
|
-
return
|
|
6154
|
+
return join14(getMemoryDir(), file);
|
|
6052
6155
|
}
|
|
6053
6156
|
function readJsonl(file) {
|
|
6054
6157
|
const path = memPath(file);
|
|
6055
|
-
if (!
|
|
6158
|
+
if (!existsSync13(path)) return [];
|
|
6056
6159
|
const out = [];
|
|
6057
|
-
for (const line of
|
|
6160
|
+
for (const line of readFileSync13(path, "utf-8").split("\n")) {
|
|
6058
6161
|
const trimmed = line.trim();
|
|
6059
6162
|
if (!trimmed) continue;
|
|
6060
6163
|
try {
|
|
@@ -6066,13 +6169,13 @@ function readJsonl(file) {
|
|
|
6066
6169
|
}
|
|
6067
6170
|
function appendJsonl(file, obj) {
|
|
6068
6171
|
try {
|
|
6069
|
-
|
|
6172
|
+
appendFileSync4(memPath(file), JSON.stringify(obj) + "\n");
|
|
6070
6173
|
} catch {
|
|
6071
6174
|
}
|
|
6072
6175
|
}
|
|
6073
6176
|
function rewriteJsonl(file, rows) {
|
|
6074
6177
|
try {
|
|
6075
|
-
|
|
6178
|
+
writeFileSync11(memPath(file), rows.map((r) => JSON.stringify(r)).join("\n") + (rows.length ? "\n" : ""));
|
|
6076
6179
|
} catch {
|
|
6077
6180
|
}
|
|
6078
6181
|
}
|
|
@@ -6081,7 +6184,7 @@ function scrubText(text) {
|
|
|
6081
6184
|
}
|
|
6082
6185
|
function addFact(input) {
|
|
6083
6186
|
const fact = {
|
|
6084
|
-
id:
|
|
6187
|
+
id: randomUUID5(),
|
|
6085
6188
|
text: scrubText(input.text),
|
|
6086
6189
|
kind: input.kind ?? "fact",
|
|
6087
6190
|
source: input.source ?? "user",
|
|
@@ -6108,7 +6211,7 @@ function summarizeAnswer(answer) {
|
|
|
6108
6211
|
}
|
|
6109
6212
|
function recordAnalysis(input) {
|
|
6110
6213
|
const entry = {
|
|
6111
|
-
id:
|
|
6214
|
+
id: randomUUID5(),
|
|
6112
6215
|
question: scrubText(input.question).slice(0, 300),
|
|
6113
6216
|
summary: scrubText(summarizeAnswer(input.answer)),
|
|
6114
6217
|
tools: input.tools,
|
|
@@ -6138,9 +6241,9 @@ function loadWinSnippets() {
|
|
|
6138
6241
|
try {
|
|
6139
6242
|
const dir = getWinsDir();
|
|
6140
6243
|
const out = [];
|
|
6141
|
-
for (const name of
|
|
6244
|
+
for (const name of readdirSync4(dir)) {
|
|
6142
6245
|
if (!name.endsWith(".md") || name.toLowerCase() === "readme.md") continue;
|
|
6143
|
-
const raw =
|
|
6246
|
+
const raw = readFileSync13(join14(dir, name), "utf-8");
|
|
6144
6247
|
const title = raw.match(/^#\s+(.+)$/m)?.[1]?.trim() ?? name.replace(/\.md$/, "");
|
|
6145
6248
|
const body = raw.replace(/^#.*$/m, "").replace(/\s+/g, " ").trim().slice(0, 300);
|
|
6146
6249
|
out.push({ id: `win:${name}`, title, text: `${title}. ${body}` });
|
|
@@ -6342,7 +6445,7 @@ async function distillSessionFactsWithTimeout(ctx, sessionId, timeoutMs = DISTIL
|
|
|
6342
6445
|
});
|
|
6343
6446
|
const raced = await Promise.race([
|
|
6344
6447
|
work,
|
|
6345
|
-
new Promise((
|
|
6448
|
+
new Promise((resolve10) => setTimeout(() => resolve10(-1), timeoutMs))
|
|
6346
6449
|
]);
|
|
6347
6450
|
if (raced >= 0) return { count: raced, background };
|
|
6348
6451
|
if (settled) return { count: await background, background };
|
|
@@ -6420,10 +6523,10 @@ __export(context_exports, {
|
|
|
6420
6523
|
setPrimaryLens: () => setPrimaryLens,
|
|
6421
6524
|
transcriptPathForSession: () => transcriptPathForSession
|
|
6422
6525
|
});
|
|
6423
|
-
import { basename as
|
|
6424
|
-
import { existsSync as
|
|
6425
|
-
import { homedir as
|
|
6426
|
-
import { randomUUID as
|
|
6526
|
+
import { basename as basename3, join as join15, resolve as resolve7, sep as sep4 } from "path";
|
|
6527
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync12, readFileSync as readFileSync14, readdirSync as readdirSync5, statSync as statSync3, rmSync as rmSync4 } from "fs";
|
|
6528
|
+
import { homedir as homedir4 } from "os";
|
|
6529
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
6427
6530
|
function isSessionStale(s) {
|
|
6428
6531
|
return Date.now() - s.mtime > STALE_SESSION_MS;
|
|
6429
6532
|
}
|
|
@@ -6436,35 +6539,35 @@ function isAnalysisReady(ctx) {
|
|
|
6436
6539
|
return Object.values(counts).some((n) => n > 0);
|
|
6437
6540
|
}
|
|
6438
6541
|
function ntrpHomeDir() {
|
|
6439
|
-
return process.env.NTRP_HOME ?
|
|
6542
|
+
return process.env.NTRP_HOME ? resolve7(process.env.NTRP_HOME) : join15(homedir4(), ".ntrp");
|
|
6440
6543
|
}
|
|
6441
6544
|
function getSessionsDir() {
|
|
6442
|
-
const dir =
|
|
6443
|
-
if (!
|
|
6444
|
-
|
|
6545
|
+
const dir = join15(ntrpHomeDir(), "sessions");
|
|
6546
|
+
if (!existsSync14(dir)) {
|
|
6547
|
+
mkdirSync6(dir, { recursive: true });
|
|
6445
6548
|
}
|
|
6446
6549
|
return dir;
|
|
6447
6550
|
}
|
|
6448
6551
|
function getDatasetsDir() {
|
|
6449
|
-
const dir =
|
|
6450
|
-
if (!
|
|
6451
|
-
|
|
6552
|
+
const dir = join15(ntrpHomeDir(), "datasets");
|
|
6553
|
+
if (!existsSync14(dir)) {
|
|
6554
|
+
mkdirSync6(dir, { recursive: true });
|
|
6452
6555
|
}
|
|
6453
6556
|
return dir;
|
|
6454
6557
|
}
|
|
6455
6558
|
function datasetPathForSession(id) {
|
|
6456
|
-
return
|
|
6559
|
+
return join15(getDatasetsDir(), `${id}.duckdb`);
|
|
6457
6560
|
}
|
|
6458
6561
|
function transcriptPathForSession(id) {
|
|
6459
|
-
return
|
|
6562
|
+
return join15(getSessionsDir(), `${id}.transcript.md`);
|
|
6460
6563
|
}
|
|
6461
6564
|
function contextDocPathForSession(id) {
|
|
6462
|
-
return
|
|
6565
|
+
return join15(getSessionsDir(), `${id}.context.md`);
|
|
6463
6566
|
}
|
|
6464
6567
|
function makeSessionId() {
|
|
6465
6568
|
const now2 = /* @__PURE__ */ new Date();
|
|
6466
6569
|
const date = now2.toISOString().slice(0, 10);
|
|
6467
|
-
const uuid2 =
|
|
6570
|
+
const uuid2 = randomUUID6().slice(0, 4);
|
|
6468
6571
|
return `${date}-${uuid2}`;
|
|
6469
6572
|
}
|
|
6470
6573
|
function isValidSessionId(id) {
|
|
@@ -6472,14 +6575,14 @@ function isValidSessionId(id) {
|
|
|
6472
6575
|
}
|
|
6473
6576
|
function sessionPathForId(id) {
|
|
6474
6577
|
if (!isValidSessionId(id)) return null;
|
|
6475
|
-
const dir =
|
|
6476
|
-
const filePath =
|
|
6477
|
-
if (filePath !== dir && !filePath.startsWith(dir +
|
|
6578
|
+
const dir = resolve7(getSessionsDir());
|
|
6579
|
+
const filePath = resolve7(dir, `${id}.json`);
|
|
6580
|
+
if (filePath !== dir && !filePath.startsWith(dir + sep4)) return null;
|
|
6478
6581
|
return filePath;
|
|
6479
6582
|
}
|
|
6480
6583
|
function initContext(oneShot, execution) {
|
|
6481
6584
|
const sessionId = makeSessionId();
|
|
6482
|
-
const sessionFile =
|
|
6585
|
+
const sessionFile = join15(getSessionsDir(), `${sessionId}.json`);
|
|
6483
6586
|
return {
|
|
6484
6587
|
sessionId,
|
|
6485
6588
|
sessionFile,
|
|
@@ -6593,7 +6696,7 @@ function recordMessage(ctx, role, content) {
|
|
|
6593
6696
|
ctx.messages.push(msg);
|
|
6594
6697
|
if (ctx.oneShot) return;
|
|
6595
6698
|
try {
|
|
6596
|
-
|
|
6699
|
+
writeFileSync12(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
|
|
6597
6700
|
} catch {
|
|
6598
6701
|
}
|
|
6599
6702
|
writeSessionContextDoc(ctx);
|
|
@@ -6601,7 +6704,7 @@ function recordMessage(ctx, role, content) {
|
|
|
6601
6704
|
function saveSessionState(ctx) {
|
|
6602
6705
|
if (ctx.oneShot) return;
|
|
6603
6706
|
try {
|
|
6604
|
-
|
|
6707
|
+
writeFileSync12(ctx.sessionFile, JSON.stringify(buildSessionFileSnapshot(ctx), null, 2) + "\n");
|
|
6605
6708
|
} catch {
|
|
6606
6709
|
}
|
|
6607
6710
|
writeSessionContextDoc(ctx);
|
|
@@ -6610,9 +6713,9 @@ function getLastActivityRelative() {
|
|
|
6610
6713
|
const dir = getSessionsDir();
|
|
6611
6714
|
let mostRecent = 0;
|
|
6612
6715
|
try {
|
|
6613
|
-
for (const name of
|
|
6716
|
+
for (const name of readdirSync5(dir)) {
|
|
6614
6717
|
if (!name.endsWith(".json")) continue;
|
|
6615
|
-
const m =
|
|
6718
|
+
const m = statSync3(join15(dir, name)).mtimeMs;
|
|
6616
6719
|
if (m > mostRecent) mostRecent = m;
|
|
6617
6720
|
}
|
|
6618
6721
|
} catch {
|
|
@@ -6638,7 +6741,7 @@ function loadSessionFile(id) {
|
|
|
6638
6741
|
const filePath = sessionPathForId(id);
|
|
6639
6742
|
if (!filePath) return null;
|
|
6640
6743
|
try {
|
|
6641
|
-
const raw =
|
|
6744
|
+
const raw = readFileSync14(filePath, "utf-8");
|
|
6642
6745
|
const session = JSON.parse(raw);
|
|
6643
6746
|
if (session.thread?.length) {
|
|
6644
6747
|
session.thread = normalizeThread(session.thread);
|
|
@@ -6652,16 +6755,16 @@ function listSessions(opts) {
|
|
|
6652
6755
|
const dir = getSessionsDir();
|
|
6653
6756
|
const entries = [];
|
|
6654
6757
|
try {
|
|
6655
|
-
const files =
|
|
6656
|
-
const filePath =
|
|
6657
|
-
return { name, filePath, mtime:
|
|
6758
|
+
const files = readdirSync5(dir).filter((name) => name.endsWith(".json")).map((name) => {
|
|
6759
|
+
const filePath = join15(dir, name);
|
|
6760
|
+
return { name, filePath, mtime: statSync3(filePath).mtimeMs };
|
|
6658
6761
|
}).sort((a, b) => b.mtime - a.mtime);
|
|
6659
6762
|
const filesToRead = opts?.limit ? files.slice(0, opts.limit) : files;
|
|
6660
6763
|
for (const { name, filePath, mtime } of filesToRead) {
|
|
6661
|
-
const id =
|
|
6764
|
+
const id = basename3(name, ".json");
|
|
6662
6765
|
if (!isValidSessionId(id)) continue;
|
|
6663
6766
|
try {
|
|
6664
|
-
const raw =
|
|
6767
|
+
const raw = readFileSync14(filePath, "utf-8");
|
|
6665
6768
|
const session = JSON.parse(raw);
|
|
6666
6769
|
entries.push({
|
|
6667
6770
|
id,
|
|
@@ -6723,7 +6826,7 @@ async function closeAllActiveSessions(ctx) {
|
|
|
6723
6826
|
skipped.push(s.id);
|
|
6724
6827
|
continue;
|
|
6725
6828
|
}
|
|
6726
|
-
|
|
6829
|
+
writeFileSync12(filePath, JSON.stringify(file, null, 2) + "\n");
|
|
6727
6830
|
writeContextDocForSessionFile(file);
|
|
6728
6831
|
closed.push(s.id);
|
|
6729
6832
|
}
|
|
@@ -6763,7 +6866,7 @@ async function rotateToFreshSession(ctx) {
|
|
|
6763
6866
|
const newId = makeSessionId();
|
|
6764
6867
|
resetContextForSwitch(ctx, {
|
|
6765
6868
|
sessionId: newId,
|
|
6766
|
-
sessionFile:
|
|
6869
|
+
sessionFile: join15(getSessionsDir(), `${newId}.json`),
|
|
6767
6870
|
messages: [],
|
|
6768
6871
|
stage: "new",
|
|
6769
6872
|
analysis: defaultSessionAnalysis(),
|
|
@@ -6802,14 +6905,14 @@ async function finalizeSession(ctx, stage) {
|
|
|
6802
6905
|
}
|
|
6803
6906
|
for (const path of [ctx.datasetPath, `${ctx.datasetPath}.wal`]) {
|
|
6804
6907
|
try {
|
|
6805
|
-
|
|
6908
|
+
rmSync4(path, { force: true });
|
|
6806
6909
|
} catch {
|
|
6807
6910
|
}
|
|
6808
6911
|
}
|
|
6809
6912
|
}
|
|
6810
6913
|
discardSessionTranscript(ctx.sessionId);
|
|
6811
6914
|
try {
|
|
6812
|
-
|
|
6915
|
+
rmSync4(contextDocPathForSession(ctx.sessionId), { force: true });
|
|
6813
6916
|
} catch {
|
|
6814
6917
|
}
|
|
6815
6918
|
return void 0;
|
|
@@ -6864,7 +6967,7 @@ async function finalizeSession(ctx, stage) {
|
|
|
6864
6967
|
file.pending_ask = ctx.pendingAsk;
|
|
6865
6968
|
}
|
|
6866
6969
|
try {
|
|
6867
|
-
|
|
6970
|
+
writeFileSync12(ctx.sessionFile, JSON.stringify(file, null, 2) + "\n");
|
|
6868
6971
|
} catch {
|
|
6869
6972
|
}
|
|
6870
6973
|
writeContextDocForSessionFile(file, { snapshot: ctx.snapshot.computeResult });
|
|
@@ -8020,22 +8123,22 @@ var init_health_score = __esm({
|
|
|
8020
8123
|
});
|
|
8021
8124
|
|
|
8022
8125
|
// src/config/profile.ts
|
|
8023
|
-
import { readFileSync as
|
|
8024
|
-
import { join as
|
|
8126
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync13, existsSync as existsSync15, mkdirSync as mkdirSync7 } from "fs";
|
|
8127
|
+
import { join as join16 } from "path";
|
|
8025
8128
|
function profilePath() {
|
|
8026
8129
|
return PROFILE_PATH;
|
|
8027
8130
|
}
|
|
8028
8131
|
function profileExists() {
|
|
8029
|
-
return
|
|
8132
|
+
return existsSync15(PROFILE_PATH);
|
|
8030
8133
|
}
|
|
8031
8134
|
function isProfileConfigured(profile = loadProfile()) {
|
|
8032
8135
|
if (!profile) return false;
|
|
8033
8136
|
return profile.company_name.trim().length > 0;
|
|
8034
8137
|
}
|
|
8035
8138
|
function loadProfile() {
|
|
8036
|
-
if (!
|
|
8139
|
+
if (!existsSync15(PROFILE_PATH)) return null;
|
|
8037
8140
|
try {
|
|
8038
|
-
const parsed = JSON.parse(
|
|
8141
|
+
const parsed = JSON.parse(readFileSync15(PROFILE_PATH, "utf-8"));
|
|
8039
8142
|
if (!parsed || typeof parsed !== "object") return null;
|
|
8040
8143
|
return parsed;
|
|
8041
8144
|
} catch {
|
|
@@ -8048,7 +8151,7 @@ var init_profile = __esm({
|
|
|
8048
8151
|
"use strict";
|
|
8049
8152
|
init_store();
|
|
8050
8153
|
NTRP_DIR3 = ntrpHome();
|
|
8051
|
-
PROFILE_PATH =
|
|
8154
|
+
PROFILE_PATH = join16(NTRP_DIR3, "profile.json");
|
|
8052
8155
|
}
|
|
8053
8156
|
});
|
|
8054
8157
|
|
|
@@ -8060,17 +8163,17 @@ __export(play_outcomes_exports, {
|
|
|
8060
8163
|
listPlayOutcomes: () => listPlayOutcomes,
|
|
8061
8164
|
recordPlayOutcomes: () => recordPlayOutcomes
|
|
8062
8165
|
});
|
|
8063
|
-
import { existsSync as
|
|
8064
|
-
import { join as
|
|
8065
|
-
import { randomUUID as
|
|
8166
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16, appendFileSync as appendFileSync5 } from "fs";
|
|
8167
|
+
import { join as join17 } from "path";
|
|
8168
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
8066
8169
|
function outcomesPath() {
|
|
8067
|
-
return
|
|
8170
|
+
return join17(getMemoryDir(), OUTCOMES_FILE);
|
|
8068
8171
|
}
|
|
8069
8172
|
function listPlayOutcomes() {
|
|
8070
8173
|
const path = outcomesPath();
|
|
8071
|
-
if (!
|
|
8174
|
+
if (!existsSync16(path)) return [];
|
|
8072
8175
|
const out = [];
|
|
8073
|
-
for (const line of
|
|
8176
|
+
for (const line of readFileSync16(path, "utf-8").split("\n")) {
|
|
8074
8177
|
const trimmed = line.trim();
|
|
8075
8178
|
if (!trimmed) continue;
|
|
8076
8179
|
try {
|
|
@@ -8100,7 +8203,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
|
|
|
8100
8203
|
if (seen.has(key)) continue;
|
|
8101
8204
|
seen.add(key);
|
|
8102
8205
|
const record = {
|
|
8103
|
-
id:
|
|
8206
|
+
id: randomUUID7(),
|
|
8104
8207
|
play_id: playId,
|
|
8105
8208
|
strategy_slug: strategy.slug,
|
|
8106
8209
|
workstream_order: outcome.workstream_order,
|
|
@@ -8113,7 +8216,7 @@ function recordPlayOutcomes(strategy, outcomes, batchId) {
|
|
|
8113
8216
|
reviewed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8114
8217
|
};
|
|
8115
8218
|
try {
|
|
8116
|
-
|
|
8219
|
+
appendFileSync5(outcomesPath(), JSON.stringify(record) + "\n");
|
|
8117
8220
|
written++;
|
|
8118
8221
|
} catch {
|
|
8119
8222
|
}
|
|
@@ -8261,7 +8364,40 @@ Close the loop to action. Produce a markdown report, a notes export, CSV
|
|
|
8261
8364
|
receipts, or a repository package \u2014 or generate a ready-to-paste prompt for
|
|
8262
8365
|
another agent to build a review deck, an Asana project, a Clay table, or an
|
|
8263
8366
|
action plan from this diagnosis. Producing an output marks the session
|
|
8264
|
-
delivered so it stops showing up as unfinished work
|
|
8367
|
+
delivered so it stops showing up as unfinished work. Files land under
|
|
8368
|
+
\`export-dir\` by kind; point Claude Desktop at a folder with \`/inbox set\`.`
|
|
8369
|
+
},
|
|
8370
|
+
{
|
|
8371
|
+
name: "exports",
|
|
8372
|
+
raw: `---
|
|
8373
|
+
name: exports
|
|
8374
|
+
description: List, open, or move export files
|
|
8375
|
+
section: Start
|
|
8376
|
+
args: [list [kind]|open|move <id|file> <dest>]
|
|
8377
|
+
handler: ../commands/exports.ts
|
|
8378
|
+
---
|
|
8379
|
+
|
|
8380
|
+
Catalog of handoffs and other deliverables. Lists recent writes from the
|
|
8381
|
+
durable \`manifest.jsonl\` under your export archive, prints absolute paths
|
|
8382
|
+
(\`open\`), and relocates files while recording the move trail so desktop AI
|
|
8383
|
+
apps can see where things went (\`move\`). Companion: \`/inbox\` sets the
|
|
8384
|
+
Claude-facing folder with stable \`latest-*\` pointers.`
|
|
8385
|
+
},
|
|
8386
|
+
{
|
|
8387
|
+
name: "inbox",
|
|
8388
|
+
raw: `---
|
|
8389
|
+
name: inbox
|
|
8390
|
+
description: Set the desktop-AI folder for handoffs
|
|
8391
|
+
section: Settings
|
|
8392
|
+
args: [show|set <path>|clear]
|
|
8393
|
+
handler: ../commands/exports.ts
|
|
8394
|
+
---
|
|
8395
|
+
|
|
8396
|
+
Declare a folder Claude Desktop (or any desktop AI) can read. NTRP copies
|
|
8397
|
+
each handoff there and overwrites stable \`latest-handoff.md\` /
|
|
8398
|
+
\`latest-handoff-deck.md\` pointers so the app always finds the newest file.
|
|
8399
|
+
\`INDEX.md\` in that folder links back to the canonical archive. Does not
|
|
8400
|
+
delete files on \`clear\` \u2014 only removes the config pointer.`
|
|
8265
8401
|
},
|
|
8266
8402
|
{
|
|
8267
8403
|
name: "onboard",
|
|
@@ -8307,7 +8443,8 @@ Validate local readiness or configure NTRP non-interactively for automation.
|
|
|
8307
8443
|
\`setup check --json\` reports license, profile, API key, database, and writable
|
|
8308
8444
|
directory state. \`setup agent\` accepts a profile JSON file or direct flags \u2014
|
|
8309
8445
|
\`--llm-key <key>\` auto-detects the provider from any pasted key
|
|
8310
|
-
(\`--llm-provider <id>\` to force one)
|
|
8446
|
+
(\`--llm-provider <id>\` to force one), plus \`--export-dir\` and
|
|
8447
|
+
\`--ai-inbox-dir\` for deliverable locations.`
|
|
8311
8448
|
},
|
|
8312
8449
|
{
|
|
8313
8450
|
name: "update",
|
|
@@ -8604,6 +8741,22 @@ handler: ../commands/progress.ts
|
|
|
8604
8741
|
Hours saved, weekly activity trend, session counts, AI token usage, and the
|
|
8605
8742
|
full milestone ladder with progress bars. Use reset (type "reset" to confirm)
|
|
8606
8743
|
to clear hours and milestones while keeping this install's identity.`
|
|
8744
|
+
},
|
|
8745
|
+
{
|
|
8746
|
+
name: "deepdive",
|
|
8747
|
+
raw: `---
|
|
8748
|
+
name: deepdive
|
|
8749
|
+
description: Metric slides \u2014 what each number means
|
|
8750
|
+
section: Navigation
|
|
8751
|
+
args: [<metric>|list|tour]
|
|
8752
|
+
handler: ../commands/deepdive.ts
|
|
8753
|
+
---
|
|
8754
|
+
|
|
8755
|
+
CLI slide deck for every vital sign and SaaS metric: definition, formula,
|
|
8756
|
+
visual, and dollar translation. Bare \`/deepdive\` runs the onboarding tour
|
|
8757
|
+
(SaaS refresher + five vitals). \`/deepdive <metric>\` jumps to one slide.
|
|
8758
|
+
\`/deepdive list\` prints the catalog. Works without an AI key. Re-run anytime
|
|
8759
|
+
from the homescreen \u2014 live values overlay when an analysis exists.`
|
|
8607
8760
|
},
|
|
8608
8761
|
{
|
|
8609
8762
|
name: "status",
|
|
@@ -8782,7 +8935,7 @@ handler: ../commands/config.ts
|
|
|
8782
8935
|
Manage CLI configuration stored at \`~/.ntrp/config.json\`. Useful keys:
|
|
8783
8936
|
\`api-key\` (Anthropic), \`openai-api-key\` (and \`groq-api-key\`, \`google-api-key\`, ...),
|
|
8784
8937
|
\`llm-primary\` (default engine), \`llm-tier\`, \`llm-auto-failover\`,
|
|
8785
|
-
\`default-format\`, \`export-dir
|
|
8938
|
+
\`default-format\`, \`export-dir\`, \`ai-inbox-dir\` (or use \`/inbox set\`).
|
|
8786
8939
|
|
|
8787
8940
|
Setting a provider key opens a hidden prompt and auto-discovers that
|
|
8788
8941
|
provider's models. Prefer \`/connect\` \u2014 it detects the provider for you.`
|
|
@@ -8888,8 +9041,8 @@ paragraph that flows into all AI surfaces.`
|
|
|
8888
9041
|
});
|
|
8889
9042
|
|
|
8890
9043
|
// src/ai/prompt-parts.ts
|
|
8891
|
-
import { existsSync as
|
|
8892
|
-
import { join as
|
|
9044
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
|
|
9045
|
+
import { join as join18 } from "path";
|
|
8893
9046
|
function buildCompanyProfileBlock() {
|
|
8894
9047
|
const p = loadProfile();
|
|
8895
9048
|
if (!p) return "";
|
|
@@ -8908,10 +9061,10 @@ function buildCompanyProfileBlock() {
|
|
|
8908
9061
|
return lines.join("\n");
|
|
8909
9062
|
}
|
|
8910
9063
|
function loadAnalystFile() {
|
|
8911
|
-
const path =
|
|
9064
|
+
const path = join18(ntrpHome(), ANALYST_FILE_NAME);
|
|
8912
9065
|
try {
|
|
8913
|
-
if (!
|
|
8914
|
-
const raw =
|
|
9066
|
+
if (!existsSync17(path)) return null;
|
|
9067
|
+
const raw = readFileSync17(path, "utf-8").trim();
|
|
8915
9068
|
if (!raw) return null;
|
|
8916
9069
|
if (raw.length <= ANALYST_FILE_MAX_CHARS) return raw;
|
|
8917
9070
|
const head = raw.slice(0, Math.floor(ANALYST_FILE_MAX_CHARS * 0.75));
|
|
@@ -9668,9 +9821,9 @@ var init_tool_schemas = __esm({
|
|
|
9668
9821
|
});
|
|
9669
9822
|
|
|
9670
9823
|
// src/ai/privacy.ts
|
|
9671
|
-
import { existsSync as
|
|
9672
|
-
import { homedir as
|
|
9673
|
-
import { join as
|
|
9824
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync8, appendFileSync as appendFileSync6 } from "fs";
|
|
9825
|
+
import { homedir as homedir5 } from "os";
|
|
9826
|
+
import { join as join19 } from "path";
|
|
9674
9827
|
function stripPII(obj) {
|
|
9675
9828
|
if (obj === null || obj === void 0) return obj;
|
|
9676
9829
|
if (typeof obj !== "object") return obj;
|
|
@@ -9685,15 +9838,15 @@ function stripPII(obj) {
|
|
|
9685
9838
|
return out;
|
|
9686
9839
|
}
|
|
9687
9840
|
function ensureAuditDir() {
|
|
9688
|
-
if (!
|
|
9689
|
-
|
|
9841
|
+
if (!existsSync18(AUDIT_DIR)) {
|
|
9842
|
+
mkdirSync8(AUDIT_DIR, { recursive: true });
|
|
9690
9843
|
}
|
|
9691
9844
|
}
|
|
9692
9845
|
function logToolCall(entry) {
|
|
9693
9846
|
ensureAuditDir();
|
|
9694
9847
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
9695
|
-
const path =
|
|
9696
|
-
|
|
9848
|
+
const path = join19(AUDIT_DIR, `agentic-${date}.jsonl`);
|
|
9849
|
+
appendFileSync6(path, JSON.stringify(entry) + "\n");
|
|
9697
9850
|
}
|
|
9698
9851
|
var PII_FIELDS, AUDIT_DIR;
|
|
9699
9852
|
var init_privacy = __esm({
|
|
@@ -9716,7 +9869,7 @@ var init_privacy = __esm({
|
|
|
9716
9869
|
"raw_data",
|
|
9717
9870
|
"metadata"
|
|
9718
9871
|
]);
|
|
9719
|
-
AUDIT_DIR =
|
|
9872
|
+
AUDIT_DIR = join19(homedir5(), ".ntrp", "audit");
|
|
9720
9873
|
}
|
|
9721
9874
|
});
|
|
9722
9875
|
|
|
@@ -11705,7 +11858,7 @@ function createPromptSession(existing, ctx) {
|
|
|
11705
11858
|
}
|
|
11706
11859
|
process.stdout.write("\n" + prompt);
|
|
11707
11860
|
try {
|
|
11708
|
-
return await new Promise((
|
|
11861
|
+
return await new Promise((resolve10, reject) => {
|
|
11709
11862
|
let value = "";
|
|
11710
11863
|
let settled = false;
|
|
11711
11864
|
const cleanup = () => {
|
|
@@ -11741,7 +11894,7 @@ function createPromptSession(existing, ctx) {
|
|
|
11741
11894
|
process.stdout.write("\n");
|
|
11742
11895
|
const trimmed = stripTerminalArtifacts(value).trim();
|
|
11743
11896
|
assertNotGlobalReplCommand(trimmed);
|
|
11744
|
-
|
|
11897
|
+
resolve10(trimmed);
|
|
11745
11898
|
});
|
|
11746
11899
|
return;
|
|
11747
11900
|
}
|
|
@@ -11757,7 +11910,7 @@ function createPromptSession(existing, ctx) {
|
|
|
11757
11910
|
process.stdout.write("\n");
|
|
11758
11911
|
const trimmed = stripTerminalArtifacts(value).trim();
|
|
11759
11912
|
assertNotGlobalReplCommand(trimmed);
|
|
11760
|
-
|
|
11913
|
+
resolve10(trimmed);
|
|
11761
11914
|
});
|
|
11762
11915
|
return;
|
|
11763
11916
|
}
|
|
@@ -12525,8 +12678,25 @@ var init_llm_attribution = __esm({
|
|
|
12525
12678
|
}
|
|
12526
12679
|
});
|
|
12527
12680
|
|
|
12528
|
-
// src/
|
|
12681
|
+
// src/ui/slides.ts
|
|
12529
12682
|
import chalk9 from "chalk";
|
|
12683
|
+
function printDeepdiveHint(metricId, label) {
|
|
12684
|
+
const name = label ?? metricId;
|
|
12685
|
+
console.log(
|
|
12686
|
+
" " + chalk9.dim("How this number works: ") + paint("accent", `/deepdive ${metricId}`) + chalk9.dim(` \u2014 ${name}`)
|
|
12687
|
+
);
|
|
12688
|
+
console.log();
|
|
12689
|
+
}
|
|
12690
|
+
var init_slides = __esm({
|
|
12691
|
+
"src/ui/slides.ts"() {
|
|
12692
|
+
"use strict";
|
|
12693
|
+
init_theme();
|
|
12694
|
+
init_layout();
|
|
12695
|
+
}
|
|
12696
|
+
});
|
|
12697
|
+
|
|
12698
|
+
// src/output/terminal.ts
|
|
12699
|
+
import chalk10 from "chalk";
|
|
12530
12700
|
import Table2 from "cli-table3";
|
|
12531
12701
|
function centerPad(text, width) {
|
|
12532
12702
|
if (text.length >= width) return text;
|
|
@@ -12545,7 +12715,7 @@ function statusBadge(status) {
|
|
|
12545
12715
|
}
|
|
12546
12716
|
}
|
|
12547
12717
|
function printHeading(label, detail) {
|
|
12548
|
-
console.log(` ${sectionHeading(label)}${detail ?
|
|
12718
|
+
console.log(` ${sectionHeading(label)}${detail ? chalk10.dim(` ${detail}`) : ""}`);
|
|
12549
12719
|
}
|
|
12550
12720
|
function printResultCard(title, rows) {
|
|
12551
12721
|
const width = resolveCardWidth({ min: 60, max: 100, margin: 4 });
|
|
@@ -12566,32 +12736,39 @@ function printVitalSignRow(vs) {
|
|
|
12566
12736
|
const label = VITAL_SIGN_LABELS[vs.vital_sign].padEnd(18);
|
|
12567
12737
|
const bar = scoreBar(vs.score, vs.status);
|
|
12568
12738
|
const score = String(Math.round(vs.score)).padStart(4);
|
|
12569
|
-
const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${
|
|
12570
|
-
console.log(` ${dot} ${label} ${bar} ${
|
|
12739
|
+
const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk10.dim(vs.dollar_label ?? "")}` : chalk10.dim("\u2014");
|
|
12740
|
+
console.log(` ${dot} ${label} ${bar} ${chalk10.bold(score)} ${chalk10.dim("\u2502")} ${impact}`);
|
|
12571
12741
|
}
|
|
12572
12742
|
function printHealthSummary(result, _pipelineMetrics) {
|
|
12573
|
-
const scoreStr = `${
|
|
12574
|
-
const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${
|
|
12743
|
+
const scoreStr = `${chalk10.bold(String(Math.round(result.overall_score)))}${chalk10.dim("/100")}`;
|
|
12744
|
+
const impact = result.total_value_at_risk != null && result.total_value_at_risk > 0 ? `${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk10.dim("total at risk")}` : chalk10.dim("No dollar-weighted risk detected");
|
|
12575
12745
|
const next = result.overall_status === "red" ? actionHint("Next:", "/playbook", "review recommended plays") : result.overall_status === "yellow" ? actionHint("Next:", "/diagnose --deep", "investigate the weak signal") : actionHint("Next:", "/report", "export the clean snapshot");
|
|
12576
12746
|
printResultCard("Overall Health", [
|
|
12577
|
-
`${
|
|
12578
|
-
`${
|
|
12579
|
-
`${
|
|
12747
|
+
`${chalk10.dim("Score")} ${scoreStr} ${statusBadge(result.overall_status)}`,
|
|
12748
|
+
`${chalk10.dim("Held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`,
|
|
12749
|
+
`${chalk10.dim("Revenue")} ${impact}`,
|
|
12580
12750
|
next
|
|
12581
12751
|
]);
|
|
12752
|
+
printDeepdiveHint(
|
|
12753
|
+
result.gating_vital_sign,
|
|
12754
|
+
VITAL_SIGN_LABELS[result.gating_vital_sign]
|
|
12755
|
+
);
|
|
12582
12756
|
}
|
|
12583
12757
|
function printHealthLine(result) {
|
|
12584
|
-
const score = `${
|
|
12758
|
+
const score = `${chalk10.bold(String(Math.round(result.overall_score)))}${chalk10.dim("/100")}`;
|
|
12585
12759
|
const parts = [
|
|
12586
|
-
`${
|
|
12587
|
-
`${
|
|
12760
|
+
`${chalk10.dim("Health")} ${score} ${statusBadge(result.overall_status)}`,
|
|
12761
|
+
`${chalk10.dim("held back by")} ${paint("accent", VITAL_SIGN_LABELS[result.gating_vital_sign])}`
|
|
12588
12762
|
];
|
|
12589
12763
|
if (result.total_value_at_risk != null && result.total_value_at_risk > 0) {
|
|
12590
|
-
parts.push(`${paint("success", formatCurrency(result.total_value_at_risk))} ${
|
|
12764
|
+
parts.push(`${paint("success", formatCurrency(result.total_value_at_risk))} ${chalk10.dim("total at risk")}`);
|
|
12591
12765
|
}
|
|
12592
12766
|
console.log();
|
|
12593
|
-
console.log(" " + parts.join(
|
|
12594
|
-
|
|
12767
|
+
console.log(" " + parts.join(chalk10.dim(" \xB7 ")));
|
|
12768
|
+
printDeepdiveHint(
|
|
12769
|
+
result.gating_vital_sign,
|
|
12770
|
+
VITAL_SIGN_LABELS[result.gating_vital_sign]
|
|
12771
|
+
);
|
|
12595
12772
|
}
|
|
12596
12773
|
function printVitalSigns(vitals) {
|
|
12597
12774
|
console.log();
|
|
@@ -12610,8 +12787,8 @@ function printSegmentSummary(segments) {
|
|
|
12610
12787
|
const dot = statusDot(seg.result.overall_status);
|
|
12611
12788
|
const name = seg.segment.name.padEnd(24);
|
|
12612
12789
|
const score = String(Math.round(seg.result.overall_score)).padStart(4);
|
|
12613
|
-
const gating =
|
|
12614
|
-
console.log(` ${dot} ${name} ${
|
|
12790
|
+
const gating = chalk10.dim(VITAL_SIGN_LABELS[seg.result.gating_vital_sign]);
|
|
12791
|
+
console.log(` ${dot} ${name} ${chalk10.bold(score)} ${chalk10.dim("\u2502")} ${gating}`);
|
|
12615
12792
|
}
|
|
12616
12793
|
console.log();
|
|
12617
12794
|
}
|
|
@@ -12636,7 +12813,7 @@ function printTopProblems(segments, limit = 7, opts = {}) {
|
|
|
12636
12813
|
if (opts.compact) return;
|
|
12637
12814
|
printHeading("Top Problems");
|
|
12638
12815
|
console.log();
|
|
12639
|
-
console.log(" " +
|
|
12816
|
+
console.log(" " + chalk10.dim("No dollar-weighted problems found across segments."));
|
|
12640
12817
|
console.log();
|
|
12641
12818
|
return;
|
|
12642
12819
|
}
|
|
@@ -12651,12 +12828,12 @@ function printTopProblems(segments, limit = 7, opts = {}) {
|
|
|
12651
12828
|
for (let i = 0; i < top.length; i++) {
|
|
12652
12829
|
const p = top[i];
|
|
12653
12830
|
console.log(
|
|
12654
|
-
` ${statusDot(p.status)} ${p.segment.padEnd(segW)} ${p.vitalSignLabel.padEnd(vitalW)} ${paint("success", dollarStrs[i].padStart(dollarW))} ${
|
|
12831
|
+
` ${statusDot(p.status)} ${p.segment.padEnd(segW)} ${p.vitalSignLabel.padEnd(vitalW)} ${paint("success", dollarStrs[i].padStart(dollarW))} ${chalk10.dim(p.dollarLabel)}`
|
|
12655
12832
|
);
|
|
12656
12833
|
}
|
|
12657
12834
|
if (problems.length > top.length) {
|
|
12658
12835
|
console.log(
|
|
12659
|
-
" " +
|
|
12836
|
+
" " + chalk10.dim(`${problems.length - top.length} more \u2014 `) + paint("accent", "/diagnose") + chalk10.dim(" for the full report")
|
|
12660
12837
|
);
|
|
12661
12838
|
}
|
|
12662
12839
|
console.log();
|
|
@@ -12665,14 +12842,14 @@ function printTopProblems(segments, limit = 7, opts = {}) {
|
|
|
12665
12842
|
const labelW = Math.max("".length, ...top.map((p) => p.dollarLabel.length));
|
|
12666
12843
|
const impactW = dollarW + 2 + labelW;
|
|
12667
12844
|
console.log(
|
|
12668
|
-
` ${sectionHeading("Top Problems")}` +
|
|
12845
|
+
` ${sectionHeading("Top Problems")}` + chalk10.dim(` (${top.length} of ${problems.length})`)
|
|
12669
12846
|
);
|
|
12670
12847
|
console.log();
|
|
12671
12848
|
const segColW = 2 + segW;
|
|
12672
12849
|
const hSeg = centerPad("Segment", segColW);
|
|
12673
12850
|
const hVital = centerPad("Vital Sign", vitalW);
|
|
12674
12851
|
const hImpact = centerPad("Revenue Impact", Math.max(impactW, "Revenue Impact".length));
|
|
12675
|
-
console.log(` ${
|
|
12852
|
+
console.log(` ${chalk10.dim(hSeg)} ${chalk10.dim(hVital)} ${chalk10.dim(hImpact)}`);
|
|
12676
12853
|
console.log();
|
|
12677
12854
|
for (let i = 0; i < top.length; i++) {
|
|
12678
12855
|
const p = top[i];
|
|
@@ -12680,39 +12857,46 @@ function printTopProblems(segments, limit = 7, opts = {}) {
|
|
|
12680
12857
|
const seg = p.segment.padEnd(segW);
|
|
12681
12858
|
const vital = p.vitalSignLabel.padEnd(vitalW);
|
|
12682
12859
|
const dollar = paint("success", dollarStrs[i].padStart(dollarW));
|
|
12683
|
-
const label =
|
|
12860
|
+
const label = chalk10.dim(p.dollarLabel);
|
|
12684
12861
|
console.log(` ${dot} ${seg} ${vital} ${dollar} ${label}`);
|
|
12685
12862
|
}
|
|
12686
12863
|
if (problems.length > top.length) {
|
|
12687
12864
|
console.log();
|
|
12688
|
-
console.log(` ${
|
|
12865
|
+
console.log(` ${chalk10.dim("Run /diagnose --segment <name> to drill in")}`);
|
|
12689
12866
|
}
|
|
12690
12867
|
console.log();
|
|
12691
12868
|
}
|
|
12692
12869
|
function printFindingCard(finding) {
|
|
12693
12870
|
const dot = severityPaint(finding.severity)("\u25CF");
|
|
12694
|
-
const dollarTag = finding.dollar_value != null && finding.dollar_value > 0 ? ` ${
|
|
12695
|
-
console.log(` ${dot} ${
|
|
12871
|
+
const dollarTag = finding.dollar_value != null && finding.dollar_value > 0 ? ` ${chalk10.dim("\xB7")} ${paint("success", formatDollarValue(finding.dollar_value))}` : "";
|
|
12872
|
+
console.log(` ${dot} ${chalk10.bold(finding.segment)}${dollarTag}`);
|
|
12696
12873
|
printMarkdown(finding.finding, { indent: 2 });
|
|
12697
12874
|
if (finding.recommended_plays && finding.recommended_plays.length > 0) {
|
|
12698
12875
|
for (const play of finding.recommended_plays) {
|
|
12699
12876
|
console.log(
|
|
12700
|
-
` ${
|
|
12877
|
+
` ${chalk10.dim("Consider:")} ${paint("accent", play.play_name)} ${chalk10.dim("\u2192")} ${chalk10.dim("/playbook " + play.play_id)}`
|
|
12701
12878
|
);
|
|
12702
12879
|
}
|
|
12703
12880
|
}
|
|
12704
|
-
|
|
12881
|
+
if (finding.recommended_focus) {
|
|
12882
|
+
printDeepdiveHint(
|
|
12883
|
+
finding.recommended_focus,
|
|
12884
|
+
VITAL_SIGN_LABELS[finding.recommended_focus]
|
|
12885
|
+
);
|
|
12886
|
+
} else {
|
|
12887
|
+
console.log();
|
|
12888
|
+
}
|
|
12705
12889
|
}
|
|
12706
12890
|
function printFindings(findings) {
|
|
12707
12891
|
if (findings.length === 0) {
|
|
12708
|
-
console.log(
|
|
12892
|
+
console.log(chalk10.dim(" No findings generated."));
|
|
12709
12893
|
return;
|
|
12710
12894
|
}
|
|
12711
12895
|
for (const finding of findings) printFindingCard(finding);
|
|
12712
12896
|
}
|
|
12713
12897
|
function printEntityCounts(counts) {
|
|
12714
12898
|
const table = new Table2({
|
|
12715
|
-
head: [
|
|
12899
|
+
head: [chalk10.dim("Entity"), chalk10.dim("Count")],
|
|
12716
12900
|
colWidths: [20, 12],
|
|
12717
12901
|
style: { head: [], border: [] }
|
|
12718
12902
|
});
|
|
@@ -12727,7 +12911,7 @@ function printSegmentDetail(seg, aggregate) {
|
|
|
12727
12911
|
console.log();
|
|
12728
12912
|
printHeading(seg.segment.name);
|
|
12729
12913
|
console.log(
|
|
12730
|
-
` ${statusDot(seg.result.overall_status)} ${color(
|
|
12914
|
+
` ${statusDot(seg.result.overall_status)} ${color(chalk10.bold(formatScore(seg.result.overall_score)))}${chalk10.dim("/100")} ${chalk10.dim("Held back by:")} ${paint("accent", VITAL_SIGN_LABELS[seg.result.gating_vital_sign])}`
|
|
12731
12915
|
);
|
|
12732
12916
|
console.log();
|
|
12733
12917
|
for (const vs of seg.result.vital_signs) {
|
|
@@ -12738,8 +12922,8 @@ function printSegmentDetail(seg, aggregate) {
|
|
|
12738
12922
|
const label = VITAL_SIGN_LABELS[vs.vital_sign].padEnd(18);
|
|
12739
12923
|
const bar = scoreBar(vs.score, vs.status);
|
|
12740
12924
|
const score = String(Math.round(vs.score)).padStart(4);
|
|
12741
|
-
const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${
|
|
12742
|
-
console.log(` ${dot} ${label} ${bar} ${
|
|
12925
|
+
const impact = vs.dollar_value != null && vs.dollar_value > 0 ? `${paint("success", formatCurrency(vs.dollar_value))} ${chalk10.dim(vs.dollar_label ?? "")}` : chalk10.dim("\u2014");
|
|
12926
|
+
console.log(` ${dot} ${label} ${bar} ${chalk10.bold(score)} ${padLeft(deltaStr, 4)} ${chalk10.dim("\u2502")} ${impact}`);
|
|
12743
12927
|
}
|
|
12744
12928
|
console.log();
|
|
12745
12929
|
}
|
|
@@ -12850,12 +13034,12 @@ async function renderDiagnoseStream(options) {
|
|
|
12850
13034
|
console.log();
|
|
12851
13035
|
}
|
|
12852
13036
|
if (collectedFindings.length === 0) {
|
|
12853
|
-
console.log(
|
|
13037
|
+
console.log(chalk10.dim(" No findings generated."));
|
|
12854
13038
|
console.log();
|
|
12855
13039
|
}
|
|
12856
13040
|
if (toolCalls > 0) {
|
|
12857
13041
|
console.log(
|
|
12858
|
-
|
|
13042
|
+
chalk10.dim(` Investigated with ${toolCalls} tool call${toolCalls === 1 ? "" : "s"}`)
|
|
12859
13043
|
);
|
|
12860
13044
|
console.log();
|
|
12861
13045
|
}
|
|
@@ -12876,7 +13060,7 @@ async function renderDiagnoseStream(options) {
|
|
|
12876
13060
|
});
|
|
12877
13061
|
} catch (err) {
|
|
12878
13062
|
findingsSpinner.fail(deep ? "Agentic investigation failed" : "AI findings failed");
|
|
12879
|
-
console.error(
|
|
13063
|
+
console.error(chalk10.dim(String(err)));
|
|
12880
13064
|
}
|
|
12881
13065
|
}
|
|
12882
13066
|
return { fullResult, findings: collectedFindings };
|
|
@@ -12890,14 +13074,14 @@ function printMetricsTable(metrics, groupOrder) {
|
|
|
12890
13074
|
for (const m of groupMetrics) {
|
|
12891
13075
|
const dot = m.unavailable_reason ? statusDot("neutral") : statusDot(m.status);
|
|
12892
13076
|
const label = m.label.padEnd(28);
|
|
12893
|
-
const valueStr = m.unavailable_reason ?
|
|
13077
|
+
const valueStr = m.unavailable_reason ? chalk10.dim("--") : chalk10.bold(m.formatted);
|
|
12894
13078
|
const confTag = m.confidence != null && m.confidence < 80 && m.confidence_label ? paint("warning", ` ${m.confidence_label} (${m.confidence})`) : "";
|
|
12895
|
-
const note = m.unavailable_reason ?
|
|
13079
|
+
const note = m.unavailable_reason ? chalk10.dim(m.unavailable_reason) : m.benchmark_note ? chalk10.dim(m.benchmark_note) : "";
|
|
12896
13080
|
console.log(` ${dot} ${label} ${valueStr}${confTag}${note ? " " + note : ""}`);
|
|
12897
13081
|
if (m.reliability_gate?.requirements?.length && (m.confidence ?? 100) < 80) {
|
|
12898
13082
|
const gate = m.reliability_gate.requirements[0];
|
|
12899
13083
|
if (gate) {
|
|
12900
|
-
console.log(
|
|
13084
|
+
console.log(chalk10.dim(` \u2514 Gate: ${gate}`));
|
|
12901
13085
|
}
|
|
12902
13086
|
}
|
|
12903
13087
|
}
|
|
@@ -12913,6 +13097,7 @@ var init_terminal = __esm({
|
|
|
12913
13097
|
init_theme();
|
|
12914
13098
|
init_layout();
|
|
12915
13099
|
init_llm_attribution();
|
|
13100
|
+
init_slides();
|
|
12916
13101
|
}
|
|
12917
13102
|
});
|
|
12918
13103
|
|
|
@@ -12922,15 +13107,30 @@ __export(metrics_report_exports, {
|
|
|
12922
13107
|
printMetricsNextSteps: () => printMetricsNextSteps,
|
|
12923
13108
|
renderMetricsReport: () => renderMetricsReport
|
|
12924
13109
|
});
|
|
12925
|
-
import
|
|
13110
|
+
import chalk11 from "chalk";
|
|
13111
|
+
function pickDeepdiveMetric(metrics) {
|
|
13112
|
+
const rank = (s) => s === "red" ? 0 : s === "yellow" ? 1 : s === "green" ? 2 : 3;
|
|
13113
|
+
const core = ["nrr", "arr", "grr", "pipeline_coverage", "win_rate", "pipeline_velocity"];
|
|
13114
|
+
const coreRank = (id) => {
|
|
13115
|
+
const i = core.indexOf(id);
|
|
13116
|
+
return i === -1 ? 99 : i;
|
|
13117
|
+
};
|
|
13118
|
+
const usable = metrics.filter((m) => m.value != null);
|
|
13119
|
+
if (usable.length === 0) return void 0;
|
|
13120
|
+
return [...usable].sort((a, b) => {
|
|
13121
|
+
const rd = rank(a.status) - rank(b.status);
|
|
13122
|
+
if (rd !== 0) return rd;
|
|
13123
|
+
return coreRank(a.metric) - coreRank(b.metric);
|
|
13124
|
+
})[0];
|
|
13125
|
+
}
|
|
12926
13126
|
function renderMetricsReport(ctx, metrics, coverage, sourceType, options = {}) {
|
|
12927
13127
|
const title = options.title ?? "SaaS Metrics Analysis";
|
|
12928
13128
|
const tier = coverageTier(coverage);
|
|
12929
13129
|
const deterministic = buildDeterministicInsights(metrics, coverage, sourceType);
|
|
12930
13130
|
const headline = pickHeadlineInsight(deterministic);
|
|
12931
13131
|
console.log();
|
|
12932
|
-
console.log(
|
|
12933
|
-
console.log(" " +
|
|
13132
|
+
console.log(chalk11.bold(` ${title}`));
|
|
13133
|
+
console.log(" " + chalk11.dim(formatCoverageHeader(sourceType, coverage)));
|
|
12934
13134
|
console.log();
|
|
12935
13135
|
printDataQualityPanel(coverage, sourceType, tier);
|
|
12936
13136
|
if (options.snapshot && coverage.distinct_quarters >= 2) {
|
|
@@ -12938,17 +13138,21 @@ function renderMetricsReport(ctx, metrics, coverage, sourceType, options = {}) {
|
|
|
12938
13138
|
}
|
|
12939
13139
|
if (headline) {
|
|
12940
13140
|
console.log(" " + paint("warning", "\u25B8 Headline"));
|
|
12941
|
-
console.log(" " +
|
|
13141
|
+
console.log(" " + chalk11.white(wrapInsight(headline)));
|
|
12942
13142
|
console.log();
|
|
12943
13143
|
}
|
|
12944
13144
|
printMetricsTable(metrics, GROUP_ORDER);
|
|
13145
|
+
const dive = pickDeepdiveMetric(metrics);
|
|
13146
|
+
if (dive) {
|
|
13147
|
+
printDeepdiveHint(dive.metric, dive.label);
|
|
13148
|
+
}
|
|
12945
13149
|
if (deterministic.length > 0) {
|
|
12946
13150
|
console.log(" " + bold("Pattern checks"));
|
|
12947
13151
|
console.log();
|
|
12948
13152
|
for (const insight of deterministic.slice(0, 5)) {
|
|
12949
13153
|
const dot = insight.severity === "warning" ? statusDot("yellow") : insight.severity === "critical" ? statusDot("red") : statusDot("neutral");
|
|
12950
13154
|
if (insight.headline) continue;
|
|
12951
|
-
console.log(` ${dot} ${
|
|
13155
|
+
console.log(` ${dot} ${chalk11.dim(wrapInsight(insight.message))}`);
|
|
12952
13156
|
}
|
|
12953
13157
|
console.log();
|
|
12954
13158
|
}
|
|
@@ -12977,7 +13181,7 @@ function printDataQualityPanel(coverage, sourceType, tier) {
|
|
|
12977
13181
|
rows.push(["Ledger", "not loaded \u2014 retention inferred from CRM"]);
|
|
12978
13182
|
}
|
|
12979
13183
|
for (const [label, value] of rows) {
|
|
12980
|
-
console.log(` ${
|
|
13184
|
+
console.log(` ${chalk11.dim(String(label).padEnd(14))} ${value}`);
|
|
12981
13185
|
}
|
|
12982
13186
|
console.log();
|
|
12983
13187
|
}
|
|
@@ -12988,10 +13192,10 @@ function printCloseTrend(snapshot, cadence) {
|
|
|
12988
13192
|
console.log(" " + bold(`Close trend (${cadence})`));
|
|
12989
13193
|
console.log();
|
|
12990
13194
|
for (const b of recent) {
|
|
12991
|
-
const newStr = b.new_arr > 0 ?
|
|
12992
|
-
const expStr = b.expansion_arr > 0 ?
|
|
13195
|
+
const newStr = b.new_arr > 0 ? chalk11.dim(` new $${formatShort(b.new_arr)}`) : "";
|
|
13196
|
+
const expStr = b.expansion_arr > 0 ? chalk11.dim(` exp $${formatShort(b.expansion_arr)}`) : "";
|
|
12993
13197
|
console.log(
|
|
12994
|
-
` ${
|
|
13198
|
+
` ${chalk11.dim(b.period.padEnd(8))} ${chalk11.bold("$" + formatShort(b.closed_won_total))}${newStr}${expStr} ${chalk11.dim(`(${b.closed_won_count} deals)`)}`
|
|
12995
13199
|
);
|
|
12996
13200
|
}
|
|
12997
13201
|
console.log();
|
|
@@ -13017,6 +13221,7 @@ var init_metrics_report = __esm({
|
|
|
13017
13221
|
init_terminal();
|
|
13018
13222
|
init_theme();
|
|
13019
13223
|
init_companion();
|
|
13224
|
+
init_slides();
|
|
13020
13225
|
GROUP_ORDER = [
|
|
13021
13226
|
"Revenue",
|
|
13022
13227
|
"Retention",
|
|
@@ -13211,7 +13416,7 @@ var diagnose_exports = {};
|
|
|
13211
13416
|
__export(diagnose_exports, {
|
|
13212
13417
|
handler: () => handler
|
|
13213
13418
|
});
|
|
13214
|
-
import
|
|
13419
|
+
import chalk12 from "chalk";
|
|
13215
13420
|
async function handler(args, ctx) {
|
|
13216
13421
|
await hydrateAnalysisFromPersistedState(ctx);
|
|
13217
13422
|
const { flags } = parseArgs(args, ["findings", "deep", "compact"]);
|
|
@@ -13240,9 +13445,9 @@ async function handler(args, ctx) {
|
|
|
13240
13445
|
}
|
|
13241
13446
|
if (options.findings && !canUseReplAi(ctx)) {
|
|
13242
13447
|
console.log();
|
|
13243
|
-
console.log(" " +
|
|
13244
|
-
console.log(" " +
|
|
13245
|
-
console.log(" " +
|
|
13448
|
+
console.log(" " + chalk12.red("AI findings run only in the interactive REPL."));
|
|
13449
|
+
console.log(" " + chalk12.dim("Vital signs compute without a key \u2014 omit --findings for numbers only."));
|
|
13450
|
+
console.log(" " + chalk12.dim("Start with ") + paint("accent", "ntrp") + chalk12.dim(", run ") + paint("accent", "/connect") + chalk12.dim(" (any provider key), then /diagnose --findings."));
|
|
13246
13451
|
console.log();
|
|
13247
13452
|
return;
|
|
13248
13453
|
}
|
|
@@ -13274,7 +13479,7 @@ async function handler(args, ctx) {
|
|
|
13274
13479
|
}
|
|
13275
13480
|
ctx.skipTimeBankDiagnoseCredit = false;
|
|
13276
13481
|
if (ctx.oneShot && options.findings) {
|
|
13277
|
-
console.log(
|
|
13482
|
+
console.log(chalk12.dim(" For follow-up questions, run `ntrp` and ask in plain English."));
|
|
13278
13483
|
console.log();
|
|
13279
13484
|
}
|
|
13280
13485
|
return summary;
|
|
@@ -13332,7 +13537,7 @@ async function runDiagnose(options, ctx) {
|
|
|
13332
13537
|
});
|
|
13333
13538
|
return buildDiagnoseSummary(fullResult.aggregate, findings);
|
|
13334
13539
|
} catch (err) {
|
|
13335
|
-
console.error(
|
|
13540
|
+
console.error(chalk12.red(String(err)));
|
|
13336
13541
|
process.exit(1);
|
|
13337
13542
|
}
|
|
13338
13543
|
}
|
|
@@ -13344,7 +13549,7 @@ async function runSegmentDiagnose(options) {
|
|
|
13344
13549
|
spinner.succeed("Diagnosis complete");
|
|
13345
13550
|
} catch (err) {
|
|
13346
13551
|
spinner.fail("Diagnosis failed");
|
|
13347
|
-
console.error(
|
|
13552
|
+
console.error(chalk12.red(String(err)));
|
|
13348
13553
|
process.exit(1);
|
|
13349
13554
|
}
|
|
13350
13555
|
const needle = options.segment.toLowerCase();
|
|
@@ -13353,19 +13558,19 @@ async function runSegmentDiagnose(options) {
|
|
|
13353
13558
|
const subs = result.segments.filter((s) => s.segment.name.toLowerCase().includes(needle));
|
|
13354
13559
|
if (subs.length === 1) match = subs[0];
|
|
13355
13560
|
else if (subs.length > 1) {
|
|
13356
|
-
console.error(
|
|
13561
|
+
console.error(chalk12.yellow(`
|
|
13357
13562
|
"${options.segment}" matches multiple segments:`));
|
|
13358
|
-
for (const s of subs) console.log(
|
|
13563
|
+
for (const s of subs) console.log(chalk12.dim(` - ${s.segment.name}`));
|
|
13359
13564
|
console.log();
|
|
13360
13565
|
return;
|
|
13361
13566
|
}
|
|
13362
13567
|
}
|
|
13363
13568
|
if (!match) {
|
|
13364
|
-
console.error(
|
|
13569
|
+
console.error(chalk12.red(`
|
|
13365
13570
|
No segment matching "${options.segment}".`));
|
|
13366
13571
|
if (result.segments.length > 0) {
|
|
13367
|
-
console.log(
|
|
13368
|
-
for (const s of result.segments) console.log(
|
|
13572
|
+
console.log(chalk12.dim(" Available segments:"));
|
|
13573
|
+
for (const s of result.segments) console.log(chalk12.dim(` - ${s.segment.name}`));
|
|
13369
13574
|
}
|
|
13370
13575
|
console.log();
|
|
13371
13576
|
return;
|
|
@@ -13419,145 +13624,1089 @@ var init_diagnose = __esm({
|
|
|
13419
13624
|
}
|
|
13420
13625
|
});
|
|
13421
13626
|
|
|
13422
|
-
// src/services/session-analysis.ts
|
|
13423
|
-
async function loadSessionAnalysisBundle() {
|
|
13424
|
-
const [diagnosis, metrics] = await Promise.all([
|
|
13425
|
-
loadLatestDiagnosis(),
|
|
13426
|
-
loadLatestMetricsAnalysis()
|
|
13427
|
-
]);
|
|
13428
|
-
return { diagnosis, metrics };
|
|
13627
|
+
// src/services/session-analysis.ts
|
|
13628
|
+
async function loadSessionAnalysisBundle() {
|
|
13629
|
+
const [diagnosis, metrics] = await Promise.all([
|
|
13630
|
+
loadLatestDiagnosis(),
|
|
13631
|
+
loadLatestMetricsAnalysis()
|
|
13632
|
+
]);
|
|
13633
|
+
return { diagnosis, metrics };
|
|
13634
|
+
}
|
|
13635
|
+
function hasAnyAnalysis(bundle) {
|
|
13636
|
+
return bundle.diagnosis != null || bundle.metrics != null;
|
|
13637
|
+
}
|
|
13638
|
+
function formatMetricLine(row) {
|
|
13639
|
+
const label = row.label ?? row.metric;
|
|
13640
|
+
const formatted = row.formatted ?? "--";
|
|
13641
|
+
const conf = row.confidence;
|
|
13642
|
+
const confStr = conf != null && conf < 80 ? ` (${conf}% conf)` : "";
|
|
13643
|
+
return `- ${label}: ${formatted}${confStr}`;
|
|
13644
|
+
}
|
|
13645
|
+
function buildHandoffContextBlock(bundle, ctx) {
|
|
13646
|
+
const { diagnosis, metrics } = bundle;
|
|
13647
|
+
const profile = loadProfile();
|
|
13648
|
+
const lines = [];
|
|
13649
|
+
if (profile?.company_name) {
|
|
13650
|
+
lines.push(`Company: ${profile.company_name} (${profile.industry})`);
|
|
13651
|
+
lines.push(`Sales motion: ${profile.sales_motion}${profile.average_deal_size ? ` \xB7 avg deal ${profile.average_deal_size}` : ""}`);
|
|
13652
|
+
if (profile.user_scope) lines.push(`My scope: ${profile.user_scope}`);
|
|
13653
|
+
}
|
|
13654
|
+
if (ctx.dataset?.label) {
|
|
13655
|
+
const counts = ctx.dataset.counts ?? {};
|
|
13656
|
+
const countStr = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(", ");
|
|
13657
|
+
lines.push(`Dataset: ${ctx.dataset.label}${countStr ? ` (${countStr})` : ""}`);
|
|
13658
|
+
}
|
|
13659
|
+
const completed = ctx.analysis.completed;
|
|
13660
|
+
if (completed.length > 0) {
|
|
13661
|
+
lines.push(`Analysis lenses completed: ${completed.join(", ")}`);
|
|
13662
|
+
}
|
|
13663
|
+
lines.push("");
|
|
13664
|
+
if (diagnosis) {
|
|
13665
|
+
const { health, findings } = diagnosis;
|
|
13666
|
+
lines.push("## GTM health (vital signs)");
|
|
13667
|
+
lines.push(`Overall score: ${Math.round(health.overall_score)} (${health.overall_status})`);
|
|
13668
|
+
if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
|
|
13669
|
+
lines.push(`Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
|
|
13670
|
+
}
|
|
13671
|
+
lines.push("");
|
|
13672
|
+
lines.push("### Vital signs");
|
|
13673
|
+
for (const vs of health.vital_signs) {
|
|
13674
|
+
const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
|
|
13675
|
+
const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
|
|
13676
|
+
lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
|
|
13677
|
+
}
|
|
13678
|
+
if (findings.length > 0) {
|
|
13679
|
+
lines.push("");
|
|
13680
|
+
lines.push("### GTM findings");
|
|
13681
|
+
appendFindings(lines, findings);
|
|
13682
|
+
}
|
|
13683
|
+
lines.push("");
|
|
13684
|
+
}
|
|
13685
|
+
if (metrics && metrics.metrics.length > 0) {
|
|
13686
|
+
lines.push("## SaaS metrics");
|
|
13687
|
+
const byKey = new Map(metrics.metrics.map((r) => [r.metric, r]));
|
|
13688
|
+
for (const key of KEY_METRICS) {
|
|
13689
|
+
const row = byKey.get(key);
|
|
13690
|
+
if (row) lines.push(formatMetricLine(row));
|
|
13691
|
+
}
|
|
13692
|
+
if (metrics.findings.length > 0) {
|
|
13693
|
+
lines.push("");
|
|
13694
|
+
lines.push("### Metrics findings");
|
|
13695
|
+
appendFindings(lines, metrics.findings);
|
|
13696
|
+
}
|
|
13697
|
+
lines.push("");
|
|
13698
|
+
}
|
|
13699
|
+
if (!diagnosis && metrics) {
|
|
13700
|
+
lines.unshift("Primary analysis: SaaS metrics (no GTM health snapshot on this session yet).", "");
|
|
13701
|
+
} else if (diagnosis && !metrics) {
|
|
13702
|
+
lines.push("(SaaS metrics not run on this session \u2014 run /metrics for the revenue view)");
|
|
13703
|
+
}
|
|
13704
|
+
return lines.join("\n").trim();
|
|
13705
|
+
}
|
|
13706
|
+
function buildExploreContextBlock(bundle, ctx) {
|
|
13707
|
+
const full = buildHandoffContextBlock(bundle, ctx);
|
|
13708
|
+
if (!full) return "";
|
|
13709
|
+
const lines = full.split("\n");
|
|
13710
|
+
const out = [
|
|
13711
|
+
"COMPLETED ANALYSIS (the user already saw the full report \u2014 cite this, do not re-dump it):",
|
|
13712
|
+
""
|
|
13713
|
+
];
|
|
13714
|
+
let inFindings = false;
|
|
13715
|
+
let findingCount = 0;
|
|
13716
|
+
for (const line of lines) {
|
|
13717
|
+
if (line.startsWith("### GTM findings") || line.startsWith("### Metrics findings")) {
|
|
13718
|
+
inFindings = true;
|
|
13719
|
+
out.push(line);
|
|
13720
|
+
continue;
|
|
13721
|
+
}
|
|
13722
|
+
if (inFindings && line.startsWith("- [")) {
|
|
13723
|
+
if (findingCount >= 5) continue;
|
|
13724
|
+
out.push(line);
|
|
13725
|
+
findingCount++;
|
|
13726
|
+
continue;
|
|
13727
|
+
}
|
|
13728
|
+
if (inFindings && line.startsWith("##")) {
|
|
13729
|
+
inFindings = false;
|
|
13730
|
+
}
|
|
13731
|
+
if (line.startsWith("## ") || line.startsWith("### Vital") || line.startsWith("- ") && !inFindings) {
|
|
13732
|
+
if (line.startsWith("(SaaS metrics not run")) continue;
|
|
13733
|
+
out.push(line);
|
|
13734
|
+
}
|
|
13735
|
+
if (line.startsWith("Overall score:") || line.startsWith("Total value at risk:")) {
|
|
13736
|
+
out.push(line);
|
|
13737
|
+
}
|
|
13738
|
+
if (line.startsWith("- ARR:") || line.startsWith("- NRR:") || line.startsWith("- GRR:")) {
|
|
13739
|
+
out.push(line);
|
|
13740
|
+
}
|
|
13741
|
+
}
|
|
13742
|
+
return out.join("\n").trim();
|
|
13743
|
+
}
|
|
13744
|
+
function appendFindings(lines, findings) {
|
|
13745
|
+
for (const f of findings.slice(0, 12)) {
|
|
13746
|
+
const dollars = f.dollar_value != null ? ` (${formatCurrency(f.dollar_value)})` : "";
|
|
13747
|
+
const plays = f.recommended_plays?.length ? ` \u2192 Plays: ${f.recommended_plays.map((p) => p.play_name).join(", ")}` : "";
|
|
13748
|
+
lines.push(`- [${f.severity}]${dollars} ${f.finding}${plays}`);
|
|
13749
|
+
}
|
|
13750
|
+
}
|
|
13751
|
+
function handoffInstructionPrefix(primary) {
|
|
13752
|
+
if (primary === "revenue_metrics") {
|
|
13753
|
+
return "the SaaS metrics and pipeline context below";
|
|
13754
|
+
}
|
|
13755
|
+
return "the pipeline diagnosis below";
|
|
13756
|
+
}
|
|
13757
|
+
var KEY_METRICS;
|
|
13758
|
+
var init_session_analysis = __esm({
|
|
13759
|
+
"src/services/session-analysis.ts"() {
|
|
13760
|
+
"use strict";
|
|
13761
|
+
init_queries();
|
|
13762
|
+
init_profile();
|
|
13763
|
+
init_formatters();
|
|
13764
|
+
init_theme();
|
|
13765
|
+
KEY_METRICS = ["arr", "nrr", "grr", "win_rate", "pipeline_coverage"];
|
|
13766
|
+
}
|
|
13767
|
+
});
|
|
13768
|
+
|
|
13769
|
+
// src/data/metric-definitions.ts
|
|
13770
|
+
function pctBand(metric, motion) {
|
|
13771
|
+
const m = motion ?? "mid_market";
|
|
13772
|
+
const t = METRICS_BENCHMARKS[m][metric];
|
|
13773
|
+
return `${motionBenchmarkLabel(m)} green \u2265${t.green}${metric === "pipeline_coverage" ? "x" : "%"}, yellow \u2265${t.yellow}${metric === "pipeline_coverage" ? "x" : "%"}`;
|
|
13774
|
+
}
|
|
13775
|
+
function monthsBand(motion) {
|
|
13776
|
+
const m = motion ?? "mid_market";
|
|
13777
|
+
const t = METRICS_BENCHMARKS[m].payback_months;
|
|
13778
|
+
return `${motionBenchmarkLabel(m)} green \u2264${t.green}mo, yellow \u2264${t.yellow}mo`;
|
|
13779
|
+
}
|
|
13780
|
+
function magicBand(motion) {
|
|
13781
|
+
const m = motion ?? "mid_market";
|
|
13782
|
+
const t = METRICS_BENCHMARKS[m].magic_number;
|
|
13783
|
+
return `${motionBenchmarkLabel(m)} green \u2265${t.green}, yellow \u2265${t.yellow}`;
|
|
13784
|
+
}
|
|
13785
|
+
function getMetricExplainer(id) {
|
|
13786
|
+
return BY_ID.get(id);
|
|
13787
|
+
}
|
|
13788
|
+
function resolveMetricId(query) {
|
|
13789
|
+
const q = query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
13790
|
+
if (!q) return void 0;
|
|
13791
|
+
if (BY_ID.has(q)) return q;
|
|
13792
|
+
const direct = ALIAS_INDEX.get(q);
|
|
13793
|
+
if (direct) return direct;
|
|
13794
|
+
const norm = q.replace(/[-\s]+/g, "_");
|
|
13795
|
+
if (BY_ID.has(norm)) return norm;
|
|
13796
|
+
return ALIAS_INDEX.get(norm);
|
|
13797
|
+
}
|
|
13798
|
+
var VITALS, SAAS, METRIC_DEFINITIONS, BY_ID, ALIAS_INDEX, SAAS_METRIC_IDS;
|
|
13799
|
+
var init_metric_definitions = __esm({
|
|
13800
|
+
"src/data/metric-definitions.ts"() {
|
|
13801
|
+
"use strict";
|
|
13802
|
+
init_metrics_benchmarks();
|
|
13803
|
+
VITALS = [
|
|
13804
|
+
{
|
|
13805
|
+
id: "freshness",
|
|
13806
|
+
kind: "vital",
|
|
13807
|
+
label: "Freshness",
|
|
13808
|
+
group: "Vital Signs",
|
|
13809
|
+
tagline: "Is your CRM telling the truth about what's alive?",
|
|
13810
|
+
how_computed: "Weighted average of people, organizations, and opportunities with recent activity (and open opps not past-due). Defaults: people/orgs 90-day window, opps 30-day window; weights 35/30/35.",
|
|
13811
|
+
formula_lines: [
|
|
13812
|
+
"freshness = people%\xD70.35 + orgs%\xD70.30 + opps%\xD70.35",
|
|
13813
|
+
"people/orgs fresh if activity within 90d",
|
|
13814
|
+
"opps fresh if activity within 30d AND not past-due"
|
|
13815
|
+
],
|
|
13816
|
+
meaning: 'Board question: "how much of this pipeline is real vs fiction?" Dollar value = sum of amount on stale opportunities \u2014 pipeline at risk.',
|
|
13817
|
+
expert_read: "Cut by owner and by stage first \u2014 freshness reds concentrate on people or process, rarely evenly. In a long-cycle enterprise motion 30 quiet days can be normal cadence; in a velocity motion it's a dead deal. A sudden cliff usually means a broken integration or a departed rep, not gradual decay. False positive to check: bulk-imported records nobody has touched yet.",
|
|
13818
|
+
deepdive: [
|
|
13819
|
+
"Status: green \u226580, yellow \u226560, red below 60 (motion presets can shift windows).",
|
|
13820
|
+
'Dollar translation: sum of amount on stale open opportunities \u2192 "pipeline at risk".',
|
|
13821
|
+
"Layer 1 of the gating stack \u2014 a red here bounds what you can trust downstream.",
|
|
13822
|
+
"Trigger play: Clean Dead Pipeline (clean-dead-pipeline) when score < 60.",
|
|
13823
|
+
"Levers: stale-deal alert at N quiet days, weekly hygiene scrub, enrichment refresh on quiet records, signal-triggered reactivation for paid-for dormant accounts."
|
|
13824
|
+
],
|
|
13825
|
+
visual: {
|
|
13826
|
+
kind: "bars",
|
|
13827
|
+
caption: "Exemplar component mix (higher = fresher)",
|
|
13828
|
+
bars: [
|
|
13829
|
+
{ label: "People", value: 72, tone: "yellow" },
|
|
13830
|
+
{ label: "Organizations", value: 81, tone: "green" },
|
|
13831
|
+
{ label: "Opportunities", value: 44, tone: "red" }
|
|
13832
|
+
]
|
|
13833
|
+
},
|
|
13834
|
+
play_id: "clean-dead-pipeline",
|
|
13835
|
+
dollar_label: "pipeline at risk",
|
|
13836
|
+
audience: {
|
|
13837
|
+
board: "Freshness answers whether the pipeline number is real. Low freshness means forecast risk \u2014 stale deals inflate coverage and hide the true gap.",
|
|
13838
|
+
ops: "Score = weighted recency across people/orgs/opps. Cut by owner and stage; install a stale-deal alert and weekly scrub. Play: Clean Dead Pipeline."
|
|
13839
|
+
},
|
|
13840
|
+
aliases: ["data freshness", "stale", "zombie deals", "crm freshness"]
|
|
13841
|
+
},
|
|
13842
|
+
{
|
|
13843
|
+
id: "flow_rate",
|
|
13844
|
+
kind: "vital",
|
|
13845
|
+
label: "Flow Rate",
|
|
13846
|
+
group: "Vital Signs",
|
|
13847
|
+
tagline: "How fast do deals actually move \u2014 and where do they die?",
|
|
13848
|
+
how_computed: "Base score from average open-deal age vs max_days, then a penalty (up to \u221220) for the share of stuck deals (no update beyond stuck_days, or past-due close). Status is driven by average open age, not the score alone.",
|
|
13849
|
+
formula_lines: [
|
|
13850
|
+
"base = 100 \xD7 (1 \u2212 avgOpenAge / max_days)",
|
|
13851
|
+
"score = base \u2212 stuckSharePenalty (\u226420)",
|
|
13852
|
+
"stuck = no update > stuck_days OR past-due close"
|
|
13853
|
+
],
|
|
13854
|
+
meaning: 'Board question: "is next quarter slipping because deals are stuck?" Dollar value = amount stuck in pipeline.',
|
|
13855
|
+
expert_read: "Cut by stage-age, not just deal-age \u2014 find the stage where deals go to die (usually one). Compare stuck-deal age to this company's own median cycle, not a generic norm. Stuck + past-due close dates together signal happy-ears forecasting, a credibility problem before it's a revenue problem.",
|
|
13856
|
+
deepdive: [
|
|
13857
|
+
"Status from avg open age: \u226445d green, \u226490d yellow, else red (defaults; max_days 120, stuck_days 60).",
|
|
13858
|
+
'Dollar translation: sum of amount on stuck deals \u2192 "stuck in pipeline".',
|
|
13859
|
+
"Layer 2 of the gating stack (with Drop Rate).",
|
|
13860
|
+
"Trigger play: Unstick the Pipeline (unstick-pipeline) when score is weak.",
|
|
13861
|
+
"Levers: stage-age report, past-due close cleanup, progression plans on stuck deals, forecast hygiene on happy-ears dates."
|
|
13862
|
+
],
|
|
13863
|
+
visual: {
|
|
13864
|
+
kind: "funnel",
|
|
13865
|
+
caption: "Exemplar stage ages \u2014 find the stage where deals go to die",
|
|
13866
|
+
funnel: [
|
|
13867
|
+
{ label: "Discovery", widthPct: 100 },
|
|
13868
|
+
{ label: "Qualify", widthPct: 78 },
|
|
13869
|
+
{ label: "Propose", widthPct: 55 },
|
|
13870
|
+
{ label: "Negotiate", widthPct: 22 },
|
|
13871
|
+
{ label: "Closed", widthPct: 12 }
|
|
13872
|
+
]
|
|
13873
|
+
},
|
|
13874
|
+
play_id: "unstick-pipeline",
|
|
13875
|
+
dollar_label: "stuck in pipeline",
|
|
13876
|
+
audience: {
|
|
13877
|
+
board: "Flow Rate is velocity risk. Stuck pipeline with past-due closes is a credibility problem for the forecast before it is a revenue miss.",
|
|
13878
|
+
ops: "Find the stage with collapsing advancement and age. Clear past-due closes, write progression plans on stuck deals. Play: Unstick the Pipeline."
|
|
13879
|
+
},
|
|
13880
|
+
aliases: ["flow rate", "deal velocity", "stuck deals", "stuck pipeline"]
|
|
13881
|
+
},
|
|
13882
|
+
{
|
|
13883
|
+
id: "drop_rate",
|
|
13884
|
+
kind: "vital",
|
|
13885
|
+
label: "Drop Rate",
|
|
13886
|
+
group: "Vital Signs",
|
|
13887
|
+
tagline: "Where do leads vanish between systems?",
|
|
13888
|
+
how_computed: "Blend of cross-system retention (marketing people also present in sales) and opportunity retention (open opps not abandoned). Defaults weight cross-system 60% / opp retention 40%. Abandoned = open opps with no activity in 30 days.",
|
|
13889
|
+
formula_lines: [
|
|
13890
|
+
"score = crossSystemRetention\xD70.6 + oppRetention\xD70.4",
|
|
13891
|
+
"cross-system = marketing people also in sales CRM",
|
|
13892
|
+
"abandoned = open opps with no activity in 30d"
|
|
13893
|
+
],
|
|
13894
|
+
meaning: 'Board question: "how much pipeline are we paying for and never working?" Dollar value = droppedCount \xD7 conversionRate \xD7 avgDealSize \u2014 est. lost at handoff.',
|
|
13895
|
+
expert_read: "This is almost always a systems failure \u2014 routing rules, unassigned territories, dead rep queues, or a sync gap between marketing and CRM \u2014 not lazy reps. First cut by lead source; the leak usually concentrates in one or two sources. The cheapest pipeline this business can buy is the leads it already paid for.",
|
|
13896
|
+
deepdive: [
|
|
13897
|
+
"Status: green \u226580, yellow \u226560, red below 60.",
|
|
13898
|
+
'Dollar translation: dropped \xD7 conversion \xD7 avg deal (fallback: drop% \xD7 open pipeline) \u2192 "est. lost at handoff".',
|
|
13899
|
+
"Layer 2 of the gating stack (with Flow Rate).",
|
|
13900
|
+
"Trigger play: Fix the Handoff Gap (fix-handoff-gap) when drop is high.",
|
|
13901
|
+
"Levers: source-level handoff audit, routing + sync repair, time-to-first-touch SLA, weekly marketing-only-leads report."
|
|
13902
|
+
],
|
|
13903
|
+
visual: {
|
|
13904
|
+
kind: "funnel",
|
|
13905
|
+
caption: "Exemplar handoff funnel \u2014 the leak is usually one or two sources",
|
|
13906
|
+
funnel: [
|
|
13907
|
+
{ label: "Marketing leads", widthPct: 100 },
|
|
13908
|
+
{ label: "In sales CRM", widthPct: 62 },
|
|
13909
|
+
{ label: "Assigned + touched", widthPct: 41 },
|
|
13910
|
+
{ label: "Active opportunities", widthPct: 28 }
|
|
13911
|
+
]
|
|
13912
|
+
},
|
|
13913
|
+
play_id: "fix-handoff-gap",
|
|
13914
|
+
dollar_label: "est. lost at handoff",
|
|
13915
|
+
audience: {
|
|
13916
|
+
board: "Drop Rate prices the handoff leak \u2014 budget already spent on leads that never reach a working rep. Usually a systems failure, not a people failure.",
|
|
13917
|
+
ops: "Audit by source, fix routing/sync/dead queues, instrument time-to-first-touch. Play: Fix the Handoff Gap."
|
|
13918
|
+
},
|
|
13919
|
+
aliases: ["drop rate", "handoff", "handoff gap", "lead leak", "marketing sales handoff"]
|
|
13920
|
+
},
|
|
13921
|
+
{
|
|
13922
|
+
id: "signal_to_noise",
|
|
13923
|
+
kind: "vital",
|
|
13924
|
+
label: "Signal:Noise",
|
|
13925
|
+
group: "Vital Signs",
|
|
13926
|
+
tagline: "How much activity is aimed at deals that can still close?",
|
|
13927
|
+
how_computed: "Over a 90-day lookback: (signal activities / all activities) \xD7 100. Signal = activity linked to an open opportunity, a pipeline person, or a pipeline organization.",
|
|
13928
|
+
formula_lines: [
|
|
13929
|
+
"score = (signalCount / activityCount) \xD7 100",
|
|
13930
|
+
"signal = linked to open opp / pipeline person / pipeline org",
|
|
13931
|
+
"lookback = trailing 90 days"
|
|
13932
|
+
],
|
|
13933
|
+
meaning: 'Board question: "are we burning capacity on dead water?" Dollar value = noiseCount \xD7 hours_per_activity \xD7 rep_hourly_cost \u2014 misdirected effort.',
|
|
13934
|
+
expert_read: "Cut by rep and by account status \u2014 noise usually means reps fishing in the pond they can see (dead accounts they know) because targeting and account lists are stale. Persistent noise is a coverage-model problem, not a coaching problem. Check whether activity is logged against closed or unlinked records \u2014 often a hygiene artifact.",
|
|
13935
|
+
deepdive: [
|
|
13936
|
+
"Status: green \u226565, yellow \u226540, red below 40.",
|
|
13937
|
+
"Dollar defaults: 0.25 hours/activity \xD7 $75/hr (config: hours_per_activity, rep_hourly_cost).",
|
|
13938
|
+
"Layer 3 of the gating stack \u2014 trust Freshness / Flow / Drop before reading activity efficiency.",
|
|
13939
|
+
"Trigger play: Retarget Misdirected Effort (retarget-effort) when score is low.",
|
|
13940
|
+
"Levers: refresh account lists, signal-based targeting, stop logging against closed/unlinked records, coverage-model redesign."
|
|
13941
|
+
],
|
|
13942
|
+
visual: {
|
|
13943
|
+
kind: "split",
|
|
13944
|
+
caption: "Exemplar activity mix \u2014 signal vs noise",
|
|
13945
|
+
bars: [
|
|
13946
|
+
{ label: "Signal", value: 38, tone: "green" },
|
|
13947
|
+
{ label: "Noise", value: 62, tone: "red" }
|
|
13948
|
+
]
|
|
13949
|
+
},
|
|
13950
|
+
play_id: "retarget-effort",
|
|
13951
|
+
dollar_label: "misdirected effort",
|
|
13952
|
+
audience: {
|
|
13953
|
+
board: "Signal:Noise prices wasted capacity. Persistent noise is usually a coverage-model problem, not a coaching problem \u2014 reps fish in dead ponds they already know.",
|
|
13954
|
+
ops: "Score = % of activities linked to live pipeline. Cut by rep and account status; refresh targeting. Play: Retarget Misdirected Effort."
|
|
13955
|
+
},
|
|
13956
|
+
aliases: ["signal to noise", "signal:noise", "s/n", "activity efficiency", "noise"]
|
|
13957
|
+
},
|
|
13958
|
+
{
|
|
13959
|
+
id: "thread_depth",
|
|
13960
|
+
kind: "vital",
|
|
13961
|
+
label: "Thread Depth",
|
|
13962
|
+
group: "Vital Signs",
|
|
13963
|
+
tagline: "How fragile is the pipeline if one champion goes dark?",
|
|
13964
|
+
how_computed: "Percent of open deals with at least multi_thread_threshold (default 2) distinct people active in the last 90 days (opp-direct contacts + same-org activity).",
|
|
13965
|
+
formula_lines: [
|
|
13966
|
+
"score = % open deals with \u22652 active people (90d)",
|
|
13967
|
+
"people counted via opp contacts + same-org activity",
|
|
13968
|
+
"threshold configurable (default 2)"
|
|
13969
|
+
],
|
|
13970
|
+
meaning: 'Board question: "how much revenue dies if one contact changes jobs?" Dollar value = sum of amount on single-threaded deals.',
|
|
13971
|
+
expert_read: "Weight by deal size \u2014 one single-threaded mega-deal outweighs ten small ones. Single-threading late in the cycle is far more dangerous than early. In enterprise motions, thread depth is a leading indicator of slipped quarters: champions change jobs, and there's no second door in.",
|
|
13972
|
+
deepdive: [
|
|
13973
|
+
"Status: green \u226565, yellow \u226540, red below 40.",
|
|
13974
|
+
'Dollar translation: sum of amount on single-threaded deals \u2192 "single-threaded".',
|
|
13975
|
+
"Layer 4 of the gating stack \u2014 read last, after the upstream vitals.",
|
|
13976
|
+
"Trigger play: Multi-Thread Your Deals (multi-thread-deals) when depth is low.",
|
|
13977
|
+
"Levers: buying-committee map, warm internal referral first, CRM contact roles, mid-stage single-thread alerts, champion job-change signals."
|
|
13978
|
+
],
|
|
13979
|
+
visual: {
|
|
13980
|
+
kind: "bars",
|
|
13981
|
+
caption: "Exemplar \u2014 multi-threaded vs single-threaded open deals",
|
|
13982
|
+
bars: [
|
|
13983
|
+
{ label: "Multi-threaded", value: 34, tone: "green" },
|
|
13984
|
+
{ label: "Single-threaded", value: 66, tone: "red" }
|
|
13985
|
+
]
|
|
13986
|
+
},
|
|
13987
|
+
play_id: "multi-thread-deals",
|
|
13988
|
+
dollar_label: "single-threaded",
|
|
13989
|
+
audience: {
|
|
13990
|
+
board: "Thread Depth is resilience risk. One single-threaded mega-deal outweighs ten small ones \u2014 late-cycle single-threading is a leading indicator of slipped quarters.",
|
|
13991
|
+
ops: "Score = % of open deals with \u22652 active contacts in 90d. Map the buying committee; alert on mid-stage singles. Play: Multi-Thread Your Deals."
|
|
13992
|
+
},
|
|
13993
|
+
aliases: ["thread depth", "multithreading", "multi-thread", "single-threaded", "buying committee"]
|
|
13994
|
+
}
|
|
13995
|
+
];
|
|
13996
|
+
SAAS = [
|
|
13997
|
+
// —— Revenue ——
|
|
13998
|
+
{
|
|
13999
|
+
id: "arr",
|
|
14000
|
+
kind: "saas",
|
|
14001
|
+
label: "ARR",
|
|
14002
|
+
group: "Revenue",
|
|
14003
|
+
tagline: "How big is the revenue engine \u2014 and from where?",
|
|
14004
|
+
how_computed: "Sum of amount on closed-won opportunities in the dataset (pipeline-inferred ARR when a pure subscription ledger is unavailable).",
|
|
14005
|
+
formula_lines: [
|
|
14006
|
+
"ARR \u2248 \u03A3 amount on closed-won opportunities",
|
|
14007
|
+
"New + Expansion = growth \xB7 Churned + Contraction = leakage"
|
|
14008
|
+
],
|
|
14009
|
+
meaning: 'Board question: "how fast are we growing, and from where?" Always decompose growth into new vs expansion \u2014 the mix is the story.',
|
|
14010
|
+
expert_read: "Always decompose growth into new vs expansion \u2014 the mix is the story. Instrument trust: prefer this company's own trailing history over any external prior; a number below its reliability gate is a hypothesis, not a fact.",
|
|
14011
|
+
deepdive: [
|
|
14012
|
+
"Companion metrics: New ARR, Expansion ARR, Churned ARR, Contraction ARR.",
|
|
14013
|
+
"Estimation method may be ledger, pipeline_inferred, or snapshot \u2014 read confidence + reliability_gate.",
|
|
14014
|
+
"Cross-check with Freshness before trusting ARR growth stories built on zombie deals."
|
|
14015
|
+
],
|
|
14016
|
+
visual: {
|
|
14017
|
+
kind: "waterfall",
|
|
14018
|
+
caption: "Exemplar ARR walk \u2014 growth vs leakage",
|
|
14019
|
+
waterfall: [
|
|
14020
|
+
{ label: "Starting", delta: 100, cumulative: 100 },
|
|
14021
|
+
{ label: "+ New", delta: 18, cumulative: 118 },
|
|
14022
|
+
{ label: "+ Expansion", delta: 12, cumulative: 130 },
|
|
14023
|
+
{ label: "\u2212 Contraction", delta: -4, cumulative: 126 },
|
|
14024
|
+
{ label: "\u2212 Churned", delta: -8, cumulative: 118 }
|
|
14025
|
+
]
|
|
14026
|
+
},
|
|
14027
|
+
audience: {
|
|
14028
|
+
board: "ARR is the size of the engine. The story is the mix \u2014 new vs expansion growth, and how much leakage (churn + contraction) ate it.",
|
|
14029
|
+
ops: "Computed as \u03A3 closed-won amounts (pipeline-inferred when no ledger). Decompose into new / expansion / churned / contraction before briefing anyone."
|
|
14030
|
+
},
|
|
14031
|
+
aliases: ["annual recurring revenue", "revenue"]
|
|
14032
|
+
},
|
|
14033
|
+
{
|
|
14034
|
+
id: "new_arr",
|
|
14035
|
+
kind: "saas",
|
|
14036
|
+
label: "New ARR",
|
|
14037
|
+
group: "Revenue",
|
|
14038
|
+
tagline: "How much growth came from brand-new customers?",
|
|
14039
|
+
how_computed: "Closed-won tagged New Business, or first closed-won deal per organization when tags are missing.",
|
|
14040
|
+
formula_lines: [
|
|
14041
|
+
"New ARR = \u03A3 closed-won tagged New Business",
|
|
14042
|
+
"fallback: first closed-won deal per organization"
|
|
14043
|
+
],
|
|
14044
|
+
meaning: 'Board question: "is growth coming from the top of funnel, or are we farming the base?"',
|
|
14045
|
+
expert_read: "Rising New ARR with falling Expansion usually means land-and-expand is underpowered \u2014 packaging or CS motion, not just sales capacity.",
|
|
14046
|
+
deepdive: [
|
|
14047
|
+
"Pair with Expansion ARR \u2014 the mix tells you which motion is carrying growth.",
|
|
14048
|
+
"Tag quality matters: untagged deals fall into the first-deal-per-org heuristic."
|
|
14049
|
+
],
|
|
14050
|
+
visual: {
|
|
14051
|
+
kind: "bars",
|
|
14052
|
+
caption: "Exemplar growth mix",
|
|
14053
|
+
bars: [
|
|
14054
|
+
{ label: "New ARR", value: 60, tone: "accent" },
|
|
14055
|
+
{ label: "Expansion ARR", value: 40, tone: "green" }
|
|
14056
|
+
]
|
|
14057
|
+
},
|
|
14058
|
+
audience: {
|
|
14059
|
+
board: "New ARR is net-new logos. Read it next to Expansion \u2014 a healthy mix beats a one-sided engine.",
|
|
14060
|
+
ops: "Prefer CRM New Business tags; otherwise first closed-won per org. Watch tag hygiene."
|
|
14061
|
+
},
|
|
14062
|
+
aliases: ["new business arr", "new logo arr"]
|
|
14063
|
+
},
|
|
14064
|
+
{
|
|
14065
|
+
id: "expansion_arr",
|
|
14066
|
+
kind: "saas",
|
|
14067
|
+
label: "Expansion ARR",
|
|
14068
|
+
group: "Revenue",
|
|
14069
|
+
tagline: "How much are existing customers buying more?",
|
|
14070
|
+
how_computed: "Closed-won tagged Expansion, or later closed-won deals per organization after the first win.",
|
|
14071
|
+
formula_lines: [
|
|
14072
|
+
"Expansion ARR = \u03A3 closed-won tagged Expansion",
|
|
14073
|
+
"fallback: later closed-won deals per organization"
|
|
14074
|
+
],
|
|
14075
|
+
meaning: 'Board question: "is the installed base compounding?"',
|
|
14076
|
+
expert_read: "Expansion is the cheapest growth. Weak Expansion with strong New ARR is a land-only motion \u2014 packaging, CS capacity, or product attach is usually the lever.",
|
|
14077
|
+
deepdive: [
|
|
14078
|
+
"Feeds NRR as the upside term.",
|
|
14079
|
+
"Compare to Contraction \u2014 net expansion = expansion \u2212 contraction."
|
|
14080
|
+
],
|
|
14081
|
+
visual: {
|
|
14082
|
+
kind: "bars",
|
|
14083
|
+
caption: "Exemplar \u2014 expansion vs contraction",
|
|
14084
|
+
bars: [
|
|
14085
|
+
{ label: "Expansion", value: 70, tone: "green" },
|
|
14086
|
+
{ label: "Contraction", value: 25, tone: "yellow" }
|
|
14087
|
+
]
|
|
14088
|
+
},
|
|
14089
|
+
audience: {
|
|
14090
|
+
board: "Expansion ARR is installed-base compounding \u2014 the cheapest growth when it works.",
|
|
14091
|
+
ops: "Tagged Expansion or subsequent wins per org. Pair with Contraction before celebrating net expansion."
|
|
14092
|
+
},
|
|
14093
|
+
aliases: ["upsell", "upsell arr", "cross-sell"]
|
|
14094
|
+
},
|
|
14095
|
+
{
|
|
14096
|
+
id: "churned_arr",
|
|
14097
|
+
kind: "saas",
|
|
14098
|
+
label: "Churned ARR",
|
|
14099
|
+
group: "Revenue",
|
|
14100
|
+
tagline: "How much revenue walked out the door?",
|
|
14101
|
+
how_computed: "Organizations with historical wins, no win in the trailing 12 months, and no active open opportunity \u2014 sum of their historical closed-won amounts.",
|
|
14102
|
+
formula_lines: [
|
|
14103
|
+
"Churned ARR = \u03A3 historical wins for orgs with",
|
|
14104
|
+
" no win in trailing 12mo AND no active open opp"
|
|
14105
|
+
],
|
|
14106
|
+
meaning: `Board question: "how leaky is the bucket before expansion papers over it?" (with Contraction, this is GRR's downside).`,
|
|
14107
|
+
expert_read: "Pipeline-inferred churn is a hypothesis \u2014 confirm with billing status when available. A spike often clusters in one segment or cohort.",
|
|
14108
|
+
deepdive: [
|
|
14109
|
+
"Feeds GRR and NRR as the churn term.",
|
|
14110
|
+
"Cut by segment / motion before treating it as a company-wide PMF problem."
|
|
14111
|
+
],
|
|
14112
|
+
visual: {
|
|
14113
|
+
kind: "bars",
|
|
14114
|
+
caption: "Exemplar leakage mix",
|
|
14115
|
+
bars: [
|
|
14116
|
+
{ label: "Churned", value: 55, tone: "red" },
|
|
14117
|
+
{ label: "Contraction", value: 30, tone: "yellow" }
|
|
14118
|
+
]
|
|
14119
|
+
},
|
|
14120
|
+
audience: {
|
|
14121
|
+
board: "Churned ARR is full logo loss. With Contraction it sets the floor of the business (GRR).",
|
|
14122
|
+
ops: "Heuristic: historical winners with no trailing-12 win and no open opp. Validate against billing when you can."
|
|
14123
|
+
},
|
|
14124
|
+
aliases: ["churn", "logo churn", "churned revenue"]
|
|
14125
|
+
},
|
|
14126
|
+
{
|
|
14127
|
+
id: "contraction_arr",
|
|
14128
|
+
kind: "saas",
|
|
14129
|
+
label: "Contraction ARR",
|
|
14130
|
+
group: "Revenue",
|
|
14131
|
+
tagline: "How much did existing customers buy less?",
|
|
14132
|
+
how_computed: "Organizations with \u22652 wins where the latest amount is less than the prior \u2014 sum of the negative deltas.",
|
|
14133
|
+
formula_lines: [
|
|
14134
|
+
"Contraction = \u03A3 (prior \u2212 latest) where latest < prior",
|
|
14135
|
+
"requires \u22652 closed-won deals per organization"
|
|
14136
|
+
],
|
|
14137
|
+
meaning: 'Board question: "are we quietly shrinking inside the base while logos stay?"',
|
|
14138
|
+
expert_read: "Contraction is often packaging, seat-reduction, or downgrade \u2014 different owner than logo churn. Same NRR can be a churn problem or a no-expansion problem.",
|
|
14139
|
+
deepdive: [
|
|
14140
|
+
"Feeds GRR and NRR.",
|
|
14141
|
+
"Needs multi-deal history per org \u2014 thin history understates contraction."
|
|
14142
|
+
],
|
|
14143
|
+
visual: {
|
|
14144
|
+
kind: "waterfall",
|
|
14145
|
+
caption: "Exemplar \u2014 contraction digs into the base",
|
|
14146
|
+
waterfall: [
|
|
14147
|
+
{ label: "Prior", delta: 100, cumulative: 100 },
|
|
14148
|
+
{ label: "Latest", delta: -18, cumulative: 82 }
|
|
14149
|
+
]
|
|
14150
|
+
},
|
|
14151
|
+
audience: {
|
|
14152
|
+
board: "Contraction is silent shrink inside retained logos \u2014 often packaging or seats, not a cancelled contract.",
|
|
14153
|
+
ops: "Requires \u22652 wins per org with a down-round. Pair with Expansion for net expansion."
|
|
14154
|
+
},
|
|
14155
|
+
aliases: ["downgrade", "seat reduction", "contraction"]
|
|
14156
|
+
},
|
|
14157
|
+
// —— Retention ——
|
|
14158
|
+
{
|
|
14159
|
+
id: "nrr",
|
|
14160
|
+
kind: "saas",
|
|
14161
|
+
label: "Net Revenue Retention",
|
|
14162
|
+
group: "Retention",
|
|
14163
|
+
tagline: "Would this business grow if sales stopped selling?",
|
|
14164
|
+
how_computed: "startingArr = ARR + Churned + Contraction \u2212 Expansion; NRR = ((starting \u2212 Churned \u2212 Contraction + Expansion) / starting) \xD7 100.",
|
|
14165
|
+
formula_lines: [
|
|
14166
|
+
"starting = ARR + churned + contraction \u2212 expansion",
|
|
14167
|
+
"NRR = (starting \u2212 churned \u2212 contraction + expansion) / starting \xD7 100",
|
|
14168
|
+
"NRR = 100% + expansion% \u2212 contraction% \u2212 churn%"
|
|
14169
|
+
],
|
|
14170
|
+
meaning: 'Board question: "would this business grow if sales stopped selling?" >100% means growing from existing customers.',
|
|
14171
|
+
expert_read: "Decompose before judging: the same 95% can be a churn problem (product/PMF) or a no-expansion problem (packaging/motion) with different owners. Priors by segment: ~97% SMB, ~108% mid-market, ~118% enterprise medians; 110%+ is a strong signal at any stage.",
|
|
14172
|
+
deepdive: [
|
|
14173
|
+
"Always show the waterfall: +expansion \u2212contraction \u2212churn.",
|
|
14174
|
+
"GRR is the floor; NRR adds expansion on top.",
|
|
14175
|
+
"On pipeline-only data, treat as a hypothesis \u2014 check confidence / reliability_gate."
|
|
14176
|
+
],
|
|
14177
|
+
visual: {
|
|
14178
|
+
kind: "waterfall",
|
|
14179
|
+
caption: "Exemplar NRR walk from 100%",
|
|
14180
|
+
waterfall: [
|
|
14181
|
+
{ label: "100%", delta: 100, cumulative: 100 },
|
|
14182
|
+
{ label: "+ Expansion", delta: 14, cumulative: 114 },
|
|
14183
|
+
{ label: "\u2212 Contraction", delta: -4, cumulative: 110 },
|
|
14184
|
+
{ label: "\u2212 Churn", delta: -6, cumulative: 104 }
|
|
14185
|
+
]
|
|
14186
|
+
},
|
|
14187
|
+
audience: {
|
|
14188
|
+
board: "NRR >100% means the base compounds without new logos. Decompose before judging \u2014 same number, different owners.",
|
|
14189
|
+
ops: "NRR = 100 + expansion \u2212 contraction \u2212 churn. Motion benchmarks calibrate green/yellow bands. Check reliability_gate on pipeline-inferred data."
|
|
14190
|
+
},
|
|
14191
|
+
aliases: ["net revenue retention", "net retention", "ndr"],
|
|
14192
|
+
benchmarkHint: (motion) => pctBand("nrr", motion)
|
|
14193
|
+
},
|
|
14194
|
+
{
|
|
14195
|
+
id: "grr",
|
|
14196
|
+
kind: "saas",
|
|
14197
|
+
label: "Gross Revenue Retention",
|
|
14198
|
+
group: "Retention",
|
|
14199
|
+
tagline: "How leaky is the bucket before expansion papers over it?",
|
|
14200
|
+
how_computed: "GRR = ((startingArr \u2212 Churned \u2212 Contraction) / startingArr) \xD7 100 \u2014 expansion is excluded on purpose.",
|
|
14201
|
+
formula_lines: [
|
|
14202
|
+
"starting = ARR + churned + contraction \u2212 expansion",
|
|
14203
|
+
"GRR = (starting \u2212 churned \u2212 contraction) / starting \xD7 100"
|
|
14204
|
+
],
|
|
14205
|
+
meaning: 'Board question: "how leaky is the bucket before expansion papers over it?" Prior: >90% healthy, >95% strong for enterprise.',
|
|
14206
|
+
expert_read: "GRR is the honesty metric. Expansion can make NRR look fine while GRR is quietly eroding \u2014 always read both.",
|
|
14207
|
+
deepdive: [
|
|
14208
|
+
"GRR never includes Expansion \u2014 that is the point.",
|
|
14209
|
+
"Owners: product/CS for churn, packaging for contraction."
|
|
14210
|
+
],
|
|
14211
|
+
visual: {
|
|
14212
|
+
kind: "gauge",
|
|
14213
|
+
caption: "Exemplar GRR \u2014 floor of the business",
|
|
14214
|
+
gauge: 92
|
|
14215
|
+
},
|
|
14216
|
+
audience: {
|
|
14217
|
+
board: "GRR is the floor \u2014 churn + contraction only. Expansion cannot paper over a leaky bucket here.",
|
|
14218
|
+
ops: "Exclude Expansion by design. Pair with NRR; diagnose churn vs contraction separately."
|
|
14219
|
+
},
|
|
14220
|
+
aliases: ["gross revenue retention", "gross retention"],
|
|
14221
|
+
benchmarkHint: (motion) => pctBand("grr", motion)
|
|
14222
|
+
},
|
|
14223
|
+
// —— Pipeline ——
|
|
14224
|
+
{
|
|
14225
|
+
id: "pipeline_coverage",
|
|
14226
|
+
kind: "saas",
|
|
14227
|
+
label: "Pipeline Coverage",
|
|
14228
|
+
group: "Pipeline",
|
|
14229
|
+
tagline: "Is next quarter already at risk?",
|
|
14230
|
+
how_computed: "Open pipeline amount \xF7 trailing-90-day closed-won amount.",
|
|
14231
|
+
formula_lines: [
|
|
14232
|
+
"Coverage = openPipeline / trailing_90d_won",
|
|
14233
|
+
"required \u2248 1 / win_rate (discount for time left)"
|
|
14234
|
+
],
|
|
14235
|
+
meaning: 'Board question: "is next quarter already at risk?" Priors scale with cycle length: ~3x velocity/SMB, 4\u20135x enterprise.',
|
|
14236
|
+
expert_read: "Coverage means nothing without win rate: required coverage \u2248 1 / win rate, discounted for time left in period. Inflated stages and zombie deals fake coverage \u2014 cross-check with Freshness before trusting it.",
|
|
14237
|
+
deepdive: [
|
|
14238
|
+
"Always pair with Win Rate and Freshness.",
|
|
14239
|
+
"Weighted Pipeline is the credibility-adjusted cousin."
|
|
14240
|
+
],
|
|
14241
|
+
visual: {
|
|
14242
|
+
kind: "gauge",
|
|
14243
|
+
caption: "Exemplar coverage vs a 3x target",
|
|
14244
|
+
gauge: 72,
|
|
14245
|
+
bars: [
|
|
14246
|
+
{ label: "Open pipeline", value: 75, tone: "accent" },
|
|
14247
|
+
{ label: "Trailing won (scaled)", value: 25, tone: "neutral" }
|
|
14248
|
+
]
|
|
14249
|
+
},
|
|
14250
|
+
audience: {
|
|
14251
|
+
board: "Coverage answers whether next quarter is already under-piped. Fake coverage from zombies is worse than an honest gap.",
|
|
14252
|
+
ops: "open / trailing-90d won. Required \u2248 1/win_rate. Cross-check Freshness before briefing."
|
|
14253
|
+
},
|
|
14254
|
+
aliases: ["coverage", "pipeline coverage", "pipe coverage"],
|
|
14255
|
+
benchmarkHint: (motion) => pctBand("pipeline_coverage", motion)
|
|
14256
|
+
},
|
|
14257
|
+
{
|
|
14258
|
+
id: "weighted_pipeline",
|
|
14259
|
+
kind: "saas",
|
|
14260
|
+
label: "Weighted Pipeline",
|
|
14261
|
+
group: "Pipeline",
|
|
14262
|
+
tagline: "What is the pipeline worth after stage probability?",
|
|
14263
|
+
how_computed: "Sum of amount \xD7 stage probability for open deals (CRM Probability when present, else stage defaults).",
|
|
14264
|
+
formula_lines: [
|
|
14265
|
+
"Weighted = \u03A3 (amount \xD7 stageProbability)",
|
|
14266
|
+
"trust \u2264 stage discipline deserves"
|
|
14267
|
+
],
|
|
14268
|
+
meaning: 'Board question: "what should we actually forecast from open pipe?"',
|
|
14269
|
+
expert_read: "Trust it only as much as stage discipline deserves. Inflated late stages make weighted pipeline a fiction.",
|
|
14270
|
+
deepdive: [
|
|
14271
|
+
"Compare to unweighted open pipeline \u2014 a huge gap means optimistic stages.",
|
|
14272
|
+
"Pair with Flow Rate (stuck late stages)."
|
|
14273
|
+
],
|
|
14274
|
+
visual: {
|
|
14275
|
+
kind: "bars",
|
|
14276
|
+
caption: "Exemplar \u2014 open vs weighted",
|
|
14277
|
+
bars: [
|
|
14278
|
+
{ label: "Open pipeline", value: 100, tone: "neutral" },
|
|
14279
|
+
{ label: "Weighted", value: 42, tone: "accent" }
|
|
14280
|
+
]
|
|
14281
|
+
},
|
|
14282
|
+
audience: {
|
|
14283
|
+
board: "Weighted Pipeline is the credibility-adjusted forecast input \u2014 only as good as stage discipline.",
|
|
14284
|
+
ops: "\u03A3 amount \xD7 probability. Audit stage probabilities when weighted << open."
|
|
14285
|
+
},
|
|
14286
|
+
aliases: ["weighted pipe", "probability-weighted pipeline"]
|
|
14287
|
+
},
|
|
14288
|
+
{
|
|
14289
|
+
id: "pipeline_created",
|
|
14290
|
+
kind: "saas",
|
|
14291
|
+
label: "Pipeline Created (90d)",
|
|
14292
|
+
group: "Pipeline",
|
|
14293
|
+
tagline: "How much new pipe did we generate recently?",
|
|
14294
|
+
how_computed: "Sum of amounts for opportunities created in the last 90 days.",
|
|
14295
|
+
formula_lines: ["Pipeline Created = \u03A3 amount where created_at within 90d"],
|
|
14296
|
+
meaning: 'Board question: "is the top of funnel still filling?"',
|
|
14297
|
+
expert_read: "Falling created pipeline with flat coverage is a future miss \u2014 coverage is lagging; created is leading.",
|
|
14298
|
+
deepdive: [
|
|
14299
|
+
"Leading indicator for next-quarter coverage.",
|
|
14300
|
+
"Cut by source / segment to find where creation stalled."
|
|
14301
|
+
],
|
|
14302
|
+
visual: {
|
|
14303
|
+
kind: "bars",
|
|
14304
|
+
caption: "Exemplar \u2014 created vs needed",
|
|
14305
|
+
bars: [
|
|
14306
|
+
{ label: "Created (90d)", value: 55, tone: "yellow" },
|
|
14307
|
+
{ label: "Target pace", value: 80, tone: "green" }
|
|
14308
|
+
]
|
|
14309
|
+
},
|
|
14310
|
+
audience: {
|
|
14311
|
+
board: "Pipeline Created is a leading indicator \u2014 coverage lagging means the miss is already in motion.",
|
|
14312
|
+
ops: "\u03A3 amounts on opps created in 90d. Cut by source when it dips."
|
|
14313
|
+
},
|
|
14314
|
+
aliases: ["pipe gen", "pipeline generation", "created pipeline"]
|
|
14315
|
+
},
|
|
14316
|
+
{
|
|
14317
|
+
id: "pipeline_velocity",
|
|
14318
|
+
kind: "saas",
|
|
14319
|
+
label: "Pipeline Velocity",
|
|
14320
|
+
group: "Pipeline",
|
|
14321
|
+
tagline: "Revenue throughput per day \u2014 four levers, one number.",
|
|
14322
|
+
how_computed: "(openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays \u2014 requires \u22653 dated closed-won deals. Unit: $/day.",
|
|
14323
|
+
formula_lines: [
|
|
14324
|
+
"Velocity = (openOpps \xD7 avgDeal \xD7 winRate) / avgCycleDays",
|
|
14325
|
+
"four levers: #opps \xB7 deal size \xB7 win rate \xB7 cycle days"
|
|
14326
|
+
],
|
|
14327
|
+
meaning: 'Board question: "which lever moved when throughput changed?" The most decision-ready pipeline metric.',
|
|
14328
|
+
expert_read: "When velocity changes, name WHICH lever moved. A win-rate rise on falling opp volume is qualification tightening, not improvement.",
|
|
14329
|
+
deepdive: [
|
|
14330
|
+
"Needs \u22653 dated wins \u2014 otherwise unavailable.",
|
|
14331
|
+
"Pairs with Flow Rate (cycle) and Win Rate (conversion)."
|
|
14332
|
+
],
|
|
14333
|
+
visual: {
|
|
14334
|
+
kind: "levers",
|
|
14335
|
+
caption: "Four levers \u2014 say which one moved",
|
|
14336
|
+
levers: ["# Open opps", "Avg deal size", "Win rate", "Cycle days"]
|
|
14337
|
+
},
|
|
14338
|
+
audience: {
|
|
14339
|
+
board: "Velocity is throughput. When it moves, demand the lever \u2014 volume, size, win rate, or cycle \u2014 not a shrug.",
|
|
14340
|
+
ops: "(opps \xD7 avgDeal \xD7 winRate) / cycleDays. Diagnose the moved lever before prescribing."
|
|
14341
|
+
},
|
|
14342
|
+
aliases: ["velocity", "pipeline velocity", "throughput"]
|
|
14343
|
+
},
|
|
14344
|
+
// —— Sales efficiency ——
|
|
14345
|
+
{
|
|
14346
|
+
id: "win_rate",
|
|
14347
|
+
kind: "saas",
|
|
14348
|
+
label: "Win Rate",
|
|
14349
|
+
group: "Sales Efficiency",
|
|
14350
|
+
tagline: "Of decided deals, how often do we win?",
|
|
14351
|
+
how_computed: "closed-won / (won + lost) \xD7 100.",
|
|
14352
|
+
formula_lines: ["Win Rate = won / (won + lost) \xD7 100"],
|
|
14353
|
+
meaning: 'Board question: "are we converting the pipe we create?" Priors: 25\u201335% SMB, 18\u201325% mid-market, 12\u201318% enterprise on qualified opps.',
|
|
14354
|
+
expert_read: "A rising win rate on falling opp volume is qualification tightening, not improvement \u2014 check the denominator.",
|
|
14355
|
+
deepdive: [
|
|
14356
|
+
"Required coverage \u2248 1 / win rate.",
|
|
14357
|
+
"Cut by segment / source before company-wide coaching."
|
|
14358
|
+
],
|
|
14359
|
+
visual: {
|
|
14360
|
+
kind: "split",
|
|
14361
|
+
caption: "Exemplar decided deals",
|
|
14362
|
+
bars: [
|
|
14363
|
+
{ label: "Won", value: 28, tone: "green" },
|
|
14364
|
+
{ label: "Lost", value: 72, tone: "red" }
|
|
14365
|
+
]
|
|
14366
|
+
},
|
|
14367
|
+
audience: {
|
|
14368
|
+
board: "Win Rate is conversion of decided deals. Rising win rate with falling volume is often tighter qualification, not better selling.",
|
|
14369
|
+
ops: "won/(won+lost). Check the denominator. Motion benchmarks set green/yellow bands."
|
|
14370
|
+
},
|
|
14371
|
+
aliases: ["close rate", "winrate", "win %"],
|
|
14372
|
+
benchmarkHint: (motion) => pctBand("win_rate", motion)
|
|
14373
|
+
},
|
|
14374
|
+
{
|
|
14375
|
+
id: "avg_deal_size",
|
|
14376
|
+
kind: "saas",
|
|
14377
|
+
label: "Avg Deal Size",
|
|
14378
|
+
group: "Sales Efficiency",
|
|
14379
|
+
tagline: "What does a typical win look like?",
|
|
14380
|
+
how_computed: "Mean amount on closed-won opportunities.",
|
|
14381
|
+
formula_lines: ["Avg Deal = mean(closed-won amount)"],
|
|
14382
|
+
meaning: 'Board question: "are we selling the motion we think we are?"',
|
|
14383
|
+
expert_read: "Deal size drifting down while volume rises often means mix shift into a lower segment \u2014 not always a problem, but it changes coverage math.",
|
|
14384
|
+
deepdive: [
|
|
14385
|
+
"Feeds Pipeline Velocity and LTV proxy.",
|
|
14386
|
+
"Cut by segment \u2014 averages hide bimodal motions."
|
|
14387
|
+
],
|
|
14388
|
+
visual: {
|
|
14389
|
+
kind: "bars",
|
|
14390
|
+
caption: "Exemplar \u2014 size mix by segment",
|
|
14391
|
+
bars: [
|
|
14392
|
+
{ label: "SMB", value: 30, tone: "neutral" },
|
|
14393
|
+
{ label: "Mid-market", value: 55, tone: "accent" },
|
|
14394
|
+
{ label: "Enterprise", value: 90, tone: "green" }
|
|
14395
|
+
]
|
|
14396
|
+
},
|
|
14397
|
+
audience: {
|
|
14398
|
+
board: "Avg Deal Size should match the motion you claim. Mix shift changes coverage and capacity math.",
|
|
14399
|
+
ops: "Mean closed-won amount. Segment before coaching on size."
|
|
14400
|
+
},
|
|
14401
|
+
aliases: ["average deal size", "asp", "acv"]
|
|
14402
|
+
},
|
|
14403
|
+
{
|
|
14404
|
+
id: "avg_sales_cycle",
|
|
14405
|
+
kind: "saas",
|
|
14406
|
+
label: "Avg Sales Cycle",
|
|
14407
|
+
group: "Sales Efficiency",
|
|
14408
|
+
tagline: "How long from create to close on wins?",
|
|
14409
|
+
how_computed: "Mean days from created_at to close date on dated closed-won deals.",
|
|
14410
|
+
formula_lines: ["Avg Cycle = mean(close_date \u2212 created_at) on dated wins"],
|
|
14411
|
+
meaning: 'Board question: "is the cycle stretching \u2014 the earliest soft signal of deal-quality decay?"',
|
|
14412
|
+
expert_read: "Cycle stretching past the motion's norm is the earliest soft signal of deal-quality decay. Pair with Flow Rate stuck stages.",
|
|
14413
|
+
deepdive: [
|
|
14414
|
+
"Feeds Pipeline Velocity as the denominator.",
|
|
14415
|
+
"Needs dated wins \u2014 missing close dates understate/omit."
|
|
14416
|
+
],
|
|
14417
|
+
visual: {
|
|
14418
|
+
kind: "bars",
|
|
14419
|
+
caption: "Exemplar cycle vs motion norm",
|
|
14420
|
+
bars: [
|
|
14421
|
+
{ label: "Your cycle", value: 78, tone: "yellow" },
|
|
14422
|
+
{ label: "Motion norm", value: 55, tone: "green" }
|
|
14423
|
+
]
|
|
14424
|
+
},
|
|
14425
|
+
audience: {
|
|
14426
|
+
board: "Cycle stretch is an early soft signal that quality or process is slipping \u2014 before the miss shows in bookings.",
|
|
14427
|
+
ops: "Mean create\u2192close on dated wins. Investigate the stage that aged."
|
|
14428
|
+
},
|
|
14429
|
+
aliases: ["sales cycle", "cycle length", "time to close"]
|
|
14430
|
+
},
|
|
14431
|
+
{
|
|
14432
|
+
id: "stage_conversion",
|
|
14433
|
+
kind: "saas",
|
|
14434
|
+
label: "Stage Conversion",
|
|
14435
|
+
group: "Sales Efficiency",
|
|
14436
|
+
tagline: "Where in the stage model does advancement collapse?",
|
|
14437
|
+
how_computed: "From metadata.stage_history stage advances when present; otherwise a win-rate proxy.",
|
|
14438
|
+
formula_lines: [
|
|
14439
|
+
"Preferred: advancement rates from stage_history",
|
|
14440
|
+
"Fallback: win-rate proxy when history is missing"
|
|
14441
|
+
],
|
|
14442
|
+
meaning: 'Board question: "which single stage is starving everything downstream?"',
|
|
14443
|
+
expert_read: "Find the one stage where conversion collapses \u2014 that's the process problem; everything downstream is starvation.",
|
|
14444
|
+
deepdive: [
|
|
14445
|
+
"Best with stage_history metadata; otherwise treat as proxy.",
|
|
14446
|
+
"Pairs with Flow Rate stage-age cuts."
|
|
14447
|
+
],
|
|
14448
|
+
visual: {
|
|
14449
|
+
kind: "funnel",
|
|
14450
|
+
caption: "Exemplar \u2014 find the collapse",
|
|
14451
|
+
funnel: [
|
|
14452
|
+
{ label: "Stage 1\u21922", widthPct: 100 },
|
|
14453
|
+
{ label: "Stage 2\u21923", widthPct: 72 },
|
|
14454
|
+
{ label: "Stage 3\u21924", widthPct: 28 },
|
|
14455
|
+
{ label: "Stage 4\u2192Close", widthPct: 18 }
|
|
14456
|
+
]
|
|
14457
|
+
},
|
|
14458
|
+
audience: {
|
|
14459
|
+
board: "Stage Conversion names the bottleneck stage \u2014 one collapse starves every stage after it.",
|
|
14460
|
+
ops: "Prefer stage_history advances. Fix the collapse stage before coaching downstream reps."
|
|
14461
|
+
},
|
|
14462
|
+
aliases: ["stage conversion", "stage advance", "conversion by stage"]
|
|
14463
|
+
},
|
|
14464
|
+
// —— Unit economics ——
|
|
14465
|
+
{
|
|
14466
|
+
id: "ltv_proxy",
|
|
14467
|
+
kind: "saas",
|
|
14468
|
+
label: "LTV (Proxy)",
|
|
14469
|
+
group: "Unit Economics",
|
|
14470
|
+
tagline: "Rough lifetime value from deal size and GRR.",
|
|
14471
|
+
how_computed: "avgDeal / ((100 \u2212 GRR) / 100) when GRR < 100. Unavailable when GRR is 100%+ or missing.",
|
|
14472
|
+
formula_lines: [
|
|
14473
|
+
"LTV \u2248 avgDeal / churnRate",
|
|
14474
|
+
"churnRate = (100 \u2212 GRR) / 100 (requires GRR < 100)"
|
|
14475
|
+
],
|
|
14476
|
+
meaning: 'Board question: "what is a customer roughly worth over their life?"',
|
|
14477
|
+
expert_read: "This is a proxy \u2014 not a cohort LTV. Use it for direction, not capital allocation.",
|
|
14478
|
+
deepdive: [
|
|
14479
|
+
"Unavailable when GRR \u2265 100 or missing.",
|
|
14480
|
+
"Pairs with CAC for LTV:CAC when spend data exists."
|
|
14481
|
+
],
|
|
14482
|
+
visual: {
|
|
14483
|
+
kind: "gauge",
|
|
14484
|
+
caption: "Exemplar LTV proxy (directional)",
|
|
14485
|
+
gauge: 68
|
|
14486
|
+
},
|
|
14487
|
+
audience: {
|
|
14488
|
+
board: "LTV Proxy is directional from deal size and GRR \u2014 not a cohort LTV. Use for orientation, not capital decisions.",
|
|
14489
|
+
ops: "avgDeal / ((100\u2212GRR)/100). Needs GRR < 100. Prefer cohort math when billing data arrives."
|
|
14490
|
+
},
|
|
14491
|
+
aliases: ["ltv", "lifetime value"]
|
|
14492
|
+
},
|
|
14493
|
+
{
|
|
14494
|
+
id: "cac",
|
|
14495
|
+
kind: "saas",
|
|
14496
|
+
label: "CAC",
|
|
14497
|
+
group: "Unit Economics",
|
|
14498
|
+
tagline: "Customer acquisition cost \u2014 needs spend data.",
|
|
14499
|
+
how_computed: "Requires campaign / sales spend data. Currently unavailable on CRM-only datasets.",
|
|
14500
|
+
formula_lines: ["CAC = sales & marketing spend / new customers", "(requires spend data \u2014 not in CRM-only exports)"],
|
|
14501
|
+
meaning: 'Board question: "what does a new logo cost to win?"',
|
|
14502
|
+
expert_read: "Without spend, NTRP cannot invent CAC. Wire campaign spend or finance exports to unlock unit economics.",
|
|
14503
|
+
deepdive: [
|
|
14504
|
+
"Always unavailable on CRM-only demos \u2014 expected.",
|
|
14505
|
+
"Unlocks LTV:CAC, Payback, Magic Number when spend lands."
|
|
14506
|
+
],
|
|
14507
|
+
visual: { kind: "none", caption: "Needs campaign spend / finance export" },
|
|
14508
|
+
audience: {
|
|
14509
|
+
board: "CAC is locked until spend data is connected \u2014 CRM alone cannot price acquisition.",
|
|
14510
|
+
ops: "Bring campaign or S&M spend. Until then unit-econ metrics stay unavailable by design."
|
|
14511
|
+
},
|
|
14512
|
+
aliases: ["customer acquisition cost", "acquisition cost"]
|
|
14513
|
+
},
|
|
14514
|
+
{
|
|
14515
|
+
id: "ltv_cac_ratio",
|
|
14516
|
+
kind: "saas",
|
|
14517
|
+
label: "LTV:CAC Ratio",
|
|
14518
|
+
group: "Unit Economics",
|
|
14519
|
+
tagline: "Is acquisition spend earning its keep?",
|
|
14520
|
+
how_computed: "LTV proxy \xF7 CAC. Unavailable without spend (CAC).",
|
|
14521
|
+
formula_lines: ["LTV:CAC = LTV_proxy / CAC", "(requires CAC)"],
|
|
14522
|
+
meaning: 'Board question: "do we earn enough lifetime value per dollar spent to acquire?"',
|
|
14523
|
+
expert_read: "Efficiency era: boards weigh LTV:CAC and payback as heavily as growth. Classic rule of thumb \u22653x, but motion and gross margin matter.",
|
|
14524
|
+
deepdive: ["Blocked on CAC. See LTV Proxy and CAC."],
|
|
14525
|
+
visual: { kind: "none", caption: "Needs CAC (spend data)" },
|
|
14526
|
+
audience: {
|
|
14527
|
+
board: "LTV:CAC is the acquisition ROI story \u2014 available once spend is wired.",
|
|
14528
|
+
ops: "LTV_proxy / CAC. Unlocks with spend import."
|
|
14529
|
+
},
|
|
14530
|
+
aliases: ["ltv cac", "ltv/cac", "ltv to cac"]
|
|
14531
|
+
},
|
|
14532
|
+
{
|
|
14533
|
+
id: "payback_months",
|
|
14534
|
+
kind: "saas",
|
|
14535
|
+
label: "Payback Months",
|
|
14536
|
+
group: "Unit Economics",
|
|
14537
|
+
tagline: "How many months to recover CAC?",
|
|
14538
|
+
how_computed: "Requires CAC / spend. Lower is better.",
|
|
14539
|
+
formula_lines: ["Payback \u2248 CAC / (monthly gross profit per customer)", "(requires spend data)"],
|
|
14540
|
+
meaning: 'Board question: "how fast does acquisition spend return?" Efficiency era prior: <18 months often healthy.',
|
|
14541
|
+
expert_read: "Boards now weigh payback (<18mo) as heavily as growth in many motions.",
|
|
14542
|
+
deepdive: ["Blocked on CAC. Benchmarks exist per motion once data lands."],
|
|
14543
|
+
visual: { kind: "none", caption: "Needs CAC (spend data)" },
|
|
14544
|
+
audience: {
|
|
14545
|
+
board: "Payback is how fast CAC returns. Efficiency-era boards often want <18 months.",
|
|
14546
|
+
ops: "Requires CAC. Motion green/yellow bands apply when available."
|
|
14547
|
+
},
|
|
14548
|
+
aliases: ["payback", "cac payback"],
|
|
14549
|
+
benchmarkHint: (motion) => monthsBand(motion)
|
|
14550
|
+
},
|
|
14551
|
+
{
|
|
14552
|
+
id: "magic_number",
|
|
14553
|
+
kind: "saas",
|
|
14554
|
+
label: "Magic Number",
|
|
14555
|
+
group: "Unit Economics",
|
|
14556
|
+
tagline: "Sales efficiency \u2014 net new ARR per sales dollar.",
|
|
14557
|
+
how_computed: "Requires sales spend. Classic form: net new ARR (quarter) / prior-quarter S&M spend.",
|
|
14558
|
+
formula_lines: [
|
|
14559
|
+
"Magic Number \u2248 Net New ARR(q) / S&M spend(q\u22121)",
|
|
14560
|
+
"(requires spend data)"
|
|
14561
|
+
],
|
|
14562
|
+
meaning: 'Board question: "how efficiently does sales spend produce net new ARR?" Prior: >0.75 often healthy; >1 strong.',
|
|
14563
|
+
expert_read: "Efficiency era: magic number >0.75 is weighed alongside growth. Without spend, NTRP keeps this unavailable rather than inventing it.",
|
|
14564
|
+
deepdive: ["Blocked on spend. Benchmarks per motion ready when data lands."],
|
|
14565
|
+
visual: { kind: "none", caption: "Needs S&M spend data" },
|
|
14566
|
+
audience: {
|
|
14567
|
+
board: "Magic Number prices sales efficiency. Available once S&M spend is connected.",
|
|
14568
|
+
ops: "Net new ARR / prior S&M. Motion benchmarks apply when spend lands."
|
|
14569
|
+
},
|
|
14570
|
+
aliases: ["sales magic number", "sales efficiency magic number"],
|
|
14571
|
+
benchmarkHint: (motion) => magicBand(motion)
|
|
14572
|
+
}
|
|
14573
|
+
];
|
|
14574
|
+
METRIC_DEFINITIONS = [...VITALS, ...SAAS];
|
|
14575
|
+
BY_ID = new Map(METRIC_DEFINITIONS.map((m) => [m.id, m]));
|
|
14576
|
+
ALIAS_INDEX = (() => {
|
|
14577
|
+
const idx = /* @__PURE__ */ new Map();
|
|
14578
|
+
for (const m of METRIC_DEFINITIONS) {
|
|
14579
|
+
idx.set(m.id.toLowerCase(), m.id);
|
|
14580
|
+
idx.set(m.label.toLowerCase(), m.id);
|
|
14581
|
+
for (const a of m.aliases ?? []) {
|
|
14582
|
+
idx.set(a.toLowerCase(), m.id);
|
|
14583
|
+
}
|
|
14584
|
+
}
|
|
14585
|
+
idx.set("signal-to-noise", "signal_to_noise");
|
|
14586
|
+
idx.set("signal:noise", "signal_to_noise");
|
|
14587
|
+
idx.set("flow-rate", "flow_rate");
|
|
14588
|
+
idx.set("drop-rate", "drop_rate");
|
|
14589
|
+
idx.set("thread-depth", "thread_depth");
|
|
14590
|
+
return idx;
|
|
14591
|
+
})();
|
|
14592
|
+
SAAS_METRIC_IDS = SAAS.map((m) => m.id);
|
|
14593
|
+
}
|
|
14594
|
+
});
|
|
14595
|
+
|
|
14596
|
+
// src/services/metric-explainers.ts
|
|
14597
|
+
function normalizeAudience(audience) {
|
|
14598
|
+
if (!audience) return "board";
|
|
14599
|
+
const a = String(audience).toLowerCase();
|
|
14600
|
+
if (a === "ops" || a === "operations" || a === "operator" || a === "team") {
|
|
14601
|
+
return "ops";
|
|
14602
|
+
}
|
|
14603
|
+
return "board";
|
|
13429
14604
|
}
|
|
13430
|
-
function
|
|
13431
|
-
return
|
|
14605
|
+
function audienceLabel(audience) {
|
|
14606
|
+
return audience === "ops" ? "ops" : "board / exec";
|
|
13432
14607
|
}
|
|
13433
|
-
function
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
return `- ${label}: ${formatted}${confStr}`;
|
|
14608
|
+
function statusRankFrom(status) {
|
|
14609
|
+
if (status === "red") return 0;
|
|
14610
|
+
if (status === "yellow") return 1;
|
|
14611
|
+
if (status === "green") return 2;
|
|
14612
|
+
return 3;
|
|
13439
14613
|
}
|
|
13440
|
-
function
|
|
13441
|
-
const
|
|
13442
|
-
const
|
|
13443
|
-
const
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
|
|
13447
|
-
if (
|
|
13448
|
-
|
|
13449
|
-
|
|
13450
|
-
const counts = ctx.dataset.counts ?? {};
|
|
13451
|
-
const countStr = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k}`).join(", ");
|
|
13452
|
-
lines.push(`Dataset: ${ctx.dataset.label}${countStr ? ` (${countStr})` : ""}`);
|
|
13453
|
-
}
|
|
13454
|
-
const completed = ctx.analysis.completed;
|
|
13455
|
-
if (completed.length > 0) {
|
|
13456
|
-
lines.push(`Analysis lenses completed: ${completed.join(", ")}`);
|
|
13457
|
-
}
|
|
13458
|
-
lines.push("");
|
|
13459
|
-
if (diagnosis) {
|
|
13460
|
-
const { health, findings } = diagnosis;
|
|
13461
|
-
lines.push("## GTM health (vital signs)");
|
|
13462
|
-
lines.push(`Overall score: ${Math.round(health.overall_score)} (${health.overall_status})`);
|
|
13463
|
-
if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
|
|
13464
|
-
lines.push(`Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
|
|
13465
|
-
}
|
|
13466
|
-
lines.push("");
|
|
13467
|
-
lines.push("### Vital signs");
|
|
13468
|
-
for (const vs of health.vital_signs) {
|
|
13469
|
-
const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
|
|
13470
|
-
const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
|
|
13471
|
-
lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
|
|
14614
|
+
function collectCandidates(bundle, opts) {
|
|
14615
|
+
const prefer = new Set(opts.prefer ?? []);
|
|
14616
|
+
const map = /* @__PURE__ */ new Map();
|
|
14617
|
+
const upsert = (id, priority, status) => {
|
|
14618
|
+
if (!getMetricExplainer(id)) return;
|
|
14619
|
+
const existing = map.get(id);
|
|
14620
|
+
const rank = statusRankFrom(status);
|
|
14621
|
+
if (!existing) {
|
|
14622
|
+
map.set(id, { id, priority, statusRank: rank });
|
|
14623
|
+
return;
|
|
13472
14624
|
}
|
|
13473
|
-
|
|
13474
|
-
|
|
13475
|
-
|
|
13476
|
-
|
|
14625
|
+
existing.priority = Math.min(existing.priority, priority);
|
|
14626
|
+
existing.statusRank = Math.min(existing.statusRank, rank);
|
|
14627
|
+
};
|
|
14628
|
+
const health = bundle?.diagnosis?.health;
|
|
14629
|
+
const fromDiag = opts.vitals ?? health?.vital_signs ?? [];
|
|
14630
|
+
const gating = opts.prefer?.[0] ?? health?.gating_vital_sign;
|
|
14631
|
+
if (gating) upsert(String(gating), 0, "red");
|
|
14632
|
+
for (const vs of fromDiag) {
|
|
14633
|
+
const id = vs.vital_sign;
|
|
14634
|
+
upsert(id, prefer.has(id) ? 0 : 1, vs.status);
|
|
14635
|
+
}
|
|
14636
|
+
const rawMetrics = opts.metrics ?? bundle?.metrics?.metrics ?? [];
|
|
14637
|
+
let sawMetrics = rawMetrics.length > 0;
|
|
14638
|
+
for (const row of rawMetrics) {
|
|
14639
|
+
const id = String(row.metric ?? row.metric ?? "");
|
|
14640
|
+
if (!id) continue;
|
|
14641
|
+
const status = String(row.status ?? row.status ?? "");
|
|
14642
|
+
const value = row.value ?? row.value;
|
|
14643
|
+
const unavailable = row.unavailable_reason ?? row.unavailable_reason;
|
|
14644
|
+
if (value == null && unavailable) continue;
|
|
14645
|
+
upsert(id, prefer.has(id) ? 0 : 2, status);
|
|
14646
|
+
}
|
|
14647
|
+
if (sawMetrics || bundle?.metrics) {
|
|
14648
|
+
for (const id of ["arr", "nrr", "pipeline_coverage", "win_rate"]) {
|
|
14649
|
+
if (!map.has(id) && getMetricExplainer(id)) {
|
|
14650
|
+
upsert(id, 3, "neutral");
|
|
14651
|
+
}
|
|
13477
14652
|
}
|
|
13478
|
-
lines.push("");
|
|
13479
14653
|
}
|
|
13480
|
-
if (
|
|
13481
|
-
|
|
13482
|
-
|
|
13483
|
-
for (const key of KEY_METRICS) {
|
|
13484
|
-
const row = byKey.get(key);
|
|
13485
|
-
if (row) lines.push(formatMetricLine(row));
|
|
13486
|
-
}
|
|
13487
|
-
if (metrics.findings.length > 0) {
|
|
13488
|
-
lines.push("");
|
|
13489
|
-
lines.push("### Metrics findings");
|
|
13490
|
-
appendFindings(lines, metrics.findings);
|
|
14654
|
+
if (map.size === 0) {
|
|
14655
|
+
for (const id of ["freshness", "flow_rate", "drop_rate", "signal_to_noise", "thread_depth"]) {
|
|
14656
|
+
upsert(id, 4, "neutral");
|
|
13491
14657
|
}
|
|
13492
|
-
lines.push("");
|
|
13493
|
-
}
|
|
13494
|
-
if (!diagnosis && metrics) {
|
|
13495
|
-
lines.unshift("Primary analysis: SaaS metrics (no GTM health snapshot on this session yet).", "");
|
|
13496
|
-
} else if (diagnosis && !metrics) {
|
|
13497
|
-
lines.push("(SaaS metrics not run on this session \u2014 run /metrics for the revenue view)");
|
|
13498
14658
|
}
|
|
13499
|
-
return
|
|
14659
|
+
return [...map.values()].sort((a, b) => {
|
|
14660
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
14661
|
+
if (a.statusRank !== b.statusRank) return a.statusRank - b.statusRank;
|
|
14662
|
+
return a.id.localeCompare(b.id);
|
|
14663
|
+
});
|
|
13500
14664
|
}
|
|
13501
|
-
function
|
|
13502
|
-
const
|
|
13503
|
-
|
|
13504
|
-
|
|
13505
|
-
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
inFindings = true;
|
|
13514
|
-
out.push(line);
|
|
13515
|
-
continue;
|
|
13516
|
-
}
|
|
13517
|
-
if (inFindings && line.startsWith("- [")) {
|
|
13518
|
-
if (findingCount >= 5) continue;
|
|
13519
|
-
out.push(line);
|
|
13520
|
-
findingCount++;
|
|
13521
|
-
continue;
|
|
13522
|
-
}
|
|
13523
|
-
if (inFindings && line.startsWith("##")) {
|
|
13524
|
-
inFindings = false;
|
|
13525
|
-
}
|
|
13526
|
-
if (line.startsWith("## ") || line.startsWith("### Vital") || line.startsWith("- ") && !inFindings) {
|
|
13527
|
-
if (line.startsWith("(SaaS metrics not run")) continue;
|
|
13528
|
-
out.push(line);
|
|
13529
|
-
}
|
|
13530
|
-
if (line.startsWith("Overall score:") || line.startsWith("Total value at risk:")) {
|
|
13531
|
-
out.push(line);
|
|
13532
|
-
}
|
|
13533
|
-
if (line.startsWith("- ARR:") || line.startsWith("- NRR:") || line.startsWith("- GRR:")) {
|
|
13534
|
-
out.push(line);
|
|
14665
|
+
function formatEntry(explainer, audience) {
|
|
14666
|
+
const framing = audience === "ops" ? explainer.audience.ops : explainer.audience.board;
|
|
14667
|
+
const lines = [];
|
|
14668
|
+
lines.push(`### ${explainer.label} (\`${explainer.id}\`)`);
|
|
14669
|
+
lines.push("");
|
|
14670
|
+
lines.push(framing);
|
|
14671
|
+
lines.push("");
|
|
14672
|
+
if (audience === "ops") {
|
|
14673
|
+
lines.push("**How it's calculated**");
|
|
14674
|
+
lines.push("");
|
|
14675
|
+
for (const f of explainer.formula_lines) {
|
|
14676
|
+
lines.push(`- \`${f}\``);
|
|
13535
14677
|
}
|
|
14678
|
+
lines.push("");
|
|
14679
|
+
} else {
|
|
14680
|
+
lines.push(`*${explainer.tagline}*`);
|
|
14681
|
+
lines.push("");
|
|
13536
14682
|
}
|
|
13537
|
-
return
|
|
13538
|
-
}
|
|
13539
|
-
function appendFindings(lines, findings) {
|
|
13540
|
-
for (const f of findings.slice(0, 12)) {
|
|
13541
|
-
const dollars = f.dollar_value != null ? ` (${formatCurrency(f.dollar_value)})` : "";
|
|
13542
|
-
const plays = f.recommended_plays?.length ? ` \u2192 Plays: ${f.recommended_plays.map((p) => p.play_name).join(", ")}` : "";
|
|
13543
|
-
lines.push(`- [${f.severity}]${dollars} ${f.finding}${plays}`);
|
|
13544
|
-
}
|
|
14683
|
+
return lines;
|
|
13545
14684
|
}
|
|
13546
|
-
function
|
|
13547
|
-
|
|
13548
|
-
|
|
14685
|
+
function buildDefinitionsAppendix(bundle, opts = {}) {
|
|
14686
|
+
const audience = normalizeAudience(opts.audience);
|
|
14687
|
+
const cap = opts.cap ?? DEFAULT_CAP;
|
|
14688
|
+
const candidates = collectCandidates(bundle, opts).slice(0, cap);
|
|
14689
|
+
if (candidates.length === 0) return "";
|
|
14690
|
+
const lines = [];
|
|
14691
|
+
lines.push(`## Metric definitions (for the ${audienceLabel(audience)})`);
|
|
14692
|
+
lines.push("");
|
|
14693
|
+
lines.push(
|
|
14694
|
+
audience === "ops" ? "Formula-first brief for operators executing the plan. Full slides: `/deepdive <metric>`." : "Meaning-first brief for the room. Full slides: `/deepdive <metric>`."
|
|
14695
|
+
);
|
|
14696
|
+
lines.push("");
|
|
14697
|
+
for (const c of candidates) {
|
|
14698
|
+
const explainer = getMetricExplainer(c.id);
|
|
14699
|
+
if (!explainer) continue;
|
|
14700
|
+
lines.push(...formatEntry(explainer, audience));
|
|
13549
14701
|
}
|
|
13550
|
-
return "
|
|
14702
|
+
return lines.join("\n");
|
|
13551
14703
|
}
|
|
13552
|
-
var
|
|
13553
|
-
var
|
|
13554
|
-
"src/services/
|
|
14704
|
+
var DEFAULT_CAP;
|
|
14705
|
+
var init_metric_explainers = __esm({
|
|
14706
|
+
"src/services/metric-explainers.ts"() {
|
|
13555
14707
|
"use strict";
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
init_formatters();
|
|
13559
|
-
init_theme();
|
|
13560
|
-
KEY_METRICS = ["arr", "nrr", "grr", "win_rate", "pipeline_coverage"];
|
|
14708
|
+
init_metric_definitions();
|
|
14709
|
+
DEFAULT_CAP = 8;
|
|
13561
14710
|
}
|
|
13562
14711
|
});
|
|
13563
14712
|
|
|
@@ -13599,16 +14748,24 @@ function buildOpenQuestions(ctx) {
|
|
|
13599
14748
|
}
|
|
13600
14749
|
return lines.length > 0 ? lines.join("\n") : "(No open questions recorded.)";
|
|
13601
14750
|
}
|
|
13602
|
-
function
|
|
14751
|
+
function audiencePhrase(audience) {
|
|
14752
|
+
if (!audience) return "an executive audience";
|
|
14753
|
+
const a = audience.toLowerCase();
|
|
14754
|
+
if (a === "ops" || a === "operations") return "an ops / operator audience";
|
|
14755
|
+
if (a === "board") return "a board / exec audience";
|
|
14756
|
+
return `a ${audience} audience`;
|
|
14757
|
+
}
|
|
14758
|
+
function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions, definitionsBlock, ctx) {
|
|
13603
14759
|
const company = loadProfile()?.company_name ?? "the company";
|
|
13604
14760
|
const contextLabel = handoffInstructionPrefix(ctx.analysis.primary);
|
|
14761
|
+
const forWhom = audiencePhrase(ctx.scope?.audience);
|
|
13605
14762
|
const instructions = {
|
|
13606
|
-
deck: `produce an executive review deck outline for ${company}. Structure: (1) Headline number; (2) Pipeline health; (3) Top 3 risks with dollar impact; (4) Recommended plays; (5) Asks / decisions.`,
|
|
13607
|
-
asana: `produce an Asana project plan with sections and tasks tied to findings. Prioritize by dollar impact.`,
|
|
13608
|
-
clay: `produce a Clay table specification to operationalize the highest-impact finding.`,
|
|
13609
|
-
plan: `produce a prioritized action plan with problem, play, first 3 steps, owner, and leading indicator per item.`
|
|
14763
|
+
deck: `produce an executive review deck outline for ${company}, framed for ${forWhom}. Structure: (1) Headline number; (2) Pipeline health; (3) Top 3 risks with dollar impact; (4) Recommended plays; (5) Asks / decisions. Use the Metric definitions appendix so every slide can briefly remind the room what the number means for this audience.`,
|
|
14764
|
+
asana: `produce an Asana project plan with sections and tasks tied to findings for ${forWhom}. Prioritize by dollar impact. Reference metric definitions when a task owner needs to know what "good" looks like.`,
|
|
14765
|
+
clay: `produce a Clay table specification to operationalize the highest-impact finding for ${forWhom}.`,
|
|
14766
|
+
plan: `produce a prioritized action plan for ${forWhom} with problem, play, first 3 steps, owner, and leading indicator per item. Ground indicators in the Metric definitions appendix.`
|
|
13610
14767
|
};
|
|
13611
|
-
|
|
14768
|
+
const parts = [
|
|
13612
14769
|
`# NTRP handoff \u2192 ${target}`,
|
|
13613
14770
|
"",
|
|
13614
14771
|
`You are an expert GTM operator. Using ${contextLabel}, ${instructions[target]}`,
|
|
@@ -13624,25 +14781,35 @@ function wrapForTarget(target, analysisBlock, conversationBlock, openQuestions,
|
|
|
13624
14781
|
conversationBlock,
|
|
13625
14782
|
"",
|
|
13626
14783
|
"---",
|
|
13627
|
-
"",
|
|
13628
|
-
"## Open questions",
|
|
13629
|
-
"",
|
|
13630
|
-
openQuestions,
|
|
13631
|
-
"",
|
|
13632
|
-
"---",
|
|
13633
14784
|
""
|
|
13634
|
-
]
|
|
14785
|
+
];
|
|
14786
|
+
if (definitionsBlock.trim()) {
|
|
14787
|
+
parts.push(definitionsBlock.trim(), "", "---", "");
|
|
14788
|
+
}
|
|
14789
|
+
parts.push("## Open questions", "", openQuestions, "", "---", "");
|
|
14790
|
+
return parts.join("\n");
|
|
13635
14791
|
}
|
|
13636
14792
|
async function buildDeliverableDraft(ctx, target = "plan") {
|
|
13637
14793
|
const bundle = await loadSessionAnalysisBundle();
|
|
13638
14794
|
const analysis = buildHandoffContextBlock(bundle, ctx);
|
|
13639
14795
|
const conversation = buildConversationSection(ctx);
|
|
13640
14796
|
const open_questions = buildOpenQuestions(ctx);
|
|
14797
|
+
const definitions = buildDefinitionsAppendix(bundle, {
|
|
14798
|
+
audience: ctx.scope?.audience,
|
|
14799
|
+
prefer: bundle.diagnosis?.health.gating_vital_sign ? [bundle.diagnosis.health.gating_vital_sign] : void 0
|
|
14800
|
+
});
|
|
13641
14801
|
if (!analysis && ctx.messages.length === 0) return null;
|
|
13642
|
-
const markdown = wrapForTarget(
|
|
14802
|
+
const markdown = wrapForTarget(
|
|
14803
|
+
target,
|
|
14804
|
+
analysis,
|
|
14805
|
+
conversation,
|
|
14806
|
+
open_questions,
|
|
14807
|
+
definitions,
|
|
14808
|
+
ctx
|
|
14809
|
+
);
|
|
13643
14810
|
return {
|
|
13644
14811
|
markdown,
|
|
13645
|
-
sections: { analysis, conversation, open_questions }
|
|
14812
|
+
sections: { analysis, conversation, open_questions, definitions }
|
|
13646
14813
|
};
|
|
13647
14814
|
}
|
|
13648
14815
|
function inferHandoffTarget(input) {
|
|
@@ -13662,6 +14829,7 @@ var init_handoff_draft = __esm({
|
|
|
13662
14829
|
"use strict";
|
|
13663
14830
|
init_profile();
|
|
13664
14831
|
init_session_analysis();
|
|
14832
|
+
init_metric_explainers();
|
|
13665
14833
|
QUESTION_LEAD_RE = /^\s*(what|why|how|when|where|who|which|is|are|was|were|do|does|did|explain|tell me|help me understand)\b/i;
|
|
13666
14834
|
SHIP_INTENT_RE = /\b(ship|export|deliver|write[- ]?up|board memo|action plan|turn (this|it|that) into|(draft|create|make|build|prepare|generate|send)\s+(me\s+)?(a\s+|the\s+)?hand[- ]?off|hand[- ]?off\s+(prompt|doc|document|plan))\b/i;
|
|
13667
14835
|
}
|
|
@@ -14887,94 +16055,94 @@ var init_strategist2 = __esm({
|
|
|
14887
16055
|
});
|
|
14888
16056
|
|
|
14889
16057
|
// src/output/strategy-brief.ts
|
|
14890
|
-
import
|
|
16058
|
+
import chalk13 from "chalk";
|
|
14891
16059
|
function printWrapped(text, width, prefix = INDENT, style) {
|
|
14892
16060
|
for (const line of wrapWords(text, width)) {
|
|
14893
16061
|
console.log(prefix + (style ? style(line) : line));
|
|
14894
16062
|
}
|
|
14895
16063
|
}
|
|
14896
16064
|
function outcomeLine(outcome) {
|
|
14897
|
-
return `${
|
|
16065
|
+
return `${chalk13.bold(outcome.metric)}: ${outcome.baseline} ${chalk13.dim("->")} ${chalk13.bold(outcome.target_range)} ${chalk13.dim(`by ${outcome.check_date} \xB7 ${outcome.measured_by}`)}`;
|
|
14898
16066
|
}
|
|
14899
16067
|
function printWorkstream(ws, width) {
|
|
14900
|
-
const plays = ws.play_ids.length > 0 ?
|
|
14901
|
-
console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${
|
|
14902
|
-
printWrapped(ws.problem, width - 5, INDENT + " ", (s) =>
|
|
16068
|
+
const plays = ws.play_ids.length > 0 ? chalk13.dim(` play: ${ws.play_ids.join(", ")}`) : "";
|
|
16069
|
+
console.log(`${INDENT}${paint("accent", `${ws.order}.`)} ${chalk13.bold(ws.title)}${plays}`);
|
|
16070
|
+
printWrapped(ws.problem, width - 5, INDENT + " ", (s) => chalk13.dim(s));
|
|
14903
16071
|
if (ws.rationale) {
|
|
14904
|
-
printWrapped(`Why now: ${ws.rationale}`, width - 5, INDENT + " ", (s) =>
|
|
16072
|
+
printWrapped(`Why now: ${ws.rationale}`, width - 5, INDENT + " ", (s) => chalk13.dim(s));
|
|
14905
16073
|
}
|
|
14906
16074
|
console.log(`${INDENT} ${outcomeLine(ws.expected_outcome)}`);
|
|
14907
16075
|
for (const li of ws.leading_indicators) {
|
|
14908
|
-
console.log(`${INDENT} ${
|
|
16076
|
+
console.log(`${INDENT} ${chalk13.dim("leads:")} ${outcomeLine(li)}`);
|
|
14909
16077
|
}
|
|
14910
16078
|
if (ws.milestones.length > 0) {
|
|
14911
|
-
console.log(`${INDENT} ${
|
|
16079
|
+
console.log(`${INDENT} ${chalk13.dim("Milestones")}`);
|
|
14912
16080
|
for (const m of ws.milestones) {
|
|
14913
|
-
console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${
|
|
16081
|
+
console.log(`${INDENT} ${paint("accent", m.due)} ${m.label} ${chalk13.dim(`(verify: ${m.verification})`)}`);
|
|
14914
16082
|
}
|
|
14915
16083
|
}
|
|
14916
16084
|
if (ws.deliverables.length > 0) {
|
|
14917
|
-
console.log(`${INDENT} ${
|
|
16085
|
+
console.log(`${INDENT} ${chalk13.dim("Deliverables")}`);
|
|
14918
16086
|
for (const d of ws.deliverables) {
|
|
14919
|
-
console.log(`${INDENT} ${
|
|
16087
|
+
console.log(`${INDENT} ${chalk13.dim("[ ]")} ${d.label} ${chalk13.dim(`(${d.kind.replace("_", " ")} \xB7 due ${d.due})`)}`);
|
|
14920
16088
|
}
|
|
14921
16089
|
}
|
|
14922
16090
|
if (ws.actions.length > 0) {
|
|
14923
|
-
console.log(`${INDENT} ${
|
|
16091
|
+
console.log(`${INDENT} ${chalk13.dim("First actions")}`);
|
|
14924
16092
|
for (const action of ws.actions.slice(0, 4)) {
|
|
14925
|
-
printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) =>
|
|
16093
|
+
printWrapped(`- ${action}`, width - 7, INDENT + " ", (s) => chalk13.dim(s));
|
|
14926
16094
|
}
|
|
14927
16095
|
}
|
|
14928
16096
|
printWrapped(
|
|
14929
16097
|
`If ${ws.contingency.trigger} (check ${ws.contingency.trigger_check_date}) -> ${ws.contingency.fallback}`,
|
|
14930
16098
|
width - 5,
|
|
14931
16099
|
INDENT + " ",
|
|
14932
|
-
(s) =>
|
|
16100
|
+
(s) => chalk13.hex("#eab308")(s)
|
|
14933
16101
|
);
|
|
14934
|
-
console.log(`${INDENT} ${
|
|
16102
|
+
console.log(`${INDENT} ${chalk13.dim(`~${Math.round(ws.effort_hours)} team-hours`)}`);
|
|
14935
16103
|
console.log();
|
|
14936
16104
|
}
|
|
14937
16105
|
function printStrategyBrief(plan, stats) {
|
|
14938
16106
|
const width = Math.min(termWidth() - 4, 92);
|
|
14939
16107
|
console.log();
|
|
14940
16108
|
console.log(
|
|
14941
|
-
`${INDENT}${
|
|
16109
|
+
`${INDENT}${chalk13.bold(`Strategy brief \u2014 ${plan.title}`)} ${chalk13.dim(`confidence ${plan.confidence.toFixed(2)} \xB7 ${plan.priority} priority \xB7 review ${plan.review_cadence.toLowerCase()}`)}`
|
|
14942
16110
|
);
|
|
14943
|
-
console.log(INDENT +
|
|
16111
|
+
console.log(INDENT + chalk13.dim(hr(width)));
|
|
14944
16112
|
printWrapped(`Objective: ${plan.objective}`, width, INDENT, (s) => paint("accent", s));
|
|
14945
16113
|
console.log();
|
|
14946
|
-
console.log(`${INDENT}${
|
|
16114
|
+
console.log(`${INDENT}${chalk13.dim("30,000 ft")}`);
|
|
14947
16115
|
printWrapped(plan.summary_30k, width);
|
|
14948
16116
|
console.log();
|
|
14949
16117
|
for (const ws of plan.workstreams) {
|
|
14950
16118
|
printWorkstream(ws, width);
|
|
14951
16119
|
}
|
|
14952
16120
|
if (plan.constraints.length > 0) {
|
|
14953
|
-
console.log(`${INDENT}${
|
|
16121
|
+
console.log(`${INDENT}${chalk13.dim("Constraints")}`);
|
|
14954
16122
|
for (const c of plan.constraints) {
|
|
14955
|
-
printWrapped(`- ${c}`, width - 2, INDENT, (s) =>
|
|
16123
|
+
printWrapped(`- ${c}`, width - 2, INDENT, (s) => chalk13.dim(s));
|
|
14956
16124
|
}
|
|
14957
16125
|
console.log();
|
|
14958
16126
|
}
|
|
14959
16127
|
if (plan.assumptions.length > 0) {
|
|
14960
|
-
console.log(`${INDENT}${
|
|
16128
|
+
console.log(`${INDENT}${chalk13.dim("Assumptions (unverified \u2014 not counted as targets)")}`);
|
|
14961
16129
|
for (const a of plan.assumptions) {
|
|
14962
|
-
printWrapped(`- ${a}`, width - 2, INDENT, (s) =>
|
|
16130
|
+
printWrapped(`- ${a}`, width - 2, INDENT, (s) => chalk13.dim(s));
|
|
14963
16131
|
}
|
|
14964
16132
|
console.log();
|
|
14965
16133
|
}
|
|
14966
16134
|
if (plan.risks.length > 0) {
|
|
14967
|
-
console.log(`${INDENT}${
|
|
16135
|
+
console.log(`${INDENT}${chalk13.dim("Risks")}`);
|
|
14968
16136
|
for (const r of plan.risks) {
|
|
14969
|
-
printWrapped(`- ${r}`, width - 2, INDENT, (s) =>
|
|
16137
|
+
printWrapped(`- ${r}`, width - 2, INDENT, (s) => chalk13.dim(s));
|
|
14970
16138
|
}
|
|
14971
16139
|
console.log();
|
|
14972
16140
|
}
|
|
14973
16141
|
const totalHours = plan.workstreams.reduce((sum, ws) => sum + ws.effort_hours, 0);
|
|
14974
|
-
console.log(INDENT +
|
|
16142
|
+
console.log(INDENT + chalk13.dim(hr(width)));
|
|
14975
16143
|
const coverage = stats.total_targets > 0 ? `${stats.measurable_targets} of ${stats.total_targets} targets measurable with current data` : "no quantified targets";
|
|
14976
|
-
const coverageStyled = stats.total_targets > 0 && stats.measurable_targets === stats.total_targets ? paint("success", coverage) :
|
|
14977
|
-
console.log(`${INDENT}${coverageStyled}${
|
|
16144
|
+
const coverageStyled = stats.total_targets > 0 && stats.measurable_targets === stats.total_targets ? paint("success", coverage) : chalk13.hex("#eab308")(coverage);
|
|
16145
|
+
console.log(`${INDENT}${coverageStyled}${chalk13.dim(` \xB7 ~${Math.round(totalHours)} total team-hours across ${plan.workstreams.length} workstream${plan.workstreams.length === 1 ? "" : "s"}`)}`);
|
|
14978
16146
|
console.log();
|
|
14979
16147
|
}
|
|
14980
16148
|
var INDENT;
|
|
@@ -14999,7 +16167,7 @@ __export(strategist_flow_exports, {
|
|
|
14999
16167
|
resumeStrategistAfterConnect: () => resumeStrategistAfterConnect,
|
|
15000
16168
|
startStrategistFlow: () => startStrategistFlow
|
|
15001
16169
|
});
|
|
15002
|
-
import
|
|
16170
|
+
import chalk14 from "chalk";
|
|
15003
16171
|
function isStrategistIntent(input) {
|
|
15004
16172
|
const line = input.trim();
|
|
15005
16173
|
if (!line) return false;
|
|
@@ -15019,11 +16187,11 @@ function queueStrategistForAnalysis(ctx, opts) {
|
|
|
15019
16187
|
saveSessionState(ctx);
|
|
15020
16188
|
console.log();
|
|
15021
16189
|
console.log(
|
|
15022
|
-
" " +
|
|
16190
|
+
" " + chalk14.dim("Strategy session queued \u2014 I'll build the plan once your data is analyzed.")
|
|
15023
16191
|
);
|
|
15024
16192
|
if (opts.origin !== "nl") {
|
|
15025
16193
|
console.log(
|
|
15026
|
-
" " +
|
|
16194
|
+
" " + chalk14.dim("Tell me what to look at, paste a CSV path, or say ") + chalk14.cyan("use demo data") + chalk14.dim(".")
|
|
15027
16195
|
);
|
|
15028
16196
|
console.log();
|
|
15029
16197
|
}
|
|
@@ -15042,7 +16210,7 @@ async function startStrategistFlow(ctx, opts) {
|
|
|
15042
16210
|
ctx.strategistState = { step: "objective_input", origin: opts.origin };
|
|
15043
16211
|
saveSessionState(ctx);
|
|
15044
16212
|
console.log();
|
|
15045
|
-
console.log(" " +
|
|
16213
|
+
console.log(" " + chalk14.dim(`What's the objective? State it like a finish line \u2014 e.g. "cut stale pipeline in half before Q4".`));
|
|
15046
16214
|
console.log();
|
|
15047
16215
|
recordMessage(ctx, "agent", "Strategist: asked for objective");
|
|
15048
16216
|
return "Awaiting objective";
|
|
@@ -15079,14 +16247,14 @@ async function handleStrategizeFlow(input, ctx) {
|
|
|
15079
16247
|
ctx.strategistState = void 0;
|
|
15080
16248
|
saveSessionState(ctx);
|
|
15081
16249
|
console.log();
|
|
15082
|
-
console.log(" " +
|
|
16250
|
+
console.log(" " + chalk14.dim("Strategy session cancelled \u2014 back to exploring."));
|
|
15083
16251
|
console.log();
|
|
15084
16252
|
return "Strategy cancelled";
|
|
15085
16253
|
}
|
|
15086
16254
|
if (state2.step === "objective_input") {
|
|
15087
16255
|
if (line.length < 8) {
|
|
15088
16256
|
console.log();
|
|
15089
|
-
console.log(" " +
|
|
16257
|
+
console.log(" " + chalk14.dim("Give me a bit more \u2014 what outcome are we planning toward?"));
|
|
15090
16258
|
console.log();
|
|
15091
16259
|
return "Awaiting objective";
|
|
15092
16260
|
}
|
|
@@ -15103,17 +16271,17 @@ async function handleStrategizeFlow(input, ctx) {
|
|
|
15103
16271
|
state2.step = "objective_input";
|
|
15104
16272
|
saveSessionState(ctx);
|
|
15105
16273
|
console.log();
|
|
15106
|
-
console.log(" " +
|
|
16274
|
+
console.log(" " + chalk14.dim("What's the objective? State it like a finish line."));
|
|
15107
16275
|
console.log();
|
|
15108
16276
|
return "Awaiting objective";
|
|
15109
16277
|
}
|
|
15110
16278
|
if (QUESTION_RE.test(line)) {
|
|
15111
16279
|
console.log();
|
|
15112
16280
|
console.log(
|
|
15113
|
-
" " +
|
|
16281
|
+
" " + chalk14.dim("That looks like a question \u2014 I'm holding a strategy objective right now.")
|
|
15114
16282
|
);
|
|
15115
16283
|
console.log(
|
|
15116
|
-
" " +
|
|
16284
|
+
" " + chalk14.dim("Say ") + chalk14.cyan("yes") + chalk14.dim(" to build the plan, ") + chalk14.cyan("adjust") + chalk14.dim(" to restate it, or ") + chalk14.cyan("cancel") + chalk14.dim(" to go answer questions first.")
|
|
15117
16285
|
);
|
|
15118
16286
|
console.log();
|
|
15119
16287
|
return "Awaiting confirm";
|
|
@@ -15126,7 +16294,7 @@ async function handleStrategizeFlow(input, ctx) {
|
|
|
15126
16294
|
}
|
|
15127
16295
|
console.log();
|
|
15128
16296
|
console.log(
|
|
15129
|
-
" " +
|
|
16297
|
+
" " + chalk14.dim("Say ") + chalk14.cyan("yes") + chalk14.dim(" to plan, ") + chalk14.cyan("adjust") + chalk14.dim(" to restate the objective, or ") + chalk14.cyan("cancel") + chalk14.dim(".")
|
|
15130
16298
|
);
|
|
15131
16299
|
console.log();
|
|
15132
16300
|
return "Awaiting confirm";
|
|
@@ -15189,7 +16357,7 @@ async function runStrategistSession(ctx) {
|
|
|
15189
16357
|
break;
|
|
15190
16358
|
case "thinking":
|
|
15191
16359
|
spinner.stop();
|
|
15192
|
-
console.log(" " +
|
|
16360
|
+
console.log(" " + chalk14.dim.italic(event.text));
|
|
15193
16361
|
spinner.start();
|
|
15194
16362
|
break;
|
|
15195
16363
|
case "notice":
|
|
@@ -15211,17 +16379,17 @@ async function runStrategistSession(ctx) {
|
|
|
15211
16379
|
spinner.stop();
|
|
15212
16380
|
} catch (err) {
|
|
15213
16381
|
spinner.fail("Strategy session failed");
|
|
15214
|
-
console.error(" " +
|
|
16382
|
+
console.error(" " + chalk14.red(String(err.message ?? err)));
|
|
15215
16383
|
ctx.strategistState = void 0;
|
|
15216
16384
|
saveSessionState(ctx);
|
|
15217
16385
|
console.log(
|
|
15218
|
-
" " +
|
|
16386
|
+
" " + chalk14.dim('Strategy session dropped \u2014 say "how should we fix this?" or run ') + paint("accent", "/strategy") + chalk14.dim(" to retry.")
|
|
15219
16387
|
);
|
|
15220
16388
|
console.log();
|
|
15221
16389
|
return;
|
|
15222
16390
|
}
|
|
15223
16391
|
if (!plan) {
|
|
15224
|
-
console.log(" " +
|
|
16392
|
+
console.log(" " + chalk14.dim("(no plan produced)"));
|
|
15225
16393
|
ctx.strategistState = void 0;
|
|
15226
16394
|
saveSessionState(ctx);
|
|
15227
16395
|
console.log();
|
|
@@ -15229,7 +16397,7 @@ async function runStrategistSession(ctx) {
|
|
|
15229
16397
|
}
|
|
15230
16398
|
printStrategyBrief(plan, stats);
|
|
15231
16399
|
for (const notice of notices.slice(0, 6)) {
|
|
15232
|
-
console.log(" " +
|
|
16400
|
+
console.log(" " + chalk14.dim(notice));
|
|
15233
16401
|
}
|
|
15234
16402
|
printLlmAttribution(meta);
|
|
15235
16403
|
console.log();
|
|
@@ -15254,18 +16422,18 @@ async function runStrategistSession(ctx) {
|
|
|
15254
16422
|
creditStrategySession(ctx);
|
|
15255
16423
|
console.log();
|
|
15256
16424
|
console.log(" " + paint("accent", `Strategy saved: ${persisted.strategy.title}`));
|
|
15257
|
-
console.log(" " +
|
|
16425
|
+
console.log(" " + chalk14.dim(persisted.library_path));
|
|
15258
16426
|
console.log(
|
|
15259
|
-
" " +
|
|
16427
|
+
" " + chalk14.dim("Check progress anytime with ") + paint("accent", `/strategy review ${persisted.strategy.slug}`) + chalk14.dim(" \u2014 future answers will reference this plan.")
|
|
15260
16428
|
);
|
|
15261
16429
|
console.log();
|
|
15262
16430
|
recordMessage(ctx, "agent", `Strategy saved: ${persisted.strategy.title} (${persisted.strategy.slug})`);
|
|
15263
16431
|
} catch (err) {
|
|
15264
|
-
console.error(" " +
|
|
16432
|
+
console.error(" " + chalk14.red(`Could not save strategy: ${String(err.message ?? err)}`));
|
|
15265
16433
|
console.log();
|
|
15266
16434
|
}
|
|
15267
16435
|
} else {
|
|
15268
|
-
console.log(" " +
|
|
16436
|
+
console.log(" " + chalk14.dim("Kept as a working draft \u2014 not saved to the library."));
|
|
15269
16437
|
console.log();
|
|
15270
16438
|
recordMessage(ctx, "agent", `Strategy drafted (unsaved): ${plan.title}`);
|
|
15271
16439
|
}
|
|
@@ -15294,16 +16462,16 @@ async function ensureSnapshot(ctx) {
|
|
|
15294
16462
|
}
|
|
15295
16463
|
function printObjectiveCard(ctx, objective, proposed) {
|
|
15296
16464
|
console.log();
|
|
15297
|
-
console.log(" " +
|
|
16465
|
+
console.log(" " + chalk14.bold("Strategy session"));
|
|
15298
16466
|
console.log(
|
|
15299
|
-
" " +
|
|
16467
|
+
" " + chalk14.dim(proposed ? "Proposed objective: " : "Objective: ") + paint("accent", objective)
|
|
15300
16468
|
);
|
|
15301
16469
|
console.log(
|
|
15302
|
-
" " +
|
|
16470
|
+
" " + chalk14.dim("I'll ground it in your live data, sequence the fixes, set measurable milestones, and stress-test the plan.")
|
|
15303
16471
|
);
|
|
15304
16472
|
console.log();
|
|
15305
16473
|
console.log(
|
|
15306
|
-
" " +
|
|
16474
|
+
" " + chalk14.dim("Confirm? ") + chalk14.cyan("\u23CE yes") + chalk14.dim(" \xB7 ") + chalk14.cyan("adjust") + chalk14.dim(" \xB7 ") + chalk14.cyan("cancel")
|
|
15307
16475
|
);
|
|
15308
16476
|
console.log();
|
|
15309
16477
|
}
|
|
@@ -15324,35 +16492,35 @@ async function printKeylessSkeletonPlan(ctx, objective) {
|
|
|
15324
16492
|
LAYERS2
|
|
15325
16493
|
);
|
|
15326
16494
|
if (triggered.length > 0) {
|
|
15327
|
-
console.log(" " +
|
|
15328
|
-
console.log(" " +
|
|
15329
|
-
console.log(" " +
|
|
16495
|
+
console.log(" " + chalk14.bold("Skeleton plan") + chalk14.dim(" \u2014 deterministic, from your computed vitals (no AI)"));
|
|
16496
|
+
console.log(" " + chalk14.dim(`Objective: ${objective}`));
|
|
16497
|
+
console.log(" " + chalk14.dim("Ordered by dependency: clean data gates moving pipeline gates efficient effort."));
|
|
15330
16498
|
console.log();
|
|
15331
16499
|
triggered.forEach(({ play, vital }, index) => {
|
|
15332
16500
|
const dollar = vital.dollar_value != null && vital.dollar_value > 0 ? ` \xB7 ${formatCurrency(vital.dollar_value)} ${vital.dollar_label ?? ""}`.trimEnd() : "";
|
|
15333
16501
|
console.log(
|
|
15334
|
-
` ${paint("accent", `${index + 1}.`)} ${
|
|
16502
|
+
` ${paint("accent", `${index + 1}.`)} ${chalk14.bold(play.name)} ${chalk14.dim(`(${play.id})`)}`
|
|
15335
16503
|
);
|
|
15336
16504
|
console.log(
|
|
15337
|
-
" " +
|
|
16505
|
+
" " + chalk14.dim(`${vital.vital_sign} ${Math.round(vital.score)} (${vital.status})${dollar}`)
|
|
15338
16506
|
);
|
|
15339
|
-
console.log(" " +
|
|
16507
|
+
console.log(" " + chalk14.dim(`Why: ${play.why.split(". ")[0]}.`));
|
|
15340
16508
|
if (play.steps[0]) {
|
|
15341
|
-
console.log(" " +
|
|
16509
|
+
console.log(" " + chalk14.dim(`First step: ${play.steps[0]}`));
|
|
15342
16510
|
}
|
|
15343
|
-
console.log(" " +
|
|
16511
|
+
console.log(" " + chalk14.dim(`Expected: ${play.expected_outcome}`));
|
|
15344
16512
|
console.log();
|
|
15345
16513
|
});
|
|
15346
16514
|
} else {
|
|
15347
|
-
console.log(" " +
|
|
16515
|
+
console.log(" " + chalk14.bold("No plays triggered") + chalk14.dim(" \u2014 every vital sign is above its play threshold."));
|
|
15348
16516
|
console.log();
|
|
15349
16517
|
}
|
|
15350
16518
|
}
|
|
15351
16519
|
console.log(
|
|
15352
|
-
" " +
|
|
16520
|
+
" " + chalk14.dim("For the full strategist \u2014 milestones, outcome ranges, contingencies \u2014 press ") + paint("accent", "\u23CE") + chalk14.dim(" to run ") + paint("accent", "/connect") + chalk14.dim(" and paste any provider's key.")
|
|
15353
16521
|
);
|
|
15354
16522
|
console.log(
|
|
15355
|
-
" " +
|
|
16523
|
+
" " + chalk14.dim("Objective kept \u2014 after ") + paint("accent", "/connect") + chalk14.dim(" I'll bring back the confirm card so you can run the full plan.")
|
|
15356
16524
|
);
|
|
15357
16525
|
console.log();
|
|
15358
16526
|
}
|
|
@@ -15398,7 +16566,7 @@ __export(keyless_ask_exports, {
|
|
|
15398
16566
|
isKeylessVitalsAsk: () => isKeylessVitalsAsk,
|
|
15399
16567
|
tryKeylessAskAnswer: () => tryKeylessAskAnswer
|
|
15400
16568
|
});
|
|
15401
|
-
import
|
|
16569
|
+
import chalk15 from "chalk";
|
|
15402
16570
|
function isKeylessVitalsAsk(input) {
|
|
15403
16571
|
return KEYLESS_ASK_RE.test(input.trim());
|
|
15404
16572
|
}
|
|
@@ -15447,35 +16615,35 @@ async function tryKeylessAskAnswer(ctx, input, opts = {}) {
|
|
|
15447
16615
|
const headline = dollarBit ? `${label} \u2014 ${dollarBit} \u2014 is the most expensive problem to solve right now.` : `${label} (score ${Math.round(primary.score)}, ${primary.status}) is the problem to fix first.`;
|
|
15448
16616
|
const runners = [...aggregate.vital_signs].filter((v) => v.vital_sign !== primary.vital_sign && (v.dollar_value ?? 0) > 0).sort((a, b) => (b.dollar_value ?? 0) - (a.dollar_value ?? 0)).slice(0, 2);
|
|
15449
16617
|
console.log();
|
|
15450
|
-
console.log(" " +
|
|
16618
|
+
console.log(" " + chalk15.bold(headline));
|
|
15451
16619
|
if (opts.fromResume) {
|
|
15452
16620
|
if (runners.length > 0) {
|
|
15453
16621
|
console.log(
|
|
15454
|
-
" " +
|
|
16622
|
+
" " + chalk15.dim("Next after that: ") + chalk15.dim(runners.map(formatRunnerBit).join(" \xB7 "))
|
|
15455
16623
|
);
|
|
15456
16624
|
}
|
|
15457
16625
|
} else {
|
|
15458
16626
|
console.log();
|
|
15459
16627
|
if (gating && gating.vital_sign !== primary.vital_sign) {
|
|
15460
16628
|
console.log(
|
|
15461
|
-
" " +
|
|
16629
|
+
" " + chalk15.dim("Gating vital: ") + paint("accent", VITAL_SIGN_LABELS[gating.vital_sign]) + chalk15.dim(` (score ${Math.round(gating.score)}) \u2014 it bounds what you can trust downstream.`)
|
|
15462
16630
|
);
|
|
15463
16631
|
}
|
|
15464
16632
|
if (runners.length > 0) {
|
|
15465
|
-
console.log(" " +
|
|
16633
|
+
console.log(" " + chalk15.dim("Also on the board:"));
|
|
15466
16634
|
for (const vs of runners) {
|
|
15467
|
-
console.log(" " +
|
|
16635
|
+
console.log(" " + chalk15.dim("\xB7 ") + formatVitalLine(vs));
|
|
15468
16636
|
}
|
|
15469
16637
|
}
|
|
15470
16638
|
if (aggregate.total_value_at_risk != null && aggregate.total_value_at_risk > 0) {
|
|
15471
16639
|
console.log(
|
|
15472
|
-
" " +
|
|
16640
|
+
" " + chalk15.dim("Total at risk: ") + chalk15.green(formatCurrency(aggregate.total_value_at_risk))
|
|
15473
16641
|
);
|
|
15474
16642
|
}
|
|
15475
16643
|
}
|
|
15476
16644
|
console.log();
|
|
15477
16645
|
console.log(
|
|
15478
|
-
" " +
|
|
16646
|
+
" " + chalk15.dim("Press ") + paint("accent", "\u23CE") + chalk15.dim(" to connect a key (") + paint("accent", "/connect") + chalk15.dim(") for the why and the plan \u2014 I'll finish this question when you do.")
|
|
15479
16647
|
);
|
|
15480
16648
|
console.log();
|
|
15481
16649
|
if (!opts.fromResume) {
|
|
@@ -15497,11 +16665,119 @@ var init_keyless_ask = __esm({
|
|
|
15497
16665
|
}
|
|
15498
16666
|
});
|
|
15499
16667
|
|
|
16668
|
+
// src/conversation/keyless-definitions.ts
|
|
16669
|
+
import chalk16 from "chalk";
|
|
16670
|
+
function isPossessiveMetricAsk(input) {
|
|
16671
|
+
return POSSESSIVE_RE.test(input.trim());
|
|
16672
|
+
}
|
|
16673
|
+
function isDefinitionAsk(input) {
|
|
16674
|
+
const line = input.trim();
|
|
16675
|
+
if (!line) return false;
|
|
16676
|
+
if (isPossessiveMetricAsk(line)) return false;
|
|
16677
|
+
return DEFINITION_RE.test(line) || MEAN_RE.test(line);
|
|
16678
|
+
}
|
|
16679
|
+
function extractDefinitionQuery(input) {
|
|
16680
|
+
const line = input.trim().replace(/[?.!]+$/, "");
|
|
16681
|
+
const mean = line.match(MEAN_RE);
|
|
16682
|
+
if (mean?.[1]) return cleanQuery(mean[1]);
|
|
16683
|
+
const how = line.match(HOW_CALC_RE);
|
|
16684
|
+
if (how?.[1]) return cleanQuery(how[1]);
|
|
16685
|
+
const what = line.match(WHAT_IS_RE);
|
|
16686
|
+
if (what?.[1]) return cleanQuery(what[1]);
|
|
16687
|
+
return void 0;
|
|
16688
|
+
}
|
|
16689
|
+
function cleanQuery(raw) {
|
|
16690
|
+
return raw.replace(/^(?:a|an|the|our|my)\s+/i, "").replace(/\b(metric|score|number|vital(?:\s+sign)?|kpi)\b/gi, "").replace(/\s+/g, " ").trim();
|
|
16691
|
+
}
|
|
16692
|
+
function matchDefinitionExplainer(input) {
|
|
16693
|
+
if (!isDefinitionAsk(input)) return void 0;
|
|
16694
|
+
const query = extractDefinitionQuery(input);
|
|
16695
|
+
if (!query) return void 0;
|
|
16696
|
+
const id = resolveMetricId(query);
|
|
16697
|
+
if (!id) {
|
|
16698
|
+
const tokens = query.split(/\s+/);
|
|
16699
|
+
for (let n = tokens.length; n >= 1; n--) {
|
|
16700
|
+
for (let i = 0; i + n <= tokens.length; i++) {
|
|
16701
|
+
const slice = tokens.slice(i, i + n).join(" ");
|
|
16702
|
+
const hit = resolveMetricId(slice);
|
|
16703
|
+
if (hit) return getMetricExplainer(hit);
|
|
16704
|
+
}
|
|
16705
|
+
}
|
|
16706
|
+
return void 0;
|
|
16707
|
+
}
|
|
16708
|
+
return getMetricExplainer(id);
|
|
16709
|
+
}
|
|
16710
|
+
function printWrapped2(text, indent = " ") {
|
|
16711
|
+
for (const line of wrapWords(text, 78)) {
|
|
16712
|
+
console.log(indent + line);
|
|
16713
|
+
}
|
|
16714
|
+
}
|
|
16715
|
+
function tryKeylessDefinitionAnswer(ctx, input) {
|
|
16716
|
+
const explainer = matchDefinitionExplainer(input);
|
|
16717
|
+
if (!explainer) return false;
|
|
16718
|
+
const motion = loadProfile()?.sales_motion ?? null;
|
|
16719
|
+
const bench = explainer.benchmarkHint?.(motion);
|
|
16720
|
+
console.log();
|
|
16721
|
+
console.log(
|
|
16722
|
+
" " + sectionHeading(explainer.label) + chalk16.dim(` \xB7 ${explainer.kind === "vital" ? "vital sign" : "SaaS metric"}`)
|
|
16723
|
+
);
|
|
16724
|
+
console.log(" " + chalk16.dim(explainer.tagline));
|
|
16725
|
+
console.log();
|
|
16726
|
+
console.log(" " + bold("What it means"));
|
|
16727
|
+
printWrapped2(explainer.meaning, " ");
|
|
16728
|
+
console.log();
|
|
16729
|
+
console.log(" " + bold("How NTRP calculates it"));
|
|
16730
|
+
printWrapped2(explainer.how_computed, " ");
|
|
16731
|
+
for (const f of explainer.formula_lines) {
|
|
16732
|
+
console.log(" " + paint("accent", f));
|
|
16733
|
+
}
|
|
16734
|
+
if (bench) {
|
|
16735
|
+
console.log();
|
|
16736
|
+
console.log(" " + chalk16.dim(`Benchmark \xB7 ${bench}`));
|
|
16737
|
+
}
|
|
16738
|
+
if (explainer.dollar_label) {
|
|
16739
|
+
console.log(
|
|
16740
|
+
" " + chalk16.dim(`Dollar translation \xB7 ${explainer.dollar_label}`)
|
|
16741
|
+
);
|
|
16742
|
+
}
|
|
16743
|
+
console.log();
|
|
16744
|
+
console.log(
|
|
16745
|
+
" " + chalk16.dim("More: ") + paint("accent", `/deepdive ${explainer.id}`) + chalk16.dim(" \xB7 full tour: ") + paint("accent", "/deepdive")
|
|
16746
|
+
);
|
|
16747
|
+
console.log();
|
|
16748
|
+
recordMessage(ctx, "user", input);
|
|
16749
|
+
recordMessage(
|
|
16750
|
+
ctx,
|
|
16751
|
+
"agent",
|
|
16752
|
+
`${explainer.label}: ${explainer.tagline} (keyless definition)`
|
|
16753
|
+
);
|
|
16754
|
+
saveSessionState(ctx);
|
|
16755
|
+
return true;
|
|
16756
|
+
}
|
|
16757
|
+
var POSSESSIVE_RE, DEFINITION_RE, MEAN_RE, HOW_CALC_RE, WHAT_IS_RE;
|
|
16758
|
+
var init_keyless_definitions = __esm({
|
|
16759
|
+
"src/conversation/keyless-definitions.ts"() {
|
|
16760
|
+
"use strict";
|
|
16761
|
+
init_context2();
|
|
16762
|
+
init_profile();
|
|
16763
|
+
init_metric_definitions();
|
|
16764
|
+
init_theme();
|
|
16765
|
+
init_layout();
|
|
16766
|
+
POSSESSIVE_RE = /\b(our|my|we|us|the company'?s|this (company|business|org|pipeline)|current|actual|latest)\b/i;
|
|
16767
|
+
DEFINITION_RE = /^(?:(?:please|can you|could you)\s+)?(?:what\s+(?:is|are|does)|what'?s|whats|define|explain|describe|how\s+(?:is|are|do(?:es)?)\s+(?:.+?\s+)?(?:calculated|computed|measured|defined)|how\s+do(?:es)?\s+(?:.+?\s+)?(?:work|get calculated)|tell me about|meaning of)\b/i;
|
|
16768
|
+
MEAN_RE = /\bwhat does\b(.+?)\bmean\b/i;
|
|
16769
|
+
HOW_CALC_RE = /\bhow (?:is|are|do(?:es)?)\b(.+?)\b(?:calculated|computed|measured|defined|work)\b/i;
|
|
16770
|
+
WHAT_IS_RE = /\b(?:what(?:'s|s)?|define|explain|describe|tell me about|meaning of)\s+(.+?)(?:\?|$)/i;
|
|
16771
|
+
}
|
|
16772
|
+
});
|
|
16773
|
+
|
|
15500
16774
|
// src/conversation/orchestrator.ts
|
|
15501
|
-
import
|
|
15502
|
-
import { writeFileSync as
|
|
15503
|
-
import { join as join19 } from "path";
|
|
16775
|
+
import chalk17 from "chalk";
|
|
16776
|
+
import { writeFileSync as writeFileSync14 } from "fs";
|
|
15504
16777
|
async function handleExploreWithoutKey(ctx, input) {
|
|
16778
|
+
if (isDefinitionAsk(input) && tryKeylessDefinitionAnswer(ctx, input)) {
|
|
16779
|
+
return;
|
|
16780
|
+
}
|
|
15505
16781
|
if (isKeylessVitalsAsk(input)) {
|
|
15506
16782
|
queuePendingAsk(ctx, input, "explore");
|
|
15507
16783
|
const answered = await tryKeylessAskAnswer(ctx, input);
|
|
@@ -15522,14 +16798,14 @@ async function handleExploreWithoutKey(ctx, input) {
|
|
|
15522
16798
|
}
|
|
15523
16799
|
if (hits === 1) {
|
|
15524
16800
|
console.log();
|
|
15525
|
-
console.log(" " +
|
|
16801
|
+
console.log(" " + chalk17.red("AI interpretation needs an LLM API key saved in config."));
|
|
15526
16802
|
console.log(
|
|
15527
|
-
" " +
|
|
16803
|
+
" " + chalk17.dim("Run ") + paint("accent", "/connect") + chalk17.dim(" and paste any provider's key (Anthropic, OpenAI, Groq, Gemini, ...).")
|
|
15528
16804
|
);
|
|
15529
|
-
console.log(" " +
|
|
16805
|
+
console.log(" " + chalk17.dim("Number crunching works without a key \u2014 only Q&A in the REPL needs one."));
|
|
15530
16806
|
if (ctx.pendingAsk) {
|
|
15531
16807
|
console.log(
|
|
15532
|
-
" " +
|
|
16808
|
+
" " + chalk17.dim("Your question is queued \u2014 I'll answer it right after ") + paint("accent", "/connect") + chalk17.dim(".")
|
|
15533
16809
|
);
|
|
15534
16810
|
}
|
|
15535
16811
|
if (ctx.gapAudit) {
|
|
@@ -15544,16 +16820,17 @@ async function handleExploreWithoutKey(ctx, input) {
|
|
|
15544
16820
|
return;
|
|
15545
16821
|
}
|
|
15546
16822
|
console.log();
|
|
15547
|
-
console.log(" " +
|
|
15548
|
-
console.log(" " +
|
|
15549
|
-
console.log(" " + paint("accent", "/
|
|
15550
|
-
console.log(" " +
|
|
15551
|
-
console.log(" " +
|
|
16823
|
+
console.log(" " + chalk17.yellow("Still no engine connected \u2014 Q&A stays offline until you run ") + paint("accent", "/connect") + chalk17.yellow("."));
|
|
16824
|
+
console.log(" " + chalk17.dim("These work without one:"));
|
|
16825
|
+
console.log(" " + paint("accent", "/deepdive") + chalk17.dim(" metric slides \u2014 what each number means"));
|
|
16826
|
+
console.log(" " + paint("accent", "/playbook") + chalk17.dim(" recommended plays from your computed vitals"));
|
|
16827
|
+
console.log(" " + chalk17.cyan('"how should we fix this?"') + chalk17.dim(" deterministic skeleton plan"));
|
|
16828
|
+
console.log(" " + paint("accent", "/handoff") + chalk17.dim(" export this analysis for another tool"));
|
|
15552
16829
|
console.log();
|
|
15553
16830
|
recordMessage(
|
|
15554
16831
|
ctx,
|
|
15555
16832
|
"agent",
|
|
15556
|
-
"No LLM engine connected \u2014 offered keyless paths (/playbook, skeleton plan, /handoff)."
|
|
16833
|
+
"No LLM engine connected \u2014 offered keyless paths (/deepdive, /playbook, skeleton plan, /handoff)."
|
|
15557
16834
|
);
|
|
15558
16835
|
}
|
|
15559
16836
|
var NO_KEY_NUDGES;
|
|
@@ -15561,7 +16838,7 @@ var init_orchestrator = __esm({
|
|
|
15561
16838
|
"src/conversation/orchestrator.ts"() {
|
|
15562
16839
|
"use strict";
|
|
15563
16840
|
init_context2();
|
|
15564
|
-
|
|
16841
|
+
init_exports_registry();
|
|
15565
16842
|
init_theme();
|
|
15566
16843
|
init_phase();
|
|
15567
16844
|
init_scope();
|
|
@@ -15573,6 +16850,7 @@ var init_orchestrator = __esm({
|
|
|
15573
16850
|
init_time_bank();
|
|
15574
16851
|
init_pending_ask();
|
|
15575
16852
|
init_keyless_ask();
|
|
16853
|
+
init_keyless_definitions();
|
|
15576
16854
|
NO_KEY_NUDGES = /* @__PURE__ */ Symbol.for("ntrp.noKeyNudges");
|
|
15577
16855
|
}
|
|
15578
16856
|
});
|
|
@@ -15738,8 +17016,8 @@ var init_bundle = __esm({
|
|
|
15738
17016
|
});
|
|
15739
17017
|
|
|
15740
17018
|
// src/repositories/markdown.ts
|
|
15741
|
-
import { mkdirSync as
|
|
15742
|
-
import { basename as
|
|
17019
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync15 } from "fs";
|
|
17020
|
+
import { basename as basename4, dirname as dirname3, join as join20, resolve as resolve8 } from "path";
|
|
15743
17021
|
import { stringify as stringifyYaml2 } from "yaml";
|
|
15744
17022
|
function renderMarkdownFiles(pkg) {
|
|
15745
17023
|
const bundleJson = JSON.stringify(pkg, null, 2) + "\n";
|
|
@@ -15926,10 +17204,10 @@ function renderStrategy(entry) {
|
|
|
15926
17204
|
].join("\n");
|
|
15927
17205
|
}
|
|
15928
17206
|
function getRootPath(target) {
|
|
15929
|
-
return
|
|
17207
|
+
return resolve8(target.directory ?? `ntrp-repository-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
15930
17208
|
}
|
|
15931
17209
|
function safeFilename(value) {
|
|
15932
|
-
return (
|
|
17210
|
+
return (basename4(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "strategy") + ".md";
|
|
15933
17211
|
}
|
|
15934
17212
|
function escapeSummary(value) {
|
|
15935
17213
|
return value.replace(/[<>]/g, "");
|
|
@@ -15942,7 +17220,7 @@ var init_markdown2 = __esm({
|
|
|
15942
17220
|
markdownRepositoryAdapter = {
|
|
15943
17221
|
kind: "markdown",
|
|
15944
17222
|
describeTarget(target) {
|
|
15945
|
-
return target.directory ? `local markdown folder ${
|
|
17223
|
+
return target.directory ? `local markdown folder ${resolve8(target.directory)}` : "local markdown folder";
|
|
15946
17224
|
},
|
|
15947
17225
|
planWrite(pkg) {
|
|
15948
17226
|
const files = renderMarkdownFiles(pkg);
|
|
@@ -15959,12 +17237,12 @@ var init_markdown2 = __esm({
|
|
|
15959
17237
|
write(pkg) {
|
|
15960
17238
|
const root = getRootPath(pkg.target);
|
|
15961
17239
|
const files = renderMarkdownFiles(pkg);
|
|
15962
|
-
|
|
17240
|
+
mkdirSync9(root, { recursive: true });
|
|
15963
17241
|
const written = [];
|
|
15964
17242
|
for (const file of files) {
|
|
15965
17243
|
const absolutePath = join20(root, file.relativePath);
|
|
15966
|
-
|
|
15967
|
-
|
|
17244
|
+
mkdirSync9(dirname3(absolutePath), { recursive: true });
|
|
17245
|
+
writeFileSync15(absolutePath, file.contents, "utf-8");
|
|
15968
17246
|
written.push(absolutePath);
|
|
15969
17247
|
}
|
|
15970
17248
|
return {
|
|
@@ -16200,7 +17478,7 @@ var nl_exports = {};
|
|
|
16200
17478
|
__export(nl_exports, {
|
|
16201
17479
|
runNaturalLanguage: () => runNaturalLanguage
|
|
16202
17480
|
});
|
|
16203
|
-
import
|
|
17481
|
+
import chalk18 from "chalk";
|
|
16204
17482
|
async function runNaturalLanguage(input, ctx) {
|
|
16205
17483
|
if (isSmokeProtocolTrigger(input)) {
|
|
16206
17484
|
recordMessage(ctx, "user", input);
|
|
@@ -16215,7 +17493,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16215
17493
|
return extractSummary(result.answer);
|
|
16216
17494
|
} catch (err) {
|
|
16217
17495
|
spinner2.fail("Smoke protocol failed");
|
|
16218
|
-
console.error(" " +
|
|
17496
|
+
console.error(" " + chalk18.red(String(err.message ?? err)));
|
|
16219
17497
|
console.log();
|
|
16220
17498
|
return;
|
|
16221
17499
|
}
|
|
@@ -16245,8 +17523,8 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16245
17523
|
spinner2.succeed(metricsFirst ? "Session context ready" : "Health snapshot ready");
|
|
16246
17524
|
} catch (err) {
|
|
16247
17525
|
spinner2.fail("Could not compute health snapshot");
|
|
16248
|
-
console.error(" " +
|
|
16249
|
-
console.log(" " +
|
|
17526
|
+
console.error(" " + chalk18.red(String(err.message ?? err)));
|
|
17527
|
+
console.log(" " + chalk18.dim("Run ") + paint("accent", "/new") + chalk18.dim(" \u2192 pick Demo to load sample data."));
|
|
16250
17528
|
console.log();
|
|
16251
17529
|
return;
|
|
16252
17530
|
}
|
|
@@ -16284,7 +17562,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16284
17562
|
break;
|
|
16285
17563
|
case "thinking":
|
|
16286
17564
|
spinner.stop();
|
|
16287
|
-
console.log(" " +
|
|
17565
|
+
console.log(" " + chalk18.dim.italic(event.text));
|
|
16288
17566
|
spinner.start("Thinking\u2026");
|
|
16289
17567
|
break;
|
|
16290
17568
|
case "answer":
|
|
@@ -16304,7 +17582,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16304
17582
|
}
|
|
16305
17583
|
} catch (err) {
|
|
16306
17584
|
spinner.fail("Error while investigating");
|
|
16307
|
-
console.error(" " +
|
|
17585
|
+
console.error(" " + chalk18.red(String(err.message ?? err)));
|
|
16308
17586
|
console.log();
|
|
16309
17587
|
return;
|
|
16310
17588
|
} finally {
|
|
@@ -16314,7 +17592,7 @@ async function runNaturalLanguage(input, ctx) {
|
|
|
16314
17592
|
ctx.conversation = distillThread(rawHistory);
|
|
16315
17593
|
}
|
|
16316
17594
|
if (!lastAnswer) {
|
|
16317
|
-
console.log(" " +
|
|
17595
|
+
console.log(" " + chalk18.dim("(no answer returned)"));
|
|
16318
17596
|
} else {
|
|
16319
17597
|
recordMessage(ctx, "agent", lastAnswer);
|
|
16320
17598
|
if (ctx.pendingAsk) {
|
|
@@ -16346,10 +17624,15 @@ function extractSummary(text) {
|
|
|
16346
17624
|
function printFindingInline(finding) {
|
|
16347
17625
|
const sev = finding.severity;
|
|
16348
17626
|
console.log();
|
|
16349
|
-
console.log(" " + severityPaint(sev)(`[${sev}]`) + " " +
|
|
17627
|
+
console.log(" " + severityPaint(sev)(`[${sev}]`) + " " + chalk18.bold(finding.segment));
|
|
16350
17628
|
printMarkdown(finding.finding, { indent: 2 });
|
|
16351
17629
|
const play = finding.recommended_plays?.[0];
|
|
16352
|
-
if (play) console.log(" " +
|
|
17630
|
+
if (play) console.log(" " + chalk18.dim("\u2192 " + play.play_name + " \u2014 " + play.rationale));
|
|
17631
|
+
if (finding.recommended_focus) {
|
|
17632
|
+
console.log(
|
|
17633
|
+
" " + chalk18.dim("How this works: ") + paint("accent", `/deepdive ${finding.recommended_focus}`)
|
|
17634
|
+
);
|
|
17635
|
+
}
|
|
16353
17636
|
}
|
|
16354
17637
|
var init_nl = __esm({
|
|
16355
17638
|
"src/cli/nl.ts"() {
|
|
@@ -16383,12 +17666,12 @@ __export(demo_exports, {
|
|
|
16383
17666
|
printDemoDisabled: () => printDemoDisabled,
|
|
16384
17667
|
setDemoEnabled: () => setDemoEnabled
|
|
16385
17668
|
});
|
|
16386
|
-
import
|
|
17669
|
+
import chalk19 from "chalk";
|
|
16387
17670
|
function printDemoDisabled() {
|
|
16388
17671
|
console.log();
|
|
16389
|
-
console.log(" " +
|
|
17672
|
+
console.log(" " + chalk19.red(DEMO_DISABLED_MESSAGE));
|
|
16390
17673
|
console.log(
|
|
16391
|
-
" " +
|
|
17674
|
+
" " + chalk19.dim("Re-enable with ") + paint("accent", "/config set demo-enabled true") + chalk19.dim(".")
|
|
16392
17675
|
);
|
|
16393
17676
|
console.log();
|
|
16394
17677
|
}
|
|
@@ -16430,7 +17713,7 @@ __export(pending_ask_exports, {
|
|
|
16430
17713
|
queuePendingAsk: () => queuePendingAsk,
|
|
16431
17714
|
resumePendingAsk: () => resumePendingAsk
|
|
16432
17715
|
});
|
|
16433
|
-
import
|
|
17716
|
+
import chalk20 from "chalk";
|
|
16434
17717
|
function looksLikeQuestion(input) {
|
|
16435
17718
|
const text = input.trim();
|
|
16436
17719
|
if (!text) return false;
|
|
@@ -16466,7 +17749,7 @@ function printFocusChip(ctx) {
|
|
|
16466
17749
|
const period = ctx.scope.time_horizon ? ` \xB7 ${ctx.scope.time_horizon}` : "";
|
|
16467
17750
|
console.log();
|
|
16468
17751
|
console.log(
|
|
16469
|
-
" " +
|
|
17752
|
+
" " + chalk20.dim("Focus: ") + paint("accent", lens) + chalk20.dim(period) + chalk20.dim(" \u2014 type ") + chalk20.cyan("adjust") + chalk20.dim(" to change")
|
|
16470
17753
|
);
|
|
16471
17754
|
console.log();
|
|
16472
17755
|
}
|
|
@@ -16477,7 +17760,7 @@ async function resumePendingAsk(ctx) {
|
|
|
16477
17760
|
if (canUseReplAi(ctx)) {
|
|
16478
17761
|
console.log();
|
|
16479
17762
|
console.log(
|
|
16480
|
-
" " +
|
|
17763
|
+
" " + chalk20.dim(
|
|
16481
17764
|
pending.keylessAnswered ? "Picking up your question with the connected engine\u2026" : "Picking up your question\u2026"
|
|
16482
17765
|
)
|
|
16483
17766
|
);
|
|
@@ -16510,7 +17793,7 @@ async function offerDemoToAnswer(ctx) {
|
|
|
16510
17793
|
const go = await prompts.confirm("Use demo data to answer this?", true);
|
|
16511
17794
|
if (!go) {
|
|
16512
17795
|
console.log(
|
|
16513
|
-
" " +
|
|
17796
|
+
" " + chalk20.dim("Paste a CSV path when ready, or say ") + chalk20.cyan("use demo data") + chalk20.dim(".")
|
|
16514
17797
|
);
|
|
16515
17798
|
console.log();
|
|
16516
17799
|
return false;
|
|
@@ -16542,7 +17825,7 @@ __export(compute_exports2, {
|
|
|
16542
17825
|
isComputeIntent: () => isComputeIntent,
|
|
16543
17826
|
runConversationCompute: () => runConversationCompute
|
|
16544
17827
|
});
|
|
16545
|
-
import
|
|
17828
|
+
import chalk21 from "chalk";
|
|
16546
17829
|
async function runConversationCompute(ctx) {
|
|
16547
17830
|
const lens = ctx.scope?.primary_lens ?? ctx.analysis.primary;
|
|
16548
17831
|
ctx.computeInProgress = true;
|
|
@@ -16597,7 +17880,7 @@ async function runConversationCompute(ctx) {
|
|
|
16597
17880
|
creditGapCompute(ctx);
|
|
16598
17881
|
return typeof summary === "string" ? summary : "Health analysis ready";
|
|
16599
17882
|
} catch (err) {
|
|
16600
|
-
console.error(" " +
|
|
17883
|
+
console.error(" " + chalk21.red(String(err.message ?? err)));
|
|
16601
17884
|
return;
|
|
16602
17885
|
} finally {
|
|
16603
17886
|
ctx.computeInProgress = false;
|
|
@@ -19366,18 +20649,18 @@ var init_generator = __esm({
|
|
|
19366
20649
|
});
|
|
19367
20650
|
|
|
19368
20651
|
// src/demo/taxonomy-cache.ts
|
|
19369
|
-
import { readFileSync as
|
|
19370
|
-
import { homedir as
|
|
20652
|
+
import { readFileSync as readFileSync18, writeFileSync as writeFileSync16, existsSync as existsSync19, mkdirSync as mkdirSync10, unlinkSync as unlinkSync3 } from "fs";
|
|
20653
|
+
import { homedir as homedir6 } from "os";
|
|
19371
20654
|
import { join as join22 } from "path";
|
|
19372
20655
|
function ensureDir5() {
|
|
19373
|
-
if (!
|
|
19374
|
-
|
|
20656
|
+
if (!existsSync19(NTRP_DIR4)) {
|
|
20657
|
+
mkdirSync10(NTRP_DIR4, { recursive: true });
|
|
19375
20658
|
}
|
|
19376
20659
|
}
|
|
19377
20660
|
function loadCachedTaxonomy(profile) {
|
|
19378
|
-
if (!
|
|
20661
|
+
if (!existsSync19(TAXONOMY_PATH)) return null;
|
|
19379
20662
|
try {
|
|
19380
|
-
const parsed = JSON.parse(
|
|
20663
|
+
const parsed = JSON.parse(readFileSync18(TAXONOMY_PATH, "utf-8"));
|
|
19381
20664
|
if (!parsed || typeof parsed !== "object") return null;
|
|
19382
20665
|
if (parsed.profile_updated_at !== profile.updated_at) return null;
|
|
19383
20666
|
return parsed;
|
|
@@ -19387,13 +20670,13 @@ function loadCachedTaxonomy(profile) {
|
|
|
19387
20670
|
}
|
|
19388
20671
|
function saveCachedTaxonomy(taxonomy) {
|
|
19389
20672
|
ensureDir5();
|
|
19390
|
-
|
|
20673
|
+
writeFileSync16(TAXONOMY_PATH, JSON.stringify(taxonomy, null, 2) + "\n");
|
|
19391
20674
|
}
|
|
19392
20675
|
var NTRP_DIR4, TAXONOMY_PATH;
|
|
19393
20676
|
var init_taxonomy_cache = __esm({
|
|
19394
20677
|
"src/demo/taxonomy-cache.ts"() {
|
|
19395
20678
|
"use strict";
|
|
19396
|
-
NTRP_DIR4 = join22(
|
|
20679
|
+
NTRP_DIR4 = join22(homedir6(), ".ntrp");
|
|
19397
20680
|
TAXONOMY_PATH = join22(NTRP_DIR4, "demo-taxonomy.json");
|
|
19398
20681
|
}
|
|
19399
20682
|
});
|
|
@@ -19630,16 +20913,16 @@ var generate_exports = {};
|
|
|
19630
20913
|
__export(generate_exports, {
|
|
19631
20914
|
handler: () => handler2
|
|
19632
20915
|
});
|
|
19633
|
-
import
|
|
20916
|
+
import chalk22 from "chalk";
|
|
19634
20917
|
async function handler2(args, ctx) {
|
|
19635
20918
|
const { flags } = parseArgs(args, ["list-scenarios", "regen-taxonomy", "brief"]);
|
|
19636
20919
|
const quiet = ctx.execution.quiet;
|
|
19637
20920
|
const brief = getBool(flags, "brief");
|
|
19638
20921
|
if (getBool(flags, "list-scenarios")) {
|
|
19639
|
-
console.log(
|
|
20922
|
+
console.log(chalk22.bold("\n Available Scenarios:\n"));
|
|
19640
20923
|
for (const s of SCENARIO_LIST) {
|
|
19641
|
-
console.log(` ${
|
|
19642
|
-
console.log(` ${
|
|
20924
|
+
console.log(` ${chalk22.cyan(s.key.padEnd(20))} ${s.label}`);
|
|
20925
|
+
console.log(` ${chalk22.dim(" ".repeat(20))} ${s.description}
|
|
19643
20926
|
`);
|
|
19644
20927
|
}
|
|
19645
20928
|
return true;
|
|
@@ -19649,9 +20932,9 @@ async function handler2(args, ctx) {
|
|
|
19649
20932
|
const skipProfile = getFalse(flags, "profile");
|
|
19650
20933
|
if (!isProfileConfigured(profile) && !skipProfile) {
|
|
19651
20934
|
console.error();
|
|
19652
|
-
console.error(" " +
|
|
19653
|
-
console.error(" " +
|
|
19654
|
-
console.error(" " +
|
|
20935
|
+
console.error(" " + chalk22.red("No company profile found."));
|
|
20936
|
+
console.error(" " + chalk22.dim("Run ") + paint("accent", "/onboard") + chalk22.dim(" first for a richer demo,"));
|
|
20937
|
+
console.error(" " + chalk22.dim("or pass ") + paint("accent", "--no-profile") + chalk22.dim(" to skip."));
|
|
19655
20938
|
console.error();
|
|
19656
20939
|
markFailure(ctx);
|
|
19657
20940
|
return false;
|
|
@@ -19659,8 +20942,8 @@ async function handler2(args, ctx) {
|
|
|
19659
20942
|
const explicitScenario = getString(flags, "scenario", "s");
|
|
19660
20943
|
const resolvedScenario = resolveScenarioInput(explicitScenario);
|
|
19661
20944
|
if (resolvedScenario === null) {
|
|
19662
|
-
console.error(
|
|
19663
|
-
console.log(
|
|
20945
|
+
console.error(chalk22.red(` Unknown scenario: ${explicitScenario}`));
|
|
20946
|
+
console.log(chalk22.dim(` Valid: ${SCENARIO_LIST.map((s) => s.key).join(", ")}`));
|
|
19664
20947
|
markFailure(ctx);
|
|
19665
20948
|
return false;
|
|
19666
20949
|
}
|
|
@@ -19674,10 +20957,10 @@ async function handler2(args, ctx) {
|
|
|
19674
20957
|
const s = getScenario(scenario);
|
|
19675
20958
|
console.log();
|
|
19676
20959
|
if (brief) {
|
|
19677
|
-
console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) +
|
|
20960
|
+
console.log(" " + paint("accent", "\u2713 Demo: ") + bold(s.label) + chalk22.dim(" \u2014 " + s.hook));
|
|
19678
20961
|
} else {
|
|
19679
20962
|
console.log(" " + paint("accent", "\u2713 Scenario: ") + bold(s.label));
|
|
19680
|
-
console.log(" " +
|
|
20963
|
+
console.log(" " + chalk22.dim(s.story));
|
|
19681
20964
|
console.log();
|
|
19682
20965
|
}
|
|
19683
20966
|
}
|
|
@@ -19707,18 +20990,18 @@ async function handler2(args, ctx) {
|
|
|
19707
20990
|
if (brief) {
|
|
19708
20991
|
spinner.succeed(`Demo loaded \u2014 ${briefCounts(result.counts)}`);
|
|
19709
20992
|
} else {
|
|
19710
|
-
spinner.succeed(`Generated demo data for "${
|
|
20993
|
+
spinner.succeed(`Generated demo data for "${chalk22.cyan(scenario)}" scenario`);
|
|
19711
20994
|
console.log();
|
|
19712
20995
|
printEntityCounts(result.counts);
|
|
19713
20996
|
}
|
|
19714
20997
|
}
|
|
19715
20998
|
if (!quiet && !brief && ctx.analysis.primary !== "revenue_metrics") {
|
|
19716
|
-
console.log(
|
|
20999
|
+
console.log(chalk22.dim("\n Run /diagnose to compute vital signs.\n"));
|
|
19717
21000
|
}
|
|
19718
21001
|
}
|
|
19719
21002
|
} catch (err) {
|
|
19720
21003
|
if (spinner) spinner.fail("Generation failed");
|
|
19721
|
-
console.error(
|
|
21004
|
+
console.error(chalk22.red(String(err)));
|
|
19722
21005
|
markFailure(ctx);
|
|
19723
21006
|
return false;
|
|
19724
21007
|
}
|
|
@@ -19756,7 +21039,7 @@ async function loadOrBuildTaxonomy(profile, forceRegen, ctx) {
|
|
|
19756
21039
|
return taxonomy;
|
|
19757
21040
|
} catch (err) {
|
|
19758
21041
|
spinner.fail("Couldn't build market taxonomy \u2014 using generic data pools");
|
|
19759
|
-
console.log(" " +
|
|
21042
|
+
console.log(" " + chalk22.dim(String(err.message ?? err)));
|
|
19760
21043
|
return void 0;
|
|
19761
21044
|
}
|
|
19762
21045
|
}
|
|
@@ -19847,9 +21130,9 @@ var ingest_exports = {};
|
|
|
19847
21130
|
__export(ingest_exports, {
|
|
19848
21131
|
handler: () => handler3
|
|
19849
21132
|
});
|
|
19850
|
-
import
|
|
19851
|
-
import { readFileSync as
|
|
19852
|
-
import { basename as
|
|
21133
|
+
import chalk23 from "chalk";
|
|
21134
|
+
import { readFileSync as readFileSync19, existsSync as existsSync20 } from "fs";
|
|
21135
|
+
import { basename as basename5 } from "path";
|
|
19853
21136
|
async function handler3(args, ctx) {
|
|
19854
21137
|
const { positional, flags } = parseArgs(args, [
|
|
19855
21138
|
"skip-resolve",
|
|
@@ -19868,21 +21151,21 @@ async function handler3(args, ctx) {
|
|
|
19868
21151
|
const source = getString(flags, "source", "s") ?? "salesforce";
|
|
19869
21152
|
const skipResolve = getBool(flags, "skip-resolve");
|
|
19870
21153
|
if (!file) {
|
|
19871
|
-
console.error(
|
|
19872
|
-
console.error(
|
|
21154
|
+
console.error(chalk23.red(" Usage: /ingest <file> [--source salesforce|hubspot|outreach]"));
|
|
21155
|
+
console.error(chalk23.dim(" /ingest --demo [--scenario <name>]"));
|
|
19873
21156
|
process.exit(1);
|
|
19874
21157
|
}
|
|
19875
|
-
if (!
|
|
19876
|
-
console.error(
|
|
21158
|
+
if (!existsSync20(file)) {
|
|
21159
|
+
console.error(chalk23.red(` File not found: ${file}`));
|
|
19877
21160
|
process.exit(1);
|
|
19878
21161
|
}
|
|
19879
21162
|
const profile = loadProfile();
|
|
19880
21163
|
const skipProfile = getFalse(flags, "profile");
|
|
19881
21164
|
if (!profile && !skipProfile) {
|
|
19882
21165
|
console.error();
|
|
19883
|
-
console.error(" " +
|
|
19884
|
-
console.error(" " +
|
|
19885
|
-
console.error(" " +
|
|
21166
|
+
console.error(" " + chalk23.red("No company profile found."));
|
|
21167
|
+
console.error(" " + chalk23.dim("Run ") + paint("accent", "/onboard") + chalk23.dim(" first for better column mapping,"));
|
|
21168
|
+
console.error(" " + chalk23.dim("or pass ") + paint("accent", "--no-profile") + chalk23.dim(" to skip."));
|
|
19886
21169
|
console.error();
|
|
19887
21170
|
process.exit(1);
|
|
19888
21171
|
}
|
|
@@ -19890,7 +21173,7 @@ async function handler3(args, ctx) {
|
|
|
19890
21173
|
try {
|
|
19891
21174
|
await initSchema();
|
|
19892
21175
|
spinner.text = "Parsing CSV\u2026";
|
|
19893
|
-
const content =
|
|
21176
|
+
const content = readFileSync19(file, "utf-8");
|
|
19894
21177
|
const { rows, headers } = parseCSV(content);
|
|
19895
21178
|
if (rows.length === 0) {
|
|
19896
21179
|
spinner.fail("CSV is empty");
|
|
@@ -19902,7 +21185,7 @@ async function handler3(args, ctx) {
|
|
|
19902
21185
|
const { importRevenueRows: importRevenueRows2 } = await Promise.resolve().then(() => (init_revenue_importer(), revenue_importer_exports));
|
|
19903
21186
|
const uploadId2 = await insertCSVUpload({
|
|
19904
21187
|
source_system: source,
|
|
19905
|
-
original_filename:
|
|
21188
|
+
original_filename: basename5(file),
|
|
19906
21189
|
row_count: rows.length,
|
|
19907
21190
|
column_mappings: { entity_type: "revenue_ledger" },
|
|
19908
21191
|
status: "processing"
|
|
@@ -19914,28 +21197,28 @@ async function handler3(args, ctx) {
|
|
|
19914
21197
|
row_count: result2.imported
|
|
19915
21198
|
});
|
|
19916
21199
|
spinner.succeed(
|
|
19917
|
-
`Imported ${
|
|
21200
|
+
`Imported ${chalk23.bold(result2.imported.toString())} revenue events from ${chalk23.dim(basename5(file))}`
|
|
19918
21201
|
);
|
|
19919
21202
|
if (result2.errors.length > 0) {
|
|
19920
|
-
console.log(
|
|
21203
|
+
console.log(chalk23.yellow(` ${result2.errors.length} rows skipped`));
|
|
19921
21204
|
}
|
|
19922
21205
|
if (ctx.analysis) {
|
|
19923
21206
|
ctx.analysis.data_source_type = "revenue_ledger";
|
|
19924
21207
|
}
|
|
19925
|
-
console.log(
|
|
19926
|
-
return `${result2.imported} revenue events from ${
|
|
21208
|
+
console.log(chalk23.dim(" Run ") + chalk23.cyan("/metrics") + chalk23.dim(" for SaaS metrics with ledger-backed retention."));
|
|
21209
|
+
return `${result2.imported} revenue events from ${basename5(file)}`;
|
|
19927
21210
|
}
|
|
19928
21211
|
spinner.text = "Detecting entity type\u2026";
|
|
19929
21212
|
const detection = detectEntityType(headers, source);
|
|
19930
21213
|
if (!detection) {
|
|
19931
21214
|
spinner.fail(`Could not auto-detect entity type for source: ${source}`);
|
|
19932
|
-
console.log(
|
|
21215
|
+
console.log(chalk23.dim(" Headers found: " + headers.join(", ")));
|
|
19933
21216
|
process.exit(1);
|
|
19934
21217
|
}
|
|
19935
21218
|
spinner.text = `Importing ${rows.length} ${detection.entityType} rows...`;
|
|
19936
21219
|
const uploadId = await insertCSVUpload({
|
|
19937
21220
|
source_system: source,
|
|
19938
|
-
original_filename:
|
|
21221
|
+
original_filename: basename5(file),
|
|
19939
21222
|
row_count: rows.length,
|
|
19940
21223
|
column_mappings: detection.mappings,
|
|
19941
21224
|
status: "processing"
|
|
@@ -19952,15 +21235,15 @@ async function handler3(args, ctx) {
|
|
|
19952
21235
|
row_count: result.imported
|
|
19953
21236
|
});
|
|
19954
21237
|
spinner.succeed(
|
|
19955
|
-
`Imported ${
|
|
21238
|
+
`Imported ${chalk23.bold(result.imported.toString())} ${detection.entityType} from ${chalk23.dim(basename5(file))} (${source})`
|
|
19956
21239
|
);
|
|
19957
21240
|
if (result.errors.length > 0) {
|
|
19958
|
-
console.log(
|
|
21241
|
+
console.log(chalk23.yellow(` ${result.errors.length} rows skipped`));
|
|
19959
21242
|
for (const err of result.errors.slice(0, 3)) {
|
|
19960
|
-
console.log(
|
|
21243
|
+
console.log(chalk23.dim(` - ${err}`));
|
|
19961
21244
|
}
|
|
19962
21245
|
if (result.errors.length > 3) {
|
|
19963
|
-
console.log(
|
|
21246
|
+
console.log(chalk23.dim(` ... and ${result.errors.length - 3} more`));
|
|
19964
21247
|
}
|
|
19965
21248
|
}
|
|
19966
21249
|
if (!skipResolve) {
|
|
@@ -19974,10 +21257,10 @@ async function handler3(args, ctx) {
|
|
|
19974
21257
|
resolveSpinner.succeed("No duplicates found");
|
|
19975
21258
|
}
|
|
19976
21259
|
}
|
|
19977
|
-
return `${result.imported} ${detection.entityType} from ${
|
|
21260
|
+
return `${result.imported} ${detection.entityType} from ${basename5(file)}`;
|
|
19978
21261
|
} catch (err) {
|
|
19979
21262
|
spinner.fail("Import failed");
|
|
19980
|
-
console.error(
|
|
21263
|
+
console.error(chalk23.red(String(err)));
|
|
19981
21264
|
process.exit(1);
|
|
19982
21265
|
}
|
|
19983
21266
|
}
|
|
@@ -20007,10 +21290,10 @@ __export(ingest_chat_exports, {
|
|
|
20007
21290
|
loadDemoFromChat: () => loadDemoFromChat,
|
|
20008
21291
|
looksLikeFilePath: () => looksLikeFilePath
|
|
20009
21292
|
});
|
|
20010
|
-
import { existsSync as
|
|
20011
|
-
import { basename as
|
|
20012
|
-
import { homedir as
|
|
20013
|
-
import
|
|
21293
|
+
import { existsSync as existsSync21 } from "fs";
|
|
21294
|
+
import { basename as basename6, resolve as resolve9 } from "path";
|
|
21295
|
+
import { homedir as homedir7 } from "os";
|
|
21296
|
+
import chalk24 from "chalk";
|
|
20014
21297
|
function extractFilePath(input) {
|
|
20015
21298
|
const trimmed = input.trim();
|
|
20016
21299
|
const patterns = [
|
|
@@ -20027,33 +21310,33 @@ function extractFilePath(input) {
|
|
|
20027
21310
|
const m = trimmed.match(re);
|
|
20028
21311
|
if (m?.[1]) {
|
|
20029
21312
|
const p = expandPath(m[1]);
|
|
20030
|
-
if (
|
|
21313
|
+
if (existsSync21(p)) return p;
|
|
20031
21314
|
}
|
|
20032
21315
|
if (!m?.[1] && re.test(trimmed) && trimmed.toLowerCase().endsWith(".csv")) {
|
|
20033
21316
|
const p = expandPath(trimmed.replace(/^["']|["']$/g, ""));
|
|
20034
|
-
if (
|
|
21317
|
+
if (existsSync21(p)) return p;
|
|
20035
21318
|
}
|
|
20036
21319
|
}
|
|
20037
21320
|
return null;
|
|
20038
21321
|
}
|
|
20039
21322
|
function expandPath(p) {
|
|
20040
|
-
if (p.startsWith("~/")) return
|
|
20041
|
-
return
|
|
21323
|
+
if (p.startsWith("~/")) return resolve9(homedir7(), p.slice(2));
|
|
21324
|
+
return resolve9(p);
|
|
20042
21325
|
}
|
|
20043
21326
|
function looksLikeFilePath(input) {
|
|
20044
21327
|
return extractFilePath(input) !== null;
|
|
20045
21328
|
}
|
|
20046
21329
|
async function ingestFromChat(ctx, filePath) {
|
|
20047
21330
|
if (!ctx.rl) {
|
|
20048
|
-
console.log(" " +
|
|
21331
|
+
console.log(" " + chalk24.red("Ingest confirm requires interactive mode."));
|
|
20049
21332
|
return false;
|
|
20050
21333
|
}
|
|
20051
|
-
const name =
|
|
21334
|
+
const name = basename6(filePath);
|
|
20052
21335
|
const prompts = createPromptSession(ctx.rl, ctx);
|
|
20053
21336
|
try {
|
|
20054
21337
|
const ok = await prompts.confirm(`Ingest ${name} as CRM export?`, true);
|
|
20055
21338
|
if (!ok) {
|
|
20056
|
-
console.log(" " +
|
|
21339
|
+
console.log(" " + chalk24.dim("Ingest cancelled."));
|
|
20057
21340
|
return false;
|
|
20058
21341
|
}
|
|
20059
21342
|
} finally {
|
|
@@ -20061,12 +21344,12 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20061
21344
|
}
|
|
20062
21345
|
const { handler: ingest } = await Promise.resolve().then(() => (init_ingest(), ingest_exports));
|
|
20063
21346
|
const { detectEntityType: detectEntityType2 } = await Promise.resolve().then(() => (init_csv_detect(), csv_detect_exports));
|
|
20064
|
-
const { readFileSync:
|
|
21347
|
+
const { readFileSync: readFileSync22 } = await import("fs");
|
|
20065
21348
|
const { parseCSV: parseCSV2 } = await Promise.resolve().then(() => (init_csv_parse(), csv_parse_exports));
|
|
20066
21349
|
const { getStoredApiKey } = await Promise.resolve().then(() => (init_repl_api(), repl_api_exports));
|
|
20067
21350
|
let headerCheckFailed = false;
|
|
20068
21351
|
try {
|
|
20069
|
-
const raw =
|
|
21352
|
+
const raw = readFileSync22(filePath, "utf-8");
|
|
20070
21353
|
const { headers } = parseCSV2(raw);
|
|
20071
21354
|
const detected = detectEntityType2(headers, "unknown");
|
|
20072
21355
|
if (!detected) headerCheckFailed = true;
|
|
@@ -20081,7 +21364,7 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20081
21364
|
false
|
|
20082
21365
|
);
|
|
20083
21366
|
if (useAi) {
|
|
20084
|
-
console.log(" " +
|
|
21367
|
+
console.log(" " + chalk24.dim("AI column mapping is not wired to ingest yet \u2014 trying standard ingest."));
|
|
20085
21368
|
}
|
|
20086
21369
|
} finally {
|
|
20087
21370
|
prompts2.close();
|
|
@@ -20108,7 +21391,7 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20108
21391
|
invalidateGapAudit(ctx);
|
|
20109
21392
|
saveSessionState(ctx);
|
|
20110
21393
|
console.log();
|
|
20111
|
-
console.log(" " + paint("accent", "\u2713 Data loaded") +
|
|
21394
|
+
console.log(" " + paint("accent", "\u2713 Data loaded") + chalk24.dim(` \u2014 ${name}`));
|
|
20112
21395
|
recordMessage(ctx, "user", `[ingested ${name}]`);
|
|
20113
21396
|
recordMessage(ctx, "agent", `Loaded ${name}. Checking what we can analyze\u2026`);
|
|
20114
21397
|
const audit = await refreshGapAudit(ctx);
|
|
@@ -20116,7 +21399,7 @@ async function ingestFromChat(ctx, filePath) {
|
|
|
20116
21399
|
if (audit.can_compute && ctx.scope?.confirmed_at) {
|
|
20117
21400
|
if (ctx.pendingAsk) {
|
|
20118
21401
|
console.log();
|
|
20119
|
-
console.log(" " +
|
|
21402
|
+
console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
|
|
20120
21403
|
await runConversationCompute(ctx);
|
|
20121
21404
|
return true;
|
|
20122
21405
|
}
|
|
@@ -20178,7 +21461,7 @@ async function loadDemoFromChat(ctx, scenario, opts = {}) {
|
|
|
20178
21461
|
const shouldAuto = opts.autoCompute || Boolean(ctx.pendingAsk && audit.can_compute && ctx.scope?.confirmed_at);
|
|
20179
21462
|
if (shouldAuto && audit.can_compute) {
|
|
20180
21463
|
console.log();
|
|
20181
|
-
console.log(" " +
|
|
21464
|
+
console.log(" " + chalk24.dim("Computing so I can answer\u2026"));
|
|
20182
21465
|
await runConversationCompute(ctx);
|
|
20183
21466
|
return true;
|
|
20184
21467
|
}
|
|
@@ -20667,9 +21950,9 @@ async function handleGetSessionBrief(input) {
|
|
|
20667
21950
|
if (!target) {
|
|
20668
21951
|
return { error: `No session matching "${raw}".` };
|
|
20669
21952
|
}
|
|
20670
|
-
const { existsSync:
|
|
21953
|
+
const { existsSync: existsSync24, readFileSync: readFileSync22 } = await import("fs");
|
|
20671
21954
|
const briefPath = contextDocPathForSession2(target.id);
|
|
20672
|
-
if (!
|
|
21955
|
+
if (!existsSync24(briefPath)) {
|
|
20673
21956
|
return {
|
|
20674
21957
|
session_id: target.id,
|
|
20675
21958
|
error: "No context brief on disk for this session (created before brief storage existed).",
|
|
@@ -20677,7 +21960,7 @@ async function handleGetSessionBrief(input) {
|
|
|
20677
21960
|
stage: target.stage ?? null
|
|
20678
21961
|
};
|
|
20679
21962
|
}
|
|
20680
|
-
return { session_id: target.id, brief:
|
|
21963
|
+
return { session_id: target.id, brief: readFileSync22(briefPath, "utf-8") };
|
|
20681
21964
|
}
|
|
20682
21965
|
function auditDenied(name, input, resultJson, start) {
|
|
20683
21966
|
logToolCall({
|
|
@@ -21659,15 +22942,15 @@ var init_verify = __esm({
|
|
|
21659
22942
|
});
|
|
21660
22943
|
|
|
21661
22944
|
// src/services/setup.ts
|
|
21662
|
-
import { existsSync as
|
|
22945
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync20, writeFileSync as writeFileSync17 } from "fs";
|
|
21663
22946
|
import { join as join23 } from "path";
|
|
21664
22947
|
function setupCheck() {
|
|
21665
22948
|
const home = ntrpHome();
|
|
21666
22949
|
let writable = false;
|
|
21667
22950
|
try {
|
|
21668
|
-
|
|
22951
|
+
mkdirSync11(home, { recursive: true });
|
|
21669
22952
|
const probe = join23(home, ".write-check");
|
|
21670
|
-
|
|
22953
|
+
writeFileSync17(probe, "ok\n");
|
|
21671
22954
|
writable = true;
|
|
21672
22955
|
} catch {
|
|
21673
22956
|
writable = false;
|
|
@@ -21685,6 +22968,7 @@ function setupCheck() {
|
|
|
21685
22968
|
api_key_env_hint: hasEnvApiKeyHint(),
|
|
21686
22969
|
default_format: getConfigValue("default-format"),
|
|
21687
22970
|
export_dir: getConfigValue("export-dir"),
|
|
22971
|
+
ai_inbox_dir: getConfigValue("ai-inbox-dir"),
|
|
21688
22972
|
llm: {
|
|
21689
22973
|
configured: hasAnyLlmProvider(),
|
|
21690
22974
|
primary: llmCfg.primary,
|
|
@@ -21717,17 +23001,17 @@ var init_setup = __esm({
|
|
|
21717
23001
|
});
|
|
21718
23002
|
|
|
21719
23003
|
// src/version.ts
|
|
21720
|
-
import { existsSync as
|
|
21721
|
-
import { dirname as
|
|
23004
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
|
|
23005
|
+
import { dirname as dirname4, join as join24 } from "path";
|
|
21722
23006
|
import { fileURLToPath } from "url";
|
|
21723
23007
|
function getInstalledVersion() {
|
|
21724
23008
|
if (cachedVersion) return cachedVersion;
|
|
21725
|
-
const start =
|
|
23009
|
+
const start = dirname4(fileURLToPath(import.meta.url));
|
|
21726
23010
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
21727
23011
|
const path = join24(start, rel);
|
|
21728
|
-
if (!
|
|
23012
|
+
if (!existsSync23(path)) continue;
|
|
21729
23013
|
try {
|
|
21730
|
-
const pkg = JSON.parse(
|
|
23014
|
+
const pkg = JSON.parse(readFileSync21(path, "utf-8"));
|
|
21731
23015
|
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
21732
23016
|
cachedVersion = pkg.version;
|
|
21733
23017
|
return cachedVersion;
|