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 +74 -137
- package/bin/connectors/llm.js +87 -0
- package/bin/content-store.js +12 -142
- package/bin/mcp-server.js +47 -50
- package/bin/slack-bot.js +136 -1060
- package/bin/team-context.js +41 -31
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/setup.sh +9 -27
package/README.md
CHANGED
|
@@ -1,68 +1,55 @@
|
|
|
1
1
|
# Wayfind
|
|
2
2
|
|
|
3
|
-
**Team
|
|
3
|
+
**Team memory for AI-assisted engineering.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
[](https://github.com/usewayfind/wayfind/actions/workflows/ci.yml)
|
|
8
|
-
[](LICENSE)
|
|
9
10
|
[](https://www.npmjs.com/package/wayfind)
|
|
11
|
+
[](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
|
-
|
|
15
|
+
---
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
/plugin marketplace add usewayfind/wayfind
|
|
21
|
-
/plugin install wayfind@usewayfind
|
|
22
|
-
```
|
|
17
|
+
## The Problem
|
|
23
18
|
|
|
24
|
-
|
|
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
|
-
|
|
23
|
+
## What Wayfind Does
|
|
31
24
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
+
<!-- TODO: demo GIF here — see #175 -->
|
|
42
32
|
|
|
43
33
|
---
|
|
44
34
|
|
|
45
|
-
##
|
|
35
|
+
## Quick Start
|
|
46
36
|
|
|
47
|
-
###
|
|
37
|
+
### Option A: Claude Code plugin
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
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
|
-
|
|
45
|
+
### Option B: npm (works with any AI tool)
|
|
62
46
|
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g wayfind
|
|
49
|
+
wayfind init
|
|
63
50
|
```
|
|
64
|
-
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
+
---
|
|
161
109
|
|
|
162
|
-
|
|
110
|
+
## Signal Connectors
|
|
163
111
|
|
|
164
|
-
|
|
112
|
+
Pull external context into digests:
|
|
165
113
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
121
|
+
---
|
|
171
122
|
|
|
172
|
-
|
|
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
|
-
|
|
125
|
+
```
|
|
126
|
+
/wayfind:init-team
|
|
127
|
+
```
|
|
180
128
|
|
|
181
|
-
|
|
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 |
|
|
194
|
-
|
|
195
|
-
| Claude Code |
|
|
196
|
-
|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
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
|
|
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
|
-
- [
|
|
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)
|
|
182
|
+
Apache 2.0. See [LICENSE](LICENSE).
|
package/bin/connectors/llm.js
CHANGED
|
@@ -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,
|
package/bin/content-store.js
CHANGED
|
@@ -610,7 +610,7 @@ async function indexJournals(options = {}) {
|
|
|
610
610
|
|
|
611
611
|
/**
|
|
612
612
|
* Search journals using semantic similarity.
|
|
613
|
-
* Falls back to
|
|
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
|
-
|
|
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
|
|
645
|
-
|
|
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,
|