@akashabot/openclaw-mem 0.1.1 → 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.
Files changed (4) hide show
  1. package/dist/cli.js +93 -11
  2. package/package.json +25 -17
  3. package/src/cli.ts +257 -150
  4. package/tsconfig.json +13 -13
package/dist/cli.js CHANGED
@@ -2,7 +2,8 @@
2
2
  import { Command } from 'commander';
3
3
  import fs from 'node:fs';
4
4
  import { v4 as uuidv4 } from 'uuid';
5
- import { addItem, hybridSearch, initSchema, openDb, searchItems } from '@akashabot/openclaw-memory-offline-core';
5
+ import * as core from '@akashabot/openclaw-memory-offline-core';
6
+ const { addItem, hybridSearch, hybridSearchFiltered, initSchema, openDb, runMigrations, searchItems, getMemoriesByEntity, getMemoriesBySession, getMemoriesByProcess, listEntities, listSessions, } = core;
6
7
  const program = new Command();
7
8
  program
8
9
  .name('openclaw-mem')
@@ -19,6 +20,7 @@ program
19
20
  withDb((dbPath) => {
20
21
  const db = openDb(dbPath);
21
22
  initSchema(db);
23
+ runMigrations(db);
22
24
  console.log(JSON.stringify({ ok: true, db: dbPath }));
23
25
  });
24
26
  });
