Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
acfb4e0
agentgrep(feat[export]): Add safe core export
tony Jul 12, 2026
3708440
agentgrep(fix[export]): Hold private dir fd
tony Jul 12, 2026
df56d83
agentgrep(fix[export]): Validate artifacts
tony Jul 12, 2026
2421cdc
agentgrep(refactor[export]): Hide prepared seam
tony Jul 12, 2026
285916d
agentgrep(feat[export]): Add headless CLI
tony Jul 12, 2026
2462c21
agentgrep(fix[export]): Protect all stores
tony Jul 12, 2026
3135d0f
agentgrep(feat[mcp]): Add bounded export
tony Jul 12, 2026
2c744ae
agentgrep(fix[mcp]): Keep export envelope
tony Jul 12, 2026
abf30f8
agentgrep(fix[mcp]): Reject aliased refs
tony Jul 12, 2026
b5acc41
agentgrep(fix[mcp]): Fail closed on discovery
tony Jul 12, 2026
fa74adf
agentgrep(docs[mcp]): Clarify ref dedupe
tony Jul 12, 2026
d9ed230
agentgrep(feat[export]): Add TUI workflow
tony Jul 12, 2026
3d1e07b
agentgrep(fix[export]): Preserve record request
tony Jul 12, 2026
8fd863d
agentgrep(docs[export]): Define portability
tony Jul 12, 2026
78e0c32
agentgrep(docs[mcp]): Register export surface
tony Jul 12, 2026
cd46008
agentgrep(fix[mcp]): Advertise export tool
tony Jul 12, 2026
4877e07
agentgrep(fix[export]): Redact phase errors
tony Jul 12, 2026
33bb095
agentgrep(fix[mcp]): Reject unsafe ref paths
tony Jul 12, 2026
4099403
agentgrep(fix[mcp]): Index ref fingerprints
tony Jul 12, 2026
9828ec2
agentgrep(fix[mcp]): Bound opaque refs
tony Jul 12, 2026
2a36cc3
agentgrep(fix[tui]): Keep filenames literal
tony Jul 12, 2026
8fd9624
agentgrep(fix[export]): Version Markdown
tony Jul 12, 2026
21dc56b
agentgrep(fix[mcp]): Fit PATH_MAX refs
tony Jul 12, 2026
dd0a218
agentgrep(fix[mcp]): Reject tilde users
tony Jul 12, 2026
5f01267
agentgrep(fix[docs]): Bound inspect refs
tony Jul 12, 2026
eb90e1c
agentgrep(feat[tui]): Export selected with e
tony Jul 14, 2026
6c0fc23
agentgrep(fix[tui]): Require pane selection
tony Jul 14, 2026
96e02f1
agentgrep(fix[tui]): Export focused pane row
tony Jul 14, 2026
c22040b
agentgrep(feat[tui]): Add export preferences
tony Jul 14, 2026
f10b1ed
agentgrep(fix[tui]): Contain tilde paths
tony Jul 14, 2026
391d19e
agentgrep(fix[tui]): Secure config writes
tony Jul 14, 2026
c3775c6
agentgrep(feat[tui]): Complete export directories
tony Jul 14, 2026
0410bde
agentgrep(feat[tui]): Add export dialog
tony Jul 14, 2026
c030a77
agentgrep(fix[tui]): Preserve export input
tony Jul 14, 2026
0e7ab5c
agentgrep(feat[tui]): Confirm selected exports
tony Jul 14, 2026
3a76f4c
agentgrep(fix[tui]): Retain saving export
tony Jul 14, 2026
0ee0b94
agentgrep(fix[tui]): Preload export settings
tony Jul 14, 2026
f63bae5
agentgrep(docs[export]): Explain TUI save flow
tony Jul 14, 2026
e069aef
agentgrep(docs[export]): Narrow MCP authority
tony Jul 14, 2026
de7b4f9
agentgrep(docs[export]): Clarify first use
tony Jul 14, 2026
23822cf
agentgrep(test[export]): Scope docs contracts
tony Jul 14, 2026
e821f47
agentgrep(docs[export]): Clarify persistence
tony Jul 14, 2026
81e0db8
agentgrep(fix[tui]): Validate template grammar
tony Jul 14, 2026
562ae60
agentgrep(fix[tui]): Use session home
tony Jul 14, 2026
62613bf
agentgrep(fix[tui]): Serialize directory scans
tony Jul 14, 2026
8b1446e
agentgrep(fix[tui]): Bound preference reads
tony Jul 14, 2026
063cbed
agentgrep(fix[tui]): Reject format controls
tony Jul 14, 2026
bbd8b0f
agentgrep(fix[tui]): Create default directory
tony Jul 14, 2026
d988ae5
agentgrep(fix[tui]): Show accepted save
tony Jul 14, 2026
c797d20
agentgrep(fix[tui]): Scroll compact export
tony Jul 14, 2026
1b7455b
agentgrep(docs[tui]): Explain export review
tony Jul 14, 2026
b9d9036
agentgrep(fix[tui]): Keep save errors inline
tony Jul 14, 2026
22dd01e
agentgrep(fix[tui]): Compact home drafts
tony Jul 14, 2026
599c178
agentgrep(fix[tui]): Clear edit on Ctrl-C
tony Jul 14, 2026
43682e9
agentgrep(fix[tui]): Keep async errors visible
tony Jul 14, 2026
3de3763
agentgrep(fix[tui]): Gate deferred errors
tony Jul 14, 2026
bf5233a
agentgrep(fix[tui]): Reject unsafe paths
tony Jul 14, 2026
ab2af12
agentgrep(fix[tui]): Defer default creation
tony Jul 14, 2026
42139db
agentgrep(refactor[tui]): Narrow widget exports
tony Jul 14, 2026
e10e086
agentgrep(docs[tui]): Clarify export key focus
tony Jul 14, 2026
18e5474
agentgrep(fix[tui]): Reject empty export path
tony Jul 14, 2026
0eb2fc8
agentgrep(fix[tui]): Bound export directory
tony Jul 14, 2026
dbac271
agentgrep(feat[tui]): Polish export dialog
tony Jul 14, 2026
e842cfb
agentgrep(refactor[tui]): Move export to pane
tony Jul 14, 2026
ff05150
agentgrep(fix[tui]): Retain slash queries
tony Jul 14, 2026
7c8492b
agentgrep(fix[tui]): Contain export focus
tony Jul 14, 2026
6b40338
agentgrep(fix[tui]): Guard pane mounting
tony Jul 14, 2026
03740d6
agentgrep(fix[tui]): Keep wide results visible
tony Jul 14, 2026
ae34531
agentgrep(docs[tui]): Name thread export
tony Jul 14, 2026
82d9404
agentgrep(fix[mcp]): Isolate export metadata
tony Jul 18, 2026
8c1b6c1
agentgrep(fix[tui]): Use public watchers
tony Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ captures a clean SVG without changing the active search. `/keys`, theme
switching, filtered-row colors, focus repair, and repeated-key quit
confirmation now behave consistently across the two layouts.

