@@ -534,9 +534,9 @@ def _run_doctor() -> int:
534534
535535
536536def _run_mcp_list () -> int :
537- """List all registered MCP servers and their tool counts."""
537+ """List configured MCP servers and their tool counts (from live discovery) ."""
538538 from hivemind .config import get_config
539- from hivemind .tools .registry import list_tools
539+ from hivemind .tools .mcp import discover_mcp_tools
540540 cfg = get_config ()
541541 servers = getattr (getattr (cfg , "mcp" , None ), "servers" , None ) or []
542542 try :
@@ -547,17 +547,14 @@ def _run_mcp_list() -> int:
547547 table .add_column ("Name" , style = "cyan" )
548548 table .add_column ("Transport" , style = "dim" )
549549 table .add_column ("Tools" , justify = "right" )
550- all_tools = list_tools ()
551- mcp_tools = [t for t in all_tools if getattr (t , "category" , "" ) == "mcp" ]
552- by_server : dict [str , int ] = {}
553- for t in mcp_tools :
554- name = getattr (t , "name" , "" )
555- if "." in name :
556- srv = name .split ("." , 1 )[0 ]
557- by_server [srv ] = by_server .get (srv , 0 ) + 1
558550 for s in servers :
559551 sname = getattr (s , "name" , "?" )
560- table .add_row (sname , getattr (s , "transport" , "?" ), str (by_server .get (sname , 0 )))
552+ try :
553+ adapters = discover_mcp_tools (s )
554+ count = len (adapters )
555+ except Exception :
556+ count = "—"
557+ table .add_row (sname , getattr (s , "transport" , "?" ), str (count ))
561558 if not servers :
562559 console .print ("No MCP servers configured. Add [[mcp.servers]] to hivemind.toml or use [cyan]hivemind mcp add[/]." )
563560 else :
@@ -1277,6 +1274,124 @@ def _run_checkpoint_restore(run_id: str) -> int:
12771274 return 0
12781275
12791276
1277+ def _run_audit_dispatch (args : object ) -> int :
1278+ """Audit: print table, export, or verify."""
1279+ from hivemind .config import get_config
1280+ from hivemind .audit .logger import AuditLogger
1281+ cmd = getattr (args , "audit_cmd" , None )
1282+ run_id = getattr (args , "run_id" , None )
1283+ export_fmt = getattr (args , "export" , None )
1284+ if cmd == "verify" :
1285+ run_id = getattr (args , "run_id" , run_id )
1286+ if not run_id :
1287+ print ("Error: run_id required for verify" , file = sys .stderr )
1288+ return 1
1289+ cfg = get_config ()
1290+ ok , msg = AuditLogger .verify (run_id , cfg .data_dir )
1291+ print (msg )
1292+ return 0 if ok else 1
1293+ if not run_id :
1294+ print ("Error: run_id required (e.g. hivemind audit events_2025-03-10...)" , file = sys .stderr )
1295+ return 1
1296+ cfg = get_config ()
1297+ logger = AuditLogger (cfg .data_dir , run_id = run_id )
1298+ if export_fmt :
1299+ out = logger .export (run_id , format = export_fmt )
1300+ print (out )
1301+ return 0
1302+ out = logger .export (run_id , format = "jsonl" )
1303+ if not out :
1304+ print (f"No audit log for run_id={ run_id } " , file = sys .stderr )
1305+ return 1
1306+ try :
1307+ from rich .console import Console
1308+ from rich .table import Table
1309+ console = Console ()
1310+ table = Table (title = f"Audit log: { run_id } " )
1311+ table .add_column ("timestamp" )
1312+ table .add_column ("event_type" )
1313+ table .add_column ("task_id" )
1314+ table .add_column ("resource" )
1315+ table .add_column ("success" )
1316+ for line in out .strip ().split ("\n " ):
1317+ if not line :
1318+ continue
1319+ import json
1320+ r = json .loads (line )
1321+ table .add_row (
1322+ r .get ("timestamp" , "" )[:19 ],
1323+ r .get ("event_type" , "" ),
1324+ r .get ("task_id" , "" ),
1325+ r .get ("resource" , "" ),
1326+ str (r .get ("success" , "" )),
1327+ )
1328+ console .print (table )
1329+ except Exception :
1330+ print (out )
1331+ return 0
1332+
1333+
1334+ def _run_explain (args : object ) -> int :
1335+ """Explain: decision records for run or task."""
1336+ run_id = getattr (args , "run_id" , None )
1337+ task_id = getattr (args , "task_id" , None )
1338+ if not run_id :
1339+ print ("Error: run_id required" , file = sys .stderr )
1340+ return 1
1341+ try :
1342+ from hivemind .explainability .decision_tree import DecisionTreeBuilder
1343+ from hivemind .config import get_config
1344+ cfg = get_config ()
1345+ events_dir = cfg .events_dir
1346+ builder = DecisionTreeBuilder ()
1347+ records = builder .build_from_events (run_id , events_dir )
1348+ if not records :
1349+ print (f"No decision records for run_id={ run_id } " , file = sys .stderr )
1350+ return 1
1351+ if task_id :
1352+ records = [r for r in records if r .task_id == task_id ]
1353+ if not records :
1354+ print (f"No task { task_id } in run { run_id } " , file = sys .stderr )
1355+ return 1
1356+ for r in records :
1357+ print (f"--- { r .task_id } ---" )
1358+ print (f" strategy: { r .strategy_selected } " )
1359+ print (f" model: { r .model_selected } ({ r .model_tier } )" )
1360+ print (f" tools: { r .tools_selected } " )
1361+ print (f" confidence: { r .confidence :.0%} " )
1362+ print (f" rationale: { r .rationale [:300 ]} ..." if len (r .rationale or "" ) > 300 else f" rationale: { r .rationale } " )
1363+ return 0
1364+ except Exception as e :
1365+ print (f"Error: { e } " , file = sys .stderr )
1366+ return 1
1367+
1368+
1369+ def _run_simulate (args : object ) -> int :
1370+ """Simulate: dry-run planning, no LLM or tools."""
1371+ import asyncio
1372+ task = getattr (args , "task" , "" )
1373+ cost_only = getattr (args , "cost_only" , False ) or getattr (args , "cost" , False )
1374+ if not task :
1375+ print ("Error: task required (e.g. hivemind simulate \" Summarize X\" )" , file = sys .stderr )
1376+ return 1
1377+ try :
1378+ from hivemind .explainability .simulation import SimulationMode
1379+ sim = SimulationMode ()
1380+ report = asyncio .run (sim .simulate (task ))
1381+ if cost_only :
1382+ print (f"Estimated cost: { getattr (report , 'estimated_cost' , 'N/A' )} " )
1383+ return 0
1384+ print (f"Tasks: { len (report .task_list )} " )
1385+ for t in report .task_list :
1386+ print (f" - { t } " )
1387+ print (f"Estimated cost: { getattr (report , 'estimated_cost' , 'N/A' )} " )
1388+ print (f"Estimated duration: { getattr (report , 'estimated_duration' , 'N/A' )} " )
1389+ return 0
1390+ except Exception as e :
1391+ print (f"Error: { e } " , file = sys .stderr )
1392+ return 1
1393+
1394+
12801395def _run_health (args : object ) -> int :
12811396 """Run health checks. Exit 0 if healthy, 1 otherwise. Print ✓/✗ per check."""
12821397 import asyncio
@@ -1867,6 +1982,7 @@ def main() -> int:
18671982 epilog = """
18681983Examples:
18691984 hivemind credentials set openai api_key
1985+ hivemind credentials set azure endpoint \" https://.../openai/v1\"
18701986 hivemind credentials list
18711987 hivemind credentials migrate
18721988 hivemind credentials export azure # print env KEY=value for sourcing
@@ -1890,6 +2006,11 @@ def main() -> int:
18902006 nargs = "?" ,
18912007 help = "Key name (e.g. api_key)" ,
18922008 )
2009+ credentials_parser .add_argument (
2010+ "value" ,
2011+ nargs = "?" ,
2012+ help = "Value (for set only). Omit to be prompted, or pipe: echo 'val' | hivemind credentials set azure endpoint" ,
2013+ )
18932014 credentials_parser .set_defaults (func = lambda a : _run_credentials (a ))
18942015
18952016 completion_parser = subparsers .add_parser (
@@ -1965,6 +2086,37 @@ def main() -> int:
19652086 checkpoint_restore_p .set_defaults (func = _run_checkpoint_dispatch )
19662087 checkpoint_parser .set_defaults (checkpoint_cmd = "list" , func = _run_checkpoint_dispatch )
19672088
2089+ audit_parser = subparsers .add_parser (
2090+ "audit" ,
2091+ help = "View or export audit log for a run" ,
2092+ description = "Print audit log as table, export to CSV/JSONL, or verify chain integrity." ,
2093+ )
2094+ audit_parser .add_argument ("run_id" , nargs = "?" , default = None , help = "Run ID (e.g. events_...)" )
2095+ audit_parser .add_argument ("--export" , choices = ["jsonl" , "csv" , "siem" ], default = None , help = "Export format" )
2096+ audit_sub = audit_parser .add_subparsers (dest = "audit_cmd" , help = "Subcommand" )
2097+ audit_verify_p = audit_sub .add_parser ("verify" , help = "Verify audit log chain integrity" )
2098+ audit_verify_p .add_argument ("run_id" , help = "Run ID to verify" )
2099+ audit_verify_p .set_defaults (audit_cmd = "verify" )
2100+ audit_parser .set_defaults (func = _run_audit_dispatch )
2101+
2102+ explain_parser = subparsers .add_parser (
2103+ "explain" ,
2104+ help = "Show decision records for a run or task" ,
2105+ description = "Print decision tree and rationale for agent actions." ,
2106+ )
2107+ explain_parser .add_argument ("run_id" , help = "Run ID" )
2108+ explain_parser .add_argument ("task_id" , nargs = "?" , default = None , help = "Optional task ID for single task" )
2109+ explain_parser .set_defaults (func = _run_explain )
2110+
2111+ simulate_parser = subparsers .add_parser (
2112+ "simulate" ,
2113+ help = "Dry-run planning without LLM or tool execution" ,
2114+ description = "Run planner and scheduler only; output task list and cost estimate." ,
2115+ )
2116+ simulate_parser .add_argument ("task" , help = "Root task description" )
2117+ simulate_parser .add_argument ("--cost" , action = "store_true" , help = "Print cost estimate only" )
2118+ simulate_parser .set_defaults (func = _run_simulate )
2119+
19682120 health_parser = subparsers .add_parser (
19692121 "health" ,
19702122 help = "Health and readiness check" ,
0 commit comments