660c523ba4
* fix: shorten MCP server name to prevent tool name length errors (#358) Root cause: Claude Code prefixes MCP tool names with `mcp__plugin_{plugin-name}_{server-name}__` which was 43 chars for `mcp__plugin_claude-mem_claude-mem-search__`. Combined with `progressive_description` (22 chars) this exceeded the 64 char limit. Changes: - Shortened MCP server name from 'claude-mem-search' to 'mem-search' (saves 8 chars, new prefix is 35 chars) - Renamed `progressive_description` tool to `help` (saves 18 chars) - Updated SKILL.md to reference new `help` tool name - Updated internal Server constructor name for consistency All tool names now safely under 64 char limit: - Longest is now `get_batch_observations` at 56 chars total - `help` is only 39 chars total Fixes #358 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor: rename get_batch_observations to get_observations The plural form naturally implies multiple items can be fetched, following WordPress conventions. Simpler and clearer naming. Also saves 6 additional characters for MCP tool name length. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * docs: update all references to renamed MCP tools Updated documentation and code comments to reflect: - progressive_description → help - get_batch_observations → get_observations Files updated: - docs/public/usage/claude-desktop.mdx - docs/public/architecture/worker-service.mdx - src/services/worker/FormattingService.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
171 lines
5.6 KiB
TypeScript
171 lines
5.6 KiB
TypeScript
/**
|
|
* FormattingService - Handles all formatting logic for search results
|
|
* Uses table format matching context-generator style for visual consistency
|
|
*/
|
|
|
|
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
|
|
import { TYPE_ICON_MAP, TYPE_WORK_EMOJI_MAP } from '../../constants/observation-metadata.js';
|
|
|
|
// Token estimation constant (matches context-generator)
|
|
const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
|
|
export class FormattingService {
|
|
/**
|
|
* Format search tips footer
|
|
*/
|
|
formatSearchTips(): string {
|
|
return `\n---
|
|
💡 Search Strategy:
|
|
1. Search with index to see titles, dates, IDs
|
|
2. Use timeline to get context around interesting results
|
|
3. Batch fetch full details: get_observations(ids=[...])
|
|
|
|
Tips:
|
|
• Filter by type: obs_type="bugfix,feature"
|
|
• Filter by date: dateStart="2025-01-01"
|
|
• Sort: orderBy="date_desc" or "date_asc"`;
|
|
}
|
|
|
|
/**
|
|
* Format time from epoch (matches context-generator formatTime)
|
|
*/
|
|
private formatTime(epoch: number): string {
|
|
return new Date(epoch).toLocaleString('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Estimate read tokens for an observation
|
|
*/
|
|
private estimateReadTokens(obs: ObservationSearchResult): number {
|
|
const size = (obs.title?.length || 0) +
|
|
(obs.subtitle?.length || 0) +
|
|
(obs.narrative?.length || 0) +
|
|
(obs.facts?.length || 0);
|
|
return Math.ceil(size / CHARS_PER_TOKEN_ESTIMATE);
|
|
}
|
|
|
|
/**
|
|
* Format observation as table row
|
|
* | ID | Time | T | Title | Read | Work |
|
|
*/
|
|
formatObservationIndex(obs: ObservationSearchResult, _index: number): string {
|
|
const id = `#${obs.id}`;
|
|
const time = this.formatTime(obs.created_at_epoch);
|
|
const icon = TYPE_ICON_MAP[obs.type as keyof typeof TYPE_ICON_MAP] || '•';
|
|
const title = obs.title || 'Untitled';
|
|
const readTokens = this.estimateReadTokens(obs);
|
|
const workEmoji = TYPE_WORK_EMOJI_MAP[obs.type as keyof typeof TYPE_WORK_EMOJI_MAP] || '🔍';
|
|
const workTokens = obs.discovery_tokens || 0;
|
|
const workDisplay = workTokens > 0 ? `${workEmoji} ${workTokens}` : '-';
|
|
|
|
return `| ${id} | ${time} | ${icon} | ${title} | ~${readTokens} | ${workDisplay} |`;
|
|
}
|
|
|
|
/**
|
|
* Format session summary as table row
|
|
* | ID | Time | T | Title | - | - |
|
|
*/
|
|
formatSessionIndex(session: SessionSummarySearchResult, _index: number): string {
|
|
const id = `#S${session.id}`;
|
|
const time = this.formatTime(session.created_at_epoch);
|
|
const icon = '🎯';
|
|
const title = session.request || `Session ${session.sdk_session_id?.substring(0, 8) || 'unknown'}`;
|
|
|
|
return `| ${id} | ${time} | ${icon} | ${title} | - | - |`;
|
|
}
|
|
|
|
/**
|
|
* Format user prompt as table row
|
|
* | ID | Time | T | Title | - | - |
|
|
*/
|
|
formatUserPromptIndex(prompt: UserPromptSearchResult, _index: number): string {
|
|
const id = `#P${prompt.id}`;
|
|
const time = this.formatTime(prompt.created_at_epoch);
|
|
const icon = '💬';
|
|
// Truncate long prompts for table display
|
|
const title = prompt.prompt_text.length > 60
|
|
? prompt.prompt_text.substring(0, 57) + '...'
|
|
: prompt.prompt_text;
|
|
|
|
return `| ${id} | ${time} | ${icon} | ${title} | - | - |`;
|
|
}
|
|
|
|
/**
|
|
* Generate table header for observations
|
|
*/
|
|
formatTableHeader(): string {
|
|
return `| ID | Time | T | Title | Read | Work |
|
|
|-----|------|---|-------|------|------|`;
|
|
}
|
|
|
|
/**
|
|
* Generate table header for search results (no Work column)
|
|
*/
|
|
formatSearchTableHeader(): string {
|
|
return `| ID | Time | T | Title | Read |
|
|
|----|------|---|-------|------|`;
|
|
}
|
|
|
|
/**
|
|
* Format observation as table row for search results (no Work column)
|
|
*/
|
|
formatObservationSearchRow(obs: ObservationSearchResult, lastTime: string): { row: string; time: string } {
|
|
const id = `#${obs.id}`;
|
|
const time = this.formatTime(obs.created_at_epoch);
|
|
const icon = TYPE_ICON_MAP[obs.type as keyof typeof TYPE_ICON_MAP] || '•';
|
|
const title = obs.title || 'Untitled';
|
|
const readTokens = this.estimateReadTokens(obs);
|
|
|
|
// Use ditto mark if same time as previous row
|
|
const timeDisplay = time === lastTime ? '″' : time;
|
|
|
|
return {
|
|
row: `| ${id} | ${timeDisplay} | ${icon} | ${title} | ~${readTokens} |`,
|
|
time
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format session summary as table row for search results (no Work column)
|
|
*/
|
|
formatSessionSearchRow(session: SessionSummarySearchResult, lastTime: string): { row: string; time: string } {
|
|
const id = `#S${session.id}`;
|
|
const time = this.formatTime(session.created_at_epoch);
|
|
const icon = '🎯';
|
|
const title = session.request || `Session ${session.sdk_session_id?.substring(0, 8) || 'unknown'}`;
|
|
|
|
// Use ditto mark if same time as previous row
|
|
const timeDisplay = time === lastTime ? '″' : time;
|
|
|
|
return {
|
|
row: `| ${id} | ${timeDisplay} | ${icon} | ${title} | - |`,
|
|
time
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Format user prompt as table row for search results (no Work column)
|
|
*/
|
|
formatUserPromptSearchRow(prompt: UserPromptSearchResult, lastTime: string): { row: string; time: string } {
|
|
const id = `#P${prompt.id}`;
|
|
const time = this.formatTime(prompt.created_at_epoch);
|
|
const icon = '💬';
|
|
// Truncate long prompts for table display
|
|
const title = prompt.prompt_text.length > 60
|
|
? prompt.prompt_text.substring(0, 57) + '...'
|
|
: prompt.prompt_text;
|
|
|
|
// Use ditto mark if same time as previous row
|
|
const timeDisplay = time === lastTime ? '″' : time;
|
|
|
|
return {
|
|
row: `| ${id} | ${timeDisplay} | ${icon} | ${title} | - |`,
|
|
time
|
|
};
|
|
}
|
|
}
|