@sonnechasser/ntrp 1.3.5 → 1.3.6

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.
@@ -1,1205 +0,0 @@
1
- #!/usr/bin/env node
2
- process.noDeprecation = true;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
-
8
- // src/config/store.ts
9
- import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
10
- import { homedir } from "os";
11
- import { join, resolve } from "path";
12
- function ntrpHome() {
13
- return NTRP_DIR;
14
- }
15
- function chmodQuiet(path, mode) {
16
- try {
17
- chmodSync(path, mode);
18
- } catch {
19
- }
20
- }
21
- function ensureDir() {
22
- if (!existsSync(NTRP_DIR)) {
23
- mkdirSync(NTRP_DIR, { recursive: true, mode: 448 });
24
- }
25
- chmodQuiet(NTRP_DIR, 448);
26
- }
27
- function writePrivateFile(path, contents) {
28
- ensureDir();
29
- writeFileSync(path, contents, { encoding: "utf-8", mode: 384 });
30
- chmodQuiet(path, 384);
31
- }
32
- function loadConfig() {
33
- if (cachedConfig) return cachedConfig;
34
- ensureDir();
35
- if (!existsSync(CONFIG_PATH)) {
36
- cachedConfig = {};
37
- return cachedConfig;
38
- }
39
- try {
40
- cachedConfig = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
41
- } catch {
42
- cachedConfig = {};
43
- }
44
- return cachedConfig;
45
- }
46
- function saveConfig(config) {
47
- ensureDir();
48
- writePrivateFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
49
- cachedConfig = config;
50
- }
51
- function resetConfigCache() {
52
- cachedConfig = null;
53
- }
54
- function setConfigValue(key, value) {
55
- const config = loadConfig();
56
- config[key] = value;
57
- saveConfig(config);
58
- }
59
- function getExportsDir() {
60
- const config = loadConfig();
61
- const dir = resolve(config["export-dir"] ?? join(NTRP_DIR, "exports"));
62
- if (!existsSync(dir)) {
63
- mkdirSync(dir, { recursive: true });
64
- }
65
- return dir;
66
- }
67
- function getConfiguredAiInboxDir() {
68
- const raw = loadConfig()["ai-inbox-dir"];
69
- return raw ? resolve(raw) : null;
70
- }
71
- var NTRP_DIR, CONFIG_PATH, cachedConfig;
72
- var init_store = __esm({
73
- "src/config/store.ts"() {
74
- "use strict";
75
- NTRP_DIR = process.env.NTRP_HOME ? resolve(process.env.NTRP_HOME) : join(homedir(), ".ntrp");
76
- CONFIG_PATH = join(NTRP_DIR, "config.json");
77
- cachedConfig = null;
78
- }
79
- });
80
-
81
- // src/services/terminal-capture.ts
82
- function redactSecrets(line) {
83
- let out = line;
84
- for (const pattern of SECRET_PATTERNS) {
85
- out = out.replace(pattern, (m) => `${m.slice(0, 6)}\u2026[redacted]`);
86
- }
87
- return out;
88
- }
89
- var SECRET_PATTERNS;
90
- var init_terminal_capture = __esm({
91
- "src/services/terminal-capture.ts"() {
92
- "use strict";
93
- SECRET_PATTERNS = [
94
- /\bsk-ant-[A-Za-z0-9_-]{8,}/g,
95
- // Anthropic
96
- /\bsk-or-[A-Za-z0-9_-]{8,}/g,
97
- // OpenRouter
98
- /\bsk-proj-[A-Za-z0-9_-]{8,}/g,
99
- // OpenAI project keys
100
- /\bsk-[A-Za-z0-9_-]{20,}/g,
101
- // OpenAI / generic sk-
102
- /\bgsk_[A-Za-z0-9_-]{8,}/g,
103
- // Groq
104
- /\bxai-[A-Za-z0-9_-]{8,}/g,
105
- // xAI
106
- /\bfw_[A-Za-z0-9_-]{8,}/g,
107
- // Fireworks
108
- /\bAIza[A-Za-z0-9_-]{10,}/g,
109
- // Google
110
- /\bNTRP-[A-Z0-9][A-Z0-9-]{8,}/g
111
- // license keys
112
- ];
113
- }
114
- });
115
-
116
- // src/output/formatters.ts
117
- function formatCurrency(value) {
118
- if (value >= 1e6) return `$${(value / 1e6).toFixed(1)}M`;
119
- if (value >= 1e3) return `$${(value / 1e3).toFixed(0)}K`;
120
- return `$${value.toFixed(0)}`;
121
- }
122
- var VITAL_SIGN_LABELS;
123
- var init_formatters = __esm({
124
- "src/output/formatters.ts"() {
125
- "use strict";
126
- VITAL_SIGN_LABELS = {
127
- freshness: "Freshness",
128
- flow_rate: "Flow Rate",
129
- drop_rate: "Drop Rate",
130
- signal_to_noise: "Signal:Noise",
131
- thread_depth: "Thread Depth"
132
- };
133
- }
134
- });
135
-
136
- // src/ui/theme.ts
137
- import chalk from "chalk";
138
- var STATUS, TOKENS, BADGE_TONE_COLORS;
139
- var init_theme = __esm({
140
- "src/ui/theme.ts"() {
141
- "use strict";
142
- init_formatters();
143
- STATUS = {
144
- green: "#22c55e",
145
- yellow: "#eab308",
146
- red: "#ef4444",
147
- neutral: "#64748b"
148
- };
149
- TOKENS = {
150
- accent: "#14b8a6",
151
- accentBright: "#2dd4bf",
152
- border: "#334155",
153
- borderMuted: "#1e293b",
154
- dim: "#64748b",
155
- text: "#e2e8f0",
156
- info: "#3b82f6",
157
- ...STATUS,
158
- success: STATUS.green,
159
- warning: STATUS.yellow,
160
- error: STATUS.red
161
- };
162
- BADGE_TONE_COLORS = {
163
- success: TOKENS.success,
164
- warning: TOKENS.warning,
165
- error: TOKENS.error,
166
- info: TOKENS.info,
167
- accent: TOKENS.accent
168
- };
169
- }
170
- });
171
-
172
- // src/services/exports-registry-smoke.ts
173
- init_store();
174
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readdirSync as readdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
175
- import { join as join6 } from "path";
176
-
177
- // src/services/exports-registry.ts
178
- import {
179
- appendFileSync,
180
- copyFileSync,
181
- cpSync,
182
- existsSync as existsSync2,
183
- mkdirSync as mkdirSync4,
184
- readFileSync as readFileSync2,
185
- readdirSync,
186
- renameSync,
187
- rmSync,
188
- statSync,
189
- writeFileSync as writeFileSync3
190
- } from "fs";
191
- import { basename as basename2, dirname as dirname2, join as join3, resolve as resolve3, sep as sep2 } from "path";
192
-
193
- // src/output/redact-write.ts
194
- init_terminal_capture();
195
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
196
- import { dirname } from "path";
197
- function redactExportText(content) {
198
- return redactSecrets(content);
199
- }
200
- function writeRedactedText(path, content) {
201
- mkdirSync2(dirname(path), { recursive: true });
202
- writeFileSync2(path, redactExportText(content), "utf-8");
203
- }
204
- function isRedactableExportPath(path) {
205
- return /\.(md|markdown|txt)$/i.test(path);
206
- }
207
-
208
- // src/services/exports-registry.ts
209
- init_store();
210
- import { randomUUID } from "crypto";
211
-
212
- // src/output/path-safety.ts
213
- init_store();
214
- import { homedir as homedir2 } from "os";
215
- import { resolve as resolve2, sep } from "path";
216
- var NTRP_HOME = ntrpHome();
217
- function resolveUserPath(path) {
218
- if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) {
219
- return resolve2(homedir2(), path.slice(2));
220
- }
221
- return resolve2(path);
222
- }
223
- function isInsideNtrp(path) {
224
- const home = ntrpHome();
225
- const resolved = resolve2(path);
226
- return resolved === home || resolved.startsWith(home + sep);
227
- }
228
-
229
- // src/services/export-kinds.ts
230
- function archiveSubdirForKind(kind) {
231
- if (kind.startsWith("prompt:")) return "handoffs";
232
- if (kind === "report") return "reports";
233
- if (kind === "notes") return "notes";
234
- if (kind === "csv") return "csv";
235
- if (kind === "publish") return "publish";
236
- return "handoffs";
237
- }
238
- function latestBasenameForKind(kind) {
239
- if (kind.startsWith("prompt:")) {
240
- const target = kind.slice("prompt:".length);
241
- return target ? `handoff-${target}.md` : "handoff.md";
242
- }
243
- if (kind === "report") return "report.md";
244
- if (kind === "notes") return "notes.md";
245
- if (kind === "csv") return "csv";
246
- if (kind === "publish") return "publish";
247
- return "handoff.md";
248
- }
249
- function inboxLatestNameForKind(kind) {
250
- if (kind.startsWith("prompt:")) {
251
- const target = kind.slice("prompt:".length);
252
- return target ? `latest-handoff-${target}.md` : "latest-handoff.md";
253
- }
254
- if (kind === "report") return "latest-report.md";
255
- if (kind === "notes") return "latest-notes.md";
256
- if (kind === "csv") return "latest-csv";
257
- if (kind === "publish") return "latest-publish";
258
- return "latest-handoff.md";
259
- }
260
-
261
- // src/services/handoff-skill.ts
262
- import { mkdirSync as mkdirSync3 } from "fs";
263
- init_store();
264
- init_theme();
265
- import { basename, join as join2 } from "path";
266
- import { homedir as homedir3 } from "os";
267
- import chalk2 from "chalk";
268
- var STANDING_SKILL_NAME = "SKILL.md";
269
- var ARCHIVE_PICKUP_NAME = "pickup.md";
270
- var INBOX_PICKUP_NAME = "latest-pickup.md";
271
- function defaultAiInboxDir() {
272
- return join2(homedir3(), "Documents", "Claude", "ntrp-inbox");
273
- }
274
- function handoffLocations() {
275
- const archiveRoot = getExportsDir();
276
- const archiveLatestDir = join2(archiveRoot, "latest");
277
- const inboxDir = getConfiguredAiInboxDir();
278
- return {
279
- archiveRoot,
280
- archiveIndex: join2(archiveRoot, "INDEX.md"),
281
- archiveLatestDir,
282
- archiveSkill: join2(archiveLatestDir, STANDING_SKILL_NAME),
283
- archivePickup: join2(archiveLatestDir, ARCHIVE_PICKUP_NAME),
284
- inboxDir,
285
- inboxIndex: inboxDir ? join2(inboxDir, "INDEX.md") : null,
286
- inboxSkill: inboxDir ? join2(inboxDir, STANDING_SKILL_NAME) : null,
287
- inboxPickup: inboxDir ? join2(inboxDir, INBOX_PICKUP_NAME) : null
288
- };
289
- }
290
- function pickupContextFromEvent(event) {
291
- const loc = handoffLocations();
292
- const writtenAt = event.at || (/* @__PURE__ */ new Date()).toISOString();
293
- const dateUtc = writtenAt.slice(0, 10);
294
- const timeUtc = writtenAt.slice(11, 16);
295
- const inboxLatest = loc.inboxDir ? join2(loc.inboxDir, inboxLatestNameForKind(event.kind)) : null;
296
- const inboxGenericLatest = loc.inboxDir && event.kind.startsWith("prompt:") ? join2(loc.inboxDir, "latest-handoff.md") : inboxLatest;
297
- return {
298
- ...loc,
299
- kind: event.kind,
300
- title: event.title,
301
- writtenAt,
302
- archivePath: event.path,
303
- archiveLatest: join2(loc.archiveLatestDir, latestBasenameForKind(event.kind)),
304
- inboxLatest,
305
- inboxGenericLatest,
306
- datedBasename: basename(event.path),
307
- dateUtc,
308
- timeUtc
309
- };
310
- }
311
- function kindLabel(kind) {
312
- if (kind.startsWith("prompt:")) {
313
- const target = kind.slice("prompt:".length);
314
- if (target === "deck") return "deck prompt";
315
- if (target === "plan") return "action-plan prompt";
316
- if (target === "asana") return "Asana project prompt";
317
- if (target === "clay") return "Clay table prompt";
318
- return target ? `${target} prompt` : "agent prompt";
319
- }
320
- if (kind === "report") return "markdown report";
321
- if (kind === "notes") return "notes export";
322
- if (kind === "csv") return "CSV receipts";
323
- if (kind === "publish") return "repository export";
324
- return kind;
325
- }
326
- function jobForKind(kind) {
327
- if (kind === "prompt:deck") {
328
- return "Open that file and follow its instructions to build an executive review deck. Do not invent numbers.";
329
- }
330
- if (kind === "prompt:plan") {
331
- return "Open that file and follow its instructions to build a prioritized action plan. Do not invent numbers.";
332
- }
333
- if (kind === "prompt:asana") {
334
- return "Open that file and follow its instructions to create the Asana project (sections + tasks). Do not invent numbers.";
335
- }
336
- if (kind === "prompt:clay") {
337
- return "Open that file and follow its instructions to spec the Clay table. Do not invent numbers.";
338
- }
339
- if (kind.startsWith("prompt:")) {
340
- return "Open that file and follow its instructions to build the deliverable. Do not invent numbers.";
341
- }
342
- if (kind === "report") {
343
- return "Open the markdown report. Brief or restyle it as asked; do not invent numbers.";
344
- }
345
- if (kind === "notes") {
346
- return "Open the notes file (Obsidian-style GTM write-up). Use it as source material; do not invent numbers.";
347
- }
348
- if (kind === "csv") {
349
- return "Open the CSV receipts folder (cover-sheet.csv plus per-vital evidence). Use those files as source data; do not invent numbers.";
350
- }
351
- if (kind === "publish") {
352
- return "Open the repository export package folder and work from the files inside.";
353
- }
354
- return "Open the file and use it as source material. Do not invent numbers.";
355
- }
356
- function filenamePattern(ctx) {
357
- const base = ctx.datedBasename;
358
- const dot = base.lastIndexOf(".");
359
- if (dot <= 0) return `${base}*`;
360
- const stem = base.slice(0, dot);
361
- const ext = base.slice(dot);
362
- const datePrefix = stem.includes(ctx.dateUtc) ? `${stem.split(ctx.dateUtc)[0]}${ctx.dateUtc}` : stem.slice(0, 12);
363
- return `${datePrefix}*${ext}`;
364
- }
365
- function buildStandingSkillMarkdown(loc = handoffLocations()) {
366
- const inboxBlock = loc.inboxDir ? `Inbox (preferred \u2014 point Claude Desktop / a project / Cursor at this folder):
367
- \`${loc.inboxDir}\`
368
-
369
- Start with:
370
- - \`SKILL.md\` \u2014 this file
371
- - \`latest-pickup.md\` \u2014 the handoff that was just written (date + exact paths)
372
- - \`latest-handoff.md\` / \`latest-handoff-<target>.md\` \u2014 newest agent prompt
373
- - \`latest-report.md\`, \`latest-notes.md\`, \`latest-csv\` \u2014 other kinds
374
- - \`INDEX.md\` \u2014 catalog with timestamps
375
- - \`archive/\` \u2014 dated copies` : `No AI inbox is configured yet. Canonical archive (always written):
376
- \`${loc.archiveRoot}\`
377
-
378
- Ask the operator to run \`/inbox set <folder>\` in ntrp so copies land in a folder you can see. Until then, use the archive paths below.`;
379
- return `---
380
- name: ntrp-handoff
381
- description: Find and execute NTRP GTM analysis handoffs (deck, plan, Asana, Clay, report, notes, CSV) from the local inbox or exports archive. Use when the user mentions an NTRP handoff, board deck, action plan, or a file ntrp just wrote.
382
- ---
383
-
384
- # Find an NTRP handoff
385
-
386
- NTRP writes GTM analysis deliverables to disk. Your job is to open the file and follow it \u2014 do not invent numbers.
387
-
388
- ## Where to look (this machine)
389
-
390
- ${inboxBlock}
391
-
392
- Canonical archive:
393
- \`${loc.archiveRoot}\`
394
-
395
- - \`latest/SKILL.md\` \u2014 this finder
396
- - \`latest/pickup.md\` \u2014 the handoff that was just written
397
- - \`latest/handoff.md\` / \`latest/handoff-<target>.md\` \u2014 newest prompt
398
- - \`INDEX.md\` \u2014 catalog with timestamps and move history
399
- - \`handoffs/\`, \`reports/\`, \`notes/\`, \`csv/\`, \`publish/\` \u2014 dated files by kind
400
-
401
- ## How to pick the file
402
-
403
- 1. If they just ran a handoff, open \`latest-pickup.md\` (inbox) or \`latest/pickup.md\` (archive). It names the exact file and date.
404
- 2. Otherwise prefer the stable pointer for what they asked for:
405
- - deck / slides \u2192 \`latest-handoff-deck.md\` (inbox) or \`latest/handoff-deck.md\` (archive)
406
- - action plan \u2192 \`latest-handoff-plan.md\`
407
- - Asana \u2192 \`latest-handoff-asana.md\`
408
- - Clay \u2192 \`latest-handoff-clay.md\`
409
- - any prompt \u2192 \`latest-handoff.md\` / \`latest/handoff.md\`
410
- - report / notes / CSV \u2192 \`latest-report.md\`, \`latest-notes.md\`, \`latest-csv\`
411
- 3. If they mention a date, open \`INDEX.md\` and pick the newest row on that UTC date. Dated filenames look like \`handoff-deck-2026-08-13-150123.md\`.
412
- 4. If none of those paths are in your workspace, ask them to attach the file or to \`/inbox set\` a folder you can read.
413
-
414
- You are not given a fresh path on every handoff. Prefer the stable \`latest-*\` pointers.
415
-
416
- Then execute the instructions in that file.
417
- `;
418
- }
419
- function buildPickupPrompt(ctx) {
420
- const lines = [
421
- `Find the NTRP GTM handoff written ${ctx.dateUtc} at ${ctx.timeUtc} UTC.`,
422
- `Kind: ${kindLabel(ctx.kind)}${ctx.title ? ` (${ctx.title})` : ""}.`,
423
- "",
424
- jobForKind(ctx.kind),
425
- "",
426
- "Look in this order (this machine):",
427
- ""
428
- ];
429
- let n = 1;
430
- if (ctx.inboxLatest) {
431
- lines.push(`${n}. Inbox pointer: ${ctx.inboxLatest}`);
432
- n++;
433
- }
434
- if (ctx.inboxGenericLatest && ctx.inboxGenericLatest !== ctx.inboxLatest) {
435
- lines.push(`${n}. Inbox generic: ${ctx.inboxGenericLatest}`);
436
- n++;
437
- }
438
- lines.push(`${n}. Archive pointer: ${ctx.archiveLatest}`);
439
- n++;
440
- lines.push(`${n}. Dated file: ${ctx.archivePath}`);
441
- lines.push(
442
- "",
443
- "If those paths are not in your workspace:",
444
- `- Open INDEX.md in ${ctx.inboxDir ?? ctx.archiveRoot}`,
445
- `- Pick the newest row dated ${ctx.dateUtc} matching ${ctx.kind}`,
446
- `- Or search for ${filenamePattern(ctx)}`,
447
- "",
448
- "Standing finder skill (same folders, install once):",
449
- `- ${ctx.inboxSkill ?? ctx.archiveSkill}`
450
- );
451
- if (ctx.inboxSkill) {
452
- lines.push(`- ${ctx.archiveSkill}`);
453
- } else {
454
- lines.push("- No AI inbox yet \u2014 in ntrp run `/inbox set <folder>` so copies land where your agent can see them.");
455
- }
456
- return lines.join("\n") + "\n";
457
- }
458
- function persistStandingSkill(loc = handoffLocations()) {
459
- mkdirSync3(loc.archiveLatestDir, { recursive: true });
460
- const md = buildStandingSkillMarkdown(loc);
461
- writeRedactedText(loc.archiveSkill, md);
462
- if (loc.inboxDir && loc.inboxSkill) {
463
- mkdirSync3(loc.inboxDir, { recursive: true });
464
- writeRedactedText(loc.inboxSkill, md);
465
- }
466
- }
467
- function persistHandoffSkillFiles(event) {
468
- const ctx = pickupContextFromEvent(event);
469
- persistStandingSkill(ctx);
470
- writeRedactedText(ctx.archivePickup, buildPickupPrompt(ctx));
471
- if (ctx.inboxDir && ctx.inboxPickup) {
472
- mkdirSync3(ctx.inboxDir, { recursive: true });
473
- writeRedactedText(ctx.inboxPickup, buildPickupPrompt(ctx));
474
- }
475
- }
476
-
477
- // src/services/exports-registry.ts
478
- var KIND_DIRS = ["handoffs", "reports", "notes", "csv", "publish"];
479
- var INBOX_ARCHIVE_KEEP = 20;
480
- function exportStamp(d = /* @__PURE__ */ new Date()) {
481
- return d.toISOString().replace(/T/, "-").replace(/:/g, "").slice(0, 15);
482
- }
483
- function ensureExportsLayout(root = getExportsDir()) {
484
- mkdirSync4(root, { recursive: true });
485
- mkdirSync4(join3(root, "latest"), { recursive: true });
486
- for (const sub of KIND_DIRS) {
487
- mkdirSync4(join3(root, sub), { recursive: true });
488
- }
489
- const readme = join3(root, "README.md");
490
- if (!existsSync2(readme)) {
491
- writeFileSync3(readme, ARCHIVE_README, "utf-8");
492
- }
493
- if (!existsSync2(join3(root, "INDEX.md"))) {
494
- writeFileSync3(join3(root, "INDEX.md"), "# NTRP exports\n\n_No exports yet._\n", "utf-8");
495
- }
496
- if (!existsSync2(join3(root, "manifest.jsonl"))) {
497
- writeFileSync3(join3(root, "manifest.jsonl"), "", "utf-8");
498
- }
499
- return root;
500
- }
501
- function getArchiveKindDir(kind) {
502
- const root = ensureExportsLayout();
503
- const dir = join3(root, archiveSubdirForKind(kind));
504
- mkdirSync4(dir, { recursive: true });
505
- return dir;
506
- }
507
- function resolveArchivePath(kind, filename) {
508
- return join3(getArchiveKindDir(kind), filename);
509
- }
510
- function getAiInboxDir() {
511
- return getConfiguredAiInboxDir();
512
- }
513
- function setAiInboxDir(path) {
514
- const resolved = resolveUserPath(path);
515
- mkdirSync4(resolved, { recursive: true });
516
- setConfigValue("ai-inbox-dir", resolved);
517
- ensureInboxLayout(resolved);
518
- persistStandingSkill();
519
- setConfigValue("ai-inbox-nudge-seen", "true");
520
- return resolved;
521
- }
522
- function ensureInboxLayout(inbox) {
523
- mkdirSync4(inbox, { recursive: true });
524
- mkdirSync4(join3(inbox, "archive"), { recursive: true });
525
- const readme = join3(inbox, "README.md");
526
- writeFileSync3(readme, buildInboxReadme(), "utf-8");
527
- if (!existsSync2(join3(inbox, "INDEX.md"))) {
528
- writeFileSync3(join3(inbox, "INDEX.md"), "# NTRP AI inbox\n\n_No exports synced yet._\n", "utf-8");
529
- }
530
- }
531
- function manifestPath(root = getExportsDir()) {
532
- return join3(root, "manifest.jsonl");
533
- }
534
- function readManifestEvents(root = getExportsDir()) {
535
- const path = manifestPath(root);
536
- if (!existsSync2(path)) return [];
537
- const text = readFileSync2(path, "utf-8");
538
- const events = [];
539
- for (const line of text.split("\n")) {
540
- const trimmed = line.trim();
541
- if (!trimmed) continue;
542
- try {
543
- events.push(JSON.parse(trimmed));
544
- } catch {
545
- }
546
- }
547
- return events;
548
- }
549
- function appendManifestEvent(event, root = getExportsDir()) {
550
- ensureExportsLayout(root);
551
- appendFileSync(manifestPath(root), JSON.stringify(event) + "\n", "utf-8");
552
- }
553
- function listExports(opts = {}) {
554
- const limit = opts.limit ?? 20;
555
- const events = readManifestEvents();
556
- const byId = /* @__PURE__ */ new Map();
557
- for (const e of events) {
558
- if (e.op === "inbox_sync") continue;
559
- byId.set(e.id, e);
560
- }
561
- let items = [...byId.values()].sort((a, b) => a.at < b.at ? 1 : a.at > b.at ? -1 : 0);
562
- if (opts.kind) {
563
- const k = opts.kind.toLowerCase();
564
- items = items.filter((e) => e.kind === opts.kind || e.kind.startsWith(k) || e.kind.includes(k));
565
- }
566
- return items.slice(0, limit);
567
- }
568
- function findExportByIdOrName(idOrPath) {
569
- const items = listExports({ limit: 500 });
570
- const needle = idOrPath.trim();
571
- const byId = items.find((e) => e.id === needle || e.id.startsWith(needle));
572
- if (byId) return byId;
573
- const base = basename2(needle);
574
- const byName = items.find((e) => basename2(e.path) === base || e.path.endsWith(needle));
575
- if (byName) return byName;
576
- const resolved = resolveUserPath(needle);
577
- return items.find((e) => e.path === resolved) ?? null;
578
- }
579
- function updateArchiveLatest(kind, sourcePath, root) {
580
- const latestDir = join3(root, "latest");
581
- mkdirSync4(latestDir, { recursive: true });
582
- const name = latestBasenameForKind(kind);
583
- const dest = join3(latestDir, name);
584
- copyPath(sourcePath, dest);
585
- if (kind.startsWith("prompt:")) {
586
- copyPath(sourcePath, join3(latestDir, "handoff.md"));
587
- }
588
- }
589
- function copyPath(src, dest) {
590
- mkdirSync4(dirname2(dest), { recursive: true });
591
- if (existsSync2(dest)) {
592
- rmSync(dest, { recursive: true, force: true });
593
- }
594
- const st = statSync(src);
595
- if (st.isDirectory()) {
596
- cpSync(src, dest, { recursive: true });
597
- } else if (isRedactableExportPath(src)) {
598
- const text = readFileSync2(src, "utf-8");
599
- writeFileSync3(dest, redactExportText(text), "utf-8");
600
- } else {
601
- copyFileSync(src, dest);
602
- }
603
- }
604
- function regenerateIndex(root = getExportsDir()) {
605
- ensureExportsLayout(root);
606
- const items = listExports({ limit: 50 });
607
- const events = readManifestEvents(root);
608
- const withHistory = items.filter((e) => (e.previous_paths?.length ?? 0) > 0);
609
- const latestDir = join3(root, "latest");
610
- const latestLines = [];
611
- if (existsSync2(latestDir)) {
612
- for (const name of readdirSync(latestDir).sort()) {
613
- latestLines.push(`- \`latest/${name}\` \u2192 \`${join3(latestDir, name)}\``);
614
- }
615
- }
616
- const lines = [
617
- "# NTRP exports",
618
- "",
619
- `Archive root: \`${root}\``,
620
- "",
621
- "Set an inbox with `/onboard` or `/inbox set`. Paste `SKILL.md` once into Claude. Later handoffs overwrite `latest-handoff.md`.",
622
- "",
623
- "## Latest pointers",
624
- ""
625
- ];
626
- if (latestLines.length > 0) lines.push(...latestLines);
627
- else lines.push("_None yet._");
628
- lines.push("", "## Recent exports", "");
629
- if (items.length === 0) {
630
- lines.push("_No exports yet._");
631
- } else {
632
- for (const e of items) {
633
- const title = e.title ? ` \u2014 ${e.title}` : "";
634
- const session = e.session_id ? ` \xB7 session ${e.session_id.slice(-4)}` : "";
635
- lines.push(`- **${e.kind}** (${e.at})${title}${session}`);
636
- lines.push(` - id: \`${e.id}\``);
637
- lines.push(` - path: \`${e.path}\``);
638
- if (e.inbox_path) lines.push(` - inbox: \`${e.inbox_path}\``);
639
- }
640
- }
641
- lines.push("", "## Location history", "");
642
- if (withHistory.length === 0) {
643
- lines.push("_No moves recorded._");
644
- } else {
645
- for (const e of withHistory) {
646
- lines.push(`- **${e.kind}** \`${e.id}\``);
647
- for (const prev of e.previous_paths ?? []) {
648
- lines.push(` - was: \`${prev}\``);
649
- }
650
- lines.push(` - now: \`${e.path}\``);
651
- }
652
- }
653
- const moveOps = events.filter((e) => e.op === "move").slice(-20).reverse();
654
- if (moveOps.length > 0) {
655
- lines.push("", "## Recent moves", "");
656
- for (const e of moveOps) {
657
- const from = e.previous_paths?.[e.previous_paths.length - 1] ?? "?";
658
- lines.push(`- ${e.at}: \`${from}\` \u2192 \`${e.path}\` (${e.kind}, \`${e.id}\`)`);
659
- }
660
- }
661
- lines.push("");
662
- writeFileSync3(join3(root, "INDEX.md"), lines.join("\n"), "utf-8");
663
- }
664
- function regenerateInboxIndex(inbox) {
665
- ensureInboxLayout(inbox);
666
- const items = listExports({ limit: 15 });
667
- const archiveRoot = getExportsDir();
668
- const lines = [
669
- "# NTRP AI inbox",
670
- "",
671
- "Start here. Install `SKILL.md` once in Claude. Newest prompt: `latest-handoff.md`. Catalog: `INDEX.md`.",
672
- "",
673
- `Canonical archive: \`${archiveRoot}\` (see \`${join3(archiveRoot, "INDEX.md")}\`).`,
674
- "",
675
- "## Latest pointers",
676
- ""
677
- ];
678
- if (existsSync2(join3(inbox, "SKILL.md"))) {
679
- lines.push("- [`SKILL.md`](./SKILL.md) \u2014 standing finder. Paste once into your agent.");
680
- }
681
- const latestNames = readdirSync(inbox).filter((n) => n.startsWith("latest-")).sort();
682
- if (latestNames.length === 0 && !existsSync2(join3(inbox, "SKILL.md"))) {
683
- lines.push("_None yet. Run a handoff after `/inbox set`._");
684
- } else {
685
- for (const name of latestNames) {
686
- const note = name === "latest-pickup.md" ? " \u2014 names the file that was just written" : "";
687
- lines.push(`- [\`${name}\`](./${name})${note}`);
688
- }
689
- }
690
- lines.push("", "## Recent exports", "");
691
- if (items.length === 0) lines.push("_No exports yet._");
692
- else {
693
- for (const e of items) {
694
- lines.push(`- **${e.kind}** (${e.at}): \`${e.path}\``);
695
- if (e.inbox_path) lines.push(` - inbox copy: \`${e.inbox_path}\``);
696
- }
697
- }
698
- lines.push("");
699
- writeFileSync3(join3(inbox, "INDEX.md"), lines.join("\n"), "utf-8");
700
- writeFileSync3(join3(inbox, "README.md"), buildInboxReadme(), "utf-8");
701
- }
702
- function buildInboxReadme() {
703
- const archive = getExportsDir();
704
- return `# NTRP AI inbox
705
-
706
- This folder is the landing folder for NTRP handoffs. Desktop AI tools read files here.
707
-
708
- ## Start here
709
-
710
- 1. Install \`SKILL.md\` once in Claude, ChatGPT, or Cursor. Then tell the agent to open the latest NTRP handoff.
711
- 2. The newest prompt is \`latest-handoff.md\` (or \`latest-handoff-deck.md\` and similar).
712
- 3. \`latest-pickup.md\` names the file that was just written.
713
-
714
- Each export overwrites the \`latest-*\` files. Dated copies are in \`archive/\`.
715
-
716
- ## Canonical archive
717
-
718
- The full history with the move trail is at:
719
-
720
- \`${archive}\`
721
-
722
- See \`${join3(archive, "INDEX.md")}\` and \`${join3(archive, "manifest.jsonl")}\`.
723
-
724
- Set the folder with \`/inbox set <path>\`. Print the skill with \`/inbox skill\`. Clear with \`/inbox clear\`. List files with \`/exports\`.
725
- `;
726
- }
727
- var ARCHIVE_README = `# NTRP exports archive
728
-
729
- This archive stores handoffs, reports, notes, CSV receipts, and publish packages by kind:
730
-
731
- - \`handoffs/\` \u2014 agent prompts (\`handoff-deck-*.md\` and similar)
732
- - \`reports/\` \u2014 markdown reports
733
- - \`notes/\` \u2014 notes files
734
- - \`csv/\` \u2014 receipt folders
735
- - \`publish/\` \u2014 repository export packages
736
- - \`latest/\` \u2014 copies of the newest file per kind
737
-
738
- \`INDEX.md\` is rebuilt from \`manifest.jsonl\` on every write or move.
739
-
740
- Set a dedicated inbox for desktop AI instead of this folder:
741
-
742
- \`\`\`
743
- /inbox set ~/Documents/Claude/ntrp-inbox
744
- \`\`\`
745
-
746
- After \`/inbox set\` or the optional \`/onboard\` step, paste \`latest/SKILL.md\` once into Claude. Later \`/handoff\` overwrites \`latest-handoff.md\`. The skill is not printed again.
747
- `;
748
- function pruneInboxArchive(archiveDir, keep = INBOX_ARCHIVE_KEEP) {
749
- if (!existsSync2(archiveDir)) return;
750
- const entries = readdirSync(archiveDir).map((name) => {
751
- const p = join3(archiveDir, name);
752
- try {
753
- return { name, path: p, mtime: statSync(p).mtimeMs };
754
- } catch {
755
- return null;
756
- }
757
- }).filter((e) => e != null).sort((a, b) => b.mtime - a.mtime);
758
- for (const old of entries.slice(keep)) {
759
- rmSync(old.path, { recursive: true, force: true });
760
- }
761
- }
762
- function syncAiInbox(entry) {
763
- const inbox = getAiInboxDir();
764
- if (!inbox) return null;
765
- if (!existsSync2(entry.path)) return null;
766
- ensureInboxLayout(inbox);
767
- const archiveDir = join3(inbox, "archive");
768
- mkdirSync4(archiveDir, { recursive: true });
769
- const base = basename2(entry.path);
770
- const archiveDest = join3(archiveDir, base);
771
- copyPath(entry.path, archiveDest);
772
- pruneInboxArchive(archiveDir);
773
- const latestName = inboxLatestNameForKind(entry.kind);
774
- const latestDest = join3(inbox, latestName);
775
- copyPath(entry.path, latestDest);
776
- if (entry.kind.startsWith("prompt:")) {
777
- copyPath(entry.path, join3(inbox, "latest-handoff.md"));
778
- }
779
- regenerateInboxIndex(inbox);
780
- return latestDest;
781
- }
782
- function recordExportWrite(opts) {
783
- const root = ensureExportsLayout();
784
- const path = resolve3(opts.path);
785
- if (!existsSync2(path)) {
786
- throw new Error(`Export path does not exist: ${path}`);
787
- }
788
- updateArchiveLatest(opts.kind, path, root);
789
- const event = {
790
- id: randomUUID().slice(0, 8),
791
- op: "write",
792
- at: (/* @__PURE__ */ new Date()).toISOString(),
793
- kind: opts.kind,
794
- path,
795
- session_id: opts.sessionId,
796
- title: opts.title
797
- };
798
- persistHandoffSkillFiles(event);
799
- const inboxPath = syncAiInbox({ kind: opts.kind, path });
800
- if (inboxPath) event.inbox_path = inboxPath;
801
- appendManifestEvent(event, root);
802
- regenerateIndex(root);
803
- return event;
804
- }
805
- function moveExport(idOrPath, destDir) {
806
- const item = findExportByIdOrName(idOrPath);
807
- if (!item) {
808
- throw new Error(`No export matching "${idOrPath}". Try /exports list.`);
809
- }
810
- if (!existsSync2(item.path)) {
811
- throw new Error(`Export file missing on disk: ${item.path}`);
812
- }
813
- const destRoot = resolveUserPath(destDir);
814
- mkdirSync4(destRoot, { recursive: true });
815
- const name = basename2(item.path);
816
- let destPath = join3(destRoot, name);
817
- if (existsSync2(destPath)) {
818
- destPath = join3(destRoot, `${exportStamp()}-${name}`);
819
- }
820
- renameSync(item.path, destPath);
821
- const previous = [...item.previous_paths ?? [], item.path];
822
- updateArchiveLatest(item.kind, destPath, ensureExportsLayout());
823
- const event = {
824
- id: item.id,
825
- op: "move",
826
- at: (/* @__PURE__ */ new Date()).toISOString(),
827
- kind: item.kind,
828
- path: destPath,
829
- previous_paths: previous,
830
- session_id: item.session_id,
831
- title: item.title
832
- };
833
- persistHandoffSkillFiles(event);
834
- const inboxPath = syncAiInbox({ kind: item.kind, path: destPath });
835
- if (inboxPath) event.inbox_path = inboxPath;
836
- appendManifestEvent(event);
837
- regenerateIndex();
838
- return event;
839
- }
840
- function archiveIndexPath() {
841
- return join3(ensureExportsLayout(), "INDEX.md");
842
- }
843
- function inboxLatestHandoffPath() {
844
- const inbox = getAiInboxDir();
845
- if (!inbox) return null;
846
- const p = join3(inbox, "latest-handoff.md");
847
- return existsSync2(p) ? p : null;
848
- }
849
-
850
- // src/services/context-doc.ts
851
- import { writeFileSync as writeFileSync6 } from "fs";
852
-
853
- // src/cli/context.ts
854
- import { basename as basename3, join as join5, resolve as resolve4, sep as sep3 } from "path";
855
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2, rmSync as rmSync3 } from "fs";
856
- import { homedir as homedir4 } from "os";
857
- import { randomUUID as randomUUID2 } from "crypto";
858
-
859
- // src/services/transcript.ts
860
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync4, rmSync as rmSync2 } from "fs";
861
- import { join as join4 } from "path";
862
- init_terminal_capture();
863
-
864
- // src/cli/context.ts
865
- var STALE_SESSION_MS = 14 * 24 * 60 * 60 * 1e3;
866
- function ntrpHomeDir() {
867
- return process.env.NTRP_HOME ? resolve4(process.env.NTRP_HOME) : join5(homedir4(), ".ntrp");
868
- }
869
- function getSessionsDir() {
870
- const dir = join5(ntrpHomeDir(), "sessions");
871
- if (!existsSync4(dir)) {
872
- mkdirSync5(dir, { recursive: true });
873
- }
874
- return dir;
875
- }
876
- function getDatasetsDir() {
877
- const dir = join5(ntrpHomeDir(), "datasets");
878
- if (!existsSync4(dir)) {
879
- mkdirSync5(dir, { recursive: true });
880
- }
881
- return dir;
882
- }
883
- function datasetPathForSession(id) {
884
- return join5(getDatasetsDir(), `${id}.duckdb`);
885
- }
886
- function transcriptPathForSession(id) {
887
- return join5(getSessionsDir(), `${id}.transcript.md`);
888
- }
889
- function defaultSessionAnalysis(primary = "gtm_health") {
890
- return { primary, completed: [] };
891
- }
892
-
893
- // src/services/context-doc.ts
894
- init_formatters();
895
- init_store();
896
- init_terminal_capture();
897
- var AGENT_EXCERPT_CHARS = 400;
898
- function buildSessionContextDoc(file, opts = {}) {
899
- const id = file.id;
900
- const shortId = id.slice(-4);
901
- const exchanges = file.exchange_count ?? Math.floor(file.messages.length / 2);
902
- const lines = [];
903
- lines.push(`# Session context \u2014 ${id}${file.name ? ` (${file.name})` : ""}`);
904
- lines.push("");
905
- lines.push("## Status");
906
- lines.push("");
907
- lines.push(`- Stage: ${file.stage ?? "new"}`);
908
- lines.push(`- Created: ${file.created_at}`);
909
- if (file.ended_at) lines.push(`- Ended: ${file.ended_at}`);
910
- lines.push(`- Updated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
911
- lines.push(`- Exchanges: ${exchanges}`);
912
- if (file.summary) lines.push(`- Summary: ${file.summary}`);
913
- if (file.resumed_from) lines.push(`- Resumed from: ${file.resumed_from}`);
914
- lines.push("");
915
- lines.push("## Dataset");
916
- lines.push("");
917
- if (file.dataset?.label || file.dataset?.source) {
918
- lines.push(`- Label: ${file.dataset.label ?? "(unlabeled)"}`);
919
- if (file.dataset.source) lines.push(`- Source: ${file.dataset.source}`);
920
- if (file.dataset.ingested_at) lines.push(`- Ingested: ${file.dataset.ingested_at}`);
921
- const counts = Object.entries(file.dataset.counts ?? {}).filter(([, n]) => n > 0);
922
- if (counts.length > 0) {
923
- lines.push(`- Counts: ${counts.map(([k, n]) => `${n.toLocaleString()} ${k}`).join(", ")}`);
924
- }
925
- } else {
926
- lines.push("- No data loaded.");
927
- }
928
- if (file.attachments && file.attachments.length > 0) {
929
- for (const a of file.attachments) {
930
- const detail = [a.entity_type, a.row_count != null ? `${a.row_count} rows` : null].filter(Boolean).join(", ");
931
- lines.push(`- Attachment: ${a.path}${detail ? ` (${detail})` : ""}`);
932
- }
933
- }
934
- lines.push("");
935
- if (file.scope) {
936
- lines.push("## Scope");
937
- lines.push("");
938
- lines.push(`- Intent: ${file.scope.intent_summary}`);
939
- lines.push(`- Lens: ${file.scope.primary_lens}`);
940
- if (file.scope.audience) lines.push(`- Audience: ${file.scope.audience}`);
941
- if (file.scope.time_horizon) lines.push(`- Time horizon: ${file.scope.time_horizon}`);
942
- if (file.scope.segments?.length) lines.push(`- Segments: ${file.scope.segments.join(", ")}`);
943
- if (file.scope.confirmed_at) lines.push(`- Confirmed: ${file.scope.confirmed_at}`);
944
- lines.push("");
945
- }
946
- lines.push("## Analysis");
947
- lines.push("");
948
- if (file.analysis) {
949
- lines.push(`- Primary lens: ${file.analysis.primary}`);
950
- lines.push(`- Completed: ${file.analysis.completed.join(", ") || "none"}`);
951
- if (file.analysis.coverage) {
952
- lines.push(
953
- `- Coverage: ${file.analysis.coverage.distinct_months} months \xB7 recommended cadence ${file.analysis.coverage.recommended_cadence}`
954
- );
955
- }
956
- if (file.analysis.data_source_type) {
957
- lines.push(`- Data source type: ${file.analysis.data_source_type}`);
958
- }
959
- if (file.analysis.headline?.length) {
960
- lines.push("");
961
- lines.push("### Headline metrics");
962
- lines.push("");
963
- for (const h of file.analysis.headline) {
964
- lines.push(`- ${h.label}: ${h.formatted}`);
965
- }
966
- }
967
- } else {
968
- lines.push("- No analysis recorded.");
969
- }
970
- const health = opts.snapshot?.aggregate;
971
- if (health) {
972
- lines.push("");
973
- lines.push("### GTM health snapshot");
974
- lines.push("");
975
- lines.push(`- Overall: ${Math.round(health.overall_score)} (${health.overall_status})`);
976
- lines.push(`- Gating vital sign: ${health.gating_vital_sign.replace(/_/g, " ")}`);
977
- if (health.total_value_at_risk != null && health.total_value_at_risk > 0) {
978
- lines.push(`- Total value at risk: ${formatCurrency(health.total_value_at_risk)}`);
979
- }
980
- for (const vs of health.vital_signs) {
981
- const label = VITAL_SIGN_LABELS[vs.vital_sign] ?? vs.vital_sign;
982
- const dollars = vs.dollar_value != null ? ` \u2014 ${formatCurrency(vs.dollar_value)}${vs.dollar_label ? ` ${vs.dollar_label}` : ""}` : "";
983
- lines.push(`- ${label}: ${Math.round(vs.score)} (${vs.status})${dollars}`);
984
- }
985
- }
986
- lines.push("");
987
- if (file.strategist) {
988
- lines.push("## Strategist (in flight)");
989
- lines.push("");
990
- lines.push(`- Step: ${file.strategist.step}`);
991
- if (file.strategist.objective) lines.push(`- Objective: ${file.strategist.objective}`);
992
- if (file.strategist.constraintsNote) {
993
- lines.push(`- Constraints: ${file.strategist.constraintsNote}`);
994
- }
995
- if (file.strategist.origin) lines.push(`- Origin: ${file.strategist.origin}`);
996
- lines.push("");
997
- }
998
- lines.push("## Deliverables");
999
- lines.push("");
1000
- if (file.deliverables && file.deliverables.length > 0) {
1001
- for (const d of file.deliverables) {
1002
- const detail = [d.path, d.note].filter(Boolean).join(" \u2014 ");
1003
- lines.push(`- ${d.kind} (${d.at})${detail ? `: ${detail}` : ""}`);
1004
- }
1005
- } else {
1006
- lines.push("- None yet.");
1007
- }
1008
- lines.push("");
1009
- lines.push("## Exports");
1010
- lines.push("");
1011
- try {
1012
- lines.push(`- Archive index: \`${archiveIndexPath()}\``);
1013
- lines.push(`- Archive root: \`${getExportsDir()}\``);
1014
- const inbox = getAiInboxDir();
1015
- if (inbox) {
1016
- lines.push(`- AI inbox: \`${inbox}\` (open \`SKILL.md\`, \`latest-pickup.md\`, or \`latest-handoff.md\`)`);
1017
- } else {
1018
- lines.push("- AI inbox: unset \u2014 `/inbox set <folder>` then paste `/inbox skill` into your agent");
1019
- }
1020
- } catch {
1021
- lines.push("- Export catalog unavailable.");
1022
- }
1023
- lines.push("");
1024
- lines.push(`## Conversation (${exchanges} exchange${exchanges === 1 ? "" : "s"})`);
1025
- lines.push("");
1026
- if (file.messages.length === 0) {
1027
- lines.push("- No exchanges yet.");
1028
- } else {
1029
- let n = 0;
1030
- for (const msg of file.messages) {
1031
- if (msg.role === "user") {
1032
- n++;
1033
- lines.push(`${n}. \u276F ${excerpt(msg.content, AGENT_EXCERPT_CHARS)}`);
1034
- } else {
1035
- lines.push(` \u21B3 ${excerpt(msg.content, AGENT_EXCERPT_CHARS)}`);
1036
- }
1037
- }
1038
- }
1039
- lines.push("");
1040
- lines.push("## Files");
1041
- lines.push("");
1042
- lines.push(`- Transcript (raw terminal): \`${transcriptPathForSession(id)}\``);
1043
- lines.push(`- Session data (JSON): \`${sessionJsonPath(id)}\``);
1044
- lines.push(`- Dataset (DuckDB): \`${datasetPathForSession(id)}\``);
1045
- lines.push("");
1046
- lines.push("## Pick up this session");
1047
- lines.push("");
1048
- lines.push(`Run \`ntrp\`, then \`/session ${shortId}\` \u2014 rebinds the dataset and reloads the`);
1049
- lines.push("conversation thread in place. Read the transcript above for the full terminal");
1050
- lines.push("history before continuing.");
1051
- lines.push("");
1052
- return lines.map(redactSecrets).join("\n");
1053
- }
1054
- function excerpt(content, max) {
1055
- const flat = content.replace(/\s+/g, " ").trim();
1056
- return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
1057
- }
1058
- function sessionJsonPath(id) {
1059
- return `${getSessionsDir()}/${id}.json`;
1060
- }
1061
-
1062
- // src/services/exports-registry-smoke.ts
1063
- var failures = [];
1064
- function assert(cond, msg) {
1065
- if (!cond) failures.push(msg);
1066
- }
1067
- function section(name) {
1068
- console.log(` \xB7 ${name}`);
1069
- }
1070
- section("archive layout + write");
1071
- {
1072
- resetConfigCache();
1073
- const root = ensureExportsLayout();
1074
- assert(existsSync5(join6(root, "handoffs")), "handoffs/ created");
1075
- assert(existsSync5(join6(root, "README.md")), "archive README seeded");
1076
- assert(existsSync5(join6(root, "manifest.jsonl")), "manifest.jsonl seeded");
1077
- const out = resolveArchivePath("prompt:deck", `handoff-deck-${exportStamp()}.md`);
1078
- writeFileSync7(out, "# Deck handoff\n\nBuild slides from this.\n", "utf-8");
1079
- const event = recordExportWrite({
1080
- kind: "prompt:deck",
1081
- path: out,
1082
- sessionId: "sess-abcd1234",
1083
- title: "deck handoff prompt"
1084
- });
1085
- assert(event.kind === "prompt:deck", "kind recorded");
1086
- assert(existsSync5(join6(root, "latest", "handoff-deck.md")), "latest/handoff-deck.md");
1087
- assert(existsSync5(join6(root, "latest", "handoff.md")), "latest/handoff.md generic");
1088
- assert(existsSync5(join6(root, "latest", "SKILL.md")), "latest/SKILL.md standing finder");
1089
- assert(existsSync5(join6(root, "latest", "pickup.md")), "latest/pickup.md this-write locator");
1090
- const pickup = readFileSync5(join6(root, "latest", "pickup.md"), "utf-8");
1091
- assert(pickup.includes("prompt:deck") || pickup.includes("deck prompt"), "pickup names kind");
1092
- assert(pickup.includes(out), "pickup names dated archive path");
1093
- assert(/20\d\d-\d\d-\d\d/.test(pickup), "pickup includes UTC date");
1094
- const standing = readFileSync5(join6(root, "latest", "SKILL.md"), "utf-8");
1095
- assert(standing.includes("name: ntrp-handoff"), "standing skill has YAML name");
1096
- assert(standing.includes(root), "standing skill names archive root");
1097
- const index = readFileSync5(archiveIndexPath(), "utf-8");
1098
- assert(index.includes("prompt:deck"), "INDEX mentions kind");
1099
- assert(index.includes(out), "INDEX mentions path");
1100
- assert(readManifestEvents().some((e) => e.op === "write" && e.path === out), "manifest write event");
1101
- }
1102
- section("ai inbox sync");
1103
- {
1104
- const inbox = join6(ntrpHome(), "claude-inbox");
1105
- setAiInboxDir(inbox);
1106
- assert(getAiInboxDir() === inbox, "inbox config set");
1107
- assert(existsSync5(join6(inbox, "README.md")), "inbox README");
1108
- const out = resolveArchivePath("prompt:plan", `handoff-plan-${exportStamp()}.md`);
1109
- writeFileSync7(out, "# Plan handoff\n", "utf-8");
1110
- const event = recordExportWrite({ kind: "prompt:plan", path: out, title: "plan" });
1111
- assert(typeof event.inbox_path === "string", "inbox_path on write event");
1112
- assert(existsSync5(join6(inbox, "latest-handoff.md")), "latest-handoff.md");
1113
- assert(existsSync5(join6(inbox, "latest-handoff-plan.md")), "latest-handoff-plan.md");
1114
- assert(existsSync5(join6(inbox, "archive")), "inbox archive dir");
1115
- assert(readdirSync3(join6(inbox, "archive")).length >= 1, "dated copy in inbox archive");
1116
- const inboxIndex = readFileSync5(join6(inbox, "INDEX.md"), "utf-8");
1117
- assert(inboxIndex.includes("latest-handoff"), "inbox INDEX lists latest");
1118
- assert(inboxIndex.includes("SKILL.md"), "inbox INDEX lists standing skill");
1119
- assert(inboxIndex.includes("latest-pickup"), "inbox INDEX lists pickup");
1120
- assert(inboxLatestHandoffPath() === join6(inbox, "latest-handoff.md"), "inboxLatestHandoffPath");
1121
- assert(existsSync5(join6(inbox, "SKILL.md")), "inbox SKILL.md");
1122
- assert(existsSync5(join6(inbox, "latest-pickup.md")), "inbox latest-pickup.md");
1123
- const inboxSkill = readFileSync5(join6(inbox, "SKILL.md"), "utf-8");
1124
- assert(inboxSkill.includes(inbox), "standing skill names inbox path");
1125
- const inboxPickup = readFileSync5(join6(inbox, "latest-pickup.md"), "utf-8");
1126
- assert(inboxPickup.includes("prompt:plan") || inboxPickup.includes("action-plan"), "inbox pickup names plan kind");
1127
- assert(inboxPickup.includes(out), "inbox pickup names dated file");
1128
- }
1129
- section("move trail");
1130
- {
1131
- const items = listExports({ limit: 10, kind: "prompt" });
1132
- assert(items.length >= 1, "listExports returns items");
1133
- const target = items.find((e) => e.kind === "prompt:deck") ?? items[0];
1134
- const dest = join6(ntrpHome(), "moved-exports");
1135
- mkdirSync6(dest, { recursive: true });
1136
- const moved = moveExport(target.id, dest);
1137
- assert(moved.op === "move", "move op");
1138
- assert(moved.path.startsWith(dest), `moved under dest (got ${moved.path})`);
1139
- assert((moved.previous_paths?.length ?? 0) >= 1, "previous_paths recorded");
1140
- assert(existsSync5(moved.path), "file exists at new path");
1141
- assert(!existsSync5(target.path), "old path gone");
1142
- const index = readFileSync5(archiveIndexPath(), "utf-8");
1143
- assert(index.includes("Location history") || index.includes("Recent moves"), "INDEX has history section");
1144
- assert(index.includes(moved.previous_paths[0]), "INDEX shows previous path");
1145
- }
1146
- section("kind dirs");
1147
- {
1148
- assert(getArchiveKindDir("notes").endsWith(`${join6("exports", "notes")}`) || getArchiveKindDir("notes").includes("/notes"), "notes kind dir");
1149
- assert(getArchiveKindDir("csv").includes("/csv") || getArchiveKindDir("csv").includes("\\csv"), "csv kind dir");
1150
- assert(getArchiveKindDir("report").includes("reports"), "reports kind dir");
1151
- }
1152
- section("path-safety NTRP_HOME");
1153
- {
1154
- const home = ntrpHome();
1155
- assert(isInsideNtrp(join6(home, "exports")), "exports inside NTRP_HOME");
1156
- assert(!isInsideNtrp("/tmp/not-ntrp-exports"), "foreign path outside");
1157
- const expanded = resolveUserPath("~/Documents/test-inbox");
1158
- assert(expanded.includes("Documents"), "tilde expands");
1159
- }
1160
- section("context brief exports blurb");
1161
- {
1162
- const file = {
1163
- id: "sess-exports-test",
1164
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1165
- messages: [],
1166
- stage: "delivered",
1167
- analysis: defaultSessionAnalysis(),
1168
- deliverables: [{ kind: "prompt:deck", at: (/* @__PURE__ */ new Date()).toISOString(), path: "/tmp/deck.md" }]
1169
- };
1170
- const doc = buildSessionContextDoc(file);
1171
- assert(doc.includes("## Exports"), "context doc has Exports section");
1172
- assert(doc.includes("## Deliverables"), "context doc has Deliverables");
1173
- assert(doc.includes("/tmp/deck.md"), "deliverable path shown");
1174
- assert(doc.includes("AI inbox"), "mentions AI inbox");
1175
- }
1176
- section("handoff skill builders");
1177
- {
1178
- const loc = handoffLocations();
1179
- const standing = buildStandingSkillMarkdown(loc);
1180
- assert(standing.includes("name: ntrp-handoff"), "YAML frontmatter");
1181
- assert(standing.includes("INDEX.md"), "standing skill mentions INDEX.md");
1182
- assert(standing.includes(loc.archiveRoot), "standing skill names archive");
1183
- const items = listExports({ limit: 1 });
1184
- assert(items.length >= 1, "have an export to build pickup from");
1185
- const ctx = pickupContextFromEvent(items[0]);
1186
- const pickup = buildPickupPrompt(ctx);
1187
- assert(pickup.includes(ctx.dateUtc), "pickup includes UTC date");
1188
- assert(pickup.includes("Look in this order"), "pickup has search order");
1189
- assert(pickup.includes("INDEX.md"), "pickup falls back to INDEX.md");
1190
- assert(pickup.includes(ctx.archivePath), "pickup names dated file");
1191
- assert(pickup.includes("SKILL.md") || pickup.includes("Standing finder"), "pickup points at standing skill");
1192
- }
1193
- section("default inbox path");
1194
- {
1195
- const d = defaultAiInboxDir();
1196
- assert(d.includes("ntrp-inbox"), `default inbox names ntrp-inbox (got ${d})`);
1197
- assert(d.includes("Claude") || d.includes("Documents"), "default inbox is under Documents/Claude");
1198
- }
1199
- if (failures.length > 0) {
1200
- console.error("\nexports-registry smoke FAILED:");
1201
- for (const f of failures) console.error(` \u2717 ${f}`);
1202
- process.exit(1);
1203
- }
1204
- console.log("\nexports-registry smoke OK");
1205
- //# sourceMappingURL=exports-registry-smoke.js.map