ex-brain 0.4.0 → 0.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ex-brain",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI personal knowledge base powered by seekdb",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
@@ -11,6 +11,20 @@ interface GraphNode {
11
11
  group: string;
12
12
  }
13
13
 
14
+ /**
15
+ * Normalize a type value. Slug-like values (no `/` in the original slug,
16
+ * contain `_`, or start with digits) are mapped to "article" so the filter
17
+ * panel doesn't list every individual document as its own type.
18
+ */
19
+ function normalizeType(rawType: string, slug: string): string {
20
+ // If the raw type equals the slug's basename, it was inferred from a flat slug
21
+ const baseName = slug.includes("/") ? slug.split("/").pop()! : slug;
22
+ if (rawType === baseName || /^\d/.test(rawType) || rawType.startsWith("rm_")) {
23
+ return "article";
24
+ }
25
+ return rawType;
26
+ }
27
+
14
28
  interface GraphEdge {
15
29
  from: string;
16
30
  to: string;
@@ -43,7 +57,8 @@ async function getGraphData(repo: BrainRepository): Promise<GraphData> {
43
57
 
44
58
  // Create nodes from pages
45
59
  for (const page of pages) {
46
- const type = page.type || "other";
60
+ const rawType = page.type || "other";
61
+ const type = normalizeType(rawType, page.slug);
47
62
  typeCounts[type] = (typeCounts[type] || 0) + 1;
48
63
 
49
64
  nodes.push({
@@ -693,6 +708,10 @@ function getGraphHtml(): string {
693
708
  const response = await fetch('/api/graph');
694
709
  graphData = await response.json();
695
710
 
711
+ // Precompute node type map for O(1) edge visibility check
712
+ nodeTypeMap = new Map();
713
+ graphData.nodes.forEach(n => nodeTypeMap.set(n.id, n.type));
714
+
696
715
  updateStats();
697
716
  renderFilters();
698
717
  renderNodeList();
@@ -837,22 +856,29 @@ function getGraphHtml(): string {
837
856
  });
838
857
  }
839
858
 
859
+ // Node type lookup for O(1) edge visibility check
860
+ let nodeTypeMap = new Map();
861
+
840
862
  function updateNetworkVisibility() {
841
863
  if (!nodes) return;
842
864
 
843
- graphData.nodes.forEach(node => {
844
- const visible = activeTypes.has(node.type);
845
- nodes.update({ id: node.id, hidden: !visible });
846
- });
865
+ // Batch update nodes
866
+ const nodeUpdates = graphData.nodes.map(node => ({
867
+ id: node.id,
868
+ hidden: !activeTypes.has(node.type),
869
+ }));
870
+ nodes.update(nodeUpdates);
847
871
 
848
- // Also hide edges connected to hidden nodes
849
- graphData.edges.forEach(edge => {
850
- const fromNode = graphData.nodes.find(n => n.id === edge.from);
851
- const toNode = graphData.nodes.find(n => n.id === edge.to);
852
- const visible = fromNode && toNode &&
853
- activeTypes.has(fromNode.type) && activeTypes.has(toNode.type);
854
- edges.update({ id: edge.from + '->' + edge.to, hidden: !visible });
872
+ // Batch update edges with O(1) lookup
873
+ const edgeUpdates = graphData.edges.map(edge => {
874
+ const fromType = nodeTypeMap.get(edge.from);
875
+ const toType = nodeTypeMap.get(edge.to);
876
+ return {
877
+ id: edge.from + '->' + edge.to,
878
+ hidden: !activeTypes.has(fromType) || !activeTypes.has(toType),
879
+ };
855
880
  });
881
+ edges.update(edgeUpdates);
856
882
  }
857
883
 
858
884
  async function selectNode(slug) {
@@ -498,10 +498,12 @@ Examples:
498
498
  )
499
499
  .action(async (opts: Record<string, string | undefined>) => {
500
500
  await withRepo(program, async (repo) => {
501
+ const rawLimit = Number(opts.limit ?? 50);
502
+ const limit = (Number.isFinite(rawLimit) && rawLimit > 0) ? rawLimit : 50;
501
503
  const rows = await repo.listPages({
502
504
  type: opts.type,
503
505
  tag: opts.tag,
504
- limit: Number(opts.limit),
506
+ limit,
505
507
  });
506
508
 
507
509
  // When --fields is set, show one page per line with tab-separated values
@@ -516,9 +518,16 @@ Examples:
516
518
  });
517
519
  console.log(vals.join("\t"));
518
520
  }
521
+ // Show count for tabular output too
522
+ if (!isJson(program) && rows.length >= limit) {
523
+ process.stderr.write(`\nShowing ${rows.length} page(s) (use --limit to show more)\n`);
524
+ }
519
525
  return;
520
526
  }
521
527
 
528
+ if (!isJson(program) && rows.length >= limit) {
529
+ process.stderr.write(`Showing ${rows.length} page(s) (use --limit to show more)\n`);
530
+ }
522
531
  print(program, rows);
523
532
  });
524
533
  });
@@ -125,7 +125,11 @@ export class BrainRepository {
125
125
  limit?: number;
126
126
  }): Promise<PageRecord[]> {
127
127
  try {
128
- const limit = filters.limit ?? 50;
128
+ // Safe default: use 50 if limit is missing, NaN, non-finite, or <= 0
129
+ const rawLimit = filters.limit;
130
+ const limit = (typeof rawLimit === 'number' && Number.isFinite(rawLimit) && rawLimit > 0)
131
+ ? rawLimit
132
+ : 50;
129
133
  const params: unknown[] = [];
130
134
  let sql = `SELECT p.slug, p.type, p.title, p.compiled_truth, p.timeline, p.frontmatter, p.created_at, p.updated_at
131
135
  FROM pages p`;
package/src/slug-utils.ts CHANGED
@@ -17,8 +17,19 @@ export function slugToTitle(slug: string): string {
17
17
  .join(" ");
18
18
  }
19
19
 
20
+ /**
21
+ * Infer page type from slug path.
22
+ * - Slugs with a path prefix (e.g. "notes/my-post") → use the prefix as type
23
+ * - Flat slugs without "/" (e.g. "26_05_20_xxx" or "rm_hui_yi_ji_yao_0325") → default to "article"
24
+ * - Fallback to "other" if empty
25
+ */
20
26
  export function inferTypeFromSlug(slug: string): string {
21
- return slug.split("/")[0] ?? "other";
27
+ const segments = slug.split("/");
28
+ if (segments.length > 1 && segments[0]) {
29
+ return segments[0];
30
+ }
31
+ // Flat slug — treat as a generic article/note
32
+ return "article";
22
33
  }
23
34
 
24
35
  /**