git-contextor
Version:
A code context tool with vector search and real-time monitoring, with optional Git integration.
183 lines (153 loc) • 6.99 kB
JavaScript
const express = require('express');
const path = require('path');
const fs = require('fs').promises;
const ignore = require('ignore');
const logger = require('../../cli/utils/logger');
const { handleChatQuery } = require('./chat');
/**
* Creates and returns the shared access router.
* @param {object} services - The core services of the application.
* @returns {express.Router} The configured router.
*/
module.exports = (services) => {
const router = express.Router();
const { contextOptimizer, sharingService } = services;
// Route to serve the dedicated HTML page for a share.
// This does not require an API key to view.
router.get('/:shareId', (req, res) => {
const publicPath = path.resolve(__dirname, '../../ui/public');
res.sendFile(path.join(publicPath, 'shared.html'));
});
// Serve static assets for shared pages without authentication
// These need to be accessible for the shared page to load properly
router.get('/:shareId/css/style.css', (req, res) => {
const publicPath = path.resolve(__dirname, '../../ui/public');
res.sendFile(path.join(publicPath, 'css', 'style.css'));
});
router.get('/:shareId/js/shared.js', (req, res) => {
const publicPath = path.resolve(__dirname, '../../ui/public');
res.sendFile(path.join(publicPath, 'js', 'shared.js'));
});
// Middleware to validate shared access
router.use('/:shareId/*', async (req, res, next) => {
const { shareId } = req.params;
const apiKey = req.headers['x-share-key'];
try {
const share = await sharingService.validateShare(shareId, apiKey);
req.share = share;
next();
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// --- File Browser Endpoints for Shared Access ---
router.get('/:shareId/files/tree', async (req, res) => {
const repoPath = contextOptimizer.config.repository.path;
try {
const gitignorePath = path.join(repoPath, '.gitignore');
const ig = ignore();
try {
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
ig.add(gitignoreContent);
} catch (e) {
if (e.code !== 'ENOENT') logger.warn('Could not read .gitignore file for share:', e);
}
ig.add('.gitcontextor/*');
const buildFileTree = async (dir, rootDir) => {
const dirents = await fs.readdir(dir, { withFileTypes: true });
const tree = [];
for (const dirent of dirents) {
const fullPath = path.join(dir, dirent.name);
const relativePath = path.relative(rootDir, fullPath);
if (ig.ignores(relativePath)) continue;
if (dirent.isDirectory()) {
tree.push({ name: dirent.name, type: 'directory', path: relativePath, children: await buildFileTree(fullPath, rootDir) });
} else if (dirent.isFile()) {
tree.push({ name: dirent.name, type: 'file', path: relativePath });
}
}
return tree;
};
const fileTree = await buildFileTree(repoPath, repoPath);
await sharingService.incrementUsage(req.params.shareId);
res.json(fileTree);
} catch (error) {
logger.error('Error building file tree for share:', error);
res.status(500).json({ error: 'Failed to build file tree.' });
}
});
router.get('/:shareId/files/content', async (req, res) => {
const relativeFilePath = req.query.path;
if (!relativeFilePath || path.isAbsolute(relativeFilePath) || relativeFilePath.includes('..')) {
return res.status(400).json({ error: 'Invalid file path.' });
}
const repoPath = contextOptimizer.config.repository.path;
const absoluteFilePath = path.join(repoPath, relativeFilePath);
if (!absoluteFilePath.startsWith(repoPath)) {
return res.status(400).json({ error: 'File path is outside the repository.' });
}
try {
const content = await fs.readFile(absoluteFilePath, 'utf8');
await sharingService.incrementUsage(req.params.shareId);
res.json({ content });
} catch (error) {
if (error.code === 'ENOENT') return res.status(404).json({ error: 'File not found.' });
logger.error(`Error reading file content for share ${req.params.shareId}:`, error);
res.status(500).json({ error: 'Failed to read file content.' });
}
});
// Semantic search endpoint for shared access
router.post('/:shareId/search', async (req, res, next) => {
const { query, maxTokens, filter } = req.body;
if (!query) {
return res.status(400).json({ error: 'Missing query' });
}
try {
await sharingService.incrementUsage(req.params.shareId);
const options = {
maxTokens: maxTokens ? parseInt(maxTokens, 10) : undefined,
filter
};
const result = await contextOptimizer.search(query, options);
res.json({
...result,
share_id: req.params.shareId,
queries_remaining: req.share.max_queries - req.share.access_count
});
} catch (error) {
logger.error(`Error in semantic search for share ${req.params.shareId}:`, error);
res.status(500).json({ error: 'An error occurred during the search.' });
}
});
// Chat endpoint for shared access
router.post('/:shareId/chat', async (req, res, next) => {
const { query } = req.body;
if (!query) {
return res.status(400).json({ error: 'Missing query' });
}
try {
await sharingService.incrementUsage(req.params.shareId);
// Use the unified chat handler for consistent behavior
const result = await handleChatQuery(query, services, `shared: ${req.share.scope.join(', ')}`);
res.json({
...result,
share_id: req.params.shareId,
queries_remaining: req.share.max_queries - req.share.access_count
});
} catch (error) {
next(error);
}
});
// Share info endpoint
router.get('/:shareId/info', (req, res) => {
res.json({
share_id: req.params.shareId,
description: req.share.description,
expires_at: req.share.expires_at,
queries_used: req.share.access_count,
queries_remaining: req.share.max_queries - req.share.access_count,
scope: req.share.scope
});
});
return router;
};