codemini-cli 0.5.10 → 0.5.11

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 (59) hide show
  1. package/OPERATIONS.md +242 -242
  2. package/README.md +588 -588
  3. package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
  4. package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
  5. package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
  6. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
  7. package/codemini-web/dist/index.html +23 -23
  8. package/codemini-web/lib/approval-manager.js +32 -32
  9. package/codemini-web/lib/runtime-bridge.js +17 -11
  10. package/codemini-web/server.js +534 -205
  11. package/deployment.md +212 -212
  12. package/package.json +1 -1
  13. package/skills/brainstorm/SKILL.md +77 -77
  14. package/skills/codemini.skills.json +40 -40
  15. package/skills/grill-me/SKILL.md +30 -30
  16. package/skills/superpowers-lite/SKILL.md +82 -82
  17. package/src/cli.js +74 -74
  18. package/src/commands/chat.js +210 -210
  19. package/src/commands/run.js +313 -313
  20. package/src/commands/skill.js +438 -304
  21. package/src/commands/web.js +57 -57
  22. package/src/core/agent-loop.js +980 -980
  23. package/src/core/ast.js +309 -307
  24. package/src/core/chat-runtime.js +6261 -6253
  25. package/src/core/command-evaluator.js +72 -72
  26. package/src/core/command-loader.js +311 -311
  27. package/src/core/command-policy.js +301 -301
  28. package/src/core/command-risk.js +156 -156
  29. package/src/core/config-store.js +289 -289
  30. package/src/core/constants.js +18 -1
  31. package/src/core/context-compact.js +365 -365
  32. package/src/core/default-system-prompt.js +114 -107
  33. package/src/core/dream-audit.js +105 -105
  34. package/src/core/dream-consolidate.js +229 -229
  35. package/src/core/dream-evaluator.js +185 -185
  36. package/src/core/fff-adapter.js +383 -383
  37. package/src/core/memory-store.js +543 -543
  38. package/src/core/project-index.js +737 -548
  39. package/src/core/project-instructions.js +98 -98
  40. package/src/core/provider/anthropic.js +514 -514
  41. package/src/core/provider/openai-compatible.js +501 -501
  42. package/src/core/reflect-skill.js +178 -178
  43. package/src/core/reply-language.js +40 -40
  44. package/src/core/session-store.js +474 -474
  45. package/src/core/shell-profile.js +237 -237
  46. package/src/core/shell.js +323 -323
  47. package/src/core/soul.js +69 -69
  48. package/src/core/system-prompt-composer.js +52 -52
  49. package/src/core/tool-args.js +199 -154
  50. package/src/core/tool-output.js +184 -184
  51. package/src/core/tool-result-store.js +206 -206
  52. package/src/core/tools.js +3024 -2893
  53. package/src/core/version.js +11 -11
  54. package/src/tui/chat-app.js +5171 -5171
  55. package/src/tui/tool-activity/presenters/misc.js +30 -30
  56. package/src/tui/tool-activity/presenters/system.js +20 -20
  57. package/templates/project-requirements/report-shell.html +582 -582
  58. package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
  59. package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
