wayfind 2.0.34 → 2.0.36

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.
@@ -249,11 +249,23 @@ async function configure() {
249
249
  .map((d) => d.trim())
250
250
  .filter(Boolean);
251
251
 
252
+ // Optional: page IDs for full content extraction
253
+ console.log('');
254
+ console.log('Optional: specific page IDs to extract full content from (comma-separated).');
255
+ console.log('These pages will have their body text included in signals, not just metadata.');
256
+ console.log('Find page IDs in the URL: notion.so/<workspace>/<page-id>');
257
+ const pageInput = await ask('Page IDs: ');
258
+ const pages = pageInput
259
+ .split(',')
260
+ .map((p) => p.trim().replace(/-/g, ''))
261
+ .filter(Boolean);
262
+
252
263
  const channelConfig = {
253
264
  transport: 'https',
254
265
  token,
255
266
  token_env: 'NOTION_TOKEN',
256
267
  databases: databases.length > 0 ? databases : null,
268
+ pages: pages.length > 0 ? pages : null,
257
269
  last_pull: null,
258
270
  };
259
271
 
@@ -261,8 +273,12 @@ async function configure() {
261
273
  console.log('Notion connector configured.');
262
274
  if (databases.length > 0) {
263
275
  console.log(`Monitoring ${databases.length} database(s).`);
264
- } else {
265
- console.log('Monitoring all shared pages.');
276
+ }
277
+ if (pages.length > 0) {
278
+ console.log(`Extracting content from ${pages.length} page(s).`);
279
+ }
280
+ if (databases.length === 0 && pages.length === 0) {
281
+ console.log('Monitoring all shared pages (metadata only).');
266
282
  }
267
283
  console.log('');
268
284
 
@@ -298,6 +314,37 @@ async function pull(config, since) {
298
314
  dbEntries.push(...entries.map((e) => ({ ...e, _databaseId: dbId })));
299
315
  }
300
316
 
317
+ // Fetch content for targeted pages
318
+ const targetedPageIds = config.pages || [];
319
+ const pageContents = {};
320
+ if (targetedPageIds.length > 0) {
321
+ for (const pageId of targetedPageIds) {
322
+ try {
323
+ const content = await fetchPageContent(token, pageId);
324
+ if (content && content.trim()) {
325
+ pageContents[pageId] = content;
326
+ }
327
+ } catch {
328
+ // Skip pages that fail — may have been deleted or unshared
329
+ }
330
+ }
331
+ // Also fetch targeted pages that aren't in the recent pages list
332
+ const recentPageIds = new Set(pages.map((p) => p.id.replace(/-/g, '')));
333
+ for (const pageId of targetedPageIds) {
334
+ if (!recentPageIds.has(pageId.replace(/-/g, ''))) {
335
+ try {
336
+ const endpoint = `/pages/${pageId}`;
337
+ const page = await notionGet(token, endpoint);
338
+ if (page && page.id) {
339
+ pages.push(page);
340
+ }
341
+ } catch {
342
+ // Skip — page may not exist
343
+ }
344
+ }
345
+ }
346
+ }
347
+
301
348
  // Fetch comment counts for active pages (top 20 by recency)
302
349
  const activePages = pages.slice(0, 20);
303
350
  const commentCounts = {};
@@ -314,6 +361,7 @@ async function pull(config, since) {
314
361
 
315
362
  // Analyze
316
363
  const analysis = analyzeActivity(pages, dbEntries, commentCounts, sinceDate, todayDate, userMap);
364
+ analysis.pageContents = pageContents;
317
365
 
318
366
  // Generate markdown
319
367
  const md = generateMarkdown(analysis, sinceDate, todayDate, timestamp, userMap);
@@ -496,6 +544,86 @@ async function fetchComments(token, pageId) {
496
544
  }
497
545
  }
498
546
 
547
+ // ── Page content extraction ────────────────────────────────────────────────
548
+
549
+ async function fetchPageContent(token, pageId, maxChars = 5000) {
550
+ const blocks = [];
551
+ let cursor = undefined;
552
+ const MAX_REQUESTS = 5;
553
+ let requests = 0;
554
+
555
+ while (requests < MAX_REQUESTS) {
556
+ requests++;
557
+ const endpoint = `/blocks/${pageId}/children?page_size=100` + (cursor ? `&start_cursor=${cursor}` : '');
558
+ let response;
559
+ try {
560
+ response = await notionGet(token, endpoint);
561
+ } catch {
562
+ break;
563
+ }
564
+ const results = Array.isArray(response.results) ? response.results : [];
565
+ blocks.push(...results);
566
+ if (!response.has_more) break;
567
+ cursor = response.next_cursor;
568
+ }
569
+
570
+ // Convert blocks to markdown
571
+ const lines = [];
572
+ let totalChars = 0;
573
+
574
+ for (const block of blocks) {
575
+ if (totalChars >= maxChars) break;
576
+ const line = blockToMarkdown(block);
577
+ if (line !== null) {
578
+ lines.push(line);
579
+ totalChars += line.length;
580
+ }
581
+ }
582
+
583
+ return lines.join('\n');
584
+ }
585
+
586
+ function blockToMarkdown(block) {
587
+ const type = block.type;
588
+ if (!type) return null;
589
+
590
+ const richTextToPlain = (rt) =>
591
+ Array.isArray(rt) ? rt.map((t) => t.plain_text || '').join('') : '';
592
+
593
+ const data = block[type];
594
+ if (!data) return null;
595
+
596
+ switch (type) {
597
+ case 'paragraph':
598
+ return richTextToPlain(data.rich_text);
599
+ case 'heading_1':
600
+ return '# ' + richTextToPlain(data.rich_text);
601
+ case 'heading_2':
602
+ return '## ' + richTextToPlain(data.rich_text);
603
+ case 'heading_3':
604
+ return '### ' + richTextToPlain(data.rich_text);
605
+ case 'bulleted_list_item':
606
+ return '- ' + richTextToPlain(data.rich_text);
607
+ case 'numbered_list_item':
608
+ return '1. ' + richTextToPlain(data.rich_text);
609
+ case 'to_do':
610
+ return (data.checked ? '- [x] ' : '- [ ] ') + richTextToPlain(data.rich_text);
611
+ case 'toggle':
612
+ return '> ' + richTextToPlain(data.rich_text);
613
+ case 'callout':
614
+ return '> ' + richTextToPlain(data.rich_text);
615
+ case 'quote':
616
+ return '> ' + richTextToPlain(data.rich_text);
617
+ case 'code':
618
+ return '```\n' + richTextToPlain(data.rich_text) + '\n```';
619
+ case 'divider':
620
+ return '---';
621
+ default:
622
+ // Skip unsupported block types (image, embed, file, etc.)
623
+ return null;
624
+ }
625
+ }
626
+
499
627
  // ── Property extraction ─────────────────────────────────────────────────────
500
628
 
501
629
  function extractTitle(page) {
@@ -700,6 +828,22 @@ function generateMarkdown(analysis, sinceDate, todayDate, timestamp, userMap) {
700
828
  lines.push('');
701
829
  }
702
830
 
831
+ // Targeted page content
832
+ const pageContents = analysis.pageContents || {};
833
+ if (Object.keys(pageContents).length > 0) {
834
+ lines.push('## Page Content');
835
+ lines.push('');
836
+ for (const [pageId, content] of Object.entries(pageContents)) {
837
+ // Find the page title from the pages list
838
+ const page = analysis.pages.find((p) => p.id.replace(/-/g, '') === pageId.replace(/-/g, ''));
839
+ const title = page ? extractTitle(page) : `Page ${pageId.slice(0, 8)}`;
840
+ lines.push(`### ${sanitizeForMarkdown(title)}`);
841
+ lines.push('');
842
+ lines.push(content);
843
+ lines.push('');
844
+ }
845
+ }
846
+
703
847
  // Summary
704
848
  lines.push('## Summary');
705
849
  lines.push('');
@@ -4322,17 +4322,17 @@ function ensureContainerConfig() {
4322
4322
  changed = true;
4323
4323
  }
4324
4324
 
4325
- // Backfill container-specific paths into existing digest config (fixes configs
4326
- // created before store_path/journal_dir/signals_dir were added)
4325
+ // Override container-specific paths in digest config the mounted connectors.json
4326
+ // may have host paths that don't exist inside the container
4327
4327
  if (config.digest) {
4328
- const defaults = {
4328
+ const containerPaths = {
4329
4329
  store_path: process.env.TEAM_CONTEXT_STORE_PATH || contentStore.DEFAULT_STORE_PATH,
4330
4330
  journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
4331
4331
  signals_dir: process.env.TEAM_CONTEXT_SIGNALS_DIR || contentStore.DEFAULT_SIGNALS_DIR,
4332
4332
  team_context_dir: process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR || '',
4333
4333
  };
4334
- for (const [key, val] of Object.entries(defaults)) {
4335
- if (!config.digest[key]) {
4334
+ for (const [key, val] of Object.entries(containerPaths)) {
4335
+ if (config.digest[key] !== val) {
4336
4336
  config.digest[key] = val;
4337
4337
  changed = true;
4338
4338
  }
@@ -4356,14 +4356,14 @@ function ensureContainerConfig() {
4356
4356
  changed = true;
4357
4357
  }
4358
4358
 
4359
- // Backfill container-specific paths into existing bot config
4359
+ // Override container-specific paths in bot config (same reason as digest above)
4360
4360
  if (config.slack_bot) {
4361
- const botDefaults = {
4361
+ const botPaths = {
4362
4362
  store_path: process.env.TEAM_CONTEXT_STORE_PATH || contentStore.DEFAULT_STORE_PATH,
4363
4363
  journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
4364
4364
  };
4365
- for (const [key, val] of Object.entries(botDefaults)) {
4366
- if (!config.slack_bot[key]) {
4365
+ for (const [key, val] of Object.entries(botPaths)) {
4366
+ if (config.slack_bot[key] !== val) {
4367
4367
  config.slack_bot[key] = val;
4368
4368
  changed = true;
4369
4369
  }
@@ -4414,11 +4414,13 @@ function ensureContainerConfig() {
4414
4414
  // Notion connector
4415
4415
  if (!config.notion && process.env.NOTION_TOKEN) {
4416
4416
  const databases = process.env.TEAM_CONTEXT_NOTION_DATABASES;
4417
+ const pages = process.env.TEAM_CONTEXT_NOTION_PAGES;
4417
4418
  config.notion = {
4418
4419
  transport: 'https',
4419
4420
  token: process.env.NOTION_TOKEN,
4420
4421
  token_env: 'NOTION_TOKEN',
4421
4422
  databases: databases ? databases.split(',').map((d) => d.trim()) : null,
4423
+ pages: pages ? pages.split(',').map((p) => p.trim().replace(/-/g, '')) : null,
4422
4424
  last_pull: null,
4423
4425
  };
4424
4426
  changed = true;
package/doctor.sh CHANGED
@@ -466,6 +466,52 @@ check_storage_backend() {
466
466
  else
467
467
  [ "$VERBOSE" = true ] && info "No connectors.json — signal freshness check skipped"
468
468
  fi
469
+
470
+ # ── 4. Embedding coverage ───────────────────────────────────────────────
471
+ local EMBED_RESULT
472
+ EMBED_RESULT=$(node -e "
473
+ try {
474
+ const storage = require('$SCRIPT_DIR/bin/storage/index.js');
475
+ const storePath = '$WAYFIND_DIR/content-store';
476
+ const backend = storage.getBackend(storePath);
477
+ const idx = backend.loadIndex();
478
+ if (!idx || !idx.entries) { console.log('SKIP'); process.exit(0); }
479
+ let total = 0, embedded = 0;
480
+ for (const e of Object.values(idx.entries)) { total++; if (e.hasEmbedding) embedded++; }
481
+ if (total === 0) { console.log('SKIP'); process.exit(0); }
482
+ const pct = Math.round(100 * embedded / total);
483
+ const hasKey = !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT);
484
+ console.log(pct + ':' + embedded + ':' + total + ':' + hasKey);
485
+ } catch (e) {
486
+ console.log('SKIP:' + e.message.split('\n')[0]);
487
+ }
488
+ " 2>/dev/null) || EMBED_RESULT="SKIP"
489
+
490
+ if [[ "$EMBED_RESULT" != SKIP* ]]; then
491
+ IFS=':' read -r PCT EMBEDDED TOTAL HAS_KEY <<< "$EMBED_RESULT"
492
+ if [ "$PCT" -ge 90 ]; then
493
+ ok "Embeddings: $EMBEDDED/$TOTAL entries ($PCT%)"
494
+ elif [ "$PCT" -ge 50 ]; then
495
+ warn "Embeddings: only $EMBEDDED/$TOTAL entries ($PCT%) — search quality degraded"
496
+ info "Run: wayfind reindex (with OPENAI_API_KEY or AZURE_OPENAI_EMBEDDING_* set)"
497
+ ISSUES=$((ISSUES + 1))
498
+ elif [ "$PCT" -gt 0 ]; then
499
+ err "Embeddings: $EMBEDDED/$TOTAL entries ($PCT%) — semantic search mostly broken"
500
+ info "Run: wayfind reindex (with OPENAI_API_KEY or AZURE_OPENAI_EMBEDDING_* set)"
501
+ ISSUES=$((ISSUES + 1))
502
+ else
503
+ if [ "$HAS_KEY" = "true" ]; then
504
+ err "Embeddings: 0/$TOTAL entries — embedding API key is set but no embeddings generated"
505
+ info "Run: wayfind reindex to generate embeddings"
506
+ ISSUES=$((ISSUES + 1))
507
+ else
508
+ warn "Embeddings: 0/$TOTAL entries — no embedding API key configured"
509
+ info "Set OPENAI_API_KEY or AZURE_OPENAI_EMBEDDING_* then run: wayfind reindex"
510
+ ISSUES=$((ISSUES + 1))
511
+ fi
512
+ fi
513
+ fi
514
+
469
515
  set -e # Restore errexit
470
516
  }
471
517
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.34",
3
+ "version": "2.0.36",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js"