wayfind 2.0.61 → 2.0.63

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/README.md CHANGED
@@ -180,13 +180,11 @@ Auto-registered during `wayfind init`. When a team container is running, the loc
180
180
 
181
181
  | Variable | Description |
182
182
  |----------|-------------|
183
- | `OPENAI_API_KEY` | Semantic search embeddings (full-text works without it) |
183
+ | `OPENAI_API_KEY` | Upgrade semantic search to OpenAI embeddings (Xenova local model is used by default — no key needed) |
184
184
  | `TEAM_CONTEXT_LLM_MODEL` | LLM for digests (default: `claude-sonnet-4-5-20250929`) |
185
185
  | `TEAM_CONTEXT_DIGEST_SCHEDULE` | Cron schedule (default: `0 8 * * 1` — Monday 8am) |
186
186
  | `TEAM_CONTEXT_EXCLUDE_REPOS` | Repos to exclude from digests |
187
187
  | `TEAM_CONTEXT_TELEMETRY` | `true` for anonymous usage telemetry |
188
- | `TEAM_CONTEXT_NO_SLACK` | Run container without Slack integration (set to `1`) |
189
- | `TEAM_CONTEXT_KEY_ROTATE_SCHEDULE` | API key rotation cron (default: `0 2 * * *`) |
190
188
 
191
189
  ---
192
190
 
@@ -421,6 +421,28 @@ function cosineSimilarity(a, b) {
421
421
  * @param {boolean} [options.embeddings] - Generate embeddings (default: true if OPENAI_API_KEY or TEAM_CONTEXT_SIMULATE)
422
422
  * @returns {Promise<Object>} - Stats: { entryCount, newEntries, updatedEntries, skippedEntries, removedEntries }
423
423
  */
424
+
425
+ /**
426
+ * Embed a batch of items concurrently.
427
+ * @param {Array<{id: string, content: string}>} items
428
+ * @param {number} [concurrency=20]
429
+ * @returns {Promise<Map<string, Array|null>>} Map from id to vector (or null on failure)
430
+ */
431
+ async function batchEmbed(items, concurrency = 20) {
432
+ const results = new Map();
433
+ for (let i = 0; i < items.length; i += concurrency) {
434
+ const chunk = items.slice(i, i + concurrency);
435
+ await Promise.all(chunk.map(async ({ id, content }) => {
436
+ try {
437
+ results.set(id, await llm.generateEmbedding(content));
438
+ } catch {
439
+ results.set(id, null);
440
+ }
441
+ }));
442
+ }
443
+ return results;
444
+ }
445
+
424
446
  async function indexJournals(options = {}) {
425
447
  const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
426
448
  const storePath = options.storePath || resolveStorePath();
@@ -497,10 +519,11 @@ async function indexJournals(options = {}) {
497
519
  }
498
520
  }
499
521
 
500
- // Compute diffs
522
+ // Compute diffs — collect embedding work for batching
501
523
  const stats = { entryCount: 0, newEntries: 0, updatedEntries: 0, skippedEntries: 0, removedEntries: 0 };
502
524
  const finalEntries = {};
503
525
  const finalEmbeddings = { ...existingEmbeddings };
526
+ const pendingEmbeds = []; // { id, content, reason: 'update'|'changed'|'new' }
504
527
 
505
528
  for (const [id, entry] of Object.entries(newEntries)) {
506
529
  const existing = existingEntries[id];
@@ -511,46 +534,38 @@ async function indexJournals(options = {}) {
511
534
  // Unchanged — but generate embedding if missing and embeddings are enabled
512
535
  entry.hasEmbedding = existing.hasEmbedding;
513
536
  if (doEmbeddings && !existing.hasEmbedding && content) {
514
- try {
515
- const vec = await llm.generateEmbedding(content);
516
- finalEmbeddings[id] = vec;
517
- entry.hasEmbedding = true;
518
- stats.updatedEntries++;
519
- } catch (err) {
520
- stats.skippedEntries++;
521
- }
537
+ pendingEmbeds.push({ id, content, reason: 'update' });
522
538
  } else {
523
539
  stats.skippedEntries++;
524
540
  }
525
541
  finalEntries[id] = entry;
526
542
  } else if (existing) {
527
543
  // Changed — re-embed
528
- if (doEmbeddings) {
529
- try {
530
- const vec = await llm.generateEmbedding(content);
531
- finalEmbeddings[id] = vec;
532
- entry.hasEmbedding = true;
533
- } catch (err) {
534
- // Keep going without embedding for this entry
535
- entry.hasEmbedding = false;
536
- delete finalEmbeddings[id];
537
- }
538
- }
539
544
  finalEntries[id] = entry;
540
545
  stats.updatedEntries++;
546
+ if (doEmbeddings) pendingEmbeds.push({ id, content, reason: 'changed' });
541
547
  } else {
542
548
  // New entry
543
- if (doEmbeddings) {
544
- try {
545
- const vec = await llm.generateEmbedding(content);
546
- finalEmbeddings[id] = vec;
547
- entry.hasEmbedding = true;
548
- } catch (err) {
549
- entry.hasEmbedding = false;
550
- }
551
- }
552
549
  finalEntries[id] = entry;
553
550
  stats.newEntries++;
551
+ if (doEmbeddings) pendingEmbeds.push({ id, content, reason: 'new' });
552
+ }
553
+ }
554
+
555
+ // Batch embed all pending entries concurrently
556
+ if (pendingEmbeds.length > 0) {
557
+ const embedResults = await batchEmbed(pendingEmbeds);
558
+ for (const { id, reason } of pendingEmbeds) {
559
+ const vec = embedResults.get(id);
560
+ if (vec) {
561
+ finalEmbeddings[id] = vec;
562
+ finalEntries[id].hasEmbedding = true;
563
+ if (reason === 'update') stats.updatedEntries++;
564
+ } else {
565
+ if (reason === 'update') stats.skippedEntries++;
566
+ else if (reason === 'changed') { finalEntries[id].hasEmbedding = false; delete finalEmbeddings[id]; }
567
+ // 'new': hasEmbedding stays false
568
+ }
554
569
  }
555
570
  }
556
571
 
@@ -1582,18 +1597,6 @@ async function indexConversations(options = {}) {
1582
1597
  };
1583
1598
  convEntry.qualityScore = computeQualityScore(convEntry);
1584
1599
  existingIndex.entries[id] = convEntry;
1585
-
1586
- if (doEmbeddings) {
1587
- try {
1588
- const vec = await llm.generateEmbedding(content);
1589
- existingEmbeddings[id] = vec;
1590
- existingIndex.entries[id].hasEmbedding = true;
1591
- } catch {
1592
- // Continue without embedding
1593
- }
1594
- }
1595
-
1596
- delete existingIndex.entries[id]._content;
1597
1600
  entryIds.push(id);
1598
1601
  stats.decisionsExtracted++;
1599
1602
  }
