wayfind 2.0.41 → 2.0.44

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() {
@@ -5000,6 +5024,52 @@ async function runContainerDoctor() {
5000
5024
  }
5001
5025
  }
5002
5026
 
5027
+ // ── Store management ────────────────────────────────────────────────────────
5028
+
5029
+ async function runStoreTrim(args) {
5030
+ const ctxConfig = readContextConfig();
5031
+ const teamId = args[0] || readRepoTeamBinding() || ctxConfig.default;
5032
+ if (!teamId) {
5033
+ console.error('No team ID resolved. Usage: wayfind store trim [team-id]');
5034
+ process.exit(1);
5035
+ }
5036
+ const team = ctxConfig.teams && ctxConfig.teams[teamId];
5037
+ if (!team) {
5038
+ console.error(`Team "${teamId}" not found.`);
5039
+ process.exit(1);
5040
+ }
5041
+ const allowedPatterns = team.bound_repos;
5042
+ if (!allowedPatterns || allowedPatterns.length === 0) {
5043
+ console.error(`Team "${teamId}" has no bound_repos in context.json. Configure them first.`);
5044
+ process.exit(1);
5045
+ }
5046
+ const storePath = contentStore.resolveStorePath(teamId);
5047
+ console.log(`Team: ${team.name} (${teamId})`);
5048
+ console.log(`Store: ${storePath}`);
5049
+ console.log(`Patterns: ${allowedPatterns.join(', ')}`);
5050
+ console.log('');
5051
+ const stats = await contentStore.trimStore(storePath, allowedPatterns);
5052
+ console.log(`Kept: ${stats.kept}`);
5053
+ console.log(`Removed: ${stats.removed}`);
5054
+ if (stats.removedRepos.length > 0) {
5055
+ console.log(`Repos removed:`);
5056
+ stats.removedRepos.forEach(r => console.log(` ${r}`));
5057
+ }
5058
+ }
5059
+
5060
+ async function runStore(args) {
5061
+ const [sub, ...subArgs] = args;
5062
+ switch (sub) {
5063
+ case 'trim':
5064
+ await runStoreTrim(subArgs);
5065
+ break;
5066
+ default:
5067
+ console.error(`Unknown store subcommand: ${sub || ''}`);
5068
+ console.error('Available: trim');
5069
+ process.exit(1);
5070
+ }
5071
+ }
5072
+
5003
5073
  // ── Command registry ────────────────────────────────────────────────────────
5004
5074
 
