@sashabogi/foundation 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/cli/setup-wizard.d.ts +30 -0
  2. package/dist/cli/setup-wizard.d.ts.map +1 -0
  3. package/dist/cli/setup-wizard.js +1645 -0
  4. package/dist/cli/setup-wizard.js.map +1 -0
  5. package/dist/cli/test-connection.d.ts +76 -0
  6. package/dist/cli/test-connection.d.ts.map +1 -0
  7. package/dist/cli/test-connection.js +697 -0
  8. package/dist/cli/test-connection.js.map +1 -0
  9. package/dist/cli.d.ts +3 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +47 -4
  12. package/dist/cli.js.map +1 -1
  13. package/dist/providers/anthropic.d.ts +178 -0
  14. package/dist/providers/anthropic.d.ts.map +1 -0
  15. package/dist/providers/anthropic.js +514 -0
  16. package/dist/providers/anthropic.js.map +1 -0
  17. package/dist/providers/base.d.ts +154 -0
  18. package/dist/providers/base.d.ts.map +1 -0
  19. package/dist/providers/base.js +227 -0
  20. package/dist/providers/base.js.map +1 -0
  21. package/dist/providers/deepseek.d.ts +23 -0
  22. package/dist/providers/deepseek.d.ts.map +1 -0
  23. package/dist/providers/deepseek.js +31 -0
  24. package/dist/providers/deepseek.js.map +1 -0
  25. package/dist/providers/fireworks.d.ts +23 -0
  26. package/dist/providers/fireworks.d.ts.map +1 -0
  27. package/dist/providers/fireworks.js +31 -0
  28. package/dist/providers/fireworks.js.map +1 -0
  29. package/dist/providers/gemini.d.ts +85 -0
  30. package/dist/providers/gemini.d.ts.map +1 -0
  31. package/dist/providers/gemini.js +414 -0
  32. package/dist/providers/gemini.js.map +1 -0
  33. package/dist/providers/groq.d.ts +23 -0
  34. package/dist/providers/groq.d.ts.map +1 -0
  35. package/dist/providers/groq.js +31 -0
  36. package/dist/providers/groq.js.map +1 -0
  37. package/dist/providers/index.d.ts +23 -0
  38. package/dist/providers/index.d.ts.map +1 -0
  39. package/dist/providers/index.js +27 -0
  40. package/dist/providers/index.js.map +1 -0
  41. package/dist/providers/kimi-code.d.ts +32 -0
  42. package/dist/providers/kimi-code.d.ts.map +1 -0
  43. package/dist/providers/kimi-code.js +46 -0
  44. package/dist/providers/kimi-code.js.map +1 -0
  45. package/dist/providers/kimi.d.ts +19 -0
  46. package/dist/providers/kimi.d.ts.map +1 -0
  47. package/dist/providers/kimi.js +27 -0
  48. package/dist/providers/kimi.js.map +1 -0
  49. package/dist/providers/manager.d.ts +144 -0
  50. package/dist/providers/manager.d.ts.map +1 -0
  51. package/dist/providers/manager.js +279 -0
  52. package/dist/providers/manager.js.map +1 -0
  53. package/dist/providers/ollama.d.ts +83 -0
  54. package/dist/providers/ollama.d.ts.map +1 -0
  55. package/dist/providers/ollama.js +450 -0
  56. package/dist/providers/ollama.js.map +1 -0
  57. package/dist/providers/openai.d.ts +91 -0
  58. package/dist/providers/openai.d.ts.map +1 -0
  59. package/dist/providers/openai.js +445 -0
  60. package/dist/providers/openai.js.map +1 -0
  61. package/dist/providers/openrouter.d.ts +23 -0
  62. package/dist/providers/openrouter.d.ts.map +1 -0
  63. package/dist/providers/openrouter.js +31 -0
  64. package/dist/providers/openrouter.js.map +1 -0
  65. package/dist/providers/perplexity.d.ts +34 -0
  66. package/dist/providers/perplexity.d.ts.map +1 -0
  67. package/dist/providers/perplexity.js +58 -0
  68. package/dist/providers/perplexity.js.map +1 -0
  69. package/dist/providers/together.d.ts +23 -0
  70. package/dist/providers/together.d.ts.map +1 -0
  71. package/dist/providers/together.js +31 -0
  72. package/dist/providers/together.js.map +1 -0
  73. package/dist/providers/types.d.ts +229 -0
  74. package/dist/providers/types.d.ts.map +1 -0
  75. package/dist/providers/types.js +73 -0
  76. package/dist/providers/types.js.map +1 -0
  77. package/dist/providers/zai.d.ts +19 -0
  78. package/dist/providers/zai.d.ts.map +1 -0
  79. package/dist/providers/zai.js +27 -0
  80. package/dist/providers/zai.js.map +1 -0
  81. package/dist/services/provider.service.d.ts +28 -0
  82. package/dist/services/provider.service.d.ts.map +1 -1
  83. package/dist/services/provider.service.js +137 -13
  84. package/dist/services/provider.service.js.map +1 -1
  85. package/dist/tools/demerzel/engine.d.ts +67 -0
  86. package/dist/tools/demerzel/engine.d.ts.map +1 -0
  87. package/dist/tools/demerzel/engine.js +401 -0
  88. package/dist/tools/demerzel/engine.js.map +1 -0
  89. package/dist/tools/demerzel/enhanced-snapshot.d.ts +67 -0
  90. package/dist/tools/demerzel/enhanced-snapshot.d.ts.map +1 -0
  91. package/dist/tools/demerzel/enhanced-snapshot.js +481 -0
  92. package/dist/tools/demerzel/enhanced-snapshot.js.map +1 -0
  93. package/dist/tools/demerzel/index.d.ts +11 -0
  94. package/dist/tools/demerzel/index.d.ts.map +1 -1
  95. package/dist/tools/demerzel/index.js +656 -85
  96. package/dist/tools/demerzel/index.js.map +1 -1
  97. package/dist/tools/demerzel/prompts.d.ts +26 -0
  98. package/dist/tools/demerzel/prompts.d.ts.map +1 -0
  99. package/dist/tools/demerzel/prompts.js +181 -0
  100. package/dist/tools/demerzel/prompts.js.map +1 -0
  101. package/dist/tools/demerzel/semantic-search.d.ts +54 -0
  102. package/dist/tools/demerzel/semantic-search.d.ts.map +1 -0
  103. package/dist/tools/demerzel/semantic-search.js +205 -0
  104. package/dist/tools/demerzel/semantic-search.js.map +1 -0
  105. package/dist/tools/demerzel/snapshot.d.ts +30 -0
  106. package/dist/tools/demerzel/snapshot.d.ts.map +1 -0
  107. package/dist/tools/demerzel/snapshot.js +169 -0
  108. package/dist/tools/demerzel/snapshot.js.map +1 -0
  109. package/package.json +2 -1