@@ -1608,6 +1611,29 @@ async function indexConversations(options = {}) {
1608
1611
  stats.transcriptsProcessed++;
1609
1612
  }
1610
1613
 
1614
+ // Batch embed all new conversation entries
1615
+ if (doEmbeddings) {
1616
+ const pendingEmbeds = Object.entries(existingIndex.entries)
1617
+ .filter(([, e]) => e._content && !e.hasEmbedding)
1618
+ .map(([id, e]) => ({ id, content: e._content }));
1619
+
1620
+ if (pendingEmbeds.length > 0) {
1621
+ const embedResults = await batchEmbed(pendingEmbeds);
1622
+ for (const { id } of pendingEmbeds) {
1623
+ const vec = embedResults.get(id);
1624
+ if (vec) {
1625
+ existingEmbeddings[id] = vec;
1626
+ existingIndex.entries[id].hasEmbedding = true;
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+
1632
+ // Strip temp _content fields
1633
+ for (const entry of Object.values(existingIndex.entries)) {
1634
+ delete entry._content;
1635
+ }
1636
+
1611
1637
  // Save everything
1612
1638
  existingIndex.entryCount = Object.keys(existingIndex.entries).length;
1613
1639
  if (doEmbeddings) {
@@ -1876,6 +1902,7 @@ async function indexSignals(options = {}) {
1876
1902
  const existingEmbeddings = doEmbeddings ? (_sigModelChanged ? {} : backend.loadEmbeddings()) : {};
1877
1903
 
1878
1904
  const stats = { fileCount: 0, newEntries: 0, updatedEntries: 0, skippedEntries: 0 };
1905
+ const signalPendingEmbeds = []; // { id, content, reason: 'update'|'changed'|'new'|'chunk-update'|'chunk-new' }
1879
1906
 
1880
1907
  // Scan channel directories
1881
1908
  let channels;
@@ -1960,14 +1987,7 @@ async function indexSignals(options = {}) {
1960
1987
  if (existing && existing.contentHash === hash) {
1961
1988
  // Unchanged — but generate embedding if missing
1962
1989
  if (doEmbeddings && !existing.hasEmbedding && content) {
1963
- try {
1964
- const vec = await llm.generateEmbedding(content);
1965
- existingEmbeddings[id] = vec;
1966
- existing.hasEmbedding = true;
1967
- stats.updatedEntries++;
1968
- } catch {
1969
- stats.skippedEntries++;
1970
- }
1990
+ signalPendingEmbeds.push({ id, content, reason: 'update' });
1971
1991
  } else {
1972
1992
  stats.skippedEntries++;
1973
1993
  }
@@ -1985,16 +2005,7 @@ async function indexSignals(options = {}) {
1985
2005
  tags,
1986
2006
  hasEmbedding: false,
1987
2007
  };
1988
-
1989
- if (doEmbeddings) {
1990
- try {
1991
- const vec = await llm.generateEmbedding(content);
1992
- existingEmbeddings[id] = vec;
1993
- existingIndex.entries[id].hasEmbedding = true;
1994
- } catch {
1995
- delete existingEmbeddings[id];
1996
- }
1997
- }
2008
+ if (doEmbeddings) signalPendingEmbeds.push({ id, content, reason: 'changed' });
1998
2009
  stats.updatedEntries++;
1999
2010
  } else {
2000
2011
  // New entry
@@ -2010,16 +2021,7 @@ async function indexSignals(options = {}) {
2010
2021
  tags,
2011
2022
  hasEmbedding: false,
2012
2023
  };
2013
-
2014
- if (doEmbeddings) {
2015
- try {
2016
- const vec = await llm.generateEmbedding(content);
2017
- existingEmbeddings[id] = vec;
2018
- existingIndex.entries[id].hasEmbedding = true;
2019
- } catch {
2020
- // Continue without embedding
2021
- }
2022
- }
2024
+ if (doEmbeddings) signalPendingEmbeds.push({ id, content, reason: 'new' });
2023
2025
  stats.newEntries++;
2024
2026
  }
2025
2027
  }
@@ -2095,13 +2097,7 @@ async function indexSignals(options = {}) {
2095
2097
 
2096
2098
  if (existingChunk && existingChunk.contentHash === chunkHash) {
2097
2099
  if (doEmbeddings && !existingChunk.hasEmbedding) {
2098
- try {
2099
- const vec = await llm.generateEmbedding(section);
2100
- existingEmbeddings[chunkId] = vec;
2101
- existingChunk.hasEmbedding = true;
2102
- } catch {
2103
- // Skip
2104
- }
2100
+ signalPendingEmbeds.push({ id: chunkId, content: section, reason: 'chunk-update' });
2105
2101
  }
2106
2102
  continue;
2107
2103
  }
@@ -2122,13 +2118,24 @@ async function indexSignals(options = {}) {
2122
2118
  };
2123
2119
 
2124
2120
  if (doEmbeddings) {
2125
- try {
2126
- const vec = await llm.generateEmbedding(section);
2127
- existingEmbeddings[chunkId] = vec;
2128
- existingIndex.entries[chunkId].hasEmbedding = true;
2129
- } catch {
2130
- // Continue without embedding
2131
- }
2121
+ signalPendingEmbeds.push({ id: chunkId, content: section, reason: 'chunk-new' });
2122
+ }
2123
+ }
2124
+ }
2125
+
2126
+ // Batch embed all pending signal entries
2127
+ if (signalPendingEmbeds.length > 0) {
2128
+ const embedResults = await batchEmbed(signalPendingEmbeds);
2129
+ for (const { id, reason } of signalPendingEmbeds) {
2130
+ const vec = embedResults.get(id);
2131
+ if (vec) {
2132
+ existingEmbeddings[id] = vec;
2133
+ existingIndex.entries[id].hasEmbedding = true;
2134
+ if (reason === 'update') stats.updatedEntries++;
2135
+ } else {
2136
+ if (reason === 'update') stats.skippedEntries++;
2137
+ else if (reason === 'changed') { existingIndex.entries[id].hasEmbedding = false; delete existingEmbeddings[id]; }
2138
+ // 'new', 'chunk-update', 'chunk-new': hasEmbedding stays false
2132
2139
  }
2133
2140
  }
2134
2141
  }
@@ -203,11 +203,13 @@ class SqliteBackend {
203
203
  for (const row of rows) {
204
204
  entries[row.id] = rowToEntry(row);
205
205
  }
206
+ const embeddingModelRow = this.db.prepare("SELECT value FROM metadata WHERE key = 'embedding_model'").get();
206
207
  return {
207
208
  version: INDEX_VERSION,
208
209
  lastUpdated: Date.now(),
209
210
  entryCount: rows.length,
210
211
  entries,
212
+ ...(embeddingModelRow ? { embedding_model: embeddingModelRow.value } : {}),
211
213
  };
212
214
  }
213
215
 
@@ -228,6 +230,9 @@ class SqliteBackend {
228
230
  for (const [id, entry] of Object.entries(entries)) {
229
231
  stmt.run(entryToRow(id, entry));
230
232
  }
233
+ if (index.embedding_model) {
234
+ this.db.prepare('INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)').run('embedding_model', index.embedding_model);
235
+ }
231
236
  });
232
237
  txn();
233
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.61",
3
+ "version": "2.0.63",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.61",
3
+ "version": "2.0.63",
4
4
  "description": "Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.",
5
5
  "author": {
6
6
  "name": "Wayfind",
package/setup.sh CHANGED
@@ -441,6 +441,41 @@ PYEOF
441
441
  fi
442
442
  fi
443
443
 
444
+ # MCP server — register wayfind-mcp in settings.json
445
+ MCP_REGISTERED=false
446
+ grep -q '"wayfind"' "$SETTINGS" 2>/dev/null && MCP_REGISTERED=true
447
+ if [ "$MCP_REGISTERED" = false ]; then
448
+ if [ "$DRY_RUN" = false ]; then
449
+ TMP_SETTINGS="$(mktemp)"
450
+ if python3 - "$SETTINGS" "$TMP_SETTINGS" <<'PYEOF' 2>/dev/null; then
451
+ import json, sys
452
+ settings_path, out_path = sys.argv[1], sys.argv[2]
453
+ try:
454
+ with open(settings_path) as f:
455
+ settings = json.load(f)
456
+ except (json.JSONDecodeError, IOError):
457
+ sys.exit(1)
458
+ mcp = settings.setdefault("mcpServers", {})
459
+ if "wayfind" not in mcp:
460
+ mcp["wayfind"] = {"command": "wayfind-mcp"}
461
+ with open(out_path, "w") as f:
462
+ json.dump(settings, f, indent=2)
463
+ f.write("\n")
464
+ PYEOF
465
+ mv "$TMP_SETTINGS" "$SETTINGS"
466
+ log "Registered wayfind MCP server in ~/.claude/settings.json"
467
+ else
468
+ rm -f "$TMP_SETTINGS"
469
+ warn "Could not register MCP server — add manually to ~/.claude/settings.json:"
470
+ warn ' "mcpServers": { "wayfind": { "command": "wayfind-mcp" } }'
471
+ fi
472
+ else
473
+ info "[dry-run] Would register wayfind MCP server in $SETTINGS"
474
+ fi
475
+ else
476
+ info "MCP server already registered in settings.json — skipped"
477
+ fi
478
+
444
479
  ;;
445
480
 
446
481
  cursor)
@@ -470,6 +505,48 @@ PYEOF
470
505
  warn "--repo path not found or not a directory: $REPO_DIR"
471
506
  fi
472
507
  fi
508
+
509
+ # MCP server — register wayfind-mcp in ~/.cursor/mcp.json
510
+ CURSOR_MCP="$HOME/.cursor/mcp.json"
511
+ run mkdir -p "$HOME/.cursor"
512
+ CURSOR_MCP_REGISTERED=false
513
+ grep -q '"wayfind"' "$CURSOR_MCP" 2>/dev/null && CURSOR_MCP_REGISTERED=true
514
+ if [ "$CURSOR_MCP_REGISTERED" = false ]; then
515
+ if [ "$DRY_RUN" = false ]; then
516
+ if [ ! -f "$CURSOR_MCP" ]; then
517
+ printf '{\n "mcpServers": {\n "wayfind": {\n "command": "wayfind-mcp",\n "args": []\n }\n }\n}\n' > "$CURSOR_MCP"
518
+ log "Created ~/.cursor/mcp.json with wayfind MCP server"
519
+ else
520
+ TMP_MCP="$(mktemp)"
521
+ if python3 - "$CURSOR_MCP" "$TMP_MCP" <<'PYEOF' 2>/dev/null; then
522
+ import json, sys
523
+ mcp_path, out_path = sys.argv[1], sys.argv[2]
524
+ try:
525
+ with open(mcp_path) as f:
526
+ config = json.load(f)
527
+ except (json.JSONDecodeError, IOError):
528
+ config = {}
529
+ mcp = config.setdefault("mcpServers", {})
530
+ if "wayfind" not in mcp:
531
+ mcp["wayfind"] = {"command": "wayfind-mcp", "args": []}
532
+ with open(out_path, "w") as f:
533
+ json.dump(config, f, indent=2)
534
+ f.write("\n")
535
+ PYEOF
536
+ mv "$TMP_MCP" "$CURSOR_MCP"
537
+ log "Registered wayfind MCP server in ~/.cursor/mcp.json"
538
+ else
539
+ rm -f "$TMP_MCP"
540
+ warn "Could not register MCP server — add manually to ~/.cursor/mcp.json:"
541
+ warn ' "mcpServers": { "wayfind": { "command": "wayfind-mcp", "args": [] } }'
542
+ fi
543
+ fi
544
+ else
545
+ info "[dry-run] Would register wayfind MCP server in ~/.cursor/mcp.json"
546
+ fi
547
+ else
548
+ info "MCP server already registered in ~/.cursor/mcp.json — skipped"
549
+ fi
473
550
  ;;
474
551
 
475
552
  generic)