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 CHANGED
@@ -6,12 +6,13 @@ per-client integration work.
6
6
 
7
7
  ## Modes
8
8
 
9
- - **Local (default, free, no signup):** memory stored in SQLite at
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
- This writes a `.threadctx.json` file (just `{ "mode": "cloud" }`) to the
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 match = arg.match(/^--([^=]+)=(.*)$/);
10
- if (match)
11
- flags[match[1]] = match[2];
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();
@@ -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
  }
@@ -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.1.2",
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": {