@voidwire/lore 0.1.14 → 0.2.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/cli.ts +29 -9
- package/index.ts +2 -0
- package/lib/list.ts +58 -0
- package/lib/semantic.ts +118 -0
- package/package.json +1 -1
package/cli.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
listSources,
|
|
27
27
|
list,
|
|
28
28
|
listDomains,
|
|
29
|
+
formatBriefList,
|
|
29
30
|
info,
|
|
30
31
|
formatInfoHuman,
|
|
31
32
|
projects,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
captureNote,
|
|
35
36
|
captureTeaching,
|
|
36
37
|
semanticSearch,
|
|
38
|
+
formatBriefSearch,
|
|
37
39
|
hasEmbeddings,
|
|
38
40
|
DOMAINS,
|
|
39
41
|
type SearchResult,
|
|
@@ -81,7 +83,7 @@ function parseArgs(args: string[]): Map<string, string> {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// Boolean flags that don't take values
|
|
84
|
-
const BOOLEAN_FLAGS = new Set(["help", "sources", "domains", "exact"]);
|
|
86
|
+
const BOOLEAN_FLAGS = new Set(["help", "sources", "domains", "exact", "brief"]);
|
|
85
87
|
|
|
86
88
|
function getPositionalArgs(args: string[]): string[] {
|
|
87
89
|
const result: string[] = [];
|
|
@@ -185,6 +187,7 @@ async function handleSearch(args: string[]): Promise<void> {
|
|
|
185
187
|
|
|
186
188
|
const limit = parsed.has("limit") ? parseInt(parsed.get("limit")!, 10) : 20;
|
|
187
189
|
const since = parsed.get("since");
|
|
190
|
+
const project = parsed.get("project");
|
|
188
191
|
|
|
189
192
|
// Handle prismis passthrough
|
|
190
193
|
if (source === "prismis") {
|
|
@@ -254,14 +257,21 @@ async function handleSearch(args: string[]): Promise<void> {
|
|
|
254
257
|
fail("No embeddings found. Run lore-embed-all first.", 2);
|
|
255
258
|
}
|
|
256
259
|
|
|
260
|
+
const brief = hasFlag(args, "brief");
|
|
261
|
+
|
|
257
262
|
try {
|
|
258
|
-
const results = await semanticSearch(query, { source, limit });
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
results
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
263
|
+
const results = await semanticSearch(query, { source, limit, project });
|
|
264
|
+
|
|
265
|
+
if (brief) {
|
|
266
|
+
console.log(formatBriefSearch(results));
|
|
267
|
+
} else {
|
|
268
|
+
output({
|
|
269
|
+
success: true,
|
|
270
|
+
results,
|
|
271
|
+
count: results.length,
|
|
272
|
+
mode: "semantic",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
265
275
|
console.error(
|
|
266
276
|
`✅ ${results.length} result${results.length !== 1 ? "s" : ""} found (semantic)`,
|
|
267
277
|
);
|
|
@@ -319,11 +329,14 @@ function handleList(args: string[]): void {
|
|
|
319
329
|
: undefined;
|
|
320
330
|
const format = parsed.get("format") || "json";
|
|
321
331
|
const project = parsed.get("project");
|
|
332
|
+
const brief = hasFlag(args, "brief");
|
|
322
333
|
|
|
323
334
|
try {
|
|
324
335
|
const result = list(domain, { limit, project });
|
|
325
336
|
|
|
326
|
-
if (
|
|
337
|
+
if (brief) {
|
|
338
|
+
console.log(formatBriefList(result));
|
|
339
|
+
} else if (format === "human") {
|
|
327
340
|
console.log(formatHumanOutput(result));
|
|
328
341
|
} else if (format === "jsonl") {
|
|
329
342
|
for (const entry of result.entries) {
|
|
@@ -581,6 +594,8 @@ Usage:
|
|
|
581
594
|
Search Options:
|
|
582
595
|
--exact Use FTS5 text search (bypasses semantic search)
|
|
583
596
|
--limit <n> Maximum results (default: 20)
|
|
597
|
+
--project <name> Filter results by project
|
|
598
|
+
--brief Compact output (titles only)
|
|
584
599
|
--since <date> Filter by date (today, yesterday, this-week, YYYY-MM-DD)
|
|
585
600
|
--sources List indexed sources with counts
|
|
586
601
|
|
|
@@ -591,6 +606,7 @@ Passthrough Sources:
|
|
|
591
606
|
List Options:
|
|
592
607
|
--limit <n> Maximum entries
|
|
593
608
|
--format <fmt> Output format: json (default), jsonl, human
|
|
609
|
+
--brief Compact output (titles only)
|
|
594
610
|
--domains List available domains
|
|
595
611
|
|
|
596
612
|
Capture Types:
|
|
@@ -638,6 +654,8 @@ Usage:
|
|
|
638
654
|
Options:
|
|
639
655
|
--exact Use FTS5 text search (bypasses semantic search)
|
|
640
656
|
--limit <n> Maximum results (default: 20)
|
|
657
|
+
--project <name> Filter results by project (post-filters KNN results)
|
|
658
|
+
--brief Compact output (titles only)
|
|
641
659
|
--since <date> Filter by date (today, yesterday, this-week, YYYY-MM-DD)
|
|
642
660
|
--sources List indexed sources with counts
|
|
643
661
|
--help Show this help
|
|
@@ -665,6 +683,7 @@ Examples:
|
|
|
665
683
|
lore search "authentication"
|
|
666
684
|
lore search blogs "typescript patterns"
|
|
667
685
|
lore search commits --since this-week "refactor"
|
|
686
|
+
lore search "authentication" --project=momentum --limit 5
|
|
668
687
|
lore search --exact "def process_data"
|
|
669
688
|
lore search prismis "kubernetes security"
|
|
670
689
|
lore search atuin "docker build"
|
|
@@ -684,6 +703,7 @@ Options:
|
|
|
684
703
|
--limit <n> Maximum entries (default: all)
|
|
685
704
|
--format <fmt> Output format: json (default), jsonl, human
|
|
686
705
|
--project <name> Filter by project name
|
|
706
|
+
--brief Compact output (titles only)
|
|
687
707
|
--domains List available domains
|
|
688
708
|
--help Show this help
|
|
689
709
|
|
package/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
export {
|
|
21
21
|
list,
|
|
22
22
|
listDomains,
|
|
23
|
+
formatBriefList,
|
|
23
24
|
DOMAINS,
|
|
24
25
|
type Domain,
|
|
25
26
|
type ListOptions,
|
|
@@ -70,6 +71,7 @@ export {
|
|
|
70
71
|
// Semantic search
|
|
71
72
|
export {
|
|
72
73
|
semanticSearch,
|
|
74
|
+
formatBriefSearch,
|
|
73
75
|
embedQuery,
|
|
74
76
|
hasEmbeddings,
|
|
75
77
|
type SemanticResult,
|
package/lib/list.ts
CHANGED
|
@@ -209,3 +209,61 @@ export function list(domain: Domain, options: ListOptions = {}): ListResult {
|
|
|
209
209
|
export function listDomains(): Domain[] {
|
|
210
210
|
return [...DOMAINS];
|
|
211
211
|
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extract project name from entry metadata
|
|
215
|
+
*/
|
|
216
|
+
function extractProjectFromEntry(entry: ListEntry, domain: string): string {
|
|
217
|
+
const field = PROJECT_FIELD[domain];
|
|
218
|
+
if (!field) return "unknown";
|
|
219
|
+
return (entry.metadata[field] as string) || "unknown";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Extract identifier from entry based on domain type
|
|
224
|
+
*/
|
|
225
|
+
function extractIdentifier(entry: ListEntry, domain: string): string {
|
|
226
|
+
const metadata = entry.metadata;
|
|
227
|
+
|
|
228
|
+
switch (domain) {
|
|
229
|
+
case "commits":
|
|
230
|
+
return (metadata.sha as string)?.substring(0, 7) || "";
|
|
231
|
+
case "sessions":
|
|
232
|
+
return (metadata.session_id as string)?.substring(0, 8) || "";
|
|
233
|
+
default:
|
|
234
|
+
return (metadata.id as string) || "";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get the best display text for an entry
|
|
240
|
+
* Commits use content (commit message), others use title
|
|
241
|
+
*/
|
|
242
|
+
function getDisplayText(entry: ListEntry, domain: string): string {
|
|
243
|
+
if (domain === "commits") {
|
|
244
|
+
return entry.content || entry.title;
|
|
245
|
+
}
|
|
246
|
+
return entry.title;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Format list result as brief, compact output
|
|
251
|
+
* One line per entry: " project: identifier - title"
|
|
252
|
+
*/
|
|
253
|
+
export function formatBriefList(result: ListResult): string {
|
|
254
|
+
const lines = [`${result.domain} (${result.count}):`];
|
|
255
|
+
|
|
256
|
+
result.entries.forEach((entry) => {
|
|
257
|
+
const project = extractProjectFromEntry(entry, result.domain);
|
|
258
|
+
const identifier = extractIdentifier(entry, result.domain);
|
|
259
|
+
const displayText = getDisplayText(entry, result.domain);
|
|
260
|
+
|
|
261
|
+
const line = identifier
|
|
262
|
+
? ` ${project}: ${identifier} - ${displayText}`
|
|
263
|
+
: ` ${project}: ${displayText}`;
|
|
264
|
+
|
|
265
|
+
lines.push(line);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return lines.join("\n");
|
|
269
|
+
}
|
package/lib/semantic.ts
CHANGED
|
@@ -32,8 +32,21 @@ export interface SemanticResult {
|
|
|
32
32
|
export interface SemanticSearchOptions {
|
|
33
33
|
source?: string;
|
|
34
34
|
limit?: number;
|
|
35
|
+
project?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Maps source types to their project field name in metadata JSON.
|
|
40
|
+
* Different sources store project names in different fields.
|
|
41
|
+
*/
|
|
42
|
+
const PROJECT_FIELD: Record<string, string> = {
|
|
43
|
+
commits: "project",
|
|
44
|
+
sessions: "project",
|
|
45
|
+
tasks: "project",
|
|
46
|
+
captures: "context",
|
|
47
|
+
teachings: "source",
|
|
48
|
+
};
|
|
49
|
+
|
|
37
50
|
const MODEL_NAME = "nomic-ai/nomic-embed-text-v1.5";
|
|
38
51
|
|
|
39
52
|
interface EmbeddingPipeline {
|
|
@@ -223,8 +236,113 @@ export async function semanticSearch(
|
|
|
223
236
|
const stmt = db.prepare(sql);
|
|
224
237
|
const results = stmt.all(...params) as SemanticResult[];
|
|
225
238
|
|
|
239
|
+
// Post-filter by project if specified
|
|
240
|
+
// KNN WHERE clause doesn't support json_extract on joined metadata,
|
|
241
|
+
// so we filter after the query returns
|
|
242
|
+
if (options.project) {
|
|
243
|
+
return results.filter((result) => {
|
|
244
|
+
const field = PROJECT_FIELD[result.source];
|
|
245
|
+
if (!field) return false;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const metadata = JSON.parse(result.metadata);
|
|
249
|
+
return metadata[field] === options.project;
|
|
250
|
+
} catch {
|
|
251
|
+
// Skip results with malformed metadata
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
226
257
|
return results;
|
|
227
258
|
} finally {
|
|
228
259
|
db.close();
|
|
229
260
|
}
|
|
230
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Extract project from result metadata
|
|
265
|
+
*/
|
|
266
|
+
function extractProjectFromMetadata(metadata: string, source: string): string {
|
|
267
|
+
const field = PROJECT_FIELD[source];
|
|
268
|
+
if (!field) return "unknown";
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(metadata);
|
|
272
|
+
return parsed[field] || "unknown";
|
|
273
|
+
} catch {
|
|
274
|
+
return "unknown";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract identifier from semantic result
|
|
280
|
+
*/
|
|
281
|
+
function extractIdentifierFromResult(result: SemanticResult): string {
|
|
282
|
+
try {
|
|
283
|
+
const metadata = JSON.parse(result.metadata);
|
|
284
|
+
|
|
285
|
+
switch (result.source) {
|
|
286
|
+
case "commits":
|
|
287
|
+
return metadata.sha?.substring(0, 7) || "";
|
|
288
|
+
case "sessions":
|
|
289
|
+
return metadata.session_id?.substring(0, 8) || "";
|
|
290
|
+
default:
|
|
291
|
+
return metadata.id || "";
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get the best display text for a result
|
|
300
|
+
* Commits use content (commit message), others use title
|
|
301
|
+
*/
|
|
302
|
+
function getDisplayText(result: SemanticResult): string {
|
|
303
|
+
if (result.source === "commits") {
|
|
304
|
+
return result.content || result.title;
|
|
305
|
+
}
|
|
306
|
+
return result.title;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Format semantic search results as brief, compact output
|
|
311
|
+
* Groups by source type, one line per result
|
|
312
|
+
*/
|
|
313
|
+
export function formatBriefSearch(results: SemanticResult[]): string {
|
|
314
|
+
if (results.length === 0) {
|
|
315
|
+
return "(no results)";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Group results by source
|
|
319
|
+
const grouped = new Map<string, SemanticResult[]>();
|
|
320
|
+
results.forEach((result) => {
|
|
321
|
+
const existing = grouped.get(result.source) || [];
|
|
322
|
+
existing.push(result);
|
|
323
|
+
grouped.set(result.source, existing);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const sections: string[] = [];
|
|
327
|
+
|
|
328
|
+
// Format each source group
|
|
329
|
+
grouped.forEach((sourceResults, source) => {
|
|
330
|
+
const lines = [`${source} (${sourceResults.length}):`];
|
|
331
|
+
|
|
332
|
+
sourceResults.forEach((r) => {
|
|
333
|
+
const project = extractProjectFromMetadata(r.metadata, r.source);
|
|
334
|
+
const identifier = extractIdentifierFromResult(r);
|
|
335
|
+
const displayText = getDisplayText(r);
|
|
336
|
+
|
|
337
|
+
const line = identifier
|
|
338
|
+
? ` ${project}: ${identifier} - ${displayText}`
|
|
339
|
+
: ` ${project}: ${displayText}`;
|
|
340
|
+
|
|
341
|
+
lines.push(line);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
sections.push(lines.join("\n"));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return sections.join("\n\n");
|
|
348
|
+
}
|