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,469 @@
1
+ 'use strict';
2
+
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { execFile } = require('child_process');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ // ── Constants ────────────────────────────────────────────────────────────────
10
+
11
+ const DEFAULT_MAX_TOKENS = 2048;
12
+ const REQUEST_TIMEOUT_MS = 60000;
13
+ const ANTHROPIC_API_URL = 'https://api.anthropic.com/v1/messages';
14
+ const ANTHROPIC_VERSION = '2023-06-01';
15
+ const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
16
+
17
+ // ── Simulation mode ─────────────────────────────────────────────────────────
18
+
19
+ function isSimulation() {
20
+ return process.env.TEAM_CONTEXT_SIMULATE === '1';
21
+ }
22
+
23
+ function getRepoRoot() {
24
+ // Walk up from this file's directory to find the repo root
25
+ let dir = __dirname;
26
+ for (let i = 0; i < 10; i++) {
27
+ if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
28
+ const parent = path.dirname(dir);
29
+ if (parent === dir) break;
30
+ dir = parent;
31
+ }
32
+ return process.cwd();
33
+ }
34
+
35
+ function loadDigestFixture(personaId) {
36
+ const fixturesDir = process.env.TEAM_CONTEXT_SIM_DIGEST_DIR
37
+ || path.join(getRepoRoot(), 'simulation', 'fixtures', 'digests');
38
+
39
+ const fixturePath = path.join(fixturesDir, `${personaId}.md`);
40
+ try {
41
+ return fs.readFileSync(fixturePath, 'utf8');
42
+ } catch {
43
+ return `[Simulated digest for ${personaId}]`;
44
+ }
45
+ }
46
+
47
+ // ── HTTP helpers ─────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Make an HTTP/HTTPS POST request using Node.js built-ins.
51
+ * @param {string} url - Full URL to POST to
52
+ * @param {Object} headers - Request headers
53
+ * @param {string} body - JSON string body
54
+ * @returns {Promise<{ statusCode: number, headers: Object, body: string }>}
55
+ */
56
+ function httpPost(url, headers, body) {
57
+ return new Promise((resolve, reject) => {
58
+ const parsed = new URL(url);
59
+ const transport = parsed.protocol === 'http:' ? http : https;
60
+
61
+ // Wall-clock deadline covers entire lifecycle (connect + response body)
62
+ const deadline = setTimeout(() => {
63
+ req.destroy(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms`));
64
+ }, REQUEST_TIMEOUT_MS);
65
+
66
+ const opts = {
67
+ hostname: parsed.hostname,
68
+ port: parsed.port || (parsed.protocol === 'http:' ? 80 : 443),
69
+ path: parsed.pathname + (parsed.search || ''),
70
+ method: 'POST',
71
+ headers: {
72
+ ...headers,
73
+ 'content-type': 'application/json',
74
+ 'content-length': Buffer.byteLength(body),
75
+ },
76
+ };
77
+
78
+ const req = transport.request(opts, (res) => {
79
+ const chunks = [];
80
+ res.on('data', (chunk) => chunks.push(chunk));
81
+ res.on('end', () => {
82
+ clearTimeout(deadline);
83
+ resolve({
84
+ statusCode: res.statusCode,
85
+ headers: res.headers,
86
+ body: Buffer.concat(chunks).toString(),
87
+ });
88
+ });
89
+ res.on('error', (err) => {
90
+ clearTimeout(deadline);
91
+ reject(new Error(`Response read error: ${err.message}`));
92
+ });
93
+ });
94
+
95
+ req.on('error', (err) => {
96
+ clearTimeout(deadline);
97
+ reject(new Error(`Network error: ${err.message}`));
98
+ });
99
+
100
+ req.write(body);
101
+ req.end();
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Check an HTTP response for common error conditions and throw descriptive errors.
107
+ * @param {Object} res - Response from httpPost
108
+ * @param {string} provider - Provider name for error messages
109
+ */
110
+ function checkResponse(res, provider) {
111
+ if (res.statusCode === 401 || res.statusCode === 403) {
112
+ throw new Error(`${provider}: Authentication failed (${res.statusCode}). Check your API key.`);
113
+ }
114
+ if (res.statusCode === 429) {
115
+ const retryAfter = res.headers['retry-after'];
116
+ const msg = retryAfter
117
+ ? `${provider}: Rate limited. Retry after ${retryAfter} seconds.`
118
+ : `${provider}: Rate limited. Try again later.`;
119
+ throw new Error(msg);
120
+ }
121
+ if (res.statusCode < 200 || res.statusCode >= 300) {
122
+ const snippet = (res.body || '').slice(0, 500);
123
+ throw new Error(`${provider}: API returned ${res.statusCode}: ${snippet}`);
124
+ }
125
+ }
126
+
127
+ // ── Provider: Anthropic ──────────────────────────────────────────────────────
128
+
129
+ async function callAnthropic(config, systemPrompt, userContent) {
130
+ const apiKey = process.env[config.api_key_env];
131
+ if (!apiKey) {
132
+ throw new Error(`Anthropic: Missing API key. Set ${config.api_key_env} environment variable.`);
133
+ }
134
+
135
+ const payload = JSON.stringify({
136
+ model: config.model,
137
+ max_tokens: config.max_tokens || DEFAULT_MAX_TOKENS,
138
+ system: systemPrompt,
139
+ messages: [{ role: 'user', content: userContent }],
140
+ });
141
+
142
+ const headers = {
143
+ 'x-api-key': apiKey,
144
+ 'anthropic-version': ANTHROPIC_VERSION,
145
+ };
146
+
147
+ const res = await httpPost(ANTHROPIC_API_URL, headers, payload);
148
+ checkResponse(res, 'Anthropic');
149
+
150
+ let data;
151
+ try {
152
+ data = JSON.parse(res.body);
153
+ } catch {
154
+ throw new Error('Anthropic: Failed to parse response JSON.');
155
+ }
156
+
157
+ if (!data.content || !Array.isArray(data.content) || data.content.length === 0) {
158
+ throw new Error('Anthropic: Response missing content array.');
159
+ }
160
+
161
+ const textBlock = data.content.find((b) => b.type === 'text');
162
+ if (!textBlock) {
163
+ throw new Error('Anthropic: Response contains no text content block.');
164
+ }
165
+ return textBlock.text;
166
+ }
167
+
168
+ // ── Provider: OpenAI-compatible ──────────────────────────────────────────────
169
+
170
+ async function callOpenAI(config, systemPrompt, userContent) {
171
+ const baseUrl = config.base_url || OPENAI_API_URL.replace('/chat/completions', '');
172
+ const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`;
173
+
174
+ const headers = {};
175
+ if (config.api_key_env) {
176
+ const apiKey = process.env[config.api_key_env];
177
+ if (!apiKey) {
178
+ throw new Error(`OpenAI: Missing API key. Set ${config.api_key_env} environment variable.`);
179
+ }
180
+ headers['Authorization'] = `Bearer ${apiKey}`;
181
+ }
182
+
183
+ const payload = JSON.stringify({
184
+ model: config.model,
185
+ max_tokens: config.max_tokens || DEFAULT_MAX_TOKENS,
186
+ messages: [
187
+ { role: 'system', content: systemPrompt },
188
+ { role: 'user', content: userContent },
189
+ ],
190
+ });
191
+
192
+ const res = await httpPost(url, headers, payload);
193
+ checkResponse(res, 'OpenAI');
194
+
195
+ let data;
196
+ try {
197
+ data = JSON.parse(res.body);
198
+ } catch {
199
+ throw new Error('OpenAI: Failed to parse response JSON.');
200
+ }
201
+
202
+ if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) {
203
+ throw new Error('OpenAI: Response missing choices array.');
204
+ }
205
+
206
+ if (!data.choices[0].message || !data.choices[0].message.content) {
207
+ throw new Error('OpenAI: Response missing message content.');
208
+ }
209
+
210
+ return data.choices[0].message.content;
211
+ }
212
+
213
+ // ── Provider: CLI ────────────────────────────────────────────────────────────
214
+
215
+ async function callCLI(config, systemPrompt, userContent) {
216
+ const command = config.command;
217
+ if (!command) {
218
+ throw new Error('CLI: No command configured. Set config.command (e.g. "ollama run llama3.2").');
219
+ }
220
+
221
+ // Split command on first space: binary + remaining args
222
+ const spaceIdx = command.indexOf(' ');
223
+ const binary = spaceIdx === -1 ? command : command.slice(0, spaceIdx);
224
+ const args = spaceIdx === -1 ? [] : command.slice(spaceIdx + 1).split(/\s+/).filter(Boolean);
225
+
226
+ const input = systemPrompt + '\n---\n' + userContent;
227
+
228
+ return new Promise((resolve, reject) => {
229
+ const proc = execFile(binary, args, {
230
+ timeout: REQUEST_TIMEOUT_MS,
231
+ maxBuffer: 10 * 1024 * 1024,
232
+ }, (err, stdout, stderr) => {
233
+ if (err) {
234
+ if (err.killed) {
235
+ reject(new Error(`CLI: Command timed out after ${REQUEST_TIMEOUT_MS}ms.`));
236
+ } else {
237
+ reject(new Error(`CLI: Command failed: ${stderr || err.message}`));
238
+ }
239
+ return;
240
+ }
241
+
242
+ const output = stdout.trim();
243
+ if (!output) {
244
+ reject(new Error('CLI: Command produced no output.'));
245
+ return;
246
+ }
247
+
248
+ resolve(output);
249
+ });
250
+
251
+ // Pipe input to stdin
252
+ if (proc.stdin) {
253
+ proc.stdin.write(input);
254
+ proc.stdin.end();
255
+ }
256
+ });
257
+ }
258
+
259
+ // ── Main call() function ─────────────────────────────────────────────────────
260
+
261
+ /**
262
+ * Call an LLM provider with a system prompt and user content.
263
+ * @param {Object} config - Provider configuration
264
+ * @param {string} config.provider - 'anthropic' | 'openai' | 'cli' | 'simulate'
265
+ * @param {string} config.model - Model identifier
266
+ * @param {string} [config.api_key_env] - Environment variable name for API key
267
+ * @param {string} [config.base_url] - Override URL for OpenAI-compatible providers
268
+ * @param {string} [config.command] - CLI command for 'cli' provider
269
+ * @param {string} [config._personaId] - Persona ID for simulation mode
270
+ * @param {string} systemPrompt - System prompt
271
+ * @param {string} userContent - User content
272
+ * @returns {Promise<string>} - LLM response text
273
+ */
274
+ async function call(config, systemPrompt, userContent) {
275
+ // Simulation mode overrides any provider setting
276
+ if (isSimulation() || config.provider === 'simulate') {
277
+ const personaId = config._personaId || 'engineering';
278
+ return loadDigestFixture(personaId);
279
+ }
280
+
281
+ switch (config.provider) {
282
+ case 'anthropic':
283
+ return callAnthropic(config, systemPrompt, userContent);
284
+ case 'openai':
285
+ return callOpenAI(config, systemPrompt, userContent);
286
+ case 'cli':
287
+ return callCLI(config, systemPrompt, userContent);
288
+ default:
289
+ throw new Error(`Unknown LLM provider: "${config.provider}". Expected: anthropic, openai, cli, simulate.`);
290
+ }
291
+ }
292
+
293
+ // ── Auto-detect available provider ───────────────────────────────────────────
294
+
295
+ /**
296
+ * Detect the best available LLM provider from environment.
297
+ * @returns {Promise<{ provider: string, model: string, api_key_env: string|null, base_url: string|null, command: string|null }|null>}
298
+ */
299
+ async function detect() {
300
+ // 1. Check for Anthropic API key
301
+ if (process.env.ANTHROPIC_API_KEY) {
302
+ return {
303
+ provider: 'anthropic',
304
+ model: 'claude-sonnet-4-5-20250929',
305
+ api_key_env: 'ANTHROPIC_API_KEY',
306
+ base_url: null,
307
+ command: null,
308
+ };
309
+ }
310
+
311
+ // 2. Check for OpenAI API key
312
+ if (process.env.OPENAI_API_KEY) {
313
+ return {
314
+ provider: 'openai',
315
+ model: 'gpt-4o-mini',
316
+ api_key_env: 'OPENAI_API_KEY',
317
+ base_url: null,
318
+ command: null,
319
+ };
320
+ }
321
+
322
+ // 3. Check if ollama binary is available
323
+ const ollamaAvailable = await new Promise((resolve) => {
324
+ execFile('which', ['ollama'], (err) => {
325
+ resolve(!err);
326
+ });
327
+ });
328
+
329
+ if (ollamaAvailable) {
330
+ return {
331
+ provider: 'openai',
332
+ model: 'llama3.2',
333
+ api_key_env: null,
334
+ base_url: 'http://localhost:11434/v1',
335
+ command: null,
336
+ };
337
+ }
338
+
339
+ // 4. Nothing found
340
+ return null;
341
+ }
342
+
343
+ // ── Embeddings ────────────────────────────────────────────────────────────────
344
+
345
+ const OPENAI_EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings';
346
+ const DEFAULT_EMBEDDING_MODEL = 'text-embedding-3-small';
347
+
348
+ /**
349
+ * Generate an embedding vector for the given text.
350
+ * Uses the OpenAI embeddings API (works with any OpenAI-compatible endpoint).
351
+ * @param {string} text - Text to embed
352
+ * @param {Object} [options] - Optional configuration
353
+ * @param {string} [options.model] - Embedding model (default: text-embedding-3-small)
354
+ * @param {string} [options.apiKeyEnv] - Env var name for API key (default: OPENAI_API_KEY)
355
+ * @param {string} [options.baseUrl] - Override base URL for OpenAI-compatible endpoints
356
+ * @returns {Promise<number[]>} - Embedding vector
357
+ */
358
+ async function generateEmbedding(text, options = {}) {
359
+ // Simulation mode: return a deterministic fake vector
360
+ if (isSimulation()) {
361
+ const dim = 1536;
362
+ const vec = [];
363
+ let seed = 0;
364
+ for (let i = 0; i < text.length; i++) seed = ((seed << 5) - seed + text.charCodeAt(i)) | 0;
365
+ for (let i = 0; i < dim; i++) {
366
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
367
+ vec.push((seed / 0x7fffffff) * 2 - 1);
368
+ }
369
+ // Normalize
370
+ const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0));
371
+ return vec.map(v => v / mag);
372
+ }
373
+
374
+ // Azure OpenAI: uses different URL pattern and api-key header
375
+ const isAzure = !!(options.azureEndpoint || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT);
376
+ if (isAzure) {
377
+ return generateEmbeddingAzure(text, options);
378
+ }
379
+
380
+ const apiKeyEnv = options.apiKeyEnv || 'OPENAI_API_KEY';
381
+ const apiKey = process.env[apiKeyEnv];
382
+ if (!apiKey) {
383
+ throw new Error(`Embeddings: Missing API key. Set ${apiKeyEnv} environment variable.`);
384
+ }
385
+
386
+ const baseUrl = options.baseUrl || OPENAI_EMBEDDINGS_URL.replace('/embeddings', '');
387
+ const url = `${baseUrl.replace(/\/+$/, '')}/embeddings`;
388
+ const model = options.model || DEFAULT_EMBEDDING_MODEL;
389
+
390
+ const payload = JSON.stringify({
391
+ model,
392
+ input: text,
393
+ });
394
+
395
+ const headers = {
396
+ 'Authorization': `Bearer ${apiKey}`,
397
+ };
398
+
399
+ const res = await httpPost(url, headers, payload);
400
+ checkResponse(res, 'Embeddings');
401
+
402
+ let data;
403
+ try {
404
+ data = JSON.parse(res.body);
405
+ } catch {
406
+ throw new Error('Embeddings: Failed to parse response JSON.');
407
+ }
408
+
409
+ if (!data.data || !Array.isArray(data.data) || data.data.length === 0) {
410
+ throw new Error('Embeddings: Response missing data array.');
411
+ }
412
+
413
+ if (!data.data[0].embedding || !Array.isArray(data.data[0].embedding)) {
414
+ throw new Error('Embeddings: Response missing embedding vector.');
415
+ }
416
+
417
+ return data.data[0].embedding;
418
+ }
419
+
420
+ /**
421
+ * Generate embedding via Azure OpenAI.
422
+ * URL pattern: {endpoint}/openai/deployments/{deployment}/embeddings?api-version=2024-02-01
423
+ * Auth: api-key header
424
+ */
425
+ async function generateEmbeddingAzure(text, options = {}) {
426
+ const endpoint = (options.azureEndpoint || process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT || '').replace(/\/+$/, '');
427
+ const apiKeyEnv = options.azureApiKeyEnv || 'AZURE_OPENAI_EMBEDDING_KEY';
428
+ const apiKey = process.env[apiKeyEnv];
429
+ const deployment = options.azureDeployment || process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT || 'text-embedding-3-small';
430
+ const apiVersion = options.azureApiVersion || '2024-02-01';
431
+
432
+ if (!endpoint) {
433
+ throw new Error('Azure OpenAI Embeddings: Set AZURE_OPENAI_EMBEDDING_ENDPOINT environment variable.');
434
+ }
435
+ if (!apiKey) {
436
+ throw new Error(`Azure OpenAI Embeddings: Set ${apiKeyEnv} environment variable.`);
437
+ }
438
+
439
+ const url = `${endpoint}/openai/deployments/${deployment}/embeddings?api-version=${apiVersion}`;
440
+ const payload = JSON.stringify({ input: text });
441
+ const headers = { 'api-key': apiKey };
442
+
443
+ const res = await httpPost(url, headers, payload);
444
+ checkResponse(res, 'Azure OpenAI Embeddings');
445
+
446
+ let data;
447
+ try {
448
+ data = JSON.parse(res.body);
449
+ } catch {
450
+ throw new Error('Azure OpenAI Embeddings: Failed to parse response JSON.');
451
+ }
452
+
453
+ if (!data.data || !Array.isArray(data.data) || data.data.length === 0) {
454
+ throw new Error('Azure OpenAI Embeddings: Response missing data array.');
455
+ }
456
+
457
+ if (!data.data[0].embedding || !Array.isArray(data.data[0].embedding)) {
458
+ throw new Error('Azure OpenAI Embeddings: Response missing embedding vector.');
459
+ }
460
+
461
+ return data.data[0].embedding;
462
+ }
463
+
464
+ module.exports = {
465
+ call,
466
+ detect,
467
+ generateEmbedding,
468
+ isSimulation,
469
+ };