#### Portable record export across CLI, TUI, and MCP (#81)

agentgrep can now turn selected search records into deterministic NDJSON or
human-readable Markdown without changing the underlying histories. The CLI
exports matching records to standard output or a chosen file, the HUD exports
one selected record or its observed thread, and MCP returns a bounded inline
artifact for existing search refs. In the HUD, `e` or transient
`/export [PATH]` captures the selected record, then replaces the right detail
region with a compact destination review without discarding the current search
or reader state. `e` remains ordinary text when an input is focused.

The review remembers its directory and filename template, previews a
filesystem-safe local timestamp with a bounded title, and keeps the draft when
No is selected. Saving refuses accidental replacement. Automatic private
exports keep canonical-ID filenames, while CLI and MCP remain isolated from
the TUI preference. Machine clients must opt in before an MCP export includes
prompt or history bodies. See {ref}`the export guide <cli-export>` for formats,
selection, and privacy details.

## agentgrep 0.1.0a50 (2026-08-09)

agentgrep 0.1.0a50 makes the store catalogue describe an agent's whole
Expand Down
46 changes: 46 additions & 0 deletions docs/_ext/agentgrep_fastmcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from agentgrep.mcp import (
AgentSelector,
CatalogAgentSelector,
ExportRecordsResponse,
FindToolResponse,
SearchScopeName,
SearchToolResponse,
Expand All @@ -31,6 +32,7 @@
StoreDescriptorModel,
ValidateQueryResponse,
)
from agentgrep.mcp.refs import MAX_RECORD_REF_CHARS
from agentgrep.query.help import query_language_summary

