total-agent-memory 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vitalii Cherepanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # total-agent-memory
2
+
3
+ > Persistent memory for Claude Code, Codex CLI, Cursor, Cline and any MCP client.
4
+ > Local-first. Hits **#5 on LoCoMo** and **96.2% R@5 on LongMemEval**.
5
+
6
+ ```bash
7
+ npx -y total-agent-memory
8
+ ```
9
+
10
+ That's it. One command bootstraps a Python virtual env in `~/.claude-memory/`,
11
+ installs the MCP server, and prints next-step instructions for wiring your IDE.
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ # Onboard (install + show next steps)
17
+ npx -y total-agent-memory
18
+
19
+ # Or explicit
20
+ npx -y total-agent-memory install
21
+ npx -y total-agent-memory connect claude-code
22
+ npx -y total-agent-memory connect cursor
23
+ npx -y total-agent-memory connect codex
24
+ npx -y total-agent-memory status
25
+ ```
26
+
27
+ Alias: `tam` (same binary).
28
+
29
+ ## Commands
30
+
31
+ | Command | What it does |
32
+ |---|---|
33
+ | `install` | Create venv, install Python server. Idempotent. |
34
+ | `start` | Run the MCP server (stdin/stdout). |
35
+ | `connect <ide>` | Wire an IDE config. `claude-code` `codex` `cursor` `cline` `continue` `windsurf` `gemini-cli` `opencode` `aider` |
36
+ | `demo` | Health-check end-to-end. |
37
+ | `status` | Print install paths, DB size. |
38
+ | `doctor` | Diagnose Python/uv/git on PATH. |
39
+ | `upgrade` | Pull the latest server version. |
40
+ | `uninstall` | Remove `~/.claude-memory` (needs `--yes`). |
41
+
42
+ ## Options
43
+
44
+ | Flag | Meaning |
45
+ |---|---|
46
+ | `--server-version 11.2.2` | Pin a specific server release. |
47
+ | `--source pypi\|git` | Force install source. Default: PyPI with GitHub fallback. |
48
+ | `--memory-dir /path` | Override install dir (default `~/.claude-memory`). |
49
+ | `--yes` | Skip prompts (needed for `uninstall`). |
50
+ | `--no-color` | Disable ANSI. |
51
+ | `--verbose` | Print every shell command. |
52
+
53
+ ## Requirements
54
+
55
+ - **Node ≥ 18** (you already have it if you're reading this).
56
+ - **Python ≥ 3.10** on PATH. The wrapper will tell you how to install it if missing.
57
+ - Optional: **[uv](https://docs.astral.sh/uv/)** — 10× faster installs, auto-detected.
58
+
59
+ ## What gets installed
60
+
61
+ ```
62
+ ~/.claude-memory/
63
+ ├── .venv/ # isolated Python env
64
+ │ └── bin/claude-total-memory # MCP server entry-point
65
+ └── memory.db # SQLite — your memory lives here
66
+ ```
67
+
68
+ Nothing global is touched. Uninstall with `tam uninstall --yes`.
69
+
70
+ ## Links
71
+
72
+ - Site: <https://totalmemory.dev>
73
+ - Server source: <https://github.com/vbcherepanov/total-agent-memory>
74
+ - PyPI package: `claude-total-memory`
75
+
76
+ ## License
77
+
78
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // total-agent-memory CLI — Node wrapper around the Python MCP server.
3
+ // Zero runtime dependencies, fast cold start, works on macOS / Linux / Windows.
4
+
5
+ 'use strict';
6
+
7
+ const { parseArgs } = require('../lib/args');
8
+ const { c, banner, dim, bold } = require('../lib/style');
9
+ const cmds = require('../lib/commands');
10
+
11
+ const VERSION = require('../package.json').version;
12
+
13
+ const HELP = `${banner()}
14
+
15
+ ${bold('Usage:')} total-agent-memory <command> [options]
16
+ ${dim('alias:')} tam <command> [options]
17
+
18
+ ${bold('Commands:')}
19
+ install Install Python server into ~/.claude-memory (idempotent)
20
+ start Start the MCP server (default if no command)
21
+ connect <ide> Wire memory into an IDE config
22
+ ${dim('claude-code | codex | cursor | cline | continue')}
23
+ ${dim('aider | windsurf | gemini-cli | opencode')}
24
+ demo Seed demo session + run a recall to prove it works
25
+ status Health check + tool count + DB stats
26
+ doctor Diagnose environment (python, uv, ports, configs)
27
+ upgrade Upgrade to the latest server version
28
+ uninstall Remove ~/.claude-memory and IDE wirings
29
+ version Print version
30
+ help Show this message
31
+
32
+ ${bold('Options:')}
33
+ --version, -v Print npm wrapper version
34
+ --server-version <v> Pin server version (default: latest)
35
+ --source <pypi|git> Where to install from (default: auto)
36
+ --memory-dir <path> Override install dir (default: ~/.claude-memory)
37
+ --yes, -y Skip prompts
38
+ --no-color Disable ANSI colors
39
+ --verbose Print every shell command
40
+
41
+ ${bold('Examples:')}
42
+ ${c.dim('# Install and start, then wire Claude Code:')}
43
+ npx -y total-agent-memory install
44
+ npx -y total-agent-memory connect claude-code
45
+
46
+ ${c.dim('# All-in-one onboarding:')}
47
+ npx -y total-agent-memory
48
+
49
+ ${c.dim('# Pin a specific server version:')}
50
+ npx -y total-agent-memory install --server-version 11.2.2
51
+
52
+ ${bold('Docs:')} https://totalmemory.dev
53
+ ${bold('Repo:')} https://github.com/vbcherepanov/total-agent-memory
54
+ `;
55
+
56
+ async function main() {
57
+ const argv = process.argv.slice(2);
58
+ const parsed = parseArgs(argv);
59
+
60
+ if (parsed.flags.version || parsed.flags.v) {
61
+ process.stdout.write(`${VERSION}\n`);
62
+ return 0;
63
+ }
64
+ if (parsed.command === 'version') {
65
+ process.stdout.write(`total-agent-memory v${VERSION}\n`);
66
+ return 0;
67
+ }
68
+ if (parsed.command === 'help' || parsed.flags.help || parsed.flags.h) {
69
+ process.stdout.write(HELP);
70
+ return 0;
71
+ }
72
+
73
+ const ctx = {
74
+ flags: parsed.flags,
75
+ positional: parsed.positional,
76
+ wrapperVersion: VERSION,
77
+ };
78
+
79
+ const cmd = parsed.command || 'onboard';
80
+ const fn = cmds[cmd];
81
+ if (!fn) {
82
+ process.stderr.write(`${c.red('error')}: unknown command "${cmd}"\n`);
83
+ process.stderr.write(HELP);
84
+ return 2;
85
+ }
86
+ return await fn(ctx);
87
+ }
88
+
89
+ main()
90
+ .then((code) => process.exit(code || 0))
91
+ .catch((err) => {
92
+ process.stderr.write(`${c.red('fatal')}: ${err && err.message ? err.message : err}\n`);
93
+ if (process.env.TAM_DEBUG || process.argv.includes('--verbose')) {
94
+ process.stderr.write(`${err && err.stack ? err.stack : ''}\n`);
95
+ }
96
+ process.exit(1);
97
+ });
package/lib/args.js ADDED
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ // Minimal CLI parser. Supports:
4
+ // <command> <positional...>
5
+ // --flag, --flag=value, --flag value
6
+ // -shortflag, -shortflag value
7
+ // bare positionals collected in order.
8
+
9
+ const FLAGS_WITH_VALUE = new Set([
10
+ '--server-version',
11
+ '--source',
12
+ '--memory-dir',
13
+ '--ide',
14
+ '--port',
15
+ '--log-level',
16
+ ]);
17
+
18
+ function normalize(name) {
19
+ return name.replace(/^-+/, '');
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const flags = {};
24
+ const positional = [];
25
+ let command = null;
26
+
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const tok = argv[i];
29
+ if (!tok) continue;
30
+ if (tok === '--') {
31
+ positional.push(...argv.slice(i + 1));
32
+ break;
33
+ }
34
+ if (tok.startsWith('--')) {
35
+ const eq = tok.indexOf('=');
36
+ let key, val;
37
+ if (eq !== -1) {
38
+ key = tok.slice(0, eq);
39
+ val = tok.slice(eq + 1);
40
+ } else {
41
+ key = tok;
42
+ if (FLAGS_WITH_VALUE.has(key) && argv[i + 1] && !argv[i + 1].startsWith('-')) {
43
+ val = argv[++i];
44
+ } else {
45
+ val = true;
46
+ }
47
+ }
48
+ flags[normalize(key)] = val;
49
+ continue;
50
+ }
51
+ if (tok.startsWith('-') && tok.length > 1) {
52
+ // Short flag(s). We treat each separately and they take no value
53
+ // except the well-known -v/--version mapping.
54
+ const short = tok.slice(1);
55
+ flags[short] = true;
56
+ continue;
57
+ }
58
+ if (command === null) {
59
+ command = tok;
60
+ } else {
61
+ positional.push(tok);
62
+ }
63
+ }
64
+
65
+ // Normalize aliases.
66
+ if (flags.y) flags.yes = true;
67
+ if (flags.h) flags.help = true;
68
+
69
+ return { command, positional, flags };
70
+ }
71
+
72
+ module.exports = { parseArgs };
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { spawn } = require('child_process');
6
+ const { install, venvBin, pinned } = require('./install');
7
+ const { connect, SUPPORTED } = require('./ide');
8
+ const { diagnose, printReport, printRemediation } = require('./doctor');
9
+ const { memoryDir, venvDir, exists, home } = require('./paths');
10
+ const { run, runOrThrow } = require('./sh');
11
+ const { c, bold, ok, fail, warn, info, banner, step } = require('./style');
12
+
13
+ async function ensureInstalled(ctx) {
14
+ const bin = venvBin(ctx.flags, 'claude-total-memory');
15
+ if (exists(bin)) return bin;
16
+ process.stdout.write(`${info('Server not installed yet — installing now…')}\n`);
17
+ const code = await install(ctx);
18
+ if (code !== 0) throw new Error('install failed');
19
+ return bin;
20
+ }
21
+
22
+ // ────────────────────────────────────────────────────────────────────────
23
+
24
+ async function cmdInstall(ctx) {
25
+ process.stdout.write(banner());
26
+ return await install(ctx);
27
+ }
28
+
29
+ async function cmdStart(ctx) {
30
+ const bin = await ensureInstalled(ctx);
31
+ process.stdout.write(`${info(`Starting MCP server: ${c.dim(bin)}`)}\n`);
32
+ // Replace this Node process with the server (transparent pass-through).
33
+ const child = spawn(bin, [], {
34
+ stdio: 'inherit',
35
+ env: { ...process.env, MEMORY_MODE: process.env.MEMORY_MODE || 'fast' },
36
+ });
37
+ return await new Promise((resolve) => child.on('close', (code) => resolve(code || 0)));
38
+ }
39
+
40
+ async function cmdConnect(ctx) {
41
+ const ide = ctx.positional[0];
42
+ if (!ide) {
43
+ process.stderr.write(`${fail('connect requires an IDE name')}\n`);
44
+ process.stderr.write(`Supported: ${SUPPORTED.join(', ')}\n`);
45
+ return 2;
46
+ }
47
+ await ensureInstalled(ctx);
48
+ try {
49
+ const { file, manual } = connect(ide, ctx.flags);
50
+ if (manual) {
51
+ process.stdout.write(`${warn(`Manual wiring required for ${ide}: ${file}`)}\n`);
52
+ return 0;
53
+ }
54
+ process.stdout.write(banner());
55
+ process.stdout.write(`${ok(`Wired ${c.bold(ide)} → ${c.dim(file)}`)}\n`);
56
+ process.stdout.write(`${info('Restart your IDE for the change to take effect.')}\n`);
57
+ return 0;
58
+ } catch (e) {
59
+ process.stderr.write(`${fail(e.message)}\n`);
60
+ return 2;
61
+ }
62
+ }
63
+
64
+ async function cmdDemo(ctx) {
65
+ const bin = await ensureInstalled(ctx);
66
+ process.stdout.write(banner());
67
+ process.stdout.write(`${info('Demo: seed a session + recall it.')}\n`);
68
+ // We use the lookup-memory CLI shipped in the same venv to drive a
69
+ // round-trip without needing an MCP client.
70
+ const cli = path.join(path.dirname(bin), process.platform === 'win32' ? 'lookup-memory.exe' : 'lookup-memory');
71
+ if (!exists(cli)) {
72
+ process.stdout.write(`${warn('lookup-memory CLI not found in venv — open an MCP client to drive the server.')}\n`);
73
+ return 0;
74
+ }
75
+ process.stdout.write(`${c.dim('$ lookup-memory --help')}\n`);
76
+ await run(cli, ['--help']);
77
+ process.stdout.write(`\n${ok('Server is healthy. Connect your IDE next:')}\n`);
78
+ process.stdout.write(` ${c.cyan('total-agent-memory connect claude-code')}\n`);
79
+ return 0;
80
+ }
81
+
82
+ async function cmdStatus(ctx) {
83
+ const env = await diagnose();
84
+ process.stdout.write(banner());
85
+ printReport(env);
86
+ const dir = memoryDir(ctx.flags);
87
+ const venv = venvDir(ctx.flags);
88
+ const bin = venvBin(ctx.flags, 'claude-total-memory');
89
+ const db = path.join(dir, 'memory.db');
90
+ process.stdout.write(`\n${bold('Install')}\n`);
91
+ process.stdout.write(` ${exists(dir) ? ok(`memory dir : ${dir}`) : warn(`memory dir : ${dir} (missing)`)}\n`);
92
+ process.stdout.write(` ${exists(venv) ? ok(`venv : ${venv}`) : warn(`venv : ${venv} (missing)`)}\n`);
93
+ process.stdout.write(` ${exists(bin) ? ok(`server bin : ${bin}`) : warn(`server bin : ${bin} (missing)`)}\n`);
94
+ if (exists(db)) {
95
+ const sz = (fs.statSync(db).size / 1024 / 1024).toFixed(2);
96
+ process.stdout.write(` ${ok(`memory.db : ${sz} MB`)}\n`);
97
+ } else {
98
+ process.stdout.write(` ${warn(`memory.db : ${db} (will be created on first save)`)}\n`);
99
+ }
100
+ return 0;
101
+ }
102
+
103
+ async function cmdDoctor(ctx) {
104
+ const env = await diagnose();
105
+ process.stdout.write(banner());
106
+ printReport(env);
107
+ printRemediation(env);
108
+ return 0;
109
+ }
110
+
111
+ async function cmdUpgrade(ctx) {
112
+ process.stdout.write(banner());
113
+ process.stdout.write(`${info('Upgrading server to the latest version…')}\n`);
114
+ // Force install with --server-version=latest.
115
+ const upgradeCtx = { ...ctx, flags: { ...ctx.flags, 'server-version': 'latest' } };
116
+ return await install(upgradeCtx);
117
+ }
118
+
119
+ async function cmdUninstall(ctx) {
120
+ process.stdout.write(banner());
121
+ const dir = memoryDir(ctx.flags);
122
+ if (!exists(dir)) {
123
+ process.stdout.write(`${ok('Nothing to remove — directory does not exist.')}\n`);
124
+ return 0;
125
+ }
126
+ process.stdout.write(`${warn(`About to remove ${dir}.`)}\n`);
127
+ if (!ctx.flags.yes) {
128
+ process.stdout.write(`${info('Re-run with --yes to confirm.')}\n`);
129
+ return 0;
130
+ }
131
+ fs.rmSync(dir, { recursive: true, force: true });
132
+ process.stdout.write(`${ok('Removed.')}\n`);
133
+ process.stdout.write(`${info('IDE configs were left intact. Edit ~/.claude.json / ~/.cursor/mcp.json / etc. manually to drop the MCP entry.')}\n`);
134
+ return 0;
135
+ }
136
+
137
+ // First-run onboarding: install + suggest a connect step.
138
+ async function cmdOnboard(ctx) {
139
+ process.stdout.write(banner());
140
+ const env = await diagnose();
141
+ printReport(env);
142
+ if (!env.python3) {
143
+ printRemediation(env);
144
+ return 1;
145
+ }
146
+ process.stdout.write(`\n${bold('Quick setup')}\n`);
147
+ process.stdout.write(` ${step(1, 2, 'install server')}\n`);
148
+ const code = await install(ctx);
149
+ if (code !== 0) return code;
150
+ process.stdout.write(` ${step(2, 2, 'next: wire an IDE')}\n\n`);
151
+ process.stdout.write(` ${c.cyan('total-agent-memory connect claude-code')}\n`);
152
+ process.stdout.write(` ${c.cyan('total-agent-memory connect cursor')}\n`);
153
+ process.stdout.write(` ${c.cyan('total-agent-memory connect codex')}\n`);
154
+ process.stdout.write(`\n${c.dim('Docs: https://totalmemory.dev')}\n`);
155
+ return 0;
156
+ }
157
+
158
+ module.exports = {
159
+ install: cmdInstall,
160
+ start: cmdStart,
161
+ connect: cmdConnect,
162
+ demo: cmdDemo,
163
+ status: cmdStatus,
164
+ doctor: cmdDoctor,
165
+ upgrade: cmdUpgrade,
166
+ uninstall: cmdUninstall,
167
+ onboard: cmdOnboard,
168
+ };
package/lib/doctor.js ADDED
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const { probe, which } = require('./sh');
4
+ const { exists } = require('./paths');
5
+ const { c, bold, ok, fail, warn, info } = require('./style');
6
+
7
+ // Returns environment summary.
8
+ async function diagnose() {
9
+ const env = {
10
+ platform: process.platform,
11
+ arch: process.arch,
12
+ node: process.version,
13
+ python: null,
14
+ python3: null,
15
+ uv: null,
16
+ pip: null,
17
+ pipx: null,
18
+ git: null,
19
+ };
20
+
21
+ for (const bin of ['python3', 'python', 'uv', 'pipx', 'git']) {
22
+ const r = probe(bin, ['--version']);
23
+ if (r.ok) {
24
+ env[bin] = { path: r.path, version: r.out };
25
+ }
26
+ }
27
+ if (env.python3 && env.python3.version) {
28
+ const m = env.python3.version.match(/(\d+)\.(\d+)\.(\d+)/);
29
+ if (m) {
30
+ env.pythonMajor = +m[1];
31
+ env.pythonMinor = +m[2];
32
+ env.pythonPatch = +m[3];
33
+ }
34
+ }
35
+ return env;
36
+ }
37
+
38
+ function pickInstaller(env) {
39
+ // Prefer uv (fastest, most reliable). Fall back to venv+pip.
40
+ if (env.uv) return 'uv';
41
+ if (env.python3 && env.pythonMajor >= 3 && env.pythonMinor >= 10) return 'venv-pip';
42
+ return null;
43
+ }
44
+
45
+ function printReport(env) {
46
+ process.stdout.write(`\n${bold('Environment')}\n`);
47
+ process.stdout.write(` ${info(`platform : ${env.platform}/${env.arch}`)}\n`);
48
+ process.stdout.write(` ${info(`node : ${env.node}`)}\n`);
49
+ if (env.python3) {
50
+ process.stdout.write(` ${ok(`python3 : ${env.python3.version} ${c.dim('(' + env.python3.path + ')')}`)}\n`);
51
+ } else {
52
+ process.stdout.write(` ${fail('python3 : not found')}\n`);
53
+ }
54
+ if (env.uv) {
55
+ process.stdout.write(` ${ok(`uv : ${env.uv.version} ${c.dim('(' + env.uv.path + ')')}`)}\n`);
56
+ } else {
57
+ process.stdout.write(` ${warn('uv : not found (optional but recommended — 10× faster installs)')}\n`);
58
+ }
59
+ if (env.git) {
60
+ process.stdout.write(` ${ok(`git : ${env.git.version}`)}\n`);
61
+ } else {
62
+ process.stdout.write(` ${warn('git : not found (needed only for --source git fallback)')}\n`);
63
+ }
64
+ return env;
65
+ }
66
+
67
+ function printRemediation(env) {
68
+ if (!env.python3) {
69
+ process.stdout.write(`\n${bold('Install Python 3.10+:')}\n`);
70
+ if (process.platform === 'darwin') {
71
+ process.stdout.write(` ${c.cyan('brew install python@3.12')}\n`);
72
+ } else if (process.platform === 'linux') {
73
+ process.stdout.write(` ${c.cyan('sudo apt install -y python3 python3-venv python3-pip')} ${c.dim('# Debian/Ubuntu')}\n`);
74
+ process.stdout.write(` ${c.cyan('sudo dnf install -y python3 python3-pip')} ${c.dim('# Fedora/RHEL')}\n`);
75
+ } else if (process.platform === 'win32') {
76
+ process.stdout.write(` ${c.cyan('winget install Python.Python.3.12')}\n`);
77
+ }
78
+ }
79
+ if (env.python3 && !env.uv) {
80
+ process.stdout.write(`\n${bold('Optional — install uv for 10× faster installs:')}\n`);
81
+ process.stdout.write(` ${c.cyan('curl -LsSf https://astral.sh/uv/install.sh | sh')}\n`);
82
+ }
83
+ }
84
+
85
+ module.exports = { diagnose, pickInstaller, printReport, printRemediation };
package/lib/ide.js ADDED
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { home, memoryDir, venvBin, exists, readJSONSafe, writeJSON, ensureDir } = require('./paths');
6
+ const { c, ok, info, warn } = require('./style');
7
+
8
+ const SUPPORTED = [
9
+ 'claude-code',
10
+ 'codex',
11
+ 'cursor',
12
+ 'cline',
13
+ 'continue',
14
+ 'aider',
15
+ 'windsurf',
16
+ 'gemini-cli',
17
+ 'opencode',
18
+ ];
19
+
20
+ function mcpEntry(flags) {
21
+ const bin = venvBin(flags, 'claude-total-memory');
22
+ return {
23
+ command: bin,
24
+ args: [],
25
+ env: {
26
+ MEMORY_MODE: 'fast',
27
+ },
28
+ };
29
+ }
30
+
31
+ function claudeCodePath() {
32
+ return path.join(home(), '.claude.json');
33
+ }
34
+
35
+ function cursorPath() {
36
+ return path.join(home(), '.cursor', 'mcp.json');
37
+ }
38
+
39
+ function clinePath() {
40
+ // Cline reads VSCode global state, but for portability we use its public
41
+ // config file location when present.
42
+ return path.join(home(), '.cline', 'mcp.json');
43
+ }
44
+
45
+ function continuePath() {
46
+ return path.join(home(), '.continue', 'config.json');
47
+ }
48
+
49
+ function codexPath() {
50
+ return path.join(home(), '.codex', 'config.toml');
51
+ }
52
+
53
+ function geminiPath() {
54
+ return path.join(home(), '.gemini', 'settings.json');
55
+ }
56
+
57
+ function windsurfPath() {
58
+ return path.join(home(), '.codeium', 'windsurf', 'mcp_config.json');
59
+ }
60
+
61
+ function aiderPath() {
62
+ return path.join(home(), '.aider.conf.yml');
63
+ }
64
+
65
+ function opencodePath() {
66
+ return path.join(home(), '.config', 'opencode', 'config.json');
67
+ }
68
+
69
+ function mergeMcpServers(file, name, entry) {
70
+ const cfg = readJSONSafe(file) || {};
71
+ cfg.mcpServers = cfg.mcpServers || {};
72
+ cfg.mcpServers[name] = entry;
73
+ writeJSON(file, cfg);
74
+ }
75
+
76
+ function wireClaudeCode(flags) {
77
+ mergeMcpServers(claudeCodePath(), 'total-agent-memory', mcpEntry(flags));
78
+ return claudeCodePath();
79
+ }
80
+ function wireCursor(flags) {
81
+ ensureDir(path.dirname(cursorPath()));
82
+ mergeMcpServers(cursorPath(), 'total-agent-memory', mcpEntry(flags));
83
+ return cursorPath();
84
+ }
85
+ function wireCline(flags) {
86
+ ensureDir(path.dirname(clinePath()));
87
+ mergeMcpServers(clinePath(), 'total-agent-memory', mcpEntry(flags));
88
+ return clinePath();
89
+ }
90
+ function wireContinue(flags) {
91
+ ensureDir(path.dirname(continuePath()));
92
+ const cfg = readJSONSafe(continuePath()) || {};
93
+ cfg.mcpServers = cfg.mcpServers || [];
94
+ cfg.mcpServers = cfg.mcpServers.filter((s) => s.name !== 'total-agent-memory');
95
+ cfg.mcpServers.push({ name: 'total-agent-memory', ...mcpEntry(flags) });
96
+ writeJSON(continuePath(), cfg);
97
+ return continuePath();
98
+ }
99
+ function wireWindsurf(flags) {
100
+ ensureDir(path.dirname(windsurfPath()));
101
+ mergeMcpServers(windsurfPath(), 'total-agent-memory', mcpEntry(flags));
102
+ return windsurfPath();
103
+ }
104
+ function wireGeminiCLI(flags) {
105
+ ensureDir(path.dirname(geminiPath()));
106
+ mergeMcpServers(geminiPath(), 'total-agent-memory', mcpEntry(flags));
107
+ return geminiPath();
108
+ }
109
+ function wireOpenCode(flags) {
110
+ ensureDir(path.dirname(opencodePath()));
111
+ mergeMcpServers(opencodePath(), 'total-agent-memory', mcpEntry(flags));
112
+ return opencodePath();
113
+ }
114
+ function wireCodex(flags) {
115
+ ensureDir(path.dirname(codexPath()));
116
+ const bin = venvBin(flags, 'claude-total-memory');
117
+ const block = `
118
+
119
+ [mcp_servers.total-agent-memory]
120
+ command = ${JSON.stringify(bin)}
121
+ args = []
122
+ env = { MEMORY_MODE = "fast" }
123
+ `;
124
+ let prev = '';
125
+ try { prev = fs.readFileSync(codexPath(), 'utf8'); } catch (_) { /* fresh */ }
126
+ if (prev.includes('[mcp_servers.total-agent-memory]')) {
127
+ return codexPath(); // already wired
128
+ }
129
+ fs.writeFileSync(codexPath(), prev + block);
130
+ return codexPath();
131
+ }
132
+ function wireAider() {
133
+ return aiderPath() + ' (manual: aider does not yet support MCP; use lookup-memory CLI instead)';
134
+ }
135
+
136
+ function connect(ide, flags) {
137
+ switch (ide) {
138
+ case 'claude-code': return { file: wireClaudeCode(flags) };
139
+ case 'codex': return { file: wireCodex(flags) };
140
+ case 'cursor': return { file: wireCursor(flags) };
141
+ case 'cline': return { file: wireCline(flags) };
142
+ case 'continue': return { file: wireContinue(flags) };
143
+ case 'windsurf': return { file: wireWindsurf(flags) };
144
+ case 'gemini-cli': return { file: wireGeminiCLI(flags) };
145
+ case 'opencode': return { file: wireOpenCode(flags) };
146
+ case 'aider': return { file: wireAider(flags), manual: true };
147
+ default: throw new Error(`unsupported IDE: ${ide}. Supported: ${SUPPORTED.join(', ')}`);
148
+ }
149
+ }
150
+
151
+ module.exports = { SUPPORTED, connect, mcpEntry };
package/lib/index.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ // Library facade so users can `require('total-agent-memory')` and drive
4
+ // the wrapper from JS as well. Useful for postinstall scripts and CI.
5
+
6
+ const cmds = require('./commands');
7
+ const { diagnose } = require('./doctor');
8
+ const { mcpEntry, SUPPORTED } = require('./ide');
9
+ const { memoryDir, venvDir, venvBin } = require('./paths');
10
+ const { install, pinned } = require('./install');
11
+
12
+ module.exports = {
13
+ ...cmds,
14
+ diagnose,
15
+ mcpEntry,
16
+ SUPPORTED_IDES: SUPPORTED,
17
+ memoryDir,
18
+ venvDir,
19
+ venvBin,
20
+ installServer: install,
21
+ pinnedVersion: pinned,
22
+ };
package/lib/install.js ADDED
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { runOrThrow, run, which } = require('./sh');
6
+ const { memoryDir, venvDir, venvBin, ensureDir, exists } = require('./paths');
7
+ const { diagnose, pickInstaller, printReport, printRemediation } = require('./doctor');
8
+ const { c, bold, ok, fail, warn, info, step } = require('./style');
9
+
10
+ const PYPI_PACKAGE = 'claude-total-memory';
11
+ // Pinned to the latest stable release at the time of the wrapper release.
12
+ // Override via `--server-version` or env TAM_SERVER_VERSION.
13
+ const DEFAULT_SERVER_VERSION = '11.2.2';
14
+ const GIT_URL = 'https://github.com/vbcherepanov/total-agent-memory.git';
15
+
16
+ function pinned(flags) {
17
+ return (
18
+ (flags && flags['server-version']) ||
19
+ process.env.TAM_SERVER_VERSION ||
20
+ DEFAULT_SERVER_VERSION
21
+ );
22
+ }
23
+
24
+ function specPyPI(flags) {
25
+ const v = pinned(flags);
26
+ return v && v !== 'latest' ? `${PYPI_PACKAGE}==${v}` : PYPI_PACKAGE;
27
+ }
28
+
29
+ function specGit(flags) {
30
+ const v = pinned(flags);
31
+ const tag = v && v !== 'latest' ? `@v${v}` : '';
32
+ return `git+${GIT_URL}${tag}`;
33
+ }
34
+
35
+ async function pypiHas(version) {
36
+ // We can't import https here cheaply — use built-in for HEAD.
37
+ return await new Promise((resolve) => {
38
+ const https = require('https');
39
+ const url = version
40
+ ? `https://pypi.org/pypi/${PYPI_PACKAGE}/${version}/json`
41
+ : `https://pypi.org/pypi/${PYPI_PACKAGE}/json`;
42
+ const req = https.get(url, (res) => {
43
+ // Drain.
44
+ res.on('data', () => {});
45
+ res.on('end', () => resolve(res.statusCode === 200));
46
+ });
47
+ req.on('error', () => resolve(false));
48
+ req.setTimeout(4000, () => { req.destroy(); resolve(false); });
49
+ });
50
+ }
51
+
52
+ async function pickSource(flags) {
53
+ const explicit = flags && flags.source;
54
+ if (explicit === 'pypi' || explicit === 'git') return explicit;
55
+ // Auto: try PyPI first, fall back to git.
56
+ if (await pypiHas(pinned(flags))) return 'pypi';
57
+ return 'git';
58
+ }
59
+
60
+ async function createVenv(flags, env) {
61
+ const venv = venvDir(flags);
62
+ if (exists(path.join(venv, process.platform === 'win32' ? 'Scripts' : 'bin'))) {
63
+ return;
64
+ }
65
+ ensureDir(memoryDir(flags));
66
+
67
+ if (env.uv) {
68
+ await runOrThrow('uv', ['venv', venv, '--python', '3.12'], { silent: true });
69
+ return;
70
+ }
71
+ await runOrThrow('python3', ['-m', 'venv', venv], { silent: true });
72
+ }
73
+
74
+ async function pipInstall(flags, env, source) {
75
+ const spec = source === 'pypi' ? specPyPI(flags) : specGit(flags);
76
+ process.stdout.write(`${info(`Installing ${c.bold(spec)} (source: ${source})…`)}\n`);
77
+
78
+ // Dev-only: skip the actual pip step so CI / smoke tests can validate the
79
+ // wrapper end-to-end without downloading ~1.5GB of ML deps.
80
+ if (process.env.TAM_INSTALL_DRY === '1') {
81
+ process.stdout.write(` ${warn('TAM_INSTALL_DRY=1 — skipping real pip install')}\n`);
82
+ // Drop a marker file so subsequent steps don't barf on missing bin.
83
+ const fs = require('fs');
84
+ const markerDir = path.dirname(venvBin(flags, 'claude-total-memory'));
85
+ fs.mkdirSync(markerDir, { recursive: true });
86
+ fs.writeFileSync(venvBin(flags, 'claude-total-memory'), '#!/bin/sh\necho "dry-run shim"\n');
87
+ fs.chmodSync(venvBin(flags, 'claude-total-memory'), 0o755);
88
+ return;
89
+ }
90
+
91
+ if (env.uv) {
92
+ // uv pip is dramatically faster.
93
+ const pyExe = venvBin(flags, 'python');
94
+ await runOrThrow('uv', ['pip', 'install', '--python', pyExe, '--upgrade', spec]);
95
+ return;
96
+ }
97
+ const pipExe = venvBin(flags, 'pip');
98
+ await runOrThrow(pipExe, ['install', '--upgrade', '--disable-pip-version-check', spec]);
99
+ }
100
+
101
+ async function install(ctx) {
102
+ const flags = ctx.flags;
103
+ const env = await diagnose();
104
+ printReport(env);
105
+
106
+ const picker = pickInstaller(env);
107
+ if (!picker) {
108
+ process.stderr.write(`\n${fail('cannot proceed: missing python3 ≥ 3.10')}\n`);
109
+ printRemediation(env);
110
+ return 1;
111
+ }
112
+
113
+ process.stdout.write(`\n${bold('Installing server…')}\n`);
114
+ process.stdout.write(` ${step(1, 3, `venv → ${c.dim(venvDir(flags))}`)}\n`);
115
+ await createVenv(flags, env);
116
+ process.stdout.write(` ${ok('venv ready')}\n`);
117
+
118
+ process.stdout.write(` ${step(2, 3, 'resolving source (PyPI → GitHub fallback)…')}\n`);
119
+ const source = await pickSource(flags);
120
+
121
+ process.stdout.write(` ${step(3, 3, 'pip install…')}\n`);
122
+ await pipInstall(flags, env, source);
123
+
124
+ // Verify entry-point present.
125
+ const bin = venvBin(flags, 'claude-total-memory');
126
+ if (!exists(bin)) {
127
+ process.stderr.write(`\n${fail(`entry-point ${bin} not found after install`)}\n`);
128
+ return 1;
129
+ }
130
+ process.stdout.write(`\n${ok(c.bold('Installation complete.'))}\n`);
131
+ process.stdout.write(`${info(`Binary: ${c.dim(bin)}`)}\n`);
132
+ return 0;
133
+ }
134
+
135
+ module.exports = { install, venvBin, pinned, DEFAULT_SERVER_VERSION, PYPI_PACKAGE };
package/lib/paths.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+
7
+ function home() {
8
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
9
+ }
10
+
11
+ function memoryDir(flags) {
12
+ if (flags && typeof flags['memory-dir'] === 'string') return flags['memory-dir'];
13
+ if (process.env.CLAUDE_MEMORY_DIR) return process.env.CLAUDE_MEMORY_DIR;
14
+ return path.join(home(), '.claude-memory');
15
+ }
16
+
17
+ function venvDir(flags) {
18
+ return path.join(memoryDir(flags), '.venv');
19
+ }
20
+
21
+ function venvBin(flags, name) {
22
+ const sub = process.platform === 'win32' ? 'Scripts' : 'bin';
23
+ const ext = process.platform === 'win32' && !name.endsWith('.exe') ? '.exe' : '';
24
+ return path.join(venvDir(flags), sub, name + ext);
25
+ }
26
+
27
+ function ensureDir(p) {
28
+ fs.mkdirSync(p, { recursive: true });
29
+ }
30
+
31
+ function exists(p) {
32
+ try { fs.accessSync(p); return true; } catch (_) { return false; }
33
+ }
34
+
35
+ function readJSONSafe(p) {
36
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
37
+ }
38
+
39
+ function writeJSON(p, obj) {
40
+ fs.mkdirSync(path.dirname(p), { recursive: true });
41
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2));
42
+ }
43
+
44
+ module.exports = { home, memoryDir, venvDir, venvBin, ensureDir, exists, readJSONSafe, writeJSON };
package/lib/sh.js ADDED
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const { spawn, spawnSync } = require('child_process');
4
+ const { c, dim } = require('./style');
5
+
6
+ function isVerbose() {
7
+ return process.argv.includes('--verbose') || process.env.TAM_DEBUG === '1';
8
+ }
9
+
10
+ function which(bin) {
11
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
12
+ const r = spawnSync(cmd, [bin], { encoding: 'utf8' });
13
+ if (r.status === 0) {
14
+ const path = (r.stdout || '').split(/\r?\n/).filter(Boolean)[0];
15
+ return path || null;
16
+ }
17
+ return null;
18
+ }
19
+
20
+ function probe(bin, args = ['--version']) {
21
+ const path = which(bin);
22
+ if (!path) return { ok: false };
23
+ const r = spawnSync(path, args, { encoding: 'utf8' });
24
+ return {
25
+ ok: r.status === 0,
26
+ path,
27
+ out: (r.stdout || '').trim() || (r.stderr || '').trim(),
28
+ };
29
+ }
30
+
31
+ function run(cmd, args, opts = {}) {
32
+ return new Promise((resolve) => {
33
+ if (isVerbose()) {
34
+ process.stderr.write(
35
+ `${dim('$')} ${[cmd, ...args].map((a) => (/\s/.test(a) ? JSON.stringify(a) : a)).join(' ')}\n`
36
+ );
37
+ }
38
+ const child = spawn(cmd, args, {
39
+ stdio: opts.silent ? ['ignore', 'pipe', 'pipe'] : 'inherit',
40
+ env: { ...process.env, ...(opts.env || {}) },
41
+ cwd: opts.cwd || process.cwd(),
42
+ });
43
+ let stdout = '';
44
+ let stderr = '';
45
+ if (opts.silent) {
46
+ child.stdout.on('data', (d) => (stdout += d.toString()));
47
+ child.stderr.on('data', (d) => (stderr += d.toString()));
48
+ }
49
+ child.on('close', (code) => resolve({ code, stdout, stderr }));
50
+ child.on('error', (err) => resolve({ code: 1, stdout, stderr: String(err) }));
51
+ });
52
+ }
53
+
54
+ async function runOrThrow(cmd, args, opts = {}) {
55
+ const r = await run(cmd, args, opts);
56
+ if (r.code !== 0) {
57
+ const err = new Error(`${cmd} ${args.join(' ')} exited with code ${r.code}`);
58
+ err.stdout = r.stdout;
59
+ err.stderr = r.stderr;
60
+ err.code = r.code;
61
+ throw err;
62
+ }
63
+ return r;
64
+ }
65
+
66
+ module.exports = { which, probe, run, runOrThrow, isVerbose };
package/lib/style.js ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const noColor =
4
+ process.argv.includes('--no-color') ||
5
+ process.env.NO_COLOR === '1' ||
6
+ process.env.TERM === 'dumb' ||
7
+ !process.stdout.isTTY;
8
+
9
+ function wrap(open, close) {
10
+ return (s) => (noColor ? String(s) : `\x1b[${open}m${s}\x1b[${close}m`);
11
+ }
12
+
13
+ const c = {
14
+ bold: wrap(1, 22),
15
+ dim: wrap(2, 22),
16
+ red: wrap(31, 39),
17
+ green: wrap(32, 39),
18
+ yellow: wrap(33, 39),
19
+ blue: wrap(34, 39),
20
+ magenta: wrap(35, 39),
21
+ cyan: wrap(36, 39),
22
+ gray: wrap(90, 39),
23
+ };
24
+
25
+ function bold(s) { return c.bold(s); }
26
+ function dim(s) { return c.dim(s); }
27
+
28
+ function banner() {
29
+ const v = require('../package.json').version;
30
+ const line = c.cyan('total-agent-memory');
31
+ return `\n${c.bold(line)} ${c.dim(`v${v}`)} ${c.dim('— persistent memory for AI coding agents')}\n`;
32
+ }
33
+
34
+ function step(n, total, msg) {
35
+ return `${c.dim(`[${n}/${total}]`)} ${msg}`;
36
+ }
37
+
38
+ function ok(msg) { return `${c.green('✓')} ${msg}`; }
39
+ function fail(msg) { return `${c.red('✗')} ${msg}`; }
40
+ function warn(msg) { return `${c.yellow('!')} ${msg}`; }
41
+ function info(msg) { return `${c.cyan('›')} ${msg}`; }
42
+
43
+ module.exports = { c, bold, dim, banner, step, ok, fail, warn, info };
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "total-agent-memory",
3
+ "version": "0.1.0",
4
+ "description": "Persistent memory for Claude Code, Codex CLI, Cursor and any MCP client. Local-first, hits #5 on LoCoMo. One command to install.",
5
+ "keywords": [
6
+ "mcp",
7
+ "memory",
8
+ "claude",
9
+ "claude-code",
10
+ "codex",
11
+ "cursor",
12
+ "cline",
13
+ "agent",
14
+ "ai",
15
+ "llm",
16
+ "knowledge-graph",
17
+ "rag",
18
+ "persistent-memory",
19
+ "local-first"
20
+ ],
21
+ "homepage": "https://totalmemory.dev",
22
+ "bugs": {
23
+ "url": "https://git.infra.vbcherepanov.com/vbcherepanov/total-memory-npm/issues"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://git.infra.vbcherepanov.com/vbcherepanov/total-memory-npm.git"
28
+ },
29
+ "license": "MIT",
30
+ "author": "Vitalii Cherepanov <vbcherepanov@gmail.com> (https://vbcherepanov.com)",
31
+ "type": "commonjs",
32
+ "bin": {
33
+ "total-agent-memory": "bin/cli.js",
34
+ "tam": "bin/cli.js"
35
+ },
36
+ "main": "lib/index.js",
37
+ "files": [
38
+ "bin/",
39
+ "lib/",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "scripts": {
47
+ "test": "node tests/smoke.test.js",
48
+ "lint": "node --check bin/cli.js && node --check lib/*.js"
49
+ },
50
+ "dependencies": {},
51
+ "devDependencies": {},
52
+ "publishConfig": {
53
+ "access": "public"
54
+ }
55
+ }