@@ -15,24 +15,241 @@
15
15
  * - demerzel_get_context: Get lines around a location
16
16
  * - demerzel_analyze: AI-powered deep analysis
17
17
  * - demerzel_semantic_search: Natural language code search
18
+ *
19
+ * Ported from Argus to Foundation.
18
20
  */
19
21
  import { z } from 'zod';
22
+ import { existsSync, readFileSync, statSync, mkdtempSync, unlinkSync } from 'fs';
23
+ import { join, resolve } from 'path';
24
+ import { tmpdir } from 'os';
25
+ import { createSnapshot, DEFAULT_EXTENSIONS, DEFAULT_EXCLUDE_PATTERNS } from './snapshot.js';
26
+ import { createEnhancedSnapshot } from './enhanced-snapshot.js';
27
+ import { analyze, searchDocument } from './engine.js';
28
+ import { SemanticIndex } from './semantic-search.js';
29
+ import { ConfigService } from '../../services/config.service.js';
30
+ import { ProviderService } from '../../services/provider.service.js';
31
+ import { isSessionDelegation } from '../../providers/types.js';
32
+ // Tool limits and defaults
33
+ const DEFAULT_FIND_FILES_LIMIT = 100;
34
+ const MAX_FIND_FILES_LIMIT = 500;
35
+ const DEFAULT_SEARCH_RESULTS = 50;
36
+ const MAX_SEARCH_RESULTS = 200;
37
+ const MAX_PATTERN_LENGTH = 500;
38
+ const MAX_WILDCARDS = 20;
39
+ /**
40
+ * Parse metadata section from an enhanced snapshot
41
+ */
42
+ function parseSnapshotMetadata(content) {
43
+ // Check if this is an enhanced snapshot
44
+ if (!content.includes('METADATA: IMPORT GRAPH')) {
45
+ return null;
46
+ }
47
+ const importGraph = {};
48
+ const exportGraph = {};
49
+ const symbolIndex = {};
50
+ const exports = [];
51
+ // Parse import graph
52
+ const importSection = content.match(/METADATA: IMPORT GRAPH\n=+\n([\s\S]*?)\n\n=+\nMETADATA:/)?.[1] || '';
53
+ for (const block of importSection.split('\n\n')) {
54
+ const lines = block.trim().split('\n');
55
+ if (lines.length > 0 && lines[0].endsWith(':')) {
56
+ const file = lines[0].slice(0, -1);
57
+ importGraph[file] = lines.slice(1).map(l => l.replace(/^\s*->\s*/, '').trim()).filter(Boolean);
58
+ }
59
+ }
60
+ // Parse export index (symbol -> files)
61
+ const exportSection = content.match(/METADATA: EXPORT INDEX\n=+\n([\s\S]*?)\n\n=+\nMETADATA:/)?.[1] || '';
62
+ for (const line of exportSection.split('\n')) {
63
+ const match = line.match(/^([\w$]+):\s*(.+)$/);
64
+ if (match) {
65
+ symbolIndex[match[1]] = match[2].split(',').map(s => s.trim());
66
+ }
67
+ }
68
+ // Parse who imports whom (reverse graph)
69
+ const whoImportsSection = content.match(/METADATA: WHO IMPORTS WHOM\n=+\n([\s\S]*)$/)?.[1] || '';
70
+ for (const block of whoImportsSection.split('\n\n')) {
71
+ const lines = block.trim().split('\n');
72
+ if (lines.length > 0 && lines[0].includes(' is imported by:')) {
73
+ const file = lines[0].replace(' is imported by:', '').trim();
74
+ exportGraph[file] = lines.slice(1).map(l => l.replace(/^\s*<-\s*/, '').trim()).filter(Boolean);
75
+ }
76
+ }
77
+ // Parse file exports
78
+ const fileExportsSection = content.match(/METADATA: FILE EXPORTS\n=+\n([\s\S]*?)\n\n=+\nMETADATA:/)?.[1] || '';
79
+ for (const line of fileExportsSection.split('\n')) {
80
+ const match = line.match(/^([^:]+):(\d+)\s*-\s*(\w+)\s+(.+)$/);
81
+ if (match) {
82
+ exports.push({
83
+ file: match[1],
84
+ line: parseInt(match[2]),
85
+ type: match[3],
86
+ symbol: match[4].split(' ')[0], // Take first word as symbol name
87
+ });
88
+ }
89
+ }
90
+ return { importGraph, exportGraph, symbolIndex, exports };
91
+ }
92
+ /**
93
+ * Create a stub provider for when no provider is configured
94
+ * Returns an error message indicating configuration is needed
95
+ */
96
+ function createStubProvider() {
97
+ return {
98
+ name: 'stub',
99
+ async complete(_messages) {
100
+ return {
101
+ content: '<<<FINAL>>>Analysis requires a configured AI provider. Please configure Foundation with an API key.<<<END>>>',
102
+ finishReason: 'stop',
103
+ };
104
+ },
105
+ };
106
+ }
107
+ /**
108
+ * Create a provider adapter that bridges Demerzel's AIProvider interface
109
+ * with Foundation's ProviderService.
110
+ *
111
+ * @param providerName - Name of the provider to use (e.g., 'ollama', 'anthropic')
112
+ * @param model - Model identifier to use for completions
113
+ * @returns AIProvider instance that delegates to Foundation's provider system
114
+ */
115
+ function createProviderAdapter(providerName, model) {
116
+ return {
117
+ name: providerName,
118
+ async complete(messages, options) {
119
+ try {
120
+ // Get the provider from ProviderService
121
+ const providerManager = ProviderService.getInstance().getProviderManager();
122
+ // Check if provider is configured
123
+ if (!providerManager.isConfigured(providerName)) {
124
+ return {
125
+ content: `<<<FINAL>>>Provider '${providerName}' is not configured. Please configure it in ~/.foundation/config.yaml or use a different provider.<<<END>>>`,
126
+ finishReason: 'error',
127
+ };
128
+ }
129
+ const provider = providerManager.get(providerName);
130
+ // Extract system message if present (Demerzel uses system role, Foundation doesn't)
131
+ let systemContent = '';
132
+ const conversationMessages = [];
133
+ for (const msg of messages) {
134
+ if (msg.role === 'system') {
135
+ systemContent = msg.content;
136
+ }
137
+ else {
138
+ conversationMessages.push({
139
+ role: msg.role,
140
+ content: msg.content,
141
+ });
142
+ }
143
+ }
144
+ // If we have a system message, prepend it to the first user message
145
+ if (systemContent && conversationMessages.length > 0 && conversationMessages[0].role === 'user') {
146
+ conversationMessages[0] = {
147
+ role: 'user',
148
+ content: `${systemContent}\n\n${conversationMessages[0].content}`,
149
+ };
150
+ }
151
+ else if (systemContent) {
152
+ // If no user message yet, add system as user message
153
+ conversationMessages.unshift({
154
+ role: 'user',
155
+ content: systemContent,
156
+ });
157
+ }
158
+ // Build the completion request
159
+ const request = {
160
+ model,
161
+ messages: conversationMessages,
162
+ temperature: options?.temperature ?? 0.7,
163
+ max_tokens: options?.maxTokens ?? 4096,
164
+ };
165
+ // Execute the completion
166
+ const response = await provider.complete(request);
167
+ // Handle session delegation response
168
+ if (isSessionDelegation(response)) {
169
+ // For session delegation, return the instructions as the content
170
+ return {
171
+ content: response.instructions,
172
+ finishReason: 'stop',
173
+ };
174
+ }
175
+ // Extract text content from the response
176
+ const textContent = response.content
177
+ .filter(block => block.type === 'text')
178
+ .map(block => block.text || '')
179
+ .join('\n');
180
+ // Convert usage information
181
+ const usage = response.usage ? {
182
+ promptTokens: response.usage.input_tokens,
183
+ completionTokens: response.usage.output_tokens,
184
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens,
185
+ } : undefined;
186
+ return {
187
+ content: textContent,
188
+ finishReason: response.stop_reason === 'end_turn' ? 'stop' :
189
+ response.stop_reason === 'max_tokens' ? 'length' : 'stop',
190
+ usage,
191
+ };
192
+ }
193
+ catch (error) {
194
+ const errorMessage = error instanceof Error ? error.message : String(error);
195
+ return {
196
+ content: `<<<FINAL>>>Error calling provider '${providerName}': ${errorMessage}<<<END>>>`,
197
+ finishReason: 'error',
198
+ };
199
+ }
200
+ },
201
+ };
202
+ }
20
203
  export function registerDemerzelTools(server) {
21
204
  // demerzel_snapshot
22
205
  server.tool('demerzel_snapshot', 'Create a codebase snapshot for efficient searching and analysis. Enhanced snapshots include import graph and export index. "I have been watching for 20,000 years."', {
23
206
  path: z.string().describe('Path to the codebase directory'),
24
207
  enhanced: z.boolean().optional().describe('Create enhanced snapshot with import graph (default: true)'),
25
208
  output: z.string().optional().describe('Output path for snapshot (default: .foundation/snapshot.txt)'),
26
- }, async ({ path, enhanced = true, output: _output }) => {
27
- // TODO: Port implementation from argus
28
- return {
29
- content: [
30
- {
31
- type: 'text',
32
- text: `[Stub] Would create ${enhanced ? 'enhanced' : 'basic'} snapshot for ${path}`,
33
- },
34
- ],
35
- };
209
+ }, async ({ path, enhanced = true, output }) => {
210
+ const projectPath = resolve(path);
211
+ if (!existsSync(projectPath)) {
212
+ return {
213
+ content: [{ type: 'text', text: `Error: Path not found: ${projectPath}` }],
214
+ isError: true,
215
+ };
216
+ }
217
+ const outputPath = output ? resolve(output) : join(projectPath, '.foundation', 'snapshot.txt');
218
+ try {
219
+ const result = enhanced
220
+ ? createEnhancedSnapshot(projectPath, outputPath, {
221
+ extensions: DEFAULT_EXTENSIONS,
222
+ excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
223
+ })
224
+ : createSnapshot(projectPath, outputPath, {
225
+ extensions: DEFAULT_EXTENSIONS,
226
+ excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
227
+ });
228
+ const response = {
229
+ outputPath: result.outputPath,
230
+ fileCount: result.fileCount,
231
+ totalLines: result.totalLines,
232
+ totalSize: result.totalSize,
233
+ enhanced,
234
+ };
235
+ if (enhanced && 'metadata' in result) {
236
+ const enhancedResult = result;
237
+ response.metadata = {
238
+ imports: enhancedResult.metadata.imports.length,
239
+ exports: enhancedResult.metadata.exports.length,
240
+ symbols: Object.keys(enhancedResult.metadata.symbolIndex).length,
241
+ };
242
+ }
243
+ return {
244
+ content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
245
+ };
246
+ }
247
+ catch (error) {
248
+ return {
249
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
250
+ isError: true,
251
+ };
252
+ }
36
253
  });
37
254
  // demerzel_search
38
255
  server.tool('demerzel_search', 'Fast regex search across codebase. Zero AI cost. Returns file:line:content matches.', {
@@ -40,77 +257,269 @@ export function registerDemerzelTools(server) {
40
257
  pattern: z.string().describe('Regex pattern to search'),
41
258
  maxResults: z.number().optional().describe('Maximum results (default: 50)'),
42
259
  caseInsensitive: z.boolean().optional().describe('Case insensitive search (default: true)'),
43
- }, async ({ path, pattern, maxResults: _maxResults = 50, caseInsensitive: _caseInsensitive = true }) => {
44
- // TODO: Port implementation from argus
45
- return {
46
- content: [
47
- {
48
- type: 'text',
49
- text: `[Stub] Would search for "${pattern}" in ${path}`,
50
- },
51
- ],
52
- };
260
+ }, async ({ path, pattern, maxResults = DEFAULT_SEARCH_RESULTS, caseInsensitive = true }) => {
261
+ const snapshotPath = resolve(path);
262
+ if (!pattern || pattern.trim() === '') {
263
+ return {
264
+ content: [{ type: 'text', text: 'Error: Pattern cannot be empty' }],
265
+ isError: true,
266
+ };
267
+ }
268
+ if (!existsSync(snapshotPath)) {
269
+ return {
270
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}. Run demerzel_snapshot first.` }],
271
+ isError: true,
272
+ };
273
+ }
274
+ try {
275
+ const matches = searchDocument(snapshotPath, pattern, {
276
+ caseInsensitive,
277
+ maxResults: Math.min(maxResults, MAX_SEARCH_RESULTS),
278
+ });
279
+ const formattedMatches = matches.map(m => ({
280
+ lineNum: m.lineNum,
281
+ line: m.line.trim(),
282
+ match: m.match,
283
+ }));
284
+ return {
285
+ content: [{
286
+ type: 'text',
287
+ text: JSON.stringify({
288
+ count: formattedMatches.length,
289
+ matches: formattedMatches,
290
+ }, null, 2),
291
+ }],
292
+ };
293
+ }
294
+ catch (error) {
295
+ return {
296
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
297
+ isError: true,
298
+ };
299
+ }
53
300
  });
54
301
  // demerzel_find_files
55
302
  server.tool('demerzel_find_files', 'Find files matching a glob pattern. Zero AI cost.', {
56
303
  path: z.string().describe('Path to snapshot file'),
57
304
  pattern: z.string().describe('Glob pattern (e.g., "*.ts", "src/**/*.tsx")'),
58
305
  limit: z.number().optional().describe('Maximum results (default: 100)'),
59
- }, async ({ path, pattern, limit: _limit = 100 }) => {
60
- // TODO: Port implementation from argus
61
- return {
62
- content: [
63
- {
64
- type: 'text',
65
- text: `[Stub] Would find files matching "${pattern}" in ${path}`,
66
- },
67
- ],
68
- };
306
+ }, async ({ path, pattern, limit = DEFAULT_FIND_FILES_LIMIT }) => {
307
+ const snapshotPath = resolve(path);
308
+ if (!pattern || pattern.trim() === '') {
309
+ return {
310
+ content: [{ type: 'text', text: 'Error: Pattern cannot be empty' }],
311
+ isError: true,
312
+ };
313
+ }
314
+ if (pattern.length > MAX_PATTERN_LENGTH) {
315
+ return {
316
+ content: [{ type: 'text', text: `Error: Pattern too long (max ${MAX_PATTERN_LENGTH} characters)` }],
317
+ isError: true,
318
+ };
319
+ }
320
+ // ReDoS protection
321
+ const starCount = (pattern.match(/\*/g) || []).length;
322
+ if (starCount > MAX_WILDCARDS) {
323
+ return {
324
+ content: [{ type: 'text', text: `Error: Too many wildcards in pattern (max ${MAX_WILDCARDS})` }],
325
+ isError: true,
326
+ };
327
+ }
328
+ if (!existsSync(snapshotPath)) {
329
+ return {
330
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}. Run demerzel_snapshot first.` }],
331
+ isError: true,
332
+ };
333
+ }
334
+ try {
335
+ const content = readFileSync(snapshotPath, 'utf-8');
336
+ // Extract all FILE: markers
337
+ const fileRegex = /^FILE: \.\/(.+)$/gm;
338
+ const files = [];
339
+ let match;
340
+ while ((match = fileRegex.exec(content)) !== null) {
341
+ files.push(match[1]);
342
+ }
343
+ // Convert glob pattern to regex
344
+ let regexPattern = pattern
345
+ .replace(/[.+^${}()|[\]\\-]/g, '\\$&')
346
+ .replace(/\*\*/g, '<<<GLOBSTAR>>>')
347
+ .replace(/\*/g, '[^/]*?')
348
+ .replace(/<<<GLOBSTAR>>>/g, '.*?')
349
+ .replace(/\?/g, '.');
350
+ const regex = new RegExp(`^${regexPattern}$`, 'i');
351
+ const matching = files.filter(f => regex.test(f));
352
+ const limited = matching.slice(0, Math.min(limit, MAX_FIND_FILES_LIMIT)).sort();
353
+ return {
354
+ content: [{
355
+ type: 'text',
356
+ text: JSON.stringify({
357
+ pattern,
358
+ files: limited,
359
+ count: limited.length,
360
+ totalMatching: matching.length,
361
+ hasMore: matching.length > limit,
362
+ }, null, 2),
363
+ }],
364
+ };
365
+ }
366
+ catch (error) {
367
+ return {
368
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
369
+ isError: true,
370
+ };
371
+ }
69
372
  });
