threadctx-mcp 0.1.2 → 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 +31 -5
- package/dist/cli.js +142 -6
- package/dist/local-store.d.ts +13 -0
- package/dist/local-store.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,13 @@ per-client integration work.
|
|
|
6
6
|
|
|
7
7
|
## Modes
|
|
8
8
|
|
|
9
|
-
- **Local (default, free, no signup):** memory stored
|
|
10
|
-
`~/.threadctx/local.json` — **zero native dependencies**, so
|
|
9
|
+
- **Local (default, free, no signup):** memory stored as a plain JSON file
|
|
10
|
+
at `~/.threadctx/local.json` — **zero native dependencies**, so
|
|
11
11
|
`npx threadctx-mcp` installs instantly on any machine with Node 18+ (no
|
|
12
12
|
compiler, no node-gyp step). No network calls except to whichever LLM
|
|
13
13
|
provider your agent already uses. Matching is keyword-based, scoped to
|
|
14
|
-
the current repo (detected via `git remote`).
|
|
14
|
+
the current repo (detected via `git remote`). Run `npx threadctx-mcp list`
|
|
15
|
+
any time to see exactly what your agents have stored.
|
|
15
16
|
- **Cloud (paid Team tier+):** memory shared across everyone on the repo,
|
|
16
17
|
with real semantic search. Requires an API key from
|
|
17
18
|
[threadctx.dev](https://threadctx.dev) (or your own self-hosted
|
|
@@ -23,16 +24,30 @@ per-client integration work.
|
|
|
23
24
|
# Local mode — nothing to configure
|
|
24
25
|
npx threadctx-mcp
|
|
25
26
|
|
|
27
|
+
# Optional: write a committable config + drop "check team memory" into your
|
|
28
|
+
# agent's project rules (CLAUDE.md / .cursorrules). Works in local mode too.
|
|
29
|
+
npx threadctx-mcp init
|
|
30
|
+
|
|
26
31
|
# Cloud mode — prints the exact MCP config block to paste
|
|
27
32
|
npx threadctx-mcp init --mode=cloud --api-key=tctx_xxx
|
|
33
|
+
|
|
34
|
+
# See what your agents have written to this machine
|
|
35
|
+
npx threadctx-mcp list # this repo
|
|
36
|
+
npx threadctx-mcp list --all # every repo
|
|
28
37
|
```
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
`init` writes a `.threadctx.json` file (just `{ "mode": "cloud" }`) to the
|
|
31
40
|
current directory. It is safe to commit — it never contains your API key.
|
|
32
41
|
The key is read from the `THREADCTX_API_KEY` environment variable at
|
|
33
42
|
runtime (set it in your MCP client's `env` block, as shown below), so
|
|
34
43
|
secrets stay out of version control by construction.
|
|
35
44
|
|
|
45
|
+
`init` also adds a small, clearly-marked block to your `CLAUDE.md` and
|
|
46
|
+
`.cursorrules` instructing the agent to call `memory_query` before a task
|
|
47
|
+
and `memory_write` after — this is what makes the memory reliably get
|
|
48
|
+
*read*, since MCP tools are pull-based (pass `--no-rules` to skip). Re-running
|
|
49
|
+
`init` refreshes that block in place instead of duplicating it.
|
|
50
|
+
|
|
36
51
|
## Claude Code setup
|
|
37
52
|
|
|
38
53
|
Add to your Claude Code MCP config (`claude mcp add` or edit
|
|
@@ -107,4 +122,15 @@ npm run build # compiles to dist/ for publishing
|
|
|
107
122
|
|
|
108
123
|
Tool descriptions are deliberately written to bias the model toward
|
|
109
124
|
calling `memory_query` proactively — MCP is pull-based, so the agent has
|
|
110
|
-
to be prompted by the description to use it; it isn't automatic.
|
|
125
|
+
to be prompted by the description to use it; it isn't automatic. For the
|
|
126
|
+
most reliable behavior, run `npx threadctx-mcp init`, which adds an explicit
|
|
127
|
+
"check team memory before/after each task" instruction to your `CLAUDE.md`
|
|
128
|
+
and `.cursorrules`.
|
|
129
|
+
|
|
130
|
+
## CLI subcommands
|
|
131
|
+
|
|
132
|
+
| Command | What it does |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `npx threadctx-mcp` | Runs the MCP server (this is what Claude Code / Cursor launch). |
|
|
135
|
+
| `npx threadctx-mcp init [--mode=cloud --api-key=…] [--no-rules]` | Writes `.threadctx.json` and the project-rules block. |
|
|
136
|
+
| `npx threadctx-mcp list [--all] [--full] [--json]` | Shows what's stored in the local on-disk memory. |
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { loadConfig } from './config.js';
|
|
5
|
+
import { LocalStore } from './local-store.js';
|
|
4
6
|
import { startServer } from './server.js';
|
|
5
7
|
const [, , command, ...rest] = process.argv;
|
|
6
8
|
function parseFlags(args) {
|
|
7
9
|
const flags = {};
|
|
8
10
|
for (const arg of args) {
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
11
|
-
flags[
|
|
11
|
+
const kv = arg.match(/^--([^=]+)=(.*)$/);
|
|
12
|
+
if (kv) {
|
|
13
|
+
flags[kv[1]] = kv[2];
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const bare = arg.match(/^--([^=]+)$/);
|
|
17
|
+
if (bare)
|
|
18
|
+
flags[bare[1]] = true;
|
|
12
19
|
}
|
|
13
20
|
return flags;
|
|
14
21
|
}
|
|
22
|
+
// The exact instruction block we drop into an agent's project rules. MCP is
|
|
23
|
+
// pull-based — the model only calls memory_query/memory_write if something tells
|
|
24
|
+
// it to — so this is the single highest-leverage thing for making the memory
|
|
25
|
+
// actually get read. Fenced by markers so re-running `init` updates in place
|
|
26
|
+
// instead of appending duplicates.
|
|
27
|
+
const RULES_START = '<!-- threadctx:start (managed — edit above/below, not between) -->';
|
|
28
|
+
const RULES_END = '<!-- threadctx:end -->';
|
|
29
|
+
const RULES_BODY = [
|
|
30
|
+
'## Team memory (threadctx)',
|
|
31
|
+
'',
|
|
32
|
+
'- **Before** starting any non-trivial task, call `memory_query` with a short',
|
|
33
|
+
' description of what you are about to do. Check for prior decisions, fixes,',
|
|
34
|
+
' and gotchas on this repo before writing code — not after.',
|
|
35
|
+
'- **After** resolving a non-obvious bug, making an architectural decision, or',
|
|
36
|
+
' learning something that would save a teammate time, call `memory_write` to',
|
|
37
|
+
' save it. Write it so a future reader has full context.',
|
|
38
|
+
].join('\n');
|
|
39
|
+
/**
|
|
40
|
+
* Insert (or refresh) the threadctx rules block in a project rules file,
|
|
41
|
+
* idempotently. Creates the file if missing. Returns what happened so the
|
|
42
|
+
* caller can report it honestly.
|
|
43
|
+
*/
|
|
44
|
+
function upsertRulesBlock(filePath) {
|
|
45
|
+
const block = `${RULES_START}\n${RULES_BODY}\n${RULES_END}\n`;
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
writeFileSync(filePath, block);
|
|
48
|
+
return 'created';
|
|
49
|
+
}
|
|
50
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
51
|
+
const startIdx = existing.indexOf(RULES_START);
|
|
52
|
+
if (startIdx !== -1) {
|
|
53
|
+
const endIdx = existing.indexOf(RULES_END, startIdx);
|
|
54
|
+
if (endIdx !== -1) {
|
|
55
|
+
const before = existing.slice(0, startIdx);
|
|
56
|
+
const after = existing.slice(endIdx + RULES_END.length);
|
|
57
|
+
const next = `${before}${block.trimEnd()}${after}`;
|
|
58
|
+
if (next === existing)
|
|
59
|
+
return 'unchanged';
|
|
60
|
+
writeFileSync(filePath, next);
|
|
61
|
+
return 'updated';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// No managed block yet — append one, keeping the user's existing content.
|
|
65
|
+
const sep = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
66
|
+
writeFileSync(filePath, `${existing}${sep}${block}`);
|
|
67
|
+
return 'updated';
|
|
68
|
+
}
|
|
15
69
|
function runInit(args) {
|
|
16
70
|
const flags = parseFlags(args);
|
|
17
71
|
const mode = flags.mode === 'cloud' ? 'cloud' : 'local';
|
|
@@ -20,9 +74,9 @@ function runInit(args) {
|
|
|
20
74
|
// at runtime (set it in your MCP client's `env` block). This keeps secrets
|
|
21
75
|
// out of version control by construction.
|
|
22
76
|
const config = { mode };
|
|
23
|
-
if (mode === 'cloud' && flags['api-url'])
|
|
77
|
+
if (mode === 'cloud' && typeof flags['api-url'] === 'string')
|
|
24
78
|
config.apiUrl = flags['api-url'];
|
|
25
|
-
const apiKey = flags['api-key'];
|
|
79
|
+
const apiKey = typeof flags['api-key'] === 'string' ? flags['api-key'] : undefined;
|
|
26
80
|
if (mode === 'cloud' && !apiKey) {
|
|
27
81
|
console.error('Cloud mode needs an API key. Pass --api-key=<tctx_...> so this command can');
|
|
28
82
|
console.error('show you the exact config block to paste.');
|
|
@@ -32,6 +86,16 @@ function runInit(args) {
|
|
|
32
86
|
const configPath = join(process.cwd(), '.threadctx.json');
|
|
33
87
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
34
88
|
console.log(`✅ Wrote ${configPath} (mode: ${mode}) — safe to commit, contains no secret.`);
|
|
89
|
+
// Drop the "always check team memory" instruction into the agent's project
|
|
90
|
+
// rules so the tools actually get used. Opt out with --no-rules.
|
|
91
|
+
if (flags['no-rules'] !== true) {
|
|
92
|
+
for (const rulesFile of ['CLAUDE.md', '.cursorrules']) {
|
|
93
|
+
const rulesPath = join(process.cwd(), rulesFile);
|
|
94
|
+
const result = upsertRulesBlock(rulesPath);
|
|
95
|
+
const verb = result === 'created' ? 'Created' : result === 'updated' ? 'Updated' : 'Already current in';
|
|
96
|
+
console.log(`✅ ${verb} ${rulesFile} — tells your agent to check team memory each task.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
35
99
|
console.log('');
|
|
36
100
|
if (mode === 'cloud') {
|
|
37
101
|
console.log('Add threadctx to your MCP client config (~/.claude/mcp.json and/or .cursor/mcp.json).');
|
|
@@ -54,11 +118,83 @@ function runInit(args) {
|
|
|
54
118
|
console.log('See the README for the exact block (identical for Claude Code and Cursor).');
|
|
55
119
|
}
|
|
56
120
|
}
|
|
121
|
+
function truncate(text, max) {
|
|
122
|
+
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
123
|
+
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A milestone-aware, on-device nudge toward cloud/team mode. Deliberately sends
|
|
127
|
+
* NOTHING off the machine — it's computed purely from the local store and shown
|
|
128
|
+
* only in surfaces a human actually reads (`list`). The whole pitch escalates as
|
|
129
|
+
* the local memory grows, because a large multi-repo store is exactly the point
|
|
130
|
+
* where "my teammates can't see any of this" becomes a real, felt problem.
|
|
131
|
+
*/
|
|
132
|
+
function cloudNudge(all) {
|
|
133
|
+
const total = all.length;
|
|
134
|
+
const repos = new Set(all.map((m) => m.repo)).size;
|
|
135
|
+
if (repos >= 2 && total >= 5) {
|
|
136
|
+
return (`↑ You've stored ${total} memories across ${repos} repos on this machine — none of it is\n` +
|
|
137
|
+
` visible to your teammates. Share it as team memory: https://threadctx.dev`);
|
|
138
|
+
}
|
|
139
|
+
if (total >= 20) {
|
|
140
|
+
return (`↑ You've built up ${total} local memories — turn them into shared team memory\n` +
|
|
141
|
+
` your whole team can query: https://threadctx.dev`);
|
|
142
|
+
}
|
|
143
|
+
return 'Local-only memory — share it across your team at https://threadctx.dev';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* `threadctx list` — show what your agents have written to *this machine* in
|
|
147
|
+
* local mode. Read-only, no network, scoped to the current repo by default
|
|
148
|
+
* (pass --all to see every repo, --full to print untruncated content, --json
|
|
149
|
+
* for machine-readable output).
|
|
150
|
+
*/
|
|
151
|
+
function runList(args) {
|
|
152
|
+
const flags = parseFlags(args);
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
if (config.mode === 'cloud') {
|
|
155
|
+
console.error('[threadctx] `list` shows the local on-disk store; you are in cloud mode.');
|
|
156
|
+
console.error('[threadctx] Cloud memories live on the server and are retrieved via memory_query.');
|
|
157
|
+
console.error('[threadctx] (Set THREADCTX_MODE=local to inspect the local store, if any.)');
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
const store = new LocalStore(config.dbPath);
|
|
161
|
+
const all = store.list();
|
|
162
|
+
const repoFilter = flags.all === true ? undefined : config.repo;
|
|
163
|
+
const memories = repoFilter ? all.filter((m) => m.repo === repoFilter) : all;
|
|
164
|
+
if (flags.json === true) {
|
|
165
|
+
console.log(JSON.stringify(memories, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const scope = repoFilter ? `for ${repoFilter}` : 'across all repos';
|
|
169
|
+
if (memories.length === 0) {
|
|
170
|
+
console.log(`No local memories ${scope} yet.`);
|
|
171
|
+
console.log('Your agents write these by calling memory_write. Nothing has been stored here.');
|
|
172
|
+
if (repoFilter && all.length > 0) {
|
|
173
|
+
console.log(`(You have ${all.length} in other repos — run with --all to see them.)`);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
console.log(`threadctx — ${memories.length} local ${memories.length === 1 ? 'memory' : 'memories'} ${scope}\n`);
|
|
178
|
+
for (const m of memories) {
|
|
179
|
+
const date = m.created_at.slice(0, 10);
|
|
180
|
+
const tags = m.tags.length ? ` [${m.tags.join(', ')}]` : '';
|
|
181
|
+
const repoTag = repoFilter ? '' : ` ${m.repo}`;
|
|
182
|
+
console.log(`● ${date}${repoTag}${tags}`);
|
|
183
|
+
console.log(` ${flags.full === true ? m.content.trim() : truncate(m.content, 160)}`);
|
|
184
|
+
console.log(` id: ${m.id.slice(0, 8)}\n`);
|
|
185
|
+
}
|
|
186
|
+
console.log(`Stored at ${config.dbPath}.`);
|
|
187
|
+
console.log(cloudNudge(all));
|
|
188
|
+
}
|
|
57
189
|
async function main() {
|
|
58
190
|
if (command === 'init') {
|
|
59
191
|
runInit(rest);
|
|
60
192
|
return;
|
|
61
193
|
}
|
|
194
|
+
if (command === 'list') {
|
|
195
|
+
runList(rest);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
62
198
|
// No subcommand: this is what Claude Code / Cursor actually launch as the
|
|
63
199
|
// MCP server process (they invoke `npx threadctx-mcp` with no arguments).
|
|
64
200
|
await startServer();
|
package/dist/local-store.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export interface LocalMemory {
|
|
|
5
5
|
created_at: string;
|
|
6
6
|
score: number;
|
|
7
7
|
}
|
|
8
|
+
export interface StoredMemory {
|
|
9
|
+
id: string;
|
|
10
|
+
repo: string;
|
|
11
|
+
content: string;
|
|
12
|
+
tags: string[];
|
|
13
|
+
created_at: string;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* Local mode storage: a single JSON file on disk, zero network calls,
|
|
10
17
|
* zero accounts, and — deliberately — zero native dependencies.
|
|
@@ -27,5 +34,11 @@ export declare class LocalStore {
|
|
|
27
34
|
private load;
|
|
28
35
|
private persist;
|
|
29
36
|
write(repo: string, content: string, tags: string[]): string;
|
|
37
|
+
/**
|
|
38
|
+
* Return stored memories newest-first, optionally scoped to one repo. Used by
|
|
39
|
+
* the `threadctx list` CLI so a developer can see exactly what their agents
|
|
40
|
+
* have written to their own machine — no query keywords, no scoring.
|
|
41
|
+
*/
|
|
42
|
+
list(repo?: string): StoredMemory[];
|
|
30
43
|
query(repo: string, taskDescription: string, maxResults: number): LocalMemory[];
|
|
31
44
|
}
|
package/dist/local-store.js
CHANGED
|
@@ -56,6 +56,17 @@ export class LocalStore {
|
|
|
56
56
|
this.persist();
|
|
57
57
|
return id;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Return stored memories newest-first, optionally scoped to one repo. Used by
|
|
61
|
+
* the `threadctx list` CLI so a developer can see exactly what their agents
|
|
62
|
+
* have written to their own machine — no query keywords, no scoring.
|
|
63
|
+
*/
|
|
64
|
+
list(repo) {
|
|
65
|
+
return this.memories
|
|
66
|
+
.filter((m) => (repo ? m.repo === repo : true))
|
|
67
|
+
.slice()
|
|
68
|
+
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
|
|
69
|
+
}
|
|
59
70
|
query(repo, taskDescription, maxResults) {
|
|
60
71
|
const keywords = taskDescription
|
|
61
72
|
.toLowerCase()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threadctx-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared memory MCP server for AI coding agents. Local-only by default; point it at threadctx.dev (or your own deployment) to share memory across your team.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|