wayfind 2.0.45 → 2.0.46
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/BOOTSTRAP_PROMPT.md +28 -2
- package/README.md +19 -0
- package/bin/mcp-server.js +225 -18
- package/bin/team-context.js +271 -10
- package/package.json +1 -1
- package/templates/global.md +6 -6
package/BOOTSTRAP_PROMPT.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
//
|
|
375
|
+
// Try local first (fastest)
|
|
174
376
|
const index = contentStore.getBackend(storePath).loadIndex();
|
|
175
|
-
if (
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
const
|
|
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;
|
package/bin/team-context.js
CHANGED
|
@@ -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
|
-
// ──
|
|
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
|
-
|
|
4184
|
-
|
|
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 = {
|
|
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
|
-
|
|
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/package.json
CHANGED
package/templates/global.md
CHANGED
|
@@ -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 `~/.
|
|
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
|
-
| `~/.
|
|
63
|
-
| `~/repos/org/repo/.
|
|
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 `.
|
|
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 `.
|
|
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 `~/.
|
|
78
|
+
4. Append to `~/.claude/memory/journal/YYYY-MM-DD.md`
|
|
79
79
|
5. Confirm: **"State saved. Say 'let's continue' next time."**
|