@@ -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+
783919def 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