om-memory-system 4.12.0-next.100 → 4.12.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.
@@ -1,3 +1,4 @@
1
+ import { type KeywordCount } from "./keyword-counts.js";
1
2
  import type { MemoryType } from "../types/index.js";
2
3
  import type { UserProfileData } from "./user-profile/types.js";
3
4
  interface ApiResponse<T = any> {
@@ -41,6 +42,10 @@ interface PaginatedResponse<T> {
41
42
  export declare function handleListTags(): Promise<ApiResponse<{
42
43
  project: TagInfo[];
43
44
  }>>;
45
+ /** Labels (comma-separated memory tags) with how many memories carry each one. */
46
+ export declare function handleListKeywords(tag?: string): Promise<ApiResponse<{
47
+ keywords: KeywordCount[];
48
+ }>>;
44
49
  export declare function handleListMemories(tag?: string, page?: number, pageSize?: number, includePrompts?: boolean, keyword?: string): Promise<ApiResponse<PaginatedResponse<Memory | any>>>;
45
50
  export declare function handleAddMemory(data: {
46
51
  content: string;
@@ -6,6 +6,7 @@ import { ensureTursoReady } from "./turso/ready.js";
6
6
  import { formatTagsForEmbedding } from "./turso/vector-utils.js";
7
7
  import { extractScopeFromContainerTag, tryExtractScopeFromContainerTag } from "./memory-scope.js";
8
8
  import { log } from "./logger.js";
9
+ import { countKeywords } from "./keyword-counts.js";
9
10
  import { CONFIG } from "../config.js";
10
11
  import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
11
12
  import { sortProfileItems, stripProfileVectors } from "../utils/profile.js";
@@ -99,38 +100,55 @@ export async function handleListTags() {
99
100
  return { success: false, error: String(error) };
100
101
  }
101
102
  }
103
+ /** Reads stored memory rows for one project tag, or for every project and user shard. */
104
+ async function loadMemoryRows(tag) {
105
+ const allMemories = [];
106
+ if (tag) {
107
+ const { scope: tagScope, hash } = extractScopeFromTag(tag);
108
+ const shards = await tursoShardManager.getAllShards(tagScope, hash);
109
+ for (const shard of shards) {
110
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
111
+ const memories = await tursoVectorSearch.listMemories(db, tag, 10000);
112
+ allMemories.push(...memories);
113
+ }
114
+ }
115
+ else {
116
+ // Iterate both project- and user-scoped shards. Previously this only
117
+ // walked project shards, which silently hid user-scope memories from the
118
+ // listing endpoint (Web UI navigation, /api/memories without a tag
119
+ // filter, …). User-scope memories still showed up in /api/search and
120
+ // /api/stats `byType`, but were invisible in /api/stats `byScope` and
121
+ // unbrowseable in the UI — a confusing UX gap. The filter keeps the
122
+ // defense-in-depth check on container_tag, just widens it to both
123
+ // canonical scope markers.
124
+ const projectShards = await tursoShardManager.getAllShards("project", "");
125
+ const userShards = await tursoShardManager.getAllShards("user", "");
126
+ for (const shard of [...projectShards, ...userShards]) {
127
+ const db = await tursoConnectionManager.getConnection(shard.dbPath);
128
+ const memories = await tursoVectorSearch.getAllMemories(db);
129
+ allMemories.push(...memories.filter((m) => m.container_tag?.includes("_project_") || m.container_tag?.includes("_user_")));
130
+ }
131
+ }
132
+ return allMemories;
133
+ }
134
+ /** Labels (comma-separated memory tags) with how many memories carry each one. */
135
+ export async function handleListKeywords(tag) {
136
+ try {
137
+ await ensureTursoReady();
138
+ const rows = await loadMemoryRows(tag);
139
+ return { success: true, data: { keywords: countKeywords(rows.map((r) => r.tags)) } };
140
+ }
141
+ catch (error) {
142
+ log("handleListKeywords: error", { error: String(error) });
143
+ return { success: false, error: String(error) };
144
+ }
145
+ }
102
146
  export async function handleListMemories(tag, page = 1, pageSize = 20, includePrompts = true, keyword) {
103
147
  try {
104
148
  await ensureTursoReady();
105
149
  // Listing only reads SQLite rows; no vector ops happen here.
106
150
  // See handleListTags comment - keep embedding init out of read paths.
107
- const allMemories = [];
108
- if (tag) {
109
- const { scope: tagScope, hash } = extractScopeFromTag(tag);
110
- const shards = await tursoShardManager.getAllShards(tagScope, hash);
111
- for (const shard of shards) {
112
- const db = await tursoConnectionManager.getConnection(shard.dbPath);
113
- const memories = await tursoVectorSearch.listMemories(db, tag, 10000);
114
- allMemories.push(...memories);
115
- }
116
- }
117
- else {
118
- // Iterate both project- and user-scoped shards. Previously this only
119
- // walked project shards, which silently hid user-scope memories from the
120
- // listing endpoint (Web UI navigation, /api/memories without a tag
121
- // filter, …). User-scope memories still showed up in /api/search and
122
- // /api/stats `byType`, but were invisible in /api/stats `byScope` and
123
- // unbrowseable in the UI — a confusing UX gap. The filter keeps the
124
- // defense-in-depth check on container_tag, just widens it to both
125
- // canonical scope markers.
126
- const projectShards = await tursoShardManager.getAllShards("project", "");
127
- const userShards = await tursoShardManager.getAllShards("user", "");
128
- for (const shard of [...projectShards, ...userShards]) {
129
- const db = await tursoConnectionManager.getConnection(shard.dbPath);
130
- const memories = await tursoVectorSearch.getAllMemories(db);
131
- allMemories.push(...memories.filter((m) => m.container_tag?.includes("_project_") || m.container_tag?.includes("_user_")));
132
- }
133
- }
151
+ const allMemories = await loadMemoryRows(tag);
134
152
  const memoriesWithType = allMemories.map((r) => {
135
153
  const metadata = safeJSONParse(r.metadata);
136
154
  return {
@@ -0,0 +1,10 @@
1
+ export interface KeywordCount {
2
+ keyword: string;
3
+ count: number;
4
+ }
5
+ /**
6
+ * Counts labels across stored `tags` fields (comma-separated). Labels are matched
7
+ * case-insensitively, the same rule the keyword filter uses, and each memory counts
8
+ * a label once. Sorted by count, then name.
9
+ */
10
+ export declare function countKeywords(tagFields: Array<string | null | undefined>): KeywordCount[];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Counts labels across stored `tags` fields (comma-separated). Labels are matched
3
+ * case-insensitively, the same rule the keyword filter uses, and each memory counts
4
+ * a label once. Sorted by count, then name.
5
+ */
6
+ export function countKeywords(tagFields) {
7
+ const counts = new Map();
8
+ for (const field of tagFields) {
9
+ if (!field)
10
+ continue;
11
+ const labels = new Set(field
12
+ .split(",")
13
+ .map((label) => label.trim().toLowerCase())
14
+ .filter(Boolean));
15
+ for (const label of labels)
16
+ counts.set(label, (counts.get(label) ?? 0) + 1);
17
+ }
18
+ return [...counts]
19
+ .map(([keyword, count]) => ({ keyword, count }))
20
+ .sort((a, b) => b.count - a.count || a.keyword.localeCompare(b.keyword));
21
+ }
@@ -12,7 +12,7 @@ import { isOlderVersion } from "./version-compare.js";
12
12
  import { AUTH_HEADER, getOrCreateAuthToken, isAuthorizedApiRequest } from "./auth-token.js";
13
13
  import { WebAuth } from "./web-auth.js";
14
14
  import { NODE_HTTP_IDLE_TIMEOUT_MS } from "./request-timeouts.js";
15
- import { handleListTags, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDetectTagMigration, handleRunTagMigrationBatch, handleGetTagMigrationProgress, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, handleAICleanup, handleApplyCleanup, handleUpdateProfileItem, } from "./api-handlers.js";
15
+ import { handleListTags, handleListKeywords, handleListMemories, handleAddMemory, handleDeleteMemory, handleBulkDelete, handleUpdateMemory, handleSearch, handleStats, handlePinMemory, handleUnpinMemory, handleRunCleanup, handleRunDeduplication, handleDetectMigration, handleRunMigration, handleDetectTagMigration, handleRunTagMigrationBatch, handleGetTagMigrationProgress, handleDeletePrompt, handleBulkDeletePrompts, handleGetUserProfile, handleGetProfileChangelog, handleGetProfileSnapshot, handleRefreshProfile, handleAICleanup, handleApplyCleanup, handleUpdateProfileItem, } from "./api-handlers.js";
16
16
  const isBun = typeof globalThis.Bun !== "undefined";
17
17
  const MIN_FAILED_TAKEOVERS = 3;
18
18
  export function attachNodeDisconnectHandlers(req, res, onDisconnect) {
@@ -1184,6 +1184,10 @@ export class WebServer {
1184
1184
  const result = await handleListTags();
1185
1185
  return this.jsonResponse(result);
1186
1186
  }
1187
+ if (path === "/api/keywords" && method === "GET") {
1188
+ const result = await handleListKeywords(url.searchParams.get("tag") || undefined);
1189
+ return this.jsonResponse(result);
1190
+ }
1187
1191
  if (path === "/api/memories" && method === "GET") {
1188
1192
  const tag = url.searchParams.get("tag") || undefined;
1189
1193
  const page = parseInt(url.searchParams.get("page") || "1");