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
package/bin/slack.js ADDED
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const HOME = process.env.HOME || process.env.USERPROFILE;
8
+
9
+ // Persona emoji map
10
+ const PERSONA_EMOJI = {
11
+ unified: ':compass:',
12
+ engineering: ':wrench:',
13
+ product: ':dart:',
14
+ design: ':art:',
15
+ strategy: ':telescope:',
16
+ };
17
+
18
+ // ── Markdown to Slack mrkdwn conversion ─────────────────────────────────────
19
+
20
+ /**
21
+ * Convert standard markdown to Slack mrkdwn format.
22
+ * Preserves code blocks unchanged; converts bold, headings, links, lists, and
23
+ * tables to Slack-compatible formatting.
24
+ * @param {string} markdown - Markdown content
25
+ * @returns {string} Slack mrkdwn content
26
+ */
27
+ function markdownToMrkdwn(markdown) {
28
+ if (!markdown) return '';
29
+
30
+ // Extract code blocks to preserve them unchanged
31
+ const codeBlocks = [];
32
+ let text = markdown.replace(/```[\s\S]*?```/g, (match) => {
33
+ codeBlocks.push(match);
34
+ return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
35
+ });
36
+
37
+ // Convert tables: strip | chars and format as indented lines
38
+ // Match table rows (lines starting with optional whitespace then |)
39
+ const lines = text.split('\n');
40
+ const result = [];
41
+ let inTable = false;
42
+ let tableHeaders = [];
43
+
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ const trimmed = line.trim();
47
+
48
+ // Detect table separator row (e.g., |---|---|)
49
+ if (/^\|[\s:-]+\|/.test(trimmed) && /^[\s|:-]+$/.test(trimmed)) {
50
+ inTable = true;
51
+ continue; // skip separator row
52
+ }
53
+
54
+ // Detect table row
55
+ if (trimmed.startsWith('|') && trimmed.endsWith('|')) {
56
+ const cells = trimmed
57
+ .slice(1, -1)
58
+ .split('|')
59
+ .map((c) => c.trim());
60
+
61
+ if (!inTable) {
62
+ // This is a header row — save headers
63
+ tableHeaders = cells;
64
+ inTable = true;
65
+ continue;
66
+ }
67
+
68
+ // Data row — format as key: value pairs
69
+ if (tableHeaders.length > 0 && tableHeaders.length === cells.length) {
70
+ for (let j = 0; j < cells.length; j++) {
71
+ if (cells[j] && cells[j] !== '-') {
72
+ result.push(` ${tableHeaders[j]}: ${cells[j]}`);
73
+ }
74
+ }
75
+ result.push('');
76
+ } else {
77
+ // No headers or mismatched — just output cells as indented text
78
+ const content = cells.filter((c) => c && c !== '-').join(' ');
79
+ if (content) {
80
+ result.push(` ${content}`);
81
+ }
82
+ }
83
+ continue;
84
+ }
85
+
86
+ // End of table
87
+ if (inTable && !trimmed.startsWith('|')) {
88
+ inTable = false;
89
+ tableHeaders = [];
90
+ }
91
+
92
+ result.push(line);
93
+ }
94
+
95
+ text = result.join('\n');
96
+
97
+ // Strip horizontal rules
98
+ text = text.replace(/^[ \t]*(---+|___+|\*\*\*+)[ \t]*$/gm, '');
99
+
100
+ // Convert bold first (before headings, to avoid collision)
101
+ text = text.replace(/\*\*(.+?)\*\*/g, '*$1*');
102
+
103
+ // Convert headings: # Heading -> *Heading*
104
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');
105
+
106
+ // Convert links: [text](url) -> <url|text>
107
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<$2|$1>');
108
+
109
+ // Convert list items: - item -> bullet item
110
+ text = text.replace(/^(\s*)- (.+)$/gm, '$1\u2022 $2');
111
+
112
+ // Restore code blocks
113
+ text = text.replace(/__CODE_BLOCK_(\d+)__/g, (_, idx) => {
114
+ return codeBlocks[parseInt(idx, 10)];
115
+ });
116
+
117
+ return text;
118
+ }
119
+
120
+ // ── Date formatting ─────────────────────────────────────────────────────────
121
+
122
+ const MONTH_ABBR = [
123
+ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
124
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
125
+ ];
126
+
127
+ /**
128
+ * Format a date range as "Feb 24 \u2013 Feb 28" (abbreviated month, en-dash).
129
+ * @param {{ from: string, to: string }} dateRange
130
+ * @returns {string}
131
+ */
132
+ function formatDateRange(dateRange) {
133
+ const from = new Date(dateRange.from + 'T00:00:00');
134
+ const to = new Date(dateRange.to + 'T00:00:00');
135
+ const fromStr = `${MONTH_ABBR[from.getUTCMonth()]} ${from.getUTCDate()}`;
136
+ const toStr = `${MONTH_ABBR[to.getUTCMonth()]} ${to.getUTCDate()}`;
137
+ return `${fromStr} \u2013 ${toStr}`;
138
+ }
139
+
140
+ /**
141
+ * Capitalize the first letter of a string.
142
+ * @param {string} str
143
+ * @returns {string}
144
+ */
145
+ function capitalize(str) {
146
+ if (!str) return '';
147
+ return str.charAt(0).toUpperCase() + str.slice(1);
148
+ }
149
+
150
+ // ── HTTP POST ───────────────────────────────────────────────────────────────
151
+
152
+ /**
153
+ * POST a JSON payload to a Slack incoming webhook URL.
154
+ * @param {string} webhookUrl - Full HTTPS webhook URL
155
+ * @param {Object} payload - JSON payload to send
156
+ * @returns {Promise<{ ok: true }>}
157
+ */
158
+ function postToWebhook(webhookUrl, payload) {
159
+ return new Promise((resolve, reject) => {
160
+ const url = new URL(webhookUrl);
161
+ const data = JSON.stringify(payload);
162
+ const opts = {
163
+ hostname: url.hostname,
164
+ path: url.pathname,
165
+ method: 'POST',
166
+ headers: {
167
+ 'Content-Type': 'application/json',
168
+ 'Content-Length': Buffer.byteLength(data),
169
+ },
170
+ };
171
+ const req = https.request(opts, (res) => {
172
+ const chunks = [];
173
+ res.on('data', (c) => chunks.push(c));
174
+ res.on('end', () => {
175
+ const body = Buffer.concat(chunks).toString();
176
+ if (res.statusCode >= 200 && res.statusCode < 300) {
177
+ resolve({ ok: true });
178
+ } else {
179
+ reject(new Error(`Slack webhook returned ${res.statusCode}: ${body}`));
180
+ }
181
+ });
182
+ });
183
+ req.on('error', reject);
184
+ req.setTimeout(10000, () => {
185
+ req.destroy();
186
+ reject(new Error('Slack webhook timeout'));
187
+ });
188
+ req.write(data);
189
+ req.end();
190
+ });
191
+ }
192
+
193
+ // ── Bot token delivery ──────────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Deliver a digest to Slack via chat.postMessage (bot token).
197
+ * Returns the message ts for reaction tracking.
198
+ *
199
+ * @param {string} botToken - Slack bot OAuth token (xoxb-...)
200
+ * @param {string} channel - Slack channel ID or name
201
+ * @param {string} content - Formatted mrkdwn content (already converted)
202
+ * @param {string} personaName - Persona ID
203
+ * @returns {Promise<{ ok: true, persona: string, ts: string, channel: string }>}
204
+ */
205
+ async function deliverViaBot(botToken, channel, content, personaName) {
206
+ const { WebClient } = require('@slack/web-api');
207
+ const client = new WebClient(botToken);
208
+
209
+ const truncated = content.length > 3900 ? content.slice(0, 3900) + '\n\n_...truncated_' : content;
210
+
211
+ const result = await client.chat.postMessage({
212
+ channel,
213
+ text: truncated,
214
+ unfurl_links: false,
215
+ });
216
+
217
+ // Post a threaded follow-up asking for feedback
218
+ try {
219
+ await client.chat.postMessage({
220
+ channel,
221
+ thread_ts: result.ts,
222
+ text: '_React to the digest or reply here with feedback — what was useful? What was missing? Your input shapes future digests._',
223
+ unfurl_links: false,
224
+ });
225
+ } catch (err) {
226
+ // Non-fatal — digest was delivered, feedback prompt is optional
227
+ }
228
+
229
+ return { ok: true, persona: personaName, ts: result.ts, channel: result.channel };
230
+ }
231
+
232
+ // ── Deliver ─────────────────────────────────────────────────────────────────
233
+
234
+ /**
235
+ * Deliver a digest to Slack via incoming webhook or bot token.
236
+ * In simulation mode, writes the payload JSON to disk instead of POSTing.
237
+ *
238
+ * @param {string} webhookUrl - Slack incoming webhook URL
239
+ * @param {string} digestContent - Markdown content of the digest
240
+ * @param {string} personaName - Persona ID (engineering, product, design, strategy)
241
+ * @param {{ from: string, to: string }} dateRange - Date range for the digest
242
+ * @param {Object} [options] - Optional delivery options
243
+ * @param {string} [options.botToken] - Slack bot token for chat.postMessage delivery
244
+ * @param {string} [options.channel] - Slack channel for bot delivery
245
+ * @returns {Promise<{ ok: true, persona: string, ts?: string, channel?: string }>}
246
+ */
247
+ async function deliver(webhookUrl, digestContent, personaName, dateRange, options) {
248
+ const emoji = PERSONA_EMOJI[personaName] || ':memo:';
249
+ const label = personaName === 'unified' ? 'Wayfind' : capitalize(personaName);
250
+ const range = formatDateRange(dateRange);
251
+ const mrkdwn = markdownToMrkdwn(digestContent);
252
+ const formattedText = `${emoji} *${label} Digest* (${range})\n\n${mrkdwn}`;
253
+
254
+ const payload = {
255
+ text: formattedText,
256
+ unfurl_links: false,
257
+ unfurl_media: false,
258
+ };
259
+
260
+ // Simulation mode: write payload to disk
261
+ if (process.env.TEAM_CONTEXT_SIMULATE === '1') {
262
+ const digestsDir = path.join(HOME, '.claude', 'team-context', 'digests');
263
+ fs.mkdirSync(digestsDir, { recursive: true });
264
+ const outFile = path.join(digestsDir, `${dateRange.to}-slack-${personaName}.json`);
265
+ fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + '\n', 'utf8');
266
+ return { ok: true, persona: personaName };
267
+ }
268
+
269
+ // Try bot token delivery first (returns message ts for reaction tracking)
270
+ const opts = options || {};
271
+ if (opts.botToken && opts.channel) {
272
+ try {
273
+ return await deliverViaBot(opts.botToken, opts.channel, formattedText, personaName);
274
+ } catch (err) {
275
+ console.error(`Bot delivery failed for ${personaName}, falling back to webhook: ${err.message}`);
276
+ }
277
+ }
278
+
279
+ // Fallback: POST to webhook
280
+ await postToWebhook(webhookUrl, payload);
281
+ return { ok: true, persona: personaName };
282
+ }
283
+
284
+ // ── Deliver All ─────────────────────────────────────────────────────────────
285
+
286
+ /**
287
+ * Deliver digests for multiple personas to Slack.
288
+ * Reads each persona's digest file and calls deliver() with a 1-second delay
289
+ * between posts to respect Slack rate limits.
290
+ *
291
+ * @param {string} webhookUrl - Slack incoming webhook URL
292
+ * @param {Object} digestResult - Return value from generateDigest():
293
+ * { files: string[], personas: string[], dateRange: { from, to } }
294
+ * @param {string[]} personaIds - Array of persona IDs to deliver
295
+ * @param {Object} [options] - Optional delivery options
296
+ * @param {string} [options.botToken] - Slack bot token for chat.postMessage delivery
297
+ * @param {string} [options.channel] - Slack channel for bot delivery
298
+ * @returns {Promise<Array<{ ok: true, persona: string, ts?: string, channel?: string }>>}
299
+ */
300
+ async function deliverAll(webhookUrl, digestResult, personaIds, options) {
301
+ const results = [];
302
+ const toDate = digestResult.dateRange.to;
303
+
304
+ for (let i = 0; i < personaIds.length; i++) {
305
+ const persona = personaIds[i];
306
+
307
+ try {
308
+ // Read the digest file for this persona
309
+ const digestFile = path.join(
310
+ HOME, '.claude', 'team-context', 'digests', persona, `${toDate}.md`
311
+ );
312
+
313
+ let content;
314
+ try {
315
+ content = fs.readFileSync(digestFile, 'utf8');
316
+ } catch {
317
+ results.push({ ok: false, persona, error: `Digest file not found: ${digestFile}` });
318
+ continue;
319
+ }
320
+
321
+ // Rate limit: 1 second delay between posts (skip before first)
322
+ if (i > 0) {
323
+ await new Promise((r) => setTimeout(r, 1000));
324
+ }
325
+
326
+ const result = await deliver(webhookUrl, content, persona, digestResult.dateRange, options);
327
+ results.push(result);
328
+ } catch (err) {
329
+ results.push({ ok: false, persona, error: err.message });
330
+ }
331
+ }
332
+
333
+ return results;
334
+ }
335
+
336
+ module.exports = {
337
+ deliver,
338
+ deliverAll,
339
+ deliverViaBot,
340
+ markdownToMrkdwn,
341
+ postToWebhook,
342
+ };
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const JsonBackend = require('./json-backend');
6
+
7
+ // ── Constants ────────────────────────────────────────────────────────────────
8
+
9
+ const JSON_FILES = [
10
+ 'index.json',
11
+ 'embeddings.json',
12
+ 'conversation-index.json',
13
+ 'digest-feedback.json',
14
+ ];
15
+
16
+ // ── Cache ────────────────────────────────────────────────────────────────────
17
+
18
+ const cache = {};
19
+
20
+ // ── Migration ────────────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Migrate existing JSON data into the SQLite backend.
24
+ * Idempotent — skips if already migrated or no JSON files exist.
25
+ * Does NOT delete JSON files after migration.
26
+ */
27
+ function migrateFromJson(sqliteBackend, storePath) {
28
+ // Already migrated?
29
+ const row = sqliteBackend.db
30
+ .prepare('SELECT value FROM metadata WHERE key = ?')
31
+ .get('migrated_from_json');
32
+ if (row) return;
33
+
34
+ // Any JSON files to migrate?
35
+ const found = JSON_FILES.filter(f =>
36
+ fs.existsSync(path.join(storePath, f))
37
+ );
38
+ if (found.length === 0) return;
39
+
40
+ const json = new JsonBackend(storePath);
41
+ json.open();
42
+
43
+ let totalEntries = 0;
44
+
45
+ // index.json → decisions table
46
+ const index = json.loadIndex();
47
+ if (index && index.entries) {
48
+ const entries = index.entries;
49
+ const count = Object.keys(entries).length;
50
+ if (count > 0) {
51
+ sqliteBackend.bulkUpsertEntries(entries);
52
+ totalEntries += count;
53
+ }
54
+ }
55
+
56
+ // embeddings.json → embeddings table
57
+ const embeddings = json.loadEmbeddings();
58
+ if (embeddings && Object.keys(embeddings).length > 0) {
59
+ sqliteBackend.saveEmbeddings(embeddings);
60
+ totalEntries += Object.keys(embeddings).length;
61
+ }
62
+
63
+ // conversation-index.json → conversation_index table
64
+ const convIndex = json.loadConversationIndex();
65
+ if (convIndex && Object.keys(convIndex).length > 0) {
66
+ sqliteBackend.saveConversationIndex(convIndex);
67
+ totalEntries += Object.keys(convIndex).length;
68
+ }
69
+
70
+ // digest-feedback.json → digest_feedback table
71
+ const feedback = json.loadFeedback();
72
+ if (feedback && feedback.digests && Object.keys(feedback.digests).length > 0) {
73
+ sqliteBackend.saveFeedback(feedback);
74
+ totalEntries += Object.keys(feedback.digests).length;
75
+ }
76
+
77
+ json.close();
78
+
79
+ // Mark migration complete
80
+ sqliteBackend.db
81
+ .prepare('INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)')
82
+ .run('migrated_from_json', new Date().toISOString());
83
+
84
+ if (totalEntries > 0) {
85
+ console.error(`[wayfind] Migrated ${totalEntries} entries from JSON to SQLite`);
86
+ }
87
+ }
88
+
89
+ // ── Backend selection ────────────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Get (or create and cache) the storage backend for the given store path.
93
+ * Honors TEAM_CONTEXT_STORAGE_BACKEND env var ('json' or 'sqlite').
94
+ * When unset, auto-detects: tries SQLite first, falls back to JSON.
95
+ *
96
+ * @param {string} storePath - Directory for storage files
97
+ * @returns {JsonBackend|SqliteBackend}
98
+ */
99
+ function getBackend(storePath) {
100
+ if (cache[storePath]) return cache[storePath];
101
+
102
+ const forced = process.env.TEAM_CONTEXT_STORAGE_BACKEND;
103
+
104
+ if (forced === 'json') {
105
+ const backend = new JsonBackend(storePath);
106
+ backend.open();
107
+ cache[storePath] = backend;
108
+ return backend;
109
+ }
110
+
111
+ if (forced === 'sqlite') {
112
+ const SqliteBackend = require('./sqlite-backend');
113
+ const backend = new SqliteBackend(storePath);
114
+ backend.open();
115
+ migrateFromJson(backend, storePath);
116
+ cache[storePath] = backend;
117
+ return backend;
118
+ }
119
+
120
+ // Auto-detect: try SQLite, fall back to JSON
121
+ try {
122
+ require.resolve('better-sqlite3');
123
+ const SqliteBackend = require('./sqlite-backend');
124
+ const backend = new SqliteBackend(storePath);
125
+ backend.open();
126
+ migrateFromJson(backend, storePath);
127
+ cache[storePath] = backend;
128
+ return backend;
129
+ } catch {
130
+ const backend = new JsonBackend(storePath);
131
+ backend.open();
132
+ cache[storePath] = backend;
133
+ return backend;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Close all cached backends and clear the cache. Useful for tests.
139
+ */
140
+ function clearCache() {
141
+ for (const storePath of Object.keys(cache)) {
142
+ try {
143
+ cache[storePath].close();
144
+ } catch {
145
+ // Ignore close errors during cleanup
146
+ }
147
+ delete cache[storePath];
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Returns the backend type string ('sqlite' or 'json') for a cached backend.
153
+ *
154
+ * @param {string} storePath
155
+ * @returns {'sqlite'|'json'|null} null if no backend is cached for this path
156
+ */
157
+ function getBackendType(storePath) {
158
+ const backend = cache[storePath];
159
+ if (!backend) return null;
160
+ if (backend instanceof JsonBackend) return 'json';
161
+ // SqliteBackend — check constructor name to avoid requiring the module
162
+ // just for a type check (it may not be available)
163
+ if (backend.constructor && backend.constructor.name === 'SqliteBackend') return 'sqlite';
164
+ return null;
165
+ }
166
+
167
+ module.exports = {
168
+ getBackend,
169
+ clearCache,
170
+ getBackendType,
171
+ };