mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-07-09 17:18:51 +00:00
fix: view_range [start, -1] drops the last line in the Claude memory tool (#1043)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
361064f231
commit
9ad351feb0
2 changed files with 92 additions and 1 deletions
85
packages/tools/src/claude-memory.test.ts
Normal file
85
packages/tools/src/claude-memory.test.ts
Normal file
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue