wayfind 2.0.43 → 2.0.45
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 +67 -0
- package/bin/team-context.js +188 -44
- package/package.json +1 -1
- package/templates/deploy/.env.example +33 -69
- package/templates/deploy/docker-compose.yml +1 -44
package/bin/content-store.js
CHANGED
|
@@ -176,6 +176,27 @@ function isRepoExcluded(repo) {
|
|
|
176
176
|
return false;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Check if a repo name matches a team scope pattern list.
|
|
181
|
+
* Patterns ending with '/' are prefix matches (e.g., 'acme/' matches 'acme/api', 'acme/frontend').
|
|
182
|
+
* All other patterns are exact matches.
|
|
183
|
+
* @param {string|null} repo
|
|
184
|
+
* @param {string[]} patterns
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
function matchesTeamScope(repo, patterns) {
|
|
188
|
+
if (!patterns || patterns.length === 0) return true;
|
|
189
|
+
if (!repo) return false;
|
|
190
|
+
for (const p of patterns) {
|
|
191
|
+
if (p.endsWith('/')) {
|
|
192
|
+
if (repo.startsWith(p)) return true;
|
|
193
|
+
} else if (repo === p) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
179
200
|
// ── Journal parsing ─────────────────────────────────────────────────────────
|
|
180
201
|
|
|
181
202
|
/**
|
|
@@ -432,6 +453,7 @@ async function indexJournals(options = {}) {
|
|
|
432
453
|
|
|
433
454
|
for (const entry of entries) {
|
|
434
455
|
if (isRepoExcluded(entry.repo)) continue;
|
|
456
|
+
if (options.repoAllowlist && !matchesTeamScope(entry.repo, options.repoAllowlist)) continue;
|
|
435
457
|
const id = generateEntryId(date, entry.repo, entry.title);
|
|
436
458
|
const author = entry.author || options.defaultAuthor || '';
|
|
437
459
|
const content = buildContent({ ...entry, date, author });
|
|
@@ -2290,6 +2312,47 @@ function deduplicateResults(results) {
|
|
|
2290
2312
|
return results.filter(r => !absorbedIds.has(r.id));
|
|
2291
2313
|
}
|
|
2292
2314
|
|
|
2315
|
+
/**
|
|
2316
|
+
* Remove entries from a store whose repo doesn't match the allowed patterns.
|
|
2317
|
+
* Trims both index and embeddings. Safe to call repeatedly (idempotent).
|
|
2318
|
+
* @param {string} storePath
|
|
2319
|
+
* @param {string[]} allowedPatterns - prefix patterns (ending '/') or exact names
|
|
2320
|
+
* @returns {{ kept: number, removed: number, removedRepos: string[] }}
|
|
2321
|
+
*/
|
|
2322
|
+
async function trimStore(storePath, allowedPatterns) {
|
|
2323
|
+
if (!allowedPatterns || allowedPatterns.length === 0) {
|
|
2324
|
+
throw new Error('allowedPatterns is required — refusing to trim to empty set');
|
|
2325
|
+
}
|
|
2326
|
+
const backend = getBackend(storePath);
|
|
2327
|
+
const idx = backend.loadIndex();
|
|
2328
|
+
const embeddings = backend.loadEmbeddings() || {};
|
|
2329
|
+
|
|
2330
|
+
const keptEntries = {};
|
|
2331
|
+
const keptEmbeddings = {};
|
|
2332
|
+
const removedRepos = [];
|
|
2333
|
+
|
|
2334
|
+
for (const [id, entry] of Object.entries(idx.entries || {})) {
|
|
2335
|
+
if (matchesTeamScope(entry.repo, allowedPatterns)) {
|
|
2336
|
+
keptEntries[id] = entry;
|
|
2337
|
+
if (embeddings[id]) keptEmbeddings[id] = embeddings[id];
|
|
2338
|
+
} else {
|
|
2339
|
+
removedRepos.push(entry.repo);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
idx.entries = keptEntries;
|
|
2344
|
+
idx.entryCount = Object.keys(keptEntries).length;
|
|
2345
|
+
idx.lastUpdated = new Date().toISOString();
|
|
2346
|
+
backend.saveIndex(idx);
|
|
2347
|
+
backend.saveEmbeddings(keptEmbeddings);
|
|
2348
|
+
|
|
2349
|
+
return {
|
|
2350
|
+
kept: idx.entryCount,
|
|
2351
|
+
removed: removedRepos.length,
|
|
2352
|
+
removedRepos: [...new Set(removedRepos)].sort(),
|
|
2353
|
+
};
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2293
2356
|
module.exports = {
|
|
2294
2357
|
// Parsing
|
|
2295
2358
|
parseJournalFile,
|
|
@@ -2318,8 +2381,12 @@ module.exports = {
|
|
|
2318
2381
|
|
|
2319
2382
|
// Filtering
|
|
2320
2383
|
isRepoExcluded,
|
|
2384
|
+
matchesTeamScope,
|
|
2321
2385
|
applyFilters,
|
|
2322
2386
|
|
|
2387
|
+
// Store maintenance
|
|
2388
|
+
trimStore,
|
|
2389
|
+
|
|
2323
2390
|
// Quality & dedup
|
|
2324
2391
|
computeQualityScore,
|
|
2325
2392
|
deduplicateResults,
|
package/bin/team-context.js
CHANGED
|
@@ -1111,8 +1111,16 @@ async function runIndexJournals(args) {
|
|
|
1111
1111
|
const journalDir = opts.dir || contentStore.DEFAULT_JOURNAL_DIR;
|
|
1112
1112
|
const storePath = opts.store || contentStore.resolveStorePath();
|
|
1113
1113
|
|
|
1114
|
+
// Load team scope allowlist from context.json — only index repos bound to the active team.
|
|
1115
|
+
const ctxConfig = readContextConfig();
|
|
1116
|
+
const teamId = readRepoTeamBinding() || ctxConfig.default;
|
|
1117
|
+
const teamConfig = teamId && ctxConfig.teams && ctxConfig.teams[teamId];
|
|
1118
|
+
const repoAllowlist = (teamConfig && teamConfig.bound_repos && teamConfig.bound_repos.length > 0)
|
|
1119
|
+
? teamConfig.bound_repos : undefined;
|
|
1120
|
+
|
|
1114
1121
|
console.log(`Indexing journals from: ${journalDir}`);
|
|
1115
1122
|
console.log(`Store: ${storePath}`);
|
|
1123
|
+
if (repoAllowlist) console.log(`Team scope (${teamId}): ${repoAllowlist.join(', ')}`);
|
|
1116
1124
|
console.log('');
|
|
1117
1125
|
|
|
1118
1126
|
try {
|
|
@@ -1120,6 +1128,7 @@ async function runIndexJournals(args) {
|
|
|
1120
1128
|
journalDir,
|
|
1121
1129
|
storePath,
|
|
1122
1130
|
embeddings: opts.noEmbeddings ? false : undefined,
|
|
1131
|
+
repoAllowlist,
|
|
1123
1132
|
});
|
|
1124
1133
|
|
|
1125
1134
|
console.log(`Indexed: ${stats.entryCount} entries`);
|
|
@@ -3591,6 +3600,21 @@ function contextBind(args) {
|
|
|
3591
3600
|
writeRepoTeamBinding(teamId);
|
|
3592
3601
|
console.log(`Bound this repo to: ${config.teams[teamId].name} (${teamId})`);
|
|
3593
3602
|
console.log('Journals from this repo will sync to that team\'s context repo.');
|
|
3603
|
+
|
|
3604
|
+
// Derive repo label (e.g., "acme/api") and add to the team's bound_repos allowlist.
|
|
3605
|
+
const cwdParts = process.cwd().split(path.sep);
|
|
3606
|
+
const reposIdx = cwdParts.lastIndexOf('repos');
|
|
3607
|
+
const repoLabel = (reposIdx >= 0 && reposIdx + 2 <= cwdParts.length)
|
|
3608
|
+
? cwdParts.slice(reposIdx + 1).join('/')
|
|
3609
|
+
: cwdParts[cwdParts.length - 1];
|
|
3610
|
+
|
|
3611
|
+
const team = config.teams[teamId];
|
|
3612
|
+
if (!team.bound_repos) team.bound_repos = [];
|
|
3613
|
+
if (!team.bound_repos.includes(repoLabel)) {
|
|
3614
|
+
team.bound_repos.push(repoLabel);
|
|
3615
|
+
writeContextConfig(config);
|
|
3616
|
+
console.log(`Added "${repoLabel}" to team scope.`);
|
|
3617
|
+
}
|
|
3594
3618
|
}
|
|
3595
3619
|
|
|
3596
3620
|
function contextList() {
|
|
@@ -3788,17 +3812,25 @@ async function runDeploy(args) {
|
|
|
3788
3812
|
}
|
|
3789
3813
|
|
|
3790
3814
|
/**
|
|
3791
|
-
* Scaffold a per-team container config
|
|
3815
|
+
* Scaffold a per-team container config in the team's registered repo (deploy/ subdir).
|
|
3816
|
+
* Falls back to ~/.claude/team-context/teams/<teamId>/deploy/ if no repo is registered.
|
|
3792
3817
|
*/
|
|
3793
3818
|
function deployTeamInit(teamId, { port } = {}) {
|
|
3794
|
-
|
|
3795
|
-
if (!teamsBaseDir) {
|
|
3819
|
+
if (!HOME) {
|
|
3796
3820
|
console.error('Cannot resolve home directory.');
|
|
3797
3821
|
process.exit(1);
|
|
3798
3822
|
}
|
|
3799
3823
|
|
|
3800
|
-
|
|
3801
|
-
const
|
|
3824
|
+
// Resolve deploy dir: team repo path first, fallback to store-adjacent
|
|
3825
|
+
const config = readContextConfig();
|
|
3826
|
+
const teamEntry = config.teams && config.teams[teamId];
|
|
3827
|
+
const teamContextPath = teamEntry ? teamEntry.path : null;
|
|
3828
|
+
const deployDir = teamContextPath
|
|
3829
|
+
? path.join(teamContextPath, 'deploy')
|
|
3830
|
+
: path.join(HOME, '.claude', 'team-context', 'teams', teamId, 'deploy');
|
|
3831
|
+
|
|
3832
|
+
// Ensure the per-team store dir exists
|
|
3833
|
+
const storeDir = path.join(HOME, '.claude', 'team-context', 'teams', teamId, 'content-store');
|
|
3802
3834
|
|
|
3803
3835
|
// Check for duplicate running container
|
|
3804
3836
|
const psResult = spawnSync('docker', ['ps', '--filter', `label=com.wayfind.team=${teamId}`, '--format', '{{.Names}}'], { stdio: 'pipe' });
|
|
@@ -3814,22 +3846,42 @@ function deployTeamInit(teamId, { port } = {}) {
|
|
|
3814
3846
|
console.log(`Scaffolding deploy config for team: ${teamId}`);
|
|
3815
3847
|
console.log(`Deploy dir: ${deployDir}`);
|
|
3816
3848
|
|
|
3817
|
-
//
|
|
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;
|
|
3849
|
+
// Auto-detect port: find all wayfind containers, pick next available
|
|
3824
3850
|
const containerName = `wayfind-${teamId}`;
|
|
3851
|
+
let assignedPort = port;
|
|
3852
|
+
if (!assignedPort) {
|
|
3853
|
+
const usedPorts = new Set();
|
|
3854
|
+
// Check labeled containers
|
|
3855
|
+
const portsResult = spawnSync('docker', [
|
|
3856
|
+
'ps', '--filter', 'label=com.wayfind.team',
|
|
3857
|
+
'--format', '{{.Ports}}',
|
|
3858
|
+
], { stdio: 'pipe' });
|
|
3859
|
+
// Also check legacy container named "wayfind"
|
|
3860
|
+
const legacyPortsResult = spawnSync('docker', [
|
|
3861
|
+
'ps', '--filter', 'name=^wayfind',
|
|
3862
|
+
'--format', '{{.Ports}}',
|
|
3863
|
+
], { stdio: 'pipe' });
|
|
3864
|
+
const allPortOutput = [
|
|
3865
|
+
(portsResult.stdout || '').toString(),
|
|
3866
|
+
(legacyPortsResult.stdout || '').toString(),
|
|
3867
|
+
].join('\n');
|
|
3868
|
+
// Extract host ports from "0.0.0.0:3141->3141/tcp" patterns
|
|
3869
|
+
for (const match of allPortOutput.matchAll(/:(\d+)->/g)) {
|
|
3870
|
+
usedPorts.add(parseInt(match[1], 10));
|
|
3871
|
+
}
|
|
3872
|
+
assignedPort = 3141;
|
|
3873
|
+
while (usedPorts.has(assignedPort)) assignedPort++;
|
|
3874
|
+
if (assignedPort !== 3141) {
|
|
3875
|
+
console.log(`Port 3141 in use — assigning port ${assignedPort}`);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3825
3878
|
|
|
3826
3879
|
// Build docker-compose.yml content with per-team overrides
|
|
3827
3880
|
const templatePath = path.join(DEPLOY_TEMPLATES_DIR, 'docker-compose.yml');
|
|
3828
3881
|
let composeContent = fs.readFileSync(templatePath, 'utf8');
|
|
3829
3882
|
composeContent = composeContent
|
|
3830
3883
|
.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}}`);
|
|
3884
|
+
.replace(/- "3141:3141"/, `- "${assignedPort}:3141"`);
|
|
3833
3885
|
|
|
3834
3886
|
// Inject Docker label for discovery
|
|
3835
3887
|
composeContent = composeContent.replace(
|
|
@@ -3845,42 +3897,63 @@ function deployTeamInit(teamId, { port } = {}) {
|
|
|
3845
3897
|
console.log(' docker-compose.yml — already exists, skipping');
|
|
3846
3898
|
}
|
|
3847
3899
|
|
|
3848
|
-
// .env.example
|
|
3900
|
+
// .env.example — full reference with all options
|
|
3849
3901
|
const envExampleSrc = path.join(DEPLOY_TEMPLATES_DIR, '.env.example');
|
|
3850
3902
|
const envExampleDst = path.join(deployDir, '.env.example');
|
|
3851
3903
|
if (!fs.existsSync(envExampleDst) && fs.existsSync(envExampleSrc)) {
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
envContent += `\nTEAM_CONTEXT_TEAM_CONTEXT_PATH=${teamContextPath}\n`;
|
|
3855
|
-
}
|
|
3856
|
-
fs.writeFileSync(envExampleDst, envContent, 'utf8');
|
|
3857
|
-
console.log(' .env.example — created');
|
|
3904
|
+
fs.copyFileSync(envExampleSrc, envExampleDst);
|
|
3905
|
+
console.log(' .env.example — created (full reference)');
|
|
3858
3906
|
}
|
|
3859
3907
|
|
|
3860
|
-
// .env
|
|
3908
|
+
// .env — minimal seed with only required keys
|
|
3861
3909
|
const envPath = path.join(deployDir, '.env');
|
|
3862
|
-
if (!fs.existsSync(envPath)
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3910
|
+
if (!fs.existsSync(envPath)) {
|
|
3911
|
+
const ghToken = detectGitHubToken();
|
|
3912
|
+
const lines = [
|
|
3913
|
+
'# Wayfind — required configuration',
|
|
3914
|
+
'# See .env.example for all available options.',
|
|
3915
|
+
'',
|
|
3916
|
+
'# Anthropic API key (for digests and bot answers)',
|
|
3917
|
+
'ANTHROPIC_API_KEY=sk-ant-your-key',
|
|
3918
|
+
'',
|
|
3919
|
+
'# GitHub token (for pulling team journals and signals)',
|
|
3920
|
+
`GITHUB_TOKEN=${ghToken || ''}`,
|
|
3921
|
+
'',
|
|
3922
|
+
`TEAM_CONTEXT_TENANT_ID=${teamId}`,
|
|
3923
|
+
];
|
|
3924
|
+
// Set volume mount path so docker-compose.yml resolves correctly
|
|
3925
|
+
if (teamContextPath) {
|
|
3926
|
+
lines.push(`TEAM_CONTEXT_TEAM_CONTEXT_PATH=${teamContextPath}`);
|
|
3927
|
+
}
|
|
3928
|
+
fs.writeFileSync(envPath, lines.join('\n') + '\n', 'utf8');
|
|
3929
|
+
console.log(' .env — created (fill in ANTHROPIC_API_KEY)');
|
|
3930
|
+
if (ghToken) {
|
|
3874
3931
|
console.log(' GITHUB_TOKEN — auto-detected from gh CLI');
|
|
3875
3932
|
}
|
|
3933
|
+
} else {
|
|
3934
|
+
console.log(' .env — already exists, skipping');
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
// Ensure deploy/.env is gitignored if we're in a repo
|
|
3938
|
+
if (teamContextPath) {
|
|
3939
|
+
const gitignorePath = path.join(teamContextPath, '.gitignore');
|
|
3940
|
+
const gitignoreEntry = 'deploy/.env';
|
|
3941
|
+
if (fs.existsSync(gitignorePath)) {
|
|
3942
|
+
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
3943
|
+
if (!content.includes(gitignoreEntry)) {
|
|
3944
|
+
fs.appendFileSync(gitignorePath, `\n${gitignoreEntry}\n`);
|
|
3945
|
+
console.log(' .gitignore — added deploy/.env');
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3876
3948
|
}
|
|
3877
3949
|
|
|
3878
3950
|
console.log('');
|
|
3879
3951
|
console.log('Next steps:');
|
|
3880
|
-
console.log(` 1.
|
|
3952
|
+
console.log(` 1. Set ANTHROPIC_API_KEY in ${deployDir}/.env`);
|
|
3881
3953
|
console.log(` 2. cd "${deployDir}" && docker compose up -d`);
|
|
3882
3954
|
console.log(` 3. Verify: curl http://localhost:${assignedPort}/healthz`);
|
|
3883
3955
|
console.log('');
|
|
3956
|
+
console.log('See .env.example for optional config (Slack, embeddings, signals, schedules).');
|
|
3884
3957
|
console.log(`Tip: run "wayfind deploy list" to see all running team containers.`);
|
|
3885
3958
|
|
|
3886
3959
|
telemetry.capture('deploy_team_init', { teamId }, CLI_USER);
|
|
@@ -3904,7 +3977,7 @@ function deployList() {
|
|
|
3904
3977
|
const rows = (psResult.stdout || '').toString().trim();
|
|
3905
3978
|
if (!rows) {
|
|
3906
3979
|
console.log('No Wayfind team containers running.');
|
|
3907
|
-
console.log('Start one with: wayfind deploy --team <teamId>
|
|
3980
|
+
console.log('Start one with: wayfind deploy --team <teamId>');
|
|
3908
3981
|
return;
|
|
3909
3982
|
}
|
|
3910
3983
|
|
|
@@ -4146,8 +4219,10 @@ async function runStart() {
|
|
|
4146
4219
|
|
|
4147
4220
|
// Validate required env vars before proceeding
|
|
4148
4221
|
const missing = [];
|
|
4149
|
-
|
|
4150
|
-
|
|
4222
|
+
// Slack tokens are only required for modes that run the bot
|
|
4223
|
+
const needsSlack = ['bot', 'all-in-one'].includes(mode) && !process.env.TEAM_CONTEXT_NO_SLACK;
|
|
4224
|
+
if (needsSlack && !process.env.SLACK_BOT_TOKEN) missing.push('SLACK_BOT_TOKEN');
|
|
4225
|
+
if (needsSlack && !process.env.SLACK_APP_TOKEN) missing.push('SLACK_APP_TOKEN');
|
|
4151
4226
|
if (!process.env.ANTHROPIC_API_KEY) missing.push('ANTHROPIC_API_KEY');
|
|
4152
4227
|
if (missing.length > 0) {
|
|
4153
4228
|
console.error('');
|
|
@@ -4157,6 +4232,8 @@ async function runStart() {
|
|
|
4157
4232
|
console.error(' cp deploy/.env.example deploy/.env');
|
|
4158
4233
|
console.error(' # Fill in your tokens, then: docker compose up -d');
|
|
4159
4234
|
console.error('');
|
|
4235
|
+
console.error('Tip: set TEAM_CONTEXT_NO_SLACK=1 to run without Slack integration.');
|
|
4236
|
+
console.error('');
|
|
4160
4237
|
process.exit(1);
|
|
4161
4238
|
}
|
|
4162
4239
|
|
|
@@ -5000,6 +5077,52 @@ async function runContainerDoctor() {
|
|
|
5000
5077
|
}
|
|
5001
5078
|
}
|
|
5002
5079
|
|
|
5080
|
+
// ── Store management ────────────────────────────────────────────────────────
|
|
5081
|
+
|
|
5082
|
+
async function runStoreTrim(args) {
|
|
5083
|
+
const ctxConfig = readContextConfig();
|
|
5084
|
+
const teamId = args[0] || readRepoTeamBinding() || ctxConfig.default;
|
|
5085
|
+
if (!teamId) {
|
|
5086
|
+
console.error('No team ID resolved. Usage: wayfind store trim [team-id]');
|
|
5087
|
+
process.exit(1);
|
|
5088
|
+
}
|
|
5089
|
+
const team = ctxConfig.teams && ctxConfig.teams[teamId];
|
|
5090
|
+
if (!team) {
|
|
5091
|
+
console.error(`Team "${teamId}" not found.`);
|
|
5092
|
+
process.exit(1);
|
|
5093
|
+
}
|
|
5094
|
+
const allowedPatterns = team.bound_repos;
|
|
5095
|
+
if (!allowedPatterns || allowedPatterns.length === 0) {
|
|
5096
|
+
console.error(`Team "${teamId}" has no bound_repos in context.json. Configure them first.`);
|
|
5097
|
+
process.exit(1);
|
|
5098
|
+
}
|
|
5099
|
+
const storePath = contentStore.resolveStorePath(teamId);
|
|
5100
|
+
console.log(`Team: ${team.name} (${teamId})`);
|
|
5101
|
+
console.log(`Store: ${storePath}`);
|
|
5102
|
+
console.log(`Patterns: ${allowedPatterns.join(', ')}`);
|
|
5103
|
+
console.log('');
|
|
5104
|
+
const stats = await contentStore.trimStore(storePath, allowedPatterns);
|
|
5105
|
+
console.log(`Kept: ${stats.kept}`);
|
|
5106
|
+
console.log(`Removed: ${stats.removed}`);
|
|
5107
|
+
if (stats.removedRepos.length > 0) {
|
|
5108
|
+
console.log(`Repos removed:`);
|
|
5109
|
+
stats.removedRepos.forEach(r => console.log(` ${r}`));
|
|
5110
|
+
}
|
|
5111
|
+
}
|
|
5112
|
+
|
|
5113
|
+
async function runStore(args) {
|
|
5114
|
+
const [sub, ...subArgs] = args;
|
|
5115
|
+
switch (sub) {
|
|
5116
|
+
case 'trim':
|
|
5117
|
+
await runStoreTrim(subArgs);
|
|
5118
|
+
break;
|
|
5119
|
+
default:
|
|
5120
|
+
console.error(`Unknown store subcommand: ${sub || ''}`);
|
|
5121
|
+
console.error('Available: trim');
|
|
5122
|
+
process.exit(1);
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
|
|
5003
5126
|
// ── Command registry ────────────────────────────────────────────────────────
|
|
5004
5127
|
|
|
5005
5128
|
const COMMANDS = {
|
|
@@ -5111,13 +5234,13 @@ const COMMANDS = {
|
|
|
5111
5234
|
if (labelDir && fs.existsSync(path.join(labelDir, 'docker-compose.yml'))) {
|
|
5112
5235
|
composeDir = labelDir;
|
|
5113
5236
|
} else {
|
|
5114
|
-
//
|
|
5115
|
-
const
|
|
5116
|
-
if (
|
|
5117
|
-
for (const tid of
|
|
5118
|
-
|
|
5237
|
+
// Check team repo paths from context.json first
|
|
5238
|
+
const updateConfig = readContextConfig();
|
|
5239
|
+
if (updateConfig.teams) {
|
|
5240
|
+
for (const [tid, entry] of Object.entries(updateConfig.teams)) {
|
|
5241
|
+
if (!entry.path) continue;
|
|
5242
|
+
const candidate = path.join(entry.path, 'deploy');
|
|
5119
5243
|
if (fs.existsSync(path.join(candidate, 'docker-compose.yml'))) {
|
|
5120
|
-
// Check if this compose file manages this container
|
|
5121
5244
|
const checkResult = spawnSync('docker', ['compose', 'ps', '--format', '{{.Name}}'], { cwd: candidate, stdio: 'pipe' });
|
|
5122
5245
|
const composeContainers = (checkResult.stdout || '').toString();
|
|
5123
5246
|
if (composeContainers.includes(containerName)) {
|
|
@@ -5127,6 +5250,23 @@ const COMMANDS = {
|
|
|
5127
5250
|
}
|
|
5128
5251
|
}
|
|
5129
5252
|
}
|
|
5253
|
+
// Fallback: check store-adjacent deploy dirs
|
|
5254
|
+
if (!composeDir) {
|
|
5255
|
+
const teamsBase = HOME ? path.join(HOME, '.claude', 'team-context', 'teams') : '';
|
|
5256
|
+
if (teamsBase && fs.existsSync(teamsBase)) {
|
|
5257
|
+
for (const tid of fs.readdirSync(teamsBase)) {
|
|
5258
|
+
const candidate = path.join(teamsBase, tid, 'deploy');
|
|
5259
|
+
if (fs.existsSync(path.join(candidate, 'docker-compose.yml'))) {
|
|
5260
|
+
const checkResult = spawnSync('docker', ['compose', 'ps', '--format', '{{.Name}}'], { cwd: candidate, stdio: 'pipe' });
|
|
5261
|
+
const composeContainers = (checkResult.stdout || '').toString();
|
|
5262
|
+
if (composeContainers.includes(containerName)) {
|
|
5263
|
+
composeDir = candidate;
|
|
5264
|
+
break;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5130
5270
|
// Legacy fallback
|
|
5131
5271
|
if (!composeDir) {
|
|
5132
5272
|
const legacyCandidates = [process.cwd(), path.join(HOME || '', 'team-context', 'deploy')];
|
|
@@ -5274,6 +5414,10 @@ const COMMANDS = {
|
|
|
5274
5414
|
desc: 'Scaffold Docker deployment in your team context repo',
|
|
5275
5415
|
run: (args) => runDeploy(args),
|
|
5276
5416
|
},
|
|
5417
|
+
store: {
|
|
5418
|
+
desc: 'Manage content store (trim)',
|
|
5419
|
+
run: (args) => runStore(args),
|
|
5420
|
+
},
|
|
5277
5421
|
onboard: {
|
|
5278
5422
|
desc: 'Generate an onboarding context pack for a repo',
|
|
5279
5423
|
run: (args) => runOnboard(args),
|
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:
|