@@ -240,6 +240,17 @@ def _setup_llm_key(kb_dir: Path | None = None) -> None:
240240 ".csv" ,
241241}
242242
243+ BUNDLE_ONLY_EXTENSIONS = {
244+ ".png" ,
245+ ".jpg" ,
246+ ".jpeg" ,
247+ ".gif" ,
248+ ".webp" ,
249+ ".bmp" ,
250+ }
251+
252+ BUNDLE_SUPPORTED_EXTENSIONS = SUPPORTED_EXTENSIONS | BUNDLE_ONLY_EXTENSIONS
253+
243254# Map raw doc types to display types
244255_TYPE_DISPLAY_MAP = {
245256 "long_pdf" : "pageindex" ,
@@ -987,9 +998,16 @@ def init(model, language):
987998 help = "Import an already-indexed PageIndex Cloud document by its doc-id "
988999 "(no local file). Mutually exclusive with PATH." ,
9891000)
1001+ @click .option (
1002+ "--ingest-pipeline" ,
1003+ "ingest_pipeline" ,
1004+ type = click .Choice (["legacy" , "bundle" ]),
1005+ default = None ,
1006+ help = "Select the add ingest pipeline. Defaults to .openkb/config.yaml ingest.pipeline, then legacy." ,
1007+ )
9901008@click .pass_context
9911009@_with_kb_lock (exclusive = True )
992- def add (ctx , path , from_pageindex_cloud ):
1010+ def add (ctx , path , from_pageindex_cloud , ingest_pipeline ):
9931011 """Add a document or directory of documents at PATH to the knowledge base.
9941012
9951013 PATH may be a local file, a local directory (which is walked
@@ -1021,13 +1039,23 @@ def add(ctx, path, from_pageindex_cloud):
10211039 click .echo ("Provide a PATH or use --from-pageindex-cloud <DOC_ID>." )
10221040 return
10231041
1024- # URL ingest: download into raw/ first, then call add_single_file explicitly.
1025- # Keep staged conversion enabled so converted source artifacts do not touch
1026- # the live KB before the mutation snapshot exists. The tri-state outcome
1027- # still lets us clean up the just-downloaded raw file on dedup.
1042+ from openkb .ingest .add import add_bundle_target , resolve_ingest_pipeline
1043+
1044+ config = load_config (kb_dir / ".openkb" / "config.yaml" )
1045+ pipeline = resolve_ingest_pipeline (config , ingest_pipeline )
1046+ supported_extensions = (
1047+ BUNDLE_SUPPORTED_EXTENSIONS if pipeline == "bundle" else SUPPORTED_EXTENSIONS
1048+ )
1049+
1050+ # Legacy URL ingest downloads into raw/ first, then calls add_single_file.
1051+ # Bundle URL ingest keeps downloads inside the bundle staging directory and
1052+ # commits through the same mutation boundary as local bundle files.
10281053 from openkb .url_ingest import looks_like_url , fetch_url_to_raw
10291054
10301055 if looks_like_url (path ):
1056+ if pipeline == "bundle" :
1057+ add_bundle_target (path , kb_dir , legacy_fallback = add_single_file )
1058+ return
10311059 fetched = fetch_url_to_raw (path , kb_dir )
10321060 if fetched is None :
10331061 return
@@ -1049,7 +1077,7 @@ def add(ctx, path, from_pageindex_cloud):
10491077 files = [
10501078 f
10511079 for f in sorted (target .rglob ("*" ))
1052- if f .is_file () and f .suffix .lower () in SUPPORTED_EXTENSIONS
1080+ if f .is_file () and f .suffix .lower () in supported_extensions
10531081 ]
10541082 if not files :
10551083 click .echo (f"No supported files found in { path } ." )
@@ -1058,15 +1086,21 @@ def add(ctx, path, from_pageindex_cloud):
10581086 click .echo (f"Found { total } supported file(s) in { path } ." )
10591087 for i , f in enumerate (files , 1 ):
10601088 click .echo (f"\n [{ i } /{ total } ] " , nl = False )
1061- add_single_file (f , kb_dir )
1089+ if pipeline == "bundle" :
1090+ add_bundle_target (f , kb_dir , legacy_fallback = add_single_file )
1091+ else :
1092+ add_single_file (f , kb_dir )
10621093 else :
1063- if target .suffix .lower () not in SUPPORTED_EXTENSIONS :
1094+ if target .suffix .lower () not in supported_extensions :
10641095 click .echo (
10651096 f"Unsupported file type: { target .suffix } . "
1066- f"Supported: { ', ' .join (sorted (SUPPORTED_EXTENSIONS ))} "
1097+ f"Supported: { ', ' .join (sorted (supported_extensions ))} "
10671098 )
10681099 return
1069- add_single_file (target , kb_dir )
1100+ if pipeline == "bundle" :
1101+ add_bundle_target (target , kb_dir , legacy_fallback = add_single_file )
1102+ else :
1103+ add_single_file (target , kb_dir )
10701104
10711105
10721106def _stream_to_tty () -> bool :
@@ -1309,6 +1343,13 @@ def remove(ctx, identifier, keep_raw, keep_empty, dry_run, yes):
13091343 )
13101344 )
13111345
1346+ bundle_path = None
1347+ if meta .get ("bundle_path" ):
1348+ candidate = kb_dir / meta ["bundle_path" ]
1349+ if candidate .exists ():
1350+ bundle_path = candidate
1351+ actions .append (("DELETE" , str (candidate .relative_to (kb_dir ))))
1352+
13121353 # Scan concept pages to predict which will be edited vs. deleted.
13131354 # Only frontmatter ``sources:`` membership drives the plan — body-only
13141355 # references (e.g. a stray ``See also:`` line a user added by hand
@@ -1424,6 +1465,8 @@ def remove(ctx, identifier, keep_raw, keep_empty, dry_run, yes):
14241465 source_json .unlink (missing_ok = True )
14251466 if images_dir .is_dir ():
14261467 shutil .rmtree (images_dir , ignore_errors = True )
1468+ if bundle_path is not None :
1469+ bundle_path .unlink (missing_ok = True )
14271470
14281471 concept_result = remove_doc_from_concept_pages (
14291472 wiki_dir ,
0 commit comments