Skip to content

Commit c2018f7

Browse files
committed
3.0.0 - File-per-thread architecture, bug fixes, and test coverage
1 parent 04e6d9d commit c2018f7

26 files changed

Lines changed: 2708 additions & 351 deletions

README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,31 @@ Checking environment...
3434
✓ Claude Code detected
3535
✓ ~/.claude directory exists
3636
37-
[1/4] PostToolUse hook (auto-tracks files you create)
37+
[1/5] PostToolUse hook (auto-tracks files you create)
3838
Status: Not installed
3939
Install to ~/.claude/settings.json? (Y/n) y
4040
✓ Hook installed
4141
42-
[2/4] SessionStart hook (shows context at session start)
42+
[2/5] SessionStart hook (shows context at session start)
4343
Status: Not installed
4444
Install to ~/.claude/settings.json? (Y/n) y
4545
✓ Hook installed
4646
47-
[3/4] MCP Server (gives Claude direct access to threadlinking tools)
47+
[3/5] MCP Server (gives Claude direct access to threadlinking tools)
4848
Status: Not configured
49-
Add to ~/.claude/settings.json mcpServers? (Y/n) y
49+
Add to ~/.claude/mcp.json? (Y/n) y
5050
✓ MCP server configured
5151
52-
[4/4] CLAUDE.md instructions (teaches Claude when/how to use threadlinking)
52+
[4/5] CLAUDE.md instructions (teaches Claude when/how to use threadlinking)
5353
Status: Not present
5454
Append to ~/.claude/CLAUDE.md? (Y/n) y
5555
✓ Instructions added
5656
57+
[5/5] Ignore file (filters noise from pending files list)
58+
Status: Not present
59+
Create ~/.threadlinkingignore? (Y/n) y
60+
✓ Ignore file created
61+
5762
Done! Threadlinking is fully configured.
5863
5964
Start a new Claude Code session to begin.
@@ -310,7 +315,7 @@ Everything is stored locally at `~/.threadlinking/thread_index.json`. No cloud,
310315

