wayfind 2.0.39 → 2.0.40
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/bin/content-store.js +115 -21
- package/bin/mcp-server.js +411 -0
- package/bin/team-context.js +241 -65
- package/package.json +4 -2
package/bin/content-store.js
CHANGED
|
@@ -17,6 +17,98 @@ const DEFAULT_SIGNALS_DIR = HOME ? path.join(HOME, '.claude', 'team-context', 's
|
|
|
17
17
|
const INDEX_VERSION = '2.0.0';
|
|
18
18
|
const FILE_PERMS = 0o600;
|
|
19
19
|
|
|
20
|
+
// ── Team-scoped path resolution ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Cache team ID for the process lifetime (cwd doesn't change mid-command). */
|
|
23
|
+
let _cachedTeamId;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the active team ID from the local repo binding or context.json default.
|
|
27
|
+
* @returns {string|null}
|
|
28
|
+
*/
|
|
29
|
+
function _resolveTeamId() {
|
|
30
|
+
if (_cachedTeamId !== undefined) return _cachedTeamId;
|
|
31
|
+
|
|
32
|
+
// 1. .claude/wayfind.json in cwd
|
|
33
|
+
try {
|
|
34
|
+
const bindingFile = require('path').join(process.cwd(), '.claude', 'wayfind.json');
|
|
35
|
+
if (require('fs').existsSync(bindingFile)) {
|
|
36
|
+
const binding = JSON.parse(require('fs').readFileSync(bindingFile, 'utf8'));
|
|
37
|
+
if (binding.team_id) { _cachedTeamId = binding.team_id; return _cachedTeamId; }
|
|
38
|
+
}
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
|
|
41
|
+
// 2. context.json default team
|
|
42
|
+
try {
|
|
43
|
+
const contextFile = HOME ? require('path').join(HOME, '.claude', 'team-context', 'context.json') : null;
|
|
44
|
+
if (contextFile && require('fs').existsSync(contextFile)) {
|
|
45
|
+
const ctx = JSON.parse(require('fs').readFileSync(contextFile, 'utf8'));
|
|
46
|
+
if (ctx.default) { _cachedTeamId = ctx.default; return _cachedTeamId; }
|
|
47
|
+
}
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
|
|
50
|
+
_cachedTeamId = null;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One-time migration: copy global content store to the team-scoped path.
|
|
56
|
+
* Safe to call repeatedly — exits immediately if team store already exists.
|
|
57
|
+
*/
|
|
58
|
+
function _autoMigrateToTeamStore(teamStorePath) {
|
|
59
|
+
if (fs.existsSync(teamStorePath)) return;
|
|
60
|
+
if (!DEFAULT_STORE_PATH || !fs.existsSync(DEFAULT_STORE_PATH)) return;
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(teamStorePath, { recursive: true });
|
|
63
|
+
for (const f of fs.readdirSync(DEFAULT_STORE_PATH)) {
|
|
64
|
+
const src = path.join(DEFAULT_STORE_PATH, f);
|
|
65
|
+
if (fs.statSync(src).isFile()) {
|
|
66
|
+
fs.copyFileSync(src, path.join(teamStorePath, f));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
fs.writeFileSync(path.join(teamStorePath, '.migrated-from-global'), new Date().toISOString());
|
|
70
|
+
} catch (_) {
|
|
71
|
+
// Non-fatal — fresh store will be created on first write
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve the content store path.
|
|
77
|
+
* Priority:
|
|
78
|
+
* 1. TEAM_CONTEXT_STORE_PATH env var (container always sets this)
|
|
79
|
+
* 2. Explicit teamId argument
|
|
80
|
+
* 3. Repo-level .claude/wayfind.json team_id
|
|
81
|
+
* 4. context.json default team
|
|
82
|
+
* 5. Legacy DEFAULT_STORE_PATH
|
|
83
|
+
* @param {string} [teamId] - Explicit team ID override
|
|
84
|
+
* @returns {string|null}
|
|
85
|
+
*/
|
|
86
|
+
function resolveStorePath(teamId) {
|
|
87
|
+
if (process.env.TEAM_CONTEXT_STORE_PATH) return process.env.TEAM_CONTEXT_STORE_PATH;
|
|
88
|
+
const tid = teamId || _resolveTeamId();
|
|
89
|
+
if (tid && HOME) {
|
|
90
|
+
const teamStorePath = path.join(HOME, '.claude', 'team-context', 'teams', tid, 'content-store');
|
|
91
|
+
_autoMigrateToTeamStore(teamStorePath);
|
|
92
|
+
return teamStorePath;
|
|
93
|
+
}
|
|
94
|
+
return DEFAULT_STORE_PATH;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the signals directory path.
|
|
99
|
+
* Same priority chain as resolveStorePath.
|
|
100
|
+
* @param {string} [teamId] - Explicit team ID override
|
|
101
|
+
* @returns {string|null}
|
|
102
|
+
*/
|
|
103
|
+
function resolveSignalsDir(teamId) {
|
|
104
|
+
if (process.env.TEAM_CONTEXT_SIGNALS_DIR) return process.env.TEAM_CONTEXT_SIGNALS_DIR;
|
|
105
|
+
const tid = teamId || _resolveTeamId();
|
|
106
|
+
if (tid && HOME) {
|
|
107
|
+
return path.join(HOME, '.claude', 'team-context', 'teams', tid, 'signals');
|
|
108
|
+
}
|
|
109
|
+
return DEFAULT_SIGNALS_DIR;
|
|
110
|
+
}
|
|
111
|
+
|
|
20
112
|
// Field mapping for journal entries
|
|
21
113
|
const FIELD_MAP = {
|
|
22
114
|
'Why': 'why',
|
|
@@ -310,7 +402,7 @@ function cosineSimilarity(a, b) {
|
|
|
310
402
|
*/
|
|
311
403
|
async function indexJournals(options = {}) {
|
|
312
404
|
const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
|
|
313
|
-
const storePath = options.storePath ||
|
|
405
|
+
const storePath = options.storePath || resolveStorePath();
|
|
314
406
|
const doEmbeddings = options.embeddings !== undefined
|
|
315
407
|
? options.embeddings
|
|
316
408
|
: !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT || llm.isSimulation());
|
|
@@ -468,7 +560,7 @@ async function indexJournals(options = {}) {
|
|
|
468
560
|
* @returns {Promise<Array<{ id: string, score: number, entry: Object }>>}
|
|
469
561
|
*/
|
|
470
562
|
async function searchJournals(query, options = {}) {
|
|
471
|
-
const storePath = options.storePath ||
|
|
563
|
+
const storePath = options.storePath || resolveStorePath();
|
|
472
564
|
const limit = options.limit || 10;
|
|
473
565
|
|
|
474
566
|
const backend = getBackend(storePath);
|
|
@@ -522,7 +614,7 @@ async function searchJournals(query, options = {}) {
|
|
|
522
614
|
* @returns {Array<{ id: string, score: number, entry: Object }>}
|
|
523
615
|
*/
|
|
524
616
|
function searchText(query, options = {}) {
|
|
525
|
-
const storePath = options.storePath ||
|
|
617
|
+
const storePath = options.storePath || resolveStorePath();
|
|
526
618
|
const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
|
|
527
619
|
const limit = options.limit || 10;
|
|
528
620
|
|
|
@@ -571,7 +663,7 @@ function searchText(query, options = {}) {
|
|
|
571
663
|
|
|
572
664
|
// For signal entries, read content directly from the signal file
|
|
573
665
|
if (entry.source === 'signal') {
|
|
574
|
-
const signalsDir = options.signalsDir ||
|
|
666
|
+
const signalsDir = options.signalsDir || resolveSignalsDir();
|
|
575
667
|
if (signalsDir) {
|
|
576
668
|
// Signal files live at signalsDir/<channel>/<date>.md or signalsDir/<channel>/<owner>/<repo>/<date>.md
|
|
577
669
|
// The repo field tells us the path: "signals/<channel>" or "<owner>/<repo>"
|
|
@@ -677,7 +769,7 @@ function applyFilters(entry, filters) {
|
|
|
677
769
|
* @returns {Array<{ id: string, entry: Object }>}
|
|
678
770
|
*/
|
|
679
771
|
function queryMetadata(options = {}) {
|
|
680
|
-
const storePath = options.storePath ||
|
|
772
|
+
const storePath = options.storePath || resolveStorePath();
|
|
681
773
|
const index = getBackend(storePath).loadIndex();
|
|
682
774
|
if (!index) return [];
|
|
683
775
|
|
|
@@ -699,7 +791,7 @@ function queryMetadata(options = {}) {
|
|
|
699
791
|
* @returns {Object} - Insights object
|
|
700
792
|
*/
|
|
701
793
|
function extractInsights(options = {}) {
|
|
702
|
-
const storePath = options.storePath ||
|
|
794
|
+
const storePath = options.storePath || resolveStorePath();
|
|
703
795
|
const index = getBackend(storePath).loadIndex();
|
|
704
796
|
if (!index || index.entryCount === 0) {
|
|
705
797
|
return {
|
|
@@ -781,9 +873,9 @@ function extractInsights(options = {}) {
|
|
|
781
873
|
* @returns {string|null} - Full entry text, or null if not found
|
|
782
874
|
*/
|
|
783
875
|
function getEntryContent(entryId, options = {}) {
|
|
784
|
-
const storePath = options.storePath ||
|
|
876
|
+
const storePath = options.storePath || resolveStorePath();
|
|
785
877
|
const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
|
|
786
|
-
const signalsDir = options.signalsDir ||
|
|
878
|
+
const signalsDir = options.signalsDir || resolveSignalsDir();
|
|
787
879
|
|
|
788
880
|
const index = getBackend(storePath).loadIndex();
|
|
789
881
|
if (!index || !index.entries[entryId]) return null;
|
|
@@ -1258,7 +1350,7 @@ function fileFingerprint(filePath) {
|
|
|
1258
1350
|
*/
|
|
1259
1351
|
async function indexConversations(options = {}) {
|
|
1260
1352
|
const projectsDir = options.projectsDir || DEFAULT_PROJECTS_DIR;
|
|
1261
|
-
const storePath = options.storePath ||
|
|
1353
|
+
const storePath = options.storePath || resolveStorePath();
|
|
1262
1354
|
const doEmbeddings = options.embeddings !== undefined
|
|
1263
1355
|
? options.embeddings
|
|
1264
1356
|
: !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT || llm.isSimulation());
|
|
@@ -1505,7 +1597,7 @@ Rules:
|
|
|
1505
1597
|
* @returns {Promise<string>} - Synthesized onboarding document in markdown
|
|
1506
1598
|
*/
|
|
1507
1599
|
async function generateOnboardingPack(repoQuery, options = {}) {
|
|
1508
|
-
const storePath = options.storePath ||
|
|
1600
|
+
const storePath = options.storePath || resolveStorePath();
|
|
1509
1601
|
const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
|
|
1510
1602
|
const days = options.days || 90;
|
|
1511
1603
|
const llmConfig = options.llmConfig || {
|
|
@@ -1683,8 +1775,8 @@ async function indexConversationsWithExport(options = {}) {
|
|
|
1683
1775
|
* @returns {Promise<Object>} - Stats: { fileCount, newEntries, updatedEntries, skippedEntries }
|
|
1684
1776
|
*/
|
|
1685
1777
|
async function indexSignals(options = {}) {
|
|
1686
|
-
const signalsDir = options.signalsDir ||
|
|
1687
|
-
const storePath = options.storePath ||
|
|
1778
|
+
const signalsDir = options.signalsDir || resolveSignalsDir();
|
|
1779
|
+
const storePath = options.storePath || resolveStorePath();
|
|
1688
1780
|
const doEmbeddings = options.embeddings !== undefined
|
|
1689
1781
|
? options.embeddings
|
|
1690
1782
|
: !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT || llm.isSimulation());
|
|
@@ -1973,11 +2065,11 @@ async function indexSignals(options = {}) {
|
|
|
1973
2065
|
// ── Digest feedback ─────────────────────────────────────────────────────────
|
|
1974
2066
|
|
|
1975
2067
|
function loadFeedback(storePath) {
|
|
1976
|
-
return getBackend(storePath ||
|
|
2068
|
+
return getBackend(storePath || resolveStorePath()).loadFeedback();
|
|
1977
2069
|
}
|
|
1978
2070
|
|
|
1979
2071
|
function saveFeedback(storePath, feedback) {
|
|
1980
|
-
getBackend(storePath ||
|
|
2072
|
+
getBackend(storePath || resolveStorePath()).saveFeedback(feedback);
|
|
1981
2073
|
}
|
|
1982
2074
|
|
|
1983
2075
|
/**
|
|
@@ -2082,7 +2174,7 @@ function getDigestFeedback(options = {}) {
|
|
|
2082
2174
|
* focus: string[] }}
|
|
2083
2175
|
*/
|
|
2084
2176
|
function computeQualityProfile(options = {}) {
|
|
2085
|
-
const storePath = options.storePath ||
|
|
2177
|
+
const storePath = options.storePath || resolveStorePath();
|
|
2086
2178
|
const days = options.days || 30;
|
|
2087
2179
|
const index = getBackend(storePath).loadIndex();
|
|
2088
2180
|
|
|
@@ -2217,12 +2309,12 @@ module.exports = {
|
|
|
2217
2309
|
getBackend,
|
|
2218
2310
|
getBackendType,
|
|
2219
2311
|
// Backward-compatible storage wrappers (delegate to backend)
|
|
2220
|
-
loadIndex: (storePath) => getBackend(storePath ||
|
|
2221
|
-
saveIndex: (storePath, index) => getBackend(storePath ||
|
|
2222
|
-
loadEmbeddings: (storePath) => getBackend(storePath ||
|
|
2223
|
-
saveEmbeddings: (storePath, embeddings) => getBackend(storePath ||
|
|
2224
|
-
loadConversationIndex: (storePath) => getBackend(storePath ||
|
|
2225
|
-
saveConversationIndex: (storePath, convIndex) => getBackend(storePath ||
|
|
2312
|
+
loadIndex: (storePath) => getBackend(storePath || resolveStorePath()).loadIndex(),
|
|
2313
|
+
saveIndex: (storePath, index) => getBackend(storePath || resolveStorePath()).saveIndex(index),
|
|
2314
|
+
loadEmbeddings: (storePath) => getBackend(storePath || resolveStorePath()).loadEmbeddings(),
|
|
2315
|
+
saveEmbeddings: (storePath, embeddings) => getBackend(storePath || resolveStorePath()).saveEmbeddings(embeddings),
|
|
2316
|
+
loadConversationIndex: (storePath) => getBackend(storePath || resolveStorePath()).loadConversationIndex(),
|
|
2317
|
+
saveConversationIndex: (storePath, convIndex) => getBackend(storePath || resolveStorePath()).saveConversationIndex(convIndex),
|
|
2226
2318
|
|
|
2227
2319
|
// Filtering
|
|
2228
2320
|
isRepoExcluded,
|
|
@@ -2256,6 +2348,8 @@ module.exports = {
|
|
|
2256
2348
|
DEFAULT_JOURNAL_DIR,
|
|
2257
2349
|
DEFAULT_PROJECTS_DIR,
|
|
2258
2350
|
DEFAULT_SIGNALS_DIR,
|
|
2351
|
+
resolveStorePath,
|
|
2352
|
+
resolveSignalsDir,
|
|
2259
2353
|
|
|
2260
2354
|
// Digest feedback
|
|
2261
2355
|
loadFeedback,
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wayfind MCP Server — stdio transport.
|
|
6
|
+
*
|
|
7
|
+
* Exposes team context (journals, signals, decisions) as MCP tools so any
|
|
8
|
+
* MCP-compatible AI tool can query the same knowledge base that Wayfind builds.
|
|
9
|
+
* Team-scoped from day one: resolveStorePath() picks the right store for the
|
|
10
|
+
* active team based on the working directory.
|
|
11
|
+
*
|
|
12
|
+
* Usage (Claude Code):
|
|
13
|
+
* Add to ~/.claude/claude_desktop_config.json:
|
|
14
|
+
* {
|
|
15
|
+
* "mcpServers": {
|
|
16
|
+
* "wayfind": { "command": "wayfind-mcp" }
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Or run directly: wayfind-mcp
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
24
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
25
|
+
const {
|
|
26
|
+
CallToolRequestSchema,
|
|
27
|
+
ListToolsRequestSchema,
|
|
28
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const contentStore = require('./content-store.js');
|
|
32
|
+
|
|
33
|
+
const pkg = require('../package.json');
|
|
34
|
+
|
|
35
|
+
// ── Tool definitions ─────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const TOOLS = [
|
|
38
|
+
{
|
|
39
|
+
name: 'search_context',
|
|
40
|
+
description: 'Search team context by natural language or keyword. Returns ranked journal entries, decisions, and signals. Use this to answer questions about past work, architectural decisions, and team activity.',
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
45
|
+
limit: { type: 'number', description: 'Max results (default: 10)' },
|
|
46
|
+
repo: { type: 'string', description: 'Filter by repository name (e.g. "MyService", "MyOrg/my-repo")' },
|
|
47
|
+
since: { type: 'string', description: 'Filter to entries on or after this date (YYYY-MM-DD)' },
|
|
48
|
+
mode: { type: 'string', enum: ['semantic', 'text'], description: 'Search mode — semantic uses embeddings, text uses keyword matching. Defaults to semantic if embeddings available.' },
|
|
49
|
+
},
|
|
50
|
+
required: ['query'],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'get_entry',
|
|
55
|
+
description: 'Retrieve the full content of a specific journal or signal entry by ID. Use the IDs returned by search_context or list_recent.',
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: 'string', description: 'Entry ID from search_context or list_recent results' },
|
|
60
|
+
},
|
|
61
|
+
required: ['id'],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'list_recent',
|
|
66
|
+
description: 'List recent journal entries and decisions, optionally filtered by repo or date range. Returns metadata (no full content — use get_entry for that).',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
limit: { type: 'number', description: 'Max entries to return (default: 20)' },
|
|
71
|
+
repo: { type: 'string', description: 'Filter by repository name' },
|
|
72
|
+
since: { type: 'string', description: 'Filter to entries on or after this date (YYYY-MM-DD)' },
|
|
73
|
+
source: { type: 'string', enum: ['journal', 'conversation', 'signal'], description: 'Filter by entry source type' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'get_signals',
|
|
79
|
+
description: 'Retrieve recent signal entries (GitHub activity, Slack summaries, Intercom updates, Notion pages) for a specific channel or all channels.',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
channel: { type: 'string', description: 'Signal channel name (e.g. "github", "slack", "intercom"). Omit for all channels.' },
|
|
84
|
+
since: { type: 'string', description: 'Filter to signals on or after this date (YYYY-MM-DD). Defaults to last 7 days.' },
|
|
85
|
+
limit: { type: 'number', description: 'Max signals to return (default: 20)' },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'get_team_status',
|
|
91
|
+
description: 'Get the current team state: who is working on what, active projects, recent decisions, and open blockers. Reads from team-state.md and personal-state.md files.',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
include_personal: { type: 'boolean', description: 'Include personal-state.md in addition to team-state.md (default: true)' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'get_personas',
|
|
101
|
+
description: 'List the configured Wayfind personas (e.g. Greg/engineering, Sean/strategy). Each persona gets a tailored digest. Useful for understanding who uses this team context and how.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'record_feedback',
|
|
109
|
+
description: 'Record that a context result was helpful or not. This improves future retrieval quality by down-weighting unhelpful entries.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
entry_id: { type: 'string', description: 'ID of the entry being rated' },
|
|
114
|
+
helpful: { type: 'boolean', description: 'Was this entry useful for the task?' },
|
|
115
|
+
query: { type: 'string', description: 'The original query that surfaced this entry (for context)' },
|
|
116
|
+
},
|
|
117
|
+
required: ['entry_id', 'helpful'],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'add_context',
|
|
122
|
+
description: 'Add a new context entry to the team knowledge base. Use this to capture decisions, blockers, or key context from an AI session that should be available to future sessions.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
title: { type: 'string', description: 'Short title for the entry (< 80 chars)' },
|
|
127
|
+
content: { type: 'string', description: 'Full content in markdown. Should include Why, What, and any key decisions.' },
|
|
128
|
+
repo: { type: 'string', description: 'Repository this entry belongs to (e.g. "MyOrg/my-repo")' },
|
|
129
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for categorization' },
|
|
130
|
+
},
|
|
131
|
+
required: ['title', 'content'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// ── Tool handlers ────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
async function handleSearchContext(args) {
|
|
139
|
+
const { query, limit = 10, repo, since, mode } = args;
|
|
140
|
+
const opts = { limit, repo, since };
|
|
141
|
+
|
|
142
|
+
let results;
|
|
143
|
+
if (mode === 'text') {
|
|
144
|
+
results = contentStore.searchText(query, opts);
|
|
145
|
+
} else {
|
|
146
|
+
results = await contentStore.searchJournals(query, opts);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!results || results.length === 0) {
|
|
150
|
+
return { found: 0, results: [], hint: 'No matches. Try a broader query or check wayfind reindex.' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
found: results.length,
|
|
155
|
+
results: results.map(r => ({
|
|
156
|
+
id: r.id,
|
|
157
|
+
score: r.score ? Math.round(r.score * 1000) / 1000 : null,
|
|
158
|
+
date: r.entry.date,
|
|
159
|
+
repo: r.entry.repo,
|
|
160
|
+
title: r.entry.title,
|
|
161
|
+
source: r.entry.source,
|
|
162
|
+
tags: r.entry.tags || [],
|
|
163
|
+
summary: r.entry.summary || null,
|
|
164
|
+
})),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleGetEntry(args) {
|
|
169
|
+
const { id } = args;
|
|
170
|
+
const storePath = contentStore.resolveStorePath();
|
|
171
|
+
const journalDir = contentStore.DEFAULT_JOURNAL_DIR;
|
|
172
|
+
|
|
173
|
+
// Get entry metadata
|
|
174
|
+
const index = contentStore.getBackend(storePath).loadIndex();
|
|
175
|
+
if (!index || !index.entries || !index.entries[id]) {
|
|
176
|
+
return { error: `Entry not found: ${id}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entry = index.entries[id];
|
|
180
|
+
const fullContent = contentStore.getEntryContent(id, { storePath, journalDir });
|
|
181
|
+
|
|
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
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleListRecent(args) {
|
|
194
|
+
const { limit = 20, repo, since, source } = args;
|
|
195
|
+
const opts = { limit, repo, since, source };
|
|
196
|
+
const results = contentStore.queryMetadata(opts);
|
|
197
|
+
const top = results.slice(0, limit);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
total: results.length,
|
|
201
|
+
showing: top.length,
|
|
202
|
+
entries: top.map(r => ({
|
|
203
|
+
id: r.id,
|
|
204
|
+
date: r.entry.date,
|
|
205
|
+
repo: r.entry.repo,
|
|
206
|
+
title: r.entry.title,
|
|
207
|
+
source: r.entry.source,
|
|
208
|
+
tags: r.entry.tags || [],
|
|
209
|
+
})),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function handleGetSignals(args) {
|
|
214
|
+
const { channel, limit = 20 } = args;
|
|
215
|
+
const signalsDir = contentStore.resolveSignalsDir();
|
|
216
|
+
const sinceDate = args.since || (() => {
|
|
217
|
+
const d = new Date();
|
|
218
|
+
d.setDate(d.getDate() - 7);
|
|
219
|
+
return d.toISOString().slice(0, 10);
|
|
220
|
+
})();
|
|
221
|
+
|
|
222
|
+
if (!signalsDir || !fs.existsSync(signalsDir)) {
|
|
223
|
+
return { error: 'No signals directory found. Run "wayfind pull --all" first.', signals: [] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const channels = channel
|
|
227
|
+
? [channel]
|
|
228
|
+
: fs.readdirSync(signalsDir).filter(d => {
|
|
229
|
+
try { return fs.statSync(path.join(signalsDir, d)).isDirectory(); } catch { return false; }
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const signals = [];
|
|
233
|
+
for (const ch of channels) {
|
|
234
|
+
const chDir = path.join(signalsDir, ch);
|
|
235
|
+
if (!fs.existsSync(chDir)) continue;
|
|
236
|
+
|
|
237
|
+
const files = fs.readdirSync(chDir)
|
|
238
|
+
.filter(f => f.endsWith('.md') && f >= sinceDate)
|
|
239
|
+
.sort()
|
|
240
|
+
.reverse()
|
|
241
|
+
.slice(0, limit);
|
|
242
|
+
|
|
243
|
+
for (const f of files) {
|
|
244
|
+
const filePath = path.join(chDir, f);
|
|
245
|
+
try {
|
|
246
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
247
|
+
signals.push({
|
|
248
|
+
channel: ch,
|
|
249
|
+
date: f.slice(0, 10),
|
|
250
|
+
file: f,
|
|
251
|
+
content: content.slice(0, 2000) + (content.length > 2000 ? '\n...(truncated)' : ''),
|
|
252
|
+
});
|
|
253
|
+
} catch (_) {}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
signals.sort((a, b) => b.date.localeCompare(a.date));
|
|
258
|
+
return { signals: signals.slice(0, limit) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function handleGetTeamStatus(args) {
|
|
262
|
+
const { include_personal = true } = args;
|
|
263
|
+
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
264
|
+
const wayfindDir = process.env.WAYFIND_DIR || (HOME ? path.join(HOME, '.claude', 'team-context') : null);
|
|
265
|
+
|
|
266
|
+
const result = {};
|
|
267
|
+
|
|
268
|
+
// Try to read team-state.md from cwd's .claude/ directory
|
|
269
|
+
const cwdTeamState = path.join(process.cwd(), '.claude', 'team-state.md');
|
|
270
|
+
const cwdPersonalState = path.join(process.cwd(), '.claude', 'personal-state.md');
|
|
271
|
+
|
|
272
|
+
for (const [key, filePath] of [['team_state', cwdTeamState], ['personal_state', cwdPersonalState]]) {
|
|
273
|
+
if (key === 'personal_state' && !include_personal) continue;
|
|
274
|
+
try {
|
|
275
|
+
if (fs.existsSync(filePath)) {
|
|
276
|
+
result[key] = fs.readFileSync(filePath, 'utf8');
|
|
277
|
+
}
|
|
278
|
+
} catch (_) {}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (Object.keys(result).length === 0) {
|
|
282
|
+
return { error: 'No state files found in .claude/. Make sure Wayfind is initialized in this repo.' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function handleGetPersonas() {
|
|
289
|
+
const HOME = process.env.HOME || process.env.USERPROFILE;
|
|
290
|
+
const connectorsFile = HOME ? path.join(HOME, '.claude', 'team-context', 'connectors.json') : null;
|
|
291
|
+
|
|
292
|
+
if (!connectorsFile || !fs.existsSync(connectorsFile)) {
|
|
293
|
+
return { error: 'connectors.json not found. Run "wayfind context init" first.', personas: [] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const config = JSON.parse(fs.readFileSync(connectorsFile, 'utf8'));
|
|
298
|
+
const personas = config.personas || config.digest?.personas || [];
|
|
299
|
+
return { personas };
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return { error: `Failed to read connectors.json: ${e.message}`, personas: [] };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function handleRecordFeedback(args) {
|
|
306
|
+
const { entry_id, helpful, query } = args;
|
|
307
|
+
const storePath = contentStore.resolveStorePath();
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const feedback = contentStore.loadFeedback(storePath);
|
|
311
|
+
const existing = feedback.entries || {};
|
|
312
|
+
existing[entry_id] = existing[entry_id] || { helpful: 0, unhelpful: 0, queries: [] };
|
|
313
|
+
|
|
314
|
+
if (helpful) {
|
|
315
|
+
existing[entry_id].helpful = (existing[entry_id].helpful || 0) + 1;
|
|
316
|
+
} else {
|
|
317
|
+
existing[entry_id].unhelpful = (existing[entry_id].unhelpful || 0) + 1;
|
|
318
|
+
}
|
|
319
|
+
if (query) {
|
|
320
|
+
existing[entry_id].queries = [...(existing[entry_id].queries || []).slice(-4), query];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
contentStore.saveFeedback(storePath, { ...feedback, entries: existing });
|
|
324
|
+
return { recorded: true, entry_id, helpful };
|
|
325
|
+
} catch (e) {
|
|
326
|
+
return { error: `Failed to record feedback: ${e.message}` };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function handleAddContext(args) {
|
|
331
|
+
const { title, content, repo, tags = [] } = args;
|
|
332
|
+
const journalDir = contentStore.DEFAULT_JOURNAL_DIR;
|
|
333
|
+
|
|
334
|
+
if (!journalDir) {
|
|
335
|
+
return { error: 'Cannot resolve journal directory.' };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
340
|
+
const sanitizedTitle = title.replace(/[^a-zA-Z0-9\s\-_]/g, '').slice(0, 60);
|
|
341
|
+
const repoLine = repo ? `\n**Repo:** ${repo}` : '';
|
|
342
|
+
const tagsLine = tags.length ? `\n**Tags:** ${tags.join(', ')}` : '';
|
|
343
|
+
|
|
344
|
+
const entry = [
|
|
345
|
+
`## ${repo || 'general'} — ${sanitizedTitle}`,
|
|
346
|
+
`**Why:** Added via Wayfind MCP`,
|
|
347
|
+
`**What:** ${content}`,
|
|
348
|
+
repoLine,
|
|
349
|
+
tagsLine,
|
|
350
|
+
].filter(Boolean).join('\n');
|
|
351
|
+
|
|
352
|
+
const filePath = path.join(journalDir, `${today}.md`);
|
|
353
|
+
fs.mkdirSync(journalDir, { recursive: true });
|
|
354
|
+
|
|
355
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
356
|
+
fs.writeFileSync(filePath, existing + (existing ? '\n\n' : '') + entry + '\n');
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
added: true,
|
|
360
|
+
date: today,
|
|
361
|
+
title: sanitizedTitle,
|
|
362
|
+
file: filePath,
|
|
363
|
+
hint: 'Run "wayfind index-journals" to make this entry searchable.',
|
|
364
|
+
};
|
|
365
|
+
} catch (e) {
|
|
366
|
+
return { error: `Failed to add context: ${e.message}` };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Server setup ─────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
const server = new Server(
|
|
373
|
+
{ name: 'wayfind', version: pkg.version },
|
|
374
|
+
{ capabilities: { tools: {} } }
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
378
|
+
|
|
379
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
380
|
+
const { name, arguments: args = {} } = request.params;
|
|
381
|
+
|
|
382
|
+
let result;
|
|
383
|
+
switch (name) {
|
|
384
|
+
case 'search_context': result = await handleSearchContext(args); break;
|
|
385
|
+
case 'get_entry': result = handleGetEntry(args); break;
|
|
386
|
+
case 'list_recent': result = handleListRecent(args); break;
|
|
387
|
+
case 'get_signals': result = handleGetSignals(args); break;
|
|
388
|
+
case 'get_team_status': result = handleGetTeamStatus(args); break;
|
|
389
|
+
case 'get_personas': result = handleGetPersonas(); break;
|
|
390
|
+
case 'record_feedback': result = handleRecordFeedback(args); break;
|
|
391
|
+
case 'add_context': result = handleAddContext(args); break;
|
|
392
|
+
default:
|
|
393
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }], isError: true };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
async function main() {
|
|
402
|
+
const transport = new StdioServerTransport();
|
|
403
|
+
await server.connect(transport);
|
|
404
|
+
// stderr so it doesn't pollute the MCP stdio channel
|
|
405
|
+
process.stderr.write(`Wayfind MCP server v${pkg.version} ready\n`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
main().catch((err) => {
|
|
409
|
+
process.stderr.write(`Fatal: ${err.message}\n`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
});
|
package/bin/team-context.js
CHANGED
|
@@ -1109,7 +1109,7 @@ function parseCSArgs(args) {
|
|
|
1109
1109
|
async function runIndexJournals(args) {
|
|
1110
1110
|
const { opts } = parseCSArgs(args);
|
|
1111
1111
|
const journalDir = opts.dir || contentStore.DEFAULT_JOURNAL_DIR;
|
|
1112
|
-
const storePath = opts.store || contentStore.
|
|
1112
|
+
const storePath = opts.store || contentStore.resolveStorePath();
|
|
1113
1113
|
|
|
1114
1114
|
console.log(`Indexing journals from: ${journalDir}`);
|
|
1115
1115
|
console.log(`Store: ${storePath}`);
|
|
@@ -1137,7 +1137,7 @@ async function runIndexJournals(args) {
|
|
|
1137
1137
|
async function runIndexConversations(args) {
|
|
1138
1138
|
const { opts } = parseCSArgs(args);
|
|
1139
1139
|
const projectsDir = opts.dir || contentStore.DEFAULT_PROJECTS_DIR;
|
|
1140
|
-
const storePath = opts.store || contentStore.
|
|
1140
|
+
const storePath = opts.store || contentStore.resolveStorePath();
|
|
1141
1141
|
|
|
1142
1142
|
console.log(`Indexing conversations from: ${projectsDir}`);
|
|
1143
1143
|
console.log(`Store: ${storePath}`);
|
|
@@ -1322,7 +1322,7 @@ function buildRepoToTeamResolver() {
|
|
|
1322
1322
|
async function runIndexConversationsWithExport(args, detectShifts = false) {
|
|
1323
1323
|
const { opts } = parseCSArgs(args);
|
|
1324
1324
|
const projectsDir = opts.dir || contentStore.DEFAULT_PROJECTS_DIR;
|
|
1325
|
-
const storePath = opts.store || contentStore.
|
|
1325
|
+
const storePath = opts.store || contentStore.resolveStorePath();
|
|
1326
1326
|
const journalDir = opts.exportDir || contentStore.DEFAULT_JOURNAL_DIR;
|
|
1327
1327
|
|
|
1328
1328
|
console.log(`Indexing conversations from: ${projectsDir}`);
|
|
@@ -2973,7 +2973,7 @@ async function runBot(args) {
|
|
|
2973
2973
|
|
|
2974
2974
|
// Check content store has entries (warn if empty)
|
|
2975
2975
|
const index = contentStore.loadIndex(
|
|
2976
|
-
config.slack_bot.store_path || contentStore.
|
|
2976
|
+
config.slack_bot.store_path || contentStore.resolveStorePath()
|
|
2977
2977
|
);
|
|
2978
2978
|
if (!index || index.entryCount === 0) {
|
|
2979
2979
|
console.log('Warning: Content store is empty. Run "wayfind index-journals" first for best results.');
|
|
@@ -3753,21 +3753,169 @@ function runPrompts(args) {
|
|
|
3753
3753
|
const DEPLOY_TEMPLATES_DIR = path.join(ROOT, 'templates', 'deploy');
|
|
3754
3754
|
|
|
3755
3755
|
async function runDeploy(args) {
|
|
3756
|
-
|
|
3756
|
+
// Parse --team <teamId> flag
|
|
3757
|
+
const teamIdx = args.indexOf('--team');
|
|
3758
|
+
const teamId = teamIdx !== -1 ? args[teamIdx + 1] : null;
|
|
3759
|
+
const portIdx = args.indexOf('--port');
|
|
3760
|
+
const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : null;
|
|
3761
|
+
|
|
3762
|
+
const filteredArgs = args.filter((a, i) => {
|
|
3763
|
+
if (a === '--team' || a === '--port') return false;
|
|
3764
|
+
if (i > 0 && (args[i - 1] === '--team' || args[i - 1] === '--port')) return false;
|
|
3765
|
+
return true;
|
|
3766
|
+
});
|
|
3767
|
+
const sub = filteredArgs[0] || 'init';
|
|
3768
|
+
|
|
3757
3769
|
switch (sub) {
|
|
3758
3770
|
case 'init':
|
|
3759
|
-
|
|
3771
|
+
if (teamId) {
|
|
3772
|
+
deployTeamInit(teamId, { port });
|
|
3773
|
+
} else {
|
|
3774
|
+
deployInit();
|
|
3775
|
+
}
|
|
3776
|
+
break;
|
|
3777
|
+
case 'list':
|
|
3778
|
+
deployList();
|
|
3760
3779
|
break;
|
|
3761
3780
|
case 'status':
|
|
3762
3781
|
deployStatus();
|
|
3763
3782
|
break;
|
|
3764
3783
|
default:
|
|
3765
3784
|
console.error(`Unknown deploy subcommand: ${sub}`);
|
|
3766
|
-
console.error('Available: init, status');
|
|
3785
|
+
console.error('Available: init [--team <id>], list, status');
|
|
3767
3786
|
process.exit(1);
|
|
3768
3787
|
}
|
|
3769
3788
|
}
|
|
3770
3789
|
|
|
3790
|
+
/**
|
|
3791
|
+
* Scaffold a per-team container config at ~/.claude/team-context/teams/<teamId>/deploy/
|
|
3792
|
+
*/
|
|
3793
|
+
function deployTeamInit(teamId, { port } = {}) {
|
|
3794
|
+
const teamsBaseDir = HOME ? path.join(HOME, '.claude', 'team-context', 'teams') : null;
|
|
3795
|
+
if (!teamsBaseDir) {
|
|
3796
|
+
console.error('Cannot resolve home directory.');
|
|
3797
|
+
process.exit(1);
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
const deployDir = path.join(teamsBaseDir, teamId, 'deploy');
|
|
3801
|
+
const storeDir = path.join(teamsBaseDir, teamId, 'content-store');
|
|
3802
|
+
|
|
3803
|
+
// Check for duplicate running container
|
|
3804
|
+
const psResult = spawnSync('docker', ['ps', '--filter', `label=com.wayfind.team=${teamId}`, '--format', '{{.Names}}'], { stdio: 'pipe' });
|
|
3805
|
+
const running = (psResult.stdout || '').toString().trim();
|
|
3806
|
+
if (running) {
|
|
3807
|
+
console.log(`Warning: container already running for team "${teamId}": ${running}`);
|
|
3808
|
+
console.log('Use "docker compose down" in the deploy directory before re-initializing.');
|
|
3809
|
+
process.exit(1);
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
fs.mkdirSync(deployDir, { recursive: true });
|
|
3813
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
3814
|
+
console.log(`Scaffolding deploy config for team: ${teamId}`);
|
|
3815
|
+
console.log(`Deploy dir: ${deployDir}`);
|
|
3816
|
+
|
|
3817
|
+
// Resolve team-context repo path for volume mount
|
|
3818
|
+
const config = readContextConfig();
|
|
3819
|
+
const teamEntry = config.teams && config.teams[teamId];
|
|
3820
|
+
const teamContextPath = teamEntry ? teamEntry.path : null;
|
|
3821
|
+
|
|
3822
|
+
// Assign port (default 3141; if taken, user should pass --port)
|
|
3823
|
+
const assignedPort = port || 3141;
|
|
3824
|
+
const containerName = `wayfind-${teamId}`;
|
|
3825
|
+
|
|
3826
|
+
// Build docker-compose.yml content with per-team overrides
|
|
3827
|
+
const templatePath = path.join(DEPLOY_TEMPLATES_DIR, 'docker-compose.yml');
|
|
3828
|
+
let composeContent = fs.readFileSync(templatePath, 'utf8');
|
|
3829
|
+
composeContent = composeContent
|
|
3830
|
+
.replace(/container_name: wayfind/, `container_name: ${containerName}`)
|
|
3831
|
+
.replace(/- "3141:3141"/, `- "${assignedPort}:3141"`)
|
|
3832
|
+
.replace(/(TEAM_CONTEXT_TENANT_ID:.*$)/m, `TEAM_CONTEXT_TENANT_ID: \${TEAM_CONTEXT_TENANT_ID:-${teamId}}`);
|
|
3833
|
+
|
|
3834
|
+
// Inject Docker label for discovery
|
|
3835
|
+
composeContent = composeContent.replace(
|
|
3836
|
+
/restart: unless-stopped/,
|
|
3837
|
+
`restart: unless-stopped\n labels:\n com.wayfind.team: "${teamId}"`
|
|
3838
|
+
);
|
|
3839
|
+
|
|
3840
|
+
const composePath = path.join(deployDir, 'docker-compose.yml');
|
|
3841
|
+
if (!fs.existsSync(composePath)) {
|
|
3842
|
+
fs.writeFileSync(composePath, composeContent, 'utf8');
|
|
3843
|
+
console.log(' docker-compose.yml — created');
|
|
3844
|
+
} else {
|
|
3845
|
+
console.log(' docker-compose.yml — already exists, skipping');
|
|
3846
|
+
}
|
|
3847
|
+
|
|
3848
|
+
// .env.example
|
|
3849
|
+
const envExampleSrc = path.join(DEPLOY_TEMPLATES_DIR, '.env.example');
|
|
3850
|
+
const envExampleDst = path.join(deployDir, '.env.example');
|
|
3851
|
+
if (!fs.existsSync(envExampleDst) && fs.existsSync(envExampleSrc)) {
|
|
3852
|
+
let envContent = fs.readFileSync(envExampleSrc, 'utf8');
|
|
3853
|
+
if (teamContextPath) {
|
|
3854
|
+
envContent += `\nTEAM_CONTEXT_TEAM_CONTEXT_PATH=${teamContextPath}\n`;
|
|
3855
|
+
}
|
|
3856
|
+
fs.writeFileSync(envExampleDst, envContent, 'utf8');
|
|
3857
|
+
console.log(' .env.example — created');
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
// .env from .env.example
|
|
3861
|
+
const envPath = path.join(deployDir, '.env');
|
|
3862
|
+
if (!fs.existsSync(envPath) && fs.existsSync(envExampleDst)) {
|
|
3863
|
+
fs.copyFileSync(envExampleDst, envPath);
|
|
3864
|
+
console.log(' .env — created from .env.example (fill in your tokens)');
|
|
3865
|
+
}
|
|
3866
|
+
|
|
3867
|
+
const ghToken = detectGitHubToken();
|
|
3868
|
+
if (ghToken && fs.existsSync(envPath)) {
|
|
3869
|
+
let envContent = fs.readFileSync(envPath, 'utf8');
|
|
3870
|
+
if (!envContent.match(/^GITHUB_TOKEN=.+/m)) {
|
|
3871
|
+
envContent = envContent.replace(/^GITHUB_TOKEN=.*$/m, `GITHUB_TOKEN=${ghToken}`);
|
|
3872
|
+
if (!envContent.includes('GITHUB_TOKEN=')) envContent += `\nGITHUB_TOKEN=${ghToken}\n`;
|
|
3873
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
3874
|
+
console.log(' GITHUB_TOKEN — auto-detected from gh CLI');
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
console.log('');
|
|
3879
|
+
console.log('Next steps:');
|
|
3880
|
+
console.log(` 1. Fill in ${deployDir}/.env with your tokens`);
|
|
3881
|
+
console.log(` 2. cd "${deployDir}" && docker compose up -d`);
|
|
3882
|
+
console.log(` 3. Verify: curl http://localhost:${assignedPort}/healthz`);
|
|
3883
|
+
console.log('');
|
|
3884
|
+
console.log(`Tip: run "wayfind deploy list" to see all running team containers.`);
|
|
3885
|
+
|
|
3886
|
+
telemetry.capture('deploy_team_init', { teamId }, CLI_USER);
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
/**
|
|
3890
|
+
* List all running Wayfind team containers.
|
|
3891
|
+
*/
|
|
3892
|
+
function deployList() {
|
|
3893
|
+
const psResult = spawnSync('docker', [
|
|
3894
|
+
'ps',
|
|
3895
|
+
'--filter', 'label=com.wayfind.team',
|
|
3896
|
+
'--format', '{{.Names}}\t{{.Status}}\t{{.Label "com.wayfind.team"}}',
|
|
3897
|
+
], { stdio: 'pipe' });
|
|
3898
|
+
|
|
3899
|
+
if (psResult.error) {
|
|
3900
|
+
console.log('Docker not available.');
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
|
|
3904
|
+
const rows = (psResult.stdout || '').toString().trim();
|
|
3905
|
+
if (!rows) {
|
|
3906
|
+
console.log('No Wayfind team containers running.');
|
|
3907
|
+
console.log('Start one with: wayfind deploy --team <teamId> && cd ~/.claude/team-context/teams/<teamId>/deploy && docker compose up -d');
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
console.log('Running Wayfind team containers:');
|
|
3912
|
+
console.log('');
|
|
3913
|
+
for (const row of rows.split('\n')) {
|
|
3914
|
+
const [name, status, team] = row.split('\t');
|
|
3915
|
+
console.log(` ${name} (team: ${team || 'unknown'}) — ${status}`);
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3771
3919
|
function deployInit() {
|
|
3772
3920
|
const deployDir = path.join(process.cwd(), 'deploy');
|
|
3773
3921
|
|
|
@@ -3962,7 +4110,7 @@ function startHealthServer() {
|
|
|
3962
4110
|
const server = http.createServer((req, res) => {
|
|
3963
4111
|
if (req.url === '/healthz' && req.method === 'GET') {
|
|
3964
4112
|
// Enrich with index freshness
|
|
3965
|
-
const storePath =
|
|
4113
|
+
const storePath = contentStore.resolveStorePath();
|
|
3966
4114
|
const index = contentStore.loadIndex(storePath);
|
|
3967
4115
|
const indexInfo = index ? {
|
|
3968
4116
|
entryCount: index.entryCount || 0,
|
|
@@ -4240,7 +4388,7 @@ async function indexConversationsIfAvailable() {
|
|
|
4240
4388
|
}
|
|
4241
4389
|
|
|
4242
4390
|
async function indexSignalsIfAvailable() {
|
|
4243
|
-
const signalsDir =
|
|
4391
|
+
const signalsDir = contentStore.resolveSignalsDir();
|
|
4244
4392
|
if (!signalsDir || !fs.existsSync(signalsDir)) {
|
|
4245
4393
|
console.log(`No signals at ${signalsDir} — skipping index.`);
|
|
4246
4394
|
return;
|
|
@@ -4310,9 +4458,9 @@ function ensureContainerConfig() {
|
|
|
4310
4458
|
api_key_env: 'ANTHROPIC_API_KEY',
|
|
4311
4459
|
},
|
|
4312
4460
|
lookback_days: 7,
|
|
4313
|
-
store_path:
|
|
4461
|
+
store_path: contentStore.resolveStorePath(),
|
|
4314
4462
|
journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
|
|
4315
|
-
signals_dir:
|
|
4463
|
+
signals_dir: contentStore.resolveSignalsDir(),
|
|
4316
4464
|
team_context_dir: process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR || '',
|
|
4317
4465
|
slack: {
|
|
4318
4466
|
webhook_url: process.env.TEAM_CONTEXT_SLACK_WEBHOOK || '',
|
|
@@ -4326,9 +4474,9 @@ function ensureContainerConfig() {
|
|
|
4326
4474
|
// may have host paths that don't exist inside the container
|
|
4327
4475
|
if (config.digest) {
|
|
4328
4476
|
const containerPaths = {
|
|
4329
|
-
store_path:
|
|
4477
|
+
store_path: contentStore.resolveStorePath(),
|
|
4330
4478
|
journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
|
|
4331
|
-
signals_dir:
|
|
4479
|
+
signals_dir: contentStore.resolveSignalsDir(),
|
|
4332
4480
|
team_context_dir: process.env.TEAM_CONTEXT_TEAM_CONTEXT_DIR || '',
|
|
4333
4481
|
};
|
|
4334
4482
|
for (const [key, val] of Object.entries(containerPaths)) {
|
|
@@ -4345,7 +4493,7 @@ function ensureContainerConfig() {
|
|
|
4345
4493
|
bot_token_env: 'SLACK_BOT_TOKEN',
|
|
4346
4494
|
app_token_env: 'SLACK_APP_TOKEN',
|
|
4347
4495
|
mode: process.env.TEAM_CONTEXT_BOT_MODE || 'local',
|
|
4348
|
-
store_path:
|
|
4496
|
+
store_path: contentStore.resolveStorePath(),
|
|
4349
4497
|
journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
|
|
4350
4498
|
llm: {
|
|
4351
4499
|
provider: 'anthropic',
|
|
@@ -4359,7 +4507,7 @@ function ensureContainerConfig() {
|
|
|
4359
4507
|
// Override container-specific paths in bot config (same reason as digest above)
|
|
4360
4508
|
if (config.slack_bot) {
|
|
4361
4509
|
const botPaths = {
|
|
4362
|
-
store_path:
|
|
4510
|
+
store_path: contentStore.resolveStorePath(),
|
|
4363
4511
|
journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
|
|
4364
4512
|
};
|
|
4365
4513
|
for (const [key, val] of Object.entries(botPaths)) {
|
|
@@ -4438,7 +4586,7 @@ function buildBotConfigFromEnv() {
|
|
|
4438
4586
|
bot_token_env: 'SLACK_BOT_TOKEN',
|
|
4439
4587
|
app_token_env: 'SLACK_APP_TOKEN',
|
|
4440
4588
|
mode: process.env.TEAM_CONTEXT_BOT_MODE || 'local',
|
|
4441
|
-
store_path:
|
|
4589
|
+
store_path: contentStore.resolveStorePath(),
|
|
4442
4590
|
journal_dir: process.env.TEAM_CONTEXT_JOURNALS_DIR || '/data/journals',
|
|
4443
4591
|
llm: {
|
|
4444
4592
|
provider: 'anthropic',
|
|
@@ -4929,64 +5077,92 @@ const COMMANDS = {
|
|
|
4929
5077
|
process.exit(result.status);
|
|
4930
5078
|
}
|
|
4931
5079
|
|
|
4932
|
-
// Step 3: Update Wayfind
|
|
5080
|
+
// Step 3: Update all Wayfind containers (per-team labeled + legacy 'wayfind')
|
|
4933
5081
|
const dockerCheck = spawnSync('docker', ['compose', 'version'], { stdio: 'pipe' });
|
|
4934
5082
|
if (!dockerCheck.error && dockerCheck.status === 0) {
|
|
4935
|
-
|
|
4936
|
-
const
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
].
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
5083
|
+
// Discover containers: prefer com.wayfind.team label, fall back to legacy 'wayfind' name
|
|
5084
|
+
const labeledResult = spawnSync('docker', [
|
|
5085
|
+
'ps',
|
|
5086
|
+
'--filter', 'label=com.wayfind.team',
|
|
5087
|
+
'--format', '{{.Names}}\t{{index .Labels "com.wayfind.team"}}',
|
|
5088
|
+
], { stdio: 'pipe' });
|
|
5089
|
+
const legacyResult = spawnSync('docker', ['ps', '--filter', 'name=^wayfind$', '--format', '{{.Names}}\t'], { stdio: 'pipe' });
|
|
5090
|
+
|
|
5091
|
+
const containerRows = [
|
|
5092
|
+
...(labeledResult.stdout || '').toString().trim().split('\n').filter(Boolean),
|
|
5093
|
+
...(legacyResult.stdout || '').toString().trim().split('\n').filter(Boolean),
|
|
5094
|
+
];
|
|
5095
|
+
|
|
5096
|
+
if (containerRows.length > 0) {
|
|
5097
|
+
console.log(`\nWayfind container(s) detected — updating ${containerRows.length} container(s)...`);
|
|
5098
|
+
|
|
5099
|
+
for (const row of containerRows) {
|
|
5100
|
+
const [containerName] = row.split('\t');
|
|
5101
|
+
if (!containerName) continue;
|
|
5102
|
+
|
|
5103
|
+
// Find compose dir via docker label
|
|
5104
|
+
const inspectResult = spawnSync('docker', [
|
|
5105
|
+
'inspect', containerName,
|
|
5106
|
+
'--format', '{{index .Config.Labels "com.docker.compose.project.working_dir"}}',
|
|
5107
|
+
], { stdio: 'pipe' });
|
|
5108
|
+
const labelDir = (inspectResult.stdout || '').toString().trim();
|
|
5109
|
+
|
|
5110
|
+
let composeDir = '';
|
|
5111
|
+
if (labelDir && fs.existsSync(path.join(labelDir, 'docker-compose.yml'))) {
|
|
5112
|
+
composeDir = labelDir;
|
|
5113
|
+
} else {
|
|
5114
|
+
// For per-team containers, check teams dir
|
|
5115
|
+
const teamsBase = HOME ? path.join(HOME, '.claude', 'team-context', 'teams') : '';
|
|
5116
|
+
if (teamsBase && fs.existsSync(teamsBase)) {
|
|
5117
|
+
for (const tid of fs.readdirSync(teamsBase)) {
|
|
5118
|
+
const candidate = path.join(teamsBase, tid, 'deploy');
|
|
5119
|
+
if (fs.existsSync(path.join(candidate, 'docker-compose.yml'))) {
|
|
5120
|
+
// Check if this compose file manages this container
|
|
5121
|
+
const checkResult = spawnSync('docker', ['compose', 'ps', '--format', '{{.Name}}'], { cwd: candidate, stdio: 'pipe' });
|
|
5122
|
+
const composeContainers = (checkResult.stdout || '').toString();
|
|
5123
|
+
if (composeContainers.includes(containerName)) {
|
|
5124
|
+
composeDir = candidate;
|
|
5125
|
+
break;
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
// Legacy fallback
|
|
5131
|
+
if (!composeDir) {
|
|
5132
|
+
const legacyCandidates = [process.cwd(), path.join(HOME || '', 'team-context', 'deploy')];
|
|
5133
|
+
for (const dir of legacyCandidates) {
|
|
5134
|
+
if (dir && fs.existsSync(path.join(dir, 'docker-compose.yml'))) {
|
|
5135
|
+
composeDir = dir;
|
|
5136
|
+
break;
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
4957
5139
|
}
|
|
4958
5140
|
}
|
|
4959
|
-
}
|
|
4960
5141
|
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
warnings.forEach(w => console.log(` \u26A0 ${w.trim()}`));
|
|
4980
|
-
console.log('\n Post-deploy warnings detected \u2014 review above');
|
|
5142
|
+
if (composeDir) {
|
|
5143
|
+
console.log(`\nUpdating ${containerName} (compose: ${composeDir})...`);
|
|
5144
|
+
const pullResult = spawnSync('docker', ['compose', 'pull'], { cwd: composeDir, stdio: 'inherit' });
|
|
5145
|
+
if (!pullResult.error && pullResult.status === 0) {
|
|
5146
|
+
spawnSync('docker', ['compose', 'up', '-d'], { cwd: composeDir, stdio: 'inherit' });
|
|
5147
|
+
console.log(`${containerName} updated.`);
|
|
5148
|
+
|
|
5149
|
+
// Post-deploy smoke check
|
|
5150
|
+
spawnSync('sleep', ['5']);
|
|
5151
|
+
const logsResult = spawnSync('docker', ['logs', '--tail', '50', containerName], { stdio: 'pipe' });
|
|
5152
|
+
const logOutput = (logsResult.stdout || '').toString() + (logsResult.stderr || '').toString();
|
|
5153
|
+
const warnings = logOutput.split('\n').filter(l => /Warning:|Error:|failed|fallback/i.test(l) && l.trim());
|
|
5154
|
+
if (warnings.length > 0) {
|
|
5155
|
+
warnings.forEach(w => console.log(` \u26A0 ${w.trim()}`));
|
|
5156
|
+
console.log(' Post-deploy warnings detected — review above');
|
|
5157
|
+
} else {
|
|
5158
|
+
console.log(' Post-deploy check: no warnings');
|
|
5159
|
+
}
|
|
4981
5160
|
} else {
|
|
4982
|
-
console.
|
|
5161
|
+
console.error(`Docker pull failed for ${containerName}.`);
|
|
4983
5162
|
}
|
|
4984
5163
|
} else {
|
|
4985
|
-
console.
|
|
5164
|
+
console.log(`Could not locate docker-compose.yml for ${containerName}. Update manually.`);
|
|
4986
5165
|
}
|
|
4987
|
-
} else {
|
|
4988
|
-
console.log('Could not locate docker-compose.yml for the Wayfind container.');
|
|
4989
|
-
console.log('Run "docker compose pull && docker compose up -d" manually in your deploy directory.');
|
|
4990
5166
|
}
|
|
4991
5167
|
}
|
|
4992
5168
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wayfind",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.40",
|
|
4
4
|
"description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"wayfind": "./bin/team-context.js"
|
|
6
|
+
"wayfind": "./bin/team-context.js",
|
|
7
|
+
"wayfind-mcp": "./bin/mcp-server.js"
|
|
7
8
|
},
|
|
8
9
|
"keywords": [
|
|
9
10
|
"ai",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"node": ">=16"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
51
53
|
"@slack/bolt": "^4.6.0",
|
|
52
54
|
"posthog-node": "^5.28.0"
|
|
53
55
|
},
|