70
373
  // demerzel_find_symbol
71
374
  server.tool('demerzel_find_symbol', 'Locate where a function, class, or type is exported. Zero AI cost. Requires enhanced snapshot.', {
72
375
  path: z.string().describe('Path to snapshot file'),
73
376
  symbol: z.string().describe('Symbol name to find (e.g., "AuthProvider", "useAuth")'),
74
377
  }, async ({ path, symbol }) => {
75
- // TODO: Port implementation from argus
76
- return {
77
- content: [
78
- {
79
- type: 'text',
80
- text: `[Stub] Would find symbol "${symbol}" in ${path}`,
81
- },
82
- ],
83
- };
378
+ const snapshotPath = resolve(path);
379
+ if (!existsSync(snapshotPath)) {
380
+ return {
381
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}` }],
382
+ isError: true,
383
+ };
384
+ }
385
+ try {
386
+ const content = readFileSync(snapshotPath, 'utf-8');
387
+ const metadata = parseSnapshotMetadata(content);
388
+ if (!metadata) {
389
+ return {
390
+ content: [{ type: 'text', text: 'Error: This snapshot does not have metadata. Create with enhanced=true.' }],
391
+ isError: true,
392
+ };
393
+ }
394
+ const files = metadata.symbolIndex[symbol] || [];
395
+ const exportDetails = metadata.exports.filter(e => e.symbol === symbol);
396
+ return {
397
+ content: [{
398
+ type: 'text',
399
+ text: JSON.stringify({
400
+ symbol,
401
+ exportedFrom: files,
402
+ details: exportDetails,
403
+ count: files.length,
404
+ }, null, 2),
405
+ }],
406
+ };
407
+ }
408
+ catch (error) {
409
+ return {
410
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
411
+ isError: true,
412
+ };
413
+ }
84
414
  });
85
415
  // demerzel_find_importers
86
416
  server.tool('demerzel_find_importers', 'Find all files that import a given module. Zero AI cost. Requires enhanced snapshot.', {
87
417
  path: z.string().describe('Path to snapshot file'),
88
418
  target: z.string().describe('File path to find importers of (e.g., "src/auth.ts")'),
89
419
  }, async ({ path, target }) => {
90
- // TODO: Port implementation from argus
91
- return {
92
- content: [
93
- {
94
- type: 'text',
95
- text: `[Stub] Would find importers of "${target}" in ${path}`,
96
- },
97
- ],
98
- };
420
+ const snapshotPath = resolve(path);
421
+ if (!existsSync(snapshotPath)) {
422
+ return {
423
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}` }],
424
+ isError: true,
425
+ };
426
+ }
427
+ try {
428
+ const content = readFileSync(snapshotPath, 'utf-8');
429
+ const metadata = parseSnapshotMetadata(content);
430
+ if (!metadata) {
431
+ return {
432
+ content: [{ type: 'text', text: 'Error: This snapshot does not have metadata. Create with enhanced=true.' }],
433
+ isError: true,
434
+ };
435
+ }
436
+ // Normalize the target path
437
+ const normalizedTarget = target.startsWith('./') ? target.slice(2) : target;
438
+ const targetVariants = [normalizedTarget, './' + normalizedTarget, normalizedTarget.replace(/\.(ts|tsx|js|jsx)$/, '')];
439
+ // Find all files that import this target
440
+ const importers = [];
441
+ for (const [file, imports] of Object.entries(metadata.importGraph)) {
442
+ for (const imp of imports) {
443
+ if (targetVariants.some(v => imp === v || imp.endsWith('/' + v) || imp.includes(v))) {
444
+ importers.push(file);
445
+ break;
446
+ }
447
+ }
448
+ }
449
+ // Also check the exportGraph (direct mapping)
450
+ for (const variant of targetVariants) {
451
+ if (metadata.exportGraph[variant]) {
452
+ importers.push(...metadata.exportGraph[variant]);
453
+ }
454
+ }
455
+ const unique = [...new Set(importers)];
456
+ return {
457
+ content: [{
458
+ type: 'text',
459
+ text: JSON.stringify({
460
+ target,
461
+ importedBy: unique,
462
+ count: unique.length,
463
+ }, null, 2),
464
+ }],
465
+ };
466
+ }
467
+ catch (error) {
468
+ return {
469
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
470
+ isError: true,
471
+ };
472
+ }
99
473
  });
