preflight-dev 3.1.0
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/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/cli.js +11 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.js +154 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +118 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/embeddings.d.ts +11 -0
- package/dist/lib/embeddings.js +88 -0
- package/dist/lib/embeddings.js.map +1 -0
- package/dist/lib/files.d.ts +15 -0
- package/dist/lib/files.js +60 -0
- package/dist/lib/files.js.map +1 -0
- package/dist/lib/git-extractor.d.ts +9 -0
- package/dist/lib/git-extractor.js +116 -0
- package/dist/lib/git-extractor.js.map +1 -0
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.js +86 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/session-parser.d.ts +45 -0
- package/dist/lib/session-parser.js +267 -0
- package/dist/lib/session-parser.js.map +1 -0
- package/dist/lib/state.d.ts +21 -0
- package/dist/lib/state.js +86 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/timeline-db.d.ts +67 -0
- package/dist/lib/timeline-db.js +380 -0
- package/dist/lib/timeline-db.js.map +1 -0
- package/dist/lib/triage.d.ts +29 -0
- package/dist/lib/triage.js +193 -0
- package/dist/lib/triage.js.map +1 -0
- package/dist/profiles.d.ts +3 -0
- package/dist/profiles.js +65 -0
- package/dist/profiles.js.map +1 -0
- package/dist/tools/audit-workspace.d.ts +2 -0
- package/dist/tools/audit-workspace.js +86 -0
- package/dist/tools/audit-workspace.js.map +1 -0
- package/dist/tools/checkpoint.d.ts +2 -0
- package/dist/tools/checkpoint.js +108 -0
- package/dist/tools/checkpoint.js.map +1 -0
- package/dist/tools/clarify-intent.d.ts +2 -0
- package/dist/tools/clarify-intent.js +180 -0
- package/dist/tools/clarify-intent.js.map +1 -0
- package/dist/tools/enrich-agent-task.d.ts +2 -0
- package/dist/tools/enrich-agent-task.js +97 -0
- package/dist/tools/enrich-agent-task.js.map +1 -0
- package/dist/tools/generate-scorecard.d.ts +2 -0
- package/dist/tools/generate-scorecard.js +617 -0
- package/dist/tools/generate-scorecard.js.map +1 -0
- package/dist/tools/log-correction.d.ts +2 -0
- package/dist/tools/log-correction.js +76 -0
- package/dist/tools/log-correction.js.map +1 -0
- package/dist/tools/onboard-project.d.ts +2 -0
- package/dist/tools/onboard-project.js +179 -0
- package/dist/tools/onboard-project.js.map +1 -0
- package/dist/tools/preflight-check.d.ts +2 -0
- package/dist/tools/preflight-check.js +229 -0
- package/dist/tools/preflight-check.js.map +1 -0
- package/dist/tools/prompt-score.d.ts +2 -0
- package/dist/tools/prompt-score.js +132 -0
- package/dist/tools/prompt-score.js.map +1 -0
- package/dist/tools/scan-sessions.d.ts +2 -0
- package/dist/tools/scan-sessions.js +182 -0
- package/dist/tools/scan-sessions.js.map +1 -0
- package/dist/tools/scope-work.d.ts +2 -0
- package/dist/tools/scope-work.js +214 -0
- package/dist/tools/scope-work.js.map +1 -0
- package/dist/tools/search-history.d.ts +2 -0
- package/dist/tools/search-history.js +130 -0
- package/dist/tools/search-history.js.map +1 -0
- package/dist/tools/sequence-tasks.d.ts +2 -0
- package/dist/tools/sequence-tasks.js +165 -0
- package/dist/tools/sequence-tasks.js.map +1 -0
- package/dist/tools/session-handoff.d.ts +2 -0
- package/dist/tools/session-handoff.js +113 -0
- package/dist/tools/session-handoff.js.map +1 -0
- package/dist/tools/session-health.d.ts +2 -0
- package/dist/tools/session-health.js +111 -0
- package/dist/tools/session-health.js.map +1 -0
- package/dist/tools/session-stats.d.ts +2 -0
- package/dist/tools/session-stats.js +112 -0
- package/dist/tools/session-stats.js.map +1 -0
- package/dist/tools/sharpen-followup.d.ts +2 -0
- package/dist/tools/sharpen-followup.js +192 -0
- package/dist/tools/sharpen-followup.js.map +1 -0
- package/dist/tools/timeline-view.d.ts +2 -0
- package/dist/tools/timeline-view.js +165 -0
- package/dist/tools/timeline-view.js.map +1 -0
- package/dist/tools/token-audit.d.ts +2 -0
- package/dist/tools/token-audit.js +227 -0
- package/dist/tools/token-audit.js.map +1 -0
- package/dist/tools/verify-completion.d.ts +2 -0
- package/dist/tools/verify-completion.js +154 -0
- package/dist/tools/verify-completion.js.map +1 -0
- package/dist/tools/what-changed.d.ts +2 -0
- package/dist/tools/what-changed.js +40 -0
- package/dist/tools/what-changed.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/cli/init.ts +133 -0
- package/src/index.ts +135 -0
- package/src/lib/config.ts +157 -0
- package/src/lib/embeddings.ts +118 -0
- package/src/lib/files.ts +59 -0
- package/src/lib/git-extractor.ts +137 -0
- package/src/lib/git.ts +89 -0
- package/src/lib/session-parser.ts +325 -0
- package/src/lib/state.ts +86 -0
- package/src/lib/timeline-db.ts +490 -0
- package/src/lib/triage.ts +255 -0
- package/src/profiles.ts +70 -0
- package/src/templates/config.yml +23 -0
- package/src/templates/triage.yml +27 -0
- package/src/tools/audit-workspace.ts +97 -0
- package/src/tools/checkpoint.ts +119 -0
- package/src/tools/clarify-intent.ts +191 -0
- package/src/tools/enrich-agent-task.ts +108 -0
- package/src/tools/generate-scorecard.ts +673 -0
- package/src/tools/log-correction.ts +89 -0
- package/src/tools/onboard-project.ts +214 -0
- package/src/tools/preflight-check.ts +263 -0
- package/src/tools/prompt-score.ts +150 -0
- package/src/tools/scan-sessions.ts +209 -0
- package/src/tools/scope-work.ts +238 -0
- package/src/tools/search-history.ts +145 -0
- package/src/tools/sequence-tasks.ts +182 -0
- package/src/tools/session-handoff.ts +125 -0
- package/src/tools/session-health.ts +107 -0
- package/src/tools/session-stats.ts +134 -0
- package/src/tools/sharpen-followup.ts +200 -0
- package/src/tools/timeline-view.ts +181 -0
- package/src/tools/token-audit.ts +259 -0
- package/src/tools/verify-completion.ts +159 -0
- package/src/tools/what-changed.ts +48 -0
- package/src/types.ts +87 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFile, writeFile, mkdir, stat } from "node:fs/promises";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join, basename, resolve } from "node:path";
|
|
7
|
+
import { createEmbeddingProvider, type EmbeddingProvider, type EmbeddingConfig } from "./embeddings.js";
|
|
8
|
+
import type { ProjectMeta, ProjectRegistry, SearchScope } from "../types.js";
|
|
9
|
+
|
|
10
|
+
// --- Types ---
|
|
11
|
+
|
|
12
|
+
export const EVENT_TYPES = [
|
|
13
|
+
"prompt", "assistant", "correction", "commit",
|
|
14
|
+
"tool_call", "compaction", "sub_agent_spawn", "error",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type EventType = (typeof EVENT_TYPES)[number];
|
|
18
|
+
|
|
19
|
+
export interface TimelineEvent {
|
|
20
|
+
id?: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
type: EventType;
|
|
23
|
+
project: string;
|
|
24
|
+
project_name?: string;
|
|
25
|
+
branch: string;
|
|
26
|
+
session_id: string;
|
|
27
|
+
source_file: string;
|
|
28
|
+
source_line: number;
|
|
29
|
+
content: string;
|
|
30
|
+
content_preview?: string;
|
|
31
|
+
vector?: number[];
|
|
32
|
+
metadata?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TimelineRecord extends Required<TimelineEvent> {}
|
|
36
|
+
|
|
37
|
+
export interface SearchOptions {
|
|
38
|
+
project_dirs?: string[];
|
|
39
|
+
project?: string;
|
|
40
|
+
branch?: string;
|
|
41
|
+
type?: EventType;
|
|
42
|
+
since?: string;
|
|
43
|
+
until?: string;
|
|
44
|
+
limit?: number;
|
|
45
|
+
offset?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ProjectInfo {
|
|
49
|
+
project: string;
|
|
50
|
+
project_name: string;
|
|
51
|
+
hash: string;
|
|
52
|
+
event_count: number;
|
|
53
|
+
last_session_index?: string;
|
|
54
|
+
last_git_index?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TimelineConfig {
|
|
58
|
+
embedding_provider: "local" | "openai";
|
|
59
|
+
embedding_model: string;
|
|
60
|
+
openai_api_key?: string;
|
|
61
|
+
indexed_projects: Record<string, {
|
|
62
|
+
last_session_index: string;
|
|
63
|
+
last_git_index: string;
|
|
64
|
+
event_count: number;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Paths ---
|
|
69
|
+
|
|
70
|
+
const PREFLIGHT_DIR = join(homedir(), ".preflight");
|
|
71
|
+
const PROJECTS_DIR = join(PREFLIGHT_DIR, "projects");
|
|
72
|
+
const CONFIG_PATH = join(PREFLIGHT_DIR, "config.json");
|
|
73
|
+
const INDEX_PATH = join(PROJECTS_DIR, "index.json");
|
|
74
|
+
|
|
75
|
+
// --- Utilities ---
|
|
76
|
+
|
|
77
|
+
/** Create deterministic hash for project directory */
|
|
78
|
+
function hashProjectDir(projectDir: string): string {
|
|
79
|
+
const absolutePath = resolve(projectDir);
|
|
80
|
+
return createHash("sha256").update(absolutePath).digest("hex").slice(0, 12);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Get paths for a project's data */
|
|
84
|
+
function getProjectPaths(projectDir: string) {
|
|
85
|
+
const hash = hashProjectDir(projectDir);
|
|
86
|
+
const projectBase = join(PROJECTS_DIR, hash);
|
|
87
|
+
return {
|
|
88
|
+
hash,
|
|
89
|
+
projectDir: projectBase,
|
|
90
|
+
dbPath: join(projectBase, "timeline.lance"),
|
|
91
|
+
metaPath: join(projectBase, "meta.json"),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Config ---
|
|
96
|
+
|
|
97
|
+
const DEFAULT_CONFIG: TimelineConfig = {
|
|
98
|
+
embedding_provider: "local",
|
|
99
|
+
embedding_model: "Xenova/all-MiniLM-L6-v2",
|
|
100
|
+
indexed_projects: {},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export async function loadConfig(): Promise<TimelineConfig> {
|
|
104
|
+
try {
|
|
105
|
+
const raw = await readFile(CONFIG_PATH, "utf-8");
|
|
106
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
107
|
+
} catch {
|
|
108
|
+
return { ...DEFAULT_CONFIG };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function saveConfig(config: TimelineConfig): Promise<void> {
|
|
113
|
+
await mkdir(PREFLIGHT_DIR, { recursive: true });
|
|
114
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Project Registry ---
|
|
118
|
+
|
|
119
|
+
export async function loadProjectRegistry(): Promise<ProjectRegistry> {
|
|
120
|
+
try {
|
|
121
|
+
const raw = await readFile(INDEX_PATH, "utf-8");
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function saveProjectRegistry(registry: ProjectRegistry): Promise<void> {
|
|
129
|
+
await mkdir(PROJECTS_DIR, { recursive: true });
|
|
130
|
+
await writeFile(INDEX_PATH, JSON.stringify(registry, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function registerProject(projectDir: string): Promise<void> {
|
|
134
|
+
const absoluteDir = resolve(projectDir);
|
|
135
|
+
const registry = await loadProjectRegistry();
|
|
136
|
+
const { hash } = getProjectPaths(absoluteDir);
|
|
137
|
+
|
|
138
|
+
if (!registry[absoluteDir]) {
|
|
139
|
+
registry[absoluteDir] = {
|
|
140
|
+
hash,
|
|
141
|
+
onboarded_at: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
await saveProjectRegistry(registry);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Project Metadata ---
|
|
148
|
+
|
|
149
|
+
export async function loadProjectMeta(projectDir: string): Promise<ProjectMeta | null> {
|
|
150
|
+
const { metaPath } = getProjectPaths(projectDir);
|
|
151
|
+
try {
|
|
152
|
+
const raw = await readFile(metaPath, "utf-8");
|
|
153
|
+
return JSON.parse(raw);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function saveProjectMeta(projectDir: string, meta: ProjectMeta): Promise<void> {
|
|
160
|
+
const { projectDir: projectBase, metaPath } = getProjectPaths(projectDir);
|
|
161
|
+
await mkdir(projectBase, { recursive: true });
|
|
162
|
+
await writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// --- Database Manager ---
|
|
166
|
+
|
|
167
|
+
const _connections = new Map<string, lancedb.Connection>();
|
|
168
|
+
let _embedder: EmbeddingProvider | null = null;
|
|
169
|
+
|
|
170
|
+
export async function getDb(projectDir: string): Promise<lancedb.Connection> {
|
|
171
|
+
const absoluteDir = resolve(projectDir);
|
|
172
|
+
const { dbPath } = getProjectPaths(absoluteDir);
|
|
173
|
+
|
|
174
|
+
if (!_connections.has(absoluteDir)) {
|
|
175
|
+
await mkdir(PROJECTS_DIR, { recursive: true });
|
|
176
|
+
const db = await lancedb.connect(dbPath);
|
|
177
|
+
_connections.set(absoluteDir, db);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return _connections.get(absoluteDir)!;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function getEmbedder(): Promise<EmbeddingProvider> {
|
|
184
|
+
if (!_embedder) {
|
|
185
|
+
const config = await loadConfig();
|
|
186
|
+
_embedder = createEmbeddingProvider({
|
|
187
|
+
provider: config.embedding_provider,
|
|
188
|
+
apiKey: config.openai_api_key,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return _embedder;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function getEventsTable(projectDir: string): Promise<lancedb.Table> {
|
|
195
|
+
const db = await getDb(projectDir);
|
|
196
|
+
try {
|
|
197
|
+
return await db.openTable("events");
|
|
198
|
+
} catch {
|
|
199
|
+
// Create with a seed record then delete it — LanceDB needs data to infer schema
|
|
200
|
+
const embedder = await getEmbedder();
|
|
201
|
+
const zeroVector = new Array(embedder.dimensions).fill(0);
|
|
202
|
+
const seed = [{
|
|
203
|
+
id: "__seed__",
|
|
204
|
+
timestamp: new Date().toISOString(),
|
|
205
|
+
type: "prompt",
|
|
206
|
+
project: "",
|
|
207
|
+
project_name: "",
|
|
208
|
+
branch: "",
|
|
209
|
+
session_id: "",
|
|
210
|
+
source_file: "",
|
|
211
|
+
source_line: 0,
|
|
212
|
+
content: "",
|
|
213
|
+
content_preview: "",
|
|
214
|
+
vector: zeroVector,
|
|
215
|
+
metadata: "{}",
|
|
216
|
+
}];
|
|
217
|
+
const table = await db.createTable("events", seed);
|
|
218
|
+
await table.delete('id = "__seed__"');
|
|
219
|
+
return table;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// --- Core Operations ---
|
|
224
|
+
|
|
225
|
+
export async function insertEvents(events: TimelineEvent[], projectDir?: string): Promise<void> {
|
|
226
|
+
if (events.length === 0) return;
|
|
227
|
+
|
|
228
|
+
// Group events by project if no specific projectDir provided
|
|
229
|
+
const eventsByProject = new Map<string, TimelineEvent[]>();
|
|
230
|
+
|
|
231
|
+
for (const event of events) {
|
|
232
|
+
const targetProject = projectDir || event.project;
|
|
233
|
+
if (!targetProject) throw new Error("Event must have project or projectDir must be specified");
|
|
234
|
+
|
|
235
|
+
if (!eventsByProject.has(targetProject)) {
|
|
236
|
+
eventsByProject.set(targetProject, []);
|
|
237
|
+
}
|
|
238
|
+
eventsByProject.get(targetProject)!.push({ ...event, project: targetProject });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const embedder = await getEmbedder();
|
|
242
|
+
|
|
243
|
+
for (const [proj, projEvents] of eventsByProject) {
|
|
244
|
+
const table = await getEventsTable(proj);
|
|
245
|
+
|
|
246
|
+
const contents = projEvents.map((e) => e.content);
|
|
247
|
+
const vectors = await embedder.embedBatch(contents);
|
|
248
|
+
|
|
249
|
+
const records = projEvents.map((e, i) => ({
|
|
250
|
+
id: e.id || randomUUID(),
|
|
251
|
+
timestamp: e.timestamp,
|
|
252
|
+
type: e.type,
|
|
253
|
+
project: e.project,
|
|
254
|
+
project_name: e.project_name || basename(e.project),
|
|
255
|
+
branch: e.branch,
|
|
256
|
+
session_id: e.session_id,
|
|
257
|
+
source_file: e.source_file,
|
|
258
|
+
source_line: e.source_line,
|
|
259
|
+
content: e.content,
|
|
260
|
+
content_preview: e.content_preview || e.content.slice(0, 200),
|
|
261
|
+
vector: vectors[i],
|
|
262
|
+
metadata: e.metadata || "{}",
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
await table.add(records);
|
|
266
|
+
|
|
267
|
+
// Update project metadata
|
|
268
|
+
await registerProject(proj);
|
|
269
|
+
const meta = await loadProjectMeta(proj) || {
|
|
270
|
+
project_dir: resolve(proj),
|
|
271
|
+
onboarded_at: new Date().toISOString(),
|
|
272
|
+
event_count: 0,
|
|
273
|
+
};
|
|
274
|
+
meta.event_count += records.length;
|
|
275
|
+
await saveProjectMeta(proj, meta);
|
|
276
|
+
|
|
277
|
+
// Update legacy config for backward compatibility
|
|
278
|
+
const config = await loadConfig();
|
|
279
|
+
if (!config.indexed_projects[proj]) {
|
|
280
|
+
config.indexed_projects[proj] = {
|
|
281
|
+
last_session_index: records[records.length - 1].timestamp,
|
|
282
|
+
last_git_index: "1970-01-01T00:00:00Z",
|
|
283
|
+
event_count: meta.event_count,
|
|
284
|
+
};
|
|
285
|
+
} else {
|
|
286
|
+
config.indexed_projects[proj].event_count += records.length;
|
|
287
|
+
}
|
|
288
|
+
await saveConfig(config);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function buildWhereFilter(opts: SearchOptions): string | undefined {
|
|
293
|
+
const clauses: string[] = [];
|
|
294
|
+
if (opts.project) clauses.push(`project = '${opts.project}'`);
|
|
295
|
+
if (opts.branch) clauses.push(`branch = '${opts.branch}'`);
|
|
296
|
+
if (opts.type) clauses.push(`type = '${opts.type}'`);
|
|
297
|
+
if (opts.since) clauses.push(`timestamp >= '${opts.since}'`);
|
|
298
|
+
if (opts.until) clauses.push(`timestamp <= '${opts.until}'`);
|
|
299
|
+
return clauses.length > 0 ? clauses.join(" AND ") : undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Search across multiple projects and merge results by score */
|
|
303
|
+
export async function searchSemantic(
|
|
304
|
+
query: string,
|
|
305
|
+
opts: SearchOptions = {},
|
|
306
|
+
): Promise<TimelineRecord[]> {
|
|
307
|
+
const embedder = await getEmbedder();
|
|
308
|
+
const queryVector = await embedder.embed(query);
|
|
309
|
+
const limit = opts.limit || 20;
|
|
310
|
+
|
|
311
|
+
// Determine which projects to search
|
|
312
|
+
let projectsToSearch = opts.project_dirs || [];
|
|
313
|
+
if (projectsToSearch.length === 0 && opts.project) {
|
|
314
|
+
projectsToSearch = [opts.project];
|
|
315
|
+
}
|
|
316
|
+
if (projectsToSearch.length === 0) {
|
|
317
|
+
// Default to current project if available
|
|
318
|
+
if (process.env.CLAUDE_PROJECT_DIR) {
|
|
319
|
+
projectsToSearch = [process.env.CLAUDE_PROJECT_DIR];
|
|
320
|
+
} else {
|
|
321
|
+
// Fall back to all indexed projects
|
|
322
|
+
const registry = await loadProjectRegistry();
|
|
323
|
+
projectsToSearch = Object.keys(registry);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const allResults: Array<TimelineRecord & { _score: number }> = [];
|
|
328
|
+
|
|
329
|
+
// Search each project
|
|
330
|
+
for (const projectDir of projectsToSearch) {
|
|
331
|
+
try {
|
|
332
|
+
const table = await getEventsTable(projectDir);
|
|
333
|
+
let search = table.search(queryVector).limit(limit * 2); // Over-fetch to allow for filtering
|
|
334
|
+
|
|
335
|
+
const where = buildWhereFilter(opts);
|
|
336
|
+
if (where) search = search.where(where);
|
|
337
|
+
|
|
338
|
+
const results = await search.toArray();
|
|
339
|
+
for (const result of results) {
|
|
340
|
+
allResults.push({
|
|
341
|
+
...(result as unknown as TimelineRecord),
|
|
342
|
+
_score: 1 - (result._distance || 0),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Skip projects that don't exist or have issues
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Sort by score and take top results
|
|
352
|
+
allResults.sort((a, b) => b._score - a._score);
|
|
353
|
+
return allResults.slice(0, limit);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function searchExact(
|
|
357
|
+
query: string,
|
|
358
|
+
opts: SearchOptions = {},
|
|
359
|
+
): Promise<TimelineRecord[]> {
|
|
360
|
+
const limit = opts.limit || 50;
|
|
361
|
+
const likeClauses = [`content LIKE '%${query.replace(/'/g, "''")}%'`];
|
|
362
|
+
const where = buildWhereFilter(opts);
|
|
363
|
+
const fullWhere = where ? `${likeClauses[0]} AND ${where}` : likeClauses[0];
|
|
364
|
+
|
|
365
|
+
// Determine which projects to search
|
|
366
|
+
let projectsToSearch = opts.project_dirs || [];
|
|
367
|
+
if (projectsToSearch.length === 0 && opts.project) {
|
|
368
|
+
projectsToSearch = [opts.project];
|
|
369
|
+
}
|
|
370
|
+
if (projectsToSearch.length === 0) {
|
|
371
|
+
if (process.env.CLAUDE_PROJECT_DIR) {
|
|
372
|
+
projectsToSearch = [process.env.CLAUDE_PROJECT_DIR];
|
|
373
|
+
} else {
|
|
374
|
+
const registry = await loadProjectRegistry();
|
|
375
|
+
projectsToSearch = Object.keys(registry);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const allResults: TimelineRecord[] = [];
|
|
380
|
+
|
|
381
|
+
for (const projectDir of projectsToSearch) {
|
|
382
|
+
try {
|
|
383
|
+
const table = await getEventsTable(projectDir);
|
|
384
|
+
const results = await table.query().where(fullWhere).limit(limit).toArray();
|
|
385
|
+
allResults.push(...(results as unknown as TimelineRecord[]));
|
|
386
|
+
} catch {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return allResults.slice(0, limit);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function getTimeline(
|
|
395
|
+
opts: SearchOptions = {},
|
|
396
|
+
): Promise<TimelineRecord[]> {
|
|
397
|
+
const limit = opts.limit || 100;
|
|
398
|
+
const where = buildWhereFilter(opts);
|
|
399
|
+
|
|
400
|
+
// Determine which projects to search
|
|
401
|
+
let projectsToSearch = opts.project_dirs || [];
|
|
402
|
+
if (projectsToSearch.length === 0 && opts.project) {
|
|
403
|
+
projectsToSearch = [opts.project];
|
|
404
|
+
}
|
|
405
|
+
if (projectsToSearch.length === 0) {
|
|
406
|
+
if (process.env.CLAUDE_PROJECT_DIR) {
|
|
407
|
+
projectsToSearch = [process.env.CLAUDE_PROJECT_DIR];
|
|
408
|
+
} else {
|
|
409
|
+
const registry = await loadProjectRegistry();
|
|
410
|
+
projectsToSearch = Object.keys(registry);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const allResults: TimelineRecord[] = [];
|
|
415
|
+
|
|
416
|
+
for (const projectDir of projectsToSearch) {
|
|
417
|
+
try {
|
|
418
|
+
const table = await getEventsTable(projectDir);
|
|
419
|
+
let q = table.query().limit(limit);
|
|
420
|
+
if (where) q = q.where(where);
|
|
421
|
+
|
|
422
|
+
const results = await q.toArray();
|
|
423
|
+
allResults.push(...(results as unknown as TimelineRecord[]));
|
|
424
|
+
} catch {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Sort chronologically
|
|
430
|
+
allResults.sort((a, b) =>
|
|
431
|
+
a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
return allResults.slice(0, limit);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function listIndexedProjects(): Promise<ProjectInfo[]> {
|
|
438
|
+
const registry = await loadProjectRegistry();
|
|
439
|
+
const projects: ProjectInfo[] = [];
|
|
440
|
+
|
|
441
|
+
for (const [projectPath, entry] of Object.entries(registry)) {
|
|
442
|
+
const meta = await loadProjectMeta(projectPath);
|
|
443
|
+
projects.push({
|
|
444
|
+
project: projectPath,
|
|
445
|
+
project_name: basename(projectPath),
|
|
446
|
+
hash: entry.hash,
|
|
447
|
+
event_count: meta?.event_count || 0,
|
|
448
|
+
last_session_index: undefined,
|
|
449
|
+
last_git_index: undefined,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return projects;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Legacy compatibility functions
|
|
457
|
+
export async function getIndexedProjects(): Promise<ProjectInfo[]> {
|
|
458
|
+
return listIndexedProjects();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function getLastIndexedTimestamp(
|
|
462
|
+
project: string,
|
|
463
|
+
source: "session" | "git",
|
|
464
|
+
): Promise<string | null> {
|
|
465
|
+
const config = await loadConfig();
|
|
466
|
+
const info = config.indexed_projects[project];
|
|
467
|
+
if (!info) return null;
|
|
468
|
+
return source === "session" ? info.last_session_index : info.last_git_index;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export async function updateLastIndexedTimestamp(
|
|
472
|
+
project: string,
|
|
473
|
+
source: "session" | "git",
|
|
474
|
+
timestamp: string,
|
|
475
|
+
): Promise<void> {
|
|
476
|
+
const config = await loadConfig();
|
|
477
|
+
if (!config.indexed_projects[project]) {
|
|
478
|
+
config.indexed_projects[project] = {
|
|
479
|
+
last_session_index: "1970-01-01T00:00:00Z",
|
|
480
|
+
last_git_index: "1970-01-01T00:00:00Z",
|
|
481
|
+
event_count: 0,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (source === "session") {
|
|
485
|
+
config.indexed_projects[project].last_session_index = timestamp;
|
|
486
|
+
} else {
|
|
487
|
+
config.indexed_projects[project].last_git_index = timestamp;
|
|
488
|
+
}
|
|
489
|
+
await saveConfig(config);
|
|
490
|
+
}
|