READONLY_TAGS = {"readonly", "agentgrep"}
Expand Down Expand Up @@ -126,6 +128,49 @@ async def search(
)


async def export_records(
refs: t.Annotated[
list[
t.Annotated[
str,
Field(min_length=1, max_length=MAX_RECORD_REF_CHARS),
]
],
Field(
min_length=1,
max_length=20,
description="One to 20 opaque refs returned by search.",
),
],
format: t.Annotated[ # noqa: A002 - public MCP argument name.
t.Literal["ndjson", "markdown"],
Field(description="Inline artifact format."),
] = "ndjson",
selection: t.Annotated[
t.Literal["records", "thread"],
Field(description="Export flat records or one observed thread."),
] = "records",
include_bodies: t.Annotated[
bool,
Field(description="Include prompt/history text in the artifact."),
] = False,
) -> ExportRecordsResponse:
"""Return selected refs as one NDJSON or Markdown TextContent artifact with structured export metadata.""" # noqa: E501
raise NotImplementedError(DOCS_ONLY_MESSAGE)


t.cast(t.Any, export_records).__fastmcp__ = types.SimpleNamespace(
name="export_records",
title="Export Records",
tags=READONLY_TAGS | {"export"},
annotations=types.SimpleNamespace(
readOnlyHint=True,
idempotentHint=True,
openWorldHint=False,
),
)