100
474
  // demerzel_get_deps
101
475
  server.tool('demerzel_get_deps', 'Get all dependencies (imports) of a specific file. Zero AI cost. Requires enhanced snapshot.', {
102
476
  path: z.string().describe('Path to snapshot file'),
103
477
  file: z.string().describe('File path to get dependencies for'),
104
478
  }, async ({ path, file }) => {
105
- // TODO: Port implementation from argus
106
- return {
107
- content: [
108
- {
109
- type: 'text',
110
- text: `[Stub] Would get dependencies of "${file}" in ${path}`,
111
- },
112
- ],
113
- };
479
+ const snapshotPath = resolve(path);
480
+ if (!existsSync(snapshotPath)) {
481
+ return {
482
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}` }],
483
+ isError: true,
484
+ };
485
+ }
486
+ try {
487
+ const content = readFileSync(snapshotPath, 'utf-8');
488
+ const metadata = parseSnapshotMetadata(content);
489
+ if (!metadata) {
490
+ return {
491
+ content: [{ type: 'text', text: 'Error: This snapshot does not have metadata. Create with enhanced=true.' }],
492
+ isError: true,
493
+ };
494
+ }
495
+ // Normalize the file path
496
+ const normalizedFile = file.startsWith('./') ? file.slice(2) : file;
497
+ const fileVariants = [normalizedFile, './' + normalizedFile];
498
+ // Find imports for this file
499
+ let imports = [];
500
+ for (const variant of fileVariants) {
501
+ if (metadata.importGraph[variant]) {
502
+ imports = metadata.importGraph[variant];
503
+ break;
504
+ }
505
+ }
506
+ return {
507
+ content: [{
508
+ type: 'text',
509
+ text: JSON.stringify({
510
+ file,
511
+ imports,
512
+ count: imports.length,
513
+ }, null, 2),
514
+ }],
515
+ };
516
+ }
517
+ catch (error) {
518
+ return {
519
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
520
+ isError: true,
521
+ };
522
+ }
114
523
  });
115
524
  // demerzel_get_context
116
525
  server.tool('demerzel_get_context', 'Get lines of code around a specific location. Zero AI cost. Use after search to get more context.', {
@@ -120,31 +529,145 @@ export function registerDemerzelTools(server) {
120
529
  before: z.number().optional().describe('Lines before (default: 10)'),
121
530
  after: z.number().optional().describe('Lines after (default: 10)'),
122
531
  }, async ({ path, file, line, before = 10, after = 10 }) => {
123
- // TODO: Port implementation from argus
124
- return {
125
- content: [
126
- {
127
- type: 'text',
128
- text: `[Stub] Would get context around line ${line} in ${file}`,
129
- },
130
- ],
131
- };
532
+ const snapshotPath = resolve(path);
533
+ if (!existsSync(snapshotPath)) {
534
+ return {
535
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}` }],
536
+ isError: true,
537
+ };
538
+ }
539
+ try {
540
+ const content = readFileSync(snapshotPath, 'utf-8');
541
+ // Find the file section in the snapshot
542
+ const normalizedTarget = file.replace(/^\.\//, '');
543
+ const fileMarkerVariants = [
544
+ `FILE: ./${normalizedTarget}`,
545
+ `FILE: ${normalizedTarget}`,
546
+ ];
547
+ let fileStart = -1;
548
+ for (const marker of fileMarkerVariants) {
549
+ fileStart = content.indexOf(marker);
550
+ if (fileStart !== -1)
551
+ break;
552
+ }
553
+ if (fileStart === -1) {
554
+ return {
555
+ content: [{ type: 'text', text: `Error: File not found in snapshot: ${file}` }],
556
+ isError: true,
557
+ };
558
+ }
559
+ // Find the end of this file section
560
+ const nextFileStart = content.indexOf('\nFILE:', fileStart + 1);
561
+ const metadataStart = content.indexOf('\nMETADATA:', fileStart);
562
+ const fileEnd = Math.min(nextFileStart === -1 ? Infinity : nextFileStart, metadataStart === -1 ? Infinity : metadataStart);
563
+ // Extract file content
564
+ const fileContent = content.slice(fileStart, fileEnd === Infinity ? undefined : fileEnd);
565
+ const fileLines = fileContent.split('\n').slice(2); // Skip FILE: header and separator
566
+ // Calculate range
567
+ const startLine = Math.max(0, line - before - 1);
568
+ const endLine = Math.min(fileLines.length, line + after);
569
+ // Extract context with line numbers
570
+ const contextLines = fileLines.slice(startLine, endLine).map((lineContent, idx) => {
571
+ const lineNum = startLine + idx + 1;
572
+ const marker = lineNum === line ? '>>>' : ' ';
573
+ return `${marker} ${lineNum.toString().padStart(4)}: ${lineContent}`;
574
+ });
575
+ return {
576
+ content: [{
577
+ type: 'text',
578
+ text: JSON.stringify({
579
+ file,
580
+ targetLine: line,
581
+ range: { start: startLine + 1, end: endLine },
582
+ content: contextLines.join('\n'),
583
+ totalLines: fileLines.length,
584
+ }, null, 2),
585
+ }],
586
+ };
587
+ }
588
+ catch (error) {
589
+ return {
590
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
591
+ isError: true,
592
+ };
593
+ }
132
594
  });
