wayfind 2.0.44 → 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 +385 -54
- package/package.json +1 -1
- package/templates/deploy/.env.example +33 -69
- package/templates/deploy/docker-compose.yml +1 -44
- 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,25 +3804,51 @@ 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
|
}
|
|
3813
3831
|
|
|
3814
3832
|
/**
|
|
3815
|
-
* Scaffold a per-team container config
|
|
3833
|
+
* Scaffold a per-team container config in the team's registered repo (deploy/ subdir).
|
|
3834
|
+
* Falls back to ~/.claude/team-context/teams/<teamId>/deploy/ if no repo is registered.
|
|
3816
3835
|
*/
|
|
3817
3836
|
function deployTeamInit(teamId, { port } = {}) {
|
|
3818
|
-
|
|
3819
|
-
if (!teamsBaseDir) {
|
|
3837
|
+
if (!HOME) {
|
|
3820
3838
|
console.error('Cannot resolve home directory.');
|
|
3821
3839
|
process.exit(1);
|
|
3822
3840
|
}
|
|
3823
3841
|
|
|
3824
|
-
|
|
3825
|
-
const
|
|
3842
|
+
// Resolve deploy dir: team repo path first, fallback to store-adjacent
|
|
3843
|
+
const config = readContextConfig();
|
|
3844
|
+
const teamEntry = config.teams && config.teams[teamId];
|
|
3845
|
+
const teamContextPath = teamEntry ? teamEntry.path : null;
|
|
3846
|
+
const deployDir = teamContextPath
|
|
3847
|
+
? path.join(teamContextPath, 'deploy')
|
|
3848
|
+
: path.join(HOME, '.claude', 'team-context', 'teams', teamId, 'deploy');
|
|
3849
|
+
|
|
3850
|
+
// Ensure the per-team store dir exists
|
|
3851
|
+
const storeDir = path.join(HOME, '.claude', 'team-context', 'teams', teamId, 'content-store');
|
|
3826
3852
|
|
|
3827
3853
|
// Check for duplicate running container
|
|
3828
3854
|
const psResult = spawnSync('docker', ['ps', '--filter', `label=com.wayfind.team=${teamId}`, '--format', '{{.Names}}'], { stdio: 'pipe' });
|
|
@@ -3838,22 +3864,42 @@ function deployTeamInit(teamId, { port } = {}) {
|
|
|
3838
3864
|
console.log(`Scaffolding deploy config for team: ${teamId}`);
|
|
3839
3865
|
console.log(`Deploy dir: ${deployDir}`);
|
|
3840
3866
|
|
|
3841
|
-
//
|
|
3842
|
-
const config = readContextConfig();
|
|
3843
|
-
const teamEntry = config.teams && config.teams[teamId];
|
|
3844
|
-
const teamContextPath = teamEntry ? teamEntry.path : null;
|
|
3845
|
-
|
|
3846
|
-
// Assign port (default 3141; if taken, user should pass --port)
|
|
3847
|
-
const assignedPort = port || 3141;
|
|
3867
|
+
// Auto-detect port: find all wayfind containers, pick next available
|
|
3848
3868
|
const containerName = `wayfind-${teamId}`;
|
|
3869
|
+
let assignedPort = port;
|
|
3870
|
+
if (!assignedPort) {
|
|
3871
|
+
const usedPorts = new Set();
|
|
3872
|
+
// Check labeled containers
|
|
3873
|
+
const portsResult = spawnSync('docker', [
|
|
3874
|
+
'ps', '--filter', 'label=com.wayfind.team',
|
|
3875
|
+
'--format', '{{.Ports}}',
|
|
3876
|
+
], { stdio: 'pipe' });
|
|
3877
|
+
// Also check legacy container named "wayfind"
|
|
3878
|
+
const legacyPortsResult = spawnSync('docker', [
|
|
3879
|
+
'ps', '--filter', 'name=^wayfind',
|
|
3880
|
+
'--format', '{{.Ports}}',
|
|
3881
|
+
], { stdio: 'pipe' });
|
|
3882
|
+
const allPortOutput = [
|
|
3883
|
+
(portsResult.stdout || '').toString(),
|
|
3884
|
+
(legacyPortsResult.stdout || '').toString(),
|
|
3885
|
+
].join('\n');
|
|
3886
|
+
// Extract host ports from "0.0.0.0:3141->3141/tcp" patterns
|
|
3887
|
+
for (const match of allPortOutput.matchAll(/:(\d+)->/g)) {
|
|
3888
|
+
usedPorts.add(parseInt(match[1], 10));
|
|
3889
|
+
}
|
|
3890
|
+
assignedPort = 3141;
|
|
3891
|
+
while (usedPorts.has(assignedPort)) assignedPort++;
|
|
3892
|
+
if (assignedPort !== 3141) {
|
|
3893
|
+
console.log(`Port 3141 in use — assigning port ${assignedPort}`);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3849
3896
|
|
|
3850
3897
|
// Build docker-compose.yml content with per-team overrides
|
|
3851
3898
|
const templatePath = path.join(DEPLOY_TEMPLATES_DIR, 'docker-compose.yml');
|
|
3852
3899
|
let composeContent = fs.readFileSync(templatePath, 'utf8');
|
|
3853
3900
|
composeContent = composeContent
|
|
3854
3901
|
.replace(/container_name: wayfind/, `container_name: ${containerName}`)
|
|
3855
|
-
.replace(/- "3141:3141"/, `- "${assignedPort}:3141"`)
|
|
3856
|
-
.replace(/(TEAM_CONTEXT_TENANT_ID:.*$)/m, `TEAM_CONTEXT_TENANT_ID: \${TEAM_CONTEXT_TENANT_ID:-${teamId}}`);
|
|
3902
|
+
.replace(/- "3141:3141"/, `- "${assignedPort}:3141"`);
|
|
3857
3903
|
|
|
3858
3904
|
// Inject Docker label for discovery
|
|
3859
3905
|
composeContent = composeContent.replace(
|
|
@@ -3869,42 +3915,72 @@ function deployTeamInit(teamId, { port } = {}) {
|
|
|
3869
3915
|
console.log(' docker-compose.yml — already exists, skipping');
|
|
3870
3916
|
}
|
|
3871
3917
|
|
|
3872
|
-
// .env.example
|
|
3918
|
+
// .env.example — full reference with all options
|
|
3873
3919
|
const envExampleSrc = path.join(DEPLOY_TEMPLATES_DIR, '.env.example');
|
|
3874
3920
|
const envExampleDst = path.join(deployDir, '.env.example');
|
|
3875
3921
|
if (!fs.existsSync(envExampleDst) && fs.existsSync(envExampleSrc)) {
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
envContent += `\nTEAM_CONTEXT_TEAM_CONTEXT_PATH=${teamContextPath}\n`;
|
|
3879
|
-
}
|
|
3880
|
-
fs.writeFileSync(envExampleDst, envContent, 'utf8');
|
|
3881
|
-
console.log(' .env.example — created');
|
|
3922
|
+
fs.copyFileSync(envExampleSrc, envExampleDst);
|
|
3923
|
+
console.log(' .env.example — created (full reference)');
|
|
3882
3924
|
}
|
|
3883
3925
|
|
|
3884
|
-
// .env
|
|
3926
|
+
// .env — minimal seed with only required keys
|
|
3885
3927
|
const envPath = path.join(deployDir, '.env');
|
|
3886
|
-
if (!fs.existsSync(envPath)
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3928
|
+
if (!fs.existsSync(envPath)) {
|
|
3929
|
+
const ghToken = detectGitHubToken();
|
|
3930
|
+
const lines = [
|
|
3931
|
+
'# Wayfind — required configuration',
|
|
3932
|
+
'# See .env.example for all available options.',
|
|
3933
|
+
'',
|
|
3934
|
+
'# Anthropic API key (for digests and bot answers)',
|
|
3935
|
+
'ANTHROPIC_API_KEY=sk-ant-your-key',
|
|
3936
|
+
'',
|
|
3937
|
+
'# GitHub token (for pulling team journals and signals)',
|
|
3938
|
+
`GITHUB_TOKEN=${ghToken || ''}`,
|
|
3939
|
+
'',
|
|
3940
|
+
`TEAM_CONTEXT_TENANT_ID=${teamId}`,
|
|
3941
|
+
];
|
|
3942
|
+
// Set volume mount path so docker-compose.yml resolves correctly
|
|
3943
|
+
if (teamContextPath) {
|
|
3944
|
+
lines.push(`TEAM_CONTEXT_TEAM_CONTEXT_PATH=${teamContextPath}`);
|
|
3945
|
+
}
|
|
3946
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf8');
|
|
3947
|
+
console.log(' .env — created (fill in ANTHROPIC_API_KEY)');
|
|
3948
|
+
if (ghToken) {
|
|
3898
3949
|
console.log(' GITHUB_TOKEN — auto-detected from gh CLI');
|
|
3899
3950
|
}
|
|
3951
|
+
} else {
|
|
3952
|
+
console.log(' .env — already exists, skipping');
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
// Ensure deploy/.env is gitignored if we're in a repo
|
|
3956
|
+
if (teamContextPath) {
|
|
3957
|
+
const gitignorePath = path.join(teamContextPath, '.gitignore');
|
|
3958
|
+
const gitignoreEntry = 'deploy/.env';
|
|
3959
|
+
if (fs.existsSync(gitignorePath)) {
|
|
3960
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
3961
|
+
if (!content.includes(gitignoreEntry)) {
|
|
3962
|
+
fs.appendFileSync(gitignorePath, `\n${gitignoreEntry}\n`);
|
|
3963
|
+
console.log(' .gitignore — added deploy/.env');
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
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).');
|
|
3900
3975
|
}
|
|
3901
3976
|
|
|
3902
3977
|
console.log('');
|
|
3903
3978
|
console.log('Next steps:');
|
|
3904
|
-
console.log(` 1.
|
|
3979
|
+
console.log(` 1. Set ANTHROPIC_API_KEY in ${deployDir}/.env`);
|
|
3905
3980
|
console.log(` 2. cd "${deployDir}" && docker compose up -d`);
|
|
3906
3981
|
console.log(` 3. Verify: curl http://localhost:${assignedPort}/healthz`);
|
|
3907
3982
|
console.log('');
|
|
3983
|
+
console.log('See .env.example for optional config (Slack, embeddings, signals, schedules).');
|
|
3908
3984
|
console.log(`Tip: run "wayfind deploy list" to see all running team containers.`);
|
|
3909
3985
|
|
|
3910
3986
|
telemetry.capture('deploy_team_init', { teamId }, CLI_USER);
|
|
@@ -3928,7 +4004,7 @@ function deployList() {
|
|
|
3928
4004
|
const rows = (psResult.stdout || '').toString().trim();
|
|
3929
4005
|
if (!rows) {
|
|
3930
4006
|
console.log('No Wayfind team containers running.');
|
|
3931
|
-
console.log('Start one with: wayfind deploy --team <teamId>
|
|
4007
|
+
console.log('Start one with: wayfind deploy --team <teamId>');
|
|
3932
4008
|
return;
|
|
3933
4009
|
}
|
|
3934
4010
|
|
|
@@ -4125,14 +4201,149 @@ function deployStatus() {
|
|
|
4125
4201
|
}
|
|
4126
4202
|
}
|
|
4127
4203
|
|
|
4128
|
-
// ──
|
|
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 ───────────────────────────────────────────────────
|
|
4129
4292
|
|
|
4130
4293
|
let healthStatus = { ok: true, mode: null, started: null, services: {} };
|
|
4131
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
|
+
|
|
4132
4330
|
function startHealthServer() {
|
|
4133
4331
|
const port = parseInt(process.env.TEAM_CONTEXT_HEALTH_PORT || '3141', 10);
|
|
4134
|
-
|
|
4135
|
-
|
|
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') {
|
|
4136
4347
|
// Enrich with index freshness
|
|
4137
4348
|
const storePath = contentStore.resolveStorePath();
|
|
4138
4349
|
const index = contentStore.loadIndex(storePath);
|
|
@@ -4147,17 +4358,108 @@ function startHealthServer() {
|
|
|
4147
4358
|
const botExpected = healthStatus.services.bot === 'running';
|
|
4148
4359
|
const slackHealthy = !botExpected || slackStatus.connected;
|
|
4149
4360
|
|
|
4150
|
-
const response = {
|
|
4361
|
+
const response = {
|
|
4362
|
+
...healthStatus,
|
|
4363
|
+
index: indexInfo,
|
|
4364
|
+
slack: slackStatus,
|
|
4365
|
+
api: { enabled: !!currentApiKey },
|
|
4366
|
+
};
|
|
4151
4367
|
const status = (healthStatus.ok && slackHealthy) ? 200 : 503;
|
|
4152
4368
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
4153
4369
|
res.end(JSON.stringify(response));
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4370
|
+
return;
|
|
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;
|
|
4157
4452
|
}
|
|
4453
|
+
|
|
4454
|
+
res.writeHead(404);
|
|
4455
|
+
res.end();
|
|
4158
4456
|
});
|
|
4159
4457
|
server.listen(port, () => {
|
|
4160
|
-
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
|
+
}
|
|
4161
4463
|
});
|
|
4162
4464
|
return server;
|
|
4163
4465
|
}
|
|
@@ -4170,8 +4472,10 @@ async function runStart() {
|
|
|
4170
4472
|
|
|
4171
4473
|
// Validate required env vars before proceeding
|
|
4172
4474
|
const missing = [];
|
|
4173
|
-
|
|
4174
|
-
|
|
4475
|
+
// Slack tokens are only required for modes that run the bot
|
|
4476
|
+
const needsSlack = ['bot', 'all-in-one'].includes(mode) && !process.env.TEAM_CONTEXT_NO_SLACK;
|
|
4477
|
+
if (needsSlack && !process.env.SLACK_BOT_TOKEN) missing.push('SLACK_BOT_TOKEN');
|
|
4478
|
+
if (needsSlack && !process.env.SLACK_APP_TOKEN) missing.push('SLACK_APP_TOKEN');
|
|
4175
4479
|
if (!process.env.ANTHROPIC_API_KEY) missing.push('ANTHROPIC_API_KEY');
|
|
4176
4480
|
if (missing.length > 0) {
|
|
4177
4481
|
console.error('');
|
|
@@ -4181,6 +4485,8 @@ async function runStart() {
|
|
|
4181
4485
|
console.error(' cp deploy/.env.example deploy/.env');
|
|
4182
4486
|
console.error(' # Fill in your tokens, then: docker compose up -d');
|
|
4183
4487
|
console.error('');
|
|
4488
|
+
console.error('Tip: set TEAM_CONTEXT_NO_SLACK=1 to run without Slack integration.');
|
|
4489
|
+
console.error('');
|
|
4184
4490
|
process.exit(1);
|
|
4185
4491
|
}
|
|
4186
4492
|
|
|
@@ -4309,6 +4615,14 @@ function runStartScheduler() {
|
|
|
4309
4615
|
await indexSignalsIfAvailable();
|
|
4310
4616
|
});
|
|
4311
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
|
+
|
|
4312
4626
|
console.log('Scheduler running. Waiting for scheduled events...');
|
|
4313
4627
|
}
|
|
4314
4628
|
|
|
@@ -5181,13 +5495,13 @@ const COMMANDS = {
|
|
|
5181
5495
|
if (labelDir && fs.existsSync(path.join(labelDir, 'docker-compose.yml'))) {
|
|
5182
5496
|
composeDir = labelDir;
|
|
5183
5497
|
} else {
|
|
5184
|
-
//
|
|
5185
|
-
const
|
|
5186
|
-
if (
|
|
5187
|
-
for (const tid of
|
|
5188
|
-
|
|
5498
|
+
// Check team repo paths from context.json first
|
|
5499
|
+
const updateConfig = readContextConfig();
|
|
5500
|
+
if (updateConfig.teams) {
|
|
5501
|
+
for (const [tid, entry] of Object.entries(updateConfig.teams)) {
|
|
5502
|
+
if (!entry.path) continue;
|
|
5503
|
+
const candidate = path.join(entry.path, 'deploy');
|
|
5189
5504
|
if (fs.existsSync(path.join(candidate, 'docker-compose.yml'))) {
|
|
5190
|
-
// Check if this compose file manages this container
|
|
5191
5505
|
const checkResult = spawnSync('docker', ['compose', 'ps', '--format', '{{.Name}}'], { cwd: candidate, stdio: 'pipe' });
|
|
5192
5506
|
const composeContainers = (checkResult.stdout || '').toString();
|
|
5193
5507
|
if (composeContainers.includes(containerName)) {
|
|
@@ -5197,6 +5511,23 @@ const COMMANDS = {
|
|
|
5197
5511
|
}
|
|
5198
5512
|
}
|
|
5199
5513
|
}
|
|
5514
|
+
// Fallback: check store-adjacent deploy dirs
|
|
5515
|
+
if (!composeDir) {
|
|
5516
|
+
const teamsBase = HOME ? path.join(HOME, '.claude', 'team-context', 'teams') : '';
|
|
5517
|
+
if (teamsBase && fs.existsSync(teamsBase)) {
|
|
5518
|
+
for (const tid of fs.readdirSync(teamsBase)) {
|
|
5519
|
+
const candidate = path.join(teamsBase, tid, 'deploy');
|
|
5520
|
+
if (fs.existsSync(path.join(candidate, 'docker-compose.yml'))) {
|
|
5521
|
+
const checkResult = spawnSync('docker', ['compose', 'ps', '--format', '{{.Name}}'], { cwd: candidate, stdio: 'pipe' });
|
|
5522
|
+
const composeContainers = (checkResult.stdout || '').toString();
|
|
5523
|
+
if (composeContainers.includes(containerName)) {
|
|
5524
|
+
composeDir = candidate;
|
|
5525
|
+
break;
|
|
5526
|
+
}
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5200
5531
|
// Legacy fallback
|
|
5201
5532
|
if (!composeDir) {
|
|
5202
5533
|
const legacyCandidates = [process.cwd(), path.join(HOME || '', 'team-context', 'deploy')];
|
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# Wayfind — Docker environment configuration
|
|
2
2
|
# Copy to .env and fill in your values: cp .env.example .env
|
|
3
|
+
#
|
|
4
|
+
# Only ANTHROPIC_API_KEY and GITHUB_TOKEN are required.
|
|
5
|
+
# Everything else is optional — add as needed.
|
|
3
6
|
|
|
4
7
|
# ── Required ──────────────────────────────────────────────────────────────────
|
|
5
8
|
|
|
6
|
-
# Slack bot tokens — from the Slack app you created using slack-app-manifest.json.
|
|
7
|
-
# Go to api.slack.com/apps → your Wayfind app → OAuth & Permissions for the bot token,
|
|
8
|
-
# and Basic Information → App-Level Tokens for the app token.
|
|
9
|
-
SLACK_BOT_TOKEN=xoxb-your-bot-token
|
|
10
|
-
SLACK_APP_TOKEN=xapp-your-app-token
|
|
11
|
-
|
|
12
9
|
# Anthropic API key (for digests and bot answers)
|
|
13
10
|
# Get one at console.anthropic.com
|
|
14
11
|
ANTHROPIC_API_KEY=sk-ant-your-key
|
|
@@ -18,73 +15,33 @@ ANTHROPIC_API_KEY=sk-ant-your-key
|
|
|
18
15
|
# Needs read access to the team-context repo.
|
|
19
16
|
GITHUB_TOKEN=
|
|
20
17
|
|
|
21
|
-
# ──
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# Fallback: webhook. Used only if bot delivery fails.
|
|
18
|
+
# ── Slack (for digest delivery and bot) ──────────────────────────────────────
|
|
19
|
+
# Without these, set TEAM_CONTEXT_NO_SLACK=1 and the container runs in
|
|
20
|
+
# scheduler/worker-only mode (no digest posting, no bot answers).
|
|
25
21
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
22
|
+
# SLACK_BOT_TOKEN=xoxb-your-bot-token
|
|
23
|
+
# SLACK_APP_TOKEN=xapp-your-app-token
|
|
28
24
|
# SLACK_DIGEST_CHANNEL=C0123456789
|
|
29
|
-
|
|
30
|
-
# Slack webhook — fallback for digest delivery if bot token is unavailable
|
|
31
25
|
# TEAM_CONTEXT_SLACK_WEBHOOK=https://hooks.slack.com/services/T.../B.../...
|
|
32
26
|
|
|
33
|
-
# ──
|
|
34
|
-
|
|
35
|
-
# Tenant identifier (prefixes storage paths)
|
|
36
|
-
# TEAM_CONTEXT_TENANT_ID=my-team
|
|
37
|
-
|
|
38
|
-
# Team repo allowlist — only journals from these repos appear in digests and queries.
|
|
39
|
-
# Supports org/* wildcards. This is the recommended way to scope a container to one team.
|
|
40
|
-
# TEAM_CONTEXT_INCLUDE_REPOS=MyOrg/*,MyOrg-Libs/*
|
|
41
|
-
|
|
42
|
-
# DEPRECATED: Blocklist approach. Use INCLUDE_REPOS instead.
|
|
43
|
-
# TEAM_CONTEXT_EXCLUDE_REPOS=wayfind,personal-project
|
|
44
|
-
|
|
45
|
-
# Encryption key — generate with: openssl rand -base64 32
|
|
46
|
-
# TEAM_CONTEXT_ENCRYPTION_KEY=
|
|
47
|
-
|
|
48
|
-
# Author slug for CLI telemetry attribution
|
|
49
|
-
# TEAM_CONTEXT_AUTHOR=greg
|
|
50
|
-
|
|
51
|
-
# ── LLM models ───────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
# Model for digest generation and onboarding packs (default: claude-sonnet-4-5-20250929)
|
|
54
|
-
# TEAM_CONTEXT_LLM_MODEL=claude-sonnet-4-5-20250929
|
|
55
|
-
|
|
56
|
-
# Model for conversation transcript extraction (default: claude-sonnet-4-5-20250929)
|
|
57
|
-
# TEAM_CONTEXT_EXTRACTION_MODEL=claude-sonnet-4-5-20250929
|
|
58
|
-
|
|
59
|
-
# ── Embeddings (without these, bot uses keyword search only) ─────────────────
|
|
27
|
+
# ── Embeddings (for semantic search — falls back to keyword search without) ──
|
|
60
28
|
# Option A: OpenAI
|
|
61
29
|
# OPENAI_API_KEY=sk-your-openai-key
|
|
62
|
-
# Option B: Azure OpenAI
|
|
30
|
+
# Option B: Azure OpenAI
|
|
63
31
|
# AZURE_OPENAI_EMBEDDING_ENDPOINT=https://your-resource.openai.azure.com/
|
|
64
32
|
# AZURE_OPENAI_EMBEDDING_KEY=your-key
|
|
65
33
|
# AZURE_OPENAI_EMBEDDING_DEPLOYMENT=text-embedding-3-small
|
|
66
34
|
|
|
67
|
-
# ──
|
|
68
|
-
|
|
69
|
-
# Override content store path (default: ~/.claude/team-context/content-store)
|
|
70
|
-
# TEAM_CONTEXT_STORE_PATH=/data/content-store
|
|
71
|
-
|
|
72
|
-
# Override signals directory (default: ~/.claude/team-context/signals)
|
|
73
|
-
# TEAM_CONTEXT_SIGNALS_DIR=/data/signals
|
|
35
|
+
# ── Team scoping ─────────────────────────────────────────────────────────────
|
|
74
36
|
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
|
|
78
|
-
# ── Schedules ────────────────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
-
# Digest schedule (cron, default: daily 12pm UTC / 7am ET)
|
|
81
|
-
# TEAM_CONTEXT_DIGEST_SCHEDULE=0 12 * * *
|
|
37
|
+
# Tenant identifier (prefixes storage paths)
|
|
38
|
+
# TEAM_CONTEXT_TENANT_ID=my-team
|
|
82
39
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
40
|
+
# Team repo allowlist — only journals from these repos appear in digests/queries.
|
|
41
|
+
# TEAM_CONTEXT_INCLUDE_REPOS=MyOrg/*,MyOrg-Libs/*
|
|
85
42
|
|
|
86
|
-
#
|
|
87
|
-
#
|
|
43
|
+
# Author slug for telemetry attribution
|
|
44
|
+
# TEAM_CONTEXT_AUTHOR=greg
|
|
88
45
|
|
|
89
46
|
# ── Signal sources ───────────────────────────────────────────────────────────
|
|
90
47
|
|
|
@@ -93,21 +50,28 @@ GITHUB_TOKEN=
|
|
|
93
50
|
|
|
94
51
|
# Intercom API token for support signal ingestion
|
|
95
52
|
# INTERCOM_TOKEN=your-intercom-token
|
|
96
|
-
|
|
97
|
-
# Filter Intercom conversations by tags (comma-separated)
|
|
98
53
|
# TEAM_CONTEXT_INTERCOM_TAGS=bug,feature-request
|
|
99
54
|
|
|
100
55
|
# Notion integration token for page/database signal ingestion
|
|
101
|
-
# Create at: https://www.notion.so/my-integrations
|
|
102
56
|
# NOTION_TOKEN=ntn_your-integration-token
|
|
103
|
-
|
|
104
|
-
# Specific Notion database IDs to monitor (comma-separated, or blank for all shared pages)
|
|
105
57
|
# TEAM_CONTEXT_NOTION_DATABASES=db_id1,db_id2
|
|
106
58
|
|
|
107
|
-
# ──
|
|
59
|
+
# ── Schedules ────────────────────────────────────────────────────────────────
|
|
108
60
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
61
|
+
# TEAM_CONTEXT_DIGEST_SCHEDULE=0 12 * * *
|
|
62
|
+
# TEAM_CONTEXT_SIGNAL_SCHEDULE=0 6 * * *
|
|
63
|
+
# TEAM_CONTEXT_REINDEX_SCHEDULE=0 * * * *
|
|
111
64
|
|
|
112
|
-
#
|
|
65
|
+
# ── Advanced ─────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
# Set to 1 to run without Slack (scheduler + worker only, no bot)
|
|
68
|
+
# TEAM_CONTEXT_NO_SLACK=1
|
|
69
|
+
|
|
70
|
+
# TEAM_CONTEXT_LLM_MODEL=claude-sonnet-4-5-20250929
|
|
71
|
+
# TEAM_CONTEXT_EXTRACTION_MODEL=claude-sonnet-4-5-20250929
|
|
72
|
+
# TEAM_CONTEXT_ENCRYPTION_KEY=
|
|
73
|
+
# TEAM_CONTEXT_HEALTH_PORT=3141
|
|
113
74
|
# TEAM_CONTEXT_TELEMETRY=true
|
|
75
|
+
# TEAM_CONTEXT_STORE_PATH=/data/content-store
|
|
76
|
+
# TEAM_CONTEXT_SIGNALS_DIR=/data/signals
|
|
77
|
+
# TEAM_CONTEXT_CONVERSATIONS_DIR=/data/conversations
|
|
@@ -7,52 +7,9 @@ services:
|
|
|
7
7
|
image: ghcr.io/usewayfind/wayfind:latest
|
|
8
8
|
container_name: wayfind
|
|
9
9
|
restart: unless-stopped
|
|
10
|
+
env_file: .env
|
|
10
11
|
environment:
|
|
11
12
|
TEAM_CONTEXT_MODE: all-in-one
|
|
12
|
-
TEAM_CONTEXT_TENANT_ID: ${TEAM_CONTEXT_TENANT_ID:-my-team}
|
|
13
|
-
TEAM_CONTEXT_AUTHOR: ${TEAM_CONTEXT_AUTHOR:-}
|
|
14
|
-
|
|
15
|
-
# Slack
|
|
16
|
-
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN}
|
|
17
|
-
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN}
|
|
18
|
-
SLACK_DIGEST_CHANNEL: ${SLACK_DIGEST_CHANNEL:-}
|
|
19
|
-
TEAM_CONTEXT_SLACK_WEBHOOK: ${TEAM_CONTEXT_SLACK_WEBHOOK:-}
|
|
20
|
-
|
|
21
|
-
# Team repo allowlist (recommended over EXCLUDE_REPOS)
|
|
22
|
-
TEAM_CONTEXT_INCLUDE_REPOS: ${TEAM_CONTEXT_INCLUDE_REPOS:-}
|
|
23
|
-
# DEPRECATED: Blocklist approach — use INCLUDE_REPOS instead
|
|
24
|
-
TEAM_CONTEXT_EXCLUDE_REPOS: ${TEAM_CONTEXT_EXCLUDE_REPOS:-}
|
|
25
|
-
|
|
26
|
-
# Telemetry (opt-in, sends anonymous usage data to improve Wayfind)
|
|
27
|
-
TEAM_CONTEXT_TELEMETRY: ${TEAM_CONTEXT_TELEMETRY:-false}
|
|
28
|
-
|
|
29
|
-
# GitHub signals
|
|
30
|
-
GITHUB_TOKEN: ${GITHUB_TOKEN:-}
|
|
31
|
-
|
|
32
|
-
# Intercom signals
|
|
33
|
-
INTERCOM_TOKEN: ${INTERCOM_TOKEN:-}
|
|
34
|
-
TEAM_CONTEXT_INTERCOM_TAGS: ${TEAM_CONTEXT_INTERCOM_TAGS:-}
|
|
35
|
-
|
|
36
|
-
# LLM
|
|
37
|
-
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
|
|
38
|
-
TEAM_CONTEXT_LLM_MODEL: ${TEAM_CONTEXT_LLM_MODEL:-claude-sonnet-4-5-20250929}
|
|
39
|
-
|
|
40
|
-
# Embeddings (for semantic search — falls back to keyword search without these)
|
|
41
|
-
# Option A: OpenAI
|
|
42
|
-
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
|
|
43
|
-
# Option B: Azure OpenAI
|
|
44
|
-
AZURE_OPENAI_EMBEDDING_ENDPOINT: ${AZURE_OPENAI_EMBEDDING_ENDPOINT:-}
|
|
45
|
-
AZURE_OPENAI_EMBEDDING_KEY: ${AZURE_OPENAI_EMBEDDING_KEY:-}
|
|
46
|
-
AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${AZURE_OPENAI_EMBEDDING_DEPLOYMENT:-text-embedding-3-small}
|
|
47
|
-
|
|
48
|
-
# Encryption — generate with: openssl rand -base64 32
|
|
49
|
-
TEAM_CONTEXT_ENCRYPTION_KEY: ${TEAM_CONTEXT_ENCRYPTION_KEY:-}
|
|
50
|
-
|
|
51
|
-
# Scheduling
|
|
52
|
-
TEAM_CONTEXT_DIGEST_SCHEDULE: ${TEAM_CONTEXT_DIGEST_SCHEDULE:-0 8 * * 1}
|
|
53
|
-
TEAM_CONTEXT_SIGNAL_SCHEDULE: ${TEAM_CONTEXT_SIGNAL_SCHEDULE:-0 6 * * *}
|
|
54
|
-
|
|
55
|
-
# Team context repo (mounted at /data/team-context for git pull)
|
|
56
13
|
TEAM_CONTEXT_TEAM_CONTEXT_DIR: /data/team-context
|
|
57
14
|
TEAM_CONTEXT_JOURNALS_DIR: /data/team-context/journals
|
|
58
15
|
volumes:
|
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."**
|