wayfind 2.0.47 → 2.0.49

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.
@@ -59,6 +59,10 @@ function splitAndLabel(content, startId) {
59
59
  * @param {Object} llmConfig - LLM config for the scoring call
60
60
  * @returns {Promise<Array<{id: number, [personaId]: number}>|null>} Scores array or null on failure
61
61
  */
62
+ // Maximum characters to send to the scoring LLM in a single call.
63
+ // Beyond this, scoring is skipped and budget truncation handles content selection.
64
+ const SCORE_MAX_CHARS = 40000;
65
+
62
66
  async function scoreItems(signalContent, journalContent, personas, llmConfig) {
63
67
  const signalResult = splitAndLabel(signalContent, 0);
64
68
  const journalResult = splitAndLabel(journalContent, signalResult.items.length);
@@ -66,6 +70,11 @@ async function scoreItems(signalContent, journalContent, personas, llmConfig) {
66
70
  const totalItems = signalResult.items.length + journalResult.items.length;
67
71
  if (totalItems === 0) return null;
68
72
 
73
+ // Skip scoring if content is too large for a reliable single LLM call.
74
+ // The token budget step will handle content selection without scoring.
75
+ const totalChars = (signalResult.labeled || '').length + (journalResult.labeled || '').length;
76
+ if (totalChars > SCORE_MAX_CHARS) return null;
77
+
69
78
  const systemPrompt = buildScoringPrompt(personas);
70
79
 
71
80
  const userParts = [];
@@ -327,28 +327,134 @@ async function teamCreate() {
327
327
  }
328
328
 
329
329
  async function teamJoin(args) {
330
- const teamId = args[0];
330
+ const input = args[0];
331
+ if (!input) {
332
+ console.error('Error: repo URL or path is required.');
333
+ console.error('Usage: wayfind team join <repo-url-or-path>');
334
+ console.error('Example: wayfind team join https://github.com/acme/team-context');
335
+ process.exit(1);
336
+ }
337
+
338
+ // Determine if input is a URL to clone or a local path
339
+ const isUrl = /^https?:\/\/|^git@|^github\.com\//.test(input);
340
+ let repoPath;
341
+
342
+ if (isUrl) {
343
+ // Parse org/repo from URL for clone destination suggestion
344
+ const urlMatch = input.match(/[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
345
+ if (!urlMatch) {
346
+ console.error(`Could not parse org/repo from URL: ${input}`);
347
+ process.exit(1);
348
+ }
349
+ const [, org, repo] = urlMatch;
350
+ const orgDir = path.join(HOME, 'repos', org);
351
+ const suggested = fs.existsSync(orgDir)
352
+ ? path.join(orgDir, repo)
353
+ : path.join(HOME, '.claude', 'team-context', repo);
354
+
355
+ let dest = suggested;
356
+ if (fs.existsSync(suggested)) {
357
+ console.log(`\nRepo already cloned at: ${suggested}`);
358
+ const useExisting = await ask(`Use existing clone? [Y/n]: `);
359
+ if (useExisting.toLowerCase() === 'n') {
360
+ const custom = await ask(`Clone to [${suggested}]: `);
361
+ dest = custom ? path.resolve(custom.replace(/^~/, HOME)) : suggested;
362
+ }
363
+ } else {
364
+ const confirm = await ask(`\nClone to ${suggested}? [Y/n]: `);
365
+ if (confirm.toLowerCase() === 'n') {
366
+ const custom = await ask(`Clone to: `);
367
+ if (!custom) { console.error('Destination required.'); process.exit(1); }
368
+ dest = path.resolve(custom.replace(/^~/, HOME));
369
+ }
370
+ console.log(`Cloning ${input}...`);
371
+ const cloneUrl = /^https?:\/\//.test(input) ? input : `https://${input}`;
372
+ const result = spawnSync('git', ['clone', cloneUrl, dest], { stdio: 'inherit' });
373
+ if (result.status !== 0) {
374
+ console.error('Clone failed.');
375
+ process.exit(1);
376
+ }
377
+ }
378
+ repoPath = dest;
379
+ } else {
380
+ repoPath = path.resolve(input.replace(/^~/, HOME));
381
+ if (!fs.existsSync(repoPath)) {
382
+ console.error(`Directory not found: ${repoPath}`);
383
+ process.exit(1);
384
+ }
385
+ }
386
+
387
+ // Read wayfind.json from the repo
388
+ const sharedConfig = readJSONFile(path.join(repoPath, 'wayfind.json')) || {};
389
+ const teamId = sharedConfig.team_id;
331
390
  if (!teamId) {
332
- console.error('Error: team ID is required.');
333
- console.error('Usage: wayfind team join <team-id>');
391
+ console.error('');
392
+ console.error(`Error: wayfind.json in that repo has no team_id.`);
393
+ console.error('Ask your team admin to run:');
394
+ console.error(` wayfind context add <team-id> ${repoPath}`);
334
395
  process.exit(1);
335
396
  }
397
+ const teamName = sharedConfig.team_name || teamId;
398
+ const containerEndpoint = sharedConfig.container_endpoint || null;
336
399
 
337
- const team = {
338
- teamId,
339
- joined: new Date().toISOString(),
400
+ // Register in local context.json
401
+ const config = readContextConfig();
402
+ if (!config.teams) config.teams = {};
403
+ const existing = config.teams[teamId];
404
+ config.teams[teamId] = {
405
+ path: repoPath,
406
+ name: teamName,
407
+ configured_at: new Date().toISOString(),
408
+ ...(containerEndpoint ? { container_endpoint: containerEndpoint } : {}),
409
+ ...(existing && existing.bound_repos ? { bound_repos: existing.bound_repos } : {}),
340
410
  };
411
+ if (!config.default) config.default = teamId;
412
+ writeContextConfig(config);
341
413
 
342
- writeJSONFile(TEAM_FILE, team);
414
+ // Check API key status
415
+ const keyFile = path.join(repoPath, '.wayfind-api-key');
416
+ const keyReady = fs.existsSync(keyFile) && (() => {
417
+ try { return fs.readFileSync(keyFile, 'utf8').trim().length >= 32; } catch { return false; }
418
+ })();
419
+
420
+ // Print confirmation
343
421
  console.log('');
344
- console.log(`Joined team ${teamId}.`);
422
+ console.log(`Joined team '${teamName}' (${teamId})`);
423
+ console.log(` Repo: ${repoPath}`);
424
+ if (containerEndpoint) {
425
+ console.log(` Semantic search: available | ${containerEndpoint}`);
426
+ } else {
427
+ console.log(` Semantic search: not configured`);
428
+ console.log(` Ask your team admin: wayfind deploy set-endpoint <url> --team ${teamId}`);
429
+ }
430
+ if (keyReady) {
431
+ console.log(` Search API key: ready — rotates daily, committed to team repo`);
432
+ } else {
433
+ console.log(` Search API key: pending — will appear after the container's first key rotation`);
434
+ console.log(` Run \`git pull\` in ${repoPath} after the container starts`);
435
+ }
436
+ console.log('');
437
+ console.log('How the search key works:');
438
+ console.log(' The container rotates this key every 24 hours and commits it to the team repo.');
439
+ console.log(' Your Claude Code sessions read the latest key automatically from the cloned repo.');
440
+ console.log(' You never need to manage it — just keep the repo up to date (git pull).');
441
+ console.log('');
442
+ console.log('Next: bind your repos to this team with:');
443
+ console.log(` wayfind context bind ${teamId} (run from each repo you work in)`);
444
+
445
+ if (existing) {
446
+ console.log('');
447
+ console.log(` (Updated existing registration for team ${teamId})`);
448
+ }
345
449
 
450
+ // Register profile in team directory
346
451
  const profile = readJSONFile(PROFILE_FILE);
347
452
  if (profile) {
348
453
  syncMemberToRegistry(profile, teamId);
349
454
  await announceToSlack(profile, teamId);
350
455
  } else {
351
- console.log(" Run 'wayfind whoami --setup' to register in the team directory.");
456
+ console.log('');
457
+ console.log("Run 'wayfind whoami --setup' to register your profile in the team directory.");
352
458
  }
353
459
  console.log('');
354
460
  }
@@ -947,12 +1053,26 @@ async function runDigest(args) {
947
1053
  process.exit(1);
948
1054
  }
949
1055
 
1056
+ // Sanitize configured paths — connectors.json may have been written from inside a container
1057
+ // with paths like /home/node/... or /data/... that don't exist on the host. Fall back to
1058
+ // local defaults for any path that doesn't resolve on this machine.
1059
+ const digestConfig = { ...config.digest };
1060
+ if (digestConfig.store_path && !fs.existsSync(digestConfig.store_path)) {
1061
+ digestConfig.store_path = contentStore.resolveStorePath();
1062
+ }
1063
+ if (digestConfig.journal_dir && !fs.existsSync(digestConfig.journal_dir)) {
1064
+ digestConfig.journal_dir = contentStore.DEFAULT_JOURNAL_DIR;
1065
+ }
1066
+ if (digestConfig.signals_dir && !fs.existsSync(digestConfig.signals_dir)) {
1067
+ digestConfig.signals_dir = contentStore.resolveSignalsDir();
1068
+ }
1069
+
950
1070
  // Generate digests
951
1071
  console.log(`Generating digests for: ${personaIds.join(', ')}`);
952
1072
  console.log(`Period: ${sinceDate} to today`);
953
1073
  console.log('');
954
1074
 
955
- const result = await digest.generateDigest(config.digest, personaIds, sinceDate, (progress) => {
1075
+ const result = await digest.generateDigest(digestConfig, personaIds, sinceDate, (progress) => {
956
1076
  if (progress.phase === 'start') {
957
1077
  process.stdout.write(` ${progress.personaId} (${progress.index + 1}/${progress.total})... `);
958
1078
  } else if (progress.phase === 'done') {
@@ -2033,6 +2153,15 @@ function journalSync(args) {
2033
2153
  teamFiles[teamId].push({ file, srcPath: path.join(journalDir, file) });
2034
2154
  }
2035
2155
 
2156
+ // Always update member version stamp for all registered teams, even if no files to sync.
2157
+ // This ensures the stamp stays current regardless of whether journals are flowing.
2158
+ if (config.teams) {
2159
+ for (const teamId of Object.keys(config.teams)) {
2160
+ const teamPath = getTeamContextPath(teamId);
2161
+ if (teamPath) stampMemberVersion(teamPath);
2162
+ }
2163
+ }
2164
+
2036
2165
  if (Object.keys(teamFiles).length === 0) {
2037
2166
  console.log('No journal files to sync.');
2038
2167
  return;
@@ -3549,9 +3678,20 @@ function contextAdd(args) {
3549
3678
  const config = readContextConfig();
3550
3679
  if (!config.teams) config.teams = {};
3551
3680
 
3552
- // Try to read team name from the repo's wayfind.json
3553
- const sharedConfig = readJSONFile(path.join(resolved, 'wayfind.json')) || {};
3681
+ // Read and update wayfind.json in the repo — write team_id/team_name so joiners
3682
+ // can read them without needing to know the ID out of band
3683
+ const sharedConfigPath = path.join(resolved, 'wayfind.json');
3684
+ const sharedConfig = readJSONFile(sharedConfigPath) || {};
3554
3685
  const teamName = sharedConfig.team_name || teamId;
3686
+ if (!sharedConfig.team_id || !sharedConfig.team_name) {
3687
+ sharedConfig.team_id = teamId;
3688
+ sharedConfig.team_name = teamName;
3689
+ try {
3690
+ fs.writeFileSync(sharedConfigPath, JSON.stringify(sharedConfig, null, 2) + '\n');
3691
+ } catch (err) {
3692
+ console.error(`Warning: could not write wayfind.json: ${err.message}`);
3693
+ }
3694
+ }
3555
3695
 
3556
3696
  config.teams[teamId] = {
3557
3697
  path: resolved,
@@ -3566,6 +3706,23 @@ function contextAdd(args) {
3566
3706
  if (Object.keys(config.teams).length === 1) {
3567
3707
  console.log(' Set as default (only team).');
3568
3708
  }
3709
+
3710
+ // Generate first search API key if one doesn't exist yet
3711
+ const keyFile = path.join(resolved, '.wayfind-api-key');
3712
+ if (!fs.existsSync(keyFile)) {
3713
+ const key = crypto.randomBytes(32).toString('hex');
3714
+ try {
3715
+ fs.writeFileSync(keyFile, key + '\n', 'utf8');
3716
+ // Resolve token for git push (CLI context — use gh CLI)
3717
+ const token = detectGitHubToken();
3718
+ if (token && !process.env.GITHUB_TOKEN) process.env.GITHUB_TOKEN = token;
3719
+ pushApiKey(resolved);
3720
+ console.log(' Generated initial search API key → committed to team repo.');
3721
+ console.log(' Teammates who join will read it automatically. It rotates daily.');
3722
+ } catch (err) {
3723
+ console.error(` Warning: could not generate API key: ${err.message}`);
3724
+ }
3725
+ }
3569
3726
  }
3570
3727
 
3571
3728
  function contextBind(args) {
@@ -3899,7 +4056,8 @@ function deployTeamInit(teamId, { port } = {}) {
3899
4056
  let composeContent = fs.readFileSync(templatePath, 'utf8');
3900
4057
  composeContent = composeContent
3901
4058
  .replace(/container_name: wayfind/, `container_name: ${containerName}`)
3902
- .replace(/- "3141:3141"/, `- "${assignedPort}:3141"`);
4059
+ .replace(/- "3141:3141"/, `- "${assignedPort}:3141"`)
4060
+ .replace(/^(services:)/m, `name: ${containerName}\n\n$1`);
3903
4061
 
3904
4062
  // Inject Docker label for discovery
3905
4063
  composeContent = composeContent.replace(
@@ -4690,7 +4848,7 @@ async function indexJournalsIfAvailable() {
4690
4848
  console.log('No journal files found — skipping index.');
4691
4849
  return;
4692
4850
  }
4693
- const hasEmbeddingKey = !!process.env.OPENAI_API_KEY;
4851
+ const hasEmbeddingKey = !!(process.env.OPENAI_API_KEY || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT);
4694
4852
  console.log(`Indexing ${entries.length} journal files from ${journalDir}${hasEmbeddingKey ? ' (with embeddings)' : ''}...`);
4695
4853
  try {
4696
4854
  const stats = await contentStore.indexJournals({
@@ -5767,6 +5925,21 @@ const COMMANDS = {
5767
5925
  // Also sync public-staging docs if they exist
5768
5926
  const publicDocsDir = path.join(sourceRoot, 'public-staging', 'docs');
5769
5927
 
5928
+ // Keep plugin.json version in sync with package.json before syncing
5929
+ const pluginJsonPath = path.join(sourceRoot, 'plugin', '.claude-plugin', 'plugin.json');
5930
+ const pkgJsonPath = path.join(sourceRoot, 'package.json');
5931
+ if (fs.existsSync(pluginJsonPath) && fs.existsSync(pkgJsonPath)) {
5932
+ try {
5933
+ const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
5934
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
5935
+ if (pluginJson.version !== pkgJson.version) {
5936
+ pluginJson.version = pkgJson.version;
5937
+ fs.writeFileSync(pluginJsonPath, JSON.stringify(pluginJson, null, 2) + '\n');
5938
+ console.log(`Updated plugin.json version to ${pkgJson.version}`);
5939
+ }
5940
+ } catch {}
5941
+ }
5942
+
5770
5943
  console.log('Syncing files...');
5771
5944
  for (const item of syncItems) {
5772
5945
  const isDir = item.endsWith('/');
@@ -5932,8 +6105,8 @@ function showHelp() {
5932
6105
  console.log(' /doctor Check installation health');
5933
6106
  console.log('');
5934
6107
  console.log('Team setup:');
5935
- console.log(' wayfind team create Create a new team');
5936
- console.log(' wayfind team join <id> Join an existing team');
6108
+ console.log(' wayfind team create Create a new team');
6109
+ console.log(' wayfind team join <repo-url-or-path> Join an existing team');
5937
6110
  console.log(' wayfind team status Show current team info');
5938
6111
  console.log(' wayfind whoami Show your profile');
5939
6112
  console.log(' wayfind whoami --setup Set up your profile and personas');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.47",
3
+ "version": "2.0.49",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "wayfind",
3
- "version": "2.0.20",
3
+ "version": "2.0.48",
4
4
  "description": "Team decision trail for AI-assisted development. Session memory, decision journals, and team digests.",
5
5
  "author": {
6
6
  "name": "Wayfind",
@@ -37,7 +37,8 @@ LAST_RUN_FILE="$HOME/.claude/team-context/.last-reindex"
37
37
  if [ -f "$LAST_RUN_FILE" ]; then
38
38
  CHANGED=$(find "$HOME/.claude/projects" -name "*.jsonl" -newer "$LAST_RUN_FILE" -print -quit 2>/dev/null)
39
39
  if [ -z "$CHANGED" ]; then
40
- # No conversation files changed — skip expensive reindex, just sync journals
40
+ # No conversation files changed — skip expensive reindex, just split and sync journals
41
+ $WAYFIND journal split 2>/dev/null
41
42
  $WAYFIND journal sync 2>/dev/null &
42
43
  exit 0
43
44
  fi
@@ -50,5 +51,6 @@ $WAYFIND reindex --conversations-only --export --detect-shifts --write-stats 2>/
50
51
  mkdir -p "$HOME/.claude/team-context"
51
52
  touch "$LAST_RUN_FILE"
52
53
 
53
- # Sync authored journals to team-context repo (backgrounded)
54
+ # Split any unsuffixed journals, then sync to team-context repo (sync backgrounded)
55
+ $WAYFIND journal split 2>/dev/null
54
56
  $WAYFIND journal sync 2>/dev/null &