fix: proper project isolation and relative path matching for file-context hook
- Use getProjectContext(cwd).allProjects for project scoping (same as SessionStart) - Convert absolute file_path to relative using cwd (observations store relative paths) - API accepts comma-separated projects param with IN() SQL filter - Remove basename matching — use full relative path to avoid cross-file collisions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -9,9 +9,11 @@ import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js'
|
||||
import { ensureWorkerRunning, workerHttpRequest } from '../../shared/worker-utils.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
|
||||
import path from 'path';
|
||||
import { isProjectExcluded } from '../../utils/project-filter.js';
|
||||
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
||||
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
||||
import { getProjectContext } from '../../utils/project-name.js';
|
||||
|
||||
const TYPE_ICONS: Record<string, string> = {
|
||||
decision: '\u2696\uFE0F',
|
||||
@@ -103,7 +105,13 @@ export const fileContextHandler: EventHandler = {
|
||||
|
||||
// Query worker for observations related to this file
|
||||
try {
|
||||
const queryParams = new URLSearchParams({ path: filePath });
|
||||
const context = getProjectContext(input.cwd);
|
||||
// Observations store relative paths — convert absolute to relative using cwd
|
||||
const cwd = input.cwd || process.cwd();
|
||||
const relativePath = path.isAbsolute(filePath) ? path.relative(cwd, filePath) : filePath;
|
||||
const queryParams = new URLSearchParams({ path: relativePath });
|
||||
// Pass all project names (parent + worktree) for unified lookup
|
||||
queryParams.set('projects', context.allProjects.join(','));
|
||||
|
||||
const response = await workerHttpRequest(`/api/observations/by-file?${queryParams.toString()}`, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { Database } from 'bun:sqlite';
|
||||
import path from 'path';
|
||||
import { logger } from '../../../utils/logger.js';
|
||||
import type { ObservationRecord } from '../../../types/database.js';
|
||||
import type { GetObservationsByIdsOptions, ObservationSessionRow } from './types.js';
|
||||
@@ -114,29 +113,25 @@ export function getObservationsForSession(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observations associated with a given file path.
|
||||
* Searches both files_read and files_modified using basename matching
|
||||
* to handle differing absolute paths across sessions.
|
||||
* Get observations associated with a given file path, scoped to specific projects.
|
||||
* Matches on the full file path (not just basename) to avoid cross-project collisions.
|
||||
*/
|
||||
export function getObservationsByFilePath(
|
||||
db: Database,
|
||||
filePath: string,
|
||||
options?: { project?: string; limit?: number }
|
||||
options?: { projects?: string[]; limit?: number }
|
||||
): ObservationRecord[] {
|
||||
const basename = path.basename(filePath);
|
||||
const likePattern = `%${basename}%`;
|
||||
const likePattern = `%${filePath}%`;
|
||||
const limit = options?.limit ?? 30;
|
||||
|
||||
const additionalConditions: string[] = [];
|
||||
const params: any[] = [likePattern, likePattern];
|
||||
|
||||
if (options?.project) {
|
||||
additionalConditions.push('AND project = ?');
|
||||
params.push(options.project);
|
||||
let projectClause = '';
|
||||
if (options?.projects?.length) {
|
||||
const placeholders = options.projects.map(() => '?').join(',');
|
||||
projectClause = `AND project IN (${placeholders})`;
|
||||
params.push(...options.projects);
|
||||
}
|
||||
|
||||
const additionalWhere = additionalConditions.join(' ');
|
||||
|
||||
const stmt = db.prepare(`
|
||||
SELECT *
|
||||
FROM observations
|
||||
@@ -144,7 +139,7 @@ export function getObservationsByFilePath(
|
||||
EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?)
|
||||
OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?)
|
||||
)
|
||||
${additionalWhere}
|
||||
${projectClause}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ${limit}
|
||||
`);
|
||||
|
||||
@@ -111,8 +111,8 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
});
|
||||
|
||||
/**
|
||||
* Get observations associated with a file path
|
||||
* GET /api/observations/by-file?path=<file_path>&project=<project>&limit=30
|
||||
* Get observations associated with a file path, scoped to projects
|
||||
* GET /api/observations/by-file?path=<file_path>&projects=<comma,separated>&limit=30
|
||||
*/
|
||||
private handleGetObservationsByFile = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const filePath = req.query.path as string | undefined;
|
||||
@@ -121,11 +121,12 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
const project = req.query.project as string | undefined;
|
||||
const projectsParam = req.query.projects as string | undefined;
|
||||
const projects = projectsParam ? projectsParam.split(',').filter(Boolean) : undefined;
|
||||
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined;
|
||||
|
||||
const db = this.dbManager.getSessionStore().db;
|
||||
const observations = getObservationsByFilePath(db, filePath, { project, limit });
|
||||
const observations = getObservationsByFilePath(db, filePath, { projects, limit });
|
||||
|
||||
res.json({ observations, count: observations.length });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user