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.
@@ -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,
@@ -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 at ~/.claude/team-context/teams/<teamId>/deploy/
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
- const teamsBaseDir = HOME ? path.join(HOME, '.claude', 'team-context', 'teams') : null;
3795
- if (!teamsBaseDir) {
3819
+ if (!HOME) {
3796
3820
  console.error('Cannot resolve home directory.');
3797
3821
  process.exit(1);
3798
3822
  }
3799
3823
 
3800
- const deployDir = path.join(teamsBaseDir, teamId, 'deploy');
3801
- const storeDir = path.join(teamsBaseDir, teamId, 'content-store');
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
- // 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;
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
- 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');
3904
+ fs.copyFileSync(envExampleSrc, envExampleDst);
3905
+ console.log(' .env.example — created (full reference)');
3858
3906
  }
3859
3907
 
3860
- // .env from .env.example
3908
+ // .env minimal seed with only required keys
3861
3909
  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');
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. Fill in ${deployDir}/.env with your tokens`);
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> && cd ~/.claude/team-context/teams/<teamId>/deploy && docker compose up -d');
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
- if (!process.env.SLACK_BOT_TOKEN) missing.push('SLACK_BOT_TOKEN');
4150
- if (!process.env.SLACK_APP_TOKEN) missing.push('SLACK_APP_TOKEN');
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
- // 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');
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,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.43",
3
+ "version": "2.0.45",
4
4
  "description": "Team decision trail for AI-assisted development. The connective tissue between product, engineering, and strategy.",
5
5
  "bin": {
6
6
  "wayfind": "./bin/team-context.js",
@@ -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
- # ── Digest delivery ───────────────────────────────────────────────────────────
22
- # Primary: bot token + channel. The bot posts digests via chat.postMessage,
23
- # which enables reaction tracking and threaded feedback.
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
- # Slack channel for digest delivery (channel ID, not name)
27
- # Right-click channel in Slack → View channel details → copy the ID at bottom
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
- # ── Optional ──────────────────────────────────────────────────────────────────
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 (if you have an AOAI deployment)
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
- # ── Path overrides ───────────────────────────────────────────────────────────
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
- # Override conversations directory (default: ~/.claude/projects)
76
- # TEAM_CONTEXT_CONVERSATIONS_DIR=/data/conversations
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
- # Signal pull schedule (cron, default: daily 6am UTC)
84
- # TEAM_CONTEXT_SIGNAL_SCHEDULE=0 6 * * *
40
+ # Team repo allowlist only journals from these repos appear in digests/queries.
41
+ # TEAM_CONTEXT_INCLUDE_REPOS=MyOrg/*,MyOrg-Libs/*
85
42
 
86
- # Reindex schedule (cron, default: hourly)
87
- # TEAM_CONTEXT_REINDEX_SCHEDULE=0 * * * *
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
- # ── Monitoring ───────────────────────────────────────────────────────────────
59
+ # ── Schedules ────────────────────────────────────────────────────────────────
108
60
 
109
- # Health check port (default: 3141)
110
- # TEAM_CONTEXT_HEALTH_PORT=3141
61
+ # TEAM_CONTEXT_DIGEST_SCHEDULE=0 12 * * *
62
+ # TEAM_CONTEXT_SIGNAL_SCHEDULE=0 6 * * *
63
+ # TEAM_CONTEXT_REINDEX_SCHEDULE=0 * * * *
111
64
 
112
- # Telemetry opt-in anonymous usage data to help improve Wayfind
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: