@voidwire/lore 0.1.15 → 0.3.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,14 +26,18 @@ import {
26
26
  listSources,
27
27
  list,
28
28
  listDomains,
29
+ formatBriefList,
29
30
  info,
30
31
  formatInfoHuman,
31
32
  projects,
33
+ about,
34
+ formatBriefAbout,
32
35
  captureTask,
33
36
  captureKnowledge,
34
37
  captureNote,
35
38
  captureTeaching,
36
39
  semanticSearch,
40
+ formatBriefSearch,
37
41
  hasEmbeddings,
38
42
  DOMAINS,
39
43
  type SearchResult,
@@ -81,7 +85,7 @@ function parseArgs(args: string[]): Map<string, string> {
81
85
  }
82
86
 
83
87
  // Boolean flags that don't take values
84
- const BOOLEAN_FLAGS = new Set(["help", "sources", "domains", "exact"]);
88
+ const BOOLEAN_FLAGS = new Set(["help", "sources", "domains", "exact", "brief"]);
85
89
 
86
90
  function getPositionalArgs(args: string[]): string[] {
87
91
  const result: string[] = [];
@@ -255,14 +259,21 @@ async function handleSearch(args: string[]): Promise<void> {
255
259
  fail("No embeddings found. Run lore-embed-all first.", 2);
256
260
  }
257
261
 
262
+ const brief = hasFlag(args, "brief");
263
+
258
264
  try {
259
265
  const results = await semanticSearch(query, { source, limit, project });
260
- output({
261
- success: true,
262
- results,
263
- count: results.length,
264
- mode: "semantic",
265
- });
266
+
267
+ if (brief) {
268
+ console.log(formatBriefSearch(results));
269
+ } else {
270
+ output({
271
+ success: true,
272
+ results,
273
+ count: results.length,
274
+ mode: "semantic",
275
+ });
276
+ }
266
277
  console.error(
267
278
  `✅ ${results.length} result${results.length !== 1 ? "s" : ""} found (semantic)`,
268
279
  );
@@ -320,11 +331,14 @@ function handleList(args: string[]): void {
320
331
  : undefined;
321
332
  const format = parsed.get("format") || "json";
322
333
  const project = parsed.get("project");
334
+ const brief = hasFlag(args, "brief");
323
335
 
324
336
  try {
325
337
  const result = list(domain, { limit, project });
326
338
 
327
- if (format === "human") {
339
+ if (brief) {
340
+ console.log(formatBriefList(result));
341
+ } else if (format === "human") {
328
342
  console.log(formatHumanOutput(result));
329
343
  } else if (format === "jsonl") {
330
344
  for (const entry of result.entries) {
@@ -405,6 +419,55 @@ function handleProjects(args: string[]): void {
405
419
  }
406
420
  }
407
421
 
422
+ // ============================================================================
423
+ // About Command
424
+ // ============================================================================
425
+
426
+ function handleAbout(args: string[]): void {
427
+ if (hasFlag(args, "help")) {
428
+ showAboutHelp();
429
+ }
430
+
431
+ const parsed = parseArgs(args);
432
+ const positional = getPositionalArgs(args);
433
+
434
+ if (positional.length === 0) {
435
+ fail("Missing project name. Use: lore about <project>");
436
+ }
437
+
438
+ const project = positional[0];
439
+ const brief = hasFlag(args, "brief");
440
+ const limit = parsed.has("limit")
441
+ ? parseInt(parsed.get("limit")!, 10)
442
+ : undefined;
443
+
444
+ try {
445
+ const result = about(project, { brief, limit });
446
+
447
+ if (brief) {
448
+ console.log(formatBriefAbout(result));
449
+ } else {
450
+ output({
451
+ success: true,
452
+ ...result,
453
+ });
454
+ }
455
+
456
+ const totalCount =
457
+ result.commits.count +
458
+ result.captures.count +
459
+ result.tasks.count +
460
+ result.teachings.count +
461
+ result.sessions.count;
462
+
463
+ console.error(`✅ ${totalCount} entries for project: ${project}`);
464
+ process.exit(0);
465
+ } catch (error) {
466
+ const message = error instanceof Error ? error.message : "Unknown error";
467
+ fail(message, 2);
468
+ }
469
+ }
470
+
408
471
  // ============================================================================
409
472
  // Capture Command
410
473
  // ============================================================================
@@ -577,12 +640,15 @@ Usage:
577
640
  lore list --domains List available domains
578
641
  lore info Show indexed sources and counts
579
642
  lore info --human Human-readable info
643
+ lore about <project> Aggregate view of project knowledge
644
+ lore about <project> --brief Compact project summary
580
645
  lore capture task|knowledge|note|teaching Capture knowledge
581
646
 
582
647
  Search Options:
583
648
  --exact Use FTS5 text search (bypasses semantic search)
584
649
  --limit <n> Maximum results (default: 20)
585
650
  --project <name> Filter results by project
651
+ --brief Compact output (titles only)
586
652
  --since <date> Filter by date (today, yesterday, this-week, YYYY-MM-DD)
587
653
  --sources List indexed sources with counts
588
654
 
@@ -593,6 +659,7 @@ Passthrough Sources:
593
659
  List Options:
594
660
  --limit <n> Maximum entries
595
661
  --format <fmt> Output format: json (default), jsonl, human
662
+ --brief Compact output (titles only)
596
663
  --domains List available domains
597
664
 
598
665
  Capture Types:
@@ -641,6 +708,7 @@ Options:
641
708
  --exact Use FTS5 text search (bypasses semantic search)
642
709
  --limit <n> Maximum results (default: 20)
643
710
  --project <name> Filter results by project (post-filters KNN results)
711
+ --brief Compact output (titles only)
644
712
  --since <date> Filter by date (today, yesterday, this-week, YYYY-MM-DD)
645
713
  --sources List indexed sources with counts
646
714
  --help Show this help
@@ -688,6 +756,7 @@ Options:
688
756
  --limit <n> Maximum entries (default: all)
689
757
  --format <fmt> Output format: json (default), jsonl, human
690
758
  --project <name> Filter by project name
759
+ --brief Compact output (titles only)
691
760
  --domains List available domains
692
761
  --help Show this help
693
762
 
@@ -774,6 +843,51 @@ Examples:
774
843
  process.exit(0);
775
844
  }
776
845
 
846
+ function showAboutHelp(): void {
847
+ console.log(`
848
+ lore about - Show everything about a project
849
+
850
+ Usage:
851
+ lore about <project> Aggregate view of project knowledge
852
+ lore about <project> --brief Compact output
853
+
854
+ Options:
855
+ --brief Compact output (titles only)
856
+ --limit <n> Results per source (default: 10)
857
+ --help Show this help
858
+
859
+ Sources queried:
860
+ commits Git commits for project
861
+ captures Quick captures in project context
862
+ tasks Development tasks for project
863
+ teachings Teachings from project
864
+ sessions Claude Code sessions for project
865
+
866
+ Output (JSON):
867
+ {
868
+ "project": "name",
869
+ "commits": [...],
870
+ "captures": [...],
871
+ "tasks": [...],
872
+ "teachings": [...],
873
+ "sessions": [...]
874
+ }
875
+
876
+ Output (--brief):
877
+ commits (3):
878
+ project: hash - commit message
879
+
880
+ captures (2):
881
+ project: insight text
882
+
883
+ Examples:
884
+ lore about momentum --brief
885
+ lore about lore | jq '.commits | length'
886
+ lore about momentum --limit 5
887
+ `);
888
+ process.exit(0);
889
+ }
890
+
777
891
  function showCaptureHelp(): void {
778
892
  console.log(`
779
893
  lore capture - Capture knowledge
@@ -855,12 +969,15 @@ function main(): void {
855
969
  case "projects":
856
970
  handleProjects(commandArgs);
857
971
  break;
972
+ case "about":
973
+ handleAbout(commandArgs);
974
+ break;
858
975
  case "capture":
859
976
  handleCapture(commandArgs);
860
977
  break;
861
978
  default:
862
979
  fail(
863
- `Unknown command: ${command}. Use: search, list, info, projects, or capture`,
980
+ `Unknown command: ${command}. Use: search, list, info, projects, about, or capture`,
864
981
  );
865
982
  }
866
983
  }
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,
@@ -38,6 +39,14 @@ export {
38
39
  // Projects
39
40
  export { projects } from "./lib/projects";
40
41
 
42
+ // About
43
+ export {
44
+ about,
45
+ formatBriefAbout,
46
+ type AboutResult,
47
+ type AboutOptions,
48
+ } from "./lib/about";
49
+
41
50
  // Prismis integration
42
51
  export {
43
52
  searchPrismis,
@@ -70,6 +79,7 @@ export {
70
79
  // Semantic search
71
80
  export {
72
81
  semanticSearch,
82
+ formatBriefSearch,
73
83
  embedQuery,
74
84
  hasEmbeddings,
75
85
  type SemanticResult,
package/lib/about.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * lib/about.ts - Project knowledge aggregation
3
+ *
4
+ * Aggregates all knowledge sources for a given project.
5
+ * Uses parallel queries via Promise.all for performance.
6
+ */
7
+
8
+ import { list, formatBriefList, type ListResult, type Domain } from "./list";
9
+
10
+ export interface AboutOptions {
11
+ brief?: boolean;
12
+ limit?: number;
13
+ }
14
+
15
+ export interface AboutResult {
16
+ project: string;
17
+ commits: ListResult;
18
+ captures: ListResult;
19
+ tasks: ListResult;
20
+ teachings: ListResult;
21
+ sessions: ListResult;
22
+ }
23
+
24
+ /**
25
+ * Sources to query for project knowledge
26
+ * Each source has a different field for project mapping (handled by list.ts)
27
+ * Note: "insights" will be added when task 2.1 is complete
28
+ */
29
+ const ABOUT_SOURCES: Domain[] = [
30
+ "commits",
31
+ "captures",
32
+ "tasks",
33
+ "teachings",
34
+ "sessions",
35
+ ];
36
+
37
+ /**
38
+ * Get aggregated knowledge about a project across all sources
39
+ *
40
+ * @param project - Project name to query
41
+ * @param options - Optional brief flag and limit
42
+ * @returns AboutResult with data from all sources, or formatted string if brief
43
+ */
44
+ export function about(
45
+ project: string,
46
+ options: AboutOptions = {},
47
+ ): AboutResult {
48
+ const limit = options.limit ?? 10;
49
+
50
+ // Query all sources in parallel
51
+ const results = ABOUT_SOURCES.map((source) => {
52
+ try {
53
+ return list(source, { project, limit });
54
+ } catch {
55
+ // Source doesn't exist or has no data - return empty result
56
+ return {
57
+ domain: source,
58
+ entries: [],
59
+ count: 0,
60
+ } as ListResult;
61
+ }
62
+ });
63
+
64
+ return {
65
+ project,
66
+ commits: results[0],
67
+ captures: results[1],
68
+ tasks: results[2],
69
+ teachings: results[3],
70
+ sessions: results[4],
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Format about result as brief, compact output
76
+ * Groups by source, skips empty sources
77
+ */
78
+ export function formatBriefAbout(result: AboutResult): string {
79
+ const sections: string[] = [];
80
+
81
+ // Format each non-empty source
82
+ if (result.commits.count > 0) {
83
+ sections.push(formatBriefList(result.commits));
84
+ }
85
+ if (result.captures.count > 0) {
86
+ sections.push(formatBriefList(result.captures));
87
+ }
88
+ if (result.tasks.count > 0) {
89
+ sections.push(formatBriefList(result.tasks));
90
+ }
91
+ if (result.teachings.count > 0) {
92
+ sections.push(formatBriefList(result.teachings));
93
+ }
94
+ if (result.sessions.count > 0) {
95
+ sections.push(formatBriefList(result.sessions));
96
+ }
97
+
98
+ if (sections.length === 0) {
99
+ return `(no results for project: ${result.project})`;
100
+ }
101
+
102
+ return sections.join("\n\n");
103
+ }
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
@@ -259,3 +259,90 @@ export async function semanticSearch(
259
259
  db.close();
260
260
  }
261
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.15",
3
+ "version": "0.3.0",
4
4
  "description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
5
5
  "type": "module",
6
6
  "main": "./index.ts",