mnueron 0.1.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/ARCHITECTURE.md +161 -0
- package/INSTALL.md +262 -0
- package/LICENSE +21 -0
- package/README.md +305 -0
- package/dashboard/index.html +838 -0
- package/dist/cli.js +685 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/dashboard/server.js +234 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/detectors/claude_code.js +72 -0
- package/dist/detectors/claude_code.js.map +1 -0
- package/dist/detectors/claude_desktop.js +37 -0
- package/dist/detectors/claude_desktop.js.map +1 -0
- package/dist/detectors/cursor.js +36 -0
- package/dist/detectors/cursor.js.map +1 -0
- package/dist/detectors/extra.js +59 -0
- package/dist/detectors/extra.js.map +1 -0
- package/dist/detectors/index.js +14 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/json_detector.js +95 -0
- package/dist/detectors/json_detector.js.map +1 -0
- package/dist/detectors/types.js +13 -0
- package/dist/detectors/types.js.map +1 -0
- package/dist/import/claude.js +82 -0
- package/dist/import/claude.js.map +1 -0
- package/dist/import/openai.js +102 -0
- package/dist/import/openai.js.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/loader.js +175 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/types.js +24 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/setup.js +123 -0
- package/dist/setup.js.map +1 -0
- package/dist/store/chunking.js +150 -0
- package/dist/store/chunking.js.map +1 -0
- package/dist/store/embeddings.js +126 -0
- package/dist/store/embeddings.js.map +1 -0
- package/dist/store/local.js +720 -0
- package/dist/store/local.js.map +1 -0
- package/dist/store/provider.js +7 -0
- package/dist/store/provider.js.map +1 -0
- package/dist/store/redactor.js +114 -0
- package/dist/store/redactor.js.map +1 -0
- package/dist/store/remote.js +62 -0
- package/dist/store/remote.js.map +1 -0
- package/dist/tools.js +312 -0
- package/dist/tools.js.map +1 -0
- package/package.json +55 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mnueron CLI.
|
|
4
|
+
* mnueron init — write Claude Desktop config entry
|
|
5
|
+
* mnueron import <file> [--ns] — bulk import a Claude/OpenAI export
|
|
6
|
+
* mnueron stats — counts by namespace
|
|
7
|
+
* mnueron search <query> — quick search from terminal
|
|
8
|
+
* mnueron namespaces — list namespaces
|
|
9
|
+
*/
|
|
10
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { loadConfig, makeProvider } from './config.js';
|
|
15
|
+
import { importClaudeExport } from './import/claude.js';
|
|
16
|
+
import { importOpenAIExport } from './import/openai.js';
|
|
17
|
+
import { runSetup, formatReport } from './setup.js';
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
async function main() {
|
|
20
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
21
|
+
switch (cmd) {
|
|
22
|
+
case 'setup': return cmdSetup(rest);
|
|
23
|
+
case 'init': return cmdSetup(rest); // alias kept for older docs
|
|
24
|
+
case 'import': return cmdImport(rest);
|
|
25
|
+
case 'stats': return cmdStats();
|
|
26
|
+
case 'search': return cmdSearch(rest);
|
|
27
|
+
case 'namespaces': return cmdNamespaces();
|
|
28
|
+
case 'dashboard': return cmdDashboard(rest);
|
|
29
|
+
case 'rebuild-embeddings': return cmdRebuildEmbeddings(rest);
|
|
30
|
+
case 'rechunk': return cmdRechunk(rest);
|
|
31
|
+
case 'migrate-to-hosted': return cmdMigrateToHosted(rest);
|
|
32
|
+
case 'plugin': return cmdPlugin(rest);
|
|
33
|
+
case 'help':
|
|
34
|
+
case '--help':
|
|
35
|
+
case '-h':
|
|
36
|
+
case undefined: return printHelp();
|
|
37
|
+
default:
|
|
38
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
39
|
+
printHelp();
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`mnueron — persistent memory for AI agents
|
|
45
|
+
|
|
46
|
+
Commands:
|
|
47
|
+
mnueron setup Detect installed AI tools and configure each one
|
|
48
|
+
[--only <tool>] Only configure one tool (claude-desktop|claude-code|cursor|windsurf|cline)
|
|
49
|
+
[--hosted <url> --token <t>] Configure for hosted mode (default: local SQLite)
|
|
50
|
+
[--dry-run] Show what would change without writing
|
|
51
|
+
[--uninstall] Remove mnueron from all detected tools
|
|
52
|
+
mnueron import <file> Bulk-import a Claude or OpenAI export
|
|
53
|
+
[--ns <name>] Target namespace (default: "default")
|
|
54
|
+
[--format claude|openai] Skip format auto-detection
|
|
55
|
+
mnueron search <query> Search memories from the terminal
|
|
56
|
+
[--ns <name>] [--k <n>]
|
|
57
|
+
mnueron stats Show counts by namespace
|
|
58
|
+
mnueron namespaces List all namespaces
|
|
59
|
+
mnueron dashboard Launch the local web dashboard
|
|
60
|
+
[--port <n>] Port to bind (default 3122)
|
|
61
|
+
[--no-open] Don't open the browser automatically
|
|
62
|
+
mnueron rebuild-embeddings Generate vector embeddings for memories saved
|
|
63
|
+
before semantic-search support was added. Run
|
|
64
|
+
once after upgrading to v0.2+.
|
|
65
|
+
mnueron rechunk Split existing oversized memories (long backfilled
|
|
66
|
+
[--threshold <n>] chats) into per-turn atomic memories. Improves
|
|
67
|
+
[--keep-original] search granularity. Run once after first backfill.
|
|
68
|
+
[--dry-run]
|
|
69
|
+
mnueron migrate-to-hosted Upload your local SQLite memories to a hosted
|
|
70
|
+
--url <https://...> mnueron backend. Idempotent via source_ref dedup.
|
|
71
|
+
--token <mnu_...> After completion, optionally flips the active provider
|
|
72
|
+
[--batch <n>] so all subsequent reads/writes go to hosted.
|
|
73
|
+
[--namespace <name>] Filter to one namespace if you only want a subset.
|
|
74
|
+
[--dry-run]
|
|
75
|
+
[--no-flip] Upload but don't change the active provider.
|
|
76
|
+
mnueron plugin <action> [name] Manage plugins enabled in ~/.mnueron/config.json
|
|
77
|
+
list Show enabled + installed plugins.
|
|
78
|
+
enable <name> Add to enabledPlugins list.
|
|
79
|
+
disable <name> Remove from enabledPlugins list.
|
|
80
|
+
add <name> Alias for enable; also reminds about npm install.
|
|
81
|
+
remove <name> Alias for disable.
|
|
82
|
+
|
|
83
|
+
Environment:
|
|
84
|
+
MNUERON_DB_PATH Local SQLite location (default: ~/.mnueron/memories.db)
|
|
85
|
+
MNUERON_API_URL Hosted backend URL (enables remote mode)
|
|
86
|
+
MNUERON_API_TOKEN Token for remote mode
|
|
87
|
+
MNUERON_NAMESPACE Default namespace (default: "default")
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
async function cmdSetup(args) {
|
|
91
|
+
const opts = {};
|
|
92
|
+
for (let i = 0; i < args.length; i++) {
|
|
93
|
+
const a = args[i];
|
|
94
|
+
if (a === '--only' && args[i + 1]) {
|
|
95
|
+
opts.only = (opts.only ?? []).concat(args[++i]);
|
|
96
|
+
}
|
|
97
|
+
else if (a === '--hosted' && args[i + 1] && args[i + 2] && args[i + 2] !== '--token') {
|
|
98
|
+
// tolerate `--hosted URL --token TOKEN` and `--hosted URL --token TOKEN`
|
|
99
|
+
const url = args[++i];
|
|
100
|
+
const next = args[i + 1];
|
|
101
|
+
if (next === '--token' && args[i + 2]) {
|
|
102
|
+
i++;
|
|
103
|
+
opts.hosted = { url, token: args[++i] };
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error('--hosted requires --token <value>');
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (a === '--hosted' && args[i + 1]) {
|
|
111
|
+
// alt parse: --hosted URL --token TOKEN
|
|
112
|
+
const url = args[++i];
|
|
113
|
+
const tokenIdx = args.indexOf('--token', i);
|
|
114
|
+
if (tokenIdx === -1 || !args[tokenIdx + 1]) {
|
|
115
|
+
console.error('--hosted requires --token <value>');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
opts.hosted = { url, token: args[tokenIdx + 1] };
|
|
119
|
+
}
|
|
120
|
+
else if (a === '--dry-run') {
|
|
121
|
+
opts.dryRun = true;
|
|
122
|
+
}
|
|
123
|
+
else if (a === '--uninstall') {
|
|
124
|
+
opts.uninstall = true;
|
|
125
|
+
}
|
|
126
|
+
else if (a === '--yes' || a === '-y') {
|
|
127
|
+
opts.yes = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const banner = `\n 🧠 mnueron — persistent memory for AI dev tools\n` +
|
|
131
|
+
` mode: ${opts.hosted ? 'hosted (' + opts.hosted.url + ')' : 'local SQLite'}\n` +
|
|
132
|
+
(opts.dryRun ? ` DRY RUN — no files will be changed\n` : '') +
|
|
133
|
+
(opts.uninstall ? ` REMOVING — will unregister from detected tools\n` : '');
|
|
134
|
+
console.log(banner);
|
|
135
|
+
const reports = await runSetup(opts);
|
|
136
|
+
console.log(formatReport(reports));
|
|
137
|
+
const ok = reports.some(r => r.status === 'configured' || r.status === 'updated' || r.status === 'uninstalled');
|
|
138
|
+
if (ok && !opts.dryRun && !opts.uninstall) {
|
|
139
|
+
console.log(`\n✨ Done. Restart any running AI tool to load the memory plugin.`);
|
|
140
|
+
console.log(` Then ask it: "What memory tools do you have?"\n`);
|
|
141
|
+
}
|
|
142
|
+
else if (!opts.dryRun && !opts.uninstall) {
|
|
143
|
+
console.log(`\n No supported AI tools detected on this machine.`);
|
|
144
|
+
console.log(` Install one of: Claude Desktop, Claude Code, Cursor, Windsurf, Cline\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function cmdImport(args) {
|
|
148
|
+
if (args.length === 0) {
|
|
149
|
+
console.error('Usage: mnueron import <file> [--ns <namespace>] [--format claude|openai]');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const file = args[0];
|
|
153
|
+
let ns = 'default';
|
|
154
|
+
let format = 'auto';
|
|
155
|
+
for (let i = 1; i < args.length; i++) {
|
|
156
|
+
if (args[i] === '--ns' && args[i + 1]) {
|
|
157
|
+
ns = args[++i];
|
|
158
|
+
}
|
|
159
|
+
else if (args[i] === '--format' && args[i + 1]) {
|
|
160
|
+
const f = args[++i];
|
|
161
|
+
if (f === 'claude' || f === 'openai')
|
|
162
|
+
format = f;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!existsSync(file)) {
|
|
166
|
+
console.error(`File not found: ${file}`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const sz = (await stat(file)).size;
|
|
170
|
+
console.log(`Reading ${file} (${(sz / 1024).toFixed(1)} KB)...`);
|
|
171
|
+
if (format === 'auto') {
|
|
172
|
+
const head = (await readFile(file, 'utf8')).slice(0, 4000);
|
|
173
|
+
if (head.includes('"chat_messages"'))
|
|
174
|
+
format = 'claude';
|
|
175
|
+
else if (head.includes('"mapping"'))
|
|
176
|
+
format = 'openai';
|
|
177
|
+
else
|
|
178
|
+
format = 'claude';
|
|
179
|
+
console.log(`Detected format: ${format}`);
|
|
180
|
+
}
|
|
181
|
+
const items = format === 'claude'
|
|
182
|
+
? await importClaudeExport(file, ns)
|
|
183
|
+
: await importOpenAIExport(file, ns);
|
|
184
|
+
console.log(`Parsed ${items.length} conversations. Saving...`);
|
|
185
|
+
const provider = makeProvider(loadConfig());
|
|
186
|
+
const result = await provider.bulkSave(items);
|
|
187
|
+
await provider.close();
|
|
188
|
+
console.log(`✓ Saved ${result.saved}, errors ${result.errors}, namespace="${ns}"`);
|
|
189
|
+
}
|
|
190
|
+
async function cmdStats() {
|
|
191
|
+
const provider = makeProvider(loadConfig());
|
|
192
|
+
const namespaces = await provider.namespaces();
|
|
193
|
+
await provider.close();
|
|
194
|
+
if (namespaces.length === 0) {
|
|
195
|
+
console.log('No memories yet.');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const total = namespaces.reduce((s, n) => s + n.count, 0);
|
|
199
|
+
console.log(`Total: ${total} memories across ${namespaces.length} namespaces\n`);
|
|
200
|
+
for (const ns of namespaces) {
|
|
201
|
+
const date = ns.last_updated ? new Date(ns.last_updated).toISOString().slice(0, 10) : '—';
|
|
202
|
+
console.log(` ${ns.name.padEnd(24)} ${String(ns.count).padStart(6)} last: ${date}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function cmdSearch(args) {
|
|
206
|
+
if (args.length === 0) {
|
|
207
|
+
console.error('Usage: mnueron search <query> [--ns <namespace>] [--k <n>]');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
let ns;
|
|
211
|
+
let k = 5;
|
|
212
|
+
const queryParts = [];
|
|
213
|
+
for (let i = 0; i < args.length; i++) {
|
|
214
|
+
if (args[i] === '--ns' && args[i + 1]) {
|
|
215
|
+
ns = args[++i];
|
|
216
|
+
}
|
|
217
|
+
else if (args[i] === '--k' && args[i + 1]) {
|
|
218
|
+
k = parseInt(args[++i], 10) || 5;
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
queryParts.push(args[i]);
|
|
222
|
+
}
|
|
223
|
+
const query = queryParts.join(' ');
|
|
224
|
+
const provider = makeProvider(loadConfig());
|
|
225
|
+
const hits = await provider.search({ query, namespace: ns, k });
|
|
226
|
+
await provider.close();
|
|
227
|
+
if (hits.length === 0) {
|
|
228
|
+
console.log('No matches.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const m of hits) {
|
|
232
|
+
const date = new Date(m.created_at).toISOString().slice(0, 10);
|
|
233
|
+
const preview = m.content.replace(/\s+/g, ' ').slice(0, 200);
|
|
234
|
+
console.log(`\n[${date}] ${m.namespace}${m.tags.length ? ` #${m.tags.join(' #')}` : ''}`);
|
|
235
|
+
console.log(` ${preview}${m.content.length > 200 ? '…' : ''}`);
|
|
236
|
+
console.log(` id: ${m.id}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function cmdNamespaces() {
|
|
240
|
+
const provider = makeProvider(loadConfig());
|
|
241
|
+
const namespaces = await provider.namespaces();
|
|
242
|
+
await provider.close();
|
|
243
|
+
for (const ns of namespaces)
|
|
244
|
+
console.log(ns.name);
|
|
245
|
+
}
|
|
246
|
+
async function cmdDashboard(args) {
|
|
247
|
+
let port = 3122;
|
|
248
|
+
let openBrowser = true;
|
|
249
|
+
for (let i = 0; i < args.length; i++) {
|
|
250
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
251
|
+
port = parseInt(args[++i], 10) || 3122;
|
|
252
|
+
}
|
|
253
|
+
else if (args[i] === '--no-open') {
|
|
254
|
+
openBrowser = false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const { startDashboard } = await import('./dashboard/server.js');
|
|
258
|
+
const provider = makeProvider(loadConfig());
|
|
259
|
+
const handle = await startDashboard(provider, port).catch((e) => {
|
|
260
|
+
if (e?.code === 'EADDRINUSE') {
|
|
261
|
+
console.error(`Port ${port} is already in use. Try --port <other>.`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
throw e;
|
|
265
|
+
});
|
|
266
|
+
console.log(`\n 🧠 mnueron dashboard`);
|
|
267
|
+
console.log(` ${handle.url}\n`);
|
|
268
|
+
console.log(` Ctrl+C to stop.`);
|
|
269
|
+
if (openBrowser)
|
|
270
|
+
openInBrowser(handle.url);
|
|
271
|
+
const shutdown = async () => {
|
|
272
|
+
await handle.close().catch(() => { });
|
|
273
|
+
await provider.close().catch(() => { });
|
|
274
|
+
process.exit(0);
|
|
275
|
+
};
|
|
276
|
+
process.on('SIGINT', shutdown);
|
|
277
|
+
process.on('SIGTERM', shutdown);
|
|
278
|
+
// wait forever
|
|
279
|
+
await new Promise(() => { });
|
|
280
|
+
}
|
|
281
|
+
async function cmdRechunk(args) {
|
|
282
|
+
let threshold = 6000;
|
|
283
|
+
let keepOriginal = false;
|
|
284
|
+
let dryRun = false;
|
|
285
|
+
for (let i = 0; i < args.length; i++) {
|
|
286
|
+
if (args[i] === '--threshold' && args[i + 1]) {
|
|
287
|
+
threshold = parseInt(args[++i], 10) || 6000;
|
|
288
|
+
}
|
|
289
|
+
else if (args[i] === '--keep-original') {
|
|
290
|
+
keepOriginal = true;
|
|
291
|
+
}
|
|
292
|
+
else if (args[i] === '--dry-run') {
|
|
293
|
+
dryRun = true;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const provider = makeProvider(loadConfig());
|
|
297
|
+
if (typeof provider.findOversizedMemories !== 'function') {
|
|
298
|
+
console.error('rechunk only supported in local mode.');
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
const rows = provider.findOversizedMemories(threshold);
|
|
302
|
+
if (rows.length === 0) {
|
|
303
|
+
console.log(`\n ✓ No memories larger than ${threshold} chars. Nothing to do.\n`);
|
|
304
|
+
await provider.close();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Sum bytes for the report
|
|
308
|
+
const totalChars = rows.reduce((s, r) => s + (r.content?.length ?? 0), 0);
|
|
309
|
+
console.log(`\n 🔪 Rechunk plan`);
|
|
310
|
+
console.log(` ${rows.length} memories over ${threshold} chars`);
|
|
311
|
+
console.log(` total content: ${(totalChars / 1024).toFixed(1)} KB`);
|
|
312
|
+
console.log(` strategy: transcript-aware split (per-turn) with sliding-window fallback`);
|
|
313
|
+
console.log(` mode: ${dryRun ? 'DRY RUN (no writes)' : keepOriginal ? 'split + keep originals' : 'split + delete originals'}\n`);
|
|
314
|
+
const { chunkContent } = await import('./store/chunking.js');
|
|
315
|
+
let oversize = 0, chunksMade = 0, deleted = 0, errors = 0;
|
|
316
|
+
for (const row of rows) {
|
|
317
|
+
oversize++;
|
|
318
|
+
const chunks = chunkContent(row.content);
|
|
319
|
+
if (chunks.length < 2) {
|
|
320
|
+
// Couldn't split — content has no transcript shape AND fits the
|
|
321
|
+
// sliding window. Skip rather than create a single useless duplicate.
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
process.stdout.write(` [${oversize}/${rows.length}] ${row.id.slice(0, 8)}… → ${chunks.length} chunks`);
|
|
325
|
+
if (dryRun) {
|
|
326
|
+
console.log(' (dry run)');
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const tags = JSON.parse(row.tags_json ?? '[]');
|
|
331
|
+
const meta = row.meta_json ? JSON.parse(row.meta_json) : {};
|
|
332
|
+
const parentRef = row.source_ref ?? `chunked:${row.id}`;
|
|
333
|
+
// Build inputs for bulkSave
|
|
334
|
+
const inputs = chunks.map((c, i) => ({
|
|
335
|
+
content: c.content,
|
|
336
|
+
namespace: row.namespace,
|
|
337
|
+
tags: [...tags, 'chunk', 'rechunked', ...(c.role ? [`role:${c.role}`] : [])],
|
|
338
|
+
source: row.source,
|
|
339
|
+
source_ref: parentRef,
|
|
340
|
+
metadata: {
|
|
341
|
+
...meta,
|
|
342
|
+
parent_ref: parentRef,
|
|
343
|
+
chunk_index: i,
|
|
344
|
+
chunk_count: chunks.length,
|
|
345
|
+
...(c.role ? { role: c.role } : {}),
|
|
346
|
+
original_id: row.id,
|
|
347
|
+
original_created_at: row.created_at,
|
|
348
|
+
},
|
|
349
|
+
}));
|
|
350
|
+
await provider.bulkSave(inputs);
|
|
351
|
+
chunksMade += chunks.length;
|
|
352
|
+
if (!keepOriginal) {
|
|
353
|
+
await provider.delete(row.id);
|
|
354
|
+
deleted++;
|
|
355
|
+
}
|
|
356
|
+
console.log(' ✓');
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
errors++;
|
|
360
|
+
console.log(` ✗ ${e?.message ?? e}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
console.log(`\n Done.`);
|
|
364
|
+
console.log(` ${chunksMade} new chunked memories created`);
|
|
365
|
+
if (!dryRun && !keepOriginal)
|
|
366
|
+
console.log(` ${deleted} originals deleted`);
|
|
367
|
+
if (errors > 0)
|
|
368
|
+
console.log(` ${errors} errors`);
|
|
369
|
+
console.log(`\n Next: run 'mnueron rebuild-embeddings' so the new chunks have vectors.\n`);
|
|
370
|
+
await provider.close();
|
|
371
|
+
}
|
|
372
|
+
async function cmdPlugin(args) {
|
|
373
|
+
const action = args[0] ?? 'list';
|
|
374
|
+
const name = args[1];
|
|
375
|
+
const { listEnabledPlugins, enablePlugin, disablePlugin } = await import('./plugins/loader.js');
|
|
376
|
+
switch (action) {
|
|
377
|
+
case 'list': {
|
|
378
|
+
const enabled = await listEnabledPlugins();
|
|
379
|
+
if (enabled.length === 0) {
|
|
380
|
+
console.log('No plugins enabled. Try:');
|
|
381
|
+
console.log(' npm install -g mnueron-plugin-redact-pii # install');
|
|
382
|
+
console.log(' mnueron plugin enable mnueron-plugin-redact-pii');
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
console.log(`Enabled plugins (${enabled.length}):`);
|
|
386
|
+
for (const n of enabled) {
|
|
387
|
+
// Check whether the plugin's npm package is actually resolvable.
|
|
388
|
+
let resolved = null;
|
|
389
|
+
try {
|
|
390
|
+
const { createRequire } = await import('node:module');
|
|
391
|
+
const require = createRequire(import.meta.url);
|
|
392
|
+
resolved = require.resolve(n + '/package.json');
|
|
393
|
+
}
|
|
394
|
+
catch { /* not installed */ }
|
|
395
|
+
const status = resolved ? '✓ installed' : '✗ NOT installed (npm install ' + n + ')';
|
|
396
|
+
console.log(` ${n.padEnd(40)} ${status}`);
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
case 'enable':
|
|
401
|
+
case 'add': {
|
|
402
|
+
if (!name) {
|
|
403
|
+
console.error('Usage: mnueron plugin enable <name>');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
await enablePlugin(name);
|
|
407
|
+
console.log(`✓ Enabled ${name} in ~/.mnueron/config.json`);
|
|
408
|
+
// Check whether it's installed; warn if not.
|
|
409
|
+
try {
|
|
410
|
+
const { createRequire } = await import('node:module');
|
|
411
|
+
const require = createRequire(import.meta.url);
|
|
412
|
+
require.resolve(name + '/package.json');
|
|
413
|
+
console.log(' Plugin package is installed — will activate on next MCP server start.');
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
console.log(` Plugin package is NOT installed yet. Run:`);
|
|
417
|
+
console.log(` npm install -g ${name}`);
|
|
418
|
+
}
|
|
419
|
+
console.log(` Restart any running mnueron processes (Claude Code, dashboard) to pick it up.`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
case 'disable':
|
|
423
|
+
case 'remove': {
|
|
424
|
+
if (!name) {
|
|
425
|
+
console.error('Usage: mnueron plugin disable <name>');
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
await disablePlugin(name);
|
|
429
|
+
console.log(`✓ Disabled ${name} in ~/.mnueron/config.json`);
|
|
430
|
+
console.log(` Restart any running mnueron processes to drop it.`);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
default:
|
|
434
|
+
console.error(`Unknown action "${action}". Try: list | enable | disable | add | remove`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function cmdMigrateToHosted(args) {
|
|
439
|
+
let url = '';
|
|
440
|
+
let token = '';
|
|
441
|
+
let batch = 100;
|
|
442
|
+
let namespace;
|
|
443
|
+
let dryRun = false;
|
|
444
|
+
let noFlip = false;
|
|
445
|
+
for (let i = 0; i < args.length; i++) {
|
|
446
|
+
if (args[i] === '--url' && args[i + 1])
|
|
447
|
+
url = args[++i];
|
|
448
|
+
else if (args[i] === '--token' && args[i + 1])
|
|
449
|
+
token = args[++i];
|
|
450
|
+
else if (args[i] === '--batch' && args[i + 1])
|
|
451
|
+
batch = parseInt(args[++i], 10) || 100;
|
|
452
|
+
else if (args[i] === '--namespace' && args[i + 1])
|
|
453
|
+
namespace = args[++i];
|
|
454
|
+
else if (args[i] === '--dry-run')
|
|
455
|
+
dryRun = true;
|
|
456
|
+
else if (args[i] === '--no-flip')
|
|
457
|
+
noFlip = true;
|
|
458
|
+
}
|
|
459
|
+
if (!url || !token) {
|
|
460
|
+
console.error(`Usage: mnueron migrate-to-hosted --url <https://api.mnueron.com> --token <mnu_...>`);
|
|
461
|
+
console.error(`Tip: sign up at the hosted dashboard to get your token, then paste here.`);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
// We migrate FROM local. Force local mode regardless of env vars so the
|
|
465
|
+
// user can't accidentally migrate hosted-to-hosted.
|
|
466
|
+
process.env.MNUERON_API_URL = '';
|
|
467
|
+
process.env.MNUERON_API_TOKEN = '';
|
|
468
|
+
const cfg = loadConfig();
|
|
469
|
+
if (cfg.mode !== 'local') {
|
|
470
|
+
console.error('migrate-to-hosted reads FROM local mode. Got mode=' + cfg.mode);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
const provider = makeProvider(cfg);
|
|
474
|
+
// Pull every memory from local
|
|
475
|
+
const namespaces = await provider.namespaces();
|
|
476
|
+
const total = namespace
|
|
477
|
+
? (namespaces.find(n => n.name === namespace)?.count ?? 0)
|
|
478
|
+
: namespaces.reduce((s, n) => s + n.count, 0);
|
|
479
|
+
console.log(`\n 🚀 mnueron migrate-to-hosted`);
|
|
480
|
+
console.log(` source: local SQLite (${total} memories${namespace ? ` in namespace ${namespace}` : ''})`);
|
|
481
|
+
console.log(` target: ${url}`);
|
|
482
|
+
console.log(` batch: ${batch}`);
|
|
483
|
+
console.log(` mode: ${dryRun ? 'DRY RUN (no writes)' : 'live upload'}\n`);
|
|
484
|
+
if (total === 0) {
|
|
485
|
+
console.log(` Nothing to migrate.`);
|
|
486
|
+
await provider.close();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
// Ping the target first
|
|
490
|
+
if (!dryRun) {
|
|
491
|
+
try {
|
|
492
|
+
const ping = await fetch(`${url.replace(/\/+$/, '')}/health`);
|
|
493
|
+
if (!ping.ok)
|
|
494
|
+
throw new Error(`/health returned ${ping.status}`);
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
console.error(`Cannot reach ${url}/health: ${e.message}`);
|
|
498
|
+
console.error(`Check the URL and that the hosted backend is running.`);
|
|
499
|
+
await provider.close();
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Stream memories in chunks of `batch`, oldest first, so the hosted side
|
|
504
|
+
// ends up with chronological order in dashboards.
|
|
505
|
+
let cursor;
|
|
506
|
+
let uploaded = 0, failed = 0;
|
|
507
|
+
const start = Date.now();
|
|
508
|
+
while (true) {
|
|
509
|
+
const page = await provider.list({
|
|
510
|
+
namespace,
|
|
511
|
+
limit: batch,
|
|
512
|
+
before: cursor,
|
|
513
|
+
});
|
|
514
|
+
if (page.length === 0)
|
|
515
|
+
break;
|
|
516
|
+
// list() returns DESC; we want to upload oldest first, but the hosted
|
|
517
|
+
// side will reorder on display anyway, so just upload as-is.
|
|
518
|
+
if (!dryRun) {
|
|
519
|
+
try {
|
|
520
|
+
const items = page.map(m => ({
|
|
521
|
+
content: m.content,
|
|
522
|
+
namespace: m.namespace,
|
|
523
|
+
tags: m.tags,
|
|
524
|
+
source: m.source,
|
|
525
|
+
source_ref: m.source_ref,
|
|
526
|
+
metadata: m.metadata,
|
|
527
|
+
}));
|
|
528
|
+
const res = await fetch(`${url.replace(/\/+$/, '')}/v1/memories/bulk`, {
|
|
529
|
+
method: 'POST',
|
|
530
|
+
headers: {
|
|
531
|
+
'Authorization': `Bearer ${token}`,
|
|
532
|
+
'Content-Type': 'application/json',
|
|
533
|
+
},
|
|
534
|
+
body: JSON.stringify({ items }),
|
|
535
|
+
});
|
|
536
|
+
if (!res.ok) {
|
|
537
|
+
const body = await res.text().catch(() => '');
|
|
538
|
+
throw new Error(`${res.status} ${res.statusText}${body ? `: ${body.slice(0, 200)}` : ''}`);
|
|
539
|
+
}
|
|
540
|
+
const data = await res.json().catch(() => ({}));
|
|
541
|
+
uploaded += (data.saved ?? page.length);
|
|
542
|
+
failed += (data.errors ?? 0);
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
failed += page.length;
|
|
546
|
+
console.error(` batch failed at cursor=${cursor}: ${e.message}`);
|
|
547
|
+
// Stop on transport error so we don't retry forever
|
|
548
|
+
if (`${e.message}`.includes('ECONNREFUSED') || `${e.message}`.includes('Cannot reach'))
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
uploaded += page.length;
|
|
554
|
+
}
|
|
555
|
+
cursor = page[page.length - 1].created_at;
|
|
556
|
+
const pct = Math.round((uploaded + failed) / total * 100);
|
|
557
|
+
process.stdout.write(`\r [${'█'.repeat(Math.floor(pct / 4)).padEnd(25, '░')}] ${pct}% ${uploaded + failed}/${total}`);
|
|
558
|
+
if (page.length < batch)
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
process.stdout.write('\n');
|
|
562
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
563
|
+
console.log(`\n ✓ Done in ${elapsed}s — ${uploaded} uploaded${failed ? `, ${failed} failed` : ''}.\n`);
|
|
564
|
+
if (!dryRun && !noFlip && failed === 0) {
|
|
565
|
+
await writeActiveProvider(url, token);
|
|
566
|
+
console.log(` 🔁 Active provider switched to hosted.`);
|
|
567
|
+
console.log(` ~/.mnueron/config.json updated with MNUERON_API_URL + MNUERON_API_TOKEN.`);
|
|
568
|
+
console.log(` Restart any running mnueron processes (Claude Code, dashboard) to pick up the change.\n`);
|
|
569
|
+
}
|
|
570
|
+
else if (noFlip) {
|
|
571
|
+
console.log(` ℹ️ --no-flip set: active provider stays on local.`);
|
|
572
|
+
console.log(` Set MNUERON_API_URL=${url} and MNUERON_API_TOKEN=<your token> in your shell when ready.\n`);
|
|
573
|
+
}
|
|
574
|
+
await provider.close();
|
|
575
|
+
}
|
|
576
|
+
async function writeActiveProvider(url, token) {
|
|
577
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
578
|
+
const { homedir } = await import('node:os');
|
|
579
|
+
const { join } = await import('node:path');
|
|
580
|
+
const dir = join(homedir(), '.mnueron');
|
|
581
|
+
await mkdir(dir, { recursive: true });
|
|
582
|
+
const configPath = join(dir, 'config.json');
|
|
583
|
+
let existing = {};
|
|
584
|
+
try {
|
|
585
|
+
const { readFile } = await import('node:fs/promises');
|
|
586
|
+
existing = JSON.parse(await readFile(configPath, 'utf8'));
|
|
587
|
+
}
|
|
588
|
+
catch { /* fresh */ }
|
|
589
|
+
existing.apiUrl = url;
|
|
590
|
+
existing.apiToken = token;
|
|
591
|
+
await writeFile(configPath, JSON.stringify(existing, null, 2), 'utf8');
|
|
592
|
+
}
|
|
593
|
+
async function openInBrowser(url) {
|
|
594
|
+
const { exec } = await import('node:child_process');
|
|
595
|
+
const cmd = process.platform === 'win32' ? `cmd /c start "" "${url}"` :
|
|
596
|
+
process.platform === 'darwin' ? `open "${url}"` :
|
|
597
|
+
`xdg-open "${url}"`;
|
|
598
|
+
exec(cmd, () => { });
|
|
599
|
+
}
|
|
600
|
+
async function cmdRebuildEmbeddings(args) {
|
|
601
|
+
const force = args.includes('--force');
|
|
602
|
+
const provider = makeProvider(loadConfig());
|
|
603
|
+
// Only LocalProvider exposes rebuildEmbeddings — type-guard accordingly.
|
|
604
|
+
const local = provider;
|
|
605
|
+
if (typeof local.rebuildEmbeddings !== 'function') {
|
|
606
|
+
console.error('rebuild-embeddings only supported in local mode.');
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
// Diagnostics — reach into the DB so we can see what's actually there.
|
|
610
|
+
// sqlite-vec virtual tables don't always answer LEFT JOIN questions the
|
|
611
|
+
// way you'd expect, so we print counts directly.
|
|
612
|
+
const db = local.db;
|
|
613
|
+
const memCount = db?.prepare('SELECT COUNT(*) c FROM memories').get()?.c ?? 0;
|
|
614
|
+
let vecCount = 0;
|
|
615
|
+
let vecOk = false;
|
|
616
|
+
try {
|
|
617
|
+
vecCount = db?.prepare('SELECT COUNT(*) c FROM memories_vec').get()?.c ?? 0;
|
|
618
|
+
vecOk = true;
|
|
619
|
+
}
|
|
620
|
+
catch (e) {
|
|
621
|
+
console.log(` memories_vec not queryable: ${e.message}`);
|
|
622
|
+
}
|
|
623
|
+
const missing = local.countMissingEmbeddings?.() ?? 0;
|
|
624
|
+
console.log(`\n 📊 Embeddings diagnostic`);
|
|
625
|
+
console.log(` memories table: ${memCount} rows`);
|
|
626
|
+
console.log(` memories_vec table: ${vecOk ? vecCount + ' rows' : 'not available'}`);
|
|
627
|
+
console.log(` missing embeddings: ${missing}`);
|
|
628
|
+
if (!vecOk) {
|
|
629
|
+
console.log(`\n sqlite-vec extension is not loaded. Aborting.\n`);
|
|
630
|
+
await provider.close();
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (missing === 0 && !force) {
|
|
634
|
+
if (vecCount >= memCount) {
|
|
635
|
+
console.log(`\n ✓ All memories already have embeddings. Nothing to do.\n`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
console.log(`\n countMissingEmbeddings returns 0 but vec table has fewer rows than memories.\n` +
|
|
639
|
+
` Run with --force to re-embed all memories.\n`);
|
|
640
|
+
}
|
|
641
|
+
await provider.close();
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
// If --force was passed, wipe the vec table first so rebuildEmbeddings
|
|
645
|
+
// re-embeds everything (its built-in query only catches missing rows).
|
|
646
|
+
if (force && vecOk) {
|
|
647
|
+
console.log(`\n --force: wiping memories_vec and re-embedding all ${memCount} memories...`);
|
|
648
|
+
try {
|
|
649
|
+
db.prepare(`DELETE FROM memories_vec`).run();
|
|
650
|
+
}
|
|
651
|
+
catch (e) {
|
|
652
|
+
console.log(` (could not wipe via DELETE: ${e.message}; trying DROP+CREATE)`);
|
|
653
|
+
db.exec(`
|
|
654
|
+
DROP TABLE IF EXISTS memories_vec;
|
|
655
|
+
CREATE VIRTUAL TABLE memories_vec USING vec0(memory_id TEXT PRIMARY KEY, embedding float[384]);
|
|
656
|
+
`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const total = force ? memCount : missing;
|
|
660
|
+
console.log(`\n 🧠 Rebuilding embeddings for ${missing} memories...`);
|
|
661
|
+
console.log(` First run downloads the all-MiniLM-L6-v2 model (~25MB).`);
|
|
662
|
+
console.log(` Cached to ~/.mnueron/models/ for subsequent runs.\n`);
|
|
663
|
+
const start = Date.now();
|
|
664
|
+
let lastLog = 0;
|
|
665
|
+
const result = await local.rebuildEmbeddings((done, total, _current) => {
|
|
666
|
+
// Throttle progress output to once per 250ms or every 16 items.
|
|
667
|
+
const now = Date.now();
|
|
668
|
+
if (now - lastLog < 250 && done < total)
|
|
669
|
+
return;
|
|
670
|
+
lastLog = now;
|
|
671
|
+
const pct = Math.round((done / total) * 100);
|
|
672
|
+
const bar = '█'.repeat(Math.floor(pct / 4)).padEnd(25, '░');
|
|
673
|
+
process.stdout.write(`\r [${bar}] ${pct}% ${done}/${total}`);
|
|
674
|
+
});
|
|
675
|
+
process.stdout.write('\n');
|
|
676
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
677
|
+
console.log(`\n ✓ Done in ${elapsed}s — ${result.updated} embedded, ` +
|
|
678
|
+
`${result.skipped} skipped, ${result.errors} errors.\n`);
|
|
679
|
+
await provider.close();
|
|
680
|
+
}
|
|
681
|
+
main().catch(e => {
|
|
682
|
+
console.error(e?.stack ?? e);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
});
|
|
685
|
+
//# sourceMappingURL=cli.js.map
|