311316
- All data stored locally at `~/.threadlinking/` with `0600` permissions (owner read/write only)
312317
- No network calls, no telemetry, no cloud sync (model download is one-time and cached)
313-
- Minimal dependencies: [commander](https://www.npmjs.com/package/commander), [@xenova/transformers](https://github.com/xenova/transformers.js), [vectra](https://github.com/Stevenic/vectra)
318+
- Focused dependencies: [commander](https://www.npmjs.com/package/commander), [@xenova/transformers](https://github.com/xenova/transformers.js), [vectra](https://github.com/Stevenic/vectra), [zod](https://github.com/colinhacks/zod), [proper-lockfile](https://github.com/moxystudio/node-proper-lockfile), [ignore](https://github.com/kaelzhang/node-ignore), [@modelcontextprotocol/sdk](https://github.com/modelcontextprotocol/sdk)
314319
- Atomic file writes to prevent corruption
315320
- Input sanitization on all user-provided strings
316321

ROADMAP.md

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,34 @@ The goal: When you ask "why did we build it this way?", the answer should be a c
88

99
---
1010

11-
## Current State (v2.0.0)
12-
13-
### Version Status
14-
- **GitHub**: v2.0.0 (main branch)
15-
- **npm**: v1.1.1 (needs publishing)
11+
## Current State (v2.0.x)
1612

1713
### All Features (Free)
1814
- Thread-based context preservation
1915
- Snippets with source, timestamp, and tags
2016
- File attachments with explain command
2117
- Cross-session, cross-repo persistence
2218
- Keyword search
23-
- Semantic search (local embeddings via wink-nlp)
19+
- Semantic search (local embeddings via @xenova/transformers)
2420
- Analytics (usage stats)
2521
- Export (markdown/JSON/timeline formats)
2622
- MCP server integration with Claude Code
23+
- `.threadlinkingignore` support with prune command
24+
- Guided setup via `threadlinking init`
2725

2826
---
2927

30-
## Phase 1: Publish v2.0.0 to npm
28+
## Phase 1: Publish & CI
3129

3230
**Priority: HIGH**
3331

34-
The main blocker for wider adoption.
35-
36-
- [ ] Add basic test suite
37-
- Core: thread CRUD, snippet operations
38-
- CLI: smoke tests for main commands
39-
- [ ] Set up GitHub Actions CI
40-
- Run tests on push
41-
- Lint check
42-
- Build verification
43-
- [ ] Update README with v2.0 features
32+
- [x] Publish v2.0.x to npm
33+
- [x] Set up GitHub Actions CI (ubuntu/macos/windows, Node 20 & 22)
34+
- [ ] Add comprehensive test suite
35+
- Core operation unit tests
36+
- Storage layer tests
37+
- Integration/lifecycle tests
4438
- [ ] Verify Windows compatibility (path separators)
45-
- [ ] **Publish v2.0.0 to npm**
4639

4740
---
4841

@@ -52,27 +45,25 @@ The main blocker for wider adoption.
5245

5346
- [ ] Submit to official MCP Registry
5447
- [ ] Add `mcp.json` manifest file
55-
- [ ] Create setup instructions for Claude Code users
56-
- **Recommend `~/.claude/mcp.json`** (dedicated MCP config, not settings.json)
57-
- Document the difference: mcp.json is for MCP servers only, settings.json mixes personal config
58-
- Provide simple copy-paste config:
59-
```json
60-
{
61-
"mcpServers": {
62-
"threadlinking": {
63-
"command": "npx",
64-
"args": ["threadlinking-mcp"]
65-
}
66-
}
67-
}
68-
```
69-
- Note: Users can also use settings.json → mcpServers if they prefer single-file config
48+
- [x] Create setup instructions for Claude Code users (`threadlinking init`)
7049
- [ ] List on directories: Smithery, Glama, MCP.so
7150
- [ ] Submit PR to awesome-mcp-servers
7251

7352
---
7453

75-
## Phase 3: IDE Integration
54+
## Phase 3: File-Per-Thread Architecture
55+
56+
Split `thread_index.json` into one file per thread for better concurrency and inspectability.
57+
58+
- [ ] Design migration strategy
59+
- [ ] New `loadThread(id)` / `saveThread(id)` / `updateThread(id, fn)` storage layer
60+
- [ ] Per-thread locking (instead of global lock)
61+
- [ ] Automatic migration from monolithic to file-per-thread
62+
- [ ] Update all operations to use new storage layer
63+
64+
---
65+
66+
## Phase 4: IDE Integration
7667

7768
- [ ] VSCode extension
7869
- Right-click file → "Show Threadlinking Context"
@@ -82,12 +73,14 @@ The main blocker for wider adoption.
8273

8374
---
8475

85-
## Phase 4: Future Enhancements
76+
## Phase 5: Future Enhancements
8677

8778
- [ ] Thread relationships (link related threads)
8879
- [ ] Context summarization (AI-generated thread summaries)
8980
- [ ] Team/org features (optional, shared threads)
9081
- [ ] Cloud sync (optional, remains local-first by default)
82+
- [ ] Command syntax redesign for power users
83+
- [ ] Flat/nested attribute system
9184

9285
---
9386

@@ -105,4 +98,4 @@ Ideas and PRs welcome. Open an issue to discuss before implementing major featur
10598

10699
---
107100

108-
*Last updated: 2026-01-24*
101+
*Last updated: 2026-03-04*

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "threadlinking",
3-
"version": "2.0.13",
3+
"version": "3.0.0",
44
"description": "Preserve the decisions behind your code",
55
"type": "module",
66
"main": "dist/index.js",

src/commands/attach.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const attachCommand = new Command('attach')
1010

1111
if (!result.success) {
1212
console.error(`Error: ${result.message}`);
13+
process.exitCode = 1;
1314
return;
1415
}
1516

src/commands/audit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,6 @@ export const auditCommand = new Command('audit')
8181
console.log(`Duplicates: ${actualDuplicates.size}`);
8282
} catch (error) {
8383
console.error(`Error: ${error instanceof Error ? error.message : error}`);
84+
process.exitCode = 1;
8485
}
8586
});

src/commands/clear.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,15 @@
11
import { Command } from 'commander';
2-
import { createInterface } from 'readline';
3-
import { loadIndex, saveIndex } from '../core/index.js';
4-
5-
function prompt(question: string): Promise<string> {
6-
const rl = createInterface({
7-
input: process.stdin,
8-
output: process.stdout,
9-
});
10-
11-
return new Promise((resolve) => {
12-
rl.question(question, (answer) => {
13-
rl.close();
14-
resolve(answer);
15-
});
16-
});
17-
}
2+
import { loadMetaIndex, deleteThreadFile, prompt } from '../core/index.js';
183

