wayfind 2.0.69 → 2.0.71

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
@@ -1,68 +1,55 @@
1
1
  # Wayfind
2
2
 
3
- **Team decision trail for AI-assisted development.**
3
+ **Team memory for AI-assisted engineering.**
4
4
 
5
- > AI makes individual engineers faster. Nobody has solved coherent, maintainable software built by a *team* over time. Every handoff loses context. Wayfind captures it.
5
+ Your AI coding assistant forgets everything between sessions. Wayfind gives it a memory for you and your whole team.
6
+
7
+ Plain markdown files. No infrastructure. Works with any MCP client.
6
8
 
7
9
  [![CI](https://github.com/usewayfind/wayfind/actions/workflows/ci.yml/badge.svg)](https://github.com/usewayfind/wayfind/actions/workflows/ci.yml)
8
- [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
9
10
  [![npm](https://img.shields.io/npm/v/wayfind)](https://www.npmjs.com/package/wayfind)
11
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
10
12
 
11
- ---
12
-
13
- ## Install
14
-
15
- ### Claude Code plugin (recommended)
13
+ **Works with:** Claude Code | Cursor | Any MCP client
16
14
 
17
- In a Claude Code session:
15
+ ---
18
16
 
19
- ```
20
- /plugin marketplace add usewayfind/wayfind
21
- /plugin install wayfind@usewayfind
22
- ```
17
+ ## The Problem
23
18
 
24
- Then initialize a repo:
19
+ AI coding assistants are stateless. Every session starts cold. The decisions you made yesterday, the architecture you agreed on last week, the reason you chose Postgres over Mongo — gone. You re-explain, or the AI guesses wrong.
25
20
 
26
- ```
27
- /wayfind:init-memory
28
- ```
21
+ Now multiply that across a team. Five engineers, each with their own AI sessions, none of them aware of what the others decided. Your PM reads the standup notes, but the AI that writes the code doesn't.
29
22
 
30
- Your AI sessions now resume where you left off instead of cold-starting.
23
+ ## What Wayfind Does
31
24
 
32
- ### npm CLI
25
+ **For you:** Sessions resume where they left off. Decisions are extracted automatically and become searchable history. Drift detection flags when work veers from the stated goal.
33
26
 
34
- The plugin includes the CLI, but you can also install it standalone:
27
+ **For your team:** A daily digest summarizes what everyone shipped, decided, and discovered tailored per role (engineering, product, design, strategy). Team members who use AI tools get session memory directly. Everyone else gets a Slack digest.
35
28
 
36
- ```bash
37
- npm install -g wayfind
38
- wayfind init
39
- ```
29
+ **For your AI tools:** An MCP server exposes your team's full decision history as tools. Claude, Cursor, or any MCP client can search decisions, browse by date, and retrieve context — no file reading or guessing.
40
30
 
41
- The CLI is required for digest generation, Slack bot, signal connectors, and team management. The plugin handles session hooks and slash commands.
31
+ <!-- TODO: demo GIF here see #175 -->
42
32
 
43
33
  ---
44
34
 
45
- ## What It Does
35
+ ## Quick Start
46
36
 
47
- ### For You (solo engineer)
37
+ ### Option A: Claude Code plugin
48
38
 
49
- - **Persistent memory** — sessions pick up where the last one ended
50
- - **Automatic journal** — decisions and rationale extracted from every session
51
- - **Searchable history** — `wayfind search-journals "auth refactor"`
52
- - **Drift detection** — AI flags when work drifts from the stated goal
53
-
54
- ### For Your Team
55
-
56
- - **Digests** — weekly summaries tailored per role (engineering, product, design, strategy)
57
- - **Slack bot** — anyone on the team asks `@wayfind` and gets answers from the decision trail
58
- - **Signal connectors** — pull context from GitHub, Intercom, and Notion into digests
59
- - **Context shift detection** — surfaces significant pivots and architecture changes
39
+ ```
40
+ /plugin marketplace add usewayfind/wayfind
41
+ /plugin install wayfind@usewayfind
42
+ /wayfind:init-memory
43
+ ```
60
44
 
61
- Only the engineer installs anything. Everyone else sees a Slack digest.
45
+ ### Option B: npm (works with any AI tool)
62
46
 
47
+ ```bash
48
+ npm install -g wayfind
49
+ wayfind init
63
50
  ```
64
- /wayfind:init-team # Set up team context, journals, and digests
65
- ```
51
+
52
+ Your next AI session has memory. That's it.
66
53
 
67
54
  ---
68
55
 
@@ -85,118 +72,72 @@ All context is plain markdown in directories you control:
85
72
 
86
73
  No proprietary formats. No vendor lock-in. `grep` works if Wayfind breaks.
87
74
 
88
- ### The Session Protocol
89
-
90
- **Start:** AI reads state files, summarizes context, asks "What's the goal?"
91
-
92
- **Mid-session:** If work drifts from the goal, the AI flags it.
75
+ ### The Session Loop
93
76
 
94
- **End:** Decisions are extracted, written as journal entries, and synced to the team repo — automatically via hooks.
77
+ 1. **Start** AI reads state files, summarizes context, asks "What's the goal?"
78
+ 2. **Mid-session** — If work drifts from the goal, the AI flags it.
79
+ 3. **End** — Decisions are extracted, written as journal entries, and synced to the team repo — automatically via hooks.
95
80
 
96
- ### Digests
81
+ ### Team Digests
97
82
 
98
- Each role sees the same underlying data through a different lens:
83
+ Each role sees the same data through a different lens:
99
84
 
100
- - **Engineering**: What shipped, what drifted, patterns
85
+ - **Engineering**: What shipped, what drifted, what patterns emerged
101
86
  - **Product**: What shipped vs. planned, discovery signals
102
- - **Design**: UX decisions, implementation gaps vs. design intent
103
87
  - **Strategy**: Cross-team patterns, drift trends, capability gaps
104
88
 
105
- ### Signal Connectors
89
+ The digest posts to Slack. Anyone on the team — engineer, PM, CEO — who uses an AI tool gets session memory. Everyone else gets the digest.
106
90
 
107
- ```bash
108
- wayfind pull github # Issues, PRs, Actions status
109
- wayfind pull intercom # Support conversations, tags, response times
110
- wayfind pull notion # Pages, databases, comments
111
- wayfind pull --all # All configured channels
112
- ```
91
+ ### MCP Server
113
92
 
114
- ---
93
+ Wayfind includes an MCP server that exposes team context to any MCP-compatible AI tool. Auto-registered during `wayfind init`.
115
94
 
116
- ## Commands
117
-
118
- ### Plugin skills (in Claude Code)
119
-
120
- | Skill | Description |
121
- |-------|-------------|
122
- | `/wayfind:init-memory` | Initialize context for the current repo |
123
- | `/wayfind:init-team` | Set up team context, journals, and digests |
124
- | `/wayfind:doctor` | Check installation health |
125
- | `/wayfind:standup` | Daily standup summary |
126
- | `/wayfind:journal` | Weekly journal digest and drift detection |
127
- | `/wayfind:review-prs` | Review overnight PRs |
128
-
129
- ### CLI commands
130
-
131
- | Command | Description |
132
- |---------|-------------|
133
- | `wayfind init` | Install for your AI tool |
134
- | `wayfind doctor` | Check installation health |
135
- | `wayfind update` | Update hooks and commands |
136
- | `wayfind status` | Cross-project status |
137
- | `wayfind team create` | Create a new team |
138
- | `wayfind team join` | Join an existing team |
139
- | `wayfind digest` | Generate persona-specific digests |
140
- | `wayfind digest --deliver` | Generate and post to Slack |
141
- | `wayfind bot` | Start the Slack bot |
142
- | `wayfind reindex` | Index journals + conversations |
143
- | `wayfind search-journals <q>` | Search decision history |
144
- | `wayfind pull <channel>` | Pull signals from a source |
145
- | `wayfind journal sync` | Sync journals to team repo |
146
- | `wayfind onboard <repo>` | Generate onboarding context pack |
147
- | `wayfind deploy init` | Scaffold Docker deployment |
148
- | `wayfind deploy --team <id>` | Scaffold per-team Docker deployment |
149
- | `wayfind deploy set-endpoint <url>` | Set container endpoint for team search |
150
- | `wayfind deploy list` | List running team containers |
151
- | `wayfind deploy status` | Check container health |
152
- | `wayfind migrate-to-plugin` | Remove old hooks (after plugin install) |
153
-
154
- Run `wayfind help` for the full list.
95
+ **Tools:**
155
96
 
156
- ---
97
+ | Tool | What it does |
98
+ |------|-------------|
99
+ | `search_context` | Search decisions by query, date range, author, or repo. Semantic or browse mode. |
100
+ | `get_entry` | Retrieve the full content of a specific entry. |
101
+ | `get_signals` | Recent GitHub, Intercom, and Notion activity. |
102
+ | `get_team_status` | Current team state: who's working on what, active projects, blockers. |
103
+ | `add_context` | Capture a decision or blocker from the current session. |
104
+ | `record_feedback` | Rate whether a result was useful (improves future retrieval). |
157
105
 
158
- ## MCP Server
106
+ Each team member's MCP server searches their local content store — journals synced from the shared team repo, with local embeddings generated automatically. No infrastructure required.
159
107
 
160
- Wayfind includes an MCP server (`wayfind-mcp`) that exposes team context to any MCP-compatible AI tool.
108
+ ---
161
109
 
162
- **Tools:** `search_context`, `get_entry`, `list_recent`, `get_signals`, `get_team_status`, `get_personas`, `record_feedback`, `add_context`
110
+ ## Signal Connectors
163
111
 
164
- Auto-registered during `wayfind init`. When a team container is running, the local MCP server proxies semantic search to it automatically — no config needed beyond the team-context repo.
112
+ Pull external context into digests:
165
113
 
166
- ---
167
-
168
- ## Environment Variables
114
+ ```bash
115
+ wayfind pull github # Issues, PRs, Actions status
116
+ wayfind pull intercom # Support conversations, tags, response times
117
+ wayfind pull notion # Pages, databases, comments
118
+ wayfind pull --all # All configured channels
119
+ ```
169
120
 
170
- ### For digests and bot
121
+ ---
171
122
 
172
- | Variable | Description |
173
- |----------|-------------|
174
- | `ANTHROPIC_API_KEY` | Digest generation and bot answers |
175
- | `SLACK_BOT_TOKEN` | Slack bot token (`xoxb-...`) |
176
- | `SLACK_APP_TOKEN` | Slack app-level token (`xapp-...`) |
177
- | `GITHUB_TOKEN` | Signal data and journal sync |
123
+ ## Team Setup
178
124
 
179
- ### Optional
125
+ ```
126
+ /wayfind:init-team
127
+ ```
180
128
 
181
- | Variable | Description |
182
- |----------|-------------|
183
- | `OPENAI_API_KEY` | Upgrade semantic search to OpenAI embeddings (Xenova local model is used by default — no key needed) |
184
- | `TEAM_CONTEXT_LLM_MODEL` | LLM for digests (default: `claude-sonnet-4-5-20250929`) |
185
- | `TEAM_CONTEXT_DIGEST_SCHEDULE` | Cron schedule (default: `0 8 * * 1` — Monday 8am) |
186
- | `TEAM_CONTEXT_EXCLUDE_REPOS` | Repos to exclude from digests |
187
- | `TEAM_CONTEXT_TELEMETRY` | `true` for anonymous usage telemetry |
129
+ This walks you through creating a team, setting up profiles, creating a shared team-context repo, and configuring Slack digest delivery. Multi-team support built in — bind repos to different teams, each with isolated context.
188
130
 
189
131
  ---
190
132
 
191
133
  ## Tool Support
192
134
 
193
- | Tool | Status | Setup |
194
- |------|--------|-------|
195
- | Claude Code | Full support (plugin) | `/plugin marketplace add usewayfind/wayfind` |
196
- | Claude Code | Full support (npm) | `wayfind init` |
197
- | Cursor | Session protocol | `wayfind init-cursor` |
198
- | Generic | Manual | See `specializations/generic/` |
199
- | Any MCP client | Full support (MCP) | `wayfind init` auto-registers |
135
+ | Tool | How | Setup |
136
+ |------|-----|-------|
137
+ | **Claude Code** | Plugin (full support) | `/plugin marketplace add usewayfind/wayfind` |
138
+ | **Cursor** | MCP server | `wayfind init` auto-registers |
139
+ | **Any MCP client** | MCP server | `wayfind init` auto-registers |
140
+ | **Slack** | Bot + digests | `wayfind bot --configure` |
200
141
 
201
142
  ---
202
143
 
@@ -206,27 +147,23 @@ Everything that runs on your machine is open source (Apache 2.0).
206
147
 
207
148
  | Open Source (this repo) | Commercial (future) |
208
149
  |---|---|
209
- | CLI and all commands | Cloud-hosted team aggregation |
150
+ | CLI, plugin, and all commands | Cloud-hosted team aggregation |
210
151
  | Session protocol and journal extraction | Managed digest delivery |
211
- | Content store and search | Web dashboard |
152
+ | Content store, semantic search, MCP server | Web dashboard |
212
153
  | Signal connectors (GitHub, Intercom, Notion) | SSO and tenant isolation |
213
154
  | Digest generation (your API key) | |
214
155
  | Slack bot (self-hosted) | |
215
156
  | Multi-team support | |
216
- | MCP server (local + container proxy) | |
217
- | Per-team content store isolation | |
218
-
219
- See [LICENSING.md](LICENSING.md) for details.
220
157
 
221
158
  ---
222
159
 
223
160
  ## Architecture
224
161
 
225
162
  - [Data Flow](docs/architecture/data-flow.md) — sessions to digests
226
- - [Principles](docs/architecture/architecture-principles.md) — the eight constraints
163
+ - [Query Path](docs/architecture/query-path.md) — how queries reach the content store
227
164
  - [Content Store](docs/architecture/content-store.md) — indexing, search, schema
165
+ - [Principles](docs/architecture/architecture-principles.md) — the eight constraints
228
166
  - [Signal Channels](docs/architecture/architecture-signal-channels.md) — connector architecture
229
- - [Signal Roadmap](docs/architecture/signal-source-roadmap.md) — planned connectors
230
167
 
231
168
  ---
232
169
 
@@ -242,4 +179,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). Good first contributions:
242
179
 
243
180
  ## License
244
181
 
245
- Apache 2.0. See [LICENSE](LICENSE) and [LICENSING.md](LICENSING.md).
182
+ Apache 2.0. See [LICENSE](LICENSE).
@@ -302,6 +302,92 @@ async function call(config, systemPrompt, userContent) {
302
302
  }
303
303
  }
304
304
 
305
+ // ── Tool-use relay ──────────────────────────────────────────────────────────
306
+
307
+ /**
308
+ * Call the Anthropic API with tool-use support, looping until the model stops.
309
+ * @param {Object} config - Provider configuration (must be Anthropic)
310
+ * @param {string} systemPrompt - System prompt
311
+ * @param {string} userContent - User message text
312
+ * @param {Array} tools - Anthropic tool-use format tool definitions
313
+ * @param {Function} handleToolCall - async (name, input) => result
314
+ * @returns {Promise<string>} - Final text response
315
+ */
316
+ async function callWithTools(config, systemPrompt, userContent, tools, handleToolCall) {
317
+ const apiKey = process.env[config.api_key_env];
318
+ if (!apiKey) {
319
+ throw new Error(`Anthropic: Missing API key. Set ${config.api_key_env} environment variable.`);
320
+ }
321
+
322
+ const headers = {
323
+ 'x-api-key': apiKey,
324
+ 'anthropic-version': ANTHROPIC_VERSION,
325
+ };
326
+
327
+ const MAX_ITERATIONS = 10;
328
+ let messages = [{ role: 'user', content: userContent }];
329
+
330
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
331
+ const payload = JSON.stringify({
332
+ model: config.model,
333
+ max_tokens: config.max_tokens || DEFAULT_MAX_TOKENS,
334
+ system: systemPrompt,
335
+ messages,
336
+ tools,
337
+ });
338
+
339
+ const res = await httpPost(ANTHROPIC_API_URL, headers, payload);
340
+ checkResponse(res, 'Anthropic');
341
+
342
+ let data;
343
+ try {
344
+ data = JSON.parse(res.body);
345
+ } catch {
346
+ throw new Error('Anthropic: Failed to parse response JSON.');
347
+ }
348
+
349
+ if (!data.content || !Array.isArray(data.content)) {
350
+ throw new Error('Anthropic: Response missing content array.');
351
+ }
352
+
353
+ // If the model is done, extract and return the final text
354
+ if (data.stop_reason !== 'tool_use') {
355
+ const textBlock = data.content.find(b => b.type === 'text');
356
+ return textBlock ? textBlock.text : '';
357
+ }
358
+
359
+ // Process tool calls
360
+ const toolUseBlocks = data.content.filter(b => b.type === 'tool_use');
361
+ if (toolUseBlocks.length === 0) {
362
+ // stop_reason is tool_use but no tool_use blocks — treat as done
363
+ const textBlock = data.content.find(b => b.type === 'text');
364
+ return textBlock ? textBlock.text : '';
365
+ }
366
+
367
+ // Build tool results
368
+ const toolResults = [];
369
+ for (const block of toolUseBlocks) {
370
+ let result;
371
+ try {
372
+ result = await handleToolCall(block.name, block.input);
373
+ } catch (err) {
374
+ result = { error: err.message };
375
+ }
376
+ toolResults.push({
377
+ type: 'tool_result',
378
+ tool_use_id: block.id,
379
+ content: typeof result === 'string' ? result : JSON.stringify(result),
380
+ });
381
+ }
382
+
383
+ // Append assistant message + tool results, then loop
384
+ messages.push({ role: 'assistant', content: data.content });
385
+ messages.push({ role: 'user', content: toolResults });
386
+ }
387
+
388
+ throw new Error('Anthropic: Tool-use loop exceeded maximum iterations (10).');
389
+ }
390
+
305
391
  // ── Auto-detect available provider ───────────────────────────────────────────
306
392
 
307
393
  /**
@@ -535,6 +621,7 @@ async function generateEmbeddingAzure(text, options = {}) {
535
621
 
536
622
  module.exports = {
537
623
  call,
624
+ callWithTools,
538
625
  detect,
539
626
  generateEmbedding,
540
627
  getEmbeddingProviderInfo,
@@ -610,7 +610,7 @@ async function indexJournals(options = {}) {
610
610
 
611
611
  /**
612
612
  * Search journals using semantic similarity.
613
- * Falls back to searchText() if no embeddings available.
613
+ * Falls back to queryMetadata() browse if no embeddings available.
614
614
  * @param {string} query - Search query
615
615
  * @param {Object} [options]
616
616
  * @param {string} [options.storePath] - Content store directory
@@ -633,7 +633,11 @@ async function searchJournals(query, options = {}) {
633
633
  const hasEmbeddings = Object.keys(embeddings).length > 0;
634
634
 
635
635
  if (!hasEmbeddings) {
636
- return searchText(query, options);
636
+ const browseResults = queryMetadata(options);
637
+ return browseResults.slice(0, limit).map(r => ({
638
+ ...r, score: null,
639
+ _hint: 'Semantic search unavailable — showing recent entries by date. Run "wayfind reindex" to enable semantic search.',
640
+ }));
637
641
  }
638
642
 
639
643
  // Generate query embedding
@@ -641,8 +645,12 @@ async function searchJournals(query, options = {}) {
641
645
  try {
642
646
  queryVec = await llm.generateEmbedding(query);
643
647
  } catch {
644
- // Fall back to text search if embedding fails
645
- return searchText(query, options);
648
+ // Fall back to browse if embedding generation fails
649
+ const browseResults = queryMetadata(options);
650
+ return browseResults.slice(0, limit).map(r => ({
651
+ ...r, score: null,
652
+ _hint: 'Semantic search unavailable — showing recent entries by date. Run "wayfind reindex" to enable semantic search.',
653
+ }));
646
654
  }
647
655
 
648
656
  // Score all entries
@@ -662,143 +670,6 @@ async function searchJournals(query, options = {}) {
662
670
  return results.slice(0, limit);
663
671
  }
664
672
 
665
- /**
666
- * Full-text search across journal entries.
667
- * Works without any API key. Matches query words against title, repo, tags.
668
- * @param {string} query - Search query
669
- * @param {Object} [options]
670
- * @param {string} [options.storePath] - Content store directory
671
- * @param {number} [options.limit] - Max results (default: 10)
672
- * @param {string} [options.repo] - Filter by repo
673
- * @param {string} [options.since] - Filter by date (YYYY-MM-DD)
674
- * @param {string} [options.until] - Filter by date (YYYY-MM-DD)
675
- * @param {boolean} [options.drifted] - Filter by drift status
676
- * @returns {Array<{ id: string, score: number, entry: Object }>}
677
- */
678
- function searchText(query, options = {}) {
679
- const storePath = options.storePath || resolveStorePath();
680
- const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
681
- const limit = options.limit || 10;
682
-
683
- const index = getBackend(storePath).loadIndex();
684
- if (!index) return [];
685
-
686
- // Normalize: split on whitespace, hyphens, underscores
687
- const queryWords = query.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 1);
688
- if (queryWords.length === 0) return [];
689
-
690
- // Pre-load journal content for full-text search (cache by date+user key)
691
- const journalCache = {};
692
- function getJournalContent(date, user) {
693
- const cacheKey = user ? `${date}-${user}` : date;
694
- if (journalCache[cacheKey] !== undefined) return journalCache[cacheKey];
695
- if (!journalDir) { journalCache[cacheKey] = null; return null; }
696
- // Try authored filename first, then plain date filename
697
- const candidates = user
698
- ? [path.join(journalDir, `${date}-${user}.md`), path.join(journalDir, `${date}.md`)]
699
- : [path.join(journalDir, `${date}.md`)];
700
- let content = null;
701
- for (const filePath of candidates) {
702
- try {
703
- content = fs.readFileSync(filePath, 'utf8').toLowerCase();
704
- break;
705
- } catch {
706
- // Try next candidate
707
- }
708
- }
709
- journalCache[cacheKey] = content;
710
- return content;
711
- }
712
-
713
- const results = [];
714
- for (const [id, entry] of Object.entries(index.entries)) {
715
- if (!applyFilters(entry, options)) continue;
716
-
717
- // Build searchable text from entry metadata (normalize hyphens/underscores)
718
- let searchable = [
719
- entry.title,
720
- entry.repo,
721
- entry.date,
722
- entry.user,
723
- ...(entry.tags || []),
724
- ].filter(Boolean).join(' ').toLowerCase().replace(/[-_]/g, ' ');
725
-
726
- // For signal entries, read content directly from the signal file
727
- if (entry.source === 'signal') {
728
- const signalsDir = options.signalsDir || resolveSignalsDir();
729
- if (signalsDir) {
730
- // Signal files live at signalsDir/<channel>/<date>.md or signalsDir/<channel>/<owner>/<repo>/<date>.md
731
- // The repo field tells us the path: "signals/<channel>" or "<owner>/<repo>"
732
- const repo = entry.repo || '';
733
- const candidates = [];
734
- if (repo.startsWith('signals/')) {
735
- const channel = repo.replace('signals/', '');
736
- candidates.push(path.join(signalsDir, channel, `${entry.date}.md`));
737
- candidates.push(path.join(signalsDir, channel, `${entry.date}-summary.md`));
738
- } else if (repo.includes('/')) {
739
- // owner/repo format — find which channel it's under
740
- for (const channel of ['github', 'intercom', 'notion']) {
741
- candidates.push(path.join(signalsDir, channel, repo, `${entry.date}.md`));
742
- }
743
- }
744
- for (const fp of candidates) {
745
- try {
746
- const content = fs.readFileSync(fp, 'utf8').toLowerCase();
747
- searchable += ' ' + content.replace(/[-_]/g, ' ');
748
- break;
749
- } catch {
750
- // Try next candidate
751
- }
752
- }
753
- }
754
- }
755
-
756
- // Also include the full journal entry content if available
757
- const journalContent = entry.source !== 'signal' ? getJournalContent(entry.date, entry.user) : null;
758
- if (journalContent) {
759
- // Find this entry's section in the journal file.
760
- // Try exact match first, then normalize hyphens/spaces for fuzzy match.
761
- const repoTitle = `${entry.repo} — ${entry.title}`.toLowerCase();
762
- let idx = journalContent.indexOf(repoTitle);
763
- if (idx === -1) {
764
- // Normalize both sides: collapse hyphens, underscores, em-dashes, and extra spaces
765
- const norm = (s) => s.replace(/[-_\u2014\u2013]/g, ' ').replace(/\s+/g, ' ').trim();
766
- const normalized = norm(repoTitle);
767
- // Search through journal headers for a normalized match
768
- const headerRegex = /\n## (.+)/g;
769
- let match;
770
- while ((match = headerRegex.exec(journalContent)) !== null) {
771
- const headerNorm = norm(match[1]);
772
- if (headerNorm.includes(normalized) || normalized.includes(headerNorm)) {
773
- idx = match.index + 1; // skip the \n
774
- break;
775
- }
776
- }
777
- }
778
- if (idx !== -1) {
779
- // Extract from header to next header (or end of file)
780
- const nextHeader = journalContent.indexOf('\n## ', idx + 1);
781
- const section = nextHeader !== -1 ? journalContent.slice(idx, nextHeader) : journalContent.slice(idx);
782
- searchable += ' ' + section.replace(/[-_]/g, ' ');
783
- }
784
- }
785
-
786
- // Score: count of matching query words
787
- let matches = 0;
788
- for (const word of queryWords) {
789
- if (searchable.includes(word)) matches++;
790
- }
791
-
792
- if (matches > 0) {
793
- const score = Math.round((matches / queryWords.length) * 1000) / 1000;
794
- results.push({ id, score, entry });
795
- }
796
- }
797
-
798
- results.sort((a, b) => b.score - a.score);
799
- return results.slice(0, limit);
800
- }
801
-
802
673
  /**
803
674
  * Apply metadata filters to an entry.
804
675
  * @param {Object} entry
@@ -2480,7 +2351,6 @@ module.exports = {
2480
2351
  applyContextShiftToState,
2481
2352
  generateOnboardingPack,
2482
2353
  searchJournals,
2483
- searchText,
2484
2354
  queryMetadata,
2485
2355
  extractInsights,
2486
2356
  computeQualityProfile,