From 5033036e62a5ec01803d66bcc5964452e1e045fe Mon Sep 17 00:00:00 2001 From: Daniel Klevebring Date: Thu, 25 Jun 2026 09:01:00 +0200 Subject: [PATCH] fix(cli): accept --content/--text for `engram store`; clearer error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transcript analysis showed the model frequently guessed `engram store --content "…"` (or --text) — natural, since `update` uses --content — and hit a silent "store: missing content" failure, leaving memories unsaved. Accept content positionally OR via --content/--text, and make the missing-content error show the correct invocation forms. Adds CLISmokeTests covering all three forms plus the helpful-error path. Co-Authored-By: Claude Opus 4.8 --- Sources/engram/main.swift | 13 ++++++-- Tests/EngramCoreTests/CLISmokeTests.swift | 36 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Sources/engram/main.swift b/Sources/engram/main.swift index a9759bc..fa3d00f 100644 --- a/Sources/engram/main.swift +++ b/Sources/engram/main.swift @@ -58,7 +58,7 @@ func parseOptions(_ args: [String]) -> (positional: [String], options: [String: var positional: [String] = [] var options: [String: String] = [:] var flags: Set = [] - let valued: Set = ["--title", "--tags", "--source", "--limit", "--content", "--verifiability", "--check-anchor", "--confidence", "--reason", "--since"] + let valued: Set = ["--title", "--tags", "--source", "--limit", "--content", "--text", "--verifiability", "--check-anchor", "--confidence", "--reason", "--since"] var index = 0 while index < args.count { let arg = args[index] @@ -174,7 +174,16 @@ do { switch command { case "store": - guard let content = positional.first else { fail("store: missing content") } + // Accept content positionally or via --content/--text. The flag forms are + // common (and natural) model guesses — `update` uses --content, so the + // model often reaches for `engram store --content "…"`. Honoring them here + // turns a silent "missing content" failure into a successful store. + let storeContent = positional.first ?? options["--content"] ?? options["--text"] + guard let content = storeContent, + !content.trimmingCharacters(in: .whitespaces).isEmpty else { + fail("store: missing content. Pass it positionally — engram store \"\" — " + + "or with --content/--text \"\".") + } let tags = options["--tags"]?.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespaces) } ?? [] let source = options["--source"] let verifiability = options["--verifiability"].flatMap(Verifiability.init(rawValue:)) diff --git a/Tests/EngramCoreTests/CLISmokeTests.swift b/Tests/EngramCoreTests/CLISmokeTests.swift index 41a94a4..00dc24a 100644 --- a/Tests/EngramCoreTests/CLISmokeTests.swift +++ b/Tests/EngramCoreTests/CLISmokeTests.swift @@ -64,6 +64,42 @@ private func tempDB() -> URL { #expect(fetchOut.contains("Paris"), "fetch output must include the stored content") } +/// The store argument forms the model actually reaches for must all succeed: +/// positional content, `--content`, and `--text`. Historically only positional +/// worked, so `engram store --content "…"` failed silently with "missing +/// content" — a real cause of un-saved memories (see the store-robustness work). +@Test func cliStoreAcceptsContentAndTextFlags() throws { + for (label, args) in [ + ("positional", ["store", "alpha fact about Paris"]), + ("--content", ["store", "--content", "beta fact about Paris"]), + ("--text", ["store", "--text", "gamma fact about Paris"]), + ] { + let db = tempDB() + defer { try? FileManager.default.removeItem(at: db) } + + let (out, exit) = try engram(args, db: db) + guard exit != -1 else { return } // binary not built — skip + #expect(exit == 0, "engram \(label) store must exit 0 (got: \(out))") + #expect(out.hasPrefix("stored "), "\(label) store must report success") + + let (fetchOut, _) = try engram(["fetch", "Paris", "--limit", "3"], db: db) + #expect(fetchOut.contains("Paris"), "\(label): stored content must be fetchable") + } +} + +/// A store with no content (any form) fails non-zero with an actionable message +/// that points at the correct invocation — not a bare "missing content". +@Test func cliStoreWithoutContentFailsWithHelpfulMessage() throws { + let db = tempDB() + defer { try? FileManager.default.removeItem(at: db) } + + let (out, exit) = try engram(["store"], db: db) + guard exit != -1 else { return } // binary not built — skip + #expect(exit != 0, "a content-less store must fail") + #expect(out.contains("--content") && out.contains("engram store"), + "the error must show the correct invocation forms (got: \(out))") +} + /// `engram stats` exits 0 and emits JSON with a numeric totalActive field. @Test func cliStatsJSON() throws { let db = tempDB()