@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 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
- output({
260
- success: true,
261
- results,
262
- count: results.length,
263
- mode: "semantic",
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 (format === "human") {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidwire/lore",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
4
4
  "description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
5
5
  "type": "module",
6
6
  "main": "./index.ts",