async def find(
pattern: t.Annotated[
str | None,
Expand Down Expand Up @@ -268,6 +313,7 @@ async def inspect_result(
str,
Field(
min_length=1,
max_length=MAX_RECORD_REF_CHARS,
description="Opaque ref from a search or find result.",
),
],
Expand Down
144 changes: 144 additions & 0 deletions docs/cli/export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
(cli-export)=

# agentgrep export

`agentgrep export` turns records matched by the shared search engine into a
portable artifact without modifying an agent's history. There are exactly two
formats. Use `ndjson` for scripts and `markdown` for reading or sharing. The
default format is `ndjson`, and the default sink is standard output. Record
bodies are included by default because running `export` is an explicit choice.

The command accepts the same agent, scope, case, and query-language filters as
search. Terms are combined with AND semantics, and the default scope is
`prompts`. The default limit is `100`; set `--limit` to any value from `1`
through `1000`.

## TUI reviewed save

Press `e` while an exact selected record has focus in the HUD results or detail
pane, or type `/export [PATH]`. Both routes open the same reviewed flow in the
right detail pane. The command text is transient: the pane restores the current
search term and its exact selection, then returns to the originating focus on
back or save. An optional path seeds the directory and filename fields.

The pane remembers the export directory and filename template in TUI-private
user configuration. On first use, the filename template is
`{date} {time} - {title}.md`; after the preferences are saved successfully, the
remembered directory and template replace the first-use defaults. Directory
completion lists existing child directories and accepts a choice with the
arrow keys and Tab.

The preview freezes local time when the pane opens. The date and time render as
the filesystem-safe `YYYY-MM-DD HH-MM-SS`, and the title token uses a bounded
normalized form of the record title without reading its body or source path.
Submitting the draft shows the directory and exact filename separately. The
confirmation starts on **No**; No returns to editing with both values intact.

Save is the mutation boundary: No and cancel perform no filesystem mutation.
An accepted Save creates the exact app-owned default directory privately when
needed, writes only the reviewed explicit no-clobber artifact, then attempts to
write the TUI-private preference file. If the artifact name already exists,
agentgrep returns to the same draft instead of replacing the file or silently
choosing another name; a later preference failure does not erase a completed
artifact. The one-shot `/export-thread` command keeps its automatic private
canonical-ID name. CLI and MCP do not consume the TUI preference: the
CLI still uses standard output or an explicit `--output` path, and MCP still
returns a bounded inline artifact, accepts no local destination, and gains no
filesystem write authority.

## Examples

Export matching prompt records as NDJSON to standard output:

```console
$ agentgrep export "release notes"
```

Omit prompt and history text while retaining portable metadata:

```console
$ agentgrep export "release notes" --no-bodies
```

Write human-readable Markdown to standard output:

```console
$ agentgrep export "release notes" --format markdown
```

Search prompts and conversation records together:

```console
$ agentgrep export "release notes" --scope all
```

Write NDJSON to a new relative file:

```console
$ agentgrep export "release notes" -o records.ndjson
```

Replace an existing regular file deliberately:

```console
$ agentgrep export "release notes" -o records.ndjson --force
```

`-o -` names standard output explicitly. `--force` is invalid with standard
output; it applies only to a file destination.

## Formats and privacy

NDJSON contains one canonical JSON object per record. Keys and record order
are stable, and a trailing newline separates every object. Markdown presents
the same allowlisted metadata as headings and lists. With bodies enabled, it
places exact valid UTF-8 text in a dynamically sized code fence so backticks in
the record cannot close the block.

Both formats include the record schema version, agent, store, kind, role,
timestamp, model, content ID, optional record ID and stability, and optional
thread ID. `--no-bodies` omits the `text` field or body section entirely.
Neither format carries source or display paths, titles, session IDs,
conversation IDs, project origin, or adapter metadata. See {ref}`ADR 0017
<adr-portable-record-export>` for the exact allowlist and ordering contract.

NDJSON represents lone surrogate code points as JSON escapes, so the artifact
remains valid UTF-8 and a JSON decoder can recover the original string.
Markdown cannot represent those values as valid UTF-8 and returns a path-free
encoding error instead of altering the text.

(cli-export-files)=

## File safety

File output refuses to overwrite an existing destination unless `--force` is
present. Even with force, the destination must be a regular file: agentgrep
rejects a symlink, a symlinked parent, and any lexical, resolved, or hard-link
alias of a source store. The CLI protects every discovered source store,
including non-default stores and stores that did not match the query, so an
export cannot replace history by choosing it as the destination.

The completed artifact is installed atomically with private file permissions.
Errors do not include the destination or source path. These rules preserve
agentgrep's read-only treatment of Codex, Claude Code, Cursor, and every other
source store. CLI export writes only the chosen artifact; a successful reviewed
TUI Save may additionally update its TUI-private preference file as described
above.

## Exit status

The command exits `0` when at least one record is exported and `1` when the
search has no matches, producing an empty valid artifact. Invalid arguments,
source failures, encoding failures, and output failures exit `2` with a
path-free diagnostic on standard error.

## Command

```{eval-rst}
.. argparse::
:module: agentgrep
:func: build_docs_parser
:prog: agentgrep
:path: export
:nodescription:
```
7 changes: 7 additions & 0 deletions docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ Ranked, deduped search grouped by session — best matches first.
Enumerate on-disk stores with fd-shaped flag grammar.
:::

:::{grid-item-card} agentgrep export
:link: export
:link-type: doc
Save deterministic NDJSON or Markdown without changing source histories.
:::

:::{grid-item-card} agentgrep ui
:link: tui
:link-type: ref
Expand Down Expand Up @@ -172,5 +178,6 @@ $ agentgrep
grep
search
find
export
reference
```
2 changes: 2 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
"SearchToolResponse",
"FindRequestModel",
"FindToolResponse",
"ExportRecordsRequest",
"ExportRecordsResponse",
"ResultStatsModel",
"SearchPageModel",
"PageInfoModel",
Expand Down
Loading
Loading