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.
- package/BOOTSTRAP_PROMPT.md +120 -0
- package/bin/connectors/github.js +617 -0
- package/bin/connectors/index.js +13 -0
- package/bin/connectors/intercom.js +595 -0
- package/bin/connectors/llm.js +469 -0
- package/bin/connectors/notion.js +747 -0
- package/bin/connectors/transport.js +325 -0
- package/bin/content-store.js +2006 -0
- package/bin/digest.js +813 -0
- package/bin/rebuild-status.js +297 -0
- package/bin/slack-bot.js +1535 -0
- package/bin/slack.js +342 -0
- package/bin/storage/index.js +171 -0
- package/bin/storage/json-backend.js +348 -0
- package/bin/storage/sqlite-backend.js +415 -0
- package/bin/team-context.js +4209 -0
- package/bin/telemetry.js +159 -0
- package/doctor.sh +291 -0
- package/install.sh +144 -0
- package/journal-summary.sh +577 -0
- package/package.json +48 -6
- package/setup.sh +641 -0
- package/specializations/claude-code/CLAUDE.md-global-fragment.md +53 -0
- package/specializations/claude-code/CLAUDE.md-repo-fragment.md +16 -0
- package/specializations/claude-code/README.md +99 -0
- package/specializations/claude-code/commands/doctor.md +31 -0
- package/specializations/claude-code/commands/init-memory.md +154 -0
- package/specializations/claude-code/commands/init-team.md +415 -0
- package/specializations/claude-code/commands/journal.md +66 -0
- package/specializations/claude-code/commands/review-prs.md +119 -0
- package/specializations/claude-code/hooks/check-global-state.sh +20 -0
- package/specializations/claude-code/hooks/session-end.sh +36 -0
- package/specializations/claude-code/settings.json +15 -0
- package/specializations/cursor/README.md +120 -0
- package/specializations/cursor/global-rule.mdc +53 -0
- package/specializations/cursor/repo-rule.mdc +25 -0
- package/specializations/generic/README.md +47 -0
- package/templates/autopilot/design.md +22 -0
- package/templates/autopilot/engineering.md +22 -0
- package/templates/autopilot/product.md +22 -0
- package/templates/autopilot/strategy.md +22 -0
- package/templates/autopilot/unified.md +24 -0
- package/templates/deploy/.env.example +110 -0
- package/templates/deploy/docker-compose.yml +63 -0
- package/templates/deploy/slack-app-manifest.json +45 -0
- package/templates/github-actions/meridian-digest.yml +85 -0
- package/templates/global.md +79 -0
- package/templates/memory-file.md +18 -0
- package/templates/personal-state.md +14 -0
- package/templates/personas.json +28 -0
- package/templates/product-state.md +41 -0
- package/templates/prompts-readme.md +19 -0
- package/templates/repo-state.md +18 -0
- package/templates/session-protocol-fragment.md +46 -0
- package/templates/slack-app-manifest.json +27 -0
- package/templates/statusline.sh +22 -0
- package/templates/strategy-state.md +39 -0
- package/templates/team-state.md +55 -0
- package/uninstall.sh +105 -0
- 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
|
+
};
|