wayfind 2.0.68 → 2.0.70
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/bin/connectors/llm.js +87 -0
- package/bin/content-store.js +13 -143
- package/bin/mcp-server.js +47 -50
- package/bin/slack-bot.js +136 -1060
- package/bin/team-context.js +41 -31
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
package/bin/connectors/llm.js
CHANGED
|
@@ -302,6 +302,92 @@ async function call(config, systemPrompt, userContent) {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
// ── Tool-use relay ──────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Call the Anthropic API with tool-use support, looping until the model stops.
|
|
309
|
+
* @param {Object} config - Provider configuration (must be Anthropic)
|
|
310
|
+
* @param {string} systemPrompt - System prompt
|
|
311
|
+
* @param {string} userContent - User message text
|
|
312
|
+
* @param {Array} tools - Anthropic tool-use format tool definitions
|
|
313
|
+
* @param {Function} handleToolCall - async (name, input) => result
|
|
314
|
+
* @returns {Promise<string>} - Final text response
|
|
315
|
+
*/
|
|
316
|
+
async function callWithTools(config, systemPrompt, userContent, tools, handleToolCall) {
|
|
317
|
+
const apiKey = process.env[config.api_key_env];
|
|
318
|
+
if (!apiKey) {
|
|
319
|
+
throw new Error(`Anthropic: Missing API key. Set ${config.api_key_env} environment variable.`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const headers = {
|
|
323
|
+
'x-api-key': apiKey,
|
|
324
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const MAX_ITERATIONS = 10;
|
|
328
|
+
let messages = [{ role: 'user', content: userContent }];
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
331
|
+
const payload = JSON.stringify({
|
|
332
|
+
model: config.model,
|
|
333
|
+
max_tokens: config.max_tokens || DEFAULT_MAX_TOKENS,
|
|
334
|
+
system: systemPrompt,
|
|
335
|
+
messages,
|
|
336
|
+
tools,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const res = await httpPost(ANTHROPIC_API_URL, headers, payload);
|
|
340
|
+
checkResponse(res, 'Anthropic');
|
|
341
|
+
|
|
342
|
+
let data;
|
|
343
|
+
try {
|
|
344
|
+
data = JSON.parse(res.body);
|
|
345
|
+
} catch {
|
|
346
|
+
throw new Error('Anthropic: Failed to parse response JSON.');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!data.content || !Array.isArray(data.content)) {
|
|
350
|
+
throw new Error('Anthropic: Response missing content array.');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// If the model is done, extract and return the final text
|
|
354
|
+
if (data.stop_reason !== 'tool_use') {
|
|
355
|
+
const textBlock = data.content.find(b => b.type === 'text');
|
|
356
|
+
return textBlock ? textBlock.text : '';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Process tool calls
|
|
360
|
+
const toolUseBlocks = data.content.filter(b => b.type === 'tool_use');
|
|
361
|
+
if (toolUseBlocks.length === 0) {
|
|
362
|
+
// stop_reason is tool_use but no tool_use blocks — treat as done
|
|
363
|
+
const textBlock = data.content.find(b => b.type === 'text');
|
|
364
|
+
return textBlock ? textBlock.text : '';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Build tool results
|
|
368
|
+
const toolResults = [];
|
|
369
|
+
for (const block of toolUseBlocks) {
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
result = await handleToolCall(block.name, block.input);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
result = { error: err.message };
|
|
375
|
+
}
|
|
376
|
+
toolResults.push({
|
|
377
|
+
type: 'tool_result',
|
|
378
|
+
tool_use_id: block.id,
|
|
379
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Append assistant message + tool results, then loop
|
|
384
|
+
messages.push({ role: 'assistant', content: data.content });
|
|
385
|
+
messages.push({ role: 'user', content: toolResults });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
throw new Error('Anthropic: Tool-use loop exceeded maximum iterations (10).');
|
|
389
|
+
}
|
|
390
|
+
|
|
305
391
|
// ── Auto-detect available provider ───────────────────────────────────────────
|
|
306
392
|
|
|
307
393
|
/**
|
|
@@ -535,6 +621,7 @@ async function generateEmbeddingAzure(text, options = {}) {
|
|
|
535
621
|
|
|
536
622
|
module.exports = {
|
|
537
623
|
call,
|
|
624
|
+
callWithTools,
|
|
538
625
|
detect,
|
|
539
626
|
generateEmbedding,
|
|
540
627
|
getEmbeddingProviderInfo,
|
package/bin/content-store.js
CHANGED
|
@@ -610,7 +610,7 @@ async function indexJournals(options = {}) {
|
|
|
610
610
|
|
|
611
611
|
/**
|
|
612
612
|
* Search journals using semantic similarity.
|
|
613
|
-
* Falls back to
|
|
613
|
+
* Falls back to queryMetadata() browse if no embeddings available.
|
|
614
614
|
* @param {string} query - Search query
|
|
615
615
|
* @param {Object} [options]
|
|
616
616
|
* @param {string} [options.storePath] - Content store directory
|
|
@@ -633,7 +633,11 @@ async function searchJournals(query, options = {}) {
|
|
|
633
633
|
const hasEmbeddings = Object.keys(embeddings).length > 0;
|
|
634
634
|
|
|
635
635
|
if (!hasEmbeddings) {
|
|
636
|
-
|
|
636
|
+
const browseResults = queryMetadata(options);
|
|
637
|
+
return browseResults.slice(0, limit).map(r => ({
|
|
638
|
+
...r, score: null,
|
|
639
|
+
_hint: 'Semantic search unavailable — showing recent entries by date. Run "wayfind reindex" to enable semantic search.',
|
|
640
|
+
}));
|
|
637
641
|
}
|
|
638
642
|
|
|
639
643
|
// Generate query embedding
|
|
@@ -641,8 +645,12 @@ async function searchJournals(query, options = {}) {
|
|
|
641
645
|
try {
|
|
642
646
|
queryVec = await llm.generateEmbedding(query);
|
|
643
647
|
} catch {
|
|
644
|
-
// Fall back to
|
|
645
|
-
|
|
648
|
+
// Fall back to browse if embedding generation fails
|
|
649
|
+
const browseResults = queryMetadata(options);
|
|
650
|
+
return browseResults.slice(0, limit).map(r => ({
|
|
651
|
+
...r, score: null,
|
|
652
|
+
_hint: 'Semantic search unavailable — showing recent entries by date. Run "wayfind reindex" to enable semantic search.',
|
|
653
|
+
}));
|
|
646
654
|
}
|
|
647
655
|
|
|
648
656
|
// Score all entries
|
|
@@ -662,143 +670,6 @@ async function searchJournals(query, options = {}) {
|
|
|
662
670
|
return results.slice(0, limit);
|
|
663
671
|
}
|
|
664
672
|
|
|
665
|
-
/**
|
|
666
|
-
* Full-text search across journal entries.
|
|
667
|
-
* Works without any API key. Matches query words against title, repo, tags.
|
|
668
|
-
* @param {string} query - Search query
|
|
669
|
-
* @param {Object} [options]
|
|
670
|
-
* @param {string} [options.storePath] - Content store directory
|
|
671
|
-
* @param {number} [options.limit] - Max results (default: 10)
|
|
672
|
-
* @param {string} [options.repo] - Filter by repo
|
|
673
|
-
* @param {string} [options.since] - Filter by date (YYYY-MM-DD)
|
|
674
|
-
* @param {string} [options.until] - Filter by date (YYYY-MM-DD)
|
|
675
|
-
* @param {boolean} [options.drifted] - Filter by drift status
|
|
676
|
-
* @returns {Array<{ id: string, score: number, entry: Object }>}
|
|
677
|
-
*/
|
|
678
|
-
function searchText(query, options = {}) {
|
|
679
|
-
const storePath = options.storePath || resolveStorePath();
|
|
680
|
-
const journalDir = options.journalDir || DEFAULT_JOURNAL_DIR;
|
|
681
|
-
const limit = options.limit || 10;
|
|
682
|
-
|
|
683
|
-
const index = getBackend(storePath).loadIndex();
|
|
684
|
-
if (!index) return [];
|
|
685
|
-
|
|
686
|
-
// Normalize: split on whitespace, hyphens, underscores
|
|
687
|
-
const queryWords = query.toLowerCase().split(/[\s\-_]+/).filter(w => w.length > 1);
|
|
688
|
-
if (queryWords.length === 0) return [];
|
|
689
|
-
|
|
690
|
-
// Pre-load journal content for full-text search (cache by date+user key)
|
|
691
|
-
const journalCache = {};
|
|
692
|
-
function getJournalContent(date, user) {
|
|
693
|
-
const cacheKey = user ? `${date}-${user}` : date;
|
|
694
|
-
if (journalCache[cacheKey] !== undefined) return journalCache[cacheKey];
|
|
695
|
-
if (!journalDir) { journalCache[cacheKey] = null; return null; }
|
|
696
|
-
// Try authored filename first, then plain date filename
|
|
697
|
-
const candidates = user
|
|
698
|
-
? [path.join(journalDir, `${date}-${user}.md`), path.join(journalDir, `${date}.md`)]
|
|
699
|
-
: [path.join(journalDir, `${date}.md`)];
|
|
700
|
-
let content = null;
|
|
701
|
-
for (const filePath of candidates) {
|
|
702
|
-
try {
|
|
703
|
-
content = fs.readFileSync(filePath, 'utf8').toLowerCase();
|
|
704
|
-
break;
|
|
705
|
-
} catch {
|
|
706
|
-
// Try next candidate
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
journalCache[cacheKey] = content;
|
|
710
|
-
return content;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const results = [];
|
|
714
|
-
for (const [id, entry] of Object.entries(index.entries)) {
|
|
715
|
-
if (!applyFilters(entry, options)) continue;
|
|
716
|
-
|
|
717
|
-
// Build searchable text from entry metadata (normalize hyphens/underscores)
|
|
718
|
-
let searchable = [
|
|
719
|
-
entry.title,
|
|
720
|
-
entry.repo,
|
|
721
|
-
entry.date,
|
|
722
|
-
entry.user,
|
|
723
|
-
...(entry.tags || []),
|
|
724
|
-
].filter(Boolean).join(' ').toLowerCase().replace(/[-_]/g, ' ');
|
|
725
|
-
|
|
726
|
-
// For signal entries, read content directly from the signal file
|
|
727
|
-
if (entry.source === 'signal') {
|
|
728
|
-
const signalsDir = options.signalsDir || resolveSignalsDir();
|
|
729
|
-
if (signalsDir) {
|
|
730
|
-
// Signal files live at signalsDir/<channel>/<date>.md or signalsDir/<channel>/<owner>/<repo>/<date>.md
|
|
731
|
-
// The repo field tells us the path: "signals/<channel>" or "<owner>/<repo>"
|
|
732
|
-
const repo = entry.repo || '';
|
|
733
|
-
const candidates = [];
|
|
734
|
-
if (repo.startsWith('signals/')) {
|
|
735
|
-
const channel = repo.replace('signals/', '');
|
|
736
|
-
candidates.push(path.join(signalsDir, channel, `${entry.date}.md`));
|
|
737
|
-
candidates.push(path.join(signalsDir, channel, `${entry.date}-summary.md`));
|
|
738
|
-
} else if (repo.includes('/')) {
|
|
739
|
-
// owner/repo format — find which channel it's under
|
|
740
|
-
for (const channel of ['github', 'intercom', 'notion']) {
|
|
741
|
-
candidates.push(path.join(signalsDir, channel, repo, `${entry.date}.md`));
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
for (const fp of candidates) {
|
|
745
|
-
try {
|
|
746
|
-
const content = fs.readFileSync(fp, 'utf8').toLowerCase();
|
|
747
|
-
searchable += ' ' + content.replace(/[-_]/g, ' ');
|
|
748
|
-
break;
|
|
749
|
-
} catch {
|
|
750
|
-
// Try next candidate
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Also include the full journal entry content if available
|
|
757
|
-
const journalContent = entry.source !== 'signal' ? getJournalContent(entry.date, entry.user) : null;
|
|
758
|
-
if (journalContent) {
|
|
759
|
-
// Find this entry's section in the journal file.
|
|
760
|
-
// Try exact match first, then normalize hyphens/spaces for fuzzy match.
|
|
761
|
-
const repoTitle = `${entry.repo} — ${entry.title}`.toLowerCase();
|
|
762
|
-
let idx = journalContent.indexOf(repoTitle);
|
|
763
|
-
if (idx === -1) {
|
|
764
|
-
// Normalize both sides: collapse hyphens, underscores, em-dashes, and extra spaces
|
|
765
|
-
const norm = (s) => s.replace(/[-_\u2014\u2013]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
766
|
-
const normalized = norm(repoTitle);
|
|
767
|
-
// Search through journal headers for a normalized match
|
|
768
|
-
const headerRegex = /\n## (.+)/g;
|
|
769
|
-
let match;
|
|
770
|
-
while ((match = headerRegex.exec(journalContent)) !== null) {
|
|
771
|
-
const headerNorm = norm(match[1]);
|
|
772
|
-
if (headerNorm.includes(normalized) || normalized.includes(headerNorm)) {
|
|
773
|
-
idx = match.index + 1; // skip the \n
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
if (idx !== -1) {
|
|
779
|
-
// Extract from header to next header (or end of file)
|
|
780
|
-
const nextHeader = journalContent.indexOf('\n## ', idx + 1);
|
|
781
|
-
const section = nextHeader !== -1 ? journalContent.slice(idx, nextHeader) : journalContent.slice(idx);
|
|
782
|
-
searchable += ' ' + section.replace(/[-_]/g, ' ');
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Score: count of matching query words
|
|
787
|
-
let matches = 0;
|
|
788
|
-
for (const word of queryWords) {
|
|
789
|
-
if (searchable.includes(word)) matches++;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
if (matches > 0) {
|
|
793
|
-
const score = Math.round((matches / queryWords.length) * 1000) / 1000;
|
|
794
|
-
results.push({ id, score, entry });
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
results.sort((a, b) => b.score - a.score);
|
|
799
|
-
return results.slice(0, limit);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
673
|
/**
|
|
803
674
|
* Apply metadata filters to an entry.
|
|
804
675
|
* @param {Object} entry
|
|
@@ -816,7 +687,7 @@ function applyFilters(entry, filters) {
|
|
|
816
687
|
if (filters.since && entry.date < filters.since) return false;
|
|
817
688
|
if (filters.until && entry.date > filters.until) return false;
|
|
818
689
|
if (filters.drifted !== undefined && entry.drifted !== filters.drifted) return false;
|
|
819
|
-
if (filters.user && entry.user
|
|
690
|
+
if (filters.user && (!entry.user || entry.user.toLowerCase() !== filters.user.toLowerCase())) return false;
|
|
820
691
|
return true;
|
|
821
692
|
}
|
|
822
693
|
|
|
@@ -2480,7 +2351,6 @@ module.exports = {
|
|
|
2480
2351
|
applyContextShiftToState,
|
|
2481
2352
|
generateOnboardingPack,
|
|
2482
2353
|
searchJournals,
|
|
2483
|
-
searchText,
|
|
2484
2354
|
queryMetadata,
|
|
2485
2355
|
extractInsights,
|
|
2486
2356
|
computeQualityProfile,
|
package/bin/mcp-server.js
CHANGED
|
@@ -228,43 +228,32 @@ async function proxyGetEntry(id) {
|
|
|
228
228
|
const TOOLS = [
|
|
229
229
|
{
|
|
230
230
|
name: 'search_context',
|
|
231
|
-
description: 'Search the team\'s
|
|
231
|
+
description: 'Search the team\'s decision history across all repos and engineers. Returns ranked entries. Use mode=browse with since/until for time-range queries ("what happened this week"). Use mode=semantic with a query for topical searches. Pass dates, authors, and repos as explicit parameters.',
|
|
232
232
|
inputSchema: {
|
|
233
233
|
type: 'object',
|
|
234
234
|
properties: {
|
|
235
|
-
query: { type: 'string', description: 'Natural language search query' },
|
|
235
|
+
query: { type: 'string', description: 'Natural language search query (required for semantic mode, optional for browse)' },
|
|
236
236
|
limit: { type: 'number', description: 'Max results (default: 10)' },
|
|
237
237
|
repo: { type: 'string', description: 'Filter by repository name (e.g. "MyService", "MyOrg/my-repo")' },
|
|
238
238
|
since: { type: 'string', description: 'Filter to entries on or after this date (YYYY-MM-DD)' },
|
|
239
|
-
|
|
239
|
+
until: { type: 'string', description: 'Filter to entries on or before this date (YYYY-MM-DD)' },
|
|
240
|
+
user: { type: 'string', description: 'Filter by author slug (lowercase first name, e.g. "nick")' },
|
|
241
|
+
source: { type: 'string', enum: ['journal', 'conversation', 'signal'], description: 'Filter by entry source type' },
|
|
242
|
+
mode: { type: 'string', enum: ['semantic', 'browse'], description: 'Search strategy. semantic (default) uses embeddings for relevance ranking. browse returns entries sorted by date (best for time-range queries).' },
|
|
240
243
|
},
|
|
241
|
-
required: ['query'],
|
|
242
244
|
},
|
|
243
245
|
},
|
|
244
246
|
{
|
|
245
247
|
name: 'get_entry',
|
|
246
|
-
description: 'Retrieve the full content of a specific journal or signal entry by ID. Use the IDs returned by search_context
|
|
248
|
+
description: 'Retrieve the full content of a specific journal or signal entry by ID. Use the IDs returned by search_context.',
|
|
247
249
|
inputSchema: {
|
|
248
250
|
type: 'object',
|
|
249
251
|
properties: {
|
|
250
|
-
id: { type: 'string', description: 'Entry ID from search_context
|
|
252
|
+
id: { type: 'string', description: 'Entry ID from search_context results' },
|
|
251
253
|
},
|
|
252
254
|
required: ['id'],
|
|
253
255
|
},
|
|
254
256
|
},
|
|
255
|
-
{
|
|
256
|
-
name: 'list_recent',
|
|
257
|
-
description: 'List recent journal entries and decisions, optionally filtered by repo or date range. Returns metadata (no full content — use get_entry for that).',
|
|
258
|
-
inputSchema: {
|
|
259
|
-
type: 'object',
|
|
260
|
-
properties: {
|
|
261
|
-
limit: { type: 'number', description: 'Max entries to return (default: 20)' },
|
|
262
|
-
repo: { type: 'string', description: 'Filter by repository name' },
|
|
263
|
-
since: { type: 'string', description: 'Filter to entries on or after this date (YYYY-MM-DD)' },
|
|
264
|
-
source: { type: 'string', enum: ['journal', 'conversation', 'signal'], description: 'Filter by entry source type' },
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
257
|
{
|
|
269
258
|
name: 'get_signals',
|
|
270
259
|
description: 'Retrieve recent signal entries (GitHub activity, Slack summaries, Intercom updates, Notion pages) for a specific channel or all channels.',
|
|
@@ -327,26 +316,53 @@ const TOOLS = [
|
|
|
327
316
|
// ── Tool handlers ────────────────────────────────────────────────────────────
|
|
328
317
|
|
|
329
318
|
async function handleSearchContext(args) {
|
|
330
|
-
const { query, limit = 10, repo, since, mode } = args;
|
|
319
|
+
const { query, limit = 10, repo, since, until, user, source, mode: rawMode } = args;
|
|
320
|
+
|
|
321
|
+
// Auto-switch to browse if no query provided
|
|
322
|
+
const mode = (!query && rawMode !== 'browse') ? 'browse' : (rawMode || 'semantic');
|
|
331
323
|
|
|
332
|
-
//
|
|
333
|
-
if (mode
|
|
334
|
-
const
|
|
324
|
+
// Browse mode — return entries sorted by date (no embeddings needed)
|
|
325
|
+
if (mode === 'browse') {
|
|
326
|
+
const opts = { limit, repo, since, until, user, source };
|
|
327
|
+
|
|
328
|
+
// Try container first
|
|
329
|
+
const containerResult = await proxySearch({ query, limit, repo, since, until, user, source, mode: 'browse' });
|
|
335
330
|
if (containerResult && containerResult.found > 0) {
|
|
336
331
|
containerResult.source = 'container';
|
|
337
332
|
return containerResult;
|
|
338
333
|
}
|
|
334
|
+
|
|
335
|
+
// Fall back to local
|
|
336
|
+
const results = contentStore.queryMetadata(opts);
|
|
337
|
+
const top = results.slice(0, limit);
|
|
338
|
+
return {
|
|
339
|
+
found: results.length,
|
|
340
|
+
showing: top.length,
|
|
341
|
+
source: 'local',
|
|
342
|
+
results: top.map(r => ({
|
|
343
|
+
id: r.id,
|
|
344
|
+
date: r.entry.date,
|
|
345
|
+
repo: r.entry.repo,
|
|
346
|
+
title: r.entry.title,
|
|
347
|
+
source: r.entry.source,
|
|
348
|
+
user: r.entry.user || null,
|
|
349
|
+
tags: r.entry.tags || [],
|
|
350
|
+
summary: r.entry.summary || null,
|
|
351
|
+
})),
|
|
352
|
+
};
|
|
339
353
|
}
|
|
340
354
|
|
|
341
|
-
//
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
} else {
|
|
347
|
-
results = await contentStore.searchJournals(query, opts);
|
|
355
|
+
// Semantic mode — try container first (has embeddings for full team)
|
|
356
|
+
const containerResult = await proxySearch({ query, limit, repo, since, until, user, source, mode });
|
|
357
|
+
if (containerResult && containerResult.found > 0) {
|
|
358
|
+
containerResult.source = 'container';
|
|
359
|
+
return containerResult;
|
|
348
360
|
}
|
|
349
361
|
|
|
362
|
+
// Fall back to local semantic search
|
|
363
|
+
const opts = { limit, repo, since, until, user, source };
|
|
364
|
+
const results = await contentStore.searchJournals(query, opts);
|
|
365
|
+
|
|
350
366
|
if (!results || results.length === 0) {
|
|
351
367
|
return { found: 0, results: [], source: 'local', hint: 'No matches. Try a broader query or check wayfind reindex.' };
|
|
352
368
|
}
|
|
@@ -361,6 +377,7 @@ async function handleSearchContext(args) {
|
|
|
361
377
|
repo: r.entry.repo,
|
|
362
378
|
title: r.entry.title,
|
|
363
379
|
source: r.entry.source,
|
|
380
|
+
user: r.entry.user || null,
|
|
364
381
|
tags: r.entry.tags || [],
|
|
365
382
|
summary: r.entry.summary || null,
|
|
366
383
|
})),
|
|
@@ -397,25 +414,6 @@ async function handleGetEntry(args) {
|
|
|
397
414
|
return { error: `Entry not found: ${id}` };
|
|
398
415
|
}
|
|
399
416
|
|
|
400
|
-
function handleListRecent(args) {
|
|
401
|
-
const { limit = 20, repo, since, source } = args;
|
|
402
|
-
const opts = { limit, repo, since, source };
|
|
403
|
-
const results = contentStore.queryMetadata(opts);
|
|
404
|
-
const top = results.slice(0, limit);
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
total: results.length,
|
|
408
|
-
showing: top.length,
|
|
409
|
-
entries: top.map(r => ({
|
|
410
|
-
id: r.id,
|
|
411
|
-
date: r.entry.date,
|
|
412
|
-
repo: r.entry.repo,
|
|
413
|
-
title: r.entry.title,
|
|
414
|
-
source: r.entry.source,
|
|
415
|
-
tags: r.entry.tags || [],
|
|
416
|
-
})),
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
417
|
|
|
420
418
|
function handleGetSignals(args) {
|
|
421
419
|
const { channel, limit = 20 } = args;
|
|
@@ -590,7 +588,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
590
588
|
switch (name) {
|
|
591
589
|
case 'search_context': result = await handleSearchContext(args); break;
|
|
592
590
|
case 'get_entry': result = await handleGetEntry(args); break;
|
|
593
|
-
case 'list_recent': result = handleListRecent(args); break;
|
|
594
591
|
case 'get_signals': result = handleGetSignals(args); break;
|
|
595
592
|
case 'get_team_status': result = handleGetTeamStatus(args); break;
|
|
596
593
|
case 'get_personas': result = handleGetPersonas(); break;
|