Search improvements, export/import, MCP server, and Smithery submission#9
Conversation
- Add --json flag to search, recent, show, session, stats for machine-readable output; structured exit codes (1=no results, used in search/show/context/session) - FTS5 operator auto-detection in search/pack/resume: AND/OR/NOT tokens and -/ * prefixes/suffixes pass through raw; plain terms still get quoted for safety. Add --fts flag to force raw passthrough explicitly. - Add prompt_hash column (SHA-256[:16], indexed) on history table; stored on all new inserts across claude/codex/cursor/relay parsers; existing DBs migrated via ALTER TABLE in init_db. - New `pack` command: token-budget-aware evidence bundle with numbered entries + resume commands, designed for pasting into an LLM context window. - New `resume` command: FTS search → best matching session → print exact resume command (claude --resume / codex resume / cursor-agent --resume=); ideal for shell aliases. - Centralise _resume_cmd / _shell_quote / _build_fts_query / _row_to_dict as shared helpers; cmd_show reuses _resume_cmd instead of duplicating logic. - Update tests: parser assertions include prompt_hash; not-found/no-results cases now assert SystemExit(1). https://claude.ai/code/session_01P9bLkkL9takMHdm2i3uq27
Exposes five MCP tools over stdio JSON-RPC 2.0, backed by the existing
SQLite FTS5 database — no extra deps, no daemon, no embeddings service:
search_history — FTS5 search across Claude/Codex/Cursor/Relay history
get_session — full prompt list for a session, with resume command
get_context — same-session + ±N-minute time-window view around an entry
recent_history — N most recent entries with source/project filters
pack_evidence — token-budget evidence bundle designed for LLM handoff
All tool errors surface as MCP isError responses rather than crashes.
Wire into Claude Code or Cursor via MCP config:
{ "mcpServers": { "ai-hist": { "command": "ai-hist", "args": ["mcp"] } } }
This is the only MCP history server covering Claude Code + Codex + Cursor
in a single index; ai-agent-history-rag-mcp (the closest prior art)
omits Cursor and requires LanceDB + an Ollama embeddings service.
https://claude.ai/code/session_01P9bLkkL9takMHdm2i3uq27
Export history to JSONL (default) or SQLite for backup or sharing with colleagues. Import deduplicates against the existing DB so it is safe to run multiple times or merge overlapping exports. ai-hist export # JSONL to stdout (pipeable) ai-hist export history.jsonl.gz # gzip-compressed JSONL ai-hist export backup.db --format sqlite # full SQLite file ai-hist export --source claude --since 2025-01-01 recent.jsonl ai-hist import colleague.jsonl # JSONL or .db, with dedup ai-hist import colleague.jsonl --dry-run # preview without writing Key behaviours: - JSONL rows carry source/session_id/project/prompt/prompt_hash/timestamp_ms - SQLite export uses init_db so the file is immediately usable as $AI_HIST_DB - Import tolerates older exports that lack prompt_hash (backfills via SHA-256) - Filtering: --source, --project, --since on export - 15 new tests covering stdout/file/gz/sqlite/dedup/dry-run/roundtrip https://claude.ai/code/session_01P9bLkkL9takMHdm2i3uq27
- New sdk-ts/src/mcp-server.ts: stdio MCP server via @modelcontextprotocol/sdk with 6 tools (search_history, get_session, get_context, recent_history, pack_evidence, history_stats) and a system prompt guide - sdk-ts/smithery.yaml: runtime: typescript for Smithery submission - sdk-ts/package.json: ai-hist-mcp bin entry, mcp-server export, keywords - sdk-ts/src/index.ts: getEntry() and getInTimeWindow() methods for get_context https://claude.ai/code/session_01P9bLkkL9takMHdm2i3uq27
The zero-dependency Python MCP implementation duplicated ~430 lines of logic already covered by sdk-ts/src/mcp-server.ts. MCP users should use the TypeScript package (npx ai-hist-mcp). Tracks issue #8. https://claude.ai/code/session_01P9bLkkL9takMHdm2i3uq27
|
Warning Review limit reached
More reviews will be available in 47 minutes and 33 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds prompt deduplication via SHA-256 hashing and complete import/export capability to the AI history tool. The Python CLI gains new pack and resume commands, unified JSON output, stricter error handling, and an MCP server for tool exposure. The TypeScript SDK adds query methods and wraps the CLI via Model Context Protocol. ChangesPrompt Hash & History Portability
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a major update to the ai-hist tool, adding database migration support for a new prompt_hash column, new CLI commands for exporting/importing history (supporting JSONL and SQLite formats), and a new pack command for generating token-budget-aware evidence bundles. Crucially, it also implements a TypeScript SDK extension and a new Model Context Protocol (MCP) server that exposes session history as tools for AI coding agents. Feedback on these changes highlights several robustness issues, including a potential crash in FTS5 query building when search terms contain double quotes, a filesystem pollution issue where sqlite3.connect creates empty files for non-existent paths, a potential TypeError during dry-run imports when prompts are null, and logic errors in the MCP server's get_context tool when handling entries without a session ID. Additionally, it is recommended to replace the custom shell quoting helper with the standard shlex.quote module.
| def _build_fts_query(terms, raw=False): | ||
| """Build an FTS5 query string from a list of terms. | ||
|
|
||
| Auto-detects FTS5 operators (uppercase AND/OR/NOT, leading -, trailing *, | ||
| pre-quoted phrases) and passes through raw. Otherwise quotes each term for | ||
| literal matching, which is the safe default for most user input. | ||
| Use raw=True or --fts to force raw FTS5 expression passthrough. | ||
| """ | ||
| if raw: | ||
| return " ".join(terms) | ||
| fts_ops = {"AND", "OR", "NOT"} | ||
| for t in terms: | ||
| if t in fts_ops or t.startswith("-") or t.endswith("*") or (t.startswith('"') and t.endswith('"')): | ||
| return " ".join(terms) | ||
| return " ".join(f'"{t}"' for t in terms) |
There was a problem hiding this comment.
If any search term contains a double quote, the query builder will produce invalid FTS5 syntax (e.g., "he"llo"), causing a sqlite3.OperationalError crash. Double quotes inside FTS5 quoted strings must be escaped by doubling them (e.g., "he""llo").
def _build_fts_query(terms, raw=False):
"""Build an FTS5 query string from a list of terms.
Auto-detects FTS5 operators (uppercase AND/OR/NOT, leading -, trailing *,
pre-quoted phrases) and passes through raw. Otherwise quotes each term for
literal matching, which is the safe default for most user input.
Use raw=True or --fts to force raw FTS5 expression passthrough.
"""
if raw:
return " ".join(terms)
fts_ops = {"AND", "OR", "NOT"}
for t in terms:
if t in fts_ops or t.startswith("-") or t.endswith("*") or (t.startswith('"') and t.endswith('"')):
return " ".join(terms)
return " ".join(f'" {t.replace(\'"\', \'""\')}"' for t in terms)| def _shell_quote(value): | ||
| import re | ||
| if re.match(r'^[A-Za-z0-9_./-]+$', value): | ||
| return value | ||
| return "'" + value.replace("'", "'\\''") + "'" |
| def _load_rows_from_sqlite(path): | ||
| """Load rows from an exported SQLite DB, tolerating missing prompt_hash.""" | ||
| try: | ||
| src = sqlite3.connect(path) | ||
| cols = {r[1] for r in src.execute("PRAGMA table_info(history)").fetchall()} |
There was a problem hiding this comment.
If the specified SQLite file does not exist, sqlite3.connect(path) will automatically create an empty database file before failing with a 'no such table' error. This leaves stray empty files on the filesystem. Check if the file exists before attempting to connect.
def _load_rows_from_sqlite(path):
"""Load rows from an exported SQLite DB, tolerating missing prompt_hash."""
if not os.path.exists(path):
print(f"Error: File '{path}' does not exist.", file=sys.stderr)
sys.exit(1)
try:
src = sqlite3.connect(path)
cols = {r[1] for r in src.execute("PRAGMA table_info(history)").fetchall()}| for row in rows[:5]: | ||
| source = row.get("source", "?") | ||
| project = row.get("project") or "" | ||
| prompt = row.get("prompt", "")[:80].replace("\n", " ") | ||
| ts_ms = row.get("timestamp_ms", 0) | ||
| dt = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts_ms / 1000)) if ts_ms else "?" | ||
| proj = f" [{project}]" if project else "" | ||
| print(f" {dt} ({source}){proj} {prompt}") |
There was a problem hiding this comment.
If row.get("prompt") is None in the imported data, row.get("prompt", "") will return None (since the key exists but has a null value). This causes None[:80] to raise a TypeError during dry-run. Coerce the prompt to a string safely.
| for row in rows[:5]: | |
| source = row.get("source", "?") | |
| project = row.get("project") or "" | |
| prompt = row.get("prompt", "")[:80].replace("\n", " ") | |
| ts_ms = row.get("timestamp_ms", 0) | |
| dt = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts_ms / 1000)) if ts_ms else "?" | |
| proj = f" [{project}]" if project else "" | |
| print(f" {dt} ({source}){proj} {prompt}") | |
| for row in rows[:5]: | |
| source = row.get("source", "?") | |
| project = row.get("project") or "" | |
| prompt = (row.get("prompt") or "")[:80].replace("\n", " ") | |
| ts_ms = row.get("timestamp_ms", 0) | |
| dt = time.strftime("%Y-%m-%d %H:%M", time.localtime(ts_ms / 1000)) if ts_ms else "?" | |
| proj = f" [{project}]" if project else "" | |
| print(f" {dt} ({source}){proj} {prompt}") |
| const lines: string[] = []; | ||
| if (entry.sessionId) { | ||
| const sessionEntries = hist.getSession(entry.sessionId); | ||
| lines.push(`=== Session ${entry.sessionId} (${sessionEntries.length} entries) ===`); | ||
| for (const e of sessionEntries) { | ||
| const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace("T", " "); | ||
| const marker = e.id === id ? ">>>" : " "; | ||
| lines.push(`${marker} [${dt}] #${e.id} (${e.source}) ${e.prompt.slice(0, 150).replace(/\n/g, " ")}`); | ||
| } | ||
| } | ||
| const windowMs = window_minutes * 60 * 1000; | ||
| const nearby = hist | ||
| .getInTimeWindow(entry.timestampMs, windowMs) | ||
| .filter((e) => e.sessionId !== entry.sessionId && e.id !== id); |
There was a problem hiding this comment.
If the target entry has no sessionId (e.g., null), it is never printed in the output. Additionally, the filter e.sessionId !== entry.sessionId becomes e.sessionId !== null, which incorrectly filters out any other nearby entries that also lack a session ID. Handle the null session ID case gracefully to display the target entry and include other sessionless entries in the nearby context.
const lines: string[] = [];
if (entry.sessionId) {
const sessionEntries = hist.getSession(entry.sessionId);
lines.push('=== Session ' + entry.sessionId + ' (' + sessionEntries.length + ' entries) ===');
for (const e of sessionEntries) {
const dt = new Date(e.timestampMs).toISOString().slice(0, 16).replace('T', ' ');
const marker = e.id === id ? '>>>' : ' ';
lines.push(marker + ' [' + dt + '] #' + e.id + ' (' + e.source + ') ' + e.prompt.slice(0, 150).replace(/\n/g, ' '));
}
} else {
const dt = new Date(entry.timestampMs).toISOString().slice(0, 16).replace('T', ' ');
lines.push('=== Target Entry #' + entry.id + ' (No Session) ===');
lines.push('>>> [' + dt + '] (' + entry.source + ') ' + entry.prompt.slice(0, 150).replace(/\n/g, ' '));
}
const windowMs = window_minutes * 60 * 1000;
const nearby = hist
.getInTimeWindow(entry.timestampMs, windowMs)
.filter((e) => e.id !== id && (entry.sessionId === null || e.sessionId !== entry.sessionId));There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
test_ai_hist.py (1)
1628-1628: ⚡ Quick winUse descriptive variable name instead of
l.The static analysis tool flags
las ambiguous (Ruff E741). Consider usinglinefor clarity.Example fix for line 1628
- lines = [l for l in captured.out.strip().splitlines() if l] + lines = [line for line in captured.out.strip().splitlines() if line]Apply the same pattern to the other flagged lines.
Also applies to: 1645-1645, 1659-1659, 1688-1688, 1701-1701
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test_ai_hist.py` at line 1628, Replace the ambiguous single-letter loop variable `l` in the list comprehension that builds `lines` (currently written as `[l for l in captured.out.strip().splitlines() if l]`) with a descriptive name like `line`; update the comprehension to `[line for line in captured.out.strip().splitlines() if line]`, and apply the same rename for the other occurrences flagged (the comprehensions at the other locations referenced in the comment: lines using `l` at the other occurrences).ai-hist (1)
1291-1293: 💤 Low valueDuplicate section comment.
Lines 1291 and 1293 both contain the same
# --- CLI ---comment header.🧹 Remove duplicate comment
-# --- CLI --------------------------------------------------------------------- - # --- CLI ---------------------------------------------------------------------🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ai-hist` around lines 1291 - 1293, Remove the duplicated section header by deleting the redundant "# --- CLI ---------------------------------------------------------------------" line so only one "# --- CLI ---------------------------------------------------------------------" remains; search for the exact string "# --- CLI ---" (full dashed variant shown in the diff) and remove the second occurrence to avoid duplicate section comments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ai-hist`:
- Around line 1198-1201: The import loop silently drops malformed JSON lines
because the except json.JSONDecodeError block just passes; add a parse_errors
counter (e.g., parse_errors = 0 before the loop) and increment it inside the
except block instead of passing, keep collecting valid rows via
rows.append(json.loads(line)), and then propagate/return the parse_errors count
to the caller (update cmd_import to receive and include parse_errors in the
final import summary or log). Ensure the variables referenced are rows,
json.loads, the except json.JSONDecodeError handler, and cmd_import so the
summary reports how many lines were skipped due to parse errors.
- Around line 103-117: The _build_fts_query function can produce malformed FTS5
syntax when a term contains embedded double quotes; fix it by sanitizing terms
before wrapping them in quotes: inside _build_fts_query (when raw is False and
the function falls through to the quoted branch), escape embedded double quotes
(and backslashes) in each term (e.g., replace backslash with double-backslash
then replace " with \") before constructing the f'"{t}"' string so queries like
foo"bar become valid FTS5 phrases; keep the existing early passthrough logic for
explicit FTS operators and raw=True unchanged.
In `@sdk-ts/src/mcp-server.ts`:
- Around line 114-162: The tool description for search_history falsely claims
FTS5 boolean operators and prefix matching while hist.search() (implemented in
index.ts) uses plain LIKE substring matching; fix by updating the search_history
tool text and the query .describe(...) to accurately state it performs
case-insensitive substring (LIKE-style) matching with literal interpretation of
characters (no AND/OR/NOT, no leading - exclusion, no trailing * prefix), and
update the identical claim in the system prompt so messaging matches actual
behavior of hist.search(); reference the search_history handler and
hist.search() when making edits.
---
Nitpick comments:
In `@ai-hist`:
- Around line 1291-1293: Remove the duplicated section header by deleting the
redundant "# --- CLI
---------------------------------------------------------------------" line so
only one "# --- CLI
---------------------------------------------------------------------" remains;
search for the exact string "# --- CLI ---" (full dashed variant shown in the
diff) and remove the second occurrence to avoid duplicate section comments.
In `@test_ai_hist.py`:
- Line 1628: Replace the ambiguous single-letter loop variable `l` in the list
comprehension that builds `lines` (currently written as `[l for l in
captured.out.strip().splitlines() if l]`) with a descriptive name like `line`;
update the comprehension to `[line for line in captured.out.strip().splitlines()
if line]`, and apply the same rename for the other occurrences flagged (the
comprehensions at the other locations referenced in the comment: lines using `l`
at the other occurrences).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c4c9d7f9-4028-42a2-bd49-d1cb58f26266
⛔ Files ignored due to path filters (1)
sdk-ts/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (6)
ai-histsdk-ts/package.jsonsdk-ts/smithery.yamlsdk-ts/src/index.tssdk-ts/src/mcp-server.tstest_ai_hist.py
| try: | ||
| rows.append(json.loads(line)) | ||
| except json.JSONDecodeError: | ||
| pass |
There was a problem hiding this comment.
Silent data loss on malformed JSONL lines during import.
Invalid JSON lines in the import file are silently skipped. The user has no visibility into how many entries were lost. Consider tracking a parse error count and including it in the final import summary.
🔧 Proposed fix to track and report parse errors
def _load_rows_from_jsonl(path):
"""Load rows from a JSONL (optionally gzip-compressed) export file."""
rows = []
+ parse_errors = 0
try:
with _open_file(path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
- pass
+ parse_errors += 1
except (OSError, EOFError) as e:
print(f"Error reading {path}: {e}", file=sys.stderr)
sys.exit(1)
- return rows
+ return rows, parse_errorsThen update cmd_import to handle and report the parse_errors count.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ai-hist` around lines 1198 - 1201, The import loop silently drops malformed
JSON lines because the except json.JSONDecodeError block just passes; add a
parse_errors counter (e.g., parse_errors = 0 before the loop) and increment it
inside the except block instead of passing, keep collecting valid rows via
rows.append(json.loads(line)), and then propagate/return the parse_errors count
to the caller (update cmd_import to receive and include parse_errors in the
final import summary or log). Ensure the variables referenced are rows,
json.loads, the except json.JSONDecodeError handler, and cmd_import so the
summary reports how many lines were skipped due to parse errors.
| server.tool( | ||
| "search_history", | ||
| "Search your AI coding agent history using full-text search across all prompts " + | ||
| "from Claude Code, Codex, Cursor, and Agent Relay. Returns matching entries with " + | ||
| "source, project path, session ID, and timestamp ordered by most recent first. " + | ||
| "Supports FTS5 boolean operators (AND, OR, NOT), leading - to exclude a term, and " + | ||
| "trailing * for prefix matching. Use get_session with a returned session_id to read " + | ||
| "the full conversation.", | ||
| { | ||
| query: z | ||
| .string() | ||
| .describe( | ||
| "Search query. Plain terms are matched literally. Use AND/OR/NOT (uppercase) for " + | ||
| 'boolean, leading - to exclude, trailing * for prefix. ' + | ||
| 'Examples: "authentication", "auth AND login", "deploy*", "refactor -test"', | ||
| ), | ||
| source: z | ||
| .enum(["claude", "codex", "cursor", "relay"]) | ||
| .optional() | ||
| .describe("Filter to a single agent source. Omit to search all sources."), | ||
| project: z | ||
| .string() | ||
| .optional() | ||
| .describe( | ||
| 'Filter by project directory path. Substring match — use a partial path like "/myproject" or "src/api".', | ||
| ), | ||
| limit: z | ||
| .number() | ||
| .int() | ||
| .min(1) | ||
| .max(100) | ||
| .optional() | ||
| .default(20) | ||
| .describe("Maximum number of results to return. Default: 20."), | ||
| }, | ||
| READ_ONLY, | ||
| async ({ query, source, project, limit }) => { | ||
| try { | ||
| const hist = await getHist(); | ||
| const results = hist.search(query, { source, project, limit }); | ||
| if (results.length === 0) return { content: [{ type: "text", text: "No results found." }] }; | ||
| const lines = [`Found ${results.length} result(s):\n`]; | ||
| for (const e of results) lines.push(fmtEntry(e)); | ||
| return { content: [{ type: "text", text: lines.join("\n\n") }] }; | ||
| } catch (err) { | ||
| return { content: [{ type: "text", text: `Error: ${String(err)}` }], isError: true }; | ||
| } | ||
| }, | ||
| ); |
There was a problem hiding this comment.
Tool description overpromises FTS5 behavior the SDK's search() doesn't implement.
This tool (and the query description) advertise FTS5 boolean operators (AND, OR, NOT), leading - to exclude, and trailing * for prefix matching. But hist.search() in index.ts does plain LIKE substring matching and explicitly escapes %/_; it has no FTS5 module. As a result, a query like auth AND login or deploy* is matched literally as a substring, so the model will be told operators work and silently get wrong/empty results.
Either align the descriptions with the actual LIKE behavior, or route this tool through the Python CLI's FTS5 search. Note the same misleading claim appears in the system prompt (lines 76‑78).
📝 Suggested description alignment
"Search your AI coding agent history using full-text search across all prompts " +
"from Claude Code, Codex, Cursor, and Agent Relay. Returns matching entries with " +
- "source, project path, session ID, and timestamp ordered by most recent first. " +
- "Supports FTS5 boolean operators (AND, OR, NOT), leading - to exclude a term, and " +
- "trailing * for prefix matching. Use get_session with a returned session_id to read " +
- "the full conversation.",
+ "source, project path, session ID, and timestamp ordered by most recent first. " +
+ "Matches the query as a case-insensitive substring across prompt and project. " +
+ "Use get_session with a returned session_id to read the full conversation.",Also update the query .describe(...) text (lines 126‑128) and the system prompt bullet (lines 76‑78) accordingly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@sdk-ts/src/mcp-server.ts` around lines 114 - 162, The tool description for
search_history falsely claims FTS5 boolean operators and prefix matching while
hist.search() (implemented in index.ts) uses plain LIKE substring matching; fix
by updating the search_history tool text and the query .describe(...) to
accurately state it performs case-insensitive substring (LIKE-style) matching
with literal interpretation of characters (no AND/OR/NOT, no leading -
exclusion, no trailing * prefix), and update the identical claim in the system
prompt so messaging matches actual behavior of hist.search(); reference the
search_history handler and hist.search() when making edits.
|
✅ pr-reviewer applied fixes — committed and pushed Reviewed the PR diff and traced the changed CLI/SDK surfaces. I fixed one related issue: sdk-ts/README.md was stale and documented the old native/synchronous SDK API. It now matches this PR’s Local checks run:
|
There was a problem hiding this comment.
✅ pr-reviewer applied fixes — committed and pushed 2a17f99 to this PR. The notes below describe what changed.
Reviewed the PR diff and traced the changed CLI/SDK surfaces.
I fixed one related issue: sdk-ts/README.md was stale and documented the old native/synchronous SDK API. It now matches this PR’s sql.js implementation, openAiHist(), JSONL fallback, MCP binary, and current schema.
Local checks run:
pytest -q-> 147 passednpm testinsdk-ts-> TypeScript build passed, node test runner completed- Targeted smoke check for new CLI JSON/search/recent/show/session/stats/pack/resume/export/import paths
npm pack --dry-runverified package contents include the updated README and MCP dist files
There was a problem hiding this comment.
5 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="ai-hist">
<violation number="1" location="ai-hist:1028">
P1: `resume` can choose non-resumable sources (e.g., relay) and fail despite matching resumable sessions.</violation>
<violation number="2" location="ai-hist:1201">
P2: Import silently ignores malformed JSONL lines, which can cause unnoticed partial imports/data loss.</violation>
</file>
<file name="sdk-ts/src/mcp-server.ts">
<violation number="1" location="sdk-ts/src/mcp-server.ts:153">
P2: `search_history` claims/accepts boolean and wildcard query syntax, but it routes to `AiHist.search()` which only does literal `LIKE` matching. MCP users will get incorrect search behavior for `AND/OR/NOT`, `-term`, and `*` queries.</violation>
<violation number="2" location="sdk-ts/src/mcp-server.ts:290">
P2: `project` filter behavior is documented as substring match, but implementation uses exact `project = ?` matching, so partial-path filters silently miss expected results.</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| sql = ( | ||
| "SELECT h.id, h.source, h.session_id, h.project, h.prompt, h.timestamp_ms " | ||
| "FROM history_fts f JOIN history h ON f.rowid = h.id " | ||
| "WHERE history_fts MATCH ? AND h.session_id IS NOT NULL AND h.session_id != '' " |
There was a problem hiding this comment.
P1: resume can choose non-resumable sources (e.g., relay) and fail despite matching resumable sessions.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ai-hist, line 1028:
<comment>`resume` can choose non-resumable sources (e.g., relay) and fail despite matching resumable sessions.</comment>
<file context>
@@ -769,47 +863,196 @@ def cmd_session(args):
+ sql = (
+ "SELECT h.id, h.source, h.session_id, h.project, h.prompt, h.timestamp_ms "
+ "FROM history_fts f JOIN history h ON f.rowid = h.id "
+ "WHERE history_fts MATCH ? AND h.session_id IS NOT NULL AND h.session_id != '' "
+ "ORDER BY h.timestamp_ms DESC LIMIT 1"
+ )
</file context>
| "WHERE history_fts MATCH ? AND h.session_id IS NOT NULL AND h.session_id != '' " | |
| "WHERE history_fts MATCH ? AND h.session_id IS NOT NULL AND h.session_id != '' AND h.source IN ('claude', 'codex', 'cursor') " |
| try: | ||
| rows.append(json.loads(line)) | ||
| except json.JSONDecodeError: | ||
| pass |
There was a problem hiding this comment.
P2: Import silently ignores malformed JSONL lines, which can cause unnoticed partial imports/data loss.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ai-hist, line 1201:
<comment>Import silently ignores malformed JSONL lines, which can cause unnoticed partial imports/data loss.</comment>
<file context>
@@ -823,6 +1066,230 @@ def cmd_watch(args):
+ try:
+ rows.append(json.loads(line))
+ except json.JSONDecodeError:
+ pass
+ except (OSError, EOFError) as e:
+ print(f"Error reading {path}: {e}", file=sys.stderr)
</file context>
| pass | |
| print(f"Invalid JSONL line in {path}: {line[:120]}", file=sys.stderr) | |
| sys.exit(1) |
| async ({ query, source, project, limit }) => { | ||
| try { | ||
| const hist = await getHist(); | ||
| const results = hist.search(query, { source, project, limit }); |
There was a problem hiding this comment.
P2: search_history claims/accepts boolean and wildcard query syntax, but it routes to AiHist.search() which only does literal LIKE matching. MCP users will get incorrect search behavior for AND/OR/NOT, -term, and * queries.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-ts/src/mcp-server.ts, line 153:
<comment>`search_history` claims/accepts boolean and wildcard query syntax, but it routes to `AiHist.search()` which only does literal `LIKE` matching. MCP users will get incorrect search behavior for `AND/OR/NOT`, `-term`, and `*` queries.</comment>
<file context>
@@ -0,0 +1,397 @@
+ async ({ query, source, project, limit }) => {
+ try {
+ const hist = await getHist();
+ const results = hist.search(query, { source, project, limit });
+ if (results.length === 0) return { content: [{ type: "text", text: "No results found." }] };
+ const lines = [`Found ${results.length} result(s):\n`];
</file context>
| async ({ n, source, project }) => { | ||
| try { | ||
| const hist = await getHist(); | ||
| const entries = hist.recent({ limit: n, source, project }); |
There was a problem hiding this comment.
P2: project filter behavior is documented as substring match, but implementation uses exact project = ? matching, so partial-path filters silently miss expected results.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At sdk-ts/src/mcp-server.ts, line 290:
<comment>`project` filter behavior is documented as substring match, but implementation uses exact `project = ?` matching, so partial-path filters silently miss expected results.</comment>
<file context>
@@ -0,0 +1,397 @@
+ async ({ n, source, project }) => {
+ try {
+ const hist = await getHist();
+ const entries = hist.recent({ limit: n, source, project });
+ if (entries.length === 0) return { content: [{ type: "text", text: "No history entries found." }] };
+ const lines = [`Most recent ${entries.length} entries:\n`];
</file context>
|
Reviewed and fixed PR #9 locally. Changes made:
Local checks passed:
|
There was a problem hiding this comment.
Reviewed and fixed PR #9 locally.
Changes made:
- Count and report malformed JSONL import parse errors.
- Added regression coverage for JSONL parse error reporting.
- Updated MCP search descriptions to match actual LIKE-based substring search.
- Aligned TypeScript SDK project filters with documented substring behavior.
- Removed duplicate CLI section header and cleaned ambiguous test variables.
- Updated SDK/root docs for
sql.js, asyncopenAiHist, MCP binary,prompt_hash, andcodex resume. - Added
0.3.0changelog notes.
Local checks passed:
pytest -q: 148 passednpm testinsdk-ts: TypeScript compile passed; Node test runner completed
|
Reviewed and fixed PR #9 locally. Changes made:
Local checks passed:
Smithery config was checked against Smithery’s stdio/startCommand documentation: https://smithery.mintlify.dev/docs/migrations/typescript-with-smithery-cli |
There was a problem hiding this comment.
Reviewed and fixed PR #9 locally.
Changes made:
ai-hist resumenow skips non-resumable sources like relay.- JSONL import now fails loudly on malformed lines before writing partial data.
- SDK project filters now use documented substring matching with escaped SQL wildcards.
- MCP search descriptions now match the SDK’s literal substring behavior.
smithery.yamlnow uses a stdiostartCommandcompatible with the current self-bootstrapping MCP server.- Added regression tests for the Python fixes and SDK project filtering.
Local checks passed:
python -m pytest -q: 149 passednpm testinsdk-ts: 3 passed
Smithery config was checked against Smithery’s stdio/startCommand documentation: https://smithery.mintlify.dev/docs/migrations/typescript-with-smithery-cli
|
Reviewed and fixed PR #9 locally. Key fixes:
Local checks passed:
|
There was a problem hiding this comment.
Reviewed and fixed PR #9 locally.
Key fixes:
ai-hist resumenow skips non-resumable sources likerelay.- JSONL import now fails before writing when malformed/non-object lines are present.
- SDK project filters now use escaped substring matching consistently.
- MCP search descriptions now match actual literal substring behavior.
smithery.yamlnow uses a stdiostartCommandfor the current self-bootstrapping MCP server.- Added Python and TypeScript regression tests.
Local checks passed:
python -m pytest -q: 149 passednpm testinsdk-ts: 4 passedpython -m py_compile ai-hist
|
Reviewed PR #9 locally and made fixes. Changed:
Local checks run:
|
There was a problem hiding this comment.
Reviewed PR #9 locally and made fixes.
Changed:
- Fixed FTS query construction in ai-hist so boolean searches still quote literal operands like
agent-relay, andfoo AND -baremits valid FTS. - Added regression coverage in test_ai_hist.py.
- Made SDK project filters use substring matching, matching CLI/MCP docs, in sdk-ts/src/index.ts.
- Corrected MCP search wording to match actual SDK substring search in sdk-ts/src/mcp-server.ts.
- Fixed the Codex resume example in README.md.
Local checks run:
python -m pytest -q-> 149 passedpython -m compileall -q ai-hist test_ai_hist.pynpm run build- SDK SQLite smoke test for partial project filters
|
CodeAnt AI is reviewing your PR. |
Summary
--jsonoutput on all commands (search,recent,show,session,stats,pack,resume) for scripting and pipingsearch:AND,OR,NOT(uppercase), leading-to exclude, trailing*for prefix wildcardsprompt_hashcolumn (SHA-256[:16]) added to the schema for dedup and fast lookup; existing DBs migrate automaticallypackcommand — assembles a token-budget-aware evidence bundle from search results, ready to paste into a new LLM contextresumecommand — finds the best matching session and prints the exact CLI command to continue it (claude --resume,codex resume, etc.)exportcommand — dump history to JSONL (stdout, file, or.gz) or SQLite; supports--source,--project,--sincefiltersimportcommand — ingest an exported JSONL/.gz/.dbwith dedup and--dry-runpreviewsdk-ts/src/mcp-server.ts) — 6 tools (search_history,get_session,get_context,recent_history,pack_evidence,history_stats) with a system prompt, tool annotations, and Smithery-ready descriptionssmithery.yamladded tosdk-ts/for Smithery submissioncmd_mcpdropped (~430 lines); MCP users usenpx ai-hist-mcpinstead (tracks Migrate core logic to Rust crate to eliminate Python/TypeScript duplication #8)getEntry()andgetInTimeWindow()methods poweringget_contextTest plan
python3 -m pytest test_ai_hist.py— 141 tests, all passingcd sdk-ts && npm run build— clean TypeScript compileai-hist search,pack,resume,export,importagainst a real history DBnpx ai-hist-mcpconnects as an MCP server in Claude CodeGenerated by Claude Code