@ottocode/server 0.1.264 → 0.1.266
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/package.json +3 -3
- package/src/routes/auth/copilot.ts +699 -0
- package/src/routes/auth/oauth.ts +578 -0
- package/src/routes/auth/onboarding.ts +45 -0
- package/src/routes/auth/providers.ts +189 -0
- package/src/routes/auth/service.ts +167 -0
- package/src/routes/auth/state.ts +23 -0
- package/src/routes/auth/status.ts +203 -0
- package/src/routes/auth/wallet.ts +229 -0
- package/src/routes/auth.ts +12 -2080
- package/src/routes/config/models-service.ts +411 -0
- package/src/routes/config/models.ts +6 -426
- package/src/routes/config/providers-service.ts +237 -0
- package/src/routes/config/providers.ts +10 -242
- package/src/routes/files/handlers.ts +297 -0
- package/src/routes/files/service.ts +313 -0
- package/src/routes/files.ts +12 -608
- package/src/routes/git/commit-service.ts +207 -0
- package/src/routes/git/commit.ts +6 -220
- package/src/routes/git/remote-service.ts +116 -0
- package/src/routes/git/remote.ts +8 -115
- package/src/routes/git/staging-service.ts +111 -0
- package/src/routes/git/staging.ts +10 -205
- package/src/routes/mcp/auth.ts +338 -0
- package/src/routes/mcp/lifecycle.ts +263 -0
- package/src/routes/mcp/servers.ts +212 -0
- package/src/routes/mcp/service.ts +664 -0
- package/src/routes/mcp/state.ts +13 -0
- package/src/routes/mcp.ts +6 -1233
- package/src/routes/ottorouter/billing.ts +593 -0
- package/src/routes/ottorouter/service.ts +92 -0
- package/src/routes/ottorouter/topup.ts +301 -0
- package/src/routes/ottorouter/wallet.ts +370 -0
- package/src/routes/ottorouter.ts +6 -1319
- package/src/routes/research/service.ts +339 -0
- package/src/routes/research.ts +12 -390
- package/src/routes/sessions/crud.ts +563 -0
- package/src/routes/sessions/queue.ts +242 -0
- package/src/routes/sessions/retry.ts +121 -0
- package/src/routes/sessions/service.ts +768 -0
- package/src/routes/sessions/share.ts +434 -0
- package/src/routes/sessions.ts +8 -1977
- package/src/routes/skills/service.ts +221 -0
- package/src/routes/skills/spec.ts +309 -0
- package/src/routes/skills.ts +31 -909
- package/src/routes/terminals/service.ts +326 -0
- package/src/routes/terminals.ts +19 -295
- package/src/routes/tunnel/service.ts +217 -0
- package/src/routes/tunnel.ts +29 -219
- package/src/runtime/agent/registry-prompts.ts +147 -0
- package/src/runtime/agent/registry.ts +6 -124
- package/src/runtime/agent/runner-errors.ts +116 -0
- package/src/runtime/agent/runner-reminders.ts +45 -0
- package/src/runtime/agent/runner-setup-model.ts +75 -0
- package/src/runtime/agent/runner-setup-prompt.ts +185 -0
- package/src/runtime/agent/runner-setup-tools.ts +103 -0
- package/src/runtime/agent/runner-setup-utils.ts +21 -0
- package/src/runtime/agent/runner-setup.ts +54 -288
- package/src/runtime/agent/runner-telemetry.ts +112 -0
- package/src/runtime/agent/runner-text.ts +108 -0
- package/src/runtime/agent/runner-tool-observer.ts +86 -0
- package/src/runtime/agent/runner.ts +79 -378
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/provider/custom.ts +73 -0
- package/src/runtime/provider/index.ts +6 -85
- package/src/runtime/provider/reasoning-builders.ts +280 -0
- package/src/runtime/provider/reasoning.ts +68 -264
- package/src/runtime/provider/xai.ts +8 -0
- package/src/tools/adapter/events.ts +116 -0
- package/src/tools/adapter/execution.ts +160 -0
- package/src/tools/adapter/pending.ts +37 -0
- package/src/tools/adapter/persistence.ts +166 -0
- package/src/tools/adapter/results.ts +97 -0
- package/src/tools/adapter.ts +124 -451
package/src/routes/files.ts
CHANGED
|
@@ -1,318 +1,12 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join, relative, resolve } from 'node:path';
|
|
5
|
-
import { spawn } from 'node:child_process';
|
|
6
|
-
import { exec } from 'node:child_process';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
|
-
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
9
|
-
import { logger } from '@ottocode/sdk';
|
|
10
|
-
import { resolveBinary } from '@ottocode/sdk/tools/bin-manager';
|
|
11
2
|
import { openApiRoute } from '../openapi/route.ts';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const DEFAULT_SEARCH_MAX_DEPTH = 12;
|
|
20
|
-
const DEFAULT_SEARCH_LIMIT = 10_000;
|
|
21
|
-
const TREE_ENTRY_LIMIT = 1000;
|
|
22
|
-
|
|
23
|
-
const EXCLUDED_DIRS = new Set([
|
|
24
|
-
'node_modules',
|
|
25
|
-
'.git',
|
|
26
|
-
'dist',
|
|
27
|
-
'build',
|
|
28
|
-
'.next',
|
|
29
|
-
'.nuxt',
|
|
30
|
-
'.turbo',
|
|
31
|
-
'.astro',
|
|
32
|
-
'.svelte-kit',
|
|
33
|
-
'.vercel',
|
|
34
|
-
'.output',
|
|
35
|
-
'coverage',
|
|
36
|
-
'.cache',
|
|
37
|
-
'__pycache__',
|
|
38
|
-
'.tsbuildinfo',
|
|
39
|
-
'target',
|
|
40
|
-
'.cargo',
|
|
41
|
-
'.rustup',
|
|
42
|
-
'vendor',
|
|
43
|
-
'.gradle',
|
|
44
|
-
'.idea',
|
|
45
|
-
'.vscode',
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
type SearchPolicy = {
|
|
49
|
-
maxDepth: number;
|
|
50
|
-
limit: number;
|
|
51
|
-
includeIgnored: boolean;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function isHomeDirectory(projectRoot: string): boolean {
|
|
55
|
-
return resolve(projectRoot) === resolve(homedir());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function clampNumber(value: number, min: number, max: number): number {
|
|
59
|
-
if (!Number.isFinite(value)) return max;
|
|
60
|
-
return Math.min(Math.max(value, min), max);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getSearchPolicy(projectRoot: string): SearchPolicy {
|
|
64
|
-
if (isHomeDirectory(projectRoot)) {
|
|
65
|
-
return {
|
|
66
|
-
maxDepth: HOME_SEARCH_MAX_DEPTH,
|
|
67
|
-
limit: HOME_SEARCH_LIMIT,
|
|
68
|
-
includeIgnored: false,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
maxDepth: DEFAULT_SEARCH_MAX_DEPTH,
|
|
73
|
-
limit: DEFAULT_SEARCH_LIMIT,
|
|
74
|
-
includeIgnored: false,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function shouldExcludeFile(name: string): boolean {
|
|
79
|
-
return EXCLUDED_FILES.has(name);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function shouldExcludeDir(name: string): boolean {
|
|
83
|
-
return EXCLUDED_DIRS.has(name);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function shouldExcludeSearchDir(name: string): boolean {
|
|
87
|
-
return shouldExcludeDir(name) || name.startsWith('.');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function listFilesWithRg(
|
|
91
|
-
projectRoot: string,
|
|
92
|
-
maxDepth: number,
|
|
93
|
-
limit: number,
|
|
94
|
-
includeIgnored = false,
|
|
95
|
-
query = '',
|
|
96
|
-
): Promise<{ files: string[]; truncated: boolean }> {
|
|
97
|
-
const rgBin = await resolveBinary('rg');
|
|
98
|
-
|
|
99
|
-
return new Promise((resolve) => {
|
|
100
|
-
const args = ['--files', '--sort', 'path', '--max-depth', String(maxDepth)];
|
|
101
|
-
if (includeIgnored) {
|
|
102
|
-
args.push('--no-ignore');
|
|
103
|
-
}
|
|
104
|
-
for (const dir of EXCLUDED_DIRS) {
|
|
105
|
-
args.push('--glob', `!**/${dir}/**`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const proc = spawn(rgBin, args, { cwd: projectRoot });
|
|
109
|
-
let stdout = '';
|
|
110
|
-
let stderr = '';
|
|
111
|
-
|
|
112
|
-
proc.stdout.on('data', (data) => {
|
|
113
|
-
stdout += data.toString();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
proc.stderr.on('data', (data) => {
|
|
117
|
-
stderr += data.toString();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
proc.on('close', (code) => {
|
|
121
|
-
if (code !== 0 && code !== 1) {
|
|
122
|
-
logger.warn('rg --files failed, falling back', { stderr } as Record<
|
|
123
|
-
string,
|
|
124
|
-
unknown
|
|
125
|
-
>);
|
|
126
|
-
resolve({ files: [], truncated: false });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const allFiles = stdout.split('\n').filter(Boolean);
|
|
131
|
-
|
|
132
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
133
|
-
const filtered = allFiles.filter((f) => {
|
|
134
|
-
const filename = f.split(/[\\/]/).pop() || f;
|
|
135
|
-
if (shouldExcludeFile(filename)) return false;
|
|
136
|
-
if (!normalizedQuery) return true;
|
|
137
|
-
return f.toLowerCase().includes(normalizedQuery);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const truncated = filtered.length > limit;
|
|
141
|
-
resolve({ files: filtered.slice(0, limit), truncated });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
proc.on('error', () => {
|
|
145
|
-
resolve({ files: [], truncated: false });
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function parseGitignore(projectRoot: string): Promise<Set<string>> {
|
|
151
|
-
const patterns = new Set<string>();
|
|
152
|
-
try {
|
|
153
|
-
const gitignorePath = join(projectRoot, '.gitignore');
|
|
154
|
-
const content = await readFile(gitignorePath, 'utf-8');
|
|
155
|
-
for (const line of content.split('\n')) {
|
|
156
|
-
const trimmed = line.trim();
|
|
157
|
-
if (trimmed && !trimmed.startsWith('#')) {
|
|
158
|
-
patterns.add(trimmed);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
} catch (_err) {}
|
|
162
|
-
return patterns;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function matchesGitignorePattern(
|
|
166
|
-
relativePath: string,
|
|
167
|
-
patterns: Set<string>,
|
|
168
|
-
): boolean {
|
|
169
|
-
for (const pattern of patterns) {
|
|
170
|
-
const cleanPattern = pattern.replace(/^\//, '').replace(/\/$/, '');
|
|
171
|
-
const pathParts = relativePath.split(/[\\/]/);
|
|
172
|
-
|
|
173
|
-
if (pattern.endsWith('/')) {
|
|
174
|
-
if (pathParts[0] === cleanPattern) return true;
|
|
175
|
-
if (relativePath.startsWith(`${cleanPattern}/`)) return true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (pattern.includes('*')) {
|
|
179
|
-
const regex = new RegExp(
|
|
180
|
-
`^${cleanPattern.replace(/\*/g, '.*').replace(/\?/g, '.')}$`,
|
|
181
|
-
);
|
|
182
|
-
if (regex.test(relativePath)) return true;
|
|
183
|
-
for (const part of pathParts) {
|
|
184
|
-
if (regex.test(part)) return true;
|
|
185
|
-
}
|
|
186
|
-
} else {
|
|
187
|
-
if (relativePath === cleanPattern) return true;
|
|
188
|
-
if (pathParts.includes(cleanPattern)) return true;
|
|
189
|
-
if (relativePath.startsWith(`${cleanPattern}/`)) return true;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return false;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function traverseDirectory(
|
|
196
|
-
dir: string,
|
|
197
|
-
projectRoot: string,
|
|
198
|
-
maxDepth: number,
|
|
199
|
-
currentDepth = 0,
|
|
200
|
-
limit: number,
|
|
201
|
-
collected: string[] = [],
|
|
202
|
-
gitignorePatterns?: Set<string>,
|
|
203
|
-
): Promise<{ files: string[]; truncated: boolean }> {
|
|
204
|
-
if (currentDepth >= maxDepth || collected.length >= limit) {
|
|
205
|
-
return { files: collected, truncated: collected.length >= limit };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
210
|
-
|
|
211
|
-
for (const entry of entries) {
|
|
212
|
-
if (collected.length >= limit) {
|
|
213
|
-
return { files: collected, truncated: true };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const fullPath = join(dir, entry.name);
|
|
217
|
-
const relativePath = relative(projectRoot, fullPath);
|
|
218
|
-
|
|
219
|
-
if (entry.isDirectory()) {
|
|
220
|
-
if (shouldExcludeSearchDir(entry.name)) continue;
|
|
221
|
-
if (
|
|
222
|
-
gitignorePatterns &&
|
|
223
|
-
matchesGitignorePattern(relativePath, gitignorePatterns)
|
|
224
|
-
) {
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const result = await traverseDirectory(
|
|
228
|
-
fullPath,
|
|
229
|
-
projectRoot,
|
|
230
|
-
maxDepth,
|
|
231
|
-
currentDepth + 1,
|
|
232
|
-
limit,
|
|
233
|
-
collected,
|
|
234
|
-
gitignorePatterns,
|
|
235
|
-
);
|
|
236
|
-
if (result.truncated) {
|
|
237
|
-
return result;
|
|
238
|
-
}
|
|
239
|
-
} else if (entry.isFile()) {
|
|
240
|
-
if (shouldExcludeFile(entry.name)) continue;
|
|
241
|
-
if (
|
|
242
|
-
gitignorePatterns &&
|
|
243
|
-
matchesGitignorePattern(relativePath, gitignorePatterns)
|
|
244
|
-
) {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
collected.push(relativePath);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
} catch (err) {
|
|
251
|
-
logger.warn(
|
|
252
|
-
`Failed to read directory ${dir}:`,
|
|
253
|
-
err as Record<string, unknown>,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return { files: collected, truncated: false };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function getChangedFiles(
|
|
261
|
-
projectRoot: string,
|
|
262
|
-
): Promise<Map<string, string>> {
|
|
263
|
-
try {
|
|
264
|
-
const { stdout } = await execAsync('git status --porcelain', {
|
|
265
|
-
cwd: projectRoot,
|
|
266
|
-
});
|
|
267
|
-
const changedFiles = new Map<string, string>();
|
|
268
|
-
for (const line of stdout.split('\n')) {
|
|
269
|
-
if (line.length > 3) {
|
|
270
|
-
const statusCode = line.substring(0, 2).trim();
|
|
271
|
-
const filePath = line.substring(3).trim();
|
|
272
|
-
|
|
273
|
-
let status = 'modified';
|
|
274
|
-
if (statusCode.includes('A')) status = 'added';
|
|
275
|
-
else if (statusCode.includes('M')) status = 'modified';
|
|
276
|
-
else if (statusCode.includes('D')) status = 'deleted';
|
|
277
|
-
else if (statusCode.includes('R')) status = 'renamed';
|
|
278
|
-
else if (statusCode.includes('?')) status = 'untracked';
|
|
279
|
-
|
|
280
|
-
changedFiles.set(filePath, status);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return changedFiles;
|
|
284
|
-
} catch (_err) {
|
|
285
|
-
return new Map();
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async function getGitIgnoredFiles(
|
|
290
|
-
projectRoot: string,
|
|
291
|
-
files: string[],
|
|
292
|
-
): Promise<Set<string>> {
|
|
293
|
-
if (files.length === 0) return new Set();
|
|
294
|
-
try {
|
|
295
|
-
return new Promise((resolve) => {
|
|
296
|
-
const proc = spawn('git', ['check-ignore', '--stdin'], {
|
|
297
|
-
cwd: projectRoot,
|
|
298
|
-
});
|
|
299
|
-
let stdout = '';
|
|
300
|
-
proc.stdout.on('data', (data) => {
|
|
301
|
-
stdout += data.toString();
|
|
302
|
-
});
|
|
303
|
-
proc.on('close', () => {
|
|
304
|
-
resolve(new Set(stdout.split('\n').filter(Boolean)));
|
|
305
|
-
});
|
|
306
|
-
proc.on('error', () => {
|
|
307
|
-
resolve(new Set());
|
|
308
|
-
});
|
|
309
|
-
proc.stdin.write(files.join('\n'));
|
|
310
|
-
proc.stdin.end();
|
|
311
|
-
});
|
|
312
|
-
} catch (_err) {
|
|
313
|
-
return new Set();
|
|
314
|
-
}
|
|
315
|
-
}
|
|
3
|
+
import {
|
|
4
|
+
handleFileTree,
|
|
5
|
+
handleListFiles,
|
|
6
|
+
handleRawFile,
|
|
7
|
+
handleReadFile,
|
|
8
|
+
handleSearchFiles,
|
|
9
|
+
} from './files/handlers.ts';
|
|
316
10
|
|
|
317
11
|
export function registerFilesRoutes(app: Hono) {
|
|
318
12
|
openApiRoute(
|
|
@@ -406,81 +100,7 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
406
100
|
},
|
|
407
101
|
},
|
|
408
102
|
},
|
|
409
|
-
|
|
410
|
-
try {
|
|
411
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
412
|
-
const policy = getSearchPolicy(projectRoot);
|
|
413
|
-
const maxDepth = clampNumber(
|
|
414
|
-
Number.parseInt(
|
|
415
|
-
c.req.query('maxDepth') || String(policy.maxDepth),
|
|
416
|
-
10,
|
|
417
|
-
),
|
|
418
|
-
1,
|
|
419
|
-
policy.maxDepth,
|
|
420
|
-
);
|
|
421
|
-
const limit = clampNumber(
|
|
422
|
-
Number.parseInt(c.req.query('limit') || String(policy.limit), 10),
|
|
423
|
-
1,
|
|
424
|
-
policy.limit,
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
let result = await listFilesWithRg(
|
|
428
|
-
projectRoot,
|
|
429
|
-
maxDepth,
|
|
430
|
-
limit,
|
|
431
|
-
policy.includeIgnored,
|
|
432
|
-
);
|
|
433
|
-
|
|
434
|
-
if (result.files.length === 0) {
|
|
435
|
-
const gitignorePatterns = await parseGitignore(projectRoot);
|
|
436
|
-
result = await traverseDirectory(
|
|
437
|
-
projectRoot,
|
|
438
|
-
projectRoot,
|
|
439
|
-
maxDepth,
|
|
440
|
-
0,
|
|
441
|
-
limit,
|
|
442
|
-
[],
|
|
443
|
-
gitignorePatterns,
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const [changedFiles, ignoredFiles] = await Promise.all([
|
|
448
|
-
getChangedFiles(projectRoot),
|
|
449
|
-
getGitIgnoredFiles(projectRoot, result.files),
|
|
450
|
-
]);
|
|
451
|
-
|
|
452
|
-
result.files.sort((a, b) => {
|
|
453
|
-
const aIgnored = ignoredFiles.has(a);
|
|
454
|
-
const bIgnored = ignoredFiles.has(b);
|
|
455
|
-
if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
|
|
456
|
-
const aChanged = changedFiles.has(a);
|
|
457
|
-
const bChanged = changedFiles.has(b);
|
|
458
|
-
if (aChanged && !bChanged) return -1;
|
|
459
|
-
if (!aChanged && bChanged) return 1;
|
|
460
|
-
return a.localeCompare(b);
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
return c.json({
|
|
464
|
-
files: result.files,
|
|
465
|
-
ignoredFiles: Array.from(ignoredFiles),
|
|
466
|
-
changedFiles: Array.from(changedFiles.entries()).map(
|
|
467
|
-
([path, status]) => ({
|
|
468
|
-
path,
|
|
469
|
-
status,
|
|
470
|
-
}),
|
|
471
|
-
),
|
|
472
|
-
truncated: result.truncated,
|
|
473
|
-
policy: {
|
|
474
|
-
maxDepth,
|
|
475
|
-
limit,
|
|
476
|
-
home: isHomeDirectory(projectRoot),
|
|
477
|
-
},
|
|
478
|
-
});
|
|
479
|
-
} catch (err) {
|
|
480
|
-
logger.error('Files route error:', err);
|
|
481
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
482
|
-
}
|
|
483
|
-
},
|
|
103
|
+
handleListFiles,
|
|
484
104
|
);
|
|
485
105
|
|
|
486
106
|
openApiRoute(
|
|
@@ -573,93 +193,7 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
573
193
|
},
|
|
574
194
|
},
|
|
575
195
|
},
|
|
576
|
-
|
|
577
|
-
try {
|
|
578
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
579
|
-
const query = c.req.query('q') || '';
|
|
580
|
-
const policy = getSearchPolicy(projectRoot);
|
|
581
|
-
const maxDepth = clampNumber(
|
|
582
|
-
Number.parseInt(
|
|
583
|
-
c.req.query('maxDepth') || String(policy.maxDepth),
|
|
584
|
-
10,
|
|
585
|
-
),
|
|
586
|
-
1,
|
|
587
|
-
policy.maxDepth,
|
|
588
|
-
);
|
|
589
|
-
const limit = clampNumber(
|
|
590
|
-
Number.parseInt(c.req.query('limit') || String(policy.limit), 10),
|
|
591
|
-
1,
|
|
592
|
-
policy.limit,
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
let result = await listFilesWithRg(
|
|
596
|
-
projectRoot,
|
|
597
|
-
maxDepth,
|
|
598
|
-
limit,
|
|
599
|
-
policy.includeIgnored,
|
|
600
|
-
query,
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
if (result.files.length === 0) {
|
|
604
|
-
const gitignorePatterns = await parseGitignore(projectRoot);
|
|
605
|
-
result = await traverseDirectory(
|
|
606
|
-
projectRoot,
|
|
607
|
-
projectRoot,
|
|
608
|
-
maxDepth,
|
|
609
|
-
0,
|
|
610
|
-
limit,
|
|
611
|
-
[],
|
|
612
|
-
gitignorePatterns,
|
|
613
|
-
);
|
|
614
|
-
const normalizedQuery = query.trim().toLowerCase();
|
|
615
|
-
if (normalizedQuery) {
|
|
616
|
-
const files = result.files.filter((file) =>
|
|
617
|
-
file.toLowerCase().includes(normalizedQuery),
|
|
618
|
-
);
|
|
619
|
-
result = {
|
|
620
|
-
files: files.slice(0, limit),
|
|
621
|
-
truncated: files.length > limit,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const [changedFiles, ignoredFiles] = await Promise.all([
|
|
627
|
-
getChangedFiles(projectRoot),
|
|
628
|
-
getGitIgnoredFiles(projectRoot, result.files),
|
|
629
|
-
]);
|
|
630
|
-
|
|
631
|
-
result.files.sort((a, b) => {
|
|
632
|
-
const aIgnored = ignoredFiles.has(a);
|
|
633
|
-
const bIgnored = ignoredFiles.has(b);
|
|
634
|
-
if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
|
|
635
|
-
const aChanged = changedFiles.has(a);
|
|
636
|
-
const bChanged = changedFiles.has(b);
|
|
637
|
-
if (aChanged && !bChanged) return -1;
|
|
638
|
-
if (!aChanged && bChanged) return 1;
|
|
639
|
-
return a.localeCompare(b);
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
return c.json({
|
|
643
|
-
files: result.files,
|
|
644
|
-
ignoredFiles: Array.from(ignoredFiles),
|
|
645
|
-
changedFiles: Array.from(changedFiles.entries()).map(
|
|
646
|
-
([path, status]) => ({
|
|
647
|
-
path,
|
|
648
|
-
status,
|
|
649
|
-
}),
|
|
650
|
-
),
|
|
651
|
-
truncated: result.truncated,
|
|
652
|
-
policy: {
|
|
653
|
-
maxDepth,
|
|
654
|
-
limit,
|
|
655
|
-
home: isHomeDirectory(projectRoot),
|
|
656
|
-
},
|
|
657
|
-
});
|
|
658
|
-
} catch (err) {
|
|
659
|
-
logger.error('Files search route error:', err);
|
|
660
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
661
|
-
}
|
|
662
|
-
},
|
|
196
|
+
handleSearchFiles,
|
|
663
197
|
);
|
|
664
198
|
|
|
665
199
|
openApiRoute(
|
|
@@ -742,69 +276,7 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
742
276
|
},
|
|
743
277
|
},
|
|
744
278
|
},
|
|
745
|
-
|
|
746
|
-
try {
|
|
747
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
748
|
-
const dirPath = c.req.query('path') || '.';
|
|
749
|
-
const targetDir = resolve(projectRoot, dirPath);
|
|
750
|
-
if (!targetDir.startsWith(resolve(projectRoot))) {
|
|
751
|
-
return c.json({ error: 'Path traversal not allowed' }, 403);
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const gitignorePatterns = await parseGitignore(projectRoot);
|
|
755
|
-
const entries = await readdir(targetDir, { withFileTypes: true });
|
|
756
|
-
const truncated = entries.length > TREE_ENTRY_LIMIT;
|
|
757
|
-
|
|
758
|
-
const items: Array<{
|
|
759
|
-
name: string;
|
|
760
|
-
path: string;
|
|
761
|
-
type: 'file' | 'directory';
|
|
762
|
-
gitignored?: boolean;
|
|
763
|
-
vendor?: boolean;
|
|
764
|
-
searchable?: boolean;
|
|
765
|
-
}> = [];
|
|
766
|
-
|
|
767
|
-
for (const entry of entries.slice(0, TREE_ENTRY_LIMIT)) {
|
|
768
|
-
const relPath = relative(projectRoot, join(targetDir, entry.name));
|
|
769
|
-
|
|
770
|
-
if (entry.isDirectory()) {
|
|
771
|
-
const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
|
|
772
|
-
const vendor = shouldExcludeDir(entry.name);
|
|
773
|
-
items.push({
|
|
774
|
-
name: entry.name,
|
|
775
|
-
path: relPath,
|
|
776
|
-
type: 'directory',
|
|
777
|
-
gitignored: ignored || undefined,
|
|
778
|
-
vendor: vendor || undefined,
|
|
779
|
-
searchable: vendor || ignored ? false : undefined,
|
|
780
|
-
});
|
|
781
|
-
} else if (entry.isFile()) {
|
|
782
|
-
if (shouldExcludeFile(entry.name)) continue;
|
|
783
|
-
const ignored = matchesGitignorePattern(relPath, gitignorePatterns);
|
|
784
|
-
items.push({
|
|
785
|
-
name: entry.name,
|
|
786
|
-
path: relPath,
|
|
787
|
-
type: 'file',
|
|
788
|
-
gitignored: ignored || undefined,
|
|
789
|
-
searchable: ignored ? false : undefined,
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
items.sort((a, b) => {
|
|
795
|
-
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
796
|
-
const aIgnored = a.gitignored ?? false;
|
|
797
|
-
const bIgnored = b.gitignored ?? false;
|
|
798
|
-
if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
|
|
799
|
-
return a.name.localeCompare(b.name);
|
|
800
|
-
});
|
|
801
|
-
|
|
802
|
-
return c.json({ items, path: dirPath, truncated });
|
|
803
|
-
} catch (err) {
|
|
804
|
-
logger.error('Files tree route error:', err);
|
|
805
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
806
|
-
}
|
|
807
|
-
},
|
|
279
|
+
handleFileTree,
|
|
808
280
|
);
|
|
809
281
|
|
|
810
282
|
openApiRoute(
|
|
@@ -880,33 +352,7 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
880
352
|
},
|
|
881
353
|
},
|
|
882
354
|
},
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
886
|
-
const filePath = c.req.query('path');
|
|
887
|
-
|
|
888
|
-
if (!filePath) {
|
|
889
|
-
return c.json(
|
|
890
|
-
{ error: 'Missing required query parameter: path' },
|
|
891
|
-
400,
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
const absPath = join(projectRoot, filePath);
|
|
896
|
-
if (!absPath.startsWith(projectRoot)) {
|
|
897
|
-
return c.json({ error: 'Path traversal not allowed' }, 403);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const content = await readFile(absPath, 'utf-8');
|
|
901
|
-
const extension = filePath.split('.').pop()?.toLowerCase() ?? '';
|
|
902
|
-
const lineCount = content.split('\n').length;
|
|
903
|
-
|
|
904
|
-
return c.json({ content, path: filePath, extension, lineCount });
|
|
905
|
-
} catch (err) {
|
|
906
|
-
logger.error('Files read route error:', err);
|
|
907
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
908
|
-
}
|
|
909
|
-
},
|
|
355
|
+
handleReadFile,
|
|
910
356
|
);
|
|
911
357
|
|
|
912
358
|
openApiRoute(
|
|
@@ -947,48 +393,6 @@ export function registerFilesRoutes(app: Hono) {
|
|
|
947
393
|
'403': { description: 'Path traversal not allowed' },
|
|
948
394
|
},
|
|
949
395
|
},
|
|
950
|
-
|
|
951
|
-
try {
|
|
952
|
-
const projectRoot = c.req.query('project') || process.cwd();
|
|
953
|
-
const filePath = c.req.query('path');
|
|
954
|
-
|
|
955
|
-
if (!filePath) {
|
|
956
|
-
return c.json(
|
|
957
|
-
{ error: 'Missing required query parameter: path' },
|
|
958
|
-
400,
|
|
959
|
-
);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
const absPath = join(projectRoot, filePath);
|
|
963
|
-
if (!absPath.startsWith(projectRoot)) {
|
|
964
|
-
return c.json({ error: 'Path traversal not allowed' }, 403);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
|
|
968
|
-
const mimeTypes: Record<string, string> = {
|
|
969
|
-
png: 'image/png',
|
|
970
|
-
jpg: 'image/jpeg',
|
|
971
|
-
jpeg: 'image/jpeg',
|
|
972
|
-
gif: 'image/gif',
|
|
973
|
-
svg: 'image/svg+xml',
|
|
974
|
-
webp: 'image/webp',
|
|
975
|
-
ico: 'image/x-icon',
|
|
976
|
-
bmp: 'image/bmp',
|
|
977
|
-
avif: 'image/avif',
|
|
978
|
-
};
|
|
979
|
-
const contentType = mimeTypes[ext] || 'application/octet-stream';
|
|
980
|
-
|
|
981
|
-
const data = await readFile(absPath);
|
|
982
|
-
return new Response(data, {
|
|
983
|
-
headers: {
|
|
984
|
-
'Content-Type': contentType,
|
|
985
|
-
'Cache-Control': 'no-cache',
|
|
986
|
-
},
|
|
987
|
-
});
|
|
988
|
-
} catch (err) {
|
|
989
|
-
logger.error('Files raw route error:', err);
|
|
990
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
991
|
-
}
|
|
992
|
-
},
|
|
396
|
+
handleRawFile,
|
|
993
397
|
);
|
|
994
398
|
}
|