diff --git a/packages/tools/src/claude-memory.test.ts b/packages/tools/src/claude-memory.test.ts new file mode 100644 index 00000000..80aea36d --- /dev/null +++ b/packages/tools/src/claude-memory.test.ts @@ -0,0 +1,85 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +// Mock the Supermemory SDK so the Claude memory tool's `view`/`readFile` path +// can be exercised deterministically without any network access. We only need +// `search.execute` to return a single document with known multi-line content. +const searchExecute = vi.fn() + +vi.mock("supermemory", () => { + return { + default: class MockSupermemory { + search = { execute: searchExecute } + add = vi.fn() + memories = { forget: vi.fn() } + }, + } +}) + +import { ClaudeMemoryTool } from "./claude-memory" + +const FILE_PATH = "/memories/notes.txt" +// 5 distinct lines so an off-by-one at either end is observable. +const FILE_CONTENT = "line1\nline2\nline3\nline4\nline5" + +function mockDocument(content: string) { + // `readFile` matches by `documentId === normalizePathToCustomId(path)`. + // normalizePathToCustomId("/memories/notes.txt") -> "memories_notes_txt" + searchExecute.mockResolvedValue({ + results: [{ documentId: "memories_notes_txt", content }], + }) +} + +describe("ClaudeMemoryTool view_range", () => { + let tool: ClaudeMemoryTool + + beforeEach(() => { + searchExecute.mockReset() + mockDocument(FILE_CONTENT) + tool = new ClaudeMemoryTool("test-api-key") + }) + + it("returns the final line when end is the -1 'to end of file' sentinel", async () => { + // Anthropic's text-editor / memory tool convention: an end of -1 means + // "read to the end of the file". The whole file must come back. + const result = await tool.handleCommand({ + command: "view", + path: FILE_PATH, + view_range: [1, -1], + }) + + expect(result.success).toBe(true) + // Regression guard: the last line must not be dropped. + expect(result.content).toContain("line5") + // And every line should be present, in order. + for (const line of ["line1", "line2", "line3", "line4", "line5"]) { + expect(result.content).toContain(line) + } + }) + + it("reads from a start line to the end when end is -1", async () => { + const result = await tool.handleCommand({ + command: "view", + path: FILE_PATH, + view_range: [3, -1], + }) + + expect(result.success).toBe(true) + expect(result.content).toContain("line3") + expect(result.content).toContain("line5") + expect(result.content).not.toContain("line2") + }) + + it("still honors explicit positive ranges", async () => { + const result = await tool.handleCommand({ + command: "view", + path: FILE_PATH, + view_range: [2, 4], + }) + + expect(result.success).toBe(true) + expect(result.content).toContain("line2") + expect(result.content).toContain("line4") + expect(result.content).not.toContain("line1") + expect(result.content).not.toContain("line5") + }) +}) diff --git a/packages/tools/src/claude-memory.ts b/packages/tools/src/claude-memory.ts index b65c8ff9..bbb60547 100644 --- a/packages/tools/src/claude-memory.ts +++ b/packages/tools/src/claude-memory.ts @@ -283,7 +283,13 @@ export class ClaudeMemoryTool { if (viewRange) { const lines = content.split("\n") const [startLine, endLine] = viewRange - const selectedLines = lines.slice(startLine - 1, endLine) + // `endLine === -1` is the documented sentinel for "read to the end + // of the file" (same convention as Anthropic's text-editor tool). + // Passing it straight to Array.slice would be interpreted as a + // from-the-end index and silently drop the final line, so map any + // negative end to the array length. + const sliceEnd = endLine < 0 ? lines.length : endLine + const selectedLines = lines.slice(startLine - 1, sliceEnd) // Format with line numbers const numberedLines = selectedLines.map(