wayfind 0.0.1 → 2.0.0

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.
Files changed (60) hide show
  1. package/BOOTSTRAP_PROMPT.md +120 -0
  2. package/bin/connectors/github.js +617 -0
  3. package/bin/connectors/index.js +13 -0
  4. package/bin/connectors/intercom.js +595 -0
  5. package/bin/connectors/llm.js +469 -0
  6. package/bin/connectors/notion.js +747 -0
  7. package/bin/connectors/transport.js +325 -0
  8. package/bin/content-store.js +2006 -0
  9. package/bin/digest.js +813 -0
  10. package/bin/rebuild-status.js +297 -0
  11. package/bin/slack-bot.js +1535 -0
  12. package/bin/slack.js +342 -0
  13. package/bin/storage/index.js +171 -0
  14. package/bin/storage/json-backend.js +348 -0
  15. package/bin/storage/sqlite-backend.js +415 -0
  16. package/bin/team-context.js +4209 -0
  17. package/bin/telemetry.js +159 -0
  18. package/doctor.sh +291 -0
  19. package/install.sh +144 -0
  20. package/journal-summary.sh +577 -0
  21. package/package.json +48 -6
  22. package/setup.sh +641 -0
  23. package/specializations/claude-code/CLAUDE.md-global-fragment.md +53 -0
  24. package/specializations/claude-code/CLAUDE.md-repo-fragment.md +16 -0
  25. package/specializations/claude-code/README.md +99 -0
  26. package/specializations/claude-code/commands/doctor.md +31 -0
  27. package/specializations/claude-code/commands/init-memory.md +154 -0
  28. package/specializations/claude-code/commands/init-team.md +415 -0
  29. package/specializations/claude-code/commands/journal.md +66 -0
  30. package/specializations/claude-code/commands/review-prs.md +119 -0
  31. package/specializations/claude-code/hooks/check-global-state.sh +20 -0
  32. package/specializations/claude-code/hooks/session-end.sh +36 -0
  33. package/specializations/claude-code/settings.json +15 -0
  34. package/specializations/cursor/README.md +120 -0
  35. package/specializations/cursor/global-rule.mdc +53 -0
  36. package/specializations/cursor/repo-rule.mdc +25 -0
  37. package/specializations/generic/README.md +47 -0
  38. package/templates/autopilot/design.md +22 -0
  39. package/templates/autopilot/engineering.md +22 -0
  40. package/templates/autopilot/product.md +22 -0
  41. package/templates/autopilot/strategy.md +22 -0
  42. package/templates/autopilot/unified.md +24 -0
  43. package/templates/deploy/.env.example +110 -0
  44. package/templates/deploy/docker-compose.yml +63 -0
  45. package/templates/deploy/slack-app-manifest.json +45 -0
  46. package/templates/github-actions/meridian-digest.yml +85 -0
  47. package/templates/global.md +79 -0
  48. package/templates/memory-file.md +18 -0
  49. package/templates/personal-state.md +14 -0
  50. package/templates/personas.json +28 -0
  51. package/templates/product-state.md +41 -0
  52. package/templates/prompts-readme.md +19 -0
  53. package/templates/repo-state.md +18 -0
  54. package/templates/session-protocol-fragment.md +46 -0
  55. package/templates/slack-app-manifest.json +27 -0
  56. package/templates/statusline.sh +22 -0
  57. package/templates/strategy-state.md +39 -0
  58. package/templates/team-state.md +55 -0
  59. package/uninstall.sh +105 -0
  60. package/README.md +0 -4