@@ -1,383 +1,383 @@
1
- import { spawn } from 'node:child_process';
2
- import { LANGUAGE_FILE_TYPES } from './constants.js';
3
- import { getPackageInfo } from './version.js';
4
-
5
- const DEFAULT_COMMAND = 'fff-mcp';
6
- const DEFAULT_TIMEOUT_MS = 15_000;
7
-
8
- function clampNumber(value, min, max, fallback) {
9
- const num = Number(value);
10
- if (!Number.isFinite(num)) return fallback;
11
- return Math.min(max, Math.max(min, num));
12
- }
13
-
14
- function encodeMessage(payload) {
15
- const body = Buffer.from(JSON.stringify(payload), 'utf8');
16
- return Buffer.concat([
17
- Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8'),
18
- body
19
- ]);
20
- }
21
-
22
- function createMessageParser(onMessage) {
23
- let buffer = Buffer.alloc(0);
24
- return (chunk) => {
25
- buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
26
- while (buffer.length > 0) {
27
- const headerEnd = buffer.indexOf('\r\n\r\n');
28
- if (headerEnd < 0) return;
29
- const headerText = buffer.slice(0, headerEnd).toString('utf8');
30
- const match = headerText.match(/Content-Length:\s*(\d+)/i);
31
- if (!match) {
32
- buffer = Buffer.alloc(0);
33
- return;
34
- }
35
- const bodyLength = Number(match[1]);
36
- const totalLength = headerEnd + 4 + bodyLength;
37
- if (buffer.length < totalLength) return;
38
- const body = buffer.slice(headerEnd + 4, totalLength).toString('utf8');
39
- buffer = buffer.slice(totalLength);
40
- try {
41
- onMessage(JSON.parse(body));
42
- } catch {
43
- // Ignore malformed frames.
44
- }
45
- }
46
- };
47
- }
48
-
49
- class FffMcpClient {
50
- constructor({ workspaceRoot, command, timeoutMs }) {
51
- this.workspaceRoot = workspaceRoot;
52
- this.command = command;
53
- this.timeoutMs = timeoutMs;
54
- this.child = null;
55
- this.pending = new Map();
56
- this.nextId = 1;
57
- this.connectPromise = null;
58
- this.connected = false;
59
- this.closed = false;
60
- this.parser = createMessageParser((message) => this.handleMessage(message));
61
- }
62
-
63
- handleMessage(message) {
64
- if (!message || typeof message !== 'object') return;
65
- if (typeof message.id !== 'number') return;
66
- const pending = this.pending.get(message.id);
67
- if (!pending) return;
68
- this.pending.delete(message.id);
69
- if (message.error) {
70
- pending.reject(new Error(String(message.error?.message || 'Unknown MCP error')));
71
- return;
72
- }
73
- pending.resolve(message.result);
74
- }
75
-
76
- async connect() {
77
- if (this.connected) return;
78
- if (this.connectPromise) return this.connectPromise;
79
- this.connectPromise = this.start();
80
- try {
81
- await this.connectPromise;
82
- this.connected = true;
83
- } finally {
84
- this.connectPromise = null;
85
- }
86
- }
87
-
88
- async start() {
89
- if (this.closed) {
90
- throw new Error('FFF MCP client already disposed');
91
- }
92
- this.child = spawn(this.command, [], {
93
- cwd: this.workspaceRoot,
94
- stdio: ['pipe', 'pipe', 'pipe']
95
- });
96
-
97
- this.child.stdout.on('data', this.parser);
98
- this.child.stderr.on('data', () => {});
99
- this.child.on('error', (error) => {
100
- this.rejectAll(error);
101
- });
102
- this.child.on('exit', (code) => {
103
- this.connected = false;
104
- this.child = null;
105
- if (!this.closed && code !== 0) {
106
- this.rejectAll(new Error(`FFF MCP exited with code ${code}`));
107
- }
108
- });
109
-
110
- await this.sendRequest('initialize', {
111
- protocolVersion: '2024-11-05',
112
- capabilities: {},
113
- clientInfo: getPackageInfo()
114
- });
115
- this.sendNotification('notifications/initialized', {});
116
- }
117
-
118
- rejectAll(error) {
119
- for (const { reject, timer } of this.pending.values()) {
120
- clearTimeout(timer);
121
- reject(error);
122
- }
123
- this.pending.clear();
124
- }
125
-
126
- sendNotification(method, params) {
127
- if (!this.child?.stdin) throw new Error('FFF MCP client is not connected');
128
- this.child.stdin.write(
129
- encodeMessage({
130
- jsonrpc: '2.0',
131
- method,
132
- params
133
- })
134
- );
135
- }
136
-
137
- sendRequest(method, params) {
138
- if (!this.child?.stdin) {
139
- return Promise.reject(new Error('FFF MCP client is not connected'));
140
- }
141
- return new Promise((resolve, reject) => {
142
- const id = this.nextId++;
143
- const timer = setTimeout(() => {
144
- this.pending.delete(id);
145
- reject(new Error(`FFF MCP request timed out after ${this.timeoutMs}ms`));
146
- }, this.timeoutMs);
147
- this.pending.set(id, {
148
- resolve: (result) => {
149
- clearTimeout(timer);
150
- resolve(result);
151
- },
152
- reject: (error) => {
153
- clearTimeout(timer);
154
- reject(error);
155
- },
156
- timer
157
- });
158
- this.child.stdin.write(
159
- encodeMessage({
160
- jsonrpc: '2.0',
161
- id,
162
- method,
163
- params
164
- })
165
- );
166
- });
167
- }
168
-
169
- async callTool(name, args) {
170
- await this.connect();
171
- return this.sendRequest('tools/call', {
172
- name,
173
- arguments: args
174
- });
175
- }
176
-
177
- async dispose() {
178
- this.closed = true;
179
- this.connected = false;
180
- if (this.child?.stdin) {
181
- try {
182
- this.child.stdin.end();
183
- } catch {}
184
- }
185
- if (this.child && !this.child.killed) {
186
- this.child.kill();
187
- }
188
- this.child = null;
189
- this.rejectAll(new Error('FFF MCP client disposed'));
190
- }
191
- }
192
-
193
- function extractTextContent(result) {
194
- const content = Array.isArray(result?.content) ? result.content : [];
195
- return content
196
- .filter((item) => item?.type === 'text' && typeof item?.text === 'string')
197
- .map((item) => item.text)
198
- .join('\n')
199
- .trim();
200
- }
201
-
202
- function stripFffFileSuffix(line) {
203
- return String(line || '')
204
- .replace(/\s+-\s+(?:hot|warm|frequent)(?:\s+git:[a-z_]+)?$/i, '')
205
- .replace(/\s+git:[a-z_]+$/i, '')
206
- .trim();
207
- }
208
-
209
- function parseFindFilesOutput(text) {
210
- const lines = String(text || '')
211
- .split(/\r?\n/)
212
- .map((line) => line.trimEnd())
213
- .filter(Boolean);
214
- const matches = [];
215
- for (const line of lines) {
216
- if (
217
- line.startsWith('→ ') ||
218
- line.startsWith('cursor:') ||
219
- /^\d+\/\d+\s+matches$/i.test(line) ||
220
- /^0 results\b/i.test(line)
221
- ) {
222
- continue;
223
- }
224
- const normalized = stripFffFileSuffix(line);
225
- if (normalized) matches.push(normalized);
226
- }
227
- return matches;
228
- }
229
-
230
- function parseGrepOutput(text, fallbackPattern = '') {
231
- const lines = String(text || '')
232
- .split(/\r?\n/)
233
- .map((line) => line.trimEnd())
234
- .filter(Boolean);
235
- const matches = [];
236
- let currentPath = '';
237
- for (const line of lines) {
238
- if (
239
- line.startsWith('→ ') ||
240
- line.startsWith('cursor:') ||
241
- /^! regex failed:/i.test(line) ||
242
- /^\d+\/\d+\s+matches shown$/i.test(line) ||
243
- /^0 (?:exact )?matches\b/i.test(line) ||
244
- /^Auto-broadened to\b/i.test(line)
245
- ) {
246
- continue;
247
- }
248
- const sectionMatch = line.match(/^\s*(\d+)\s*[:|-]\s*(.*)$/);
249
- if (sectionMatch && currentPath) {
250
- const [, lineNumber, preview] = sectionMatch;
251
- matches.push({
252
- path: currentPath,
253
- line: Number(lineNumber),
254
- column: 1,
255
- preview: String(preview || '').trim()
256
- });
257
- continue;
258
- }
259
- const fileCandidate = stripFffFileSuffix(line);
260
- if (fileCandidate && !/^\d/.test(fileCandidate)) {
261
- currentPath = fileCandidate;
262
- }
263
- }
264
- return {
265
- pattern: fallbackPattern,
266
- matches,
267
- truncated: /cursor:/i.test(text)
268
- };
269
- }
270
-
271
- function normalizePathPrefix(value) {
272
- const text = String(value || '').trim().replace(/\\/g, '/').replace(/^\.\/+/, '');
273
- if (!text || text === '.') return '';
274
- return text.endsWith('/') ? text : `${text}/`;
275
- }
276
-
277
- function buildGrepQuery(pattern, args = {}) {
278
- const pieces = [];
279
- const pathPrefix = normalizePathPrefix(args.path);
280
- if (pathPrefix) pieces.push(pathPrefix);
281
- const fileTypes = Array.isArray(args.file_types) ? args.file_types : [];
282
- const language = String(args.language || '').trim().toLowerCase();
283
- const mergedTypes = [...new Set([...fileTypes, ...(LANGUAGE_FILE_TYPES[language] || [])])];
284
- if (mergedTypes.length === 1) {
285
- pieces.push(`*.${mergedTypes[0]}`);
286
- } else if (mergedTypes.length > 1) {
287
- pieces.push(`*.{${mergedTypes.join(',')}}`);
288
- }
289
- pieces.push(String(pattern || '').trim());
290
- return pieces.filter(Boolean).join(' ');
291
- }
292
-
293
- function buildImmediateItems(relativePath, filePaths, includeHidden = false) {
294
- const prefix = normalizePathPrefix(relativePath);
295
- const directories = new Set();
296
- const files = new Set();
297
- for (const filePath of filePaths) {
298
- const normalized = String(filePath || '').replace(/\\/g, '/');
299
- if (!normalized.startsWith(prefix)) continue;
300
- const remainder = normalized.slice(prefix.length);
301
- if (!remainder) continue;
302
- const [head, ...rest] = remainder.split('/');
303
- if (!head) continue;
304
- if (!includeHidden && head.startsWith('.')) continue;
305
- if (rest.length > 0) {
306
- directories.add(head);
307
- } else {
308
- files.add(head);
309
- }
310
- }
311
- const dirItems = [...directories].sort((a, b) => a.localeCompare(b)).map((name) => ({
312
- name,
313
- path: `${prefix}${name}`.replace(/\/$/, ''),
314
- type: 'dir'
315
- }));
316
- const fileItems = [...files].sort((a, b) => a.localeCompare(b)).map((name) => ({
317
- name,
318
- path: `${prefix}${name}`,
319
- type: 'file'
320
- }));
321
- return [...dirItems, ...fileItems];
322
- }
323
-
324
- export function createFffAdapter({ workspaceRoot, config }) {
325
- const command = String(config?.search?.fff_command || config?.tooling?.fff_command || DEFAULT_COMMAND).trim() || DEFAULT_COMMAND;
326
- const timeoutMs = clampNumber(
327
- config?.search?.fff_timeout_ms || config?.tooling?.fff_timeout_ms,
328
- 1_000,
329
- 120_000,
330
- DEFAULT_TIMEOUT_MS
331
- );
332
- const client = new FffMcpClient({ workspaceRoot, command, timeoutMs });
333
-
334
- return {
335
- async connect() {
336
- await client.connect();
337
- },
338
-
339
- async grep(args) {
340
- const pattern = String(args?.pattern || '').trim();
341
- if (!pattern) return null;
342
- const result = await client.callTool('grep', {
343
- query: buildGrepQuery(pattern, args),
344
- max_results: clampNumber(args?.max_results, 1, 200, 50)
345
- });
346
- return parseGrepOutput(extractTextContent(result), pattern);
347
- },
348
-
349
- async glob(args) {
350
- const pattern = String(args?.pattern || '').trim();
351
- if (!pattern) return null;
352
- const limit = clampNumber(args?.max_results, 1, 500, 200);
353
- const result = await client.callTool('find_files', {
354
- query: pattern,
355
- max_results: limit
356
- });
357
- const matches = parseFindFilesOutput(extractTextContent(result));
358
- return {
359
- pattern,
360
- matches,
361
- truncated: matches.length >= limit
362
- };
363
- },
364
-
365
- async list(args) {
366
- const relativePath = String(args?.path || '.').trim();
367
- if (!relativePath || relativePath === '.') return null;
368
- const result = await client.callTool('find_files', {
369
- query: normalizePathPrefix(relativePath),
370
- max_results: 500
371
- });
372
- const filePaths = parseFindFilesOutput(extractTextContent(result));
373
- return {
374
- path: relativePath,
375
- items: buildImmediateItems(relativePath, filePaths, Boolean(args?.include_hidden))
376
- };
377
- },
378
-
379
- async dispose() {
380
- await client.dispose();
381
- }
382
- };
383
- }
1
+ import { spawn } from 'node:child_process';
2
+ import { LANGUAGE_FILE_TYPES } from './constants.js';
3
+ import { getPackageInfo } from './version.js';
4
+
5
+ const DEFAULT_COMMAND = 'fff-mcp';
6
+ const DEFAULT_TIMEOUT_MS = 15_000;
7
+
8
+ function clampNumber(value, min, max, fallback) {
9
+ const num = Number(value);
10
+ if (!Number.isFinite(num)) return fallback;
11
+ return Math.min(max, Math.max(min, num));
12
+ }
13
+
14
+ function encodeMessage(payload) {
15
+ const body = Buffer.from(JSON.stringify(payload), 'utf8');
16
+ return Buffer.concat([
17
+ Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, 'utf8'),
18
+ body
19
+ ]);
20
+ }
21
+
22
+ function createMessageParser(onMessage) {
23
+ let buffer = Buffer.alloc(0);
24
+ return (chunk) => {
25
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
26
+ while (buffer.length > 0) {
27
+ const headerEnd = buffer.indexOf('\r\n\r\n');
28
+ if (headerEnd < 0) return;
29
+ const headerText = buffer.slice(0, headerEnd).toString('utf8');
30
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
31
+ if (!match) {
32
+ buffer = Buffer.alloc(0);
33
+ return;
34
+ }
35
+ const bodyLength = Number(match[1]);
36
+ const totalLength = headerEnd + 4 + bodyLength;
37
+ if (buffer.length < totalLength) return;
38
+ const body = buffer.slice(headerEnd + 4, totalLength).toString('utf8');
39
+ buffer = buffer.slice(totalLength);
40
+ try {
41
+ onMessage(JSON.parse(body));
42
+ } catch {
43
+ // Ignore malformed frames.
44
+ }
45
+ }
46
+ };
47
+ }
48
+
49
+ class FffMcpClient {
50
+ constructor({ workspaceRoot, command, timeoutMs }) {
51
+ this.workspaceRoot = workspaceRoot;
52
+ this.command = command;
53
+ this.timeoutMs = timeoutMs;
54
+ this.child = null;
55
+ this.pending = new Map();
56
+ this.nextId = 1;
57
+ this.connectPromise = null;
58
+ this.connected = false;
59
+ this.closed = false;
60
+ this.parser = createMessageParser((message) => this.handleMessage(message));
61
+ }
62
+
63
+ handleMessage(message) {
64
+ if (!message || typeof message !== 'object') return;
65
+ if (typeof message.id !== 'number') return;
66
+ const pending = this.pending.get(message.id);
67
+ if (!pending) return;
68
+ this.pending.delete(message.id);
69
+ if (message.error) {
70
+ pending.reject(new Error(String(message.error?.message || 'Unknown MCP error')));
71
+ return;
72
+ }
73
+ pending.resolve(message.result);
74
+ }
75
+
76
+ async connect() {
77
+ if (this.connected) return;
78
+ if (this.connectPromise) return this.connectPromise;
79
+ this.connectPromise = this.start();
80
+ try {
81
+ await this.connectPromise;
82
+ this.connected = true;
83
+ } finally {
84
+ this.connectPromise = null;
85
+ }
86
+ }
87
+
88
+ async start() {
89
+ if (this.closed) {
90
+ throw new Error('FFF MCP client already disposed');
91
+ }
92
+ this.child = spawn(this.command, [], {
93
+ cwd: this.workspaceRoot,
94
+ stdio: ['pipe', 'pipe', 'pipe']
95
+ });
96
+
97
+ this.child.stdout.on('data', this.parser);
98
+ this.child.stderr.on('data', () => {});
99
+ this.child.on('error', (error) => {
100
+ this.rejectAll(error);
101
+ });
102
+ this.child.on('exit', (code) => {
103
+ this.connected = false;
104
+ this.child = null;
105
+ if (!this.closed && code !== 0) {
106
+ this.rejectAll(new Error(`FFF MCP exited with code ${code}`));
107
+ }
108
+ });
109
+
110
+ await this.sendRequest('initialize', {
111
+ protocolVersion: '2024-11-05',
112
+ capabilities: {},
113
+ clientInfo: getPackageInfo()
114
+ });
115
+ this.sendNotification('notifications/initialized', {});
116
+ }
117
+
118
+ rejectAll(error) {
119
+ for (const { reject, timer } of this.pending.values()) {
120
+ clearTimeout(timer);
121
+ reject(error);
122
+ }
123
+ this.pending.clear();
124
+ }
125
+
126
+ sendNotification(method, params) {
127
+ if (!this.child?.stdin) throw new Error('FFF MCP client is not connected');
128
+ this.child.stdin.write(
129
+ encodeMessage({
130
+ jsonrpc: '2.0',
131
+ method,
132
+ params
133
+ })
134
+ );
135
+ }
136
+
137
+ sendRequest(method, params) {
138
+ if (!this.child?.stdin) {
139
+ return Promise.reject(new Error('FFF MCP client is not connected'));
140
+ }
141
+ return new Promise((resolve, reject) => {
142
+ const id = this.nextId++;
143
+ const timer = setTimeout(() => {
144
+ this.pending.delete(id);
145
+ reject(new Error(`FFF MCP request timed out after ${this.timeoutMs}ms`));
146
+ }, this.timeoutMs);
147
+ this.pending.set(id, {
148
+ resolve: (result) => {
149
+ clearTimeout(timer);
150
+ resolve(result);
151
+ },
152
+ reject: (error) => {
153
+ clearTimeout(timer);
154
+ reject(error);
155
+ },
156
+ timer
157
+ });
158
+ this.child.stdin.write(
159
+ encodeMessage({
160
+ jsonrpc: '2.0',
161
+ id,
162
+ method,
163
+ params
164
+ })
165
+ );
166
+ });
167
+ }
168
+
169
+ async callTool(name, args) {
170
+ await this.connect();
171
+ return this.sendRequest('tools/call', {
172
+ name,
173
+ arguments: args
174
+ });
175
+ }
176
+
177
+ async dispose() {
178
+ this.closed = true;
179
+ this.connected = false;
180
+ if (this.child?.stdin) {
181
+ try {
182
+ this.child.stdin.end();
183
+ } catch {}
184
+ }
185
+ if (this.child && !this.child.killed) {
186
+ this.child.kill();
187
+ }
188
+ this.child = null;
189
+ this.rejectAll(new Error('FFF MCP client disposed'));
190
+ }
191
+ }
192
+
193
+ function extractTextContent(result) {
194
+ const content = Array.isArray(result?.content) ? result.content : [];
195
+ return content
196
+ .filter((item) => item?.type === 'text' && typeof item?.text === 'string')
197
+ .map((item) => item.text)
198
+ .join('\n')
199
+ .trim();
200
+ }
201
+
202
+ function stripFffFileSuffix(line) {
203
+ return String(line || '')
204
+ .replace(/\s+-\s+(?:hot|warm|frequent)(?:\s+git:[a-z_]+)?$/i, '')
205
+ .replace(/\s+git:[a-z_]+$/i, '')
206
+ .trim();
207
+ }
208
+
209
+ function parseFindFilesOutput(text) {
210
+ const lines = String(text || '')
211
+ .split(/\r?\n/)
212
+ .map((line) => line.trimEnd())
213
+ .filter(Boolean);
214
+ const matches = [];
215
+ for (const line of lines) {
216
+ if (
217
+ line.startsWith('→ ') ||
218
+ line.startsWith('cursor:') ||
219
+ /^\d+\/\d+\s+matches$/i.test(line) ||
220
+ /^0 results\b/i.test(line)
221
+ ) {
222
+ continue;
223
+ }
224
+ const normalized = stripFffFileSuffix(line);
225
+ if (normalized) matches.push(normalized);
226
+ }
227
+ return matches;
228
+ }
229
+
230
+ function parseGrepOutput(text, fallbackPattern = '') {
231
+ const lines = String(text || '')
232
+ .split(/\r?\n/)
233
+ .map((line) => line.trimEnd())
234
+ .filter(Boolean);
235
+ const matches = [];
236
+ let currentPath = '';
237
+ for (const line of lines) {
238
+ if (
239
+ line.startsWith('→ ') ||
240
+ line.startsWith('cursor:') ||
241
+ /^! regex failed:/i.test(line) ||
242
+ /^\d+\/\d+\s+matches shown$/i.test(line) ||
243
+ /^0 (?:exact )?matches\b/i.test(line) ||
244
+ /^Auto-broadened to\b/i.test(line)
245
+ ) {
246
+ continue;
247
+ }
248
+ const sectionMatch = line.match(/^\s*(\d+)\s*[:|-]\s*(.*)$/);
249
+ if (sectionMatch && currentPath) {
250
+ const [, lineNumber, preview] = sectionMatch;
251
+ matches.push({
252
+ path: currentPath,
253
+ line: Number(lineNumber),
254
+ column: 1,
255
+ preview: String(preview || '').trim()
256
+ });
257
+ continue;
258
+ }
259
+ const fileCandidate = stripFffFileSuffix(line);
260
+ if (fileCandidate && !/^\d/.test(fileCandidate)) {
261
+ currentPath = fileCandidate;
262
+ }
263
+ }
264
+ return {
265
+ pattern: fallbackPattern,
266
+ matches,
267
+ truncated: /cursor:/i.test(text)
268
+ };
269
+ }
270
+
271
+ function normalizePathPrefix(value) {
272
+ const text = String(value || '').trim().replace(/\\/g, '/').replace(/^\.\/+/, '');
273
+ if (!text || text === '.') return '';
274
+ return text.endsWith('/') ? text : `${text}/`;
275
+ }
276
+
277
+ function buildGrepQuery(pattern, args = {}) {
278
+ const pieces = [];
279
+ const pathPrefix = normalizePathPrefix(args.path);
280
+ if (pathPrefix) pieces.push(pathPrefix);
281
+ const fileTypes = Array.isArray(args.file_types) ? args.file_types : [];
282
+ const language = String(args.language || '').trim().toLowerCase();
283
+ const mergedTypes = [...new Set([...fileTypes, ...(LANGUAGE_FILE_TYPES[language] || [])])];
284
+ if (mergedTypes.length === 1) {
285
+ pieces.push(`*.${mergedTypes[0]}`);
286
+ } else if (mergedTypes.length > 1) {
287
+ pieces.push(`*.{${mergedTypes.join(',')}}`);
288
+ }
289
+ pieces.push(String(pattern || '').trim());
290
+ return pieces.filter(Boolean).join(' ');
291
+ }
292
+
293
+ function buildImmediateItems(relativePath, filePaths, includeHidden = false) {
294
+ const prefix = normalizePathPrefix(relativePath);
295
+ const directories = new Set();
296
+ const files = new Set();
297
+ for (const filePath of filePaths) {
298
+ const normalized = String(filePath || '').replace(/\\/g, '/');
299
+ if (!normalized.startsWith(prefix)) continue;
300
+ const remainder = normalized.slice(prefix.length);
301
+ if (!remainder) continue;
302
+ const [head, ...rest] = remainder.split('/');
303
+ if (!head) continue;
304
+ if (!includeHidden && head.startsWith('.')) continue;
305
+ if (rest.length > 0) {
306
+ directories.add(head);
307
+ } else {
308
+ files.add(head);
309
+ }
310
+ }
311
+ const dirItems = [...directories].sort((a, b) => a.localeCompare(b)).map((name) => ({
312
+ name,
313
+ path: `${prefix}${name}`.replace(/\/$/, ''),
314
+ type: 'dir'
315
+ }));
316
+ const fileItems = [...files].sort((a, b) => a.localeCompare(b)).map((name) => ({
317
+ name,
318
+ path: `${prefix}${name}`,
319
+ type: 'file'
320
+ }));
321
+ return [...dirItems, ...fileItems];
322
+ }
323
+
324
+ export function createFffAdapter({ workspaceRoot, config }) {
325
+ const command = String(config?.search?.fff_command || config?.tooling?.fff_command || DEFAULT_COMMAND).trim() || DEFAULT_COMMAND;
326
+ const timeoutMs = clampNumber(
327
+ config?.search?.fff_timeout_ms || config?.tooling?.fff_timeout_ms,
328
+ 1_000,
329
+ 120_000,
330
+ DEFAULT_TIMEOUT_MS
331
+ );
332
+ const client = new FffMcpClient({ workspaceRoot, command, timeoutMs });
333
+
334
+ return {
335
+ async connect() {
336
+ await client.connect();
337
+ },
338
+
339
+ async grep(args) {
340
+ const pattern = String(args?.pattern || '').trim();
341
+ if (!pattern) return null;
342
+ const result = await client.callTool('grep', {
343
+ query: buildGrepQuery(pattern, args),
344
+ max_results: clampNumber(args?.max_results, 1, 200, 50)
345
+ });
346
+ return parseGrepOutput(extractTextContent(result), pattern);
347
+ },
348
+
349
+ async glob(args) {
350
+ const pattern = String(args?.pattern || '').trim();
351
+ if (!pattern) return null;
352
+ const limit = clampNumber(args?.max_results, 1, 500, 200);
353
+ const result = await client.callTool('find_files', {
354
+ query: pattern,
355
+ max_results: limit
356
+ });
357
+ const matches = parseFindFilesOutput(extractTextContent(result));
358
+ return {
359
+ pattern,
360
+ matches,
361
+ truncated: matches.length >= limit
362
+ };
363
+ },
364
+
365
+ async list(args) {
366
+ const relativePath = String(args?.path || '.').trim();
367
+ if (!relativePath || relativePath === '.') return null;
368
+ const result = await client.callTool('find_files', {
369
+ query: normalizePathPrefix(relativePath),
370
+ max_results: 500
371
+ });
372
+ const filePaths = parseFindFilesOutput(extractTextContent(result));
373
+ return {
374
+ path: relativePath,
375
+ items: buildImmediateItems(relativePath, filePaths, Boolean(args?.include_hidden))
376
+ };
377
+ },
378
+
379
+ async dispose() {
380
+ await client.dispose();
381
+ }
382
+ };
383
+ }