Skip to content

Commit cbfbe7d

Browse files
authored
Merge pull request #4 from thrialectics/delete-command
Delete command
2 parents c03c163 + 5072010 commit cbfbe7d

11 files changed

Lines changed: 144 additions & 32 deletions

File tree

src/commands/delete.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Command } from 'commander';
2-
import { deleteThreadFile, loadMetaIndex, validateTag, prompt } from '../core/index.js';
3-
import { getSemanticIndex } from '../embeddings/index.js';
2+
import { validateTag, prompt, deleteThread } from '../core/index.js';
43

54
export const deleteCommand = new Command('delete')
65
.description('Delete a thread')
@@ -10,15 +9,6 @@ export const deleteCommand = new Command('delete')
109
try {
1110
const validatedId = validateTag(threadId);
1211

13-
// Check existence via meta index
14-
const meta = loadMetaIndex();
15-
if (!meta.threads[validatedId]) {
16-
console.error(`Thread ID '${validatedId}' not found.`);
17-
console.error('Tip: Run `threadlinking list` to see available threads.');
18-
process.exitCode = 1;
19-
return;
20-
}
21-
2212
if (!options.yes) {
2313
const answer = await prompt(
2414
`Delete thread '${validatedId}'? This cannot be undone. (y/N): `
@@ -29,17 +19,18 @@ export const deleteCommand = new Command('delete')
2919
}
3020
}
3121

32-
deleteThreadFile(validatedId);
22+
const result = await deleteThread({ threadId: validatedId });
3323

34-
// Clean up semantic index embeddings for the deleted thread
35-
try {
36-
const semanticIndex = await getSemanticIndex();
37-
await semanticIndex.deleteThread(validatedId);
38-
} catch {
39-
// Non-fatal: semantic index may not exist
24+
if (!result.success) {
25+
console.error(result.message);
26+
if (result.error === 'THREAD_NOT_FOUND') {
27+
console.error('Tip: Run `threadlinking list` to see available threads.');
28+
}
29+
process.exitCode = 1;
30+
return;
4031
}
4132

42-
console.log(`Deleted thread '${validatedId}'.`);
33+
console.log(result.message);
4334
} catch (error) {
4435
console.error(`Error: ${error instanceof Error ? error.message : error}`);
4536
process.exitCode = 1;

src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export * from './types.js';
88
export {
99
loadIndex,
1010
saveIndex,
11-
getIndexPath,
11+
getMetaIndexPath,
1212
getBaseDir,
1313
getThreadsDir,
1414
loadPending,

src/core/operations/delete.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Delete operation - permanently remove a thread and clean up references
2+
// Returns result object for MCP compatibility
3+
4+
import { loadMetaIndex, loadThread, deleteThreadFile, updateThread } from '../storage.js';
5+
import { validateTag } from '../utils.js';
6+
import { getSemanticIndex } from '../../embeddings/index.js';
7+
import type { OperationResult } from '../types.js';
8+
9+
export interface DeleteInput {
10+
threadId: string;
11+
}
12+
13+
export interface DeleteResult {
14+
threadId: string;
15+
snippetsRemoved: number;
16+
filesUnlinked: number;
17+
semanticEntriesRemoved: number;
18+
relatedThreadsUpdated: string[];
19+
}
20+
21+
export async function deleteThread(input: DeleteInput): Promise<OperationResult<DeleteResult>> {
22+
try {
23+
const validatedId = validateTag(input.threadId);
24+
25+
// Check thread exists via meta index (fast)
26+
const meta = loadMetaIndex();
27+
if (!meta.threads[validatedId]) {
28+
return {
29+
success: false,
30+
message: `Thread '${validatedId}' not found.`,
31+
error: 'THREAD_NOT_FOUND',
32+
};
33+
}
34+
35+
// Load full thread data so we can report what's being removed
36+
// and clean up bidirectional related-thread references
37+
const thread = loadThread(validatedId);
38+
const snippetsRemoved = thread?.snippets?.length ?? 0;
39+
const filesUnlinked = thread?.linked_files?.length ?? 0;
40+
const relatedThreads = thread?.related ?? [];
41+
42+
// Remove this thread from every related thread's `related` array.
43+
// Without this, deleting A leaves stale references in B, C, etc.
44+
const relatedThreadsUpdated: string[] = [];
45+
for (const relatedId of relatedThreads) {
46+
try {
47+
updateThread(relatedId, (relatedThread) => {
48+
const related = relatedThread.related || [];
49+
relatedThread.related = related.filter((r) => r !== validatedId);
50+
relatedThread.date_modified = new Date().toISOString();
51+
return relatedThread;
52+
});
53+
relatedThreadsUpdated.push(relatedId);
54+
} catch {
55+
// Related thread may already be gone — not fatal
56+
}
57+
}
58+
59+
// Delete the thread file and remove from meta index
60+
deleteThreadFile(validatedId);
61+
62+
// Clean up semantic index embeddings (non-fatal if index doesn't exist)
63+
let semanticEntriesRemoved = 0;
64+
try {
65+
const semanticIndex = await getSemanticIndex();
66+
semanticEntriesRemoved = await semanticIndex.deleteThread(validatedId);
67+
} catch {
68+
// Semantic index may not exist or may not be initialized
69+
}
70+
71+
return {
72+
success: true,
73+
message: `Thread '${validatedId}' deleted.`,
74+
data: {
75+
threadId: validatedId,
76+
snippetsRemoved,
77+
filesUnlinked,
78+
semanticEntriesRemoved,
79+
relatedThreadsUpdated,
80+
},
81+
};
82+
} catch (error) {
83+
return {
84+
success: false,
85+
message: error instanceof Error ? error.message : String(error),
86+
error: 'DELETE_ERROR',
87+
};
88+
}
89+
}

src/core/operations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
export { addSnippet } from './snippet.js';
55
export { createThread } from './create.js';
6+
export { deleteThread } from './delete.js';
7+
export type { DeleteResult } from './delete.js';
68
export { attachFile, detachFile } from './attach.js';
79
export { explainFile } from './explain.js';
810
export { showThread, getThread } from './show.js';

src/core/operations/semantic.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Semantic search operation
22
// Uses all-MiniLM-L6-v2 embeddings via @xenova/transformers (pure Node.js)
33

4-
import { loadMetaIndex, loadThread, loadIndex, getIndexPath } from '../storage.js';
4+
import { loadMetaIndex, loadThread, loadIndex, getMetaIndexPath } from '../storage.js';
55
import type { OperationResult, Thread } from '../types.js';
66
import { getEmbedder, stopEmbedder } from '../../embeddings/embedder.js';
77
import {
@@ -49,7 +49,7 @@ export async function semanticSearch(
4949

5050
// Check if index is stale and reload if needed
5151
let staleWarning: string | undefined;
52-
const threadIndexPath = getIndexPath();
52+
const threadIndexPath = getMetaIndexPath();
5353
if (fs.existsSync(threadIndexPath)) {
5454
const stats = fs.statSync(threadIndexPath);
5555
if (semanticIndex.isStale(stats.mtime)) {

src/core/storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ export function updateIndex(updateFn: (index: ThreadIndex) => ThreadIndex): Thre
430430

431431
// ===== Path Getters =====
432432

433-
export function getIndexPath(): string {
433+
export function getMetaIndexPath(): string {
434434
return META_INDEX_PATH;
435435
}
436436

src/embeddings/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,6 @@ export function resetSemanticIndex(): void {
255255
defaultIndex = null;
256256
}
257257

258-
export function getIndexPath(): string {
258+
export function getSemanticIndexPath(): string {
259259
return INDEX_DIR;
260260
}

src/mcp/server.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
// Core operations
88
addSnippet,
99
createThread,
10+
deleteThread,
1011
attachFile,
1112
detachFile,
1213
explainFile,
@@ -18,6 +19,7 @@ import {
1819
rebuildSemanticIndex,
1920
getAnalytics,
2021
exportThread,
22+
parseTags,
2123
} from '../core/index.js';
2224
import { VERSION } from '../version.js';
2325

@@ -61,7 +63,7 @@ Proactively save context when:
6163
source: z.string().optional().describe('Source identifier (defaults to claude-code)'),
6264
},
6365
async (args) => {
64-
const tags = args.tags?.split(',').map((t) => t.trim()).filter((t) => t);
66+
const tags = args.tags ? parseTags(args.tags) : undefined;
6567
const result = await addSnippet({
6668
threadId: args.thread_id,
6769
content: args.content,
@@ -113,6 +115,40 @@ Proactively save context when:
113115
}
114116
);
115117

118+
// threadlinking_delete - Permanently delete a thread
119+
server.tool(
120+
'threadlinking_delete',
121+
'Permanently delete a thread and all its snippets. Also cleans up related-thread references and semantic index entries. This cannot be undone.',
122+
{
123+
thread_id: z.string().describe('Thread name to delete'),
124+
},
125+
async (args) => {
126+
const result = await deleteThread({ threadId: args.thread_id });
127+
128+
if (!result.success) {
129+
return {
130+
content: [{ type: 'text', text: `Error: ${result.message}` }],
131+
isError: true,
132+
};
133+
}
134+
135+
const d = result.data!;
136+
const parts: string[] = [result.message];
137+
parts.push(`- ${d.snippetsRemoved} snippet(s) removed`);
138+
parts.push(`- ${d.filesUnlinked} file link(s) removed`);
139+
if (d.relatedThreadsUpdated.length > 0) {
140+
parts.push(`- Updated related threads: ${d.relatedThreadsUpdated.join(', ')}`);
141+
}
142+
if (d.semanticEntriesRemoved > 0) {
143+
parts.push(`- ${d.semanticEntriesRemoved} semantic index entry/entries removed`);
144+
}
145+
146+
return {
147+
content: [{ type: 'text', text: parts.join('\n') }],
148+
};
149+
}
150+
);
151+
116152
// threadlinking_attach - Link a file to a thread
117153
server.tool(
118154
'threadlinking_attach',
@@ -377,7 +413,7 @@ Proactively save context when:
377413
parts.push('## Available Features');
378414
parts.push('');
379415
parts.push('**Core:**');
380-
parts.push('- snippet, attach, detach, explain, show, list, search, create');
416+
parts.push('- snippet, attach, detach, explain, show, list, search, create, delete');
381417
parts.push('');
382418
parts.push('**Advanced:**');
383419
parts.push('- semantic_search (natural language search)');

src/storage.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/types.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

0 commit comments

Comments
 (0)