@@ -33,10 +35,15 @@ function registerAddCommand(name) {
33
35
  .option('--source <source>', 'Source (e.g. "whatsapp")')
34
36
  .option('--source-id <sourceId>', 'Source id / external id')
35
37
  .option('--meta <json>', 'JSON metadata (string)')
38
+ // Phase 1: Attribution & Session
39
+ .option('--entity-id <entityId>', 'Who said/wrote this (e.g. "loic", "system")')
40
+ .option('--process-id <processId>', 'Which agent/process captured this (e.g. "akasha")')
41
+ .option('--session-id <sessionId>', 'Session/conversation grouping')
36
42
  .action((textArg, cmdOpts) => {
37
43
  withDb((dbPath) => {
38
44
  const db = openDb(dbPath);
39
45
  initSchema(db);
46
+ runMigrations(db);
40
47
  const text = (textArg ?? fs.readFileSync(0, 'utf8')).trim();
41
48
  if (!text) {
42
49
  process.exitCode = 2;
@@ -67,6 +74,10 @@ function registerAddCommand(name) {
67
74
  source: cmdOpts.source ? String(cmdOpts.source) : null,
68
75
  source_id: cmdOpts.sourceId ? String(cmdOpts.sourceId) : null,
69
76
  meta,
77
+ // Phase 1: Attribution & Session
78
+ entity_id: cmdOpts.entityId ? String(cmdOpts.entityId) : null,
79
+ process_id: cmdOpts.processId ? String(cmdOpts.processId) : null,
80
+ session_id: cmdOpts.sessionId ? String(cmdOpts.sessionId) : null,
70
81
  });
71
82
  console.log(JSON.stringify({ ok: true, item }));
72
83
  });
@@ -87,11 +98,23 @@ function registerSearchCommand(name) {
87
98
  .option('--ollama-base-url <url>', 'Ollama baseUrl (OpenAI-compatible). e.g. http://127.0.0.1:11434')
88
99
  .option('--embedding-model <id>', 'Embedding model id (default bge-m3)')
89
100
  .option('--ollama-timeout-ms <n>', 'Ollama timeout in ms (default 3000)')
101
+ // Phase 1: Filtering
102
+ .option('--entity-id <entityId>', 'Filter by entity (who said/wrote)')
103
+ .option('--process-id <processId>', 'Filter by process (which agent captured)')
104
+ .option('--session-id <sessionId>', 'Filter by session/conversation')
90
105
  .action(async (query, cmdOpts) => {
91
106
  await withDb(async (dbPath) => {
92
107
  const db = openDb(dbPath);
93
108
  initSchema(db);
109
+ runMigrations(db);
94
110
  const limit = Math.max(1, Math.min(200, Number(cmdOpts.limit ?? 10)));
111
+ // Check if any filter is specified
112
+ const hasFilter = cmdOpts.entityId || cmdOpts.processId || cmdOpts.sessionId;
113
+ const filter = hasFilter ? {
114
+ entity_id: cmdOpts.entityId || undefined,
115
+ process_id: cmdOpts.processId || undefined,
116
+ session_id: cmdOpts.sessionId || undefined,
117
+ } : undefined;
95
118
  if (!cmdOpts.hybrid) {
96
119
  const out = searchItems(db, query, limit);
97
120
  console.log(JSON.stringify({ ok: true, mode: 'lexical', ...out }));
@@ -99,19 +122,25 @@ function registerSearchCommand(name) {
99
122
  }
100
123
  const candidates = cmdOpts.candidates ? Math.max(limit, Number(cmdOpts.candidates)) : undefined;
101
124
  const semanticWeight = Math.max(0, Math.min(1, Number(cmdOpts.semanticWeight ?? 0.7)));
102
- const results = await hybridSearch(db, {
103
- dbPath,
104
- ollamaBaseUrl: cmdOpts.ollamaBaseUrl,
105
- embeddingModel: cmdOpts.embeddingModel,
106
- ollamaTimeoutMs: cmdOpts.ollamaTimeoutMs ? Number(cmdOpts.ollamaTimeoutMs) : undefined,
107
- },
108
- // IMPORTANT: hybridSearch expects an already-escaped FTS query.
109
- // We can reuse searchItems() to get the escapedQuery.
110
- searchItems(db, query, 1).escapedQuery, { topK: limit, candidates, semanticWeight });
125
+ // Use filtered search if filter options are specified
126
+ const results = await (hasFilter
127
+ ? hybridSearchFiltered(db, {
128
+ dbPath,
129
+ ollamaBaseUrl: cmdOpts.ollamaBaseUrl,
130
+ embeddingModel: cmdOpts.embeddingModel,
131
+ ollamaTimeoutMs: cmdOpts.ollamaTimeoutMs ? Number(cmdOpts.ollamaTimeoutMs) : undefined,
132
+ }, searchItems(db, query, 1).escapedQuery, { topK: limit, candidates, semanticWeight, filter })
133
+ : hybridSearch(db, {
134
+ dbPath,
135
+ ollamaBaseUrl: cmdOpts.ollamaBaseUrl,
136
+ embeddingModel: cmdOpts.embeddingModel,
137
+ ollamaTimeoutMs: cmdOpts.ollamaTimeoutMs ? Number(cmdOpts.ollamaTimeoutMs) : undefined,
138
+ }, searchItems(db, query, 1).escapedQuery, { topK: limit, candidates, semanticWeight }));
111
139
  console.log(JSON.stringify({
112
140
  ok: true,
113
- mode: 'hybrid',
141
+ mode: hasFilter ? 'hybrid-filtered' : 'hybrid',
114
142
  query,
143
+ filter: hasFilter ? filter : undefined,
115
144
  results,
116
145
  embeddingModel: cmdOpts.embeddingModel ?? 'bge-m3',
117
146
  ollamaBaseUrl: cmdOpts.ollamaBaseUrl ?? 'http://127.0.0.1:11434',
@@ -122,4 +151,57 @@ function registerSearchCommand(name) {
122
151
  registerSearchCommand('search');
123
152
  // Back-compat with README/skill naming.
124
153
  registerSearchCommand('recall');
154
+ // Phase 1: Utility commands for attribution & session
155
+ program
156
+ .command('list-entities')
157
+ .description('List all distinct entity_ids in the database')
158
+ .action(() => {
159
+ withDb((dbPath) => {
160
+ const db = openDb(dbPath);
161
+ initSchema(db);
162
+ runMigrations(db);
163
+ const entities = listEntities(db);
164
+ console.log(JSON.stringify({ ok: true, entities, count: entities.length }));
165
+ });
166
+ });
167
+ program
168
+ .command('list-sessions')
169
+ .description('List all distinct session_ids in the database')
170
+ .action(() => {
171
+ withDb((dbPath) => {
172
+ const db = openDb(dbPath);
173
+ initSchema(db);
174
+ runMigrations(db);
175
+ const sessions = listSessions(db);
176
+ console.log(JSON.stringify({ ok: true, sessions, count: sessions.length }));
177
+ });
178
+ });
179
+ program
180
+ .command('get-by-entity <entityId>')
181
+ .description('Get all memories from a specific entity')
182
+ .option('--limit <n>', 'Max results (default 50)', '50')
183
+ .action((entityId, cmdOpts) => {
184
+ withDb((dbPath) => {
185
+ const db = openDb(dbPath);
186
+ initSchema(db);
187
+ runMigrations(db);
188
+ const limit = Math.max(1, Math.min(500, Number(cmdOpts.limit ?? 50)));
189
+ const items = getMemoriesByEntity(db, entityId, limit);
190
+ console.log(JSON.stringify({ ok: true, entityId, count: items.length, items }));
191
+ });
192
+ });
193
+ program
194
+ .command('get-by-session <sessionId>')
195
+ .description('Get all memories from a specific session/conversation')
196
+ .option('--limit <n>', 'Max results (default 100)', '100')
197
+ .action((sessionId, cmdOpts) => {
198
+ withDb((dbPath) => {
199
+ const db = openDb(dbPath);
200
+ initSchema(db);
201
+ runMigrations(db);
202
+ const limit = Math.max(1, Math.min(500, Number(cmdOpts.limit ?? 100)));
203
+ const items = getMemoriesBySession(db, sessionId, limit);
204
+ console.log(JSON.stringify({ ok: true, sessionId, count: items.length, items }));
205
+ });
206
+ });
125
207
  program.parse();
package/package.json CHANGED
@@ -1,17 +1,25 @@
1
- {
2
- "name": "@akashabot/openclaw-mem",
3
- "version": "0.1.1",
4
- "type": "module",
5
- "bin": {
6
- "openclaw-mem": "./dist/cli.js"
7
- },
8
- "main": "./dist/cli.js",
9
- "dependencies": {
10
- "commander": "^12.1.0",
11
- "uuid": "^10.0.0",
12
- "@akashabot/openclaw-memory-offline-core": "0.1.1"
13
- },
14
- "devDependencies": {
15
- "@types/uuid": "^10.0.0"
16
- }
17
- }
1
+ {
2
+ "name": "@akashabot/openclaw-mem",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "CLI for OpenClaw Offline Memory (SQLite FTS + optional embeddings)",
6
+ "bin": {
7
+ "openclaw-mem": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/cli.js",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/AkashaBot/openclaw-memory-offline-sqlite.git",
13
+ "directory": "packages/cli"
14
+ },
15
+ "author": "AkashaBot",
16
+ "license": "MIT",
17
+ "dependencies": {
18
+ "commander": "^12.1.0",
19
+ "uuid": "^10.0.0",
20
+ "@akashabot/openclaw-memory-offline-core": "^0.2.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/uuid": "^10.0.0"
24
+ }
25
+ }
package/src/cli.ts CHANGED
@@ -1,150 +1,257 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import fs from 'node:fs';
4
- import { v4 as uuidv4 } from 'uuid';
5
- import { addItem, hybridSearch, initSchema, openDb, searchItems } from '@akashabot/openclaw-memory-offline-core';
6
-
7
- const program = new Command();
8
-
9
- program
10
- .name('openclaw-mem')
11
- .description('Offline memory (SQLite FTS + optional Ollama embeddings)')
12
- .option('--db <path>', 'SQLite db path', 'memory.sqlite');
13
-
14
- function withDb<T>(fn: (dbPath: string) => T): T {
15
- const opts = program.opts<{ db: string }>();
16
- return fn(opts.db);
17
- }
18
-
19
- program
20
- .command('init')
21
- .description('Initialize the SQLite database schema')
22
- .action(() => {
23
- withDb((dbPath) => {
24
- const db = openDb(dbPath);
25
- initSchema(db);
26
- console.log(JSON.stringify({ ok: true, db: dbPath }));
27
- });
28
- });
29
-
30
- function registerAddCommand(name: string) {
31
- program
32
- .command(name)
33
- .description('Add an item (and update FTS index)')
34
- .argument('[text]', 'Text content. If omitted, reads from stdin.')
35
- .option('--id <id>', 'Item id (default: uuid)')
36
- .option('--title <title>', 'Title')
37
- .option('--tags <tags>', 'Tags (freeform, e.g. "a,b,c")')
38
- .option('--source <source>', 'Source (e.g. "whatsapp")')
39
- .option('--source-id <sourceId>', 'Source id / external id')
40
- .option('--meta <json>', 'JSON metadata (string)')
41
- .action((textArg: string | undefined, cmdOpts) => {
42
- withDb((dbPath) => {
43
- const db = openDb(dbPath);
44
- initSchema(db);
45
-
46
- const text = (textArg ?? fs.readFileSync(0, 'utf8')).trim();
47
- if (!text) {
48
- process.exitCode = 2;
49
- console.log(JSON.stringify({ ok: false, error: 'Missing text (arg or stdin)' }));
50
- return;
51
- }
52
-
53
- let meta: unknown = undefined;
54
- if (cmdOpts.meta) {
55
- try {
56
- meta = JSON.parse(String(cmdOpts.meta));
57
- } catch (e: any) {
58
- process.exitCode = 2;
59
- console.log(
60
- JSON.stringify({
61
- ok: false,
62
- error: 'Invalid JSON for --meta',
63
- details: String(e?.message ?? e),
64
- })
65
- );
66
- return;
67
- }
68
- }
69
-
70
- const id = String(cmdOpts.id ?? uuidv4());
71
- const item = addItem(db, {
72
- id,
73
- title: cmdOpts.title ? String(cmdOpts.title) : null,
74
- text,
75
- tags: cmdOpts.tags ? String(cmdOpts.tags) : null,
76
- source: cmdOpts.source ? String(cmdOpts.source) : null,
77
- source_id: cmdOpts.sourceId ? String(cmdOpts.sourceId) : null,
78
- meta,
79
- });
80
-
81
- console.log(JSON.stringify({ ok: true, item }));
82
- });
83
- });
84
- }
85
-
86
- registerAddCommand('add');
87
- // Back-compat with README/skill naming.
88
- registerAddCommand('remember');
89
-
90
- function registerSearchCommand(name: string) {
91
- program
92
- .command(name)
93
- .description('Search items using SQLite FTS5 (bm25), optionally reranked with Ollama embeddings')
94
- .argument('<query>', 'FTS query (will be minimally escaped)')
95
- .option('--limit <n>', 'Max results (default 10, max 200)', '10')
96
- .option('--hybrid', 'Enable semantic rerank using Ollama embeddings', false)
97
- .option('--candidates <n>', 'How many lexical candidates to rerank in hybrid mode (default max(50,limit))')
98
- .option('--semantic-weight <w>', 'Hybrid weight for semantic score (0..1, default 0.7)', '0.7')
99
- .option('--ollama-base-url <url>', 'Ollama baseUrl (OpenAI-compatible). e.g. http://127.0.0.1:11434')
100
- .option('--embedding-model <id>', 'Embedding model id (default bge-m3)')
101
- .option('--ollama-timeout-ms <n>', 'Ollama timeout in ms (default 3000)')
102
- .action(async (query: string, cmdOpts) => {
103
- await withDb(async (dbPath) => {
104
- const db = openDb(dbPath);
105
- initSchema(db);
106
-
107
- const limit = Math.max(1, Math.min(200, Number(cmdOpts.limit ?? 10)));
108
-
109
- if (!cmdOpts.hybrid) {
110
- const out = searchItems(db, query, limit);
111
- console.log(JSON.stringify({ ok: true, mode: 'lexical', ...out }));
112
- return;
113
- }
114
-
115
- const candidates = cmdOpts.candidates ? Math.max(limit, Number(cmdOpts.candidates)) : undefined;
116
- const semanticWeight = Math.max(0, Math.min(1, Number(cmdOpts.semanticWeight ?? 0.7)));
117
-
118
- const results = await hybridSearch(
119
- db,
120
- {
121
- dbPath,
122
- ollamaBaseUrl: cmdOpts.ollamaBaseUrl,
123
- embeddingModel: cmdOpts.embeddingModel,
124
- ollamaTimeoutMs: cmdOpts.ollamaTimeoutMs ? Number(cmdOpts.ollamaTimeoutMs) : undefined,
125
- },
126
- // IMPORTANT: hybridSearch expects an already-escaped FTS query.
127
- // We can reuse searchItems() to get the escapedQuery.
128
- searchItems(db, query, 1).escapedQuery,
129
- { topK: limit, candidates, semanticWeight }
130
- );
131
-
132
- console.log(
133
- JSON.stringify({
134
- ok: true,
135
- mode: 'hybrid',
136
- query,
137
- results,
138
- embeddingModel: cmdOpts.embeddingModel ?? 'bge-m3',
139
- ollamaBaseUrl: cmdOpts.ollamaBaseUrl ?? 'http://127.0.0.1:11434',
140
- })
141
- );
142
- });
143
- });
144
- }
145
-
146
- registerSearchCommand('search');
147
- // Back-compat with README/skill naming.
148
- registerSearchCommand('recall');
149
-
150
- program.parse();
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import fs from 'node:fs';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import * as core from '@akashabot/openclaw-memory-offline-core';
6
+
7
+ const {
8
+ addItem,
9
+ hybridSearch,
10
+ hybridSearchFiltered,
11
+ initSchema,
12
+ openDb,
13
+ runMigrations,
14
+ searchItems,
15
+ getMemoriesByEntity,
16
+ getMemoriesBySession,
17
+ getMemoriesByProcess,
18
+ listEntities,
19
+ listSessions,
20
+ } = core;
21
+
22
+ const program = new Command();
23
+
24
+ program
25
+ .name('openclaw-mem')
26
+ .description('Offline memory (SQLite FTS + optional Ollama embeddings)')
27
+ .option('--db <path>', 'SQLite db path', 'memory.sqlite');
28
+
29
+ function withDb<T>(fn: (dbPath: string) => T): T {
30
+ const opts = program.opts<{ db: string }>();
31
+ return fn(opts.db);
32
+ }
33
+
34
+ program
35
+ .command('init')
36
+ .description('Initialize the SQLite database schema')
37
+ .action(() => {
38
+ withDb((dbPath) => {
39
+ const db = openDb(dbPath);
40
+ initSchema(db);
41
+ runMigrations(db);
42
+ console.log(JSON.stringify({ ok: true, db: dbPath }));
43
+ });
44
+ });
45
+
46
+ function registerAddCommand(name: string) {
47
+ program
48
+ .command(name)
49
+ .description('Add an item (and update FTS index)')
50
+ .argument('[text]', 'Text content. If omitted, reads from stdin.')
51
+ .option('--id <id>', 'Item id (default: uuid)')
52
+ .option('--title <title>', 'Title')
53
+ .option('--tags <tags>', 'Tags (freeform, e.g. "a,b,c")')
54
+ .option('--source <source>', 'Source (e.g. "whatsapp")')
55
+ .option('--source-id <sourceId>', 'Source id / external id')
56
+ .option('--meta <json>', 'JSON metadata (string)')
57
+ // Phase 1: Attribution & Session
58
+ .option('--entity-id <entityId>', 'Who said/wrote this (e.g. "loic", "system")')
59
+ .option('--process-id <processId>', 'Which agent/process captured this (e.g. "akasha")')
60
+ .option('--session-id <sessionId>', 'Session/conversation grouping')
61
+ .action((textArg: string | undefined, cmdOpts) => {
62
+ withDb((dbPath) => {
63
+ const db = openDb(dbPath);
64
+ initSchema(db);
65
+ runMigrations(db);
66
+
67
+ const text = (textArg ?? fs.readFileSync(0, 'utf8')).trim();
68
+ if (!text) {
69
+ process.exitCode = 2;
70
+ console.log(JSON.stringify({ ok: false, error: 'Missing text (arg or stdin)' }));
71
+ return;
72
+ }
73
+
74
+ let meta: unknown = undefined;
75
+ if (cmdOpts.meta) {
76
+ try {
77
+ meta = JSON.parse(String(cmdOpts.meta));
78
+ } catch (e: any) {
79
+ process.exitCode = 2;
80
+ console.log(
81
+ JSON.stringify({
82
+ ok: false,
83
+ error: 'Invalid JSON for --meta',
84
+ details: String(e?.message ?? e),
85
+ })
86
+ );
87
+ return;
88
+ }
89
+ }
90
+
91
+ const id = String(cmdOpts.id ?? uuidv4());
92
+ const item = addItem(db, {
93
+ id,
94
+ title: cmdOpts.title ? String(cmdOpts.title) : null,
95
+ text,
96
+ tags: cmdOpts.tags ? String(cmdOpts.tags) : null,
97
+ source: cmdOpts.source ? String(cmdOpts.source) : null,
98
+ source_id: cmdOpts.sourceId ? String(cmdOpts.sourceId) : null,
99
+ meta,
100
+ // Phase 1: Attribution & Session
101
+ entity_id: cmdOpts.entityId ? String(cmdOpts.entityId) : null,
102
+ process_id: cmdOpts.processId ? String(cmdOpts.processId) : null,
103
+ session_id: cmdOpts.sessionId ? String(cmdOpts.sessionId) : null,
104
+ });
105
+
106
+ console.log(JSON.stringify({ ok: true, item }));
107
+ });
108
+ });
109
+ }
110
+
111
+ registerAddCommand('add');
112
+ // Back-compat with README/skill naming.
113
+ registerAddCommand('remember');
114
+
115
+ function registerSearchCommand(name: string) {
116
+ program
117
+ .command(name)
118
+ .description('Search items using SQLite FTS5 (bm25), optionally reranked with Ollama embeddings')
119
+ .argument('<query>', 'FTS query (will be minimally escaped)')
120
+ .option('--limit <n>', 'Max results (default 10, max 200)', '10')
121
+ .option('--hybrid', 'Enable semantic rerank using Ollama embeddings', false)
122
+ .option('--candidates <n>', 'How many lexical candidates to rerank in hybrid mode (default max(50,limit))')
123
+ .option('--semantic-weight <w>', 'Hybrid weight for semantic score (0..1, default 0.7)', '0.7')
124
+ .option('--ollama-base-url <url>', 'Ollama baseUrl (OpenAI-compatible). e.g. http://127.0.0.1:11434')
125
+ .option('--embedding-model <id>', 'Embedding model id (default bge-m3)')
126
+ .option('--ollama-timeout-ms <n>', 'Ollama timeout in ms (default 3000)')
127
+ // Phase 1: Filtering
128
+ .option('--entity-id <entityId>', 'Filter by entity (who said/wrote)')
129
+ .option('--process-id <processId>', 'Filter by process (which agent captured)')
130
+ .option('--session-id <sessionId>', 'Filter by session/conversation')
131
+ .action(async (query: string, cmdOpts) => {
132
+ await withDb(async (dbPath) => {
133
+ const db = openDb(dbPath);
134
+ initSchema(db);
135
+ runMigrations(db);
136
+
137
+ const limit = Math.max(1, Math.min(200, Number(cmdOpts.limit ?? 10)));
138
+
139
+ // Check if any filter is specified
140
+ const hasFilter = cmdOpts.entityId || cmdOpts.processId || cmdOpts.sessionId;
141
+ const filter = hasFilter ? {
142
+ entity_id: cmdOpts.entityId || undefined,
143
+ process_id: cmdOpts.processId || undefined,
144
+ session_id: cmdOpts.sessionId || undefined,
145
+ } : undefined;
146
+
147
+ if (!cmdOpts.hybrid) {
148
+ const out = searchItems(db, query, limit);
149
+ console.log(JSON.stringify({ ok: true, mode: 'lexical', ...out }));
150
+ return;
151
+ }
152
+
153
+ const candidates = cmdOpts.candidates ? Math.max(limit, Number(cmdOpts.candidates)) : undefined;
154
+ const semanticWeight = Math.max(0, Math.min(1, Number(cmdOpts.semanticWeight ?? 0.7)));
155
+
156
+ // Use filtered search if filter options are specified
157
+ const results = await (hasFilter
158
+ ? hybridSearchFiltered(
159
+ db,
160
+ {
161
+ dbPath,
162
+ ollamaBaseUrl: cmdOpts.ollamaBaseUrl,
163
+ embeddingModel: cmdOpts.embeddingModel,
164
+ ollamaTimeoutMs: cmdOpts.ollamaTimeoutMs ? Number(cmdOpts.ollamaTimeoutMs) : undefined,
165
+ },
166
+ searchItems(db, query, 1).escapedQuery,
167
+ { topK: limit, candidates, semanticWeight, filter }
168
+ )
169
+ : hybridSearch(
170
+ db,
171
+ {
172
+ dbPath,
173
+ ollamaBaseUrl: cmdOpts.ollamaBaseUrl,
174
+ embeddingModel: cmdOpts.embeddingModel,
175
+ ollamaTimeoutMs: cmdOpts.ollamaTimeoutMs ? Number(cmdOpts.ollamaTimeoutMs) : undefined,
176
+ },
177
+ searchItems(db, query, 1).escapedQuery,
178
+ { topK: limit, candidates, semanticWeight }
179
+ ));
180
+
181
+ console.log(
182
+ JSON.stringify({
183
+ ok: true,
184
+ mode: hasFilter ? 'hybrid-filtered' : 'hybrid',
185
+ query,
186
+ filter: hasFilter ? filter : undefined,
187
+ results,
188
+ embeddingModel: cmdOpts.embeddingModel ?? 'bge-m3',
189
+ ollamaBaseUrl: cmdOpts.ollamaBaseUrl ?? 'http://127.0.0.1:11434',
190
+ })
191
+ );
192
+ });
193
+ });
194
+ }
195
+
196
+ registerSearchCommand('search');
197
+ // Back-compat with README/skill naming.
198
+ registerSearchCommand('recall');
199
+
200
+ // Phase 1: Utility commands for attribution & session
201
+ program
202
+ .command('list-entities')
203
+ .description('List all distinct entity_ids in the database')
204
+ .action(() => {
205
+ withDb((dbPath) => {
206
+ const db = openDb(dbPath);
207
+ initSchema(db);
208
+ runMigrations(db);
209
+ const entities = listEntities(db);
210
+ console.log(JSON.stringify({ ok: true, entities, count: entities.length }));
211
+ });
212
+ });
213
+
214
+ program
215
+ .command('list-sessions')
216
+ .description('List all distinct session_ids in the database')
217
+ .action(() => {
218
+ withDb((dbPath) => {
219
+ const db = openDb(dbPath);
220
+ initSchema(db);
221
+ runMigrations(db);
222
+ const sessions = listSessions(db);
223
+ console.log(JSON.stringify({ ok: true, sessions, count: sessions.length }));
224
+ });
225
+ });
226
+
227
+ program
228
+ .command('get-by-entity <entityId>')
229
+ .description('Get all memories from a specific entity')
230
+ .option('--limit <n>', 'Max results (default 50)', '50')
231
+ .action((entityId: string, cmdOpts) => {
232
+ withDb((dbPath) => {
233
+ const db = openDb(dbPath);
234
+ initSchema(db);
235
+ runMigrations(db);
236
+ const limit = Math.max(1, Math.min(500, Number(cmdOpts.limit ?? 50)));
237
+ const items = getMemoriesByEntity(db, entityId, limit);
238
+ console.log(JSON.stringify({ ok: true, entityId, count: items.length, items }));
239
+ });
240
+ });
241
+
242
+ program
243
+ .command('get-by-session <sessionId>')
244
+ .description('Get all memories from a specific session/conversation')
245
+ .option('--limit <n>', 'Max results (default 100)', '100')
246
+ .action((sessionId: string, cmdOpts) => {
247
+ withDb((dbPath) => {
248
+ const db = openDb(dbPath);
249
+ initSchema(db);
250
+ runMigrations(db);
251
+ const limit = Math.max(1, Math.min(500, Number(cmdOpts.limit ?? 100)));
252
+ const items = getMemoriesBySession(db, sessionId, limit);
253
+ console.log(JSON.stringify({ ok: true, sessionId, count: items.length, items }));
254
+ });
255
+ });
256
+
257
+ program.parse();
package/tsconfig.json CHANGED
@@ -1,13 +1,13 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "composite": true,
5
- "declaration": true,
6
- "rootDir": "src",
7
- "outDir": "dist"
8
- },
9
- "references": [
10
- { "path": "../core" }
11
- ],
12
- "include": ["src/**/*.ts"]
13
- }
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "declaration": true,
6
+ "rootDir": "src",
7
+ "outDir": "dist"
8
+ },
9
+ "references": [
10
+ { "path": "../core" }
11
+ ],
12
+ "include": ["src/**/*.ts"]
13
+ }