194
export const clearCommand = new Command('clear')
205
.description('Delete ALL threads from the index (dangerous)')
216
.option('--yes', 'Skip confirmation prompts')
227
.action(async (options) => {
238
try {
24-
const index = loadIndex();
9+
const meta = loadMetaIndex();
10+
const threadIds = Object.keys(meta.threads);
2511

26-
if (Object.keys(index).length === 0) {
12+
if (threadIds.length === 0) {
2713
console.log('Index is already empty.');
2814
return;
2915
}
@@ -44,9 +30,14 @@ export const clearCommand = new Command('clear')
4430
}
4531
}
4632

47-
saveIndex({});
33+
// Delete each thread file
34+
for (const id of threadIds) {
35+
deleteThreadFile(id);
36+
}
37+
4838
console.log('All threads deleted.');
4939
} catch (error) {
5040
console.error(`Error: ${error instanceof Error ? error.message : error}`);
41+
process.exitCode = 1;
5142
}
5243
});

src/commands/delete.ts

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
import { Command } from 'commander';
2-
import { createInterface } from 'readline';
3-
import { loadIndex, saveIndex, validateTag } from '../core/index.js';
4-
5-
function prompt(question: string): Promise<string> {
6-
const rl = createInterface({
7-
input: process.stdin,
8-
output: process.stdout,
9-
});
10-
11-
return new Promise((resolve) => {
12-
rl.question(question, (answer) => {
13-
rl.close();
14-
resolve(answer);
15-
});
16-
});
17-
}
2+
import { deleteThreadFile, loadMetaIndex, validateTag, prompt } from '../core/index.js';
3+
import { getSemanticIndex } from '../embeddings/index.js';
184

195
export const deleteCommand = new Command('delete')
206
.description('Delete a thread')
217
.argument('<thread_id>', 'Thread tag or UUID')
22-
.option('--yes', 'Skip confirmation')
8+
.option('--yes', 'Skip confirmation prompts')
239
.action(async (threadId: string, options) => {
2410
try {
25-
const index = loadIndex();
2611
const validatedId = validateTag(threadId);
2712

28-
if (!index[validatedId]) {
13+
// Check existence via meta index
14+
const meta = loadMetaIndex();
15+
if (!meta.threads[validatedId]) {
2916
console.error(`Thread ID '${validatedId}' not found.`);
17+
process.exitCode = 1;
3018
return;
3119
}
3220

@@ -40,10 +28,19 @@ export const deleteCommand = new Command('delete')
4028
}
4129
}
4230

43-
delete index[validatedId];
44-
saveIndex(index);
31+
deleteThreadFile(validatedId);
32+
33+
// Clean up semantic index embeddings for the deleted thread
34+
try {
35+
const semanticIndex = await getSemanticIndex();
36+
await semanticIndex.deleteThread(validatedId);
37+
} catch {
38+
// Non-fatal: semantic index may not exist
39+
}
40+
4541
console.log(`Deleted thread '${validatedId}'.`);
4642
} catch (error) {
4743
console.error(`Error: ${error instanceof Error ? error.message : error}`);
44+
process.exitCode = 1;
4845
}
4946
});

src/commands/detach.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const detachCommand = new Command('detach')
1010

1111
if (!result.success) {
1212
console.error(`Error: ${result.message}`);
13+
process.exitCode = 1;
1314
return;
1415
}
1516

src/commands/explain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const explainCommand = new Command('explain')
1111

1212
if (!result.success) {
1313
console.error(`Error: ${result.message}`);
14+
process.exitCode = 1;
1415
return;
1516
}
1617

src/commands/init.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { Command } from 'commander';
22
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, copyFileSync } from 'fs';
33
import { homedir } from 'os';
44
import { join } from 'path';
5-
import { createInterface } from 'readline';
65
import { getDefaultIgnoreContent } from '../core/ignore.js';
6+
import { prompt } from '../core/utils.js';
77

88
const CLAUDE_DIR = join(homedir(), '.claude');
99
const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
@@ -93,18 +93,9 @@ interface SetupStatus {
9393
mcpJsonValid: boolean;
9494
}
9595

96-
function ask(question: string): Promise<string> {
97-
const rl = createInterface({
98-
input: process.stdin,
99-
output: process.stdout,
100-
});
101-
102-
return new Promise((resolve) => {
103-
rl.question(question, (answer) => {
104-
rl.close();
105-
resolve(answer.trim().toLowerCase());
106-
});
107-
});
96+
async function ask(question: string): Promise<string> {
97+
const answer = await prompt(question);
98+
return answer.trim().toLowerCase();
10899
}
109100

110101
function loadSettings(): { settings: Record<string, unknown>; valid: boolean } {

0 commit comments

Comments
 (0)