wayfind 2.0.45 → 2.0.47

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.
@@ -27,6 +27,9 @@ Once both steps complete:
27
27
  6. Ask me if I want to set up team context (/wayfind:init-team) for shared journals,
28
28
  digests, and product state
29
29
  7. Let me know that anonymous usage telemetry is enabled by default (set TEAM_CONTEXT_TELEMETRY=false to opt out)
30
+ 8. If I'm joining an existing team, check if there's a container_endpoint in my
31
+ context.json — if so, my MCP server will automatically proxy semantic search
32
+ to the team's container. No extra setup needed.
30
33
  ```
31
34
 
32
35
  ---
@@ -36,10 +39,12 @@ Once both steps complete:
36
39
  The CLI install creates:
37
40
  - `~/.claude/memory/` and `~/.claude/memory/journal/`
38
41
  - `~/.claude/global-state.md` (your persistent index)
42
+ - `~/.claude/team-context/context.json` (team registry for multi-team support)
39
43
 
40
44
  The plugin provides:
41
45
  - SessionStart and Stop hooks (context loading, decision extraction)
42
46
  - Slash commands: `/wayfind:init-memory`, `/wayfind:init-team`, `/wayfind:journal`, `/wayfind:doctor`, `/wayfind:standup`
47
+ - MCP server (`wayfind-mcp`) — registered automatically, gives any MCP-compatible AI tool access to your team's context
43
48
 
44
49
  After the paste, Claude will walk you through filling in your preferences. From
45
50
  then on, every session in every repo will start with full context of where you
@@ -100,6 +105,27 @@ Claude will create `.claude/team-state.md` (shared) and `.claude/personal-state.
100
105
 
101
106
  ---
102
107
 
108
+ ## Container deployment (for teams)
109
+
110
+ One team member (the team owner) runs a Docker container that provides:
111
+ - Slack bot, automated digests, signal connectors
112
+ - Semantic search API for the whole team's content
113
+ - API key auto-rotation (daily, committed to team-context repo)
114
+
115
+ Other team members don't need Docker — their local MCP server automatically
116
+ proxies search queries to the container via the shared API key.
117
+
118
+ ```bash
119
+ wayfind deploy --team <teamId> # Scaffold config
120
+ # Edit deploy/.env with your Anthropic key
121
+ cd deploy && docker compose up -d # Start container
122
+ wayfind deploy set-endpoint http://your-hostname:3141 --team <teamId> # Set endpoint for team
123
+ ```
124
+
125
+ Team members pull the team-context repo to get the API key and endpoint config.
126
+
127
+ ---
128
+
103
129
  ## For Cursor users
104
130
 
105
131
  ```
@@ -117,11 +143,11 @@ https://github.com/usewayfind/wayfind
117
143
 
118
144
  To install a specific version:
119
145
  ```
120
- npm install -g wayfind@2.0.15
146
+ npm install -g wayfind@2.0.45
121
147
  wayfind init
122
148
  ```
123
149
 
124
150
  Or via the shell installer with a pinned version:
125
151
  ```