5005
5075
  const COMMANDS = {
@@ -5274,6 +5344,10 @@ const COMMANDS = {
5274
5344
  desc: 'Scaffold Docker deployment in your team context repo',
5275
5345
  run: (args) => runDeploy(args),
5276
5346
  },
5347
+ store: {
5348
+ desc: 'Manage content store (trim)',
5349
+ run: (args) => runStore(args),
5350
+ },
5277
5351
  onboard: {
5278
5352
  desc: 'Generate an onboarding context pack for a repo',
5279
5353
  run: (args) => runOnboard(args),
package/doctor.sh CHANGED
@@ -363,16 +363,45 @@ check_storage_backend() {
363
363
  info "TEAM_CONTEXT_STORAGE_BACKEND=$TEAM_CONTEXT_STORAGE_BACKEND (env override)"
364
364
  fi
365
365
 
366
- local WAYFIND_DIR="${WAYFIND_DIR:-$HOME/.claude/team-context}"
367
366
  local SCRIPT_DIR
368
367
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
369
368
 
369
+ # Resolve the team-scoped store path (respects .claude/wayfind.json + context.json default)
370
+ local RESOLVED_STORE ACTIVE_TEAM_ID
371
+ RESOLVED_STORE=$(node -e "
372
+ try {
373
+ const cs = require('$SCRIPT_DIR/bin/content-store.js');
374
+ console.log(cs.resolveStorePath());
375
+ } catch (e) {
376
+ console.log('SKIP:' + e.message.split('\n')[0]);
377
+ }
378
+ " 2>/dev/null) || RESOLVED_STORE="SKIP:content-store not available"
379
+ ACTIVE_TEAM_ID=$(node -e "
380
+ try {
381
+ const cs = require('$SCRIPT_DIR/bin/content-store.js');
382
+ // resolveStorePath triggers team ID resolution; extract from path
383
+ const p = cs.resolveStorePath();
384
+ const m = p && p.match(/teams\/([^/]+)\/content-store/);
385
+ console.log(m ? m[1] : 'default');
386
+ } catch (e) {
387
+ console.log('unknown');
388
+ }
389
+ " 2>/dev/null) || ACTIVE_TEAM_ID="unknown"
390
+
391
+ if [[ "$RESOLVED_STORE" != SKIP:* ]]; then
392
+ ok "Active team: $ACTIVE_TEAM_ID"
393
+ info "Store path: $RESOLVED_STORE"
394
+ fi
395
+
396
+ local WAYFIND_DIR="${WAYFIND_DIR:-$HOME/.claude/team-context}"
397
+
370
398
  # ── 1. Which backend is active ───────────────────────────────────────────
371
399
  local BACKEND_RESULT=""
372
400
  BACKEND_RESULT=$(node -e "
373
401
  try {
402
+ const cs = require('$SCRIPT_DIR/bin/content-store.js');
374
403
  const storage = require('$SCRIPT_DIR/bin/storage/index.js');
375
- const storePath = '$WAYFIND_DIR';
404
+ const storePath = cs.resolveStorePath();
376
405
  storage.getBackend(storePath);
377
406
  const info = storage.getBackendInfo(storePath);
378
407
  if (!info) { console.log('NONE'); process.exit(0); }
@@ -472,8 +501,9 @@ check_storage_backend() {
472
501
  local EMBED_RESULT
473
502
  EMBED_RESULT=$(node -e "
474
503
  try {
504
+ const cs = require('$SCRIPT_DIR/bin/content-store.js');
475
505
  const storage = require('$SCRIPT_DIR/bin/storage/index.js');
476
- const storePath = '$WAYFIND_DIR/content-store';
506
+ const storePath = cs.resolveStorePath();
477
507
  const backend = storage.getBackend(storePath);
478
508
  const idx = backend.loadIndex();
479
509
  if (!idx || !idx.entries) { console.log('SKIP'); process.exit(0); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.41",
3
+ "version": "2.0.44",
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",
package/setup.sh CHANGED
@@ -441,48 +441,6 @@ PYEOF
441
441
  fi
442
442
  fi
443
443
 
444
- # Status line script
445
- STATUSLINE_DEST="$HOME/.claude/team-context/statusline.sh"
446
- if [ ! -f "$STATUSLINE_DEST" ] || [ "$UPDATE" = true ]; then
447
- run cp "$SCRIPT_DIR/templates/statusline.sh" "$STATUSLINE_DEST"
448
- run chmod +x "$STATUSLINE_DEST"
449
- log "Installed status line: $STATUSLINE_DEST"
450
- else
451
- info "Status line already exists: $STATUSLINE_DEST — skipped"
452
- fi
453
-
454
- # Merge statusLine config into settings.json
455
- if [ -f "$SETTINGS" ] && ! grep -q "statusLine" "$SETTINGS" 2>/dev/null; then
456
- if [ "$DRY_RUN" = false ]; then
457
- TMP_SL="$(mktemp)"
458
- if python3 - "$SETTINGS" "$STATUSLINE_DEST" "$TMP_SL" <<'PYEOF' 2>/dev/null; then
459
- import json, sys
460
- settings_path, sl_cmd, out_path = sys.argv[1], sys.argv[2], sys.argv[3]
461
- try:
462
- with open(settings_path) as f:
463
- settings = json.load(f)
464
- except (json.JSONDecodeError, IOError):
465
- sys.exit(1)
466
- if "statusLine" not in settings:
467
- settings["statusLine"] = {"type": "command", "command": sl_cmd, "padding": 2}
468
- with open(out_path, "w") as f:
469
- json.dump(settings, f, indent=2)
470
- f.write("\n")
471
- PYEOF
472
- mv "$TMP_SL" "$SETTINGS"
473
- log "Added statusLine config to $SETTINGS"
474
- else
475
- rm -f "$TMP_SL"
476
- warn "Could not add statusLine to $SETTINGS (malformed JSON or python3 unavailable)"
477
- fi
478
- else
479
- info "[dry-run] Would add statusLine to $SETTINGS"
480
- fi
481
- else
482
- if [ -f "$SETTINGS" ]; then
483
- info "statusLine already configured in settings.json — skipped"
484
- fi
485
- fi
486
444
  ;;
487
445
 
488
446
  cursor)