@mem-weave/server 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.
- package/README.md +74 -0
- package/dist/cli-entry.js +49 -0
- package/dist/cli.js +53 -0
- package/dist/commands/backup.js +28 -0
- package/dist/commands/doctor.js +108 -0
- package/dist/commands/help.js +29 -0
- package/dist/commands/index.js +27 -0
- package/dist/commands/init.js +58 -0
- package/dist/commands/migrate.js +25 -0
- package/dist/commands/start.js +29 -0
- package/dist/commands/status.js +19 -0
- package/dist/commands/stop.js +46 -0
- package/dist/commands/version.js +21 -0
- package/dist/core/config.js +161 -0
- package/dist/core/decay.js +50 -0
- package/dist/core/types.js +72 -0
- package/dist/db/database.js +58 -0
- package/dist/db/repositories/access-log-repo.js +59 -0
- package/dist/db/repositories/consolidation-run-repo.js +86 -0
- package/dist/db/repositories/device-repo.js +66 -0
- package/dist/db/repositories/edge-repo.js +104 -0
- package/dist/db/repositories/memory-repo.js +294 -0
- package/dist/db/repositories/observation-repo.js +65 -0
- package/dist/db/repositories/session-repo.js +81 -0
- package/dist/db/repositories/stats-repo.js +92 -0
- package/dist/db/repositories/vector-repo.js +55 -0
- package/dist/db/schema.js +185 -0
- package/dist/injection/bundler.js +39 -0
- package/dist/injection/formatter.js +23 -0
- package/dist/prompts/compression.js +43 -0
- package/dist/prompts/edge-extract.js +21 -0
- package/dist/prompts/value-gate.js +27 -0
- package/dist/providers/embedding/index.js +36 -0
- package/dist/providers/embedding/local-xenova.js +166 -0
- package/dist/providers/embedding/noop.js +40 -0
- package/dist/providers/embedding/openai-compatible.js +46 -0
- package/dist/providers/llm/index.js +12 -0
- package/dist/providers/llm/noop.js +5 -0
- package/dist/providers/llm/openai.js +45 -0
- package/dist/rest/routes/consolidation.js +62 -0
- package/dist/rest/routes/devices.js +47 -0
- package/dist/rest/routes/injection.js +76 -0
- package/dist/rest/routes/memories.js +349 -0
- package/dist/rest/routes/observations.js +29 -0
- package/dist/rest/routes/sessions.js +37 -0
- package/dist/rest/routes/settings.js +25 -0
- package/dist/rest/routes/stats.js +15 -0
- package/dist/retrieval/bm25-search.js +91 -0
- package/dist/retrieval/causal-chain.js +197 -0
- package/dist/retrieval/fusion.js +48 -0
- package/dist/retrieval/graph-traversal.js +144 -0
- package/dist/retrieval/search-engine.js +150 -0
- package/dist/retrieval/vector-search.js +91 -0
- package/dist/server/auth.js +80 -0
- package/dist/server/bootstrap.js +28 -0
- package/dist/server/http.js +77 -0
- package/dist/server/logger.js +36 -0
- package/dist/server/rate-limiter.js +81 -0
- package/dist/server/scheduler.js +99 -0
- package/dist/workers/association.js +41 -0
- package/dist/workers/compressor.js +14 -0
- package/dist/workers/consolidator.js +201 -0
- package/dist/workers/embedder.js +102 -0
- package/dist/workers/graph-worker.js +166 -0
- package/dist/workers/value-gate.js +38 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @mem-weave/server
|
|
2
|
+
|
|
3
|
+
**MemWeave local-first memory infrastructure for AI agents.** Structured memory, 4-layer retrieval (BM25 + vector + graph + causal), token-budgeted injection, server-side write deduplication, and background consolidation. Fastify REST API + CLI.
|
|
4
|
+
|
|
5
|
+
This is the **server** half of the MemWeave stack. The complementary packages are:
|
|
6
|
+
|
|
7
|
+
- **[@mem-weave/mcp](https://www.npmjs.com/package/@mem-weave/mcp)** — stdio MCP server exposing 10 tools
|
|
8
|
+
- **[@mem-weave/opencode-plugin](https://www.npmjs.com/package/@mem-weave/opencode-plugin)** — OpenCode plugin that auto-injects memory + registers the MCP server
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @mem-weave/server
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. Initialize a config + data dir in the current directory
|
|
20
|
+
memweave init
|
|
21
|
+
|
|
22
|
+
# 2. Start the server (foreground)
|
|
23
|
+
memweave start
|
|
24
|
+
|
|
25
|
+
# Server listens on http://127.0.0.1:3131 by default.
|
|
26
|
+
# Health check:
|
|
27
|
+
curl http://127.0.0.1:3131/api/v1/health
|
|
28
|
+
# → {"ok":true,"service":"memweave-server"}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
In another terminal, point an MCP client (e.g. `@mem-weave/opencode-plugin`) at `http://127.0.0.1:3131`.
|
|
32
|
+
|
|
33
|
+
## CLI
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
memweave start Start the HTTP server + background workers (default)
|
|
37
|
+
memweave stop Stop a running memweave-server (via PID file)
|
|
38
|
+
memweave status Probe /api/v1/health
|
|
39
|
+
memweave init Create default config, DB, and device key
|
|
40
|
+
memweave doctor Check dependencies, port, DB, embedding/LLM config
|
|
41
|
+
memweave migrate Run schema migration (idempotent)
|
|
42
|
+
memweave backup [path] Copy the SQLite DB to a snapshot file
|
|
43
|
+
memweave help Show help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> Note: the `mcp` subcommand was removed in v0.2. Install `@mem-weave/mcp` and run its `memweave-mcp` bin instead.
|
|
47
|
+
|
|
48
|
+
## REST API
|
|
49
|
+
|
|
50
|
+
All routes are prefixed `/api/v1/`. Selected endpoints:
|
|
51
|
+
|
|
52
|
+
| Method | Path | Purpose |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `GET` | `/health` | Liveness check |
|
|
55
|
+
| `POST` | `/memories` | Create a memory (rate-limited per API key) |
|
|
56
|
+
| `POST` | `/memories/search` | 4-layer hybrid search; `mode: 'compact' \| 'full'` |
|
|
57
|
+
| `GET` | `/memories/:id` | Fetch one memory (full record, used by `memory_expand`) |
|
|
58
|
+
| `PATCH` | `/memories/:id` | Edit title / content / summary / importance / confidence |
|
|
59
|
+
| `DELETE` | `/memories/:id` | Soft-delete (`deleted_at`) with reason |
|
|
60
|
+
| `POST` | `/inject` | Token-budgeted summary XML for a session/phase |
|
|
61
|
+
| `GET` | `/stats` | Dashboard aggregates |
|
|
62
|
+
| `GET` | `/sessions` | Recent sessions with observation counts |
|
|
63
|
+
| `GET` | `/consolidation/runs` | Background "sleep" cycle history |
|
|
64
|
+
| `POST` | `/consolidate` | Manually trigger a consolidation run |
|
|
65
|
+
| `GET/POST/DELETE` | `/devices` | Device API key management |
|
|
66
|
+
| `GET` | `/settings` | Server config (secrets masked) |
|
|
67
|
+
|
|
68
|
+
## Programmatic use
|
|
69
|
+
|
|
70
|
+
The server is also a TypeScript library. Importing the HTTP bootstrap from `@mem-weave/server/dist/server/bootstrap.js` boots everything in-process. See `src/server/bootstrap.ts` for the entry point.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MemWeave CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* This file is the executable for the `memweave` bin command. It parses
|
|
6
|
+
* argv, dispatches to the right command, and prints the result.
|
|
7
|
+
*/
|
|
8
|
+
import { parseCli, runCli, CliError } from './cli.js';
|
|
9
|
+
async function main() {
|
|
10
|
+
let parsed;
|
|
11
|
+
try {
|
|
12
|
+
parsed = parseCli(process.argv);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (err instanceof CliError) {
|
|
16
|
+
// eslint-disable-next-line no-console
|
|
17
|
+
console.error(err.message);
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
const result = await runCli({
|
|
23
|
+
command: parsed.command,
|
|
24
|
+
args: parsed.args,
|
|
25
|
+
env: process.env,
|
|
26
|
+
configPath: process.env.MEMWEAVE_CONFIG
|
|
27
|
+
});
|
|
28
|
+
// Print message + data, and exit non-zero on failure
|
|
29
|
+
if (result.message) {
|
|
30
|
+
if (result.ok) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.log(result.message);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.error(result.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (result.data !== undefined) {
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
42
|
+
}
|
|
43
|
+
process.exit(result.ok ? 0 : 1);
|
|
44
|
+
}
|
|
45
|
+
main().catch((err) => {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.error('memweave: unexpected error:', err);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemWeave CLI 鈥?design spec 搂9.11.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands (all optional; running without args shows help):
|
|
5
|
+
*
|
|
6
|
+
* memweave start Start the HTTP server + background workers (default)
|
|
7
|
+
* memweave stop Stop a running memweave-server (via PID file)
|
|
8
|
+
* memweave status Probe /api/v1/health
|
|
9
|
+
* memweave init Create default config, DB, and device key
|
|
10
|
+
* memweave doctor Check dependencies, port, DB, embedding/LLM config
|
|
11
|
+
* memweave mcp Start the stdio MCP shim
|
|
12
|
+
* memweave migrate Run schema migration (idempotent)
|
|
13
|
+
* memweave backup [path] Copy the SQLite DB to a snapshot file
|
|
14
|
+
* memweave help Show help
|
|
15
|
+
*
|
|
16
|
+
* Note: this file is the *parser / dispatcher*. The actual logic for each
|
|
17
|
+
* command lives in `./commands/`. The CLI never edits `bootstrap.ts` 鈥?the
|
|
18
|
+
* default `start` command simply invokes the existing server bootstrap.
|
|
19
|
+
*/
|
|
20
|
+
import { runCommand } from './commands/index.js';
|
|
21
|
+
export function parseCli(argv) {
|
|
22
|
+
const [, , cmd, ...rest] = argv;
|
|
23
|
+
if (!cmd || cmd === '-h' || cmd === '--help')
|
|
24
|
+
return { command: 'help', args: [] };
|
|
25
|
+
if (cmd === '-v' || cmd === '--version' || cmd === 'version')
|
|
26
|
+
return { command: 'version', args: [] };
|
|
27
|
+
const known = ['start', 'stop', 'status', 'init', 'doctor', 'migrate', 'backup', 'help'];
|
|
28
|
+
if (cmd === 'mcp') {
|
|
29
|
+
throw new CliError('The `mcp` subcommand was removed in v0.2.\n' +
|
|
30
|
+
'Install the @mem-weave/mcp package and run its `memweave-mcp` bin instead:\n' +
|
|
31
|
+
' npm install -g @mem-weave/mcp\n' +
|
|
32
|
+
' memweave-mcp');
|
|
33
|
+
}
|
|
34
|
+
if (known.includes(cmd)) {
|
|
35
|
+
return { command: cmd, args: rest };
|
|
36
|
+
}
|
|
37
|
+
throw new CliError(`Unknown command: ${cmd}\nRun \`memweave help\` for usage.`);
|
|
38
|
+
}
|
|
39
|
+
export class CliError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'CliError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export async function runCli(invocation) {
|
|
46
|
+
const ctx = {
|
|
47
|
+
env: invocation.env,
|
|
48
|
+
args: invocation.args,
|
|
49
|
+
configPath: invocation.configPath
|
|
50
|
+
};
|
|
51
|
+
return runCommand(invocation.command, ctx);
|
|
52
|
+
}
|
|
53
|
+
export { runCommand } from './commands/index.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { mkdirSync } from 'node:fs';
|
|
4
|
+
import { loadConfig } from '../core/config.js';
|
|
5
|
+
import { expandPath } from '../core/config.js';
|
|
6
|
+
/**
|
|
7
|
+
* `memweave backup [path]` — copy the SQLite DB to a snapshot file.
|
|
8
|
+
*
|
|
9
|
+
* Uses SQLite's recommended "backup" semantics: a simple file copy. For
|
|
10
|
+
* very large DBs a proper `.backup` would be safer, but file copy is
|
|
11
|
+
* acceptable for v1 since we use WAL mode.
|
|
12
|
+
*/
|
|
13
|
+
export const backupCommand = async (ctx) => {
|
|
14
|
+
const config = loadConfig(ctx.configPath);
|
|
15
|
+
const dbPath = expandPath(config.storage.path);
|
|
16
|
+
if (!existsSync(dbPath)) {
|
|
17
|
+
return { ok: false, message: `Database file not found: ${dbPath}` };
|
|
18
|
+
}
|
|
19
|
+
const dest = ctx.args[0] ?? `${dbPath}.backup-${new Date().toISOString().replace(/[:.]/g, '-')}.db`;
|
|
20
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
21
|
+
copyFileSync(dbPath, dest);
|
|
22
|
+
const srcSize = statSync(dbPath).size;
|
|
23
|
+
return {
|
|
24
|
+
ok: true,
|
|
25
|
+
message: `Backed up ${dbPath} (${srcSize} bytes) to ${dest}`,
|
|
26
|
+
data: { source: dbPath, dest, size: srcSize }
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { loadConfig } from '../core/config.js';
|
|
5
|
+
import { expandPath } from '../core/config.js';
|
|
6
|
+
import { openDatabase } from '../db/database.js';
|
|
7
|
+
/**
|
|
8
|
+
* `memweave doctor` — environment / configuration health check.
|
|
9
|
+
*
|
|
10
|
+
* Checks:
|
|
11
|
+
* - config loads cleanly
|
|
12
|
+
* - DB file is reachable and the schema applies
|
|
13
|
+
* - port is free
|
|
14
|
+
* - sqlite-vec extension loads (when used)
|
|
15
|
+
* - LLM provider has a usable API key (when not noop)
|
|
16
|
+
* - Embedding provider has a usable config (when not noop)
|
|
17
|
+
*/
|
|
18
|
+
export const doctorCommand = async (ctx) => {
|
|
19
|
+
const results = [];
|
|
20
|
+
// 1. Config loads
|
|
21
|
+
let config;
|
|
22
|
+
try {
|
|
23
|
+
config = loadConfig(ctx.configPath);
|
|
24
|
+
results.push({ name: 'config', ok: true, detail: 'loaded' });
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { ok: false, message: 'Config load failed', data: { results, error: err.message } };
|
|
28
|
+
}
|
|
29
|
+
const dbPath = expandPath(config.storage.path);
|
|
30
|
+
// 2. DB open + schema
|
|
31
|
+
try {
|
|
32
|
+
const db = openDatabase(dbPath, { vectorDimensions: config.embedding.dimensions });
|
|
33
|
+
try {
|
|
34
|
+
const tables = db.prepare(`SELECT COUNT(*) as c FROM sqlite_master WHERE type='table'`).get();
|
|
35
|
+
results.push({ name: 'database', ok: true, detail: `${tables.c} tables; path=${dbPath}` });
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
db.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
results.push({ name: 'database', ok: false, detail: err.message });
|
|
43
|
+
}
|
|
44
|
+
// 3. Port available
|
|
45
|
+
try {
|
|
46
|
+
const probe = await fetch(`http://127.0.0.1:${config.server.port}/api/v1/health`, {
|
|
47
|
+
signal: AbortSignal.timeout(500)
|
|
48
|
+
});
|
|
49
|
+
if (probe.ok) {
|
|
50
|
+
results.push({ name: 'port', ok: true, detail: `port ${config.server.port} already serving (likely already running)` });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
results.push({ name: 'port', ok: true, detail: `port ${config.server.port} responding but not OK` });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
results.push({ name: 'port', ok: true, detail: `port ${config.server.port} appears free` });
|
|
58
|
+
}
|
|
59
|
+
// 4. sqlite-vec
|
|
60
|
+
try {
|
|
61
|
+
const db = openDatabase(dbPath, { vectorDimensions: config.embedding.dimensions });
|
|
62
|
+
try {
|
|
63
|
+
const v = db.prepare(`SELECT vec_version() AS v`).get();
|
|
64
|
+
results.push({ name: 'sqlite-vec', ok: true, detail: v.v });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
results.push({ name: 'sqlite-vec', ok: false, detail: err.message });
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
results.push({ name: 'sqlite-vec', ok: false, detail: 'extension failed to load' });
|
|
75
|
+
}
|
|
76
|
+
// 5. LLM
|
|
77
|
+
if (config.llm.provider === 'openai-compatible') {
|
|
78
|
+
const hasKey = Boolean(config.llm.apiKey);
|
|
79
|
+
results.push({ name: 'llm', ok: hasKey, detail: hasKey ? 'apiKey set' : 'apiKey missing' });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
results.push({ name: 'llm', ok: true, detail: 'noop' });
|
|
83
|
+
}
|
|
84
|
+
// 6. Embedding
|
|
85
|
+
if (config.embedding.provider === 'openai-compatible') {
|
|
86
|
+
const ok = Boolean(config.embedding.apiKey) && Boolean(config.embedding.baseUrl);
|
|
87
|
+
results.push({ name: 'embedding', ok, detail: ok ? 'configured' : 'apiKey or baseUrl missing' });
|
|
88
|
+
}
|
|
89
|
+
else if (config.embedding.provider === 'local-xenova') {
|
|
90
|
+
results.push({ name: 'embedding', ok: true, detail: 'local-xenova (stub in v1)' });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
results.push({ name: 'embedding', ok: true, detail: 'noop' });
|
|
94
|
+
}
|
|
95
|
+
// 7. PID file existence (informational)
|
|
96
|
+
const pidPath = join(tmpdir(), 'memweave.pid');
|
|
97
|
+
results.push({
|
|
98
|
+
name: 'pid-file',
|
|
99
|
+
ok: true,
|
|
100
|
+
detail: existsSync(pidPath) ? `${pidPath} present` : 'none (server not started via CLI)'
|
|
101
|
+
});
|
|
102
|
+
const allOk = results.every((r) => r.ok);
|
|
103
|
+
return {
|
|
104
|
+
ok: allOk,
|
|
105
|
+
message: allOk ? 'All checks passed.' : 'Some checks failed.',
|
|
106
|
+
data: { results }
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const HELP = `MemWeave — persistent cross-device memory for AI coding agents
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
memweave <command> [options]
|
|
5
|
+
|
|
6
|
+
Commands:
|
|
7
|
+
start Start the HTTP server + background workers
|
|
8
|
+
stop Stop a running memweave-server (via PID file)
|
|
9
|
+
status Probe /api/v1/health
|
|
10
|
+
init Create default config, DB, and device key
|
|
11
|
+
doctor Check dependencies, port, DB, embedding/LLM config
|
|
12
|
+
mcp Start the stdio MCP shim
|
|
13
|
+
migrate Apply schema (idempotent) and preview consolidation
|
|
14
|
+
backup [path] Copy the SQLite DB to a snapshot file
|
|
15
|
+
help Show this help
|
|
16
|
+
version Print version
|
|
17
|
+
|
|
18
|
+
Config (env or ~/.memweave/config.jsonc):
|
|
19
|
+
MEMWEAVE_CONFIG Path to config.jsonc
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
memweave init
|
|
23
|
+
memweave doctor
|
|
24
|
+
memweave start
|
|
25
|
+
memweave backup ~/backups/memweave-$(date +%Y%m%d).db
|
|
26
|
+
`;
|
|
27
|
+
export const helpCommand = async (_ctx) => {
|
|
28
|
+
return { ok: true, message: HELP };
|
|
29
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { startCommand } from './start.js';
|
|
2
|
+
import { stopCommand } from './stop.js';
|
|
3
|
+
import { statusCommand } from './status.js';
|
|
4
|
+
import { initCommand } from './init.js';
|
|
5
|
+
import { doctorCommand } from './doctor.js';
|
|
6
|
+
import { migrateCommand } from './migrate.js';
|
|
7
|
+
import { backupCommand } from './backup.js';
|
|
8
|
+
import { helpCommand } from './help.js';
|
|
9
|
+
import { versionCommand } from './version.js';
|
|
10
|
+
const handlers = {
|
|
11
|
+
start: startCommand,
|
|
12
|
+
stop: stopCommand,
|
|
13
|
+
status: statusCommand,
|
|
14
|
+
init: initCommand,
|
|
15
|
+
doctor: doctorCommand,
|
|
16
|
+
migrate: migrateCommand,
|
|
17
|
+
backup: backupCommand,
|
|
18
|
+
help: helpCommand,
|
|
19
|
+
version: versionCommand
|
|
20
|
+
};
|
|
21
|
+
export async function runCommand(command, ctx) {
|
|
22
|
+
const handler = handlers[command];
|
|
23
|
+
if (!handler) {
|
|
24
|
+
return { ok: false, message: `No handler for command: ${command}` };
|
|
25
|
+
}
|
|
26
|
+
return handler(ctx);
|
|
27
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
import { loadConfig } from '../core/config.js';
|
|
6
|
+
import { expandPath, expandEnv } from '../core/config.js';
|
|
7
|
+
import { openDatabase } from '../db/database.js';
|
|
8
|
+
const DEFAULT_CONFIG_JSONC = `{
|
|
9
|
+
// MemWeave config (JSONC: comments allowed)
|
|
10
|
+
"server": {
|
|
11
|
+
"host": "127.0.0.1",
|
|
12
|
+
"port": 3131
|
|
13
|
+
},
|
|
14
|
+
"storage": {
|
|
15
|
+
"path": "~/.memweave/data/memweave.db"
|
|
16
|
+
},
|
|
17
|
+
"auth": {
|
|
18
|
+
"defaultTenantName": "default",
|
|
19
|
+
"deviceApiKey": "REPLACE_WITH_RANDOM_KEY"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
/**
|
|
24
|
+
* `memweave init` — create the config dir, default config file (if missing),
|
|
25
|
+
* the SQLite DB, and a default tenant + device with a random API key.
|
|
26
|
+
*
|
|
27
|
+
* Idempotent: re-running `init` won't clobber existing state. It only
|
|
28
|
+
* creates things that don't already exist.
|
|
29
|
+
*/
|
|
30
|
+
export const initCommand = async (ctx) => {
|
|
31
|
+
const configDir = join(homedir(), '.memweave');
|
|
32
|
+
mkdirSync(configDir, { recursive: true });
|
|
33
|
+
mkdirSync(join(configDir, 'data'), { recursive: true });
|
|
34
|
+
const configPath = ctx.configPath ?? join(configDir, 'config.jsonc');
|
|
35
|
+
if (!existsSync(configPath)) {
|
|
36
|
+
const apiKey = randomBytes(24).toString('hex');
|
|
37
|
+
const body = DEFAULT_CONFIG_JSONC.replace('REPLACE_WITH_RANDOM_KEY', apiKey);
|
|
38
|
+
writeFileSync(configPath, body, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
const config = loadConfig(configPath);
|
|
41
|
+
const dbPath = expandPath(config.storage.path);
|
|
42
|
+
// Force env:// expansion in case the config uses placeholders
|
|
43
|
+
void expandEnv(config.auth.deviceApiKey);
|
|
44
|
+
// Open DB (applies schema) and ensure default tenant + device
|
|
45
|
+
const db = openDatabase(dbPath);
|
|
46
|
+
try {
|
|
47
|
+
db.prepare('INSERT OR IGNORE INTO tenants (id, name, api_key_hash, created_at) VALUES (?, ?, ?, ?)')
|
|
48
|
+
.run('tenant_default', config.auth.defaultTenantName, config.auth.deviceApiKey, Date.now());
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
db.close();
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
message: `Initialized memweave at ${configDir}`,
|
|
56
|
+
data: { configPath, dbPath }
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { loadConfig } from '../core/config.js';
|
|
2
|
+
import { expandPath } from '../core/config.js';
|
|
3
|
+
import { openDatabase } from '../db/database.js';
|
|
4
|
+
import { runConsolidation } from '../workers/consolidator.js';
|
|
5
|
+
/**
|
|
6
|
+
* `memweave migrate` — apply the schema (idempotent: CREATE TABLE IF NOT EXISTS)
|
|
7
|
+
* and run a one-shot consolidation pass.
|
|
8
|
+
*/
|
|
9
|
+
export const migrateCommand = async (ctx) => {
|
|
10
|
+
const config = loadConfig(ctx.configPath);
|
|
11
|
+
const dbPath = expandPath(config.storage.path);
|
|
12
|
+
const db = openDatabase(dbPath);
|
|
13
|
+
try {
|
|
14
|
+
const tables = db.prepare(`SELECT COUNT(*) as c FROM sqlite_master WHERE type='table'`).get();
|
|
15
|
+
const consolidation = runConsolidation(db, 'tenant_default', { dryRun: true });
|
|
16
|
+
return {
|
|
17
|
+
ok: true,
|
|
18
|
+
message: `Schema applied. ${tables.c} tables present.`,
|
|
19
|
+
data: { dbPath, tableCount: tables.c, consolidationPreview: consolidation }
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
db.close();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { loadConfig } from '../core/config.js';
|
|
2
|
+
import { expandPath } from '../core/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* `memweave start` — load config, open DB, start HTTP server + schedulers.
|
|
5
|
+
*
|
|
6
|
+
* This delegates to the same `createHttpServer` + `startConsolidationScheduler`
|
|
7
|
+
* used by the legacy `src/server/bootstrap.ts`, so behavior is identical.
|
|
8
|
+
*/
|
|
9
|
+
export const startCommand = async (ctx) => {
|
|
10
|
+
const config = loadConfig(ctx.configPath);
|
|
11
|
+
const dbPath = expandPath(config.storage.path);
|
|
12
|
+
// Lazy import to avoid pulling fastify in tests of the parser itself.
|
|
13
|
+
const { createHttpServer } = await import('../server/http.js');
|
|
14
|
+
const { startConsolidationScheduler } = await import('../server/scheduler.js');
|
|
15
|
+
const app = await createHttpServer({ dbPath });
|
|
16
|
+
if (config.consolidation.enabled) {
|
|
17
|
+
startConsolidationScheduler({
|
|
18
|
+
dbPath,
|
|
19
|
+
intervalMs: config.consolidation.intervalHours * 60 * 60 * 1000,
|
|
20
|
+
runOnStart: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
await app.listen({ host: config.server.host, port: config.server.port });
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
message: `memweave-server listening on ${config.server.host}:${config.server.port}`,
|
|
27
|
+
data: { host: config.server.host, port: config.server.port, dbPath }
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { loadConfig } from '../core/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* `memweave status` — probe the running memweave-server's health endpoint.
|
|
4
|
+
*/
|
|
5
|
+
export const statusCommand = async (ctx) => {
|
|
6
|
+
const config = loadConfig(ctx.configPath);
|
|
7
|
+
const url = `http://${config.server.host}:${config.server.port}/api/v1/health`;
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3000) });
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
return { ok: false, message: `Health check returned ${res.status}` };
|
|
12
|
+
}
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
return { ok: true, message: 'memweave-server is up.', data };
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return { ok: false, message: `Cannot reach ${url}: ${err.message}` };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { loadConfig } from '../core/config.js';
|
|
5
|
+
import { expandPath } from '../core/config.js';
|
|
6
|
+
const PID_FILENAME = 'memweave.pid';
|
|
7
|
+
/**
|
|
8
|
+
* `memweave stop` — read PID file, send SIGTERM, wait, SIGKILL fallback.
|
|
9
|
+
*/
|
|
10
|
+
export const stopCommand = async (ctx) => {
|
|
11
|
+
const config = loadConfig(ctx.configPath);
|
|
12
|
+
const dbPath = expandPath(config.storage.path);
|
|
13
|
+
const pidPath = join(tmpdir(), PID_FILENAME);
|
|
14
|
+
if (!existsSync(pidPath)) {
|
|
15
|
+
return { ok: true, message: 'No PID file found; nothing to stop.', data: { pidPath } };
|
|
16
|
+
}
|
|
17
|
+
const pid = Number.parseInt(readFileSync(pidPath, 'utf8').trim(), 10);
|
|
18
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
19
|
+
return { ok: false, message: `Invalid PID file: ${pidPath}` };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 'SIGTERM');
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
return { ok: false, message: `Failed to send SIGTERM to PID ${pid}: ${err.message}` };
|
|
26
|
+
}
|
|
27
|
+
// Give it a moment to exit
|
|
28
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
// Still alive, escalate
|
|
32
|
+
process.kill(pid, 'SIGKILL');
|
|
33
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Already dead
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(pidPath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
void dbPath;
|
|
45
|
+
return { ok: true, message: `Sent SIGTERM/SIGKILL to PID ${pid}`, data: { pid } };
|
|
46
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const PACKAGE_NAME = 'memweave';
|
|
4
|
+
export const versionCommand = async (_ctx) => {
|
|
5
|
+
// Read the package.json of this workspace
|
|
6
|
+
let version = 'unknown';
|
|
7
|
+
try {
|
|
8
|
+
// The compiled dist layout puts package.json at the workspace root;
|
|
9
|
+
// tests are run from the workspace root, so process.cwd() works.
|
|
10
|
+
const pkgPath = join(process.cwd(), 'package.json');
|
|
11
|
+
if (existsSync(pkgPath)) {
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
13
|
+
if (pkg.name === PACKAGE_NAME && pkg.version)
|
|
14
|
+
version = pkg.version;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// ignore
|
|
19
|
+
}
|
|
20
|
+
return { ok: true, message: `${PACKAGE_NAME} ${version}` };
|
|
21
|
+
};
|