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 +1 -3
- package/bin/content-store.js +90 -83
- package/bin/storage/sqlite-backend.js +5 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/setup.sh +77 -0
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` |
|
|
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
|
|
package/bin/content-store.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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
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)
|