From 7fdde860c95a608b29b0c1c2138b04d550989486 Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:42:04 -0400 Subject: [PATCH 1/8] Remove dead re-export shims (src/storage.ts, utils.ts, types.ts) --- src/storage.ts | 2 -- src/types.ts | 2 -- src/utils.ts | 2 -- 3 files changed, 6 deletions(-) delete mode 100644 src/storage.ts delete mode 100644 src/types.ts delete mode 100644 src/utils.ts diff --git a/src/storage.ts b/src/storage.ts deleted file mode 100644 index 2923e3c..0000000 --- a/src/storage.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from core for backwards compatibility -export * from './core/storage.js'; diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 13c8cc8..0000000 --- a/src/types.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from core for backwards compatibility -export * from './core/types.js'; diff --git a/src/utils.ts b/src/utils.ts deleted file mode 100644 index 585021c..0000000 --- a/src/utils.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from core for backwards compatibility -export * from './core/utils.js'; From 46917daa829639f2f50649da7fac91f7f361c4ca Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:42:29 -0400 Subject: [PATCH 2/8] Rename getIndexPath to getMetaIndexPath/getSemanticIndexPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two functions with the same name returned different paths (meta index vs semantic index). Renamed for clarity: storage version → getMetaIndexPath, embeddings → getSemanticIndexPath. --- src/core/index.ts | 2 +- src/core/operations/semantic.ts | 4 ++-- src/core/storage.ts | 2 +- src/embeddings/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/index.ts b/src/core/index.ts index cde1110..cdfd695 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -8,7 +8,7 @@ export * from './types.js'; export { loadIndex, saveIndex, - getIndexPath, + getMetaIndexPath, getBaseDir, getThreadsDir, loadPending, diff --git a/src/core/operations/semantic.ts b/src/core/operations/semantic.ts index eace2df..772d201 100644 --- a/src/core/operations/semantic.ts +++ b/src/core/operations/semantic.ts @@ -1,7 +1,7 @@ // Semantic search operation // Uses all-MiniLM-L6-v2 embeddings via @xenova/transformers (pure Node.js) -import { loadMetaIndex, loadThread, loadIndex, getIndexPath } from '../storage.js'; +import { loadMetaIndex, loadThread, loadIndex, getMetaIndexPath } from '../storage.js'; import type { OperationResult, Thread } from '../types.js'; import { getEmbedder, stopEmbedder } from '../../embeddings/embedder.js'; import { @@ -49,7 +49,7 @@ export async function semanticSearch( // Check if index is stale and reload if needed let staleWarning: string | undefined; - const threadIndexPath = getIndexPath(); + const threadIndexPath = getMetaIndexPath(); if (fs.existsSync(threadIndexPath)) { const stats = fs.statSync(threadIndexPath); if (semanticIndex.isStale(stats.mtime)) { diff --git a/src/core/storage.ts b/src/core/storage.ts index b82a596..9eca88a 100644 --- a/src/core/storage.ts +++ b/src/core/storage.ts @@ -430,7 +430,7 @@ export function updateIndex(updateFn: (index: ThreadIndex) => ThreadIndex): Thre // ===== Path Getters ===== -export function getIndexPath(): string { +export function getMetaIndexPath(): string { return META_INDEX_PATH; } diff --git a/src/embeddings/index.ts b/src/embeddings/index.ts index 7f4cad6..08fd60d 100644 --- a/src/embeddings/index.ts +++ b/src/embeddings/index.ts @@ -255,6 +255,6 @@ export function resetSemanticIndex(): void { defaultIndex = null; } -export function getIndexPath(): string { +export function getSemanticIndexPath(): string { return INDEX_DIR; } From 7605adae6f149692c80265b0baef86e457940224 Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Mon, 16 Mar 2026 01:42:39 -0400 Subject: [PATCH 3/8] =?UTF-8?q?Fix=20MCP=20tag=20casing=20bug=20=E2=80=94?= =?UTF-8?q?=20use=20parseTags()=20instead=20of=20inline=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP inline parsing was missing toLowerCase(), causing tags to be stored with inconsistent casing vs CLI. Now uses shared parseTags() from core. --- src/mcp/server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 937fa87..1953623 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -18,6 +18,7 @@ import { rebuildSemanticIndex, getAnalytics, exportThread, + parseTags, } from '../core/index.js'; import { VERSION } from '../version.js'; @@ -61,7 +62,7 @@ Proactively save context when: source: z.string().optional().describe('Source identifier (defaults to claude-code)'), }, async (args) => { - const tags = args.tags?.split(',').map((t) => t.trim()).filter((t) => t); + const tags = args.tags ? parseTags(args.tags) : undefined; const result = await addSnippet({ threadId: args.thread_id, content: args.content, From abeff807216c393e846c38b2b1ba953017c2ab0d Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:07:49 -0400 Subject: [PATCH 4/8] Add deleteThread core operation with related-thread cleanup Creates src/core/operations/delete.ts following the established operation pattern. Handles validation, existence check, bidirectional related-thread reference cleanup (fixes stale references bug), thread file deletion, and non-fatal semantic index cleanup. Returns detailed DeleteResult stats. --- src/core/operations/delete.ts | 89 +++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/core/operations/delete.ts diff --git a/src/core/operations/delete.ts b/src/core/operations/delete.ts new file mode 100644 index 0000000..dfe3dde --- /dev/null +++ b/src/core/operations/delete.ts @@ -0,0 +1,89 @@ +// Delete operation - permanently remove a thread and clean up references +// Returns result object for MCP compatibility + +import { loadMetaIndex, loadThread, deleteThreadFile, updateThread } from '../storage.js'; +import { validateTag } from '../utils.js'; +import { getSemanticIndex } from '../../embeddings/index.js'; +import type { OperationResult } from '../types.js'; + +export interface DeleteInput { + threadId: string; +} + +export interface DeleteResult { + threadId: string; + snippetsRemoved: number; + filesUnlinked: number; + semanticEntriesRemoved: number; + relatedThreadsUpdated: string[]; +} + +export async function deleteThread(input: DeleteInput): Promise> { + try { + const validatedId = validateTag(input.threadId); + + // Check thread exists via meta index (fast) + const meta = loadMetaIndex(); + if (!meta.threads[validatedId]) { + return { + success: false, + message: `Thread '${validatedId}' not found.`, + error: 'THREAD_NOT_FOUND', + }; + } + + // Load full thread data so we can report what's being removed + // and clean up bidirectional related-thread references + const thread = loadThread(validatedId); + const snippetsRemoved = thread?.snippets?.length ?? 0; + const filesUnlinked = thread?.linked_files?.length ?? 0; + const relatedThreads = thread?.related ?? []; + + // Remove this thread from every related thread's `related` array. + // Without this, deleting A leaves stale references in B, C, etc. + const relatedThreadsUpdated: string[] = []; + for (const relatedId of relatedThreads) { + try { + updateThread(relatedId, (relatedThread) => { + const related = relatedThread.related || []; + relatedThread.related = related.filter((r) => r !== validatedId); + relatedThread.date_modified = new Date().toISOString(); + return relatedThread; + }); + relatedThreadsUpdated.push(relatedId); + } catch { + // Related thread may already be gone — not fatal + } + } + + // Delete the thread file and remove from meta index + deleteThreadFile(validatedId); + + // Clean up semantic index embeddings (non-fatal if index doesn't exist) + let semanticEntriesRemoved = 0; + try { + const semanticIndex = await getSemanticIndex(); + semanticEntriesRemoved = await semanticIndex.deleteThread(validatedId); + } catch { + // Semantic index may not exist or may not be initialized + } + + return { + success: true, + message: `Thread '${validatedId}' deleted.`, + data: { + threadId: validatedId, + snippetsRemoved, + filesUnlinked, + semanticEntriesRemoved, + relatedThreadsUpdated, + }, + }; + } catch (error) { + return { + success: false, + message: error instanceof Error ? error.message : String(error), + error: 'DELETE_ERROR', + }; + } +} From a9ec0d0bd1bca21a352a30a0c63d989ee0d7f375 Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:09:26 -0400 Subject: [PATCH 5/8] Export deleteThread from operations index --- src/core/operations/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/operations/index.ts b/src/core/operations/index.ts index 310755d..8465b1c 100644 --- a/src/core/operations/index.ts +++ b/src/core/operations/index.ts @@ -3,6 +3,8 @@ export { addSnippet } from './snippet.js'; export { createThread } from './create.js'; +export { deleteThread } from './delete.js'; +export type { DeleteResult } from './delete.js'; export { attachFile, detachFile } from './attach.js'; export { explainFile } from './explain.js'; export { showThread, getThread } from './show.js'; From 2c9863032bc3f3c4c44a19e064c51a38349e8de8 Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:10:43 -0400 Subject: [PATCH 6/8] Add threadlinking_delete MCP tool Exposes thread deletion as an MCP tool. Calls the deleteThread core operation and returns a summary of what was cleaned up (snippets, file links, related threads, semantic entries). Description explicitly marks the operation as permanent and irreversible. --- src/mcp/server.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 1953623..9a43d59 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -7,6 +7,7 @@ import { // Core operations addSnippet, createThread, + deleteThread, attachFile, detachFile, explainFile, @@ -114,6 +115,40 @@ Proactively save context when: } ); + // threadlinking_delete - Permanently delete a thread + server.tool( + 'threadlinking_delete', + 'Permanently delete a thread and all its snippets. Also cleans up related-thread references and semantic index entries. This cannot be undone.', + { + thread_id: z.string().describe('Thread name to delete'), + }, + async (args) => { + const result = await deleteThread({ threadId: args.thread_id }); + + if (!result.success) { + return { + content: [{ type: 'text', text: `Error: ${result.message}` }], + isError: true, + }; + } + + const d = result.data!; + const parts: string[] = [result.message]; + parts.push(`- ${d.snippetsRemoved} snippet(s) removed`); + parts.push(`- ${d.filesUnlinked} file link(s) removed`); + if (d.relatedThreadsUpdated.length > 0) { + parts.push(`- Updated related threads: ${d.relatedThreadsUpdated.join(', ')}`); + } + if (d.semanticEntriesRemoved > 0) { + parts.push(`- ${d.semanticEntriesRemoved} semantic index entry/entries removed`); + } + + return { + content: [{ type: 'text', text: parts.join('\n') }], + }; + } + ); + // threadlinking_attach - Link a file to a thread server.tool( 'threadlinking_attach', From f48b3fc374b7102b97b054332577cb4a53ba4ae7 Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:11:35 -0400 Subject: [PATCH 7/8] Refactor CLI delete to use deleteThread core operation Replaces direct storage calls and inline semantic cleanup with a single call to deleteThread(). CLI now only handles confirmation prompting and output formatting. Gets related-thread cleanup for free from the operation. --- src/commands/delete.ts | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/commands/delete.ts b/src/commands/delete.ts index 2027d51..200893b 100644 --- a/src/commands/delete.ts +++ b/src/commands/delete.ts @@ -1,6 +1,5 @@ import { Command } from 'commander'; -import { deleteThreadFile, loadMetaIndex, validateTag, prompt } from '../core/index.js'; -import { getSemanticIndex } from '../embeddings/index.js'; +import { validateTag, prompt, deleteThread } from '../core/index.js'; export const deleteCommand = new Command('delete') .description('Delete a thread') @@ -10,15 +9,6 @@ export const deleteCommand = new Command('delete') try { const validatedId = validateTag(threadId); - // Check existence via meta index - const meta = loadMetaIndex(); - if (!meta.threads[validatedId]) { - console.error(`Thread ID '${validatedId}' not found.`); - console.error('Tip: Run `threadlinking list` to see available threads.'); - process.exitCode = 1; - return; - } - if (!options.yes) { const answer = await prompt( `Delete thread '${validatedId}'? This cannot be undone. (y/N): ` @@ -29,17 +19,18 @@ export const deleteCommand = new Command('delete') } } - deleteThreadFile(validatedId); + const result = await deleteThread({ threadId: validatedId }); - // Clean up semantic index embeddings for the deleted thread - try { - const semanticIndex = await getSemanticIndex(); - await semanticIndex.deleteThread(validatedId); - } catch { - // Non-fatal: semantic index may not exist + if (!result.success) { + console.error(result.message); + if (result.error === 'THREAD_NOT_FOUND') { + console.error('Tip: Run `threadlinking list` to see available threads.'); + } + process.exitCode = 1; + return; } - console.log(`Deleted thread '${validatedId}'.`); + console.log(result.message); } catch (error) { console.error(`Error: ${error instanceof Error ? error.message : error}`); process.exitCode = 1; From 5072010f58ae168d53e7aa944cf2cd82f457fc5a Mon Sep 17 00:00:00 2001 From: thrialectics <203729272+thrialectics@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:40:17 -0400 Subject: [PATCH 8/8] Add delete to threadlinking_status core feature list --- src/mcp/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 9a43d59..441e088 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -413,7 +413,7 @@ Proactively save context when: parts.push('## Available Features'); parts.push(''); parts.push('**Core:**'); - parts.push('- snippet, attach, detach, explain, show, list, search, create'); + parts.push('- snippet, attach, detach, explain, show, list, search, create, delete'); parts.push(''); parts.push('**Advanced:**'); parts.push('- semantic_search (natural language search)');