@@ -0,0 +1,297 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // ── Constants ────────────────────────────────────────────────────────────────
7
+
8
+ const HOME = process.env.HOME || process.env.USERPROFILE;
9
+ const DEFAULT_GLOBAL_STATE = HOME ? path.join(HOME, '.claude', 'global-state.md') : null;
10
+ const DEFAULT_ROOTS = HOME ? [path.join(HOME, 'repos')] : [];
11
+
12
+ // Header variants to try, in priority order
13
+ const STATUS_HEADERS = [
14
+ 'Current Status',
15
+ 'Current State',
16
+ 'Recent Work',
17
+ 'Current Sprint Focus',
18
+ 'Current Focus',
19
+ ];
20
+
21
+ const NEXT_HEADERS = [
22
+ "What's Next",
23
+ 'Next Session',
24
+ "What's Left",
25
+ ];
26
+
27
+ // ── Scanning ─────────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Recursively find .claude/state.md, .claude/team-state.md, and
31
+ * .claude/personal-state.md files under the given root directories.
32
+ * Returns an array of { repoDir, stateFile } objects.
33
+ * Prefers team-state.md over state.md when both exist.
34
+ * @param {string[]} roots - Directories to scan
35
+ * @returns {Array<{ repoDir: string, stateFile: string }>}
36
+ */
37
+ function scanStateFiles(roots) {
38
+ const results = [];
39
+ const seen = new Set();
40
+
41
+ for (const root of roots) {
42
+ if (!fs.existsSync(root)) continue;
43
+ scanDir(root, results, seen, 0);
44
+ }
45
+
46
+ return results;
47
+ }
48
+
49
+ function scanDir(dir, results, seen, depth) {
50
+ // Don't recurse too deep (repos are typically 1-3 levels under root)
51
+ if (depth > 4) return;
52
+
53
+ let entries;
54
+ try {
55
+ entries = fs.readdirSync(dir, { withFileTypes: true });
56
+ } catch {
57
+ return;
58
+ }
59
+
60
+ // Check if this directory has a .claude/ with state files
61
+ const claudeDir = path.join(dir, '.claude');
62
+ if (fs.existsSync(claudeDir)) {
63
+ const teamState = path.join(claudeDir, 'team-state.md');
64
+ const personalState = path.join(claudeDir, 'personal-state.md');
65
+ const plainState = path.join(claudeDir, 'state.md');
66
+
67
+ // Prefer team-state.md, fall back to state.md
68
+ let stateFile = null;
69
+ if (fs.existsSync(teamState)) stateFile = teamState;
70
+ else if (fs.existsSync(plainState)) stateFile = plainState;
71
+
72
+ if (stateFile && !seen.has(dir)) {
73
+ seen.add(dir);
74
+ results.push({ repoDir: dir, stateFile });
75
+
76
+ // Also note personal-state.md if it exists (for richer parsing)
77
+ if (fs.existsSync(personalState)) {
78
+ results[results.length - 1].personalStateFile = personalState;
79
+ }
80
+ }
81
+ }
82
+
83
+ // Recurse into subdirectories (skip node_modules, .git, etc.)
84
+ const skip = new Set(['node_modules', '.git', '.claude', 'vendor', 'dist', 'build']);
85
+ for (const entry of entries) {
86
+ if (!entry.isDirectory()) continue;
87
+ if (skip.has(entry.name)) continue;
88
+ if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
89
+ scanDir(path.join(dir, entry.name), results, seen, depth + 1);
90
+ }
91
+ }
92
+
93
+ // ── Parsing ──────────────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Parse a state file to extract structured data.
97
+ * @param {string} filePath - Path to a state.md or team-state.md
98
+ * @returns {{ project: string, repo: string, updated: string, status: string, next: string } | null}
99
+ */
100
+ function parseStateFile(filePath) {
101
+ let content;
102
+ try {
103
+ content = fs.readFileSync(filePath, 'utf8');
104
+ } catch {
105
+ return null;
106
+ }
107
+
108
+ if (!content.trim()) return null;
109
+
110
+ // Normalize CRLF → LF (some state files have Windows line endings)
111
+ const lines = content.split('\n').map(l => l.replace(/\r$/, ''));
112
+
113
+ // Extract project name from H1: "# Name — suffix" or "# Name"
114
+ const h1 = lines.find(l => /^# /.test(l));
115
+ let project = '';
116
+ if (h1) {
117
+ const match = h1.match(/^# (.+?)(?:\s*[—–-]\s*.+)?$/);
118
+ project = match ? match[1].trim() : h1.replace(/^# /, '').trim();
119
+ }
120
+
121
+ // Extract "Last updated: YYYY-MM-DD"
122
+ const updatedLine = lines.find(l => /^Last updated:/i.test(l));
123
+ let updated = '';
124
+ if (updatedLine) {
125
+ const match = updatedLine.match(/(\d{4}-\d{2}-\d{2})/);
126
+ updated = match ? match[1] : '';
127
+ }
128
+
129
+ // Derive repo path (relative to HOME)
130
+ const repoDir = path.dirname(path.dirname(filePath));
131
+ let repo = repoDir;
132
+ if (HOME && repoDir.startsWith(HOME)) {
133
+ repo = '~' + repoDir.slice(HOME.length);
134
+ }
135
+
136
+ // Extract status section
137
+ const status = extractSection(lines, STATUS_HEADERS);
138
+
139
+ // Extract next-steps section
140
+ const next = extractSection(lines, NEXT_HEADERS);
141
+
142
+ return { project, repo, updated, status, next };
143
+ }
144
+
145
+ /**
146
+ * Extract the first paragraph from the first matching section header.
147
+ * Truncates to ~120 chars.
148
+ * @param {string[]} lines - File lines
149
+ * @param {string[]} headers - Header names to try in priority order
150
+ * @returns {string}
151
+ */
152
+ function extractSection(lines, headers) {
153
+ for (const header of headers) {
154
+ const idx = lines.findIndex(l => {
155
+ const match = l.match(/^#{2,3}\s+(.+)$/);
156
+ return match && match[1].trim() === header;
157
+ });
158
+ if (idx === -1) continue;
159
+
160
+ // Collect first paragraph after the header
161
+ const para = [];
162
+ for (let i = idx + 1; i < lines.length; i++) {
163
+ const line = lines[i];
164
+ // Stop at next heading or empty line after content
165
+ if (/^#{1,3}\s/.test(line)) break;
166
+ if (line.trim() === '' && para.length > 0) break;
167
+ if (line.trim() === '') continue; // skip leading blank lines
168
+ para.push(line.trim());
169
+ }
170
+
171
+ if (para.length === 0) continue;
172
+
173
+ let text = para.join(' ');
174
+ // Strip markdown formatting for table readability
175
+ text = text.replace(/\*\*/g, '').replace(/`/g, '');
176
+ // Truncate
177
+ if (text.length > 120) {
178
+ text = text.slice(0, 117) + '...';
179
+ }
180
+ return text;
181
+ }
182
+
183
+ return '';
184
+ }
185
+
186
+ // ── Table generation ─────────────────────────────────────────────────────────
187
+
188
+ /**
189
+ * Build a markdown table from parsed entries.
190
+ * @param {Array<{ project: string, repo: string, updated: string, status: string, next: string }>} entries
191
+ * @returns {string}
192
+ */
193
+ function buildStatusTable(entries) {
194
+ // Sort by updated date descending (most recent first)
195
+ const sorted = [...entries].sort((a, b) => {
196
+ if (!a.updated && !b.updated) return 0;
197
+ if (!a.updated) return 1;
198
+ if (!b.updated) return -1;
199
+ return b.updated.localeCompare(a.updated);
200
+ });
201
+
202
+ const lines = [
203
+ '| Project | Repo | Updated | Status | Next |',
204
+ '|---------|------|---------|--------|------|',
205
+ ];
206
+
207
+ for (const e of sorted) {
208
+ // Escape pipe characters in content
209
+ const status = (e.status || '').replace(/\|/g, '\\|');
210
+ const next = (e.next || '').replace(/\|/g, '\\|');
211
+ const project = (e.project || '').replace(/\|/g, '\\|');
212
+ const repo = (e.repo || '').replace(/\|/g, '\\|');
213
+ lines.push(`| ${project} | ${repo} | ${e.updated || ''} | ${status} | ${next} |`);
214
+ }
215
+
216
+ return lines.join('\n');
217
+ }
218
+
219
+ // ── Global state update ──────────────────────────────────────────────────────
220
+
221
+ /**
222
+ * Replace the ## Active Projects section in global-state.md with the given table.
223
+ * Preserves all other sections.
224
+ * @param {string} globalStatePath - Path to global-state.md
225
+ * @param {string} table - Markdown table string
226
+ * @returns {{ written: boolean, path: string }}
227
+ */
228
+ function updateGlobalState(globalStatePath, table) {
229
+ const gPath = globalStatePath || DEFAULT_GLOBAL_STATE;
230
+ if (!gPath) {
231
+ throw new Error('Cannot determine global-state.md path');
232
+ }
233
+
234
+ let content;
235
+ try {
236
+ content = fs.readFileSync(gPath, 'utf8');
237
+ } catch {
238
+ throw new Error(`Cannot read ${gPath}`);
239
+ }
240
+
241
+ const lines = content.split('\n');
242
+
243
+ // Find "## Active Projects" section
244
+ const startIdx = lines.findIndex(l => /^## Active Projects/.test(l));
245
+ if (startIdx === -1) {
246
+ throw new Error('No "## Active Projects" section found in global-state.md');
247
+ }
248
+
249
+ // Find the next ## section after Active Projects
250
+ let endIdx = lines.length;
251
+ for (let i = startIdx + 1; i < lines.length; i++) {
252
+ if (/^## /.test(lines[i])) {
253
+ endIdx = i;
254
+ break;
255
+ }
256
+ }
257
+
258
+ // Build replacement section
259
+ const replacement = [
260
+ '## Active Projects',
261
+ '<!-- AUTO-GENERATED by `wayfind status --write`. Do not edit manually. -->',
262
+ '',
263
+ table,
264
+ '',
265
+ ];
266
+
267
+ // Splice
268
+ const newLines = [
269
+ ...lines.slice(0, startIdx),
270
+ ...replacement,
271
+ ...lines.slice(endIdx),
272
+ ];
273
+
274
+ // Update "Last updated" date at the top
275
+ const today = new Date().toISOString().split('T')[0];
276
+ const newContent = newLines.join('\n').replace(
277
+ /^(Last updated:)\s*\S+/m,
278
+ `$1 ${today}`
279
+ );
280
+
281
+ fs.writeFileSync(gPath, newContent);
282
+ return { written: true, path: gPath };
283
+ }
284
+
285
+ // ── Exports ──────────────────────────────────────────────────────────────────
286
+
287
+ module.exports = {
288
+ scanStateFiles,
289
+ parseStateFile,
290
+ buildStatusTable,
291
+ updateGlobalState,
292
+ extractSection,
293
+ DEFAULT_GLOBAL_STATE,
294
+ DEFAULT_ROOTS,
295
+ STATUS_HEADERS,
296
+ NEXT_HEADERS,
297
+ };