133
595
  // demerzel_analyze
134
596
  server.tool('demerzel_analyze', 'AI-powered deep analysis of codebase. Uses recursive reasoning across entire codebase. ~500 tokens cost.', {
135
- path: z.string().describe('Path to snapshot file'),
597
+ path: z.string().describe('Path to snapshot file or codebase directory'),
136
598
  query: z.string().describe('Question about the codebase (be specific for best results)'),
137
599
  maxTurns: z.number().optional().describe('Maximum reasoning turns (default: 15)'),
138
600
  }, async ({ path, query, maxTurns = 15 }) => {
139
- // TODO: Port implementation from argus
140
- return {
141
- content: [
142
- {
143
- type: 'text',
144
- text: `[Stub] Would analyze "${query}" in ${path}`,
145
- },
146
- ],
147
- };
601
+ const inputPath = resolve(path);
602
+ if (!existsSync(inputPath)) {
603
+ return {
604
+ content: [{ type: 'text', text: `Error: Path not found: ${inputPath}` }],
605
+ isError: true,
606
+ };
607
+ }
608
+ let snapshotPath = inputPath;
609
+ let tempSnapshot = false;
610
+ try {
611
+ // If it's a directory, create a temporary enhanced snapshot
612
+ const stats = statSync(inputPath);
613
+ if (stats.isDirectory()) {
614
+ const tempDir = mkdtempSync(join(tmpdir(), 'demerzel-'));
615
+ snapshotPath = join(tempDir, 'snapshot.txt');
616
+ createEnhancedSnapshot(inputPath, snapshotPath, {
617
+ extensions: DEFAULT_EXTENSIONS,
618
+ excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
619
+ });
620
+ tempSnapshot = true;
621
+ }
622
+ // Get provider configuration from ConfigService
623
+ const argusConfig = ConfigService.getInstance().getArgusConfig();
624
+ const providerName = argusConfig.provider ?? 'ollama';
625
+ const model = argusConfig.model ?? 'qwen2.5-coder:7b';
626
+ // Create provider adapter, falling back to stub if not configured
627
+ let provider;
628
+ try {
629
+ const providerManager = ProviderService.getInstance().getProviderManager();
630
+ if (providerManager.isConfigured(providerName)) {
631
+ provider = createProviderAdapter(providerName, model);
632
+ }
633
+ else {
634
+ // Fallback to stub provider with informative message
635
+ provider = createStubProvider();
636
+ }
637
+ }
638
+ catch {
639
+ // Fallback to stub if ProviderService is not initialized
640
+ provider = createStubProvider();
641
+ }
642
+ const result = await analyze(provider, snapshotPath, query, { maxTurns });
643
+ return {
644
+ content: [{
645
+ type: 'text',
646
+ text: JSON.stringify({
647
+ answer: result.answer,
648
+ success: result.success,
649
+ turns: result.turns,
650
+ commands: result.commands,
651
+ }, null, 2),
652
+ }],
653
+ };
654
+ }
655
+ catch (error) {
656
+ return {
657
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
658
+ isError: true,
659
+ };
660
+ }
661
+ finally {
662
+ if (tempSnapshot && existsSync(snapshotPath)) {
663
+ try {
664
+ unlinkSync(snapshotPath);
665
+ }
666
+ catch {
667
+ // Ignore cleanup errors
668
+ }
669
+ }
670
+ }
148
671
  });