126
- WAYFIND_VERSION=v2.0.15 bash <(curl -fsSL https://raw.githubusercontent.com/usewayfind/wayfind/main/install.sh)
152
+ WAYFIND_VERSION=v2.0.45 bash <(curl -fsSL https://raw.githubusercontent.com/usewayfind/wayfind/main/install.sh)
127
153
  ```
package/README.md CHANGED
@@ -145,12 +145,26 @@ wayfind pull --all # All configured channels
145
145
  | `wayfind journal sync` | Sync journals to team repo |
146
146
  | `wayfind onboard <repo>` | Generate onboarding context pack |
147
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 |
148
152
  | `wayfind migrate-to-plugin` | Remove old hooks (after plugin install) |
149
153
 
150
154
  Run `wayfind help` for the full list.
151
155
 
152
156
  ---
153
157
 
158
+ ## MCP Server
159
+
160
+ Wayfind includes an MCP server (`wayfind-mcp`) that exposes team context to any MCP-compatible AI tool.
161
+
162
+ **Tools:** `search_context`, `get_entry`, `list_recent`, `get_signals`, `get_team_status`, `get_personas`, `record_feedback`, `add_context`
163
+
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.
165
+
166
+ ---
167
+
154
168
  ## Environment Variables
155
169
 
156
170
  ### For digests and bot
@@ -171,6 +185,8 @@ Run `wayfind help` for the full list.
171
185
  | `TEAM_CONTEXT_DIGEST_SCHEDULE` | Cron schedule (default: `0 8 * * 1` — Monday 8am) |
172
186
  | `TEAM_CONTEXT_EXCLUDE_REPOS` | Repos to exclude from digests |
173
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 * * *`) |
174
190
 
175
191
  ---
176
192
 
@@ -182,6 +198,7 @@ Run `wayfind help` for the full list.
182
198
  | Claude Code | Full support (npm) | `wayfind init` |
183
199
  | Cursor | Session protocol | `wayfind init-cursor` |
184
200
  | Generic | Manual | See `specializations/generic/` |
201
+ | Any MCP client | Full support (MCP) | `wayfind init` auto-registers |
185
202
 
186
203
  ---
187
204
 
@@ -198,6 +215,8 @@ Everything that runs on your machine is open source (Apache 2.0).
198
215
  | Digest generation (your API key) | |
199
216
  | Slack bot (self-hosted) | |
200
217
  | Multi-team support | |
218
+ | MCP server (local + container proxy) | |
219
+ | Per-team content store isolation | |
201
220
 
202
221
  See [LICENSING.md](LICENSING.md) for details.
203
222
 
package/bin/mcp-server.js CHANGED
@@ -30,8 +30,199 @@ const path = require('path');
30
30
  const fs = require('fs');
31
31
  const contentStore = require('./content-store.js');
32
32
 
33
+ const http = require('http');
34
+
33
35
  const pkg = require('../package.json');
34
36
 
37
+ // ── Container proxy ─────────────────────────────────────────────────────────
38
+
39
+ const HOME = process.env.HOME || process.env.USERPROFILE;
40
+ const WAYFIND_DIR = HOME ? path.join(HOME, '.claude', 'team-context') : null;
41
+
42
+ /**
43
+ * Read context.json to find the active team's container_endpoint.
44
+ */
45
+ function getContainerEndpoint() {
46
+ if (!WAYFIND_DIR) return null;
47
+ const configPath = path.join(WAYFIND_DIR, 'context.json');
48
+ try {
49
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
50
+ // Resolve active team: repo binding → default
51
+ const teamId = getActiveTeamId(config);
52
+ if (!teamId || !config.teams || !config.teams[teamId]) return null;
53
+ return config.teams[teamId].container_endpoint || null;
54
+ } catch (_) {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Resolve active team ID from context.json (simplified — mirrors team-context.js logic).
61
+ */
62
+ function getActiveTeamId(config) {
63
+ // Check repo-level binding first
64
+ try {
65
+ const bindingFile = path.join(process.cwd(), '.claude', 'wayfind.json');
66
+ if (fs.existsSync(bindingFile)) {
67
+ const binding = JSON.parse(fs.readFileSync(bindingFile, 'utf8'));
68
+ if (binding.team_id) return binding.team_id;
69
+ }
70
+ } catch (_) {}
71
+ return config.default || null;
72
+ }
73
+
74
+ /**
75
+ * Read the shared API key from the team-context repo.
76
+ */
77
+ function readApiKey() {
78
+ if (!WAYFIND_DIR) return null;
79
+ try {
80
+ const config = JSON.parse(fs.readFileSync(path.join(WAYFIND_DIR, 'context.json'), 'utf8'));
81
+ const teamId = getActiveTeamId(config);
82
+ if (!teamId || !config.teams || !config.teams[teamId]) return null;
83
+ const teamPath = config.teams[teamId].path;
84
+ if (!teamPath) return null;
85
+ const keyFile = path.join(teamPath, '.wayfind-api-key');
86
+ return fs.readFileSync(keyFile, 'utf8').trim();
87
+ } catch (_) {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ // Cache for API key (re-read from disk on 401)
93
+ let cachedApiKey = null;
94
+
95
+ /**
96
+ * HTTP POST to the container's search API.
97
+ * Returns parsed JSON or null on failure.
98
+ */
99
+ function containerPost(endpoint, apiKey, body) {
100
+ return new Promise((resolve) => {
101
+ try {
102
+ const url = new URL(endpoint);
103
+ const postData = JSON.stringify(body);
104
+ const req = http.request({
105
+ hostname: url.hostname,
106
+ port: url.port || 80,
107
+ path: url.pathname,
108
+ method: 'POST',
109
+ timeout: 10000,
110
+ headers: {
111
+ 'Content-Type': 'application/json',
112
+ 'Content-Length': Buffer.byteLength(postData),
113
+ 'Authorization': `Bearer ${apiKey}`,
114
+ },
115
+ }, (res) => {
116
+ let data = '';
117
+ res.on('data', (chunk) => { data += chunk; });
118
+ res.on('end', () => {
119
+ resolve({ status: res.statusCode, body: data });
120
+ });
121
+ });
122
+ req.on('error', () => resolve(null));
123
+ req.on('timeout', () => { req.destroy(); resolve(null); });
124
+ req.write(postData);
125
+ req.end();
126
+ } catch (_) {
127
+ resolve(null);
128
+ }
129
+ });
130
+ }
131
+
132
+ /**
133
+ * HTTP GET from the container's entry API.
134
+ */
135
+ function containerGet(endpoint, apiKey) {
136
+ return new Promise((resolve) => {
137
+ try {
138
+ const url = new URL(endpoint);
139
+ const req = http.request({
140
+ hostname: url.hostname,
141
+ port: url.port || 80,
142
+ path: url.pathname + url.search,
143
+ method: 'GET',
144
+ timeout: 10000,
145
+ headers: {
146
+ 'Authorization': `Bearer ${apiKey}`,
147
+ },
148
+ }, (res) => {
149
+ let data = '';
150
+ res.on('data', (chunk) => { data += chunk; });
151
+ res.on('end', () => {
152
+ resolve({ status: res.statusCode, body: data });
153
+ });
154
+ });
155
+ req.on('error', () => resolve(null));
156
+ req.on('timeout', () => { req.destroy(); resolve(null); });
157
+ req.end();
158
+ } catch (_) {
159
+ resolve(null);
160
+ }
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Try to proxy a search request to the container.
166
+ * On 401, re-reads the API key from disk and retries once.
167
+ * Returns parsed result or null if container unreachable/unavailable.
168
+ */
169
+ async function proxySearch(body) {
170
+ const endpoint = getContainerEndpoint();
171
+ if (!endpoint) return null;
172
+
173
+ if (!cachedApiKey) cachedApiKey = readApiKey();
174
+ if (!cachedApiKey) return null;
175
+
176
+ const searchUrl = `${endpoint}/api/search`;
177
+ let result = await containerPost(searchUrl, cachedApiKey, body);
178
+
179
+ // On 401, re-read key (may have been rotated) and retry once
180
+ if (result && result.status === 401) {
181
+ process.stderr.write('Container returned 401 — re-reading API key...\n');
182
+ cachedApiKey = readApiKey();
183
+ if (!cachedApiKey) return null;
184
+ result = await containerPost(searchUrl, cachedApiKey, body);
185
+ }
186
+
187
+ if (!result || result.status !== 200) return null;
188
+
189
+ try {
190
+ return JSON.parse(result.body);
191
+ } catch (_) {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Try to proxy an entry retrieval to the container.
198
+ * Same 401-retry logic as proxySearch.
199
+ */
200
+ async function proxyGetEntry(id) {
201
+ const endpoint = getContainerEndpoint();
202
+ if (!endpoint) return null;
203
+
204
+ if (!cachedApiKey) cachedApiKey = readApiKey();
205
+ if (!cachedApiKey) return null;
206
+
207
+ const entryUrl = `${endpoint}/api/entry/${encodeURIComponent(id)}`;
208
+ let result = await containerGet(entryUrl, cachedApiKey);
209
+
210
+ if (result && result.status === 401) {
211
+ process.stderr.write('Container returned 401 — re-reading API key...\n');
212
+ cachedApiKey = readApiKey();
213
+ if (!cachedApiKey) return null;
214
+ result = await containerGet(entryUrl, cachedApiKey);
215
+ }
216
+
217
+ if (!result || result.status !== 200) return null;
218
+
219
+ try {
220
+ return JSON.parse(result.body);
221
+ } catch (_) {
222
+ return null;
223
+ }
224
+ }
225
+
35
226
  // ── Tool definitions ─────────────────────────────────────────────────────────
36
227
 
37
228
  const TOOLS = [
@@ -137,8 +328,18 @@ const TOOLS = [
137
328
 
138
329
  async function handleSearchContext(args) {
139
330
  const { query, limit = 10, repo, since, mode } = args;
140
- const opts = { limit, repo, since };
141
331
 
332
+ // Try container first for semantic search (has embeddings for full team)
333
+ if (mode !== 'text') {
334
+ const containerResult = await proxySearch({ query, limit, repo, since, mode });
335
+ if (containerResult && containerResult.found > 0) {
336
+ containerResult.source = 'container';
337
+ return containerResult;
338
+ }
339
+ }
340
+
341
+ // Fall back to local search
342
+ const opts = { limit, repo, since };
142
343
  let results;
143
344
  if (mode === 'text') {
144
345
  results = contentStore.searchText(query, opts);
@@ -147,11 +348,12 @@ async function handleSearchContext(args) {
147
348
  }
148
349
 
149
350
  if (!results || results.length === 0) {
150
- return { found: 0, results: [], hint: 'No matches. Try a broader query or check wayfind reindex.' };
351
+ return { found: 0, results: [], source: 'local', hint: 'No matches. Try a broader query or check wayfind reindex.' };
151
352
  }
152
353
 
153
354
  return {
154
355
  found: results.length,
356
+ source: 'local',
155
357
  results: results.map(r => ({
156
358
  id: r.id,
157
359
  score: r.score ? Math.round(r.score * 1000) / 1000 : null,
@@ -165,29 +367,34 @@ async function handleSearchContext(args) {
165
367
  };
166
368
  }
167
369
 
168
- function handleGetEntry(args) {
370
+ async function handleGetEntry(args) {
169
371
  const { id } = args;
170
372
  const storePath = contentStore.resolveStorePath();
171
373
  const journalDir = contentStore.DEFAULT_JOURNAL_DIR;
172
374
 
173
- // Get entry metadata
375
+ // Try local first (fastest)
174
376
  const index = contentStore.getBackend(storePath).loadIndex();
175
- if (!index || !index.entries || !index.entries[id]) {
176
- return { error: `Entry not found: ${id}` };
377
+ if (index && index.entries && index.entries[id]) {
378
+ const entry = index.entries[id];
379
+ const fullContent = contentStore.getEntryContent(id, { storePath, journalDir });
380
+ return {
381
+ id,
382
+ date: entry.date,
383
+ repo: entry.repo,
384
+ title: entry.title,
385
+ source: entry.source,
386
+ tags: entry.tags || [],
387
+ content: fullContent || entry.summary || null,
388
+ };
177
389
  }
178
390
 
179
- const entry = index.entries[id];
180
- const fullContent = contentStore.getEntryContent(id, { storePath, journalDir });
391
+ // Not found locally — try container (may have entries from other team members)
392
+ const containerResult = await proxyGetEntry(id);
393
+ if (containerResult && !containerResult.error) {
394
+ return containerResult;
395
+ }
181
396
 
182
- return {
183
- id,
184
- date: entry.date,
185
- repo: entry.repo,
186
- title: entry.title,
187
- source: entry.source,
188
- tags: entry.tags || [],
189
- content: fullContent || entry.summary || null,
190
- };
397
+ return { error: `Entry not found: ${id}` };
191
398
  }
192
399
 
193
400
  function handleListRecent(args) {
@@ -382,7 +589,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
382
589
  let result;
383
590
  switch (name) {
384
591
  case 'search_context': result = await handleSearchContext(args); break;
385
- case 'get_entry': result = handleGetEntry(args); break;
592
+ case 'get_entry': result = await handleGetEntry(args); break;
386
593
  case 'list_recent': result = handleListRecent(args); break;
387
594
  case 'get_signals': result = handleGetSignals(args); break;
388
595
  case 'get_team_status': result = handleGetTeamStatus(args); break;
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # Daily memory systems comparison report — posts to Slack via bot token.
3
- # Add to crontab: 43 8 * * * /home/greg/repos/greg/wayfind/bin/memory-report.sh
3
+ # Add to crontab: 43 8 * * * $(which wayfind | xargs dirname)/memory-report.sh
4
4
  #
5
5
  # Runs on host (needs access to ~/.claude/projects for auto-memory).
6
6
  # Pulls SLACK_BOT_TOKEN from the wayfind container if not set locally.
@@ -2103,7 +2103,7 @@ function commitAndPushTeamJournals(teamContextPath, copied) {
2103
2103
  stampMemberVersion(teamContextPath);
2104
2104
 
2105
2105
  try {
2106
- const gitAdd = spawnSync('git', ['add', 'journals/', 'members/'], { cwd: teamContextPath, stdio: 'pipe' });
2106
+ const gitAdd = spawnSync('git', ['add', 'journals/', 'members/', '.wayfind-api-key'], { cwd: teamContextPath, stdio: 'pipe' });
2107
2107
  if (gitAdd.status !== 0) {
2108
2108
  console.error(`git add failed: ${(gitAdd.stderr || '').toString().trim()}`);
2109
2109
  return;
@@ -3804,9 +3804,27 @@ async function runDeploy(args) {
3804
3804
  case 'status':
3805
3805
  deployStatus();
3806
3806
  break;
3807
+ case 'set-endpoint': {
3808
+ const endpointTeamId = teamId || (readContextConfig().default);
3809
+ const endpointUrl = filteredArgs[1];
3810
+ if (!endpointTeamId || !endpointUrl) {
3811
+ console.error('Usage: wayfind deploy set-endpoint <url> --team <id>');
3812
+ console.error('Example: wayfind deploy set-endpoint http://gregs-laptop:3141 --team abc123');
3813
+ process.exit(1);
3814
+ }
3815
+ const cfg = readContextConfig();
3816
+ if (!cfg.teams || !cfg.teams[endpointTeamId]) {
3817
+ console.error(`Team "${endpointTeamId}" not found in context.json`);
3818
+ process.exit(1);
3819
+ }
3820
+ cfg.teams[endpointTeamId].container_endpoint = endpointUrl;
3821
+ writeContextConfig(cfg);
3822
+ console.log(`Set container_endpoint for team "${endpointTeamId}" to ${endpointUrl}`);
3823
+ break;
3824
+ }
3807
3825
  default:
3808
3826
  console.error(`Unknown deploy subcommand: ${sub}`);
3809
- console.error('Available: init [--team <id>], list, status');
3827
+ console.error('Available: init [--team <id>], list, status, set-endpoint');
3810
3828
  process.exit(1);
3811
3829
  }
3812
3830
  }
@@ -3947,6 +3965,15 @@ function deployTeamInit(teamId, { port } = {}) {
3947
3965
  }
3948
3966
  }
3949
3967
 
3968
+ // Store container_endpoint in context.json so team members' MCP can discover it
3969
+ const updatedConfig = readContextConfig();
3970
+ if (updatedConfig.teams && updatedConfig.teams[teamId]) {
3971
+ updatedConfig.teams[teamId].container_endpoint = `http://localhost:${assignedPort}`;
3972
+ writeContextConfig(updatedConfig);
3973
+ console.log(` context.json — set container_endpoint to http://localhost:${assignedPort}`);
3974
+ console.log(' Tip: update the hostname if team members connect over a network (e.g. Tailscale).');
3975
+ }
3976
+
3950
3977
  console.log('');
3951
3978
  console.log('Next steps:');
3952
3979
  console.log(` 1. Set ANTHROPIC_API_KEY in ${deployDir}/.env`);
@@ -4174,14 +4201,149 @@ function deployStatus() {
4174
4201
  }
4175
4202
  }
4176
4203
 
4177
- // ── Health endpoint ──────────────────────────────────────────────────────────
4204
+ // ── API key management ──────────────────────────────────────────────────────
4205
+
4206
+ /**
4207
+ * Read or generate the API key for container search endpoints.
4208
+ * Key is stored in the team-context repo so team members can read it.
4209
+ */
4210
+ function getOrCreateApiKey() {
4211
+ const teamDir = process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR;
4212
+ if (!teamDir) return null;
4213
+
4214
+ const keyFile = path.join(teamDir, '.wayfind-api-key');
4215
+ try {
4216
+ if (fs.existsSync(keyFile)) {
4217
+ const key = fs.readFileSync(keyFile, 'utf8').trim();
4218
+ if (key.length >= 32) return key;
4219
+ }
4220
+ } catch (_) {}
4221
+
4222
+ // Generate a new key
4223
+ const key = crypto.randomBytes(32).toString('hex');
4224
+ try {
4225
+ fs.writeFileSync(keyFile, key + '\n', 'utf8');
4226
+ console.log(`[${new Date().toISOString()}] Generated new API key in ${keyFile}`);
4227
+ } catch (err) {
4228
+ console.error(`Failed to write API key: ${err.message}`);
4229
+ return null;
4230
+ }
4231
+ return key;
4232
+ }
4233
+
4234
+ /**
4235
+ * Rotate the API key and commit/push it to the team-context repo.
4236
+ */
4237
+ function rotateApiKey() {
4238
+ const teamDir = process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR;
4239
+ if (!teamDir) return;
4240
+
4241
+ const key = crypto.randomBytes(32).toString('hex');
4242
+ const keyFile = path.join(teamDir, '.wayfind-api-key');
4243
+ try {
4244
+ fs.writeFileSync(keyFile, key + '\n', 'utf8');
4245
+ currentApiKey = key;
4246
+ console.log(`[${new Date().toISOString()}] Rotated API key`);
4247
+ pushApiKey(teamDir);
4248
+ } catch (err) {
4249
+ console.error(`Key rotation failed: ${err.message}`);
4250
+ }
4251
+ }
4252
+
4253
+ /**
4254
+ * Git add/commit/push the API key file.
4255
+ */
4256
+ function pushApiKey(teamDir) {
4257
+ const token = process.env.GITHUB_TOKEN;
4258
+ const env = { ...process.env };
4259
+ const gitConfig = [['safe.directory', teamDir]];
4260
+ if (token) {
4261
+ env.GIT_ASKPASS = 'echo';
4262
+ env.GIT_TERMINAL_PROMPT = '0';
4263
+ gitConfig.push(['credential.helper', '']);
4264
+ gitConfig.push([`url.https://x-access-token:${token}@github.com/.insteadOf`, 'https://github.com/']);
4265
+ }
4266
+ env.GIT_CONFIG_COUNT = String(gitConfig.length);
4267
+ for (let i = 0; i < gitConfig.length; i++) {
4268
+ env[`GIT_CONFIG_KEY_${i}`] = gitConfig[i][0];
4269
+ env[`GIT_CONFIG_VALUE_${i}`] = gitConfig[i][1];
4270
+ }
4271
+
4272
+ try {
4273
+ spawnSync('git', ['add', '.wayfind-api-key'], { cwd: teamDir, env, stdio: 'pipe' });
4274
+ const diff = spawnSync('git', ['diff', '--cached', '--quiet'], { cwd: teamDir, env, stdio: 'pipe' });
4275
+ if (diff.status === 0) return; // Nothing to commit
4276
+ spawnSync('git', ['commit', '-m', 'Rotate Wayfind API key'], { cwd: teamDir, env, stdio: 'pipe' });
4277
+ const push = spawnSync('git', ['push'], { cwd: teamDir, env, stdio: 'pipe', timeout: 30000 });
4278
+ if (push.status !== 0) {
4279
+ // Rebase and retry on conflict
4280
+ spawnSync('git', ['pull', '--rebase'], { cwd: teamDir, env, stdio: 'pipe', timeout: 30000 });
4281
+ spawnSync('git', ['push'], { cwd: teamDir, env, stdio: 'pipe', timeout: 30000 });
4282
+ }
4283
+ } catch (err) {
4284
+ console.error(`API key push failed: ${err.message}`);
4285
+ }
4286
+ }
4287
+
4288
+ // Current in-memory API key (loaded on startup, updated on rotation)
4289
+ let currentApiKey = null;
4290
+
4291
+ // ── Health + API endpoint ───────────────────────────────────────────────────
4178
4292
 
4179
4293
  let healthStatus = { ok: true, mode: null, started: null, services: {} };
4180
4294
 
4295
+ /**
4296
+ * Parse JSON body from an incoming request.
4297
+ */
4298
+ function parseJsonBody(req) {
4299
+ return new Promise((resolve, reject) => {
4300
+ let body = '';
4301
+ req.on('data', (chunk) => { body += chunk; if (body.length > 1e6) reject(new Error('Body too large')); });
4302
+ req.on('end', () => {
4303
+ try { resolve(body ? JSON.parse(body) : {}); }
4304
+ catch (e) { reject(e); }
4305
+ });
4306
+ req.on('error', reject);
4307
+ });
4308
+ }
4309
+
4310
+ /**
4311
+ * Check Authorization header against the current API key.
4312
+ * Returns true if authorized, false otherwise (and sends 401).
4313
+ */
4314
+ function checkApiAuth(req, res) {
4315
+ if (!currentApiKey) {
4316
+ res.writeHead(503, { 'Content-Type': 'application/json' });
4317
+ res.end(JSON.stringify({ error: 'API key not configured' }));
4318
+ return false;
4319
+ }
4320
+ const auth = req.headers['authorization'] || '';
4321
+ const token = auth.startsWith('Bearer ') ? auth.slice(7).trim() : '';
4322
+ if (token !== currentApiKey) {
4323
+ res.writeHead(401, { 'Content-Type': 'application/json' });
4324
+ res.end(JSON.stringify({ error: 'Invalid or expired API key' }));
4325
+ return false;
4326
+ }
4327
+ return true;
4328
+ }
4329
+
4181
4330
  function startHealthServer() {
4182
4331
  const port = parseInt(process.env.TEAM_CONTEXT_HEALTH_PORT || '3141', 10);
4183
- const server = http.createServer((req, res) => {
4184
- if (req.url === '/healthz' && req.method === 'GET') {
4332
+
4333
+ // Load API key for search endpoints
4334
+ const teamDir = process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR;
4335
+ const keyExisted = teamDir && fs.existsSync(path.join(teamDir, '.wayfind-api-key'));
4336
+ currentApiKey = getOrCreateApiKey();
4337
+ if (currentApiKey) {
4338
+ console.log('API search endpoints enabled (key loaded)');
4339
+ // Push newly generated key so team members can pull it
4340
+ if (!keyExisted && teamDir) pushApiKey(teamDir);
4341
+ }
4342
+
4343
+ const server = http.createServer(async (req, res) => {
4344
+ const url = new URL(req.url, `http://localhost:${port}`);
4345
+
4346
+ if (url.pathname === '/healthz' && req.method === 'GET') {
4185
4347
  // Enrich with index freshness
4186
4348
  const storePath = contentStore.resolveStorePath();
4187
4349
  const index = contentStore.loadIndex(storePath);
@@ -4196,17 +4358,108 @@ function startHealthServer() {
4196
4358
  const botExpected = healthStatus.services.bot === 'running';
4197
4359
  const slackHealthy = !botExpected || slackStatus.connected;
4198
4360
 
4199
- const response = { ...healthStatus, index: indexInfo, slack: slackStatus };
4361
+ const response = {
4362
+ ...healthStatus,
4363
+ index: indexInfo,
4364
+ slack: slackStatus,
4365
+ api: { enabled: !!currentApiKey },
4366
+ };
4200
4367
  const status = (healthStatus.ok && slackHealthy) ? 200 : 503;
4201
4368
  res.writeHead(status, { 'Content-Type': 'application/json' });
4202
4369
  res.end(JSON.stringify(response));
4203
- } else {
4204
- res.writeHead(404);
4205
- res.end();
4370
+ return;
4206
4371
  }
4372
+
4373
+ // ── Search API: POST /api/search ──
4374
+ if (url.pathname === '/api/search' && req.method === 'POST') {
4375
+ if (!checkApiAuth(req, res)) return;
4376
+ try {
4377
+ const body = await parseJsonBody(req);
4378
+ const { query, limit = 10, repo, since, mode } = body;
4379
+ if (!query) {
4380
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4381
+ res.end(JSON.stringify({ error: 'query is required' }));
4382
+ return;
4383
+ }
4384
+
4385
+ const opts = { limit, repo, since };
4386
+ let results;
4387
+ if (mode === 'text') {
4388
+ results = contentStore.searchText(query, opts);
4389
+ } else {
4390
+ results = await contentStore.searchJournals(query, opts);
4391
+ }
4392
+
4393
+ const mapped = (results || []).map(r => ({
4394
+ id: r.id,
4395
+ score: r.score ? Math.round(r.score * 1000) / 1000 : null,
4396
+ date: r.entry.date,
4397
+ repo: r.entry.repo,
4398
+ title: r.entry.title,
4399
+ source: r.entry.source,
4400
+ tags: r.entry.tags || [],
4401
+ summary: r.entry.summary || null,
4402
+ }));
4403
+
4404
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4405
+ res.end(JSON.stringify({ found: mapped.length, results: mapped }));
4406
+ } catch (err) {
4407
+ res.writeHead(500, { 'Content-Type': 'application/json' });
4408
+ res.end(JSON.stringify({ error: err.message }));
4409
+ }
4410
+ return;
4411
+ }
4412
+
4413
+ // ── Entry API: GET /api/entry/:id ──
4414
+ if (url.pathname.startsWith('/api/entry/') && req.method === 'GET') {
4415
+ if (!checkApiAuth(req, res)) return;
4416
+ try {
4417
+ const id = decodeURIComponent(url.pathname.slice('/api/entry/'.length));
4418
+ if (!id) {
4419
+ res.writeHead(400, { 'Content-Type': 'application/json' });
4420
+ res.end(JSON.stringify({ error: 'entry id is required' }));
4421
+ return;
4422
+ }
4423
+
4424
+ const storePath = contentStore.resolveStorePath();
4425
+ const journalDir = process.env.TEAM_CONTEXT_JOURNALS_DIR || contentStore.DEFAULT_JOURNAL_DIR;
4426
+ const index = contentStore.getBackend(storePath).loadIndex();
4427
+
4428
+ if (!index || !index.entries || !index.entries[id]) {
4429
+ res.writeHead(404, { 'Content-Type': 'application/json' });
4430
+ res.end(JSON.stringify({ error: `Entry not found: ${id}` }));
4431
+ return;
4432
+ }
4433
+
4434
+ const entry = index.entries[id];
4435
+ const fullContent = contentStore.getEntryContent(id, { storePath, journalDir });
4436
+
4437
+ res.writeHead(200, { 'Content-Type': 'application/json' });
4438
+ res.end(JSON.stringify({
4439
+ id,
4440
+ date: entry.date,
4441
+ repo: entry.repo,
4442
+ title: entry.title,
4443
+ source: entry.source,
4444
+ tags: entry.tags || [],
4445
+ content: fullContent || entry.summary || null,
4446
+ }));
4447
+ } catch (err) {
4448
+ res.writeHead(500, { 'Content-Type': 'application/json' });
4449
+ res.end(JSON.stringify({ error: err.message }));
4450
+ }
4451
+ return;
4452
+ }
4453
+
4454
+ res.writeHead(404);
4455
+ res.end();
4207
4456
  });
4208
4457
  server.listen(port, () => {
4209
- console.log(`Health endpoint: http://0.0.0.0:${port}/healthz`);
4458
+ console.log(`Health + API endpoint: http://0.0.0.0:${port}/healthz`);
4459
+ if (currentApiKey) {
4460
+ console.log(` Search API: POST http://0.0.0.0:${port}/api/search`);
4461
+ console.log(` Entry API: GET http://0.0.0.0:${port}/api/entry/:id`);
4462
+ }
4210
4463
  });
4211
4464
  return server;
4212
4465
  }
@@ -4362,6 +4615,14 @@ function runStartScheduler() {
4362
4615
  await indexSignalsIfAvailable();
4363
4616
  });
4364
4617
 
4618
+ // Rotate API key daily (default 2am)
4619
+ const keyRotateSchedule = process.env.TEAM_CONTEXT_KEY_ROTATE_SCHEDULE || '0 2 * * *';
4620
+ console.log(`API key rotation schedule: ${keyRotateSchedule}`);
4621
+ scheduleCron(keyRotateSchedule, () => {
4622
+ console.log(`[${new Date().toISOString()}] Rotating API key...`);
4623
+ rotateApiKey();
4624
+ });
4625
+
4365
4626
  console.log('Scheduler running. Waiting for scheduled events...');
4366
4627
  }
4367
4628
 
package/doctor.sh CHANGED
@@ -275,9 +275,7 @@ check_team_context_freshness() {
275
275
 
276
276
  # Find team-context path via wayfind CLI
277
277
  local TEAM_PATH=""
278
- if [ -f "$HOME/repos/greg/wayfind/bin/team-context.js" ]; then
279
- TEAM_PATH=$(node "$HOME/repos/greg/wayfind/bin/team-context.js" context show 2>/dev/null | grep 'Path:' | head -1 | sed 's/.*Path: *//' || true)
280
- elif command -v wayfind >/dev/null 2>&1; then
278
+ if command -v wayfind >/dev/null 2>&1; then
281
279
  TEAM_PATH=$(wayfind context show 2>/dev/null | grep 'Path:' | head -1 | sed 's/.*Path: *//' || true)
282
280
  fi
283
281
 
@@ -337,9 +335,7 @@ check_team_versions() {
337
335
 
338
336
  # Check min_version from check-version command output
339
337
  local CHECK_OUTPUT=""
340
- if [ -f "$HOME/repos/greg/wayfind/bin/team-context.js" ]; then
341
- CHECK_OUTPUT=$(node "$HOME/repos/greg/wayfind/bin/team-context.js" check-version 2>&1 || true)
342
- elif command -v wayfind >/dev/null 2>&1; then
338
+ if command -v wayfind >/dev/null 2>&1; then
343
339
  CHECK_OUTPUT=$(wayfind check-version 2>&1 || true)
344
340
  fi
345
341
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.45",
3
+ "version": "2.0.47",
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",
@@ -19,7 +19,6 @@ fi
19
19
  WAYFIND="$(command -v wayfind 2>/dev/null || echo "")"
20
20
  if [ -z "$WAYFIND" ]; then
21
21
  for candidate in \
22
- "$HOME/repos/greg/wayfind/bin/team-context.js" \
23
22
  "$HOME/repos/wayfind/bin/team-context.js"; do
24
23
  if [ -f "$candidate" ]; then
25
24
  WAYFIND="node $candidate"
@@ -10,7 +10,6 @@ WAYFIND="$(command -v wayfind 2>/dev/null || echo "")"
10
10
  if [ -z "$WAYFIND" ]; then
11
11
  # Try common local checkout paths
12
12
  for candidate in \
13
- "$HOME/repos/greg/wayfind/bin/team-context.js" \
14
13
  "$HOME/repos/wayfind/bin/team-context.js"; do
15
14
  if [ -f "$candidate" ]; then
16
15
  WAYFIND="node $candidate"
@@ -32,14 +32,14 @@ bash ~/.claude/team-context/journal-summary.sh --dir ~/.ai-memory/memory/journal
32
32
 
33
33
  ## Step 2: If journal-summary.sh is not installed
34
34
 
35
- Run setup.sh with `--update` to install it:
35
+ Run `wayfind update` to install it:
36
36
  ```bash
37
- bash ~/repos/greg/wayfind/setup.sh --tool claude-code --update
37
+ wayfind update
38
38
  ```
39
39
 
40
- Or install manually:
40
+ Or install manually from the npm package:
41
41
  ```bash
42
- cp ~/repos/greg/wayfind/journal-summary.sh ~/.claude/team-context/journal-summary.sh
42
+ cp "$(npm root -g)/wayfind/journal-summary.sh" ~/.claude/team-context/journal-summary.sh
43
43
  chmod +x ~/.claude/team-context/journal-summary.sh
44
44
  ```
45
45
 
@@ -10,15 +10,9 @@ Show a daily standup summary from journals and state files.
10
10
 
11
11
  ```bash
12
12
  # Current repo only (default)
13
- node ~/repos/greg/wayfind/bin/team-context.js standup
13
+ wayfind standup
14
14
 
15
15
  # All repos
16
- node ~/repos/greg/wayfind/bin/team-context.js standup --all
17
- ```
18
-
19
- If the local checkout isn't available:
20
- ```bash
21
- wayfind standup
22
16
  wayfind standup --all
23
17
  ```
24
18
 
@@ -9,12 +9,8 @@
9
9
 
10
10
  set -euo pipefail
11
11
 
12
- # Use local wayfind checkout if available, otherwise try npx
13
- WAYFIND_BIN="$HOME/repos/greg/wayfind/bin/team-context.js"
14
- if [ -f "$WAYFIND_BIN" ]; then
15
- node "$WAYFIND_BIN" status --write --quiet 2>/dev/null || true
16
- node "$WAYFIND_BIN" check-version 2>/dev/null || true
17
- elif command -v wayfind >/dev/null 2>&1; then
12
+ # Use installed wayfind CLI
13
+ if command -v wayfind >/dev/null 2>&1; then
18
14
  wayfind status --write --quiet 2>/dev/null || true
19
15
  wayfind check-version 2>/dev/null || true
20
16
  fi
@@ -49,7 +49,7 @@ Record how you actually think about recurring decision types. Examples:
49
49
 
50
50
  ## Memory Files (load on demand)
51
51
 
52
- Load these from `~/.ai-memory/memory/` when the session topic matches:
52
+ Load these from `~/.claude/memory/` when the session topic matches:
53
53
 
54
54
  | File | When to load | Summary |
55
55
  |------|-------------|---------|
@@ -59,21 +59,21 @@ Load these from `~/.ai-memory/memory/` when the session topic matches:
59
59
 
60
60
  | Location | Covers |
61
61
  |----------|--------|
62
- | `~/.ai-memory/state.md` | Admin work, non-repo tasks |
63
- | `~/repos/org/repo/.ai-memory/state.md` | What this repo is about |
62
+ | `~/.claude/state.md` | Admin work, non-repo tasks |
63
+ | `~/repos/org/repo/.claude/state.md` | What this repo is about |
64
64
 
65
65
  ## Session Protocol
66
66
 
67
67
  **Start:**
68
- 1. Read this file + the repo's `.ai-memory/state.md`
68
+ 1. Read this file + the repo's `.claude/state.md`
69
69
  2. Check Memory Files table — load any that match this session's topic
70
70
  3. Summarize current state, then ask: **"What's the goal for this session? What does success look like?"**
71
71
 
72
72
  **Mid-session drift check:** If work diverges from the stated goal, flag it gently and ask whether to stay the course or pivot.
73
73
 
74
74
  **End (on "stop" / "done" / "pause" / "tomorrow"):**
75
- 1. Update the repo's `.ai-memory/state.md`
75
+ 1. Update the repo's `.claude/state.md`
76
76
  2. Do NOT update this file's Active Projects table — it is rebuilt automatically by `wayfind status`.
77
77
  3. Create/update topic memory files for any significant new cross-repo context
78
- 4. Append to `~/.ai-memory/memory/journal/YYYY-MM-DD.md`
78
+ 4. Append to `~/.claude/memory/journal/YYYY-MM-DD.md`
79
79
  5. Confirm: **"State saved. Say 'let's continue' next time."**