Skip to content

Commit 5ed9fa7

Browse files
jpheinclaude
andcommitted
feat(cli): add --source flag to mine subcommand (#57)
Route `mempalace mine --source <adapter>` through the source-adapter plugin contract instead of the built-in mine pipeline. When --source is provided, _mine_via_adapter constructs a PalaceContext and iterates the adapter's ingest() yields, upserting DrawerRecords with proper wing/room/agent metadata from route hints or CLI defaults. Features: - --source <name> selects a registered adapter by entry-point name - --source list enumerates installed adapters - --source <name> --dry-run shows summary without filing - Route hints from adapters take priority; CLI --wing is fallback - Unknown adapter names exit 1 with available-adapters hint - Palace open failures exit 1 with error message - KeyboardInterrupt reports partial progress and exits 130 13 tests in tests/test_cli_source.py cover dispatch routing, metadata stamping, wing derivation, argparse wiring, dry-run, error paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 31d1df6 commit 5ed9fa7

2 files changed

Lines changed: 595 additions & 0 deletions

File tree

mempalace/cli.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,149 @@ def _maybe_run_mine_after_init(args, cfg) -> None:
780780
sys.exit(1)
781781

782782

783+
def _mine_via_adapter(args) -> None:
784+
"""Route ``mempalace mine --source <adapter>`` through the adapter plugin contract.
785+
786+
Constructs a :class:`PalaceContext`, calls :meth:`BaseSourceAdapter.ingest`,
787+
and upserts every yielded :class:`DrawerRecord` into the palace. This is
788+
the CLI-side glue between the source-adapter subsystem and the existing
789+
mine command — ``cmd_mine`` delegates here when ``--source`` is present.
790+
"""
791+
from .config import normalize_wing_name
792+
from .sources import (
793+
DrawerRecord,
794+
PalaceContext,
795+
SourceItemMetadata,
796+
SourceRef,
797+
available_adapters,
798+
get_adapter,
799+
)
800+
801+
adapter_name = args.source
802+
803+
# ``--source list`` is a convenience alias: show installed adapters and exit.
804+
if adapter_name == "list":
805+
names = available_adapters()
806+
if names:
807+
print("Installed source adapters:")
808+
for name in names:
809+
print(f" {name}")
810+
else:
811+
print("No source adapters installed.")
812+
return
813+
814+
try:
815+
adapter = get_adapter(adapter_name)
816+
except KeyError as e:
817+
print(f" ERROR: {e}", file=sys.stderr)
818+
sys.exit(1)
819+
820+
directory = os.path.abspath(os.path.expanduser(args.dir))
821+
wing = args.wing or normalize_wing_name(Path(directory).name)
822+
agent = getattr(args, "agent", "mempalace")
823+
dry_run = getattr(args, "dry_run", False)
824+
limit = getattr(args, "limit", 0)
825+
826+
source = SourceRef(
827+
local_path=directory,
828+
options={
829+
"wing": wing,
830+
"agent": agent,
831+
"limit": limit,
832+
},
833+
)
834+
835+
if dry_run:
836+
print(f"\n DRY RUN: would mine {directory} via adapter '{adapter_name}'")
837+
print(f" Wing: {wing}")
838+
summary = adapter.source_summary(source=source)
839+
if summary.item_count is not None:
840+
print(f" Items: {summary.item_count}")
841+
print(f" Description: {summary.description}")
842+
print()
843+
return
844+
845+
# Build palace context — open the drawer collection and knowledge graph.
846+
from .knowledge_graph import KnowledgeGraph
847+
from .palace import get_collection
848+
849+
palace_path = (
850+
os.path.expanduser(args.palace) if args.palace else MempalaceConfig().palace_path
851+
)
852+
try:
853+
collection = get_collection(palace_path, create=True)
854+
except Exception as e:
855+
print(f" ERROR: cannot open palace at {palace_path}: {e}", file=sys.stderr)
856+
sys.exit(1)
857+
858+
kg_path = os.path.join(palace_path, ".mempalace", "knowledge_graph.sqlite3")
859+
os.makedirs(os.path.dirname(kg_path), exist_ok=True)
860+
kg = KnowledgeGraph(db_path=kg_path)
861+
862+
palace_ctx = PalaceContext(
863+
drawer_collection=collection,
864+
knowledge_graph=kg,
865+
palace_path=palace_path,
866+
adapter_name=adapter_name,
867+
adapter_version=adapter.adapter_version,
868+
)
869+
870+
print(f"\n Mining {directory} via adapter '{adapter_name}' (v{adapter.adapter_version})")
871+
print(f" Wing: {wing}")
872+
873+
drawer_count = 0
874+
item_count = 0
875+
876+
try:
877+
for result in adapter.ingest(source=source, palace=palace_ctx):
878+
if isinstance(result, SourceItemMetadata):
879+
item_count += 1
880+
# Check incremental: skip if palace already has current version
881+
if adapter.is_current(item=result, existing_metadata=None):
882+
palace_ctx.skip_current_item()
883+
continue
884+
885+
if isinstance(result, DrawerRecord):
886+
if palace_ctx.is_skip_requested():
887+
continue
888+
889+
# Apply route hint: prefer adapter-supplied wing/room, fall
890+
# back to CLI-specified wing and a default room.
891+
meta = dict(result.metadata)
892+
hint = result.route_hint
893+
meta["wing"] = (hint.wing if hint and hint.wing else wing)
894+
meta["room"] = (hint.room if hint and hint.room else "general")
895+
meta["agent"] = agent
896+
meta["source_file"] = result.source_file
897+
898+
enriched = DrawerRecord(
899+
content=result.content,
900+
source_file=result.source_file,
901+
chunk_index=result.chunk_index,
902+
metadata=meta,
903+
route_hint=result.route_hint,
904+
)
905+
palace_ctx.upsert_drawer(enriched)
906+
drawer_count += 1
907+
except KeyboardInterrupt:
908+
print(f"\n Interrupted. Filed {drawer_count} drawers from {item_count} items.")
909+
sys.exit(130)
910+
finally:
911+
try:
912+
kg.close()
913+
except Exception:
914+
pass
915+
916+
print(f" Filed {drawer_count} drawers from {item_count} items.\n")
917+
918+
783919
def cmd_mine(args):
920+
# --source flag: route through the adapter plugin contract
921+
source_adapter = getattr(args, "source", None)
922+
if source_adapter:
923+
_mine_via_adapter(args)
924+
return
925+
784926
if _daemon_strict() and not args.palace:
785927
# Daemon-strict: route to /mine. The daemon owns the canonical
786928
# palace and its filesystem layout, and translates client-side
@@ -2399,6 +2541,18 @@ def main():
23992541
p_mine.add_argument(
24002542
"--dry-run", action="store_true", help="Show what would be filed without filing"
24012543
)
2544+
p_mine.add_argument(
2545+
"--source",
2546+
default=None,
2547+
metavar="ADAPTER",
2548+
help=(
2549+
"Route through a registered source adapter instead of the built-in "
2550+
"mine pipeline. Available adapters are discovered from the "
2551+
"'mempalace.sources' entry-point group (e.g. filesystem, conversations, "
2552+
"opencode, codex, gemini, aider). Use 'mempalace mine --source list' "
2553+
"to see installed adapters."
2554+
),
2555+
)
24022556
p_mine.add_argument(
24032557
"--extract",
24042558
choices=["exchange", "general"],

0 commit comments

Comments
 (0)