wayfind 2.0.9 → 2.0.12
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/bin/content-store.js +33 -9
- package/bin/digest.js +47 -10
- package/bin/slack-bot.js +19 -0
- package/bin/team-context.js +3 -2
- package/package.json +1 -1
- package/specializations/claude-code/hooks/session-end.sh +23 -3
- package/templates/deploy/.env.example +5 -2
- package/templates/deploy/docker-compose.yml +3 -1
package/bin/content-store.js
CHANGED
|
@@ -39,11 +39,14 @@ const ENTRY_HEADER_RE = /^##\s+(.+?)\s+[—–]\s+(.+)$/;
|
|
|
39
39
|
const FIELD_RE = /^\*\*([^*:]+):\*\*\s*(.*)$/;
|
|
40
40
|
const DATE_FILE_RE = /^(\d{4}-\d{2}-\d{2})(?:-([a-z0-9._-]+))?\.md$/;
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
// Team repo allowlist — when set, only entries from matching repos are indexed/queried.
|
|
43
|
+
// Replaces the old TEAM_CONTEXT_EXCLUDE_REPOS blocklist approach.
|
|
44
|
+
// Format: comma-separated, supports org/* wildcards (e.g., "HopSkipInc/*,Doorbell/*")
|
|
45
|
+
const INCLUDE_REPOS = (process.env.TEAM_CONTEXT_INCLUDE_REPOS || '')
|
|
46
|
+
.split(',').map(r => r.trim().toLowerCase()).filter(Boolean);
|
|
47
|
+
|
|
48
|
+
// DEPRECATED: Legacy blocklist — use TEAM_CONTEXT_INCLUDE_REPOS instead.
|
|
49
|
+
// Still read for backward compatibility during migration.
|
|
47
50
|
const EXCLUDE_REPOS = (process.env.TEAM_CONTEXT_EXCLUDE_REPOS || '')
|
|
48
51
|
.split(',').map(r => r.trim().toLowerCase()).filter(Boolean);
|
|
49
52
|
|
|
@@ -52,13 +55,33 @@ const DRIFT_POSITIVE = ['drift', 'drifted', 'tangent', 'pivoted', 'sidetracked',
|
|
|
52
55
|
const DRIFT_NEGATIVE = ['no drift', 'no tangent', 'on track', 'focused', 'laser focused', 'stayed focused'];
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
|
-
* Check if a repo
|
|
56
|
-
*
|
|
58
|
+
* Check if a repo should be filtered out.
|
|
59
|
+
* If INCLUDE_REPOS is set: only allow repos matching the allowlist.
|
|
60
|
+
* If only EXCLUDE_REPOS is set (legacy): block repos on the blocklist.
|
|
61
|
+
* Supports org/* wildcard patterns.
|
|
57
62
|
*/
|
|
58
63
|
function isRepoExcluded(repo) {
|
|
59
|
-
if (!
|
|
64
|
+
if (!repo) return (INCLUDE_REPOS.length > 0); // exclude null repo only when allowlist active
|
|
60
65
|
const lower = repo.toLowerCase();
|
|
61
|
-
|
|
66
|
+
|
|
67
|
+
// Allowlist takes priority — if set, only matching repos pass
|
|
68
|
+
if (INCLUDE_REPOS.length > 0) {
|
|
69
|
+
return !INCLUDE_REPOS.some(pattern => {
|
|
70
|
+
if (pattern.endsWith('/*')) {
|
|
71
|
+
// org/* wildcard — match org prefix
|
|
72
|
+
const org = pattern.slice(0, -2);
|
|
73
|
+
return lower.startsWith(org + '/');
|
|
74
|
+
}
|
|
75
|
+
return lower === pattern || lower.endsWith('/' + pattern) || lower.startsWith(pattern + '/');
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Legacy blocklist fallback
|
|
80
|
+
if (EXCLUDE_REPOS.length > 0) {
|
|
81
|
+
return EXCLUDE_REPOS.some(ex => lower === ex || lower.endsWith('/' + ex) || lower.startsWith(ex + '/'));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
// ── Journal parsing ─────────────────────────────────────────────────────────
|
|
@@ -1974,6 +1997,7 @@ module.exports = {
|
|
|
1974
1997
|
saveConversationIndex: (storePath, convIndex) => getBackend(storePath || DEFAULT_STORE_PATH).saveConversationIndex(convIndex),
|
|
1975
1998
|
|
|
1976
1999
|
// Filtering
|
|
2000
|
+
isRepoExcluded,
|
|
1977
2001
|
applyFilters,
|
|
1978
2002
|
|
|
1979
2003
|
// Core operations
|
package/bin/digest.js
CHANGED
|
@@ -11,13 +11,32 @@ const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
|
11
11
|
const WAYFIND_DIR = path.join(HOME, '.claude', 'team-context');
|
|
12
12
|
const ENV_FILE = path.join(WAYFIND_DIR, '.env');
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// Team repo allowlist — mirrors content-store.js logic for digest-time filtering.
|
|
15
|
+
// When INCLUDE_REPOS is set, sections mentioning repos NOT on the list are removed.
|
|
16
|
+
// Falls back to EXCLUDE_REPOS (legacy) for backward compatibility.
|
|
17
|
+
const INCLUDE_REPOS_RAW = (process.env.TEAM_CONTEXT_INCLUDE_REPOS || '')
|
|
18
|
+
.split(',').map(r => r.trim()).filter(Boolean);
|
|
15
19
|
const EXCLUDE_REPOS_RAW = (process.env.TEAM_CONTEXT_EXCLUDE_REPOS || '')
|
|
16
20
|
.split(',').map(r => r.trim()).filter(Boolean);
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Check if a repo name matches the include list.
|
|
24
|
+
* Supports org/* wildcards.
|
|
25
|
+
*/
|
|
26
|
+
function isRepoIncluded(repo) {
|
|
27
|
+
if (INCLUDE_REPOS_RAW.length === 0) return true; // no filter = include all
|
|
28
|
+
const lower = repo.toLowerCase();
|
|
29
|
+
return INCLUDE_REPOS_RAW.some(pattern => {
|
|
30
|
+
const p = pattern.toLowerCase();
|
|
31
|
+
if (p.endsWith('/*')) {
|
|
32
|
+
return lower.startsWith(p.slice(0, -2) + '/');
|
|
33
|
+
}
|
|
34
|
+
return lower === p || lower.endsWith('/' + p) || lower.startsWith(p + '/');
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
function buildExcludePattern() {
|
|
19
39
|
if (EXCLUDE_REPOS_RAW.length === 0) return null;
|
|
20
|
-
// Match repo names as whole words (case-insensitive)
|
|
21
40
|
const escaped = EXCLUDE_REPOS_RAW.map(r => r.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
22
41
|
return new RegExp(`\\b(${escaped.join('|')})\\b`, 'i');
|
|
23
42
|
}
|
|
@@ -25,15 +44,33 @@ function buildExcludePattern() {
|
|
|
25
44
|
const EXCLUDE_CONTENT_RE = buildExcludePattern();
|
|
26
45
|
|
|
27
46
|
/**
|
|
28
|
-
* Filter assembled content sections by
|
|
29
|
-
*
|
|
47
|
+
* Filter assembled content sections by team boundaries.
|
|
48
|
+
* Sections are separated by \n\n---\n\n.
|
|
49
|
+
* If INCLUDE_REPOS is set: keep only sections mentioning included repos.
|
|
50
|
+
* Falls back to EXCLUDE_REPOS regex for backward compatibility.
|
|
30
51
|
*/
|
|
31
52
|
function filterExcludedContent(content) {
|
|
32
|
-
if (!
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
53
|
+
if (!content) return content;
|
|
54
|
+
|
|
55
|
+
const sections = content.split('\n\n---\n\n');
|
|
56
|
+
|
|
57
|
+
if (INCLUDE_REPOS_RAW.length > 0) {
|
|
58
|
+
// Allowlist mode: extract repo from section header (### DATE — REPO) and check
|
|
59
|
+
return sections.filter(section => {
|
|
60
|
+
const repoMatch = section.match(/^###\s+\S+\s+[—–]\s+(\S+)/);
|
|
61
|
+
if (!repoMatch) return true; // keep non-journal sections (signals, etc.)
|
|
62
|
+
return isRepoIncluded(repoMatch[1]);
|
|
63
|
+
}).join('\n\n---\n\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Legacy: regex blocklist
|
|
67
|
+
if (EXCLUDE_CONTENT_RE) {
|
|
68
|
+
return sections
|
|
69
|
+
.filter(section => !EXCLUDE_CONTENT_RE.test(section))
|
|
70
|
+
.join('\n\n---\n\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return content;
|
|
37
74
|
}
|
|
38
75
|
|
|
39
76
|
// ── Env file helpers ────────────────────────────────────────────────────────
|
|
@@ -636,7 +673,7 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
|
|
|
636
673
|
journalContent = collectJournals(sinceDate, config.journal_dir);
|
|
637
674
|
}
|
|
638
675
|
|
|
639
|
-
// Filter
|
|
676
|
+
// Filter by team boundaries (TEAM_CONTEXT_INCLUDE_REPOS allowlist, falls back to EXCLUDE_REPOS)
|
|
640
677
|
journalContent = filterExcludedContent(journalContent);
|
|
641
678
|
signalContent = filterExcludedContent(signalContent);
|
|
642
679
|
|
package/bin/slack-bot.js
CHANGED
|
@@ -536,6 +536,25 @@ async function searchDecisionTrail(query, config) {
|
|
|
536
536
|
results = contentStore.searchText(searchQuery, searchOpts);
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
// If text search returned results but none are journal entries (all signals/conversations),
|
|
540
|
+
// and we have date filters, also try metadata browse to find actual journal entries.
|
|
541
|
+
// This handles broad queries like "today's activity" where keyword search matches
|
|
542
|
+
// signal files but misses the journal entries the user actually wants.
|
|
543
|
+
if (results && results.length > 0 && (dateFilters.since || dateFilters.until)) {
|
|
544
|
+
const hasJournalResults = results.some(r => {
|
|
545
|
+
const source = r.entry?.source;
|
|
546
|
+
return !source || source === 'journal';
|
|
547
|
+
});
|
|
548
|
+
if (!hasJournalResults) {
|
|
549
|
+
const browseOpts = { ...searchOpts };
|
|
550
|
+
const metadataResults = contentStore.queryMetadata(browseOpts);
|
|
551
|
+
if (metadataResults.length > 0) {
|
|
552
|
+
// Prefer metadata browse (actual journal entries) over signal-only text search
|
|
553
|
+
results = metadataResults.slice(0, searchOpts.limit).map(r => ({ ...r, score: 1 }));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
539
558
|
// If date-filtered search returned empty, try browsing by date
|
|
540
559
|
if ((!results || results.length === 0) && (dateFilters.since || dateFilters.until)) {
|
|
541
560
|
const browseOpts = { ...searchOpts };
|
package/bin/team-context.js
CHANGED
|
@@ -1311,8 +1311,9 @@ async function runIndexConversationsWithExport(args, detectShifts = false) {
|
|
|
1311
1311
|
}
|
|
1312
1312
|
}
|
|
1313
1313
|
|
|
1314
|
-
// Context shift detection — single classification per reindex run
|
|
1315
|
-
|
|
1314
|
+
// Context shift detection — single classification per reindex run.
|
|
1315
|
+
// Skip entirely when no new decisions were extracted (saves an LLM call).
|
|
1316
|
+
if (detectShifts && stats.decisionsExtracted > 0 && stats.pendingExports && stats.pendingExports.length > 0) {
|
|
1316
1317
|
console.log('');
|
|
1317
1318
|
console.log('=== Context Shift Detection ===');
|
|
1318
1319
|
|
package/package.json
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# Runs incremental conversation indexing with journal export after each session.
|
|
4
4
|
# Extracted decisions get written to the journal directory so they sync via git
|
|
5
5
|
# and the container's journal indexer picks them up.
|
|
6
|
+
#
|
|
7
|
+
# Performance target: <5s for the common case (no new conversations to process).
|
|
6
8
|
|
|
7
9
|
set -euo pipefail
|
|
8
10
|
|
|
@@ -24,6 +26,20 @@ if [ -z "$WAYFIND" ]; then
|
|
|
24
26
|
fi
|
|
25
27
|
fi
|
|
26
28
|
|
|
29
|
+
# ── Fast path: skip reindex if no conversation files changed ──────────────────
|
|
30
|
+
# The full reindex pipeline (load store, scan transcripts, hash check, LLM calls)
|
|
31
|
+
# has a ~2-3s baseline cost even when nothing changed. This filesystem check
|
|
32
|
+
# short-circuits in <50ms for the common case.
|
|
33
|
+
LAST_RUN_FILE="$HOME/.claude/team-context/.last-reindex"
|
|
34
|
+
if [ -f "$LAST_RUN_FILE" ]; then
|
|
35
|
+
CHANGED=$(find "$HOME/.claude/projects" -name "*.jsonl" -newer "$LAST_RUN_FILE" 2>/dev/null | head -1)
|
|
36
|
+
if [ -z "$CHANGED" ]; then
|
|
37
|
+
# No conversation files changed — skip expensive reindex, just sync journals
|
|
38
|
+
$WAYFIND journal sync 2>/dev/null &
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
|
|
27
43
|
# Run incremental reindex (conversations only — journals are handled by the journal write itself)
|
|
28
44
|
# --conversations-only: skip journals (just written by the session, no need to re-index)
|
|
29
45
|
# --export: write extracted decisions as journal entries for git sync
|
|
@@ -31,6 +47,10 @@ fi
|
|
|
31
47
|
# --write-stats: write session stats JSON for status line display
|
|
32
48
|
$WAYFIND reindex --conversations-only --export --detect-shifts --write-stats 2>/dev/null || true
|
|
33
49
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
$
|
|
50
|
+
# Update the marker so the next session's fast-path check works
|
|
51
|
+
mkdir -p "$HOME/.claude/team-context"
|
|
52
|
+
touch "$LAST_RUN_FILE"
|
|
53
|
+
|
|
54
|
+
# Sync authored journals to team-context repo (commit + push) — backgrounded
|
|
55
|
+
# so the session can exit immediately. Git push is the slowest part (~1-3s).
|
|
56
|
+
$WAYFIND journal sync 2>/dev/null &
|
|
@@ -35,8 +35,11 @@ GITHUB_TOKEN=
|
|
|
35
35
|
# Tenant identifier (prefixes storage paths)
|
|
36
36
|
# TEAM_CONTEXT_TENANT_ID=my-team
|
|
37
37
|
|
|
38
|
-
#
|
|
39
|
-
#
|
|
38
|
+
# Team repo allowlist — only journals from these repos appear in digests and queries.
|
|
39
|
+
# Supports org/* wildcards. This is the recommended way to scope a container to one team.
|
|
40
|
+
# TEAM_CONTEXT_INCLUDE_REPOS=MyOrg/*,MyOrg-Libs/*
|
|
41
|
+
|
|
42
|
+
# DEPRECATED: Blocklist approach. Use INCLUDE_REPOS instead.
|
|
40
43
|
# TEAM_CONTEXT_EXCLUDE_REPOS=wayfind,personal-project
|
|
41
44
|
|
|
42
45
|
# Encryption key — generate with: openssl rand -base64 32
|
|
@@ -18,7 +18,9 @@ services:
|
|
|
18
18
|
SLACK_DIGEST_CHANNEL: ${SLACK_DIGEST_CHANNEL:-}
|
|
19
19
|
TEAM_CONTEXT_SLACK_WEBHOOK: ${TEAM_CONTEXT_SLACK_WEBHOOK:-}
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# Team repo allowlist (recommended over EXCLUDE_REPOS)
|
|
22
|
+
TEAM_CONTEXT_INCLUDE_REPOS: ${TEAM_CONTEXT_INCLUDE_REPOS:-}
|
|
23
|
+
# DEPRECATED: Blocklist approach — use INCLUDE_REPOS instead
|
|
22
24
|
TEAM_CONTEXT_EXCLUDE_REPOS: ${TEAM_CONTEXT_EXCLUDE_REPOS:-}
|
|
23
25
|
|
|
24
26
|
# Telemetry (opt-in, sends anonymous usage data to improve Wayfind)
|