149
672
  // demerzel_semantic_search
150
673
  server.tool('demerzel_semantic_search', 'Search code using natural language. Uses FTS5 full-text search. More flexible than regex.', {
@@ -152,15 +675,63 @@ export function registerDemerzelTools(server) {
152
675
  query: z.string().describe('Natural language query or code terms'),
153
676
  limit: z.number().optional().describe('Maximum results (default: 20)'),
154
677
  }, async ({ path, query, limit = 20 }) => {
155
- // TODO: Port implementation from argus
156
- return {
157
- content: [
158
- {
159
- type: 'text',
160
- text: `[Stub] Would semantic search "${query}" in ${path}`,
161
- },
162
- ],
163
- };
678
+ const projectPath = resolve(path);
679
+ if (!query || query.trim() === '') {
680
+ return {
681
+ content: [{ type: 'text', text: 'Error: Query cannot be empty' }],
682
+ isError: true,
683
+ };
684
+ }
685
+ const snapshotPath = join(projectPath, '.foundation', 'snapshot.txt');
686
+ const indexPath = join(projectPath, '.foundation', 'search.db');
687
+ if (!existsSync(snapshotPath)) {
688
+ return {
689
+ content: [{ type: 'text', text: `Error: Snapshot not found: ${snapshotPath}. Run demerzel_snapshot first.` }],
690
+ isError: true,
691
+ };
692
+ }
693
+ let index = null;
694
+ try {
695
+ index = new SemanticIndex(indexPath);
696
+ // Check if index needs rebuilding
697
+ const stats = index.getStats();
698
+ const snapshotMtime = statSync(snapshotPath).mtimeMs;
699
+ const needsReindex = !stats.lastIndexed ||
700
+ new Date(stats.lastIndexed).getTime() < snapshotMtime ||
701
+ stats.snapshotPath !== snapshotPath;
702
+ if (needsReindex) {
703
+ index.indexFromSnapshot(snapshotPath);
704
+ }
705
+ const results = index.search(query, limit);
706
+ return {
707
+ content: [{
708
+ type: 'text',
709
+ text: JSON.stringify({
710
+ query,
711
+ count: results.length,
712
+ results: results.map(r => ({
713
+ file: r.file,
714
+ symbol: r.symbol,
715
+ type: r.type,
716
+ snippet: r.content.split('\n').slice(0, 5).join('\n'),
717
+ })),
718
+ }, null, 2),
719
+ }],
720
+ };
721
+ }
722
+ catch (error) {
723
+ return {
724
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
725
+ isError: true,
726
+ };
727
+ }
728
+ finally {
729
+ if (index) {
730
+ index.close();
731
+ }
732
+ }
164
733
  });
165
734
  }
735
+ // Re-export types and functions for external use
736
+ export { createSnapshot, createEnhancedSnapshot, analyze, searchDocument, SemanticIndex };
166
737
  //# sourceMappingURL=index.js.map