lance-context 0.1.0 → 1.1.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/README.md +232 -23
- package/dist/__tests__/ast-chunker.test.d.ts +2 -0
- package/dist/__tests__/ast-chunker.test.d.ts.map +1 -0
- package/dist/__tests__/ast-chunker.test.js +307 -0
- package/dist/__tests__/ast-chunker.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +242 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/dashboard/beads.test.d.ts +2 -0
- package/dist/__tests__/dashboard/beads.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/beads.test.js +151 -0
- package/dist/__tests__/dashboard/beads.test.js.map +1 -0
- package/dist/__tests__/dashboard/index.test.d.ts +2 -0
- package/dist/__tests__/dashboard/index.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/index.test.js +116 -0
- package/dist/__tests__/dashboard/index.test.js.map +1 -0
- package/dist/__tests__/dashboard/routes.test.d.ts +2 -0
- package/dist/__tests__/dashboard/routes.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/routes.test.js +125 -0
- package/dist/__tests__/dashboard/routes.test.js.map +1 -0
- package/dist/__tests__/dashboard/server.test.d.ts +2 -0
- package/dist/__tests__/dashboard/server.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/server.test.js +75 -0
- package/dist/__tests__/dashboard/server.test.js.map +1 -0
- package/dist/__tests__/dashboard/state.test.d.ts +2 -0
- package/dist/__tests__/dashboard/state.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard/state.test.js +124 -0
- package/dist/__tests__/dashboard/state.test.js.map +1 -0
- package/dist/__tests__/embeddings/factory.test.d.ts +2 -0
- package/dist/__tests__/embeddings/factory.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/factory.test.js +100 -0
- package/dist/__tests__/embeddings/factory.test.js.map +1 -0
- package/dist/__tests__/embeddings/jina.test.d.ts +2 -0
- package/dist/__tests__/embeddings/jina.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/jina.test.js +156 -0
- package/dist/__tests__/embeddings/jina.test.js.map +1 -0
- package/dist/__tests__/embeddings/ollama.test.d.ts +2 -0
- package/dist/__tests__/embeddings/ollama.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/ollama.test.js +172 -0
- package/dist/__tests__/embeddings/ollama.test.js.map +1 -0
- package/dist/__tests__/embeddings/rate-limiter.test.d.ts +2 -0
- package/dist/__tests__/embeddings/rate-limiter.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/rate-limiter.test.js +163 -0
- package/dist/__tests__/embeddings/rate-limiter.test.js.map +1 -0
- package/dist/__tests__/embeddings/retry.test.d.ts +2 -0
- package/dist/__tests__/embeddings/retry.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/retry.test.js +260 -0
- package/dist/__tests__/embeddings/retry.test.js.map +1 -0
- package/dist/__tests__/embeddings/types.test.d.ts +2 -0
- package/dist/__tests__/embeddings/types.test.d.ts.map +1 -0
- package/dist/__tests__/embeddings/types.test.js +31 -0
- package/dist/__tests__/embeddings/types.test.js.map +1 -0
- package/dist/__tests__/mocks/embedding-backend.mock.d.ts +10 -0
- package/dist/__tests__/mocks/embedding-backend.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/embedding-backend.mock.js +39 -0
- package/dist/__tests__/mocks/embedding-backend.mock.js.map +1 -0
- package/dist/__tests__/mocks/fetch.mock.d.ts +38 -0
- package/dist/__tests__/mocks/fetch.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/fetch.mock.js +74 -0
- package/dist/__tests__/mocks/fetch.mock.js.map +1 -0
- package/dist/__tests__/mocks/lancedb.mock.d.ts +38 -0
- package/dist/__tests__/mocks/lancedb.mock.d.ts.map +1 -0
- package/dist/__tests__/mocks/lancedb.mock.js +63 -0
- package/dist/__tests__/mocks/lancedb.mock.js.map +1 -0
- package/dist/__tests__/search/clustering.test.d.ts +2 -0
- package/dist/__tests__/search/clustering.test.d.ts.map +1 -0
- package/dist/__tests__/search/clustering.test.js +230 -0
- package/dist/__tests__/search/clustering.test.js.map +1 -0
- package/dist/__tests__/search/hybrid-search.test.d.ts +2 -0
- package/dist/__tests__/search/hybrid-search.test.d.ts.map +1 -0
- package/dist/__tests__/search/hybrid-search.test.js +186 -0
- package/dist/__tests__/search/hybrid-search.test.js.map +1 -0
- package/dist/__tests__/search/indexer.test.d.ts +2 -0
- package/dist/__tests__/search/indexer.test.d.ts.map +1 -0
- package/dist/__tests__/search/indexer.test.js +878 -0
- package/dist/__tests__/search/indexer.test.js.map +1 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.d.ts +2 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.d.ts.map +1 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.js +228 -0
- package/dist/__tests__/search/tree-sitter-chunker.test.js.map +1 -0
- package/dist/__tests__/setup.d.ts +2 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +11 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/__tests__/utils/concurrency.test.d.ts +2 -0
- package/dist/__tests__/utils/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/utils/concurrency.test.js +83 -0
- package/dist/__tests__/utils/concurrency.test.js.map +1 -0
- package/dist/__tests__/utils/errors.test.d.ts +2 -0
- package/dist/__tests__/utils/errors.test.d.ts.map +1 -0
- package/dist/__tests__/utils/errors.test.js +136 -0
- package/dist/__tests__/utils/errors.test.js.map +1 -0
- package/dist/__tests__/utils/type-guards.test.d.ts +2 -0
- package/dist/__tests__/utils/type-guards.test.d.ts.map +1 -0
- package/dist/__tests__/utils/type-guards.test.js +80 -0
- package/dist/__tests__/utils/type-guards.test.js.map +1 -0
- package/dist/__tests__/worktree/worktree-manager.test.d.ts +2 -0
- package/dist/__tests__/worktree/worktree-manager.test.d.ts.map +1 -0
- package/dist/__tests__/worktree/worktree-manager.test.js +403 -0
- package/dist/__tests__/worktree/worktree-manager.test.js.map +1 -0
- package/dist/config.d.ts +122 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +508 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/beads.d.ts +35 -0
- package/dist/dashboard/beads.d.ts.map +1 -0
- package/dist/dashboard/beads.js +102 -0
- package/dist/dashboard/beads.js.map +1 -0
- package/dist/dashboard/events.d.ts +46 -0
- package/dist/dashboard/events.d.ts.map +1 -0
- package/dist/dashboard/events.js +141 -0
- package/dist/dashboard/events.js.map +1 -0
- package/dist/dashboard/index.d.ts +69 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +93 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/routes.d.ts +6 -0
- package/dist/dashboard/routes.d.ts.map +1 -0
- package/dist/dashboard/routes.js +245 -0
- package/dist/dashboard/routes.js.map +1 -0
- package/dist/dashboard/server.d.ts +27 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +72 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/state.d.ts +125 -0
- package/dist/dashboard/state.d.ts.map +1 -0
- package/dist/dashboard/state.js +264 -0
- package/dist/dashboard/state.js.map +1 -0
- package/dist/dashboard/ui.d.ts +6 -0
- package/dist/dashboard/ui.d.ts.map +1 -0
- package/dist/dashboard/ui.js +1421 -0
- package/dist/dashboard/ui.js.map +1 -0
- package/dist/embeddings/index.d.ts +20 -2
- package/dist/embeddings/index.d.ts.map +1 -1
- package/dist/embeddings/index.js +49 -6
- package/dist/embeddings/index.js.map +1 -1
- package/dist/embeddings/jina.d.ts +9 -0
- package/dist/embeddings/jina.d.ts.map +1 -1
- package/dist/embeddings/jina.js +42 -2
- package/dist/embeddings/jina.js.map +1 -1
- package/dist/embeddings/ollama.d.ts +2 -0
- package/dist/embeddings/ollama.d.ts.map +1 -1
- package/dist/embeddings/ollama.js +21 -5
- package/dist/embeddings/ollama.js.map +1 -1
- package/dist/embeddings/rate-limiter.d.ts +75 -0
- package/dist/embeddings/rate-limiter.d.ts.map +1 -0
- package/dist/embeddings/rate-limiter.js +145 -0
- package/dist/embeddings/rate-limiter.js.map +1 -0
- package/dist/embeddings/retry.d.ts +14 -0
- package/dist/embeddings/retry.d.ts.map +1 -0
- package/dist/embeddings/retry.js +89 -0
- package/dist/embeddings/retry.js.map +1 -0
- package/dist/embeddings/types.d.ts +56 -2
- package/dist/embeddings/types.d.ts.map +1 -1
- package/dist/embeddings/types.js +16 -0
- package/dist/embeddings/types.js.map +1 -1
- package/dist/index.js +1871 -44
- package/dist/index.js.map +1 -1
- package/dist/memory/index.d.ts +63 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +168 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/search/ast-chunker.d.ts +34 -0
- package/dist/search/ast-chunker.d.ts.map +1 -0
- package/dist/search/ast-chunker.js +261 -0
- package/dist/search/ast-chunker.js.map +1 -0
- package/dist/search/clustering.d.ts +77 -0
- package/dist/search/clustering.d.ts.map +1 -0
- package/dist/search/clustering.js +455 -0
- package/dist/search/clustering.js.map +1 -0
- package/dist/search/indexer.d.ts +239 -3
- package/dist/search/indexer.d.ts.map +1 -1
- package/dist/search/indexer.js +941 -45
- package/dist/search/indexer.js.map +1 -1
- package/dist/search/tree-sitter-chunker.d.ts +69 -0
- package/dist/search/tree-sitter-chunker.d.ts.map +1 -0
- package/dist/search/tree-sitter-chunker.js +436 -0
- package/dist/search/tree-sitter-chunker.js.map +1 -0
- package/dist/symbols/index.d.ts +14 -0
- package/dist/symbols/index.d.ts.map +1 -0
- package/dist/symbols/index.js +19 -0
- package/dist/symbols/index.js.map +1 -0
- package/dist/symbols/name-path.d.ts +113 -0
- package/dist/symbols/name-path.d.ts.map +1 -0
- package/dist/symbols/name-path.js +194 -0
- package/dist/symbols/name-path.js.map +1 -0
- package/dist/symbols/pattern-search.d.ts +14 -0
- package/dist/symbols/pattern-search.d.ts.map +1 -0
- package/dist/symbols/pattern-search.js +224 -0
- package/dist/symbols/pattern-search.js.map +1 -0
- package/dist/symbols/reference-finder.d.ts +38 -0
- package/dist/symbols/reference-finder.d.ts.map +1 -0
- package/dist/symbols/reference-finder.js +376 -0
- package/dist/symbols/reference-finder.js.map +1 -0
- package/dist/symbols/symbol-editor.d.ts +81 -0
- package/dist/symbols/symbol-editor.d.ts.map +1 -0
- package/dist/symbols/symbol-editor.js +257 -0
- package/dist/symbols/symbol-editor.js.map +1 -0
- package/dist/symbols/symbol-extractor.d.ts +49 -0
- package/dist/symbols/symbol-extractor.d.ts.map +1 -0
- package/dist/symbols/symbol-extractor.js +593 -0
- package/dist/symbols/symbol-extractor.js.map +1 -0
- package/dist/symbols/symbol-renamer.d.ts +81 -0
- package/dist/symbols/symbol-renamer.d.ts.map +1 -0
- package/dist/symbols/symbol-renamer.js +204 -0
- package/dist/symbols/symbol-renamer.js.map +1 -0
- package/dist/symbols/types.d.ts +234 -0
- package/dist/symbols/types.d.ts.map +1 -0
- package/dist/symbols/types.js +106 -0
- package/dist/symbols/types.js.map +1 -0
- package/dist/utils/concurrency.d.ts +32 -0
- package/dist/utils/concurrency.d.ts.map +1 -0
- package/dist/utils/concurrency.js +57 -0
- package/dist/utils/concurrency.js.map +1 -0
- package/dist/utils/errors.d.ts +36 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +91 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/type-guards.d.ts +17 -0
- package/dist/utils/type-guards.d.ts.map +1 -0
- package/dist/utils/type-guards.js +25 -0
- package/dist/utils/type-guards.js.map +1 -0
- package/dist/worktree/index.d.ts +6 -0
- package/dist/worktree/index.d.ts.map +1 -0
- package/dist/worktree/index.js +6 -0
- package/dist/worktree/index.js.map +1 -0
- package/dist/worktree/types.d.ts +101 -0
- package/dist/worktree/types.d.ts.map +1 -0
- package/dist/worktree/types.js +6 -0
- package/dist/worktree/types.js.map +1 -0
- package/dist/worktree/worktree-manager.d.ts +80 -0
- package/dist/worktree/worktree-manager.d.ts.map +1 -0
- package/dist/worktree/worktree-manager.js +407 -0
- package/dist/worktree/worktree-manager.js.map +1 -0
- package/package.json +39 -5
- package/scripts/postinstall.js +48 -0
package/dist/index.js
CHANGED
|
@@ -1,82 +1,860 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const packageJson = require('../package.json');
|
|
5
13
|
import { createEmbeddingBackend } from './embeddings/index.js';
|
|
6
14
|
import { CodeIndexer } from './search/indexer.js';
|
|
15
|
+
import { isStringArray, isString, isNumber, isBoolean } from './utils/type-guards.js';
|
|
16
|
+
import { logError, formatErrorResponse, wrapError, LanceContextError } from './utils/errors.js';
|
|
17
|
+
import { loadConfig, loadSecrets, getInstructions, getDashboardConfig } from './config.js';
|
|
18
|
+
import { startDashboard, stopDashboard, dashboardState, isPortAvailable, } from './dashboard/index.js';
|
|
19
|
+
// Symbolic analysis imports
|
|
20
|
+
import { SymbolExtractor, searchForPattern, formatPatternSearchResults, ReferenceFinder, formatReferencesResult, SymbolEditor, SymbolRenamer, formatRenameResult, SymbolKindNames, parseNamePath, matchNamePath, formatNamePath, } from './symbols/index.js';
|
|
21
|
+
import { MemoryManager, formatMemoryList } from './memory/index.js';
|
|
22
|
+
import { WorktreeManager, formatWorktreeInfo, formatWorktreeList } from './worktree/index.js';
|
|
23
|
+
/**
|
|
24
|
+
* Check if browser was recently opened (within the last hour)
|
|
25
|
+
*/
|
|
26
|
+
function wasBrowserRecentlyOpened(projectPath) {
|
|
27
|
+
const flagFile = path.join(projectPath, '.lance-context', 'browser-opened');
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(flagFile)) {
|
|
30
|
+
const timestamp = parseInt(fs.readFileSync(flagFile, 'utf-8'), 10);
|
|
31
|
+
const oneHourAgo = Date.now() - 60 * 60 * 1000;
|
|
32
|
+
return timestamp > oneHourAgo;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Ignore errors
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Record that browser was opened
|
|
42
|
+
*/
|
|
43
|
+
function recordBrowserOpened(projectPath) {
|
|
44
|
+
const flagFile = path.join(projectPath, '.lance-context', 'browser-opened');
|
|
45
|
+
try {
|
|
46
|
+
fs.writeFileSync(flagFile, Date.now().toString());
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Open a URL in the user's default browser (cross-platform)
|
|
54
|
+
*/
|
|
55
|
+
function openBrowser(url, projectPath) {
|
|
56
|
+
// Don't open if already opened recently
|
|
57
|
+
if (wasBrowserRecentlyOpened(projectPath)) {
|
|
58
|
+
console.error('[lance-context] Dashboard was recently opened, skipping');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const platform = process.platform;
|
|
62
|
+
let command;
|
|
63
|
+
switch (platform) {
|
|
64
|
+
case 'darwin':
|
|
65
|
+
command = `open "${url}"`;
|
|
66
|
+
break;
|
|
67
|
+
case 'win32':
|
|
68
|
+
command = `start "" "${url}"`;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
// Linux and others
|
|
72
|
+
command = `xdg-open "${url}"`;
|
|
73
|
+
}
|
|
74
|
+
exec(command, (error) => {
|
|
75
|
+
if (error) {
|
|
76
|
+
console.error('[lance-context] Failed to open browser:', error.message);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
recordBrowserOpened(projectPath);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
7
83
|
const PROJECT_PATH = process.env.LANCE_CONTEXT_PROJECT || process.cwd();
|
|
8
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Brief guidance appended to tool responses to reinforce tool selection preferences.
|
|
86
|
+
*/
|
|
87
|
+
const TOOL_GUIDANCE = `
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
**Tip:** Prefer lance-context's \`search_code\` over pattern-based tools (grep, find_symbol) when exploring code or unsure of exact names.`;
|
|
91
|
+
/**
|
|
92
|
+
* Server instructions provided at MCP initialization.
|
|
93
|
+
* These guide Claude on when to use lance-context tools vs alternatives.
|
|
94
|
+
*/
|
|
95
|
+
const SERVER_INSTRUCTIONS = `# lance-context - Semantic Code Search & Symbol Analysis
|
|
96
|
+
|
|
97
|
+
## When to Use lance-context Tools
|
|
98
|
+
|
|
99
|
+
**PREFER lance-context tools** over pattern-based alternatives (grep, find) for code exploration:
|
|
100
|
+
|
|
101
|
+
| Task | Use lance-context | Instead of |
|
|
102
|
+
|------|-------------------|------------|
|
|
103
|
+
| Find code by concept | \`search_code\` | grep with regex |
|
|
104
|
+
| Unsure of exact names | \`search_code\` | wildcards, substring matching |
|
|
105
|
+
| Explore unfamiliar code | \`search_code\` | multiple grep/find attempts |
|
|
106
|
+
| Find similar patterns | \`search_similar\` | manual comparison |
|
|
107
|
+
| Commit changes | \`commit\` | git commit |
|
|
108
|
+
| Understand file structure | \`get_symbols_overview\` | reading entire file |
|
|
109
|
+
| Find specific function/class | \`find_symbol\` | grep for definition |
|
|
110
|
+
| Find symbol usages | \`find_referencing_symbols\` | grep for name |
|
|
111
|
+
| Search with regex | \`search_for_pattern\` | grep/rg |
|
|
112
|
+
|
|
113
|
+
## Tool Categories
|
|
114
|
+
|
|
115
|
+
### Semantic Search
|
|
116
|
+
- **search_code**: Natural language code search. One call replaces multiple pattern searches.
|
|
117
|
+
- **search_similar**: Find duplicate/related code patterns.
|
|
118
|
+
|
|
119
|
+
### Symbol Navigation
|
|
120
|
+
- **get_symbols_overview**: List all symbols in a file grouped by kind (Class, Function, etc.)
|
|
121
|
+
- **find_symbol**: Search by name path pattern (e.g., "MyClass/myMethod", "get*")
|
|
122
|
+
- **find_referencing_symbols**: Find all usages of a symbol across codebase
|
|
123
|
+
- **search_for_pattern**: Regex search with context lines
|
|
124
|
+
|
|
125
|
+
### Symbol Editing
|
|
126
|
+
- **replace_symbol_body**: Replace entire symbol definition
|
|
127
|
+
- **insert_before_symbol**: Add code before a symbol
|
|
128
|
+
- **insert_after_symbol**: Add code after a symbol
|
|
129
|
+
- **rename_symbol**: Rename symbol across entire codebase
|
|
130
|
+
|
|
131
|
+
### Memory (Project Context)
|
|
132
|
+
- **write_memory**: Save project-specific notes/decisions
|
|
133
|
+
- **read_memory**: Retrieve saved context
|
|
134
|
+
- **list_memories**: See available memories
|
|
135
|
+
- **edit_memory**: Find/replace in memory
|
|
136
|
+
- **delete_memory**: Remove a memory
|
|
137
|
+
|
|
138
|
+
### Git & Index
|
|
139
|
+
- **commit**: Git commit with validation (feature branch, message format)
|
|
140
|
+
- **index_codebase**: Build/update the search index
|
|
141
|
+
- **get_index_status**: Check if index is ready
|
|
142
|
+
|
|
143
|
+
## CRITICAL: Always Use the \`commit\` Tool
|
|
144
|
+
|
|
145
|
+
**NEVER use raw \`git commit\`** - ALWAYS use the \`commit\` MCP tool.
|
|
146
|
+
|
|
147
|
+
The \`commit\` tool:
|
|
148
|
+
- Validates you're on a feature branch (not main)
|
|
149
|
+
- Checks message format (≤72 chars, imperative mood)
|
|
150
|
+
- Enforces single responsibility per commit
|
|
151
|
+
- Prevents common mistakes
|
|
152
|
+
|
|
153
|
+
A post-commit hook will warn if commits bypass this tool.
|
|
154
|
+
|
|
155
|
+
## Signs You Should Have Used search_code
|
|
156
|
+
|
|
157
|
+
- You used wildcards or regex alternation
|
|
158
|
+
- Multiple search attempts to find something
|
|
159
|
+
- Pattern-based search returned nothing
|
|
160
|
+
- Searching for a concept, not an exact identifier`;
|
|
161
|
+
/**
|
|
162
|
+
* Priority instructions returned by get_project_instructions
|
|
163
|
+
*/
|
|
164
|
+
const PRIORITY_INSTRUCTIONS = `
|
|
165
|
+
## Tool Selection Priority
|
|
166
|
+
|
|
167
|
+
**ALWAYS prefer lance-context tools** over other code exploration tools (Serena, grep, find):
|
|
168
|
+
|
|
169
|
+
1. **search_code** - Use for ANY code exploration:
|
|
170
|
+
- Finding code by concept ("error handling", "authentication")
|
|
171
|
+
- When unsure of exact names or locations
|
|
172
|
+
- When you would otherwise use wildcards or regex
|
|
173
|
+
- If your first search attempt fails
|
|
174
|
+
|
|
175
|
+
2. **search_similar** - Finding related/duplicate code patterns
|
|
176
|
+
|
|
177
|
+
3. **get_index_status** - Check if index is ready before searching
|
|
178
|
+
|
|
179
|
+
4. **commit** - **ALWAYS use instead of \`git commit\`** (MANDATORY):
|
|
180
|
+
- NEVER run raw \`git commit\` - always use this tool
|
|
181
|
+
- Validates you're on a feature branch (not main)
|
|
182
|
+
- Checks message format (≤72 chars, imperative mood)
|
|
183
|
+
- Enforces single responsibility per commit
|
|
184
|
+
- Auto-appends Co-Authored-By trailer
|
|
185
|
+
|
|
186
|
+
**Signs you should have used search_code:**
|
|
187
|
+
- You used wildcards or regex alternation (e.g., \`foo|bar\`)
|
|
188
|
+
- You made multiple search calls to find something
|
|
189
|
+
- You searched for a partial name with substring matching
|
|
190
|
+
- Your pattern-based search returned nothing
|
|
191
|
+
|
|
192
|
+
`;
|
|
193
|
+
// Package version - read from package.json
|
|
194
|
+
const PACKAGE_VERSION = packageJson.version;
|
|
195
|
+
/**
|
|
196
|
+
* Check for updates from npm registry (non-blocking).
|
|
197
|
+
* Logs a warning if a newer version is available.
|
|
198
|
+
*/
|
|
199
|
+
async function checkForUpdates() {
|
|
200
|
+
try {
|
|
201
|
+
// Use npm view command to get latest version
|
|
202
|
+
const { stdout } = await execAsync('npm view lance-context version 2>/dev/null', {
|
|
203
|
+
timeout: 5000, // 5 second timeout
|
|
204
|
+
});
|
|
205
|
+
const latestVersion = stdout.trim();
|
|
206
|
+
if (latestVersion && latestVersion !== PACKAGE_VERSION) {
|
|
207
|
+
// Simple semver comparison: split and compare major.minor.patch
|
|
208
|
+
const current = PACKAGE_VERSION.split('.').map(Number);
|
|
209
|
+
const latest = latestVersion.split('.').map(Number);
|
|
210
|
+
const isOutdated = latest[0] > current[0] ||
|
|
211
|
+
(latest[0] === current[0] && latest[1] > current[1]) ||
|
|
212
|
+
(latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]);
|
|
213
|
+
if (isOutdated) {
|
|
214
|
+
console.error(`[lance-context] Update available: ${PACKAGE_VERSION} → ${latestVersion}`);
|
|
215
|
+
console.error('[lance-context] Run: npm install -g lance-context@latest');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Silently ignore update check failures (network issues, npm not available, etc.)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
let indexerPromise = null;
|
|
224
|
+
let configPromise = null;
|
|
225
|
+
async function getConfig() {
|
|
226
|
+
if (!configPromise) {
|
|
227
|
+
configPromise = loadConfig(PROJECT_PATH);
|
|
228
|
+
}
|
|
229
|
+
return configPromise;
|
|
230
|
+
}
|
|
9
231
|
async function getIndexer() {
|
|
10
|
-
if (!
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
232
|
+
if (!indexerPromise) {
|
|
233
|
+
indexerPromise = (async () => {
|
|
234
|
+
// Load config and secrets to configure embedding backend
|
|
235
|
+
const config = await getConfig();
|
|
236
|
+
const secrets = await loadSecrets(PROJECT_PATH);
|
|
237
|
+
const backend = await createEmbeddingBackend({
|
|
238
|
+
backend: config.embedding?.backend,
|
|
239
|
+
apiKey: secrets.jinaApiKey,
|
|
240
|
+
});
|
|
241
|
+
const idx = new CodeIndexer(PROJECT_PATH, backend);
|
|
242
|
+
await idx.initialize();
|
|
243
|
+
// Share indexer and config with dashboard state
|
|
244
|
+
dashboardState.setIndexer(idx);
|
|
245
|
+
dashboardState.setConfig(config);
|
|
246
|
+
dashboardState.setProjectPath(PROJECT_PATH);
|
|
247
|
+
return idx;
|
|
248
|
+
})();
|
|
14
249
|
}
|
|
15
|
-
return
|
|
250
|
+
return indexerPromise;
|
|
16
251
|
}
|
|
17
252
|
const server = new Server({
|
|
18
253
|
name: 'lance-context',
|
|
19
|
-
version:
|
|
254
|
+
version: PACKAGE_VERSION,
|
|
20
255
|
}, {
|
|
21
256
|
capabilities: {
|
|
22
257
|
tools: {},
|
|
23
258
|
},
|
|
259
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
24
260
|
});
|
|
25
261
|
// List available tools
|
|
26
262
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
27
263
|
return {
|
|
28
264
|
tools: [
|
|
29
265
|
{
|
|
30
|
-
name: 'index_codebase',
|
|
31
|
-
description: 'Index the codebase for semantic search. Creates vector embeddings of all code files.',
|
|
266
|
+
name: 'index_codebase',
|
|
267
|
+
description: 'Index the codebase for semantic search. Creates vector embeddings of all code files. Supports incremental indexing - only changed files are re-indexed unless forceReindex is true.',
|
|
268
|
+
inputSchema: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
patterns: {
|
|
272
|
+
type: 'array',
|
|
273
|
+
items: { type: 'string' },
|
|
274
|
+
description: 'Glob patterns for files to index (default: common code files)',
|
|
275
|
+
},
|
|
276
|
+
excludePatterns: {
|
|
277
|
+
type: 'array',
|
|
278
|
+
items: { type: 'string' },
|
|
279
|
+
description: 'Glob patterns for files to exclude (default: node_modules, dist, .git)',
|
|
280
|
+
},
|
|
281
|
+
forceReindex: {
|
|
282
|
+
type: 'boolean',
|
|
283
|
+
description: 'Force a full reindex, ignoring cached file modification times (default: false)',
|
|
284
|
+
},
|
|
285
|
+
autoRepair: {
|
|
286
|
+
type: 'boolean',
|
|
287
|
+
description: 'Automatically repair a corrupted index by forcing a full reindex (default: false)',
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'search_code',
|
|
294
|
+
description: 'Search the codebase using natural language. Returns relevant code snippets. PREFER THIS TOOL when: (1) you are unsure of exact symbol/function names, (2) you would need wildcards or regex to find something, (3) you are exploring code by concept rather than exact identifier, (4) your first search attempt failed or returned nothing. Semantic search handles name uncertainty naturally - one call here replaces multiple pattern-based searches.',
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: {
|
|
298
|
+
query: {
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: 'Natural language query to search for',
|
|
301
|
+
},
|
|
302
|
+
limit: {
|
|
303
|
+
type: 'number',
|
|
304
|
+
description: 'Maximum number of results (default: 10)',
|
|
305
|
+
},
|
|
306
|
+
pathPattern: {
|
|
307
|
+
type: 'string',
|
|
308
|
+
description: "Glob pattern to filter results by file path (e.g., 'src/**/*.ts', '!**/*.test.ts')",
|
|
309
|
+
},
|
|
310
|
+
languages: {
|
|
311
|
+
type: 'array',
|
|
312
|
+
items: { type: 'string' },
|
|
313
|
+
description: "Filter results to specific languages (e.g., ['typescript', 'javascript'])",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
required: ['query'],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'get_index_status',
|
|
321
|
+
description: 'Get the current status of the code index.',
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: 'clear_index',
|
|
329
|
+
description: 'Clear the code index.',
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'get_project_instructions',
|
|
337
|
+
description: 'Get project-specific instructions from the .lance-context.json config file. Returns instructions for how to work with this codebase.',
|
|
338
|
+
inputSchema: {
|
|
339
|
+
type: 'object',
|
|
340
|
+
properties: {},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
name: 'search_similar',
|
|
345
|
+
description: 'Find code semantically similar to a given code snippet or file location. Useful for finding duplicate logic, similar implementations, or related code patterns.',
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
filepath: {
|
|
350
|
+
type: 'string',
|
|
351
|
+
description: 'File path (relative to project root) to find similar code for. Use with startLine/endLine to specify a range.',
|
|
352
|
+
},
|
|
353
|
+
startLine: {
|
|
354
|
+
type: 'number',
|
|
355
|
+
description: 'Starting line number (1-indexed). Requires filepath.',
|
|
356
|
+
},
|
|
357
|
+
endLine: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: 'Ending line number (1-indexed). Requires filepath.',
|
|
360
|
+
},
|
|
361
|
+
code: {
|
|
362
|
+
type: 'string',
|
|
363
|
+
description: 'Code snippet to find similar code for. Alternative to filepath - provide either code or filepath, not both.',
|
|
364
|
+
},
|
|
365
|
+
limit: {
|
|
366
|
+
type: 'number',
|
|
367
|
+
description: 'Maximum number of results (default: 10)',
|
|
368
|
+
},
|
|
369
|
+
threshold: {
|
|
370
|
+
type: 'number',
|
|
371
|
+
description: 'Minimum similarity score 0-1 (default: 0)',
|
|
372
|
+
},
|
|
373
|
+
excludeSelf: {
|
|
374
|
+
type: 'boolean',
|
|
375
|
+
description: 'Exclude the source chunk from results (default: true)',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: 'summarize_codebase',
|
|
382
|
+
description: 'Generate a comprehensive summary of the codebase including file statistics, language distribution, and discovered concept areas. Uses k-means clustering on embeddings to identify related code groups.',
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: 'object',
|
|
385
|
+
properties: {
|
|
386
|
+
numClusters: {
|
|
387
|
+
type: 'number',
|
|
388
|
+
description: 'Target number of concept clusters (default: auto-determined based on codebase size)',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: 'list_concepts',
|
|
395
|
+
description: 'List all discovered concept clusters in the codebase. Each cluster represents a semantic grouping of related code (e.g., authentication, database, API handlers). Returns cluster labels, sizes, and representative code chunks.',
|
|
396
|
+
inputSchema: {
|
|
397
|
+
type: 'object',
|
|
398
|
+
properties: {
|
|
399
|
+
forceRecluster: {
|
|
400
|
+
type: 'boolean',
|
|
401
|
+
description: 'Force reclustering even if cached results exist (default: false)',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: 'search_by_concept',
|
|
408
|
+
description: 'Search for code within a specific concept cluster. Use list_concepts first to discover available clusters and their IDs. Can optionally combine with a semantic query to search within the cluster.',
|
|
409
|
+
inputSchema: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {
|
|
412
|
+
conceptId: {
|
|
413
|
+
type: 'number',
|
|
414
|
+
description: 'The cluster ID to search within (from list_concepts)',
|
|
415
|
+
},
|
|
416
|
+
query: {
|
|
417
|
+
type: 'string',
|
|
418
|
+
description: 'Optional semantic query to search within the cluster. If not provided, returns representative chunks.',
|
|
419
|
+
},
|
|
420
|
+
limit: {
|
|
421
|
+
type: 'number',
|
|
422
|
+
description: 'Maximum number of results (default: 10)',
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
required: ['conceptId'],
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'commit',
|
|
430
|
+
description: 'Create a git commit with validation. USE THIS TOOL instead of running git commit directly. This tool enforces project commit rules: (1) validates you are on a feature branch (not main), (2) checks commit message format (<=72 chars, imperative mood, single responsibility), (3) returns commit rules as a reminder. Prevents common mistakes like committing to main or multi-responsibility commits.',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
message: {
|
|
435
|
+
type: 'string',
|
|
436
|
+
description: 'The commit message. Must be <=72 characters, imperative mood, single responsibility.',
|
|
437
|
+
},
|
|
438
|
+
files: {
|
|
439
|
+
type: 'array',
|
|
440
|
+
items: { type: 'string' },
|
|
441
|
+
description: 'Files to stage before committing. If not provided, commits already-staged files.',
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
required: ['message'],
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
// --- Symbolic Analysis Tools ---
|
|
448
|
+
{
|
|
449
|
+
name: 'get_symbols_overview',
|
|
450
|
+
description: "Get a high-level overview of code symbols in a file. Returns symbols grouped by kind (Class, Function, Method, etc.) in a compact format. Use this to understand a file's structure before diving into specific symbols.",
|
|
451
|
+
inputSchema: {
|
|
452
|
+
type: 'object',
|
|
453
|
+
properties: {
|
|
454
|
+
relative_path: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: 'The relative path to the file to analyze.',
|
|
457
|
+
},
|
|
458
|
+
depth: {
|
|
459
|
+
type: 'number',
|
|
460
|
+
description: 'Depth of descendants to retrieve (0 = top-level only). Default: 0.',
|
|
461
|
+
},
|
|
462
|
+
max_answer_chars: {
|
|
463
|
+
type: 'number',
|
|
464
|
+
description: 'Maximum response size in characters. Default: 50000.',
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
required: ['relative_path'],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
name: 'find_symbol',
|
|
472
|
+
description: 'Find symbols by name path pattern. Supports: (1) simple name "myFunction", (2) relative path "MyClass/myMethod", (3) absolute path "/MyClass/myMethod", (4) glob pattern "get*" with substring_matching. Returns symbol locations and optionally their source code body.',
|
|
473
|
+
inputSchema: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: {
|
|
476
|
+
name_path_pattern: {
|
|
477
|
+
type: 'string',
|
|
478
|
+
description: 'The name path pattern to search for (e.g., "MyClass/myMethod", "get*").',
|
|
479
|
+
},
|
|
480
|
+
relative_path: {
|
|
481
|
+
type: 'string',
|
|
482
|
+
description: 'Optional. Restrict search to this file or directory.',
|
|
483
|
+
},
|
|
484
|
+
depth: {
|
|
485
|
+
type: 'number',
|
|
486
|
+
description: 'Depth of descendants to retrieve (e.g., 1 for class methods). Default: 0.',
|
|
487
|
+
},
|
|
488
|
+
include_body: {
|
|
489
|
+
type: 'boolean',
|
|
490
|
+
description: "Whether to include the symbol's source code. Default: false.",
|
|
491
|
+
},
|
|
492
|
+
include_info: {
|
|
493
|
+
type: 'boolean',
|
|
494
|
+
description: 'Whether to include additional info (docstring, signature). Default: false.',
|
|
495
|
+
},
|
|
496
|
+
substring_matching: {
|
|
497
|
+
type: 'boolean',
|
|
498
|
+
description: 'If true, use substring matching for the last element of the pattern. Default: false.',
|
|
499
|
+
},
|
|
500
|
+
include_kinds: {
|
|
501
|
+
type: 'array',
|
|
502
|
+
items: { type: 'number' },
|
|
503
|
+
description: 'LSP symbol kind integers to include. If not provided, all kinds are included.',
|
|
504
|
+
},
|
|
505
|
+
exclude_kinds: {
|
|
506
|
+
type: 'array',
|
|
507
|
+
items: { type: 'number' },
|
|
508
|
+
description: 'LSP symbol kind integers to exclude. Takes precedence over include_kinds.',
|
|
509
|
+
},
|
|
510
|
+
max_answer_chars: {
|
|
511
|
+
type: 'number',
|
|
512
|
+
description: 'Maximum response size in characters. Default: 50000.',
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
required: ['name_path_pattern'],
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: 'find_referencing_symbols',
|
|
520
|
+
description: 'Find all references to a symbol across the codebase. Returns code snippets showing where the symbol is used.',
|
|
521
|
+
inputSchema: {
|
|
522
|
+
type: 'object',
|
|
523
|
+
properties: {
|
|
524
|
+
name_path: {
|
|
525
|
+
type: 'string',
|
|
526
|
+
description: 'Name path of the symbol to find references for (e.g., "MyClass/myMethod").',
|
|
527
|
+
},
|
|
528
|
+
relative_path: {
|
|
529
|
+
type: 'string',
|
|
530
|
+
description: 'The relative path to the file containing the symbol.',
|
|
531
|
+
},
|
|
532
|
+
include_info: {
|
|
533
|
+
type: 'boolean',
|
|
534
|
+
description: 'Whether to include additional info about referencing symbols. Default: false.',
|
|
535
|
+
},
|
|
536
|
+
include_kinds: {
|
|
537
|
+
type: 'array',
|
|
538
|
+
items: { type: 'number' },
|
|
539
|
+
description: 'LSP symbol kind integers to include.',
|
|
540
|
+
},
|
|
541
|
+
exclude_kinds: {
|
|
542
|
+
type: 'array',
|
|
543
|
+
items: { type: 'number' },
|
|
544
|
+
description: 'LSP symbol kind integers to exclude.',
|
|
545
|
+
},
|
|
546
|
+
max_answer_chars: {
|
|
547
|
+
type: 'number',
|
|
548
|
+
description: 'Maximum response size in characters. Default: 50000.',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
required: ['name_path', 'relative_path'],
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: 'search_for_pattern',
|
|
556
|
+
description: 'Search for a regex pattern in the codebase. Returns matched lines with optional context. Useful for finding code patterns, TODO comments, specific strings, etc.',
|
|
557
|
+
inputSchema: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
properties: {
|
|
560
|
+
substring_pattern: {
|
|
561
|
+
type: 'string',
|
|
562
|
+
description: 'Regular expression pattern to search for.',
|
|
563
|
+
},
|
|
564
|
+
relative_path: {
|
|
565
|
+
type: 'string',
|
|
566
|
+
description: 'Restrict search to this file or directory. Default: entire project.',
|
|
567
|
+
},
|
|
568
|
+
restrict_search_to_code_files: {
|
|
569
|
+
type: 'boolean',
|
|
570
|
+
description: 'Only search in code files (not config, docs). Default: false.',
|
|
571
|
+
},
|
|
572
|
+
paths_include_glob: {
|
|
573
|
+
type: 'string',
|
|
574
|
+
description: 'Glob pattern for files to include (e.g., "*.py", "src/**/*.ts").',
|
|
575
|
+
},
|
|
576
|
+
paths_exclude_glob: {
|
|
577
|
+
type: 'string',
|
|
578
|
+
description: 'Glob pattern for files to exclude (e.g., "*test*", "**/*_generated.py").',
|
|
579
|
+
},
|
|
580
|
+
context_lines_before: {
|
|
581
|
+
type: 'number',
|
|
582
|
+
description: 'Number of context lines before each match. Default: 0.',
|
|
583
|
+
},
|
|
584
|
+
context_lines_after: {
|
|
585
|
+
type: 'number',
|
|
586
|
+
description: 'Number of context lines after each match. Default: 0.',
|
|
587
|
+
},
|
|
588
|
+
max_answer_chars: {
|
|
589
|
+
type: 'number',
|
|
590
|
+
description: 'Maximum response size in characters. Default: 50000.',
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
required: ['substring_pattern'],
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
// --- Memory Tools ---
|
|
597
|
+
{
|
|
598
|
+
name: 'write_memory',
|
|
599
|
+
description: 'Write information about this project to a named memory file. Memories persist across sessions and can be read later. Useful for storing architectural decisions, patterns, or project-specific context.',
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: 'object',
|
|
602
|
+
properties: {
|
|
603
|
+
memory_file_name: {
|
|
604
|
+
type: 'string',
|
|
605
|
+
description: 'The name of the memory (will be saved as .md file).',
|
|
606
|
+
},
|
|
607
|
+
content: {
|
|
608
|
+
type: 'string',
|
|
609
|
+
description: 'The markdown content to write to the memory.',
|
|
610
|
+
},
|
|
611
|
+
max_answer_chars: {
|
|
612
|
+
type: 'number',
|
|
613
|
+
description: 'Maximum response size in characters. Default: 50000.',
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
required: ['memory_file_name', 'content'],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
name: 'read_memory',
|
|
621
|
+
description: 'Read the content of a memory file. Only read memories that are relevant to the current task.',
|
|
622
|
+
inputSchema: {
|
|
623
|
+
type: 'object',
|
|
624
|
+
properties: {
|
|
625
|
+
memory_file_name: {
|
|
626
|
+
type: 'string',
|
|
627
|
+
description: 'The name of the memory to read.',
|
|
628
|
+
},
|
|
629
|
+
max_answer_chars: {
|
|
630
|
+
type: 'number',
|
|
631
|
+
description: 'Maximum response size in characters. Default: 50000.',
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
required: ['memory_file_name'],
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: 'list_memories',
|
|
639
|
+
description: 'List all available memory files for this project. Use this to discover what project context has been saved.',
|
|
640
|
+
inputSchema: {
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: 'delete_memory',
|
|
647
|
+
description: 'Delete a memory file. Only delete memories when explicitly requested by the user.',
|
|
648
|
+
inputSchema: {
|
|
649
|
+
type: 'object',
|
|
650
|
+
properties: {
|
|
651
|
+
memory_file_name: {
|
|
652
|
+
type: 'string',
|
|
653
|
+
description: 'The name of the memory to delete.',
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
required: ['memory_file_name'],
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: 'edit_memory',
|
|
661
|
+
description: 'Edit a memory file using find/replace. Supports both literal string and regex replacement.',
|
|
662
|
+
inputSchema: {
|
|
663
|
+
type: 'object',
|
|
664
|
+
properties: {
|
|
665
|
+
memory_file_name: {
|
|
666
|
+
type: 'string',
|
|
667
|
+
description: 'The name of the memory to edit.',
|
|
668
|
+
},
|
|
669
|
+
needle: {
|
|
670
|
+
type: 'string',
|
|
671
|
+
description: 'The string or regex pattern to search for.',
|
|
672
|
+
},
|
|
673
|
+
repl: {
|
|
674
|
+
type: 'string',
|
|
675
|
+
description: 'The replacement string.',
|
|
676
|
+
},
|
|
677
|
+
mode: {
|
|
678
|
+
type: 'string',
|
|
679
|
+
enum: ['literal', 'regex'],
|
|
680
|
+
description: 'How to interpret the needle: "literal" for exact match, "regex" for regex pattern.',
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
required: ['memory_file_name', 'needle', 'repl', 'mode'],
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
// --- Symbol Editing Tools ---
|
|
687
|
+
{
|
|
688
|
+
name: 'replace_symbol_body',
|
|
689
|
+
description: 'Replace the entire body of a symbol (function, class, method, etc.) with new content. Use this for significant rewrites of symbol definitions.',
|
|
690
|
+
inputSchema: {
|
|
691
|
+
type: 'object',
|
|
692
|
+
properties: {
|
|
693
|
+
name_path: {
|
|
694
|
+
type: 'string',
|
|
695
|
+
description: 'Name path of the symbol to replace (e.g., "MyClass/myMethod").',
|
|
696
|
+
},
|
|
697
|
+
relative_path: {
|
|
698
|
+
type: 'string',
|
|
699
|
+
description: 'The relative path to the file containing the symbol.',
|
|
700
|
+
},
|
|
701
|
+
body: {
|
|
702
|
+
type: 'string',
|
|
703
|
+
description: 'The new body content for the symbol (including signature line for functions).',
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
required: ['name_path', 'relative_path', 'body'],
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: 'insert_before_symbol',
|
|
711
|
+
description: 'Insert code before a symbol definition. Useful for adding new functions, classes, or imports before an existing symbol.',
|
|
712
|
+
inputSchema: {
|
|
713
|
+
type: 'object',
|
|
714
|
+
properties: {
|
|
715
|
+
name_path: {
|
|
716
|
+
type: 'string',
|
|
717
|
+
description: 'Name path of the symbol to insert before.',
|
|
718
|
+
},
|
|
719
|
+
relative_path: {
|
|
720
|
+
type: 'string',
|
|
721
|
+
description: 'The relative path to the file containing the symbol.',
|
|
722
|
+
},
|
|
723
|
+
body: {
|
|
724
|
+
type: 'string',
|
|
725
|
+
description: 'The content to insert before the symbol.',
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
required: ['name_path', 'relative_path', 'body'],
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: 'insert_after_symbol',
|
|
733
|
+
description: 'Insert code after a symbol definition. Useful for adding new functions, classes, or code after an existing symbol.',
|
|
734
|
+
inputSchema: {
|
|
735
|
+
type: 'object',
|
|
736
|
+
properties: {
|
|
737
|
+
name_path: {
|
|
738
|
+
type: 'string',
|
|
739
|
+
description: 'Name path of the symbol to insert after.',
|
|
740
|
+
},
|
|
741
|
+
relative_path: {
|
|
742
|
+
type: 'string',
|
|
743
|
+
description: 'The relative path to the file containing the symbol.',
|
|
744
|
+
},
|
|
745
|
+
body: {
|
|
746
|
+
type: 'string',
|
|
747
|
+
description: 'The content to insert after the symbol.',
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
required: ['name_path', 'relative_path', 'body'],
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: 'rename_symbol',
|
|
755
|
+
description: 'Rename a symbol throughout the entire codebase. Updates the symbol definition and all references.',
|
|
32
756
|
inputSchema: {
|
|
33
757
|
type: 'object',
|
|
34
758
|
properties: {
|
|
35
|
-
|
|
36
|
-
type: '
|
|
37
|
-
|
|
38
|
-
description: 'Glob patterns for files to index (default: common code files)',
|
|
759
|
+
name_path: {
|
|
760
|
+
type: 'string',
|
|
761
|
+
description: 'Name path of the symbol to rename.',
|
|
39
762
|
},
|
|
40
|
-
|
|
41
|
-
type: '
|
|
42
|
-
|
|
43
|
-
|
|
763
|
+
relative_path: {
|
|
764
|
+
type: 'string',
|
|
765
|
+
description: 'The relative path to the file containing the symbol definition.',
|
|
766
|
+
},
|
|
767
|
+
new_name: {
|
|
768
|
+
type: 'string',
|
|
769
|
+
description: 'The new name for the symbol.',
|
|
770
|
+
},
|
|
771
|
+
dry_run: {
|
|
772
|
+
type: 'boolean',
|
|
773
|
+
description: 'If true, preview changes without making them. Default: false.',
|
|
44
774
|
},
|
|
45
775
|
},
|
|
776
|
+
required: ['name_path', 'relative_path', 'new_name'],
|
|
46
777
|
},
|
|
47
778
|
},
|
|
779
|
+
// --- Worktree Tools ---
|
|
48
780
|
{
|
|
49
|
-
name: '
|
|
50
|
-
description: '
|
|
781
|
+
name: 'create_worktree',
|
|
782
|
+
description: 'Create an isolated git worktree for parallel agent work. Prevents file conflicts when multiple agents work simultaneously. Creates a new branch and optionally installs dependencies.',
|
|
51
783
|
inputSchema: {
|
|
52
784
|
type: 'object',
|
|
53
785
|
properties: {
|
|
54
|
-
|
|
786
|
+
short_name: {
|
|
55
787
|
type: 'string',
|
|
56
|
-
description: '
|
|
788
|
+
description: 'Short descriptive name for the worktree (e.g., "add-auth", "fix-login"). Used in branch name.',
|
|
57
789
|
},
|
|
58
|
-
|
|
59
|
-
type: '
|
|
60
|
-
description: '
|
|
790
|
+
issue_id: {
|
|
791
|
+
type: 'string',
|
|
792
|
+
description: 'Optional issue ID (e.g., "bd-123"). Combined with short_name for naming.',
|
|
793
|
+
},
|
|
794
|
+
prefix: {
|
|
795
|
+
type: 'string',
|
|
796
|
+
enum: ['feature', 'fix', 'refactor', 'docs', 'test'],
|
|
797
|
+
description: 'Branch prefix (default: "feature").',
|
|
798
|
+
},
|
|
799
|
+
base_branch: {
|
|
800
|
+
type: 'string',
|
|
801
|
+
description: 'Base branch to create from (default: main or current branch).',
|
|
802
|
+
},
|
|
803
|
+
install_deps: {
|
|
804
|
+
type: 'boolean',
|
|
805
|
+
description: 'Whether to install dependencies after creation (default: true).',
|
|
806
|
+
},
|
|
807
|
+
package_manager: {
|
|
808
|
+
type: 'string',
|
|
809
|
+
enum: ['npm', 'yarn', 'pnpm', 'bun'],
|
|
810
|
+
description: 'Package manager to use (default: auto-detect from lock file).',
|
|
61
811
|
},
|
|
62
812
|
},
|
|
63
|
-
required: ['
|
|
813
|
+
required: ['short_name'],
|
|
64
814
|
},
|
|
65
815
|
},
|
|
66
816
|
{
|
|
67
|
-
name: '
|
|
68
|
-
description: '
|
|
817
|
+
name: 'list_worktrees',
|
|
818
|
+
description: 'List all agent worktrees and their status. Shows branch, commit, dirty state, and ahead/behind counts.',
|
|
69
819
|
inputSchema: {
|
|
70
820
|
type: 'object',
|
|
71
821
|
properties: {},
|
|
72
822
|
},
|
|
73
823
|
},
|
|
74
824
|
{
|
|
75
|
-
name: '
|
|
76
|
-
description: '
|
|
825
|
+
name: 'remove_worktree',
|
|
826
|
+
description: 'Remove an agent worktree. Optionally deletes the associated branch. Fails if worktree has uncommitted changes unless force is true.',
|
|
77
827
|
inputSchema: {
|
|
78
828
|
type: 'object',
|
|
79
|
-
properties: {
|
|
829
|
+
properties: {
|
|
830
|
+
name: {
|
|
831
|
+
type: 'string',
|
|
832
|
+
description: 'Name of the worktree to remove.',
|
|
833
|
+
},
|
|
834
|
+
delete_branch: {
|
|
835
|
+
type: 'boolean',
|
|
836
|
+
description: 'Whether to also delete the branch (default: false).',
|
|
837
|
+
},
|
|
838
|
+
force: {
|
|
839
|
+
type: 'boolean',
|
|
840
|
+
description: 'Force removal even if worktree has uncommitted changes (default: false).',
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
required: ['name'],
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
name: 'worktree_status',
|
|
848
|
+
description: 'Get detailed status of a specific worktree including branch, commit, and dirty state.',
|
|
849
|
+
inputSchema: {
|
|
850
|
+
type: 'object',
|
|
851
|
+
properties: {
|
|
852
|
+
name: {
|
|
853
|
+
type: 'string',
|
|
854
|
+
description: 'Name of the worktree to get status for.',
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
required: ['name'],
|
|
80
858
|
},
|
|
81
859
|
},
|
|
82
860
|
],
|
|
@@ -85,45 +863,124 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
85
863
|
// Handle tool calls
|
|
86
864
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
87
865
|
const { name, arguments: args } = request.params;
|
|
866
|
+
// Record command usage for dashboard
|
|
867
|
+
const validCommands = [
|
|
868
|
+
'index_codebase',
|
|
869
|
+
'search_code',
|
|
870
|
+
'search_similar',
|
|
871
|
+
'get_index_status',
|
|
872
|
+
'clear_index',
|
|
873
|
+
'get_project_instructions',
|
|
874
|
+
'commit',
|
|
875
|
+
// Symbolic analysis
|
|
876
|
+
'get_symbols_overview',
|
|
877
|
+
'find_symbol',
|
|
878
|
+
'find_referencing_symbols',
|
|
879
|
+
'search_for_pattern',
|
|
880
|
+
'replace_symbol_body',
|
|
881
|
+
'insert_before_symbol',
|
|
882
|
+
'insert_after_symbol',
|
|
883
|
+
'rename_symbol',
|
|
884
|
+
// Memory
|
|
885
|
+
'write_memory',
|
|
886
|
+
'read_memory',
|
|
887
|
+
'list_memories',
|
|
888
|
+
'delete_memory',
|
|
889
|
+
'edit_memory',
|
|
890
|
+
// Worktree
|
|
891
|
+
'create_worktree',
|
|
892
|
+
'list_worktrees',
|
|
893
|
+
'remove_worktree',
|
|
894
|
+
'worktree_status',
|
|
895
|
+
// Clustering
|
|
896
|
+
'list_concepts',
|
|
897
|
+
'search_by_concept',
|
|
898
|
+
'summarize_codebase',
|
|
899
|
+
];
|
|
900
|
+
if (validCommands.includes(name)) {
|
|
901
|
+
dashboardState.recordCommandUsage(name);
|
|
902
|
+
}
|
|
88
903
|
try {
|
|
89
904
|
const idx = await getIndexer();
|
|
90
905
|
switch (name) {
|
|
91
906
|
case 'index_codebase': {
|
|
92
|
-
const patterns = args?.patterns
|
|
93
|
-
const excludePatterns = args?.excludePatterns
|
|
94
|
-
|
|
907
|
+
const patterns = isStringArray(args?.patterns) ? args.patterns : undefined;
|
|
908
|
+
const excludePatterns = isStringArray(args?.excludePatterns)
|
|
909
|
+
? args.excludePatterns
|
|
910
|
+
: undefined;
|
|
911
|
+
const forceReindex = isBoolean(args?.forceReindex) ? args.forceReindex : false;
|
|
912
|
+
const autoRepair = isBoolean(args?.autoRepair) ? args.autoRepair : false;
|
|
913
|
+
// Notify dashboard of indexing start
|
|
914
|
+
dashboardState.onIndexingStart();
|
|
915
|
+
const result = await idx.indexCodebase(patterns, excludePatterns, forceReindex, (progress) => {
|
|
916
|
+
// Emit progress events to dashboard
|
|
917
|
+
dashboardState.onProgress(progress);
|
|
918
|
+
}, autoRepair);
|
|
919
|
+
// Notify dashboard of indexing completion
|
|
920
|
+
dashboardState.onIndexingComplete(result);
|
|
921
|
+
const mode = result.repaired
|
|
922
|
+
? 'Repaired (corruption detected)'
|
|
923
|
+
: result.incremental
|
|
924
|
+
? 'Incremental update'
|
|
925
|
+
: 'Full reindex';
|
|
95
926
|
return {
|
|
96
927
|
content: [
|
|
97
928
|
{
|
|
98
929
|
type: 'text',
|
|
99
|
-
text:
|
|
930
|
+
text: `${mode}: Indexed ${result.filesIndexed} files, total ${result.chunksCreated} chunks.${TOOL_GUIDANCE}`,
|
|
100
931
|
},
|
|
101
932
|
],
|
|
102
933
|
};
|
|
103
934
|
}
|
|
104
935
|
case 'search_code': {
|
|
105
|
-
const query = args?.query;
|
|
106
|
-
|
|
107
|
-
|
|
936
|
+
const query = isString(args?.query) ? args.query : '';
|
|
937
|
+
if (!query) {
|
|
938
|
+
throw new LanceContextError('query is required', 'validation', { tool: 'search_code' });
|
|
939
|
+
}
|
|
940
|
+
const results = await idx.search({
|
|
941
|
+
query,
|
|
942
|
+
limit: isNumber(args?.limit) ? args.limit : 10,
|
|
943
|
+
pathPattern: isString(args?.pathPattern) ? args.pathPattern : undefined,
|
|
944
|
+
languages: isStringArray(args?.languages) ? args.languages : undefined,
|
|
945
|
+
});
|
|
108
946
|
const formatted = results
|
|
109
|
-
.map((r, i) =>
|
|
947
|
+
.map((r, i) => {
|
|
948
|
+
// Build header with optional symbol context
|
|
949
|
+
let header = `## Result ${i + 1}: ${r.filepath}:${r.startLine}-${r.endLine}`;
|
|
950
|
+
if (r.symbolName) {
|
|
951
|
+
const typeLabel = r.symbolType ? ` (${r.symbolType})` : '';
|
|
952
|
+
header += `\n**Symbol:** \`${r.symbolName}\`${typeLabel}`;
|
|
953
|
+
}
|
|
954
|
+
return `${header}\n\`\`\`${r.language}\n${r.content}\n\`\`\``;
|
|
955
|
+
})
|
|
110
956
|
.join('\n\n');
|
|
111
957
|
return {
|
|
112
958
|
content: [
|
|
113
959
|
{
|
|
114
960
|
type: 'text',
|
|
115
|
-
text: formatted || 'No results found.',
|
|
961
|
+
text: (formatted || 'No results found.') + TOOL_GUIDANCE,
|
|
116
962
|
},
|
|
117
963
|
],
|
|
118
964
|
};
|
|
119
965
|
}
|
|
120
966
|
case 'get_index_status': {
|
|
121
967
|
const status = await idx.getStatus();
|
|
968
|
+
let statusText = JSON.stringify(status, null, 2);
|
|
969
|
+
// Add corruption warning if detected
|
|
970
|
+
if (status.corrupted) {
|
|
971
|
+
statusText =
|
|
972
|
+
`**WARNING: Index corruption detected!**\n` +
|
|
973
|
+
`Reason: ${status.corruptionReason}\n` +
|
|
974
|
+
`\nTo repair, either:\n` +
|
|
975
|
+
`1. Run \`index_codebase\` with \`autoRepair: true\`\n` +
|
|
976
|
+
`2. Run \`clear_index\` followed by \`index_codebase\`\n\n` +
|
|
977
|
+
statusText;
|
|
978
|
+
}
|
|
122
979
|
return {
|
|
123
980
|
content: [
|
|
124
981
|
{
|
|
125
982
|
type: 'text',
|
|
126
|
-
text:
|
|
983
|
+
text: statusText + TOOL_GUIDANCE,
|
|
127
984
|
},
|
|
128
985
|
],
|
|
129
986
|
};
|
|
@@ -134,21 +991,896 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
134
991
|
content: [
|
|
135
992
|
{
|
|
136
993
|
type: 'text',
|
|
137
|
-
text: 'Index cleared.',
|
|
994
|
+
text: 'Index cleared.' + TOOL_GUIDANCE,
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
case 'get_project_instructions': {
|
|
1000
|
+
const config = await loadConfig(PROJECT_PATH);
|
|
1001
|
+
const projectInstructions = getInstructions(config);
|
|
1002
|
+
const fullInstructions = PRIORITY_INSTRUCTIONS + (projectInstructions || '');
|
|
1003
|
+
return {
|
|
1004
|
+
content: [
|
|
1005
|
+
{
|
|
1006
|
+
type: 'text',
|
|
1007
|
+
text: fullInstructions ||
|
|
1008
|
+
'No project instructions configured. Add an "instructions" field to .lance-context.json.',
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
case 'search_similar': {
|
|
1014
|
+
const results = await idx.searchSimilar({
|
|
1015
|
+
filepath: isString(args?.filepath) ? args.filepath : undefined,
|
|
1016
|
+
startLine: isNumber(args?.startLine) ? args.startLine : undefined,
|
|
1017
|
+
endLine: isNumber(args?.endLine) ? args.endLine : undefined,
|
|
1018
|
+
code: isString(args?.code) ? args.code : undefined,
|
|
1019
|
+
limit: isNumber(args?.limit) ? args.limit : 10,
|
|
1020
|
+
threshold: isNumber(args?.threshold) ? args.threshold : 0,
|
|
1021
|
+
excludeSelf: isBoolean(args?.excludeSelf) ? args.excludeSelf : true,
|
|
1022
|
+
});
|
|
1023
|
+
if (results.length === 0) {
|
|
1024
|
+
return {
|
|
1025
|
+
content: [
|
|
1026
|
+
{
|
|
1027
|
+
type: 'text',
|
|
1028
|
+
text: 'No similar code found.' + TOOL_GUIDANCE,
|
|
1029
|
+
},
|
|
1030
|
+
],
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const formatted = results
|
|
1034
|
+
.map((r, i) => {
|
|
1035
|
+
let header = `## Similar ${i + 1}: ${r.filepath}:${r.startLine}-${r.endLine} (${(r.similarity * 100).toFixed(1)}% similar)`;
|
|
1036
|
+
if (r.symbolName) {
|
|
1037
|
+
const typeLabel = r.symbolType ? ` (${r.symbolType})` : '';
|
|
1038
|
+
header += `\n**Symbol:** \`${r.symbolName}\`${typeLabel}`;
|
|
1039
|
+
}
|
|
1040
|
+
return `${header}\n\`\`\`${r.language}\n${r.content}\n\`\`\``;
|
|
1041
|
+
})
|
|
1042
|
+
.join('\n\n');
|
|
1043
|
+
return {
|
|
1044
|
+
content: [
|
|
1045
|
+
{
|
|
1046
|
+
type: 'text',
|
|
1047
|
+
text: formatted + TOOL_GUIDANCE,
|
|
1048
|
+
},
|
|
1049
|
+
],
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
case 'summarize_codebase': {
|
|
1053
|
+
const numClusters = isNumber(args?.numClusters) ? args.numClusters : undefined;
|
|
1054
|
+
const summary = await idx.summarizeCodebase(numClusters ? { numClusters } : undefined);
|
|
1055
|
+
const languageList = summary.languages
|
|
1056
|
+
.map((l) => `- **${l.language}**: ${l.fileCount} files, ${l.chunkCount} chunks`)
|
|
1057
|
+
.join('\n');
|
|
1058
|
+
const conceptList = summary.concepts
|
|
1059
|
+
.map((c) => {
|
|
1060
|
+
const keywords = c.keywords.slice(0, 5).join(', ');
|
|
1061
|
+
return `- **Cluster ${c.id}: ${c.label}** (${c.size} chunks)\n Keywords: ${keywords}`;
|
|
1062
|
+
})
|
|
1063
|
+
.join('\n');
|
|
1064
|
+
const formatted = `# Codebase Summary
|
|
1065
|
+
|
|
1066
|
+
## Overview
|
|
1067
|
+
- **Total Files**: ${summary.totalFiles}
|
|
1068
|
+
- **Total Chunks**: ${summary.totalChunks}
|
|
1069
|
+
- **Concept Clusters**: ${summary.concepts.length}
|
|
1070
|
+
- **Clustering Quality**: ${(summary.clusteringQuality * 100).toFixed(1)}% (silhouette score)
|
|
1071
|
+
- **Generated At**: ${summary.generatedAt}
|
|
1072
|
+
|
|
1073
|
+
## Languages
|
|
1074
|
+
${languageList}
|
|
1075
|
+
|
|
1076
|
+
## Concept Areas
|
|
1077
|
+
${conceptList}`;
|
|
1078
|
+
return {
|
|
1079
|
+
content: [
|
|
1080
|
+
{
|
|
1081
|
+
type: 'text',
|
|
1082
|
+
text: formatted + TOOL_GUIDANCE,
|
|
1083
|
+
},
|
|
1084
|
+
],
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
case 'list_concepts': {
|
|
1088
|
+
const forceRecluster = isBoolean(args?.forceRecluster) ? args.forceRecluster : false;
|
|
1089
|
+
const concepts = await idx.listConcepts(forceRecluster);
|
|
1090
|
+
if (concepts.length === 0) {
|
|
1091
|
+
return {
|
|
1092
|
+
content: [
|
|
1093
|
+
{
|
|
1094
|
+
type: 'text',
|
|
1095
|
+
text: 'No concept clusters found. Make sure the codebase is indexed first.' +
|
|
1096
|
+
TOOL_GUIDANCE,
|
|
1097
|
+
},
|
|
1098
|
+
],
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const formatted = concepts
|
|
1102
|
+
.map((c) => {
|
|
1103
|
+
const keywords = c.keywords.slice(0, 5).join(', ');
|
|
1104
|
+
return `## Cluster ${c.id}: ${c.label}
|
|
1105
|
+
- **Size**: ${c.size} code chunks
|
|
1106
|
+
- **Keywords**: ${keywords}
|
|
1107
|
+
- **Representatives**: ${c.representativeChunks.slice(0, 3).join(', ')}`;
|
|
1108
|
+
})
|
|
1109
|
+
.join('\n\n');
|
|
1110
|
+
return {
|
|
1111
|
+
content: [
|
|
1112
|
+
{
|
|
1113
|
+
type: 'text',
|
|
1114
|
+
text: `# Concept Clusters\n\n${formatted}` + TOOL_GUIDANCE,
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
case 'search_by_concept': {
|
|
1120
|
+
const conceptId = isNumber(args?.conceptId) ? args.conceptId : -1;
|
|
1121
|
+
if (conceptId < 0) {
|
|
1122
|
+
throw new LanceContextError('conceptId is required and must be a non-negative number', 'validation', { tool: 'search_by_concept' });
|
|
1123
|
+
}
|
|
1124
|
+
const query = isString(args?.query) ? args.query : undefined;
|
|
1125
|
+
const limit = isNumber(args?.limit) ? args.limit : 10;
|
|
1126
|
+
const results = await idx.searchByConcept(conceptId, query, limit);
|
|
1127
|
+
if (results.length === 0) {
|
|
1128
|
+
return {
|
|
1129
|
+
content: [
|
|
1130
|
+
{
|
|
1131
|
+
type: 'text',
|
|
1132
|
+
text: `No code found in concept cluster ${conceptId}. Try list_concepts to see available clusters.` +
|
|
1133
|
+
TOOL_GUIDANCE,
|
|
1134
|
+
},
|
|
1135
|
+
],
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const formatted = results
|
|
1139
|
+
.map((r, i) => {
|
|
1140
|
+
let header = `## Result ${i + 1}: ${r.filepath}:${r.startLine}-${r.endLine}`;
|
|
1141
|
+
if (r.symbolName) {
|
|
1142
|
+
const typeLabel = r.symbolType ? ` (${r.symbolType})` : '';
|
|
1143
|
+
header += `\n**Symbol:** \`${r.symbolName}\`${typeLabel}`;
|
|
1144
|
+
}
|
|
1145
|
+
return `${header}\n\`\`\`${r.language}\n${r.content}\n\`\`\``;
|
|
1146
|
+
})
|
|
1147
|
+
.join('\n\n');
|
|
1148
|
+
return {
|
|
1149
|
+
content: [
|
|
1150
|
+
{
|
|
1151
|
+
type: 'text',
|
|
1152
|
+
text: formatted + TOOL_GUIDANCE,
|
|
1153
|
+
},
|
|
1154
|
+
],
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
case 'commit': {
|
|
1158
|
+
const message = isString(args?.message) ? args.message : '';
|
|
1159
|
+
const files = isStringArray(args?.files) ? args.files : [];
|
|
1160
|
+
if (!message) {
|
|
1161
|
+
throw new LanceContextError('message is required', 'validation', { tool: 'commit' });
|
|
1162
|
+
}
|
|
1163
|
+
// Commit rules to return with every response
|
|
1164
|
+
const COMMIT_RULES = `
|
|
1165
|
+
## Commit Rules Reminder
|
|
1166
|
+
|
|
1167
|
+
1. **Branch**: Must be on a feature branch, not main/master
|
|
1168
|
+
2. **Message length**: Subject line must be ≤72 characters
|
|
1169
|
+
3. **Imperative mood**: "Add feature" not "Added feature"
|
|
1170
|
+
4. **Single responsibility**: One logical change per commit
|
|
1171
|
+
5. **Body format**: Only "Co-Authored-By: Claude <noreply@anthropic.com>"
|
|
1172
|
+
|
|
1173
|
+
**Signs of multi-responsibility** (split into separate commits):
|
|
1174
|
+
- Message contains "and" connecting actions
|
|
1175
|
+
- Message lists multiple changes with commas
|
|
1176
|
+
- Changes span unrelated files/features
|
|
1177
|
+
`;
|
|
1178
|
+
const errors = [];
|
|
1179
|
+
const warnings = [];
|
|
1180
|
+
// Check 1: Not on main/master branch
|
|
1181
|
+
let currentBranch = '';
|
|
1182
|
+
try {
|
|
1183
|
+
const { stdout } = await execAsync('git branch --show-current', { cwd: PROJECT_PATH });
|
|
1184
|
+
currentBranch = stdout.trim();
|
|
1185
|
+
if (currentBranch === 'main' || currentBranch === 'master') {
|
|
1186
|
+
errors.push(`Cannot commit directly to ${currentBranch}. Create a feature branch first:\n git checkout -b feature/your-feature-name`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
catch {
|
|
1190
|
+
errors.push('Failed to determine current branch. Are you in a git repository?');
|
|
1191
|
+
}
|
|
1192
|
+
// Check 2: Message length
|
|
1193
|
+
const subjectLine = message.split('\n')[0];
|
|
1194
|
+
if (subjectLine.length > 72) {
|
|
1195
|
+
errors.push(`Subject line is ${subjectLine.length} characters (max 72). Shorten it.`);
|
|
1196
|
+
}
|
|
1197
|
+
// Check 3: Imperative mood (heuristic - check for common past tense patterns)
|
|
1198
|
+
const pastTensePatterns = /^(Added|Fixed|Updated|Changed|Removed|Implemented|Created|Deleted|Modified|Refactored|Merged)\b/i;
|
|
1199
|
+
if (pastTensePatterns.test(subjectLine)) {
|
|
1200
|
+
warnings.push(`Subject may not be imperative mood. Use "Add" not "Added", "Fix" not "Fixed", etc.`);
|
|
1201
|
+
}
|
|
1202
|
+
// Check 4: Single responsibility (heuristic - check for "and" or multiple verbs)
|
|
1203
|
+
const multiResponsibilityPatterns = /\b(and|,)\s+(add|fix|update|change|remove|implement|create|delete|modify|refactor)\b/i;
|
|
1204
|
+
if (multiResponsibilityPatterns.test(subjectLine)) {
|
|
1205
|
+
errors.push(`Message suggests multiple responsibilities. Split into separate commits.`);
|
|
1206
|
+
}
|
|
1207
|
+
// If there are blocking errors, return them without committing
|
|
1208
|
+
if (errors.length > 0) {
|
|
1209
|
+
return {
|
|
1210
|
+
content: [
|
|
1211
|
+
{
|
|
1212
|
+
type: 'text',
|
|
1213
|
+
text: `## Commit Blocked\n\n**Errors:**\n${errors.map((e) => `- ${e}`).join('\n')}\n${warnings.length > 0 ? `\n**Warnings:**\n${warnings.map((w) => `- ${w}`).join('\n')}` : ''}\n${COMMIT_RULES}`,
|
|
1214
|
+
},
|
|
1215
|
+
],
|
|
1216
|
+
isError: true,
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
// Stage files if provided
|
|
1220
|
+
if (files.length > 0) {
|
|
1221
|
+
try {
|
|
1222
|
+
const fileArgs = files.map((f) => `"${f}"`).join(' ');
|
|
1223
|
+
await execAsync(`git add ${fileArgs}`, { cwd: PROJECT_PATH });
|
|
1224
|
+
}
|
|
1225
|
+
catch (e) {
|
|
1226
|
+
throw wrapError('Failed to stage files', 'git', e, { files });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// Check if there are staged changes
|
|
1230
|
+
try {
|
|
1231
|
+
const { stdout } = await execAsync('git diff --cached --name-only', {
|
|
1232
|
+
cwd: PROJECT_PATH,
|
|
1233
|
+
});
|
|
1234
|
+
if (!stdout.trim()) {
|
|
1235
|
+
throw new LanceContextError('No staged changes to commit. Stage files first or pass files parameter.', 'git');
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
catch (e) {
|
|
1239
|
+
if (e instanceof LanceContextError) {
|
|
1240
|
+
throw e;
|
|
1241
|
+
}
|
|
1242
|
+
throw wrapError('Failed to check staged changes', 'git', e);
|
|
1243
|
+
}
|
|
1244
|
+
// Build commit message with Co-Authored-By
|
|
1245
|
+
const fullMessage = `${message}\n\nCo-Authored-By: Claude <noreply@anthropic.com>`;
|
|
1246
|
+
// Write marker file to indicate commit is via MCP tool (for post-commit hook)
|
|
1247
|
+
const markerPath = path.join(PROJECT_PATH, '.git', 'MCP_COMMIT_MARKER');
|
|
1248
|
+
try {
|
|
1249
|
+
fs.writeFileSync(markerPath, Date.now().toString());
|
|
1250
|
+
}
|
|
1251
|
+
catch {
|
|
1252
|
+
// Ignore marker write failures - non-critical
|
|
1253
|
+
}
|
|
1254
|
+
// Execute commit
|
|
1255
|
+
try {
|
|
1256
|
+
const { stdout } = await execAsync(`git commit -m "${fullMessage.replace(/"/g, '\\"')}"`, { cwd: PROJECT_PATH });
|
|
1257
|
+
let response = `## Commit Successful\n\n${stdout.trim()}`;
|
|
1258
|
+
if (warnings.length > 0) {
|
|
1259
|
+
response += `\n\n**Warnings:**\n${warnings.map((w) => `- ${w}`).join('\n')}`;
|
|
1260
|
+
}
|
|
1261
|
+
response += `\n${COMMIT_RULES}`;
|
|
1262
|
+
return {
|
|
1263
|
+
content: [
|
|
1264
|
+
{
|
|
1265
|
+
type: 'text',
|
|
1266
|
+
text: response,
|
|
1267
|
+
},
|
|
1268
|
+
],
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
catch (e) {
|
|
1272
|
+
throw wrapError('Git commit failed', 'git', e, { message });
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
// --- Symbolic Analysis Tools ---
|
|
1276
|
+
case 'get_symbols_overview': {
|
|
1277
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : '';
|
|
1278
|
+
if (!relativePath) {
|
|
1279
|
+
throw new LanceContextError('relative_path is required', 'validation', {
|
|
1280
|
+
tool: 'get_symbols_overview',
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
const depth = isNumber(args?.depth) ? args.depth : 0;
|
|
1284
|
+
const extractor = new SymbolExtractor(PROJECT_PATH);
|
|
1285
|
+
const overview = await extractor.getSymbolsOverview(relativePath, depth);
|
|
1286
|
+
// Format the output
|
|
1287
|
+
const parts = [];
|
|
1288
|
+
parts.push(`## Symbols in ${overview.filepath}\n`);
|
|
1289
|
+
parts.push(`Total: ${overview.totalSymbols} symbols\n`);
|
|
1290
|
+
for (const [kindName, entries] of Object.entries(overview.byKind)) {
|
|
1291
|
+
parts.push(`\n### ${kindName} (${entries.length})\n`);
|
|
1292
|
+
for (const entry of entries) {
|
|
1293
|
+
const childInfo = entry.children ? ` [${entry.children} children]` : '';
|
|
1294
|
+
parts.push(`- **${entry.name}** (${entry.lines})${childInfo}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
content: [
|
|
1299
|
+
{
|
|
1300
|
+
type: 'text',
|
|
1301
|
+
text: parts.join('\n') + TOOL_GUIDANCE,
|
|
1302
|
+
},
|
|
1303
|
+
],
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
case 'find_symbol': {
|
|
1307
|
+
const namePathPattern = isString(args?.name_path_pattern) ? args.name_path_pattern : '';
|
|
1308
|
+
if (!namePathPattern) {
|
|
1309
|
+
throw new LanceContextError('name_path_pattern is required', 'validation', {
|
|
1310
|
+
tool: 'find_symbol',
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : undefined;
|
|
1314
|
+
const depth = isNumber(args?.depth) ? args.depth : 0;
|
|
1315
|
+
const includeBody = isBoolean(args?.include_body) ? args.include_body : false;
|
|
1316
|
+
const substringMatching = isBoolean(args?.substring_matching)
|
|
1317
|
+
? args.substring_matching
|
|
1318
|
+
: false;
|
|
1319
|
+
const includeKinds = Array.isArray(args?.include_kinds)
|
|
1320
|
+
? args.include_kinds
|
|
1321
|
+
: undefined;
|
|
1322
|
+
const excludeKinds = Array.isArray(args?.exclude_kinds)
|
|
1323
|
+
? args.exclude_kinds
|
|
1324
|
+
: undefined;
|
|
1325
|
+
const extractor = new SymbolExtractor(PROJECT_PATH);
|
|
1326
|
+
// If relativePath is provided, search in that file/directory
|
|
1327
|
+
// Otherwise, we need to search the whole codebase (more expensive)
|
|
1328
|
+
const files = [];
|
|
1329
|
+
if (relativePath) {
|
|
1330
|
+
const fullPath = path.join(PROJECT_PATH, relativePath);
|
|
1331
|
+
try {
|
|
1332
|
+
const fsStat = fs.statSync(fullPath);
|
|
1333
|
+
if (fsStat.isFile()) {
|
|
1334
|
+
files.push(relativePath);
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
// Directory - find all analyzable files
|
|
1338
|
+
const { glob: globFn } = await import('glob');
|
|
1339
|
+
const codeExtensions = [
|
|
1340
|
+
'*.ts',
|
|
1341
|
+
'*.tsx',
|
|
1342
|
+
'*.js',
|
|
1343
|
+
'*.jsx',
|
|
1344
|
+
'*.py',
|
|
1345
|
+
'*.go',
|
|
1346
|
+
'*.rs',
|
|
1347
|
+
'*.java',
|
|
1348
|
+
'*.rb',
|
|
1349
|
+
];
|
|
1350
|
+
for (const ext of codeExtensions) {
|
|
1351
|
+
const matches = await globFn(`**/${ext}`, {
|
|
1352
|
+
cwd: fullPath,
|
|
1353
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
1354
|
+
});
|
|
1355
|
+
files.push(...matches.map((f) => path.join(relativePath, f)));
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
throw new LanceContextError(`Path not found: ${relativePath}`, 'validation', {
|
|
1361
|
+
tool: 'find_symbol',
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
else {
|
|
1366
|
+
// Search whole codebase - expensive, limit to reasonable set
|
|
1367
|
+
const { glob: globFn } = await import('glob');
|
|
1368
|
+
const codeExtensions = [
|
|
1369
|
+
'*.ts',
|
|
1370
|
+
'*.tsx',
|
|
1371
|
+
'*.js',
|
|
1372
|
+
'*.jsx',
|
|
1373
|
+
'*.py',
|
|
1374
|
+
'*.go',
|
|
1375
|
+
'*.rs',
|
|
1376
|
+
'*.java',
|
|
1377
|
+
'*.rb',
|
|
1378
|
+
];
|
|
1379
|
+
for (const ext of codeExtensions) {
|
|
1380
|
+
const matches = await globFn(`**/${ext}`, {
|
|
1381
|
+
cwd: PROJECT_PATH,
|
|
1382
|
+
ignore: ['node_modules/**', 'dist/**', '.git/**'],
|
|
1383
|
+
});
|
|
1384
|
+
files.push(...matches);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
// Parse the pattern
|
|
1388
|
+
const pattern = parseNamePath(namePathPattern);
|
|
1389
|
+
// Find matching symbols
|
|
1390
|
+
const matchedSymbols = [];
|
|
1391
|
+
for (const file of files.slice(0, 100)) {
|
|
1392
|
+
// Limit to prevent timeout
|
|
1393
|
+
try {
|
|
1394
|
+
const symbols = await extractor.extractSymbols(file, includeBody);
|
|
1395
|
+
const findMatches = (syms, currentDepth) => {
|
|
1396
|
+
for (const sym of syms) {
|
|
1397
|
+
// Apply kind filters
|
|
1398
|
+
if (excludeKinds && excludeKinds.includes(sym.kind))
|
|
1399
|
+
continue;
|
|
1400
|
+
if (includeKinds && !includeKinds.includes(sym.kind))
|
|
1401
|
+
continue;
|
|
1402
|
+
if (matchNamePath(sym.namePath, pattern, substringMatching)) {
|
|
1403
|
+
matchedSymbols.push({ symbol: sym, file });
|
|
1404
|
+
}
|
|
1405
|
+
// Search children up to requested depth
|
|
1406
|
+
if (currentDepth < depth && sym.children) {
|
|
1407
|
+
findMatches(sym.children, currentDepth + 1);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
findMatches(symbols, 0);
|
|
1412
|
+
}
|
|
1413
|
+
catch {
|
|
1414
|
+
// Skip files that can't be analyzed
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (matchedSymbols.length === 0) {
|
|
1418
|
+
return {
|
|
1419
|
+
content: [
|
|
1420
|
+
{
|
|
1421
|
+
type: 'text',
|
|
1422
|
+
text: `No symbols found matching pattern: ${namePathPattern}` + TOOL_GUIDANCE,
|
|
1423
|
+
},
|
|
1424
|
+
],
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
// Format results
|
|
1428
|
+
const parts = [];
|
|
1429
|
+
parts.push(`Found ${matchedSymbols.length} matching symbol(s):\n`);
|
|
1430
|
+
for (const { symbol } of matchedSymbols) {
|
|
1431
|
+
const kindName = SymbolKindNames[symbol.kind];
|
|
1432
|
+
parts.push(`\n## ${formatNamePath(symbol.namePath)} (${kindName})`);
|
|
1433
|
+
parts.push(`**Location:** ${symbol.location.filepath}:${symbol.location.startLine}-${symbol.location.endLine}`);
|
|
1434
|
+
if (symbol.body) {
|
|
1435
|
+
parts.push('\n```');
|
|
1436
|
+
parts.push(symbol.body);
|
|
1437
|
+
parts.push('```');
|
|
1438
|
+
}
|
|
1439
|
+
if (symbol.children && symbol.children.length > 0) {
|
|
1440
|
+
parts.push(`\n**Children:** ${symbol.children.length}`);
|
|
1441
|
+
for (const child of symbol.children) {
|
|
1442
|
+
const childKind = SymbolKindNames[child.kind];
|
|
1443
|
+
parts.push(` - ${child.name} (${childKind}, lines ${child.location.startLine}-${child.location.endLine})`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return {
|
|
1448
|
+
content: [
|
|
1449
|
+
{
|
|
1450
|
+
type: 'text',
|
|
1451
|
+
text: parts.join('\n') + TOOL_GUIDANCE,
|
|
1452
|
+
},
|
|
1453
|
+
],
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
case 'find_referencing_symbols': {
|
|
1457
|
+
const namePath = isString(args?.name_path) ? args.name_path : '';
|
|
1458
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : '';
|
|
1459
|
+
if (!namePath || !relativePath) {
|
|
1460
|
+
throw new LanceContextError('name_path and relative_path are required', 'validation', {
|
|
1461
|
+
tool: 'find_referencing_symbols',
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
const includeInfo = isBoolean(args?.include_info) ? args.include_info : false;
|
|
1465
|
+
const includeKinds = Array.isArray(args?.include_kinds)
|
|
1466
|
+
? args.include_kinds
|
|
1467
|
+
: undefined;
|
|
1468
|
+
const excludeKinds = Array.isArray(args?.exclude_kinds)
|
|
1469
|
+
? args.exclude_kinds
|
|
1470
|
+
: undefined;
|
|
1471
|
+
const finder = new ReferenceFinder(PROJECT_PATH);
|
|
1472
|
+
const references = await finder.findReferences({
|
|
1473
|
+
namePath,
|
|
1474
|
+
relativePath,
|
|
1475
|
+
includeInfo,
|
|
1476
|
+
includeKinds,
|
|
1477
|
+
excludeKinds,
|
|
1478
|
+
});
|
|
1479
|
+
return {
|
|
1480
|
+
content: [
|
|
1481
|
+
{
|
|
1482
|
+
type: 'text',
|
|
1483
|
+
text: formatReferencesResult(references) + TOOL_GUIDANCE,
|
|
1484
|
+
},
|
|
1485
|
+
],
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
case 'search_for_pattern': {
|
|
1489
|
+
const substringPattern = isString(args?.substring_pattern) ? args.substring_pattern : '';
|
|
1490
|
+
if (!substringPattern) {
|
|
1491
|
+
throw new LanceContextError('substring_pattern is required', 'validation', {
|
|
1492
|
+
tool: 'search_for_pattern',
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
const result = await searchForPattern(PROJECT_PATH, {
|
|
1496
|
+
substringPattern,
|
|
1497
|
+
relativePath: isString(args?.relative_path) ? args.relative_path : undefined,
|
|
1498
|
+
restrictSearchToCodeFiles: isBoolean(args?.restrict_search_to_code_files)
|
|
1499
|
+
? args.restrict_search_to_code_files
|
|
1500
|
+
: false,
|
|
1501
|
+
pathsIncludeGlob: isString(args?.paths_include_glob)
|
|
1502
|
+
? args.paths_include_glob
|
|
1503
|
+
: undefined,
|
|
1504
|
+
pathsExcludeGlob: isString(args?.paths_exclude_glob)
|
|
1505
|
+
? args.paths_exclude_glob
|
|
1506
|
+
: undefined,
|
|
1507
|
+
contextLinesBefore: isNumber(args?.context_lines_before) ? args.context_lines_before : 0,
|
|
1508
|
+
contextLinesAfter: isNumber(args?.context_lines_after) ? args.context_lines_after : 0,
|
|
1509
|
+
maxAnswerChars: isNumber(args?.max_answer_chars) ? args.max_answer_chars : 50000,
|
|
1510
|
+
});
|
|
1511
|
+
return {
|
|
1512
|
+
content: [
|
|
1513
|
+
{
|
|
1514
|
+
type: 'text',
|
|
1515
|
+
text: formatPatternSearchResults(result) + TOOL_GUIDANCE,
|
|
1516
|
+
},
|
|
1517
|
+
],
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
// --- Memory Tools ---
|
|
1521
|
+
case 'write_memory': {
|
|
1522
|
+
const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
|
|
1523
|
+
const content = isString(args?.content) ? args.content : '';
|
|
1524
|
+
if (!memoryFileName || !content) {
|
|
1525
|
+
throw new LanceContextError('memory_file_name and content are required', 'validation', {
|
|
1526
|
+
tool: 'write_memory',
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
const memoryManager = new MemoryManager(PROJECT_PATH);
|
|
1530
|
+
await memoryManager.writeMemory(memoryFileName, content);
|
|
1531
|
+
return {
|
|
1532
|
+
content: [
|
|
1533
|
+
{
|
|
1534
|
+
type: 'text',
|
|
1535
|
+
text: `Memory "${memoryFileName}" saved successfully.` + TOOL_GUIDANCE,
|
|
1536
|
+
},
|
|
1537
|
+
],
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
case 'read_memory': {
|
|
1541
|
+
const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
|
|
1542
|
+
if (!memoryFileName) {
|
|
1543
|
+
throw new LanceContextError('memory_file_name is required', 'validation', {
|
|
1544
|
+
tool: 'read_memory',
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
const memoryManager = new MemoryManager(PROJECT_PATH);
|
|
1548
|
+
const content = await memoryManager.readMemory(memoryFileName);
|
|
1549
|
+
return {
|
|
1550
|
+
content: [
|
|
1551
|
+
{
|
|
1552
|
+
type: 'text',
|
|
1553
|
+
text: `## Memory: ${memoryFileName}\n\n${content}` + TOOL_GUIDANCE,
|
|
1554
|
+
},
|
|
1555
|
+
],
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
case 'list_memories': {
|
|
1559
|
+
const memoryManager = new MemoryManager(PROJECT_PATH);
|
|
1560
|
+
const memories = await memoryManager.listMemories();
|
|
1561
|
+
return {
|
|
1562
|
+
content: [
|
|
1563
|
+
{
|
|
1564
|
+
type: 'text',
|
|
1565
|
+
text: formatMemoryList(memories) + TOOL_GUIDANCE,
|
|
1566
|
+
},
|
|
1567
|
+
],
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
case 'delete_memory': {
|
|
1571
|
+
const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
|
|
1572
|
+
if (!memoryFileName) {
|
|
1573
|
+
throw new LanceContextError('memory_file_name is required', 'validation', {
|
|
1574
|
+
tool: 'delete_memory',
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
const memoryManager = new MemoryManager(PROJECT_PATH);
|
|
1578
|
+
await memoryManager.deleteMemory(memoryFileName);
|
|
1579
|
+
return {
|
|
1580
|
+
content: [
|
|
1581
|
+
{
|
|
1582
|
+
type: 'text',
|
|
1583
|
+
text: `Memory "${memoryFileName}" deleted successfully.` + TOOL_GUIDANCE,
|
|
1584
|
+
},
|
|
1585
|
+
],
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
case 'edit_memory': {
|
|
1589
|
+
const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
|
|
1590
|
+
const needle = isString(args?.needle) ? args.needle : '';
|
|
1591
|
+
const repl = isString(args?.repl) ? args.repl : '';
|
|
1592
|
+
const mode = isString(args?.mode) ? args.mode : '';
|
|
1593
|
+
if (!memoryFileName || !needle || mode === '') {
|
|
1594
|
+
throw new LanceContextError('memory_file_name, needle, repl, and mode are required', 'validation', { tool: 'edit_memory' });
|
|
1595
|
+
}
|
|
1596
|
+
if (mode !== 'literal' && mode !== 'regex') {
|
|
1597
|
+
throw new LanceContextError('mode must be "literal" or "regex"', 'validation', {
|
|
1598
|
+
tool: 'edit_memory',
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
const memoryManager = new MemoryManager(PROJECT_PATH);
|
|
1602
|
+
const result = await memoryManager.editMemory(memoryFileName, needle, repl, mode);
|
|
1603
|
+
return {
|
|
1604
|
+
content: [
|
|
1605
|
+
{
|
|
1606
|
+
type: 'text',
|
|
1607
|
+
text: `Memory "${memoryFileName}" edited. ${result.matchCount} replacement(s) made.` +
|
|
1608
|
+
TOOL_GUIDANCE,
|
|
1609
|
+
},
|
|
1610
|
+
],
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
// --- Symbol Editing Tools ---
|
|
1614
|
+
case 'replace_symbol_body': {
|
|
1615
|
+
const namePath = isString(args?.name_path) ? args.name_path : '';
|
|
1616
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : '';
|
|
1617
|
+
const body = isString(args?.body) ? args.body : '';
|
|
1618
|
+
if (!namePath || !relativePath || !body) {
|
|
1619
|
+
throw new LanceContextError('name_path, relative_path, and body are required', 'validation', { tool: 'replace_symbol_body' });
|
|
1620
|
+
}
|
|
1621
|
+
const editor = new SymbolEditor(PROJECT_PATH);
|
|
1622
|
+
const result = await editor.replaceSymbolBody({ namePath, relativePath, body });
|
|
1623
|
+
if (!result.success) {
|
|
1624
|
+
return {
|
|
1625
|
+
content: [
|
|
1626
|
+
{
|
|
1627
|
+
type: 'text',
|
|
1628
|
+
text: `Failed to replace symbol: ${result.error}` + TOOL_GUIDANCE,
|
|
1629
|
+
},
|
|
1630
|
+
],
|
|
1631
|
+
isError: true,
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
return {
|
|
1635
|
+
content: [
|
|
1636
|
+
{
|
|
1637
|
+
type: 'text',
|
|
1638
|
+
text: `Symbol "${result.symbolName}" replaced in ${result.filepath}.\n` +
|
|
1639
|
+
`New location: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
|
|
1640
|
+
TOOL_GUIDANCE,
|
|
1641
|
+
},
|
|
1642
|
+
],
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
case 'insert_before_symbol': {
|
|
1646
|
+
const namePath = isString(args?.name_path) ? args.name_path : '';
|
|
1647
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : '';
|
|
1648
|
+
const body = isString(args?.body) ? args.body : '';
|
|
1649
|
+
if (!namePath || !relativePath || !body) {
|
|
1650
|
+
throw new LanceContextError('name_path, relative_path, and body are required', 'validation', { tool: 'insert_before_symbol' });
|
|
1651
|
+
}
|
|
1652
|
+
const editor = new SymbolEditor(PROJECT_PATH);
|
|
1653
|
+
const result = await editor.insertBeforeSymbol({ namePath, relativePath, body });
|
|
1654
|
+
if (!result.success) {
|
|
1655
|
+
return {
|
|
1656
|
+
content: [
|
|
1657
|
+
{
|
|
1658
|
+
type: 'text',
|
|
1659
|
+
text: `Failed to insert before symbol: ${result.error}` + TOOL_GUIDANCE,
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
isError: true,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
content: [
|
|
1667
|
+
{
|
|
1668
|
+
type: 'text',
|
|
1669
|
+
text: `Content inserted before "${result.symbolName}" in ${result.filepath}.\n` +
|
|
1670
|
+
`Inserted at: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
|
|
1671
|
+
TOOL_GUIDANCE,
|
|
1672
|
+
},
|
|
1673
|
+
],
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
case 'insert_after_symbol': {
|
|
1677
|
+
const namePath = isString(args?.name_path) ? args.name_path : '';
|
|
1678
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : '';
|
|
1679
|
+
const body = isString(args?.body) ? args.body : '';
|
|
1680
|
+
if (!namePath || !relativePath || !body) {
|
|
1681
|
+
throw new LanceContextError('name_path, relative_path, and body are required', 'validation', { tool: 'insert_after_symbol' });
|
|
1682
|
+
}
|
|
1683
|
+
const editor = new SymbolEditor(PROJECT_PATH);
|
|
1684
|
+
const result = await editor.insertAfterSymbol({ namePath, relativePath, body });
|
|
1685
|
+
if (!result.success) {
|
|
1686
|
+
return {
|
|
1687
|
+
content: [
|
|
1688
|
+
{
|
|
1689
|
+
type: 'text',
|
|
1690
|
+
text: `Failed to insert after symbol: ${result.error}` + TOOL_GUIDANCE,
|
|
1691
|
+
},
|
|
1692
|
+
],
|
|
1693
|
+
isError: true,
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
return {
|
|
1697
|
+
content: [
|
|
1698
|
+
{
|
|
1699
|
+
type: 'text',
|
|
1700
|
+
text: `Content inserted after "${result.symbolName}" in ${result.filepath}.\n` +
|
|
1701
|
+
`Inserted at: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
|
|
1702
|
+
TOOL_GUIDANCE,
|
|
1703
|
+
},
|
|
1704
|
+
],
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
case 'rename_symbol': {
|
|
1708
|
+
const namePath = isString(args?.name_path) ? args.name_path : '';
|
|
1709
|
+
const relativePath = isString(args?.relative_path) ? args.relative_path : '';
|
|
1710
|
+
const newName = isString(args?.new_name) ? args.new_name : '';
|
|
1711
|
+
const dryRun = isBoolean(args?.dry_run) ? args.dry_run : false;
|
|
1712
|
+
if (!namePath || !relativePath || !newName) {
|
|
1713
|
+
throw new LanceContextError('name_path, relative_path, and new_name are required', 'validation', { tool: 'rename_symbol' });
|
|
1714
|
+
}
|
|
1715
|
+
const renamer = new SymbolRenamer(PROJECT_PATH);
|
|
1716
|
+
const result = await renamer.renameSymbol({ namePath, relativePath, newName, dryRun });
|
|
1717
|
+
if (!result.success) {
|
|
1718
|
+
return {
|
|
1719
|
+
content: [
|
|
1720
|
+
{
|
|
1721
|
+
type: 'text',
|
|
1722
|
+
text: `Failed to rename symbol: ${result.error}` + TOOL_GUIDANCE,
|
|
1723
|
+
},
|
|
1724
|
+
],
|
|
1725
|
+
isError: true,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
const modeLabel = dryRun ? ' (dry run)' : '';
|
|
1729
|
+
return {
|
|
1730
|
+
content: [
|
|
1731
|
+
{
|
|
1732
|
+
type: 'text',
|
|
1733
|
+
text: formatRenameResult(result) + modeLabel + TOOL_GUIDANCE,
|
|
1734
|
+
},
|
|
1735
|
+
],
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
// --- Worktree Tools ---
|
|
1739
|
+
case 'create_worktree': {
|
|
1740
|
+
const shortName = isString(args?.short_name) ? args.short_name : '';
|
|
1741
|
+
if (!shortName) {
|
|
1742
|
+
throw new LanceContextError('short_name is required', 'validation', {
|
|
1743
|
+
tool: 'create_worktree',
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
const worktreeManager = new WorktreeManager(PROJECT_PATH);
|
|
1747
|
+
const result = await worktreeManager.createWorktree({
|
|
1748
|
+
shortName,
|
|
1749
|
+
issueId: isString(args?.issue_id) ? args.issue_id : undefined,
|
|
1750
|
+
prefix: isString(args?.prefix)
|
|
1751
|
+
? args.prefix
|
|
1752
|
+
: undefined,
|
|
1753
|
+
baseBranch: isString(args?.base_branch) ? args.base_branch : undefined,
|
|
1754
|
+
installDeps: isBoolean(args?.install_deps) ? args.install_deps : true,
|
|
1755
|
+
packageManager: isString(args?.package_manager)
|
|
1756
|
+
? args.package_manager
|
|
1757
|
+
: undefined,
|
|
1758
|
+
});
|
|
1759
|
+
if (!result.success) {
|
|
1760
|
+
return {
|
|
1761
|
+
content: [
|
|
1762
|
+
{
|
|
1763
|
+
type: 'text',
|
|
1764
|
+
text: `Failed to create worktree: ${result.error}` + TOOL_GUIDANCE,
|
|
1765
|
+
},
|
|
1766
|
+
],
|
|
1767
|
+
isError: true,
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
const worktree = result.worktree;
|
|
1771
|
+
const parts = [];
|
|
1772
|
+
parts.push('## Worktree Created\n');
|
|
1773
|
+
parts.push(`**Name:** ${worktree?.name}`);
|
|
1774
|
+
parts.push(`**Path:** ${worktree?.path}`);
|
|
1775
|
+
parts.push(`**Branch:** ${worktree?.branch}`);
|
|
1776
|
+
if (result.depsInstalled !== undefined) {
|
|
1777
|
+
const depsStatus = result.depsInstalled ? 'installed' : 'skipped/failed';
|
|
1778
|
+
const timeInfo = result.depsInstallTime ? ` (${result.depsInstallTime}ms)` : '';
|
|
1779
|
+
parts.push(`**Dependencies:** ${depsStatus}${timeInfo}`);
|
|
1780
|
+
}
|
|
1781
|
+
parts.push('\n**Usage:** Spawn agent with `cwd: "' + (worktree?.path ?? '') + '"`');
|
|
1782
|
+
return {
|
|
1783
|
+
content: [
|
|
1784
|
+
{
|
|
1785
|
+
type: 'text',
|
|
1786
|
+
text: parts.join('\n') + TOOL_GUIDANCE,
|
|
1787
|
+
},
|
|
1788
|
+
],
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
case 'list_worktrees': {
|
|
1792
|
+
const worktreeManager = new WorktreeManager(PROJECT_PATH);
|
|
1793
|
+
const result = await worktreeManager.listWorktrees();
|
|
1794
|
+
return {
|
|
1795
|
+
content: [
|
|
1796
|
+
{
|
|
1797
|
+
type: 'text',
|
|
1798
|
+
text: formatWorktreeList(result) + TOOL_GUIDANCE,
|
|
1799
|
+
},
|
|
1800
|
+
],
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
case 'remove_worktree': {
|
|
1804
|
+
const worktreeName = isString(args?.name) ? args.name : '';
|
|
1805
|
+
if (!worktreeName) {
|
|
1806
|
+
throw new LanceContextError('name is required', 'validation', {
|
|
1807
|
+
tool: 'remove_worktree',
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
const worktreeManager = new WorktreeManager(PROJECT_PATH);
|
|
1811
|
+
const result = await worktreeManager.removeWorktree({
|
|
1812
|
+
name: worktreeName,
|
|
1813
|
+
deleteBranch: isBoolean(args?.delete_branch) ? args.delete_branch : false,
|
|
1814
|
+
force: isBoolean(args?.force) ? args.force : false,
|
|
1815
|
+
});
|
|
1816
|
+
if (!result.success) {
|
|
1817
|
+
return {
|
|
1818
|
+
content: [
|
|
1819
|
+
{
|
|
1820
|
+
type: 'text',
|
|
1821
|
+
text: `Failed to remove worktree: ${result.error}` + TOOL_GUIDANCE,
|
|
1822
|
+
},
|
|
1823
|
+
],
|
|
1824
|
+
isError: true,
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
const parts = [];
|
|
1828
|
+
parts.push('## Worktree Removed\n');
|
|
1829
|
+
parts.push(`**Name:** ${worktreeName}`);
|
|
1830
|
+
if (result.branch) {
|
|
1831
|
+
parts.push(`**Branch:** ${result.branch}`);
|
|
1832
|
+
parts.push(`**Branch deleted:** ${result.branchDeleted ? 'yes' : 'no'}`);
|
|
1833
|
+
}
|
|
1834
|
+
return {
|
|
1835
|
+
content: [
|
|
1836
|
+
{
|
|
1837
|
+
type: 'text',
|
|
1838
|
+
text: parts.join('\n') + TOOL_GUIDANCE,
|
|
1839
|
+
},
|
|
1840
|
+
],
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
case 'worktree_status': {
|
|
1844
|
+
const worktreeName = isString(args?.name) ? args.name : '';
|
|
1845
|
+
if (!worktreeName) {
|
|
1846
|
+
throw new LanceContextError('name is required', 'validation', {
|
|
1847
|
+
tool: 'worktree_status',
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
const worktreeManager = new WorktreeManager(PROJECT_PATH);
|
|
1851
|
+
const info = await worktreeManager.getWorktreeInfo(worktreeName);
|
|
1852
|
+
if (!info) {
|
|
1853
|
+
return {
|
|
1854
|
+
content: [
|
|
1855
|
+
{
|
|
1856
|
+
type: 'text',
|
|
1857
|
+
text: `Worktree "${worktreeName}" not found.` + TOOL_GUIDANCE,
|
|
1858
|
+
},
|
|
1859
|
+
],
|
|
1860
|
+
isError: true,
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
content: [
|
|
1865
|
+
{
|
|
1866
|
+
type: 'text',
|
|
1867
|
+
text: formatWorktreeInfo(info) + TOOL_GUIDANCE,
|
|
138
1868
|
},
|
|
139
1869
|
],
|
|
140
1870
|
};
|
|
141
1871
|
}
|
|
142
1872
|
default:
|
|
143
|
-
throw new
|
|
1873
|
+
throw new LanceContextError(`Unknown tool: ${name}`, 'validation', { tool: name });
|
|
144
1874
|
}
|
|
145
1875
|
}
|
|
146
1876
|
catch (error) {
|
|
1877
|
+
// Log full error details server-side for debugging
|
|
1878
|
+
logError(error, name);
|
|
147
1879
|
return {
|
|
148
1880
|
content: [
|
|
149
1881
|
{
|
|
150
1882
|
type: 'text',
|
|
151
|
-
text:
|
|
1883
|
+
text: formatErrorResponse(error),
|
|
152
1884
|
},
|
|
153
1885
|
],
|
|
154
1886
|
isError: true,
|
|
@@ -157,10 +1889,105 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
157
1889
|
});
|
|
158
1890
|
// Start server
|
|
159
1891
|
async function main() {
|
|
1892
|
+
// Check for updates in background (non-blocking)
|
|
1893
|
+
checkForUpdates();
|
|
1894
|
+
// Load config to check if dashboard is enabled
|
|
1895
|
+
const config = await getConfig();
|
|
1896
|
+
const dashboardConfig = getDashboardConfig(config);
|
|
1897
|
+
// Initialize the indexer eagerly so dashboard has data
|
|
1898
|
+
let indexer = null;
|
|
1899
|
+
try {
|
|
1900
|
+
indexer = await getIndexer();
|
|
1901
|
+
}
|
|
1902
|
+
catch (error) {
|
|
1903
|
+
console.error('[lance-context] Failed to initialize indexer:', error);
|
|
1904
|
+
}
|
|
1905
|
+
// Auto-index if project is not yet indexed or backend has changed
|
|
1906
|
+
if (indexer) {
|
|
1907
|
+
const status = await indexer.getStatus();
|
|
1908
|
+
const needsIndex = !status.indexed;
|
|
1909
|
+
const needsReindex = status.indexed && status.backendMismatch;
|
|
1910
|
+
if (needsIndex) {
|
|
1911
|
+
console.error('[lance-context] Project not indexed, starting auto-index...');
|
|
1912
|
+
}
|
|
1913
|
+
else if (needsReindex) {
|
|
1914
|
+
console.error(`[lance-context] ${status.backendMismatchReason}`);
|
|
1915
|
+
console.error('[lance-context] Starting automatic reindex with new backend...');
|
|
1916
|
+
}
|
|
1917
|
+
if (needsIndex || needsReindex) {
|
|
1918
|
+
dashboardState.onIndexingStart();
|
|
1919
|
+
// Run indexing in background so server can start immediately
|
|
1920
|
+
// Force reindex if backend changed to rebuild all vectors
|
|
1921
|
+
indexer
|
|
1922
|
+
.indexCodebase(undefined, undefined, needsReindex, (progress) => {
|
|
1923
|
+
dashboardState.onProgress(progress);
|
|
1924
|
+
})
|
|
1925
|
+
.then((result) => {
|
|
1926
|
+
dashboardState.onIndexingComplete(result);
|
|
1927
|
+
console.error(`[lance-context] Auto-index complete: ${result.filesIndexed} files, ${result.chunksCreated} chunks`);
|
|
1928
|
+
})
|
|
1929
|
+
.catch((error) => {
|
|
1930
|
+
console.error('[lance-context] Auto-index failed:', error);
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
// Start dashboard if enabled
|
|
1935
|
+
if (dashboardConfig.enabled) {
|
|
1936
|
+
const dashboardPort = dashboardConfig.port || 24300;
|
|
1937
|
+
const portAvailable = await isPortAvailable(dashboardPort);
|
|
1938
|
+
if (!portAvailable) {
|
|
1939
|
+
// Another process is already running the dashboard
|
|
1940
|
+
console.error(`[lance-context] Dashboard already running on port ${dashboardPort}`);
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
try {
|
|
1944
|
+
const dashboard = await startDashboard({
|
|
1945
|
+
port: dashboardPort,
|
|
1946
|
+
config,
|
|
1947
|
+
projectPath: PROJECT_PATH,
|
|
1948
|
+
version: PACKAGE_VERSION,
|
|
1949
|
+
});
|
|
1950
|
+
console.error(`[lance-context] Dashboard started at ${dashboard.url}`);
|
|
1951
|
+
// Open dashboard in user's default browser if configured
|
|
1952
|
+
if (dashboardConfig.openBrowser) {
|
|
1953
|
+
openBrowser(dashboard.url, PROJECT_PATH);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
catch (error) {
|
|
1957
|
+
console.error('[lance-context] Failed to start dashboard:', error);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
160
1961
|
const transport = new StdioServerTransport();
|
|
161
1962
|
await server.connect(transport);
|
|
162
1963
|
console.error('[lance-context] MCP server started');
|
|
163
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Gracefully shutdown the server and cleanup resources
|
|
1967
|
+
*/
|
|
1968
|
+
async function shutdown(signal) {
|
|
1969
|
+
console.error(`[lance-context] Received ${signal}, shutting down gracefully...`);
|
|
1970
|
+
try {
|
|
1971
|
+
// Stop the dashboard server
|
|
1972
|
+
await stopDashboard();
|
|
1973
|
+
console.error('[lance-context] Dashboard stopped');
|
|
1974
|
+
}
|
|
1975
|
+
catch (error) {
|
|
1976
|
+
console.error('[lance-context] Error stopping dashboard:', error);
|
|
1977
|
+
}
|
|
1978
|
+
// Close the MCP server connection
|
|
1979
|
+
try {
|
|
1980
|
+
await server.close();
|
|
1981
|
+
console.error('[lance-context] MCP server closed');
|
|
1982
|
+
}
|
|
1983
|
+
catch (error) {
|
|
1984
|
+
console.error('[lance-context] Error closing MCP server:', error);
|
|
1985
|
+
}
|
|
1986
|
+
process.exit(0);
|
|
1987
|
+
}
|
|
1988
|
+
// Register signal handlers for graceful shutdown
|
|
1989
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1990
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
164
1991
|
main().catch((error) => {
|
|
165
1992
|
console.error('[lance-context] Fatal error:', error);
|
|
166
1993
|
process.exit(1);
|