bone-agent 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +17 -0
  2. package/config.yaml.example +5 -2
  3. package/package.json +1 -1
  4. package/prompts/main/communication_style.md +1 -1
  5. package/prompts/main/dream.md +23 -9
  6. package/prompts/main/skills.md +3 -0
  7. package/prompts/micro/communication_style.md +1 -1
  8. package/prompts/micro/skills.md +1 -0
  9. package/src/core/agentic.py +138 -38
  10. package/src/core/chat_manager.py +19 -6
  11. package/src/core/config_manager.py +8 -1
  12. package/src/core/cron.py +0 -4
  13. package/src/core/metadata.py +75 -0
  14. package/src/core/skills.py +463 -0
  15. package/src/core/sub_agent.py +93 -43
  16. package/src/core/tool_feedback.py +87 -76
  17. package/src/llm/client.py +7 -2
  18. package/src/llm/codex_provider.py +350 -0
  19. package/src/llm/config.py +46 -2
  20. package/src/llm/prompts.py +12 -7
  21. package/src/llm/providers.py +3 -1
  22. package/src/llm/token_tracker.py +15 -0
  23. package/src/tools/__init__.py +24 -85
  24. package/src/tools/create_file.py +1 -1
  25. package/src/tools/directory.py +1 -1
  26. package/src/tools/edit.py +5 -1
  27. package/src/tools/file_reader.py +1 -1
  28. package/src/tools/helpers/__init__.py +1 -7
  29. package/src/tools/helpers/base.py +65 -16
  30. package/src/tools/helpers/loader.py +2 -88
  31. package/src/tools/helpers/path_resolver.py +54 -3
  32. package/src/tools/helpers/plugin_manifest.py +99 -70
  33. package/src/tools/review_sub_agent.py +2 -1
  34. package/src/tools/rg_search.py +24 -7
  35. package/src/tools/search_plugins.py +140 -72
  36. package/src/tools/shell.py +3 -3
  37. package/src/ui/commands.py +355 -33
  38. package/src/ui/displays.py +26 -1
  39. package/src/ui/main.py +0 -4
  40. package/src/ui/tool_confirmation.py +16 -5
  41. package/src/utils/editor.py +88 -39
  42. package/src/utils/settings.py +6 -2
  43. package/src/utils/validation.py +10 -0
@@ -8,7 +8,7 @@ from typing import Optional
8
8
  from llm import config
9
9
 
10
10
  from core.config_manager import ConfigManager as ConfigManagerClass
11
- from ui.displays import show_help_table, show_cron_help_table
11
+ from ui.displays import show_help_table, show_cron_help_table, show_skills_help_table
12
12
  from ui.banner import display_startup_banner
13
13
  from core.agentic import SubAgentPanel
14
14
  from ui.setting_selector import SettingSelector, SettingCategory, SettingOption
@@ -591,12 +591,15 @@ def _handle_clear(chat_manager, console, debug_mode_container, args, cron_schedu
591
591
  conv_cache_creation = chat_manager.token_tracker.conv_cache_creation_tokens
592
592
  if conv_cache_read > 0 or conv_cache_creation > 0:
593
593
  total_cached = conv_cache_read + conv_cache_creation
594
- cache_hit_pct = (
594
+ cache_activity_read_pct = (
595
595
  conv_cache_read / total_cached * 100
596
596
  ) if total_cached > 0 else 0
597
+ cache_coverage_pct = (
598
+ conv_cache_read / conv_in * 100
599
+ ) if conv_in > 0 else 0
597
600
  console.print(f" Cache read: {conv_cache_read:,} tokens")
598
601
  console.print(f" Cache write: {conv_cache_creation:,} tokens")
599
- console.print(f" ({cache_hit_pct:.0f}% cache hit rate)")
602
+ console.print(f" ({cache_coverage_pct:.0f}% input cached, {cache_activity_read_pct:.0f}% cache reads)")
600
603
 
601
604
  # Display cost — combined actual + estimated, with config-based fallback
602
605
  tracker_conv = chat_manager.token_tracker
@@ -665,11 +668,12 @@ def _open_provider_editor(chat_manager, console, provider):
665
668
  min_val=0.0, step=0.01,
666
669
  ))
667
670
 
668
- category = SettingCategory(title=f"{provider.capitalize()} Settings", settings=settings)
671
+ provider_label = config.get_provider_display_name(provider)
672
+ category = SettingCategory(title=f"{provider_label} Settings", settings=settings)
669
673
 
670
674
  selector = SettingSelector(
671
675
  categories=[category],
672
- title=f"Configure {provider.capitalize()}",
676
+ title=f"Configure {provider_label}",
673
677
  )
674
678
 
675
679
  changes = selector.run()
@@ -742,7 +746,8 @@ def _handle_provider(chat_manager, console, debug_mode_container, args, cron_sch
742
746
  # Validate provider name
743
747
  if provider not in config.get_providers():
744
748
  console.print(f"[red]Error: Unknown provider '{provider}'[/red]")
745
- console.print(f"[dim]Available providers: {', '.join(config.get_providers())}[/dim]")
749
+ available = ', '.join(config.get_provider_display_name(prov) for prov in config.get_providers())
750
+ console.print(f"[dim]Available providers: {available}[/dim]")
746
751
  return CommandResult(status="handled")
747
752
 
748
753
  # Switch directly to the named provider
@@ -756,7 +761,7 @@ def _handle_provider(chat_manager, console, debug_mode_container, args, cron_sch
756
761
 
757
762
  cfg = config.get_provider_config(provider)
758
763
  model = cfg.get('model') or cfg.get('api_model') or ''
759
- label = f"{provider.capitalize()}"
764
+ label = config.get_provider_display_name(provider)
760
765
  if model:
761
766
  label += f" ({model})"
762
767
  console.print(f"[green]Switched to {label}[/green]")
@@ -772,7 +777,7 @@ def _handle_provider(chat_manager, console, debug_mode_container, args, cron_sch
772
777
  for prov in config.get_providers():
773
778
  cfg = config.get_provider_config(prov)
774
779
  model = cfg.get('model') or cfg.get('api_model') or ''
775
- entry = {"value": prov, "text": prov.capitalize()}
780
+ entry = {"value": prov, "text": config.get_provider_display_name(prov)}
776
781
  if model:
777
782
  entry["description"] = model[:40]
778
783
  provider_options.append(entry)
@@ -1092,18 +1097,38 @@ def _handle_usage(chat_manager, console, debug_mode_container, args, cron_schedu
1092
1097
  console.print(f" Output tokens: {tracker.total_completion_tokens:,}")
1093
1098
  console.print(f" Total tokens: {tracker.total_tokens:,}")
1094
1099
 
1095
- # Display cache token breakdown (if any cache tokens were recorded)
1096
- has_cache = tracker.total_cache_read_tokens > 0 or tracker.total_cache_creation_tokens > 0
1097
- if has_cache:
1100
+ # Display cache token breakdown when cache tokens were recorded.
1101
+ # Codex can report an explicit 0 cached_tokens value, which is still useful
1102
+ # confirmation that prompt-cache usage data is flowing through.
1103
+ show_cache = (
1104
+ tracker.total_cache_read_tokens > 0
1105
+ or tracker.total_cache_creation_tokens > 0
1106
+ or current_provider == "codex"
1107
+ )
1108
+ if show_cache:
1098
1109
  total_cached = tracker.total_cache_read_tokens + tracker.total_cache_creation_tokens
1099
- cache_hit_pct = (
1110
+ cache_activity_read_pct = (
1100
1111
  tracker.total_cache_read_tokens
1101
1112
  / total_cached * 100
1102
1113
  ) if total_cached > 0 else 0
1114
+ cache_coverage_pct = (
1115
+ tracker.total_cache_read_tokens
1116
+ / tracker.total_prompt_tokens * 100
1117
+ ) if tracker.total_prompt_tokens > 0 else 0
1103
1118
  console.print()
1104
- console.print(f"[#5F9EA0]Input Cache ({cache_hit_pct:.0f}% hit rate):[/#5F9EA0]")
1119
+ console.print(
1120
+ f"[#5F9EA0]Input Cache ({cache_coverage_pct:.0f}% input cached, "
1121
+ f"{cache_activity_read_pct:.0f}% cache reads):[/#5F9EA0]"
1122
+ )
1105
1123
  console.print(f" Cache read: {tracker.total_cache_read_tokens:,} tokens")
1106
1124
  console.print(f" Cache write: {tracker.total_cache_creation_tokens:,} tokens")
1125
+ if current_provider == "codex" and total_cached == 0:
1126
+ if tracker.last_cache_metrics_reported is False:
1127
+ keys = ", ".join(tracker.last_usage_keys) if tracker.last_usage_keys else "none"
1128
+ console.print(" [dim]Codex did not report any cache-token fields in the last usage payload.[/dim]")
1129
+ console.print(f" [dim]Last usage keys: {keys}[/dim]")
1130
+ elif tracker.last_cache_metrics_reported is None:
1131
+ console.print(" [dim]No usage payload has been recorded yet for this Codex session.[/dim]")
1107
1132
  console.print()
1108
1133
 
1109
1134
 
@@ -2184,8 +2209,8 @@ def _handle_obsidian(chat_manager, console, debug_mode_container, args, cron_sch
2184
2209
  return CommandResult(status="handled")
2185
2210
 
2186
2211
 
2187
- def _persist_disabled_tools(console):
2188
- """Persist current disabled_tools to config file.
2212
+ def _persist_tool_visibility(console):
2213
+ """Persist tool and skill visibility state to config file.
2189
2214
 
2190
2215
  Returns True on success, False on failure.
2191
2216
  """
@@ -2194,6 +2219,7 @@ def _persist_disabled_tools(console):
2194
2219
  if "TOOL_SETTINGS" not in cfg_data:
2195
2220
  cfg_data["TOOL_SETTINGS"] = {}
2196
2221
  cfg_data["TOOL_SETTINGS"]["disabled_tools"] = list(tool_settings.disabled_tools)
2222
+ cfg_data["TOOL_SETTINGS"]["hidden_skills"] = list(tool_settings.hidden_skills)
2197
2223
  config_manager.save(cfg_data)
2198
2224
  return True
2199
2225
  except Exception as e:
@@ -2202,18 +2228,23 @@ def _persist_disabled_tools(console):
2202
2228
 
2203
2229
 
2204
2230
  def _handle_tools(chat_manager, console, debug_mode_container, args, cron_scheduler=None):
2205
- """Handle /tools command — toggle individual tools or groups on/off.
2231
+ """Handle /tools command — manage tool availability and skill discovery visibility.
2206
2232
 
2207
- No args: Launch interactive SettingSelector with tools grouped by category.
2233
+ No args: Launch interactive SettingSelector with tools/plugins grouped by category
2234
+ and a skills section for discovery visibility.
2208
2235
  Subcommands:
2209
- list — show all tools with group labels and status
2210
- enable <name> — enable a single tool
2211
- disable <name> — disable a single tool
2212
- enable-group <key> — enable all tools in a group (e.g. file_ops, search, shell)
2236
+ list — show all tools, plugins, and skills with status
2237
+ enable <name> — enable a core tool or plugin
2238
+ disable <name> — disable a core tool or plugin
2239
+ show-skill <name> — make a skill visible in discovery surfaces
2240
+ hide-skill <name> — hide a skill from discovery surfaces
2241
+ enable-group <key> — enable all tools in a group (e.g. file_ops, task_mgmt)
2213
2242
  disable-group <key> — disable all tools in a group
2214
2243
  """
2244
+ from core.skills import iter_skill_summaries, validate_skill_name
2215
2245
  from ui.setting_selector import SettingOption, SettingCategory, SettingSelector
2216
2246
  from tools.helpers.base import ToolRegistry, TOOL_GROUPS
2247
+ from tools.helpers.plugin_manifest import plugin_manifest
2217
2248
 
2218
2249
  # Text subcommands
2219
2250
  if args:
@@ -2221,17 +2252,27 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2221
2252
 
2222
2253
  if args_clean.lower() in ("list", "status"):
2223
2254
  all_tools = sorted(ToolRegistry._tools.values(), key=lambda t: t.name)
2255
+ plugin_defs = sorted(plugin_manifest.get_all(), key=lambda t: t.name)
2256
+ skills = sorted(iter_skill_summaries(), key=lambda s: s.name)
2224
2257
  disabled = ToolRegistry.get_disabled()
2225
- console.print(f"[bold #5F9EA0]Tools: {len(all_tools) - len(disabled)} enabled, {len(disabled)} disabled[/bold #5F9EA0]")
2258
+ hidden_skills = set(tool_settings.hidden_skills)
2259
+ disabled_tools = {t.name for t in all_tools if t.name in disabled}
2260
+ console.print(
2261
+ f"[bold #5F9EA0]Tools: {len(all_tools) - len(disabled_tools)} enabled, {len(disabled_tools)} disabled[/bold #5F9EA0]"
2262
+ )
2263
+ console.print(
2264
+ f"[bold #5F9EA0]User plugins: {sum(1 for p in plugin_defs if p.name not in disabled)} enabled, {sum(1 for p in plugin_defs if p.name in disabled)} disabled[/bold #5F9EA0]"
2265
+ )
2266
+ console.print(
2267
+ f"[bold #5F9EA0]Skills: {len(skills) - len(hidden_skills)} visible, {len(hidden_skills)} hidden[/bold #5F9EA0]"
2268
+ )
2226
2269
  console.print()
2227
2270
 
2228
- # Build reverse lookup: tool_name -> group_label
2229
2271
  tool_to_group = {}
2230
2272
  for gkey, gdef in TOOL_GROUPS.items():
2231
2273
  for tname in gdef["tools"]:
2232
2274
  tool_to_group.setdefault(tname, []).append(gdef["label"])
2233
2275
 
2234
- # Group tools for display
2235
2276
  current_group = None
2236
2277
  for t in all_tools:
2237
2278
  groups = tool_to_group.get(t.name, [])
@@ -2243,6 +2284,18 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2243
2284
  status = "[red]off[/red]" if is_off else "[green]on[/green] "
2244
2285
  console.print(f" {status} {t.name}")
2245
2286
 
2287
+ console.print()
2288
+ console.print(" [bold]User plugins[/bold]")
2289
+ for plugin in plugin_defs:
2290
+ status = "[red]off[/red]" if plugin.name in disabled else "[green]on[/green] "
2291
+ console.print(f" {status} {plugin.name}")
2292
+
2293
+ console.print()
2294
+ console.print(" [bold]Skills[/bold]")
2295
+ for skill in skills:
2296
+ status = "[red]hidden[/red]" if skill.name in hidden_skills else "[green]visible[/green]"
2297
+ console.print(f" {status} {skill.name}")
2298
+
2246
2299
  console.print()
2247
2300
  console.print("[dim]Groups:[/dim] " + ", ".join(
2248
2301
  f"[bold]{k}[/bold] ({v['label']})" for k, v in TOOL_GROUPS.items()
@@ -2281,17 +2334,17 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2281
2334
 
2282
2335
  # Sync and persist
2283
2336
  tool_settings.disabled_tools = sorted(ToolRegistry.get_disabled())
2284
- _persist_disabled_tools(console)
2337
+ _persist_tool_visibility(console)
2285
2338
  console.print()
2286
2339
  return CommandResult(status="handled")
2287
2340
 
2288
- # Single tool operations
2341
+ # Single tool/plugin operations
2289
2342
  if action in ("enable", "disable"):
2290
- # Match case-insensitively against registered tools
2291
2343
  all_registered_lower = {t.name.lower(): t.name for t in ToolRegistry._tools.values()}
2344
+ all_registered_lower.update({t.name.lower(): t.name for t in plugin_manifest.get_all()})
2292
2345
  matched = all_registered_lower.get(target.lower())
2293
2346
  if not matched:
2294
- console.print(f"[red]Unknown tool: {target}[/red]")
2347
+ console.print(f"[red]Unknown tool or plugin: {target}[/red]")
2295
2348
  console.print(f"[dim]Run [bold #5F9EA0]/tools list[/bold #5F9EA0] to see all tools.[/dim]")
2296
2349
  return CommandResult(status="handled")
2297
2350
 
@@ -2305,17 +2358,44 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2305
2358
  tool_settings.disabled_tools.append(matched)
2306
2359
  console.print(f"[yellow]Disabled: {matched}[/yellow]")
2307
2360
 
2308
- _persist_disabled_tools(console)
2361
+ _persist_tool_visibility(console)
2362
+ console.print()
2363
+ return CommandResult(status="handled")
2364
+
2365
+ if action in ("show-skill", "hide-skill"):
2366
+ try:
2367
+ skill_name = validate_skill_name(target)
2368
+ except Exception as e:
2369
+ console.print(f"[red]{e}[/red]")
2370
+ return CommandResult(status="handled")
2371
+
2372
+ known_skills = {skill.name for skill in iter_skill_summaries()}
2373
+ if skill_name not in known_skills:
2374
+ console.print(f"[red]Unknown skill: {skill_name}[/red]")
2375
+ return CommandResult(status="handled")
2376
+
2377
+ if action == "show-skill":
2378
+ tool_settings.hidden_skills = [n for n in tool_settings.hidden_skills if n != skill_name]
2379
+ console.print(f"[green]Skill visible in discovery: {skill_name}[/green]")
2380
+ else:
2381
+ if skill_name not in tool_settings.hidden_skills:
2382
+ tool_settings.hidden_skills.append(skill_name)
2383
+ console.print(f"[yellow]Skill hidden from discovery: {skill_name}[/yellow]")
2384
+
2385
+ tool_settings.hidden_skills = sorted(set(tool_settings.hidden_skills))
2386
+ _persist_tool_visibility(console)
2309
2387
  console.print()
2310
2388
  return CommandResult(status="handled")
2311
2389
 
2312
2390
  console.print(f"[red]Unknown subcommand: {args}[/red]")
2313
- console.print("Usage: [bold #5F9EA0]/tools[/bold #5F9EA0] [list | enable <name> | disable <name> | enable-group <key> | disable-group <key>]")
2391
+ console.print("Usage: [bold #5F9EA0]/tools[/bold #5F9EA0] [list | enable <name> | disable <name> | show-skill <name> | hide-skill <name> | enable-group <key> | disable-group <key>]")
2314
2392
  return CommandResult(status="handled")
2315
2393
 
2316
2394
  # No args — interactive toggle UI, organized by groups
2317
2395
  all_tools_map = {t.name: t for t in ToolRegistry._tools.values()}
2396
+ plugin_tools = {t.name: t for t in plugin_manifest.get_all()}
2318
2397
  disabled = ToolRegistry.get_disabled()
2398
+ hidden_skills = set(tool_settings.hidden_skills)
2319
2399
 
2320
2400
  categories = []
2321
2401
  for gkey, gdef in TOOL_GROUPS.items():
@@ -2355,10 +2435,36 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2355
2435
  input_type="boolean",
2356
2436
  on_text="ON",
2357
2437
  off_text="OFF",
2358
- description=f"Modes: {modes}",
2359
2438
  ))
2360
2439
  categories.append(SettingCategory(title="Other", settings=other_options))
2361
2440
 
2441
+ plugin_options = []
2442
+ for name in sorted(plugin_tools):
2443
+ is_off = name in disabled
2444
+ plugin_options.append(SettingOption(
2445
+ key=name,
2446
+ text=name,
2447
+ value=not is_off,
2448
+ input_type="boolean",
2449
+ on_text="ON",
2450
+ off_text="OFF",
2451
+ ))
2452
+ if plugin_options:
2453
+ categories.append(SettingCategory(title="User plugins", settings=plugin_options))
2454
+
2455
+ skill_options = []
2456
+ for skill in sorted(iter_skill_summaries(), key=lambda s: s.name):
2457
+ skill_options.append(SettingOption(
2458
+ key=f"skill:{skill.name}",
2459
+ text=skill.name,
2460
+ value=skill.name not in hidden_skills,
2461
+ input_type="boolean",
2462
+ on_text="VISIBLE",
2463
+ off_text="HIDDEN",
2464
+ ))
2465
+ if skill_options:
2466
+ categories.append(SettingCategory(title="Skills", settings=skill_options))
2467
+
2362
2468
  selector = SettingSelector(
2363
2469
  categories=categories,
2364
2470
  title="Tool Settings",
@@ -2377,7 +2483,17 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2377
2483
  # Apply changes
2378
2484
  newly_disabled = []
2379
2485
  newly_enabled = []
2486
+ newly_hidden_skills = []
2487
+ newly_visible_skills = []
2380
2488
  for name, enabled in changes.items():
2489
+ if name.startswith("skill:"):
2490
+ skill_name = name.split(":", 1)[1]
2491
+ if enabled and skill_name in hidden_skills:
2492
+ newly_visible_skills.append(skill_name)
2493
+ elif not enabled and skill_name not in hidden_skills:
2494
+ newly_hidden_skills.append(skill_name)
2495
+ continue
2496
+
2381
2497
  if not enabled and name not in disabled:
2382
2498
  ToolRegistry.disable(name)
2383
2499
  newly_disabled.append(name)
@@ -2385,10 +2501,13 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2385
2501
  ToolRegistry.enable(name)
2386
2502
  newly_enabled.append(name)
2387
2503
 
2388
- # Sync tool_settings.disabled_tools to be the full current disabled set
2389
2504
  tool_settings.disabled_tools = sorted(ToolRegistry.get_disabled())
2505
+ next_hidden_skills = set(hidden_skills)
2506
+ next_hidden_skills.update(newly_hidden_skills)
2507
+ next_hidden_skills.difference_update(newly_visible_skills)
2508
+ tool_settings.hidden_skills = sorted(next_hidden_skills)
2390
2509
 
2391
- _persist_disabled_tools(console)
2510
+ _persist_tool_visibility(console)
2392
2511
 
2393
2512
  # Summary
2394
2513
  change_lines = []
@@ -2396,6 +2515,10 @@ def _handle_tools(chat_manager, console, debug_mode_container, args, cron_schedu
2396
2515
  change_lines.append(f" [yellow]Disabled:[/yellow] {name}")
2397
2516
  for name in newly_enabled:
2398
2517
  change_lines.append(f" [green]Enabled:[/green] {name}")
2518
+ for name in newly_hidden_skills:
2519
+ change_lines.append(f" [yellow]Hidden skill:[/yellow] {name}")
2520
+ for name in newly_visible_skills:
2521
+ change_lines.append(f" [green]Visible skill:[/green] {name}")
2399
2522
 
2400
2523
  if change_lines:
2401
2524
  total_enabled = len(ToolRegistry.get_all())
@@ -2461,6 +2584,12 @@ def _handle_cd(chat_manager, console, debug_mode_container, args, cron_scheduler
2461
2584
  except Exception:
2462
2585
  pass
2463
2586
 
2587
+ # Rebuild system prompt so project root stays current
2588
+ try:
2589
+ chat_manager.update_system_prompt()
2590
+ except Exception:
2591
+ pass
2592
+
2464
2593
  return CommandResult(status="handled")
2465
2594
 
2466
2595
 
@@ -2515,6 +2644,198 @@ def _handle_prompt(chat_manager, console, debug_mode_container, args, cron_sched
2515
2644
  return CommandResult(status="handled")
2516
2645
 
2517
2646
 
2647
+ def _print_skills_usage(console):
2648
+ show_skills_help_table(console)
2649
+
2650
+
2651
+ def _skills_list(console, query=None):
2652
+ from datetime import datetime
2653
+ from core.skills import get_skills_dir, list_skills
2654
+
2655
+ skills = list_skills(query=query)
2656
+ if not skills:
2657
+ console.print("[dim]No skills found.[/dim]")
2658
+ console.print(f"[dim]Directory: {get_skills_dir()}[/dim]")
2659
+ console.print("[dim]Create one with: [bold #5F9EA0]/skills add frontend_design[/bold #5F9EA0][/dim]")
2660
+ return
2661
+
2662
+ table = Table(show_header=True, box=box.SIMPLE_HEAD)
2663
+ table.add_column("Skill", no_wrap=True)
2664
+ table.add_column("Preview")
2665
+ table.add_column("Modified", no_wrap=True)
2666
+ for skill in skills:
2667
+ modified = datetime.fromtimestamp(skill.modified).strftime("%Y-%m-%d %H:%M")
2668
+ table.add_row(f"[bold]{skill.name}[/bold]", skill.preview, modified)
2669
+ console.print(table)
2670
+ console.print("[dim]Load with: [bold #5F9EA0]/skills load <name>[/bold #5F9EA0][/dim]")
2671
+
2672
+
2673
+ def _open_skill_editor(console, debug_mode_container, initial_content):
2674
+ from utils.editor import open_editor_for_content
2675
+
2676
+ return open_editor_for_content(
2677
+ console,
2678
+ initial_content=initial_content,
2679
+ debug_mode=debug_mode_container["debug"],
2680
+ )
2681
+
2682
+
2683
+ def _write_skill_from_editor(console, debug_mode_container, name, initial_content, *, overwrite, verb):
2684
+ from core.skills import write_skill
2685
+
2686
+ success, content = _open_skill_editor(console, debug_mode_container, initial_content)
2687
+ if not success or not content or not content.strip():
2688
+ console.print("[dim]Cancelled.[/dim]")
2689
+ return None
2690
+ path = write_skill(name, content, overwrite=overwrite)
2691
+ console.print(f"[green]{verb} skill '{name}'.[/green] [dim]{path}[/dim]")
2692
+ return path
2693
+
2694
+
2695
+ def _handle_skills(chat_manager, console, debug_mode_container, args, cron_sched=None):
2696
+ """Handle /skills command — manage reusable prompt snippets."""
2697
+ from core.skills import (
2698
+ SkillError,
2699
+ activate_skill,
2700
+ get_skills_dir,
2701
+ read_skill,
2702
+ remove_skill,
2703
+ validate_skill_name,
2704
+ write_skill,
2705
+ )
2706
+
2707
+ if not args or not args.strip():
2708
+ _skills_list(console)
2709
+ return CommandResult(status="handled")
2710
+
2711
+ args_clean = args.strip()
2712
+ parts = args_clean.split(maxsplit=2)
2713
+ subcmd = parts[0].lower()
2714
+
2715
+ try:
2716
+ if subcmd in ("help", "-h", "--help"):
2717
+ _print_skills_usage(console)
2718
+ return CommandResult(status="handled")
2719
+
2720
+ if subcmd in ("list", "ls"):
2721
+ list_parts = args_clean.split(maxsplit=1)
2722
+ query = list_parts[1] if len(list_parts) > 1 else None
2723
+ _skills_list(console, query=query)
2724
+ return CommandResult(status="handled")
2725
+
2726
+ if subcmd == "dir":
2727
+ console.print(str(get_skills_dir()))
2728
+ return CommandResult(status="handled")
2729
+
2730
+ if subcmd == "show":
2731
+ if len(parts) < 2:
2732
+ console.print("[red]Usage: /skills show <name>[/red]")
2733
+ return CommandResult(status="handled")
2734
+ name = validate_skill_name(parts[1])
2735
+ content = read_skill(name, strip_heading=False)
2736
+ console.print(Markdown(left_align_headings(content), code_theme=MonokaiDarkBGStyle, justify="left"))
2737
+ return CommandResult(status="handled")
2738
+
2739
+ if subcmd in ("load", "use"):
2740
+ if len(parts) < 2:
2741
+ console.print(f"[red]Usage: /skills {subcmd} <name>[/red]")
2742
+ return CommandResult(status="handled")
2743
+ name = validate_skill_name(parts[1])
2744
+ tokens = activate_skill(chat_manager, name, read_skill(name))
2745
+ console.print(f"[green]Activated skill '{name}' for this chat.[/green] [dim](~{tokens:,} tokens)[/dim]")
2746
+ return CommandResult(status="handled")
2747
+
2748
+ if subcmd in ("remove", "rm", "delete"):
2749
+ if len(parts) < 2:
2750
+ console.print("[red]Usage: /skills remove <name>[/red]")
2751
+ return CommandResult(status="handled")
2752
+ name = validate_skill_name(parts[1])
2753
+ from rich.prompt import Confirm
2754
+ if not Confirm.ask(f"Remove skill '{name}'?", default=False):
2755
+ console.print("[dim]Cancelled.[/dim]")
2756
+ return CommandResult(status="handled")
2757
+ remove_skill(name)
2758
+ console.print(f"[green]Removed skill '{name}'.[/green]")
2759
+ return CommandResult(status="handled")
2760
+
2761
+ if subcmd == "edit":
2762
+ if len(parts) < 2:
2763
+ console.print("[red]Usage: /skills edit <name>[/red]")
2764
+ return CommandResult(status="handled")
2765
+ name = validate_skill_name(parts[1])
2766
+ try:
2767
+ initial_content = read_skill(name, strip_heading=False)
2768
+ except SkillError:
2769
+ initial_content = f"# {name}\n\n"
2770
+ _write_skill_from_editor(
2771
+ console,
2772
+ debug_mode_container,
2773
+ name,
2774
+ initial_content,
2775
+ overwrite=True,
2776
+ verb="Saved",
2777
+ )
2778
+ return CommandResult(status="handled")
2779
+
2780
+ if subcmd in ("add", "create", "new"):
2781
+ if len(parts) < 2:
2782
+ console.print(f"[red]Usage: /skills {subcmd} <name> [prompt][/red]")
2783
+ return CommandResult(status="handled")
2784
+ name = validate_skill_name(parts[1])
2785
+ if len(parts) >= 3:
2786
+ path = write_skill(name, parts[2], overwrite=False)
2787
+ console.print(f"[green]Created skill '{name}'.[/green] [dim]{path}[/dim]")
2788
+ return CommandResult(status="handled")
2789
+ _write_skill_from_editor(
2790
+ console,
2791
+ debug_mode_container,
2792
+ name,
2793
+ f"# {name}\n\n",
2794
+ overwrite=False,
2795
+ verb="Created",
2796
+ )
2797
+ return CommandResult(status="handled")
2798
+
2799
+ if subcmd == "modify":
2800
+ cmd_parts = args_clean.split(maxsplit=2)
2801
+ if len(cmd_parts) < 2:
2802
+ console.print("[red]Usage: /skills modify <name> [prompt][/red]")
2803
+ return CommandResult(status="handled")
2804
+ name = validate_skill_name(cmd_parts[1])
2805
+ if len(cmd_parts) < 3:
2806
+ _write_skill_from_editor(
2807
+ console,
2808
+ debug_mode_container,
2809
+ name,
2810
+ read_skill(name, strip_heading=False),
2811
+ overwrite=True,
2812
+ verb="Updated",
2813
+ )
2814
+ return CommandResult(status="handled")
2815
+ read_skill(name)
2816
+ path = write_skill(name, cmd_parts[2], overwrite=True)
2817
+ console.print(f"[green]Updated skill '{name}'.[/green] [dim]{path}[/dim]")
2818
+ return CommandResult(status="handled")
2819
+
2820
+ if len(parts) >= 2:
2821
+ name = validate_skill_name(parts[0])
2822
+ _, body = args_clean.split(maxsplit=1)
2823
+ path = write_skill(name, body, overwrite=False)
2824
+ console.print(f"[green]Created skill '{name}'.[/green] [dim]{path}[/dim]")
2825
+ return CommandResult(status="handled")
2826
+
2827
+ console.print("[red]Usage: /skills add <name> [prompt][/red]")
2828
+ console.print("[dim]Run [bold #5F9EA0]/skills help[/bold #5F9EA0] for all commands.[/dim]")
2829
+ return CommandResult(status="handled")
2830
+
2831
+ except SkillError as e:
2832
+ console.print(f"[red]{e}[/red]")
2833
+ return CommandResult(status="handled")
2834
+ except Exception as e:
2835
+ console.print(f"[red]Skills command failed: {e}[/red]")
2836
+ return CommandResult(status="handled")
2837
+
2838
+
2518
2839
  def _handle_obsidian_init(console, obsidian_settings):
2519
2840
  """Handle /obsidian init — scaffold project folder structure in vault."""
2520
2841
  if not obsidian_settings.is_active():
@@ -2741,6 +3062,7 @@ _COMMAND_HANDLERS = {
2741
3062
  "/setup": _handle_setup,
2742
3063
  "/cron": _handle_cron,
2743
3064
  "/prompt": _handle_prompt,
3065
+ "/skills": _handle_skills,
2744
3066
  }
2745
3067
 
2746
3068
 
@@ -22,7 +22,7 @@ def show_provider_table(current_provider: str, console):
22
22
  else:
23
23
  status = "✅" if cfg.get("api_key") else "❌ (set API key)"
24
24
  active = " [green](active)[/green]" if provider == current_provider else ""
25
- table.add_row(provider.capitalize(), status, f"{model[:40]}{active}")
25
+ table.add_row(config.get_provider_display_name(provider), status, f"{model[:40]}{active}")
26
26
 
27
27
  console.print(table)
28
28
 
@@ -66,6 +66,7 @@ def show_help_table(console):
66
66
  table.add_row("[bold #5F9EA0]/cd[/bold #5F9EA0] [path]", "Change working directory (no args to show current)")
67
67
  table.add_row("[bold #5F9EA0]/edit[/bold #5F9EA0], [bold #5F9EA0]/e[/bold #5F9EA0]", "Open editor for multi-line input")
68
68
  table.add_row("[bold #5F9EA0]/review[/bold #5F9EA0] [args], [bold #5F9EA0]/r[/bold #5F9EA0]", "Code review git changes (e.g. /review --staged, /review main..HEAD)")
69
+ table.add_row("[bold #5F9EA0]/skills[/bold #5F9EA0] [list|add|modify|remove|use]", "Manage reusable prompt skills")
69
70
  table.add_row("[bold #5F9EA0]/obsidian[/bold #5F9EA0] [set|enable|disable|status|init]", "Manage vault integration, scaffold project folders")
70
71
  table.add_row("[bold #5F9EA0]/tools[/bold #5F9EA0] [list|enable|disable|enable-group|disable-group]", "Toggle tools or groups (e.g. file_ops, task_mgmt)")
71
72
  table.add_row("[bold #5F9EA0]/setup[/bold #5F9EA0]", "Re-run the first-run setup wizard")
@@ -142,6 +143,30 @@ def show_cron_help_table(console):
142
143
  console.print("")
143
144
 
144
145
 
146
+ def show_skills_help_table(console):
147
+ """Display skills command help table.
148
+
149
+ Args:
150
+ console: Rich Console instance for output.
151
+ """
152
+ console.print("")
153
+ table = Table(show_header=True, box=box.SIMPLE_HEAD)
154
+ table.add_column("Command", no_wrap=True)
155
+ table.add_column("Description")
156
+
157
+ table.add_row("[bold #5F9EA0]/skills list[/bold #5F9EA0]", "List skills")
158
+ table.add_row("[bold #5F9EA0]/skills add[/bold #5F9EA0] <name>", "Create a skill in your editor")
159
+ table.add_row("[bold #5F9EA0]/skills edit[/bold #5F9EA0] <name>", "Edit an existing skill")
160
+ table.add_row("[bold #5F9EA0]/skills modify[/bold #5F9EA0] <name> <prompt>", "Replace a skill")
161
+ table.add_row("[bold #5F9EA0]/skills show[/bold #5F9EA0] <name>", "Show a skill")
162
+ table.add_row("[bold #5F9EA0]/skills load[/bold #5F9EA0] <name>", "Load a skill into this chat")
163
+ table.add_row("[bold #5F9EA0]/skills remove[/bold #5F9EA0] <name>", "Delete a skill")
164
+ table.add_row("[bold #5F9EA0]/skills dir[/bold #5F9EA0]", "Show the skills directory")
165
+
166
+ console.print(Panel(table, title="[bold #5F9EA0]Skills[/bold #5F9EA0]", border_style="grey23", padding=(0, 2)))
167
+ console.print("")
168
+
169
+
145
170
  def show_config_overview(chat_manager, console, debug_mode_container, current_provider):
146
171
  """Display comprehensive configuration overview.
147
172
 
package/src/ui/main.py CHANGED
@@ -35,7 +35,6 @@ from core.agentic import agentic_answer
35
35
  from utils.settings import MonokaiDarkBGStyle, left_align_headings
36
36
  from utils.paths import REPO_ROOT, RG_EXE_PATH
37
37
  from exceptions import BoneAgentError
38
- from tools.loader import load_all_tools
39
38
 
40
39
  # Console setup
41
40
  console = Console(theme=Theme({
@@ -382,9 +381,6 @@ def main():
382
381
  """Main interactive chat loop."""
383
382
 
384
383
  # Load all tools (built-in and user tools)
385
- # This populates the ToolRegistry with all decorated tools
386
- load_all_tools()
387
-
388
384
  # Check for config.yaml — run setup wizard on first run
389
385
  from ui.setup_wizard import is_first_run, run_wizard as _run_setup_wizard
390
386