@ottocode/server 0.1.259 → 0.1.261
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 +4 -3
- package/src/index.ts +5 -4
- package/src/openapi/register.ts +92 -0
- package/src/openapi/route.ts +22 -0
- package/src/routes/ask.ts +210 -99
- package/src/routes/auth.ts +1701 -626
- package/src/routes/branch.ts +281 -90
- package/src/routes/config/agents.ts +79 -32
- package/src/routes/config/cwd.ts +46 -14
- package/src/routes/config/debug.ts +159 -30
- package/src/routes/config/defaults.ts +182 -64
- package/src/routes/config/main.ts +109 -73
- package/src/routes/config/models.ts +304 -137
- package/src/routes/config/providers.ts +462 -166
- package/src/routes/config/utils.ts +2 -2
- package/src/routes/doctor.ts +395 -161
- package/src/routes/files.ts +650 -260
- package/src/routes/git/branch.ts +143 -52
- package/src/routes/git/commit.ts +347 -141
- package/src/routes/git/diff.ts +239 -116
- package/src/routes/git/init.ts +103 -23
- package/src/routes/git/pull.ts +167 -65
- package/src/routes/git/push.ts +222 -117
- package/src/routes/git/remote.ts +401 -100
- package/src/routes/git/staging.ts +502 -141
- package/src/routes/git/status.ts +171 -78
- package/src/routes/mcp.ts +1129 -404
- package/src/routes/openapi.ts +27 -4
- package/src/routes/ottorouter.ts +1221 -389
- package/src/routes/provider-usage.ts +153 -36
- package/src/routes/research.ts +817 -370
- package/src/routes/root.ts +50 -6
- package/src/routes/session-approval.ts +228 -54
- package/src/routes/session-files.ts +265 -134
- package/src/routes/session-messages.ts +330 -150
- package/src/routes/session-stream.ts +83 -2
- package/src/routes/sessions.ts +1830 -780
- package/src/routes/skills.ts +849 -161
- package/src/routes/terminals.ts +469 -103
- package/src/routes/tunnel.ts +394 -118
- package/src/runtime/agent/runner-reasoning.ts +38 -3
- package/src/runtime/agent/runner.ts +1 -0
- package/src/runtime/ask/service.ts +1 -0
- package/src/runtime/message/compaction-limits.ts +3 -3
- package/src/runtime/provider/reasoning.ts +18 -7
- package/src/runtime/session/db-operations.ts +4 -3
- package/src/runtime/utils/token.ts +7 -2
- package/src/tools/adapter.ts +21 -0
- package/src/openapi/paths/ask.ts +0 -81
- package/src/openapi/paths/auth.ts +0 -687
- package/src/openapi/paths/branch.ts +0 -102
- package/src/openapi/paths/config.ts +0 -485
- package/src/openapi/paths/doctor.ts +0 -165
- package/src/openapi/paths/files.ts +0 -236
- package/src/openapi/paths/git.ts +0 -690
- package/src/openapi/paths/mcp.ts +0 -339
- package/src/openapi/paths/messages.ts +0 -103
- package/src/openapi/paths/ottorouter.ts +0 -594
- package/src/openapi/paths/provider-usage.ts +0 -59
- package/src/openapi/paths/research.ts +0 -227
- package/src/openapi/paths/session-approval.ts +0 -93
- package/src/openapi/paths/session-extras.ts +0 -336
- package/src/openapi/paths/session-files.ts +0 -91
- package/src/openapi/paths/sessions.ts +0 -210
- package/src/openapi/paths/skills.ts +0 -377
- package/src/openapi/paths/stream.ts +0 -26
- package/src/openapi/paths/terminals.ts +0 -226
- package/src/openapi/paths/tunnel.ts +0 -163
- package/src/openapi/spec.ts +0 -73
package/src/routes/files.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { promisify } from 'node:util';
|
|
|
8
8
|
import { serializeError } from '../runtime/errors/api-error.ts';
|
|
9
9
|
import { logger } from '@ottocode/sdk';
|
|
10
10
|
import { resolveBinary } from '@ottocode/sdk/tools/bin-manager';
|
|
11
|
+
import { openApiRoute } from '../openapi/route.ts';
|
|
11
12
|
|
|
12
13
|
const execAsync = promisify(exec);
|
|
13
14
|
|
|
@@ -314,291 +315,680 @@ async function getGitIgnoredFiles(
|
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
export function registerFilesRoutes(app: Hono) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
318
|
+
openApiRoute(
|
|
319
|
+
app,
|
|
320
|
+
{
|
|
321
|
+
method: 'get',
|
|
322
|
+
path: '/v1/files',
|
|
323
|
+
tags: ['files'],
|
|
324
|
+
operationId: 'listFiles',
|
|
325
|
+
summary: 'List project files',
|
|
326
|
+
description:
|
|
327
|
+
'Returns list of files in the project directory, excluding common build artifacts and dependencies',
|
|
328
|
+
parameters: [
|
|
329
|
+
{
|
|
330
|
+
in: 'query',
|
|
331
|
+
name: 'project',
|
|
332
|
+
required: false,
|
|
333
|
+
schema: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
},
|
|
336
|
+
description:
|
|
337
|
+
'Project root override (defaults to current working directory).',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
in: 'query',
|
|
341
|
+
name: 'maxDepth',
|
|
342
|
+
required: false,
|
|
343
|
+
schema: {
|
|
344
|
+
type: 'integer',
|
|
345
|
+
default: 10,
|
|
346
|
+
},
|
|
347
|
+
description: 'Maximum directory depth to traverse',
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
in: 'query',
|
|
351
|
+
name: 'limit',
|
|
352
|
+
required: false,
|
|
353
|
+
schema: {
|
|
354
|
+
type: 'integer',
|
|
355
|
+
default: 1000,
|
|
356
|
+
},
|
|
357
|
+
description: 'Maximum number of files to return',
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
responses: {
|
|
361
|
+
'200': {
|
|
362
|
+
description: 'OK',
|
|
363
|
+
content: {
|
|
364
|
+
'application/json': {
|
|
365
|
+
schema: {
|
|
366
|
+
type: 'object',
|
|
367
|
+
properties: {
|
|
368
|
+
files: {
|
|
369
|
+
type: 'array',
|
|
370
|
+
items: {
|
|
371
|
+
type: 'string',
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
changedFiles: {
|
|
375
|
+
type: 'array',
|
|
376
|
+
items: {
|
|
377
|
+
type: 'object',
|
|
378
|
+
properties: {
|
|
379
|
+
path: {
|
|
380
|
+
type: 'string',
|
|
381
|
+
},
|
|
382
|
+
status: {
|
|
383
|
+
type: 'string',
|
|
384
|
+
enum: [
|
|
385
|
+
'added',
|
|
386
|
+
'modified',
|
|
387
|
+
'deleted',
|
|
388
|
+
'renamed',
|
|
389
|
+
'untracked',
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
required: ['path', 'status'],
|
|
394
|
+
},
|
|
395
|
+
description:
|
|
396
|
+
'List of files with uncommitted changes (from git status)',
|
|
397
|
+
},
|
|
398
|
+
truncated: {
|
|
399
|
+
type: 'boolean',
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
required: ['files', 'changedFiles', 'truncated'],
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
async (c) => {
|
|
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
|
+
);
|
|
338
426
|
|
|
339
|
-
|
|
340
|
-
const gitignorePatterns = await parseGitignore(projectRoot);
|
|
341
|
-
result = await traverseDirectory(
|
|
342
|
-
projectRoot,
|
|
427
|
+
let result = await listFilesWithRg(
|
|
343
428
|
projectRoot,
|
|
344
429
|
maxDepth,
|
|
345
|
-
0,
|
|
346
430
|
limit,
|
|
347
|
-
|
|
348
|
-
gitignorePatterns,
|
|
431
|
+
policy.includeIgnored,
|
|
349
432
|
);
|
|
350
|
-
}
|
|
351
433
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (!aChanged && bChanged) return 1;
|
|
365
|
-
return a.localeCompare(b);
|
|
366
|
-
});
|
|
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
|
+
}
|
|
367
446
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
+
},
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
openApiRoute(
|
|
487
|
+
app,
|
|
488
|
+
{
|
|
489
|
+
method: 'get',
|
|
490
|
+
path: '/v1/files/search',
|
|
491
|
+
tags: ['files'],
|
|
492
|
+
operationId: 'searchFiles',
|
|
493
|
+
summary: 'Search project files',
|
|
494
|
+
description:
|
|
495
|
+
'Searches files for mentions and quick-open. Excludes dependencies, build artifacts, and gitignored files by default.',
|
|
496
|
+
parameters: [
|
|
497
|
+
{
|
|
498
|
+
in: 'query',
|
|
499
|
+
name: 'project',
|
|
500
|
+
required: false,
|
|
501
|
+
schema: {
|
|
502
|
+
type: 'string',
|
|
503
|
+
},
|
|
504
|
+
description:
|
|
505
|
+
'Project root override (defaults to current working directory).',
|
|
382
506
|
},
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
507
|
+
{
|
|
508
|
+
in: 'query',
|
|
509
|
+
name: 'q',
|
|
510
|
+
required: false,
|
|
511
|
+
schema: {
|
|
512
|
+
type: 'string',
|
|
513
|
+
default: '',
|
|
514
|
+
},
|
|
515
|
+
description: 'Search query',
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
in: 'query',
|
|
519
|
+
name: 'maxDepth',
|
|
520
|
+
required: false,
|
|
521
|
+
schema: {
|
|
522
|
+
type: 'integer',
|
|
523
|
+
},
|
|
524
|
+
description: 'Maximum directory depth to traverse',
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
in: 'query',
|
|
528
|
+
name: 'limit',
|
|
529
|
+
required: false,
|
|
530
|
+
schema: {
|
|
531
|
+
type: 'integer',
|
|
532
|
+
},
|
|
533
|
+
description: 'Maximum number of files to return',
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
responses: {
|
|
537
|
+
'200': {
|
|
538
|
+
description: 'OK',
|
|
539
|
+
content: {
|
|
540
|
+
'application/json': {
|
|
541
|
+
schema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: {
|
|
544
|
+
files: {
|
|
545
|
+
type: 'array',
|
|
546
|
+
items: {
|
|
547
|
+
type: 'string',
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
changedFiles: {
|
|
551
|
+
type: 'array',
|
|
552
|
+
items: {
|
|
553
|
+
type: 'object',
|
|
554
|
+
properties: {
|
|
555
|
+
path: {
|
|
556
|
+
type: 'string',
|
|
557
|
+
},
|
|
558
|
+
status: {
|
|
559
|
+
type: 'string',
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
required: ['path', 'status'],
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
truncated: {
|
|
566
|
+
type: 'boolean',
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
required: ['files', 'changedFiles', 'truncated'],
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
async (c) => {
|
|
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
|
+
);
|
|
413
594
|
|
|
414
|
-
|
|
415
|
-
const gitignorePatterns = await parseGitignore(projectRoot);
|
|
416
|
-
result = await traverseDirectory(
|
|
417
|
-
projectRoot,
|
|
595
|
+
let result = await listFilesWithRg(
|
|
418
596
|
projectRoot,
|
|
419
597
|
maxDepth,
|
|
420
|
-
0,
|
|
421
598
|
limit,
|
|
422
|
-
|
|
423
|
-
|
|
599
|
+
policy.includeIgnored,
|
|
600
|
+
query,
|
|
424
601
|
);
|
|
425
|
-
|
|
426
|
-
if (
|
|
427
|
-
const
|
|
428
|
-
|
|
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,
|
|
429
613
|
);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|
|
434
624
|
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const [changedFiles, ignoredFiles] = await Promise.all([
|
|
438
|
-
getChangedFiles(projectRoot),
|
|
439
|
-
getGitIgnoredFiles(projectRoot, result.files),
|
|
440
|
-
]);
|
|
441
|
-
|
|
442
|
-
result.files.sort((a, b) => {
|
|
443
|
-
const aIgnored = ignoredFiles.has(a);
|
|
444
|
-
const bIgnored = ignoredFiles.has(b);
|
|
445
|
-
if (aIgnored !== bIgnored) return aIgnored ? 1 : -1;
|
|
446
|
-
const aChanged = changedFiles.has(a);
|
|
447
|
-
const bChanged = changedFiles.has(b);
|
|
448
|
-
if (aChanged && !bChanged) return -1;
|
|
449
|
-
if (!aChanged && bChanged) return 1;
|
|
450
|
-
return a.localeCompare(b);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
return c.json({
|
|
454
|
-
files: result.files,
|
|
455
|
-
ignoredFiles: Array.from(ignoredFiles),
|
|
456
|
-
changedFiles: Array.from(changedFiles.entries()).map(
|
|
457
|
-
([path, status]) => ({
|
|
458
|
-
path,
|
|
459
|
-
status,
|
|
460
|
-
}),
|
|
461
|
-
),
|
|
462
|
-
truncated: result.truncated,
|
|
463
|
-
policy: {
|
|
464
|
-
maxDepth,
|
|
465
|
-
limit,
|
|
466
|
-
home: isHomeDirectory(projectRoot),
|
|
467
|
-
},
|
|
468
|
-
});
|
|
469
|
-
} catch (err) {
|
|
470
|
-
logger.error('Files search route error:', err);
|
|
471
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
625
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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);
|
|
482
661
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
openApiRoute(
|
|
666
|
+
app,
|
|
667
|
+
{
|
|
668
|
+
method: 'get',
|
|
669
|
+
path: '/v1/files/tree',
|
|
670
|
+
tags: ['files'],
|
|
671
|
+
operationId: 'getFileTree',
|
|
672
|
+
summary: 'Get directory tree listing',
|
|
673
|
+
parameters: [
|
|
674
|
+
{
|
|
675
|
+
in: 'query',
|
|
676
|
+
name: 'project',
|
|
677
|
+
required: false,
|
|
678
|
+
schema: {
|
|
679
|
+
type: 'string',
|
|
680
|
+
},
|
|
681
|
+
description:
|
|
682
|
+
'Project root override (defaults to current working directory).',
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
in: 'query',
|
|
686
|
+
name: 'path',
|
|
687
|
+
required: false,
|
|
688
|
+
schema: {
|
|
689
|
+
type: 'string',
|
|
690
|
+
default: '.',
|
|
691
|
+
},
|
|
692
|
+
description: 'Directory path relative to project root',
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
responses: {
|
|
696
|
+
'200': {
|
|
697
|
+
description: 'OK',
|
|
698
|
+
content: {
|
|
699
|
+
'application/json': {
|
|
700
|
+
schema: {
|
|
701
|
+
type: 'object',
|
|
702
|
+
properties: {
|
|
703
|
+
items: {
|
|
704
|
+
type: 'array',
|
|
705
|
+
items: {
|
|
706
|
+
type: 'object',
|
|
707
|
+
properties: {
|
|
708
|
+
name: {
|
|
709
|
+
type: 'string',
|
|
710
|
+
},
|
|
711
|
+
path: {
|
|
712
|
+
type: 'string',
|
|
713
|
+
},
|
|
714
|
+
type: {
|
|
715
|
+
type: 'string',
|
|
716
|
+
enum: ['file', 'directory'],
|
|
717
|
+
},
|
|
718
|
+
gitignored: {
|
|
719
|
+
type: 'boolean',
|
|
720
|
+
},
|
|
721
|
+
vendor: {
|
|
722
|
+
type: 'boolean',
|
|
723
|
+
},
|
|
724
|
+
searchable: {
|
|
725
|
+
type: 'boolean',
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
required: ['name', 'path', 'type'],
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
path: {
|
|
732
|
+
type: 'string',
|
|
733
|
+
},
|
|
734
|
+
truncated: {
|
|
735
|
+
type: 'boolean',
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
required: ['items', 'path', 'truncated'],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
async (c) => {
|
|
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);
|
|
521
752
|
}
|
|
522
|
-
}
|
|
523
753
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
}
|
|
547
793
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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);
|
|
551
806
|
}
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
openApiRoute(
|
|
811
|
+
app,
|
|
812
|
+
{
|
|
813
|
+
method: 'get',
|
|
814
|
+
path: '/v1/files/read',
|
|
815
|
+
tags: ['files'],
|
|
816
|
+
operationId: 'readFile',
|
|
817
|
+
summary: 'Read file content',
|
|
818
|
+
parameters: [
|
|
819
|
+
{
|
|
820
|
+
in: 'query',
|
|
821
|
+
name: 'project',
|
|
822
|
+
required: false,
|
|
823
|
+
schema: {
|
|
824
|
+
type: 'string',
|
|
825
|
+
},
|
|
826
|
+
description:
|
|
827
|
+
'Project root override (defaults to current working directory).',
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
in: 'query',
|
|
831
|
+
name: 'path',
|
|
832
|
+
required: true,
|
|
833
|
+
schema: {
|
|
834
|
+
type: 'string',
|
|
835
|
+
},
|
|
836
|
+
description: 'File path relative to project root',
|
|
837
|
+
},
|
|
838
|
+
],
|
|
839
|
+
responses: {
|
|
840
|
+
'200': {
|
|
841
|
+
description: 'OK',
|
|
842
|
+
content: {
|
|
843
|
+
'application/json': {
|
|
844
|
+
schema: {
|
|
845
|
+
type: 'object',
|
|
846
|
+
properties: {
|
|
847
|
+
content: {
|
|
848
|
+
type: 'string',
|
|
849
|
+
},
|
|
850
|
+
path: {
|
|
851
|
+
type: 'string',
|
|
852
|
+
},
|
|
853
|
+
extension: {
|
|
854
|
+
type: 'string',
|
|
855
|
+
},
|
|
856
|
+
lineCount: {
|
|
857
|
+
type: 'integer',
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
required: ['content', 'path', 'extension', 'lineCount'],
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
'400': {
|
|
866
|
+
description: 'Bad Request',
|
|
867
|
+
content: {
|
|
868
|
+
'application/json': {
|
|
869
|
+
schema: {
|
|
870
|
+
type: 'object',
|
|
871
|
+
properties: {
|
|
872
|
+
error: {
|
|
873
|
+
type: 'string',
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
required: ['error'],
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
async (c) => {
|
|
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
|
+
}
|
|
552
894
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
return c.json({ content, path: filePath, extension, lineCount });
|
|
558
|
-
} catch (err) {
|
|
559
|
-
logger.error('Files read route error:', err);
|
|
560
|
-
return c.json({ error: serializeError(err) }, 500);
|
|
561
|
-
}
|
|
562
|
-
});
|
|
895
|
+
const absPath = join(projectRoot, filePath);
|
|
896
|
+
if (!absPath.startsWith(projectRoot)) {
|
|
897
|
+
return c.json({ error: 'Path traversal not allowed' }, 403);
|
|
898
|
+
}
|
|
563
899
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const filePath = c.req.query('path');
|
|
900
|
+
const content = await readFile(absPath, 'utf-8');
|
|
901
|
+
const extension = filePath.split('.').pop()?.toLowerCase() ?? '';
|
|
902
|
+
const lineCount = content.split('\n').length;
|
|
568
903
|
|
|
569
|
-
|
|
570
|
-
|
|
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);
|
|
571
908
|
}
|
|
909
|
+
},
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
openApiRoute(
|
|
913
|
+
app,
|
|
914
|
+
{
|
|
915
|
+
method: 'get',
|
|
916
|
+
path: '/v1/files/raw',
|
|
917
|
+
tags: ['files'],
|
|
918
|
+
operationId: 'getFileRaw',
|
|
919
|
+
summary: 'Read raw file bytes',
|
|
920
|
+
parameters: [
|
|
921
|
+
{
|
|
922
|
+
in: 'query',
|
|
923
|
+
name: 'project',
|
|
924
|
+
required: false,
|
|
925
|
+
schema: { type: 'string' },
|
|
926
|
+
description:
|
|
927
|
+
'Project root override (defaults to current working directory).',
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
in: 'query',
|
|
931
|
+
name: 'path',
|
|
932
|
+
required: true,
|
|
933
|
+
schema: { type: 'string' },
|
|
934
|
+
description: 'Relative file path to read.',
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
responses: {
|
|
938
|
+
'200': {
|
|
939
|
+
description: 'Raw file content',
|
|
940
|
+
content: {
|
|
941
|
+
'application/octet-stream': {
|
|
942
|
+
schema: { type: 'string', format: 'binary' },
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
},
|
|
946
|
+
'400': { description: 'Missing path parameter' },
|
|
947
|
+
'403': { description: 'Path traversal not allowed' },
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
async (c) => {
|
|
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
|
+
}
|
|
572
961
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
962
|
+
const absPath = join(projectRoot, filePath);
|
|
963
|
+
if (!absPath.startsWith(projectRoot)) {
|
|
964
|
+
return c.json({ error: 'Path traversal not allowed' }, 403);
|
|
965
|
+
}
|
|
577
966
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
+
},
|
|
993
|
+
);
|
|
604
994
|
}
|