agent-devkit 0.0.3 → 0.1.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 (57) hide show
  1. package/README.md +170 -1
  2. package/package.json +1 -1
  3. package/runtime/README.md +192 -14
  4. package/runtime/agent +29 -13
  5. package/runtime/agents/README.md +3 -0
  6. package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
  7. package/runtime/agents/github-pr-reviewer/README.md +7 -0
  8. package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
  9. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
  10. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
  11. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
  12. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
  13. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
  14. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
  15. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
  16. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
  17. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
  18. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
  19. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
  20. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
  21. package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
  22. package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
  23. package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
  24. package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
  25. package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
  26. package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
  27. package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
  28. package/runtime/cli/README.md +93 -0
  29. package/runtime/cli/aikit/__init__.py +1 -1
  30. package/runtime/cli/aikit/aliases.py +196 -0
  31. package/runtime/cli/aikit/app_home.py +78 -0
  32. package/runtime/cli/aikit/audit.py +344 -0
  33. package/runtime/cli/aikit/calendar.py +163 -0
  34. package/runtime/cli/aikit/diagnostics.py +9 -0
  35. package/runtime/cli/aikit/github_pr.py +254 -0
  36. package/runtime/cli/aikit/identity.py +75 -0
  37. package/runtime/cli/aikit/llm.py +240 -48
  38. package/runtime/cli/aikit/main.py +924 -24
  39. package/runtime/cli/aikit/memory.py +148 -8
  40. package/runtime/cli/aikit/notifications.py +9 -0
  41. package/runtime/cli/aikit/permissions.py +345 -0
  42. package/runtime/cli/aikit/personality.py +123 -0
  43. package/runtime/cli/aikit/providers.py +4 -5
  44. package/runtime/cli/aikit/scheduler.py +18 -0
  45. package/runtime/cli/aikit/sessions.py +396 -0
  46. package/runtime/cli/aikit/setup_wizard.py +12 -0
  47. package/runtime/cli/aikit/tasks.py +351 -0
  48. package/runtime/cli/aikit/toolchain.py +274 -0
  49. package/runtime/plugins/claude-code-ai-devkit/plugin.json +1 -1
  50. package/runtime/plugins/claude-skill-ai-devkit/plugin.json +1 -1
  51. package/runtime/providers/calendar.yaml +28 -0
  52. package/runtime/providers/github.yaml +42 -0
  53. package/runtime/providers/local-notification.yaml +19 -0
  54. package/runtime/providers/local-scheduler.yaml +24 -0
  55. package/runtime/scripts/README.md +1 -1
  56. package/runtime/scripts/verify-release-alignment.mjs +1 -1
  57. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -12,19 +12,48 @@ import sys
12
12
  from pathlib import Path
13
13
  from typing import Any
14
14
 
15
+ from cli.aikit.aliases import add_alias, list_aliases, remove_alias, sync_aliases
15
16
  from cli.aikit import __version__
17
+ from cli.aikit.audit import export_audit, list_audits, record_audit, show_audit
18
+ from cli.aikit.calendar import calendar_list, calendar_summary, calendar_today, calendar_tomorrow, configure_calendar
16
19
  from cli.aikit.diagnostics import build_diagnostics
17
20
  from cli.aikit.fallback import evaluate_provider_requirements
21
+ from cli.aikit.github_pr import pr_create_automation, pr_inspect, pr_list_review_requests, pr_review
18
22
  from cli.aikit.guardrails import evaluate_execution_guardrails
23
+ from cli.aikit.identity import enforce_identity_response, is_identity_question, local_identity_response, public_name
19
24
  from cli.aikit.llm import (
20
25
  configure_backend,
21
26
  doctor_backends,
22
27
  invoke_agent_prompt,
23
28
  list_backends,
24
- resolve_backend,
29
+ llm_preference,
25
30
  set_default_backend,
31
+ set_llm_preference,
26
32
  )
27
- from cli.aikit.memory import record_usage, reset_memory, show_memory, napkin_context
33
+ from cli.aikit.memory import memory_path_payload, napkin_context, record_usage, reset_memory, show_memory
34
+ from cli.aikit.personality import load_personality, reset_personality, setup_personality, update_personality
35
+ from cli.aikit.permissions import grant_permission, revoke_permission, show_permissions
36
+ from cli.aikit.sessions import (
37
+ build_contextual_prompt,
38
+ get_or_create_session,
39
+ list_sessions,
40
+ record_exchange,
41
+ resume_session,
42
+ show_session,
43
+ )
44
+ from cli.aikit.setup_wizard import setup_wizard
45
+ from cli.aikit.scheduler import run_scheduler_once, scheduler_daemon_plan
46
+ from cli.aikit.tasks import (
47
+ create_task,
48
+ delete_task,
49
+ list_tasks,
50
+ run_task,
51
+ show_task,
52
+ task_history,
53
+ update_task_schedule,
54
+ update_task_status,
55
+ )
56
+ from cli.aikit.toolchain import doctor_toolchain, install_toolchain, list_toolchain
28
57
  from cli.aikit.install import InstallError, install_runtime
29
58
  from cli.aikit.lock import lock_status, parse_profiles
30
59
  from cli.aikit.output import run_payload
@@ -69,6 +98,17 @@ DETERMINISTIC_COMMANDS = (
69
98
  "credential",
70
99
  "source",
71
100
  "memory",
101
+ "personality",
102
+ "setup",
103
+ "alias",
104
+ "session",
105
+ "toolchain",
106
+ "task",
107
+ "scheduler",
108
+ "calendar",
109
+ "pr",
110
+ "permissions",
111
+ "audit",
72
112
  "install",
73
113
  )
74
114
  LLM_COMMANDS = ("agent",)
@@ -86,11 +126,13 @@ def main(argv: list[str] | None = None, *, prog: str | None = None) -> int:
86
126
  try:
87
127
  result = dispatch(args)
88
128
  except DevKitError as exc:
129
+ maybe_record_cli_audit(args, result=None, error=str(exc))
89
130
  print(f"error: {exc}", file=sys.stderr)
90
131
  return 1
91
132
 
92
133
  if result is None:
93
134
  return 0
135
+ maybe_record_cli_audit(args, result=result, error=None)
94
136
 
95
137
  if getattr(args, "json", False):
96
138
  print(json.dumps(result, ensure_ascii=False, indent=2))
@@ -109,7 +151,15 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
109
151
  description="AI DevKit CLI",
110
152
  )
111
153
  parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
154
+ parser.add_argument("--dry-run", dest="global_dry_run", action="store_true", help="show execution plan without external write effects")
112
155
  parser.add_argument("-v", "--version", action="store_true", help="print CLI version and exit")
156
+ parser.add_argument(
157
+ "-s",
158
+ "--sessions",
159
+ dest="sessions_shortcut",
160
+ action="store_true",
161
+ help="list local conversation sessions and exit",
162
+ )
113
163
 
114
164
  subparsers = parser.add_subparsers(dest="command")
115
165
 
@@ -119,6 +169,10 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
119
169
  )
120
170
  agent_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
121
171
  agent_parser.add_argument("--llm", help="LLM backend id to use")
172
+ agent_parser.add_argument("--dry-run", action="store_true", help="show execution plan without invoking LLM or external writes")
173
+ agent_parser.add_argument("--no-llm-fallback", action="store_true", help="disable automatic fallback to secondary LLM backends")
174
+ agent_parser.add_argument("--session", dest="session_id", help="resume a local conversation session")
175
+ agent_parser.add_argument("--new-session", action="store_true", help="start a new local conversation session")
122
176
  agent_parser.add_argument("prompt", nargs=argparse.REMAINDER)
123
177
 
124
178
  commands_parser = subparsers.add_parser("commands", help="list CLI command modes")
@@ -159,10 +213,98 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
159
213
 
160
214
  memory_parser = subparsers.add_parser("memory", help="inspect or reset local AI DevKit memory")
161
215
  memory_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
162
- memory_parser.add_argument("action", nargs="?", default="show", choices=["show", "reset"])
216
+ memory_parser.add_argument("action", nargs="?", default="show", choices=["show", "path", "reset"])
163
217
  memory_parser.add_argument("--agent", dest="agent_id")
164
218
  memory_parser.add_argument("--source", dest="source_id")
165
219
  memory_parser.add_argument("--all", action="store_true", help="reset all local memory")
220
+ memory_parser.add_argument("--sessions", action="store_true", help="reset local conversation sessions")
221
+ memory_parser.add_argument("--tasks", action="store_true", help="reset local task schedules")
222
+ memory_parser.add_argument("--cache", action="store_true", help="reset local cache")
223
+
224
+ personality_parser = subparsers.add_parser("personality", help="inspect or update local Agent DevKit personality")
225
+ personality_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
226
+ personality_parser.add_argument("action", nargs="?", default="show", choices=["show", "edit", "reset"])
227
+ personality_parser.add_argument("--name", dest="agent_name", help="public agent name")
228
+ personality_parser.add_argument("--user-name", help="user name")
229
+ personality_parser.add_argument("--language", help="default response language")
230
+ personality_parser.add_argument("--tone", help="response tone")
231
+ personality_parser.add_argument("--detail-level", help="response detail level")
232
+
233
+ setup_parser = subparsers.add_parser("setup", help="run setup helpers")
234
+ setup_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
235
+ setup_parser.add_argument("--dry-run", action="store_true", help="show setup plan without installing external tools")
236
+ setup_parser.add_argument("--yes", action="store_true", help="confirm setup actions")
237
+ setup_parser.add_argument("action", nargs="?", default="plan", choices=["plan", "personality"])
238
+
239
+ alias_parser = subparsers.add_parser("alias", help="manage local command aliases for agent")
240
+ alias_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
241
+ alias_parser.add_argument("action", nargs="?", default="list", choices=["add", "list", "remove", "sync"])
242
+ alias_parser.add_argument("name", nargs="?")
243
+ alias_parser.add_argument("--force", action="store_true", help="allow replacing an existing local alias file")
244
+
245
+ session_parser = subparsers.add_parser("session", help="manage local conversation sessions")
246
+ session_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
247
+ session_parser.add_argument("action", nargs="?", default="list", choices=["list", "show", "resume"])
248
+ session_parser.add_argument("session_id", nargs="?")
249
+
250
+ toolchain_parser = subparsers.add_parser("toolchain", help="inspect or install external tools")
251
+ toolchain_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
252
+ toolchain_parser.add_argument("action", nargs="?", default="list", choices=["list", "doctor", "install"])
253
+ toolchain_parser.add_argument("tool", nargs="?", default="all")
254
+ toolchain_parser.add_argument("--dry-run", action="store_true", help="show install plan without executing it")
255
+ toolchain_parser.add_argument("--yes", action="store_true", help="confirm external tool installation")
256
+
257
+ task_parser = subparsers.add_parser("task", help="manage local scheduled tasks")
258
+ task_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
259
+ task_parser.add_argument("action", nargs="?", default="list", choices=["create", "list", "show", "history", "run", "pause", "resume", "update", "enable", "disable", "delete"])
260
+ task_parser.add_argument("task_id", nargs="?")
261
+ task_parser.add_argument("--title")
262
+ task_parser.add_argument("--prompt")
263
+ task_parser.add_argument("--every")
264
+ task_parser.add_argument("--cron")
265
+ task_parser.add_argument("--dry-run", action="store_true")
266
+ task_parser.add_argument("--yes", action="store_true")
267
+
268
+ scheduler_parser = subparsers.add_parser("scheduler", help="run local scheduler")
269
+ scheduler_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
270
+ scheduler_parser.add_argument("action", nargs="?", default="run-once", choices=["run-once", "daemon"])
271
+ scheduler_parser.add_argument("--dry-run", action="store_true")
272
+
273
+ calendar_parser = subparsers.add_parser("calendar", help="inspect configured calendar")
274
+ calendar_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
275
+ calendar_parser.add_argument("action", nargs="?", default="today", choices=["today", "tomorrow", "list", "configure"])
276
+ calendar_parser.add_argument("--from", dest="date_from")
277
+ calendar_parser.add_argument("--to", dest="date_to")
278
+ calendar_parser.add_argument("--ics")
279
+ calendar_parser.add_argument("--timezone")
280
+
281
+ pr_parser = subparsers.add_parser("pr", help="review GitHub pull requests")
282
+ pr_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
283
+ pr_parser.add_argument("action", nargs="?", default="list-review-requests", choices=["list-review-requests", "inspect", "review", "automation"])
284
+ pr_parser.add_argument("pr_ref", nargs="?")
285
+ pr_parser.add_argument("automation_action", nargs="?")
286
+ pr_parser.add_argument("--approve", action="store_true")
287
+ pr_parser.add_argument("--request-changes", action="store_true")
288
+ pr_parser.add_argument("--comment")
289
+ pr_parser.add_argument("--allow-write", action="store_true")
290
+ pr_parser.add_argument("--dry-run", action="store_true")
291
+ pr_parser.add_argument("--time", default="09:00")
292
+
293
+ permissions_parser = subparsers.add_parser("permissions", help="manage local permission policies")
294
+ permissions_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
295
+ permissions_parser.add_argument("action", nargs="?", default="show", choices=["show", "grant", "revoke"])
296
+ permissions_parser.add_argument("agent", nargs="?")
297
+ permissions_parser.add_argument("provider", nargs="?")
298
+ permissions_parser.add_argument("level", nargs="?")
299
+ permissions_parser.add_argument("--project")
300
+ permissions_parser.add_argument("--task", dest="task_id")
301
+
302
+ audit_parser = subparsers.add_parser("audit", help="inspect local execution audit trail")
303
+ audit_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
304
+ audit_parser.add_argument("action", nargs="?", default="list", choices=["list", "show", "export"])
305
+ audit_parser.add_argument("execution_id", nargs="?")
306
+ audit_parser.add_argument("--limit", type=int, default=20)
307
+ audit_parser.add_argument("--format", default="md", choices=["md", "json"])
166
308
 
167
309
  llm_parser = subparsers.add_parser("llm", help="manage LLM backends")
168
310
  llm_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
@@ -170,14 +312,17 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
170
312
  "action",
171
313
  nargs="?",
172
314
  default="list",
173
- choices=["list", "doctor", "configure", "set-default"],
315
+ choices=["list", "doctor", "configure", "set-default", "preference"],
174
316
  )
175
317
  llm_parser.add_argument("backend", nargs="?")
318
+ llm_parser.add_argument("preference_value", nargs="?")
176
319
  llm_parser.add_argument("--api-key-env", help="environment variable that stores the API key")
177
320
  llm_parser.add_argument("--base-url", help="OpenAI-compatible base URL")
178
321
  llm_parser.add_argument("--model", help="default model id")
179
322
  llm_parser.add_argument("--command", dest="host_command", help="host CLI command name or path")
180
323
  llm_parser.add_argument("--set-default", action="store_true", help="set backend as the default LLM")
324
+ llm_parser.add_argument("--primary", help="primary backend for LLM preference")
325
+ llm_parser.add_argument("--order", help="comma-separated fallback order")
181
326
 
182
327
  install_parser = subparsers.add_parser("install", help="install AI DevKit host artifacts")
183
328
  install_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
@@ -237,6 +382,8 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
237
382
  def dispatch(args: argparse.Namespace) -> dict[str, Any] | None:
238
383
  if args.version:
239
384
  return {"kind": "version", "program": getattr(args, "prog_name", "aikit"), "version": __version__}
385
+ if args.sessions_shortcut:
386
+ return list_sessions()
240
387
 
241
388
  if not args.command:
242
389
  raise DevKitError("missing command. Use --help for usage.")
@@ -256,6 +403,28 @@ def dispatch(args: argparse.Namespace) -> dict[str, Any] | None:
256
403
  return dispatch_source(args)
257
404
  if command == "memory":
258
405
  return dispatch_memory(args)
406
+ if command == "personality":
407
+ return dispatch_personality(args)
408
+ if command == "setup":
409
+ return dispatch_setup(args)
410
+ if command == "alias":
411
+ return dispatch_alias(args)
412
+ if command == "session":
413
+ return dispatch_session(args)
414
+ if command == "toolchain":
415
+ return dispatch_toolchain(args)
416
+ if command == "task":
417
+ return dispatch_task(args)
418
+ if command == "scheduler":
419
+ return dispatch_scheduler(args)
420
+ if command == "calendar":
421
+ return dispatch_calendar(args)
422
+ if command == "pr":
423
+ return dispatch_pr(args)
424
+ if command == "permissions":
425
+ return dispatch_permissions(args)
426
+ if command == "audit":
427
+ return dispatch_audit(args)
259
428
  if command == "llm":
260
429
  return dispatch_llm(args)
261
430
  if command == "install":
@@ -327,8 +496,22 @@ def list_command_modes() -> dict[str, Any]:
327
496
  }
328
497
 
329
498
 
499
+ def maybe_record_cli_audit(args: argparse.Namespace, *, result: dict[str, Any] | None, error: str | None) -> None:
500
+ command = canonical_command(getattr(args, "command", None) or "")
501
+ if command in {"", "audit"} or getattr(args, "version", False):
502
+ return
503
+ try:
504
+ audit = record_audit(command=command, args=vars(args), result=result, error=error)
505
+ except Exception:
506
+ return
507
+ if result is not None:
508
+ result["audit"] = audit
509
+
510
+
330
511
  def dispatch_llm(args: argparse.Namespace) -> dict[str, Any]:
331
512
  try:
513
+ if args.action != "preference" and args.preference_value:
514
+ raise DevKitError(f"llm {args.action} received an unexpected argument: {args.preference_value}")
332
515
  if args.action == "list":
333
516
  if args.backend:
334
517
  raise DevKitError("llm list does not accept a backend argument")
@@ -350,6 +533,19 @@ def dispatch_llm(args: argparse.Namespace) -> dict[str, Any]:
350
533
  if not args.backend:
351
534
  raise DevKitError("llm set-default requires a backend")
352
535
  return set_default_backend(args.backend)
536
+ if args.action == "preference":
537
+ if args.backend in {None, "show"}:
538
+ return llm_preference()
539
+ if args.backend == "set":
540
+ if not args.primary and not args.order:
541
+ raise DevKitError("llm preference set requires --primary or --order")
542
+ return set_llm_preference(primary=args.primary, order=args.order)
543
+ if args.backend == "reorder":
544
+ order = args.preference_value or args.order
545
+ if not order:
546
+ raise DevKitError("llm preference reorder requires an order value or --order")
547
+ return set_llm_preference(order=order)
548
+ raise DevKitError("llm preference action must be show, set or reorder")
353
549
  except ValueError as exc:
354
550
  raise DevKitError(str(exc)) from exc
355
551
  raise DevKitError(f"unsupported llm action: {args.action}")
@@ -363,7 +559,7 @@ def dispatch_install(args: argparse.Namespace) -> dict[str, Any]:
363
559
  host=args.host,
364
560
  target=Path(args.target) if args.target else None,
365
561
  home=Path(args.home) if args.home else None,
366
- dry_run=args.dry_run,
562
+ dry_run=effective_dry_run(args),
367
563
  profiles=parse_profiles(args.profiles),
368
564
  )
369
565
  except InstallError as exc:
@@ -452,40 +648,476 @@ def dispatch_source(args: argparse.Namespace) -> dict[str, Any]:
452
648
  def dispatch_memory(args: argparse.Namespace) -> dict[str, Any]:
453
649
  if args.action == "show":
454
650
  return show_memory(ROOT, agent_id=args.agent_id, source_id=args.source_id)
651
+ if args.action == "path":
652
+ return memory_path_payload()
455
653
  if args.action == "reset":
456
- return reset_memory(all_memory=args.all, agent_id=args.agent_id, source_id=args.source_id)
654
+ return reset_memory(
655
+ all_memory=args.all,
656
+ agent_id=args.agent_id,
657
+ source_id=args.source_id,
658
+ reset_sessions=args.sessions,
659
+ reset_tasks=args.tasks,
660
+ reset_cache=args.cache,
661
+ )
457
662
  raise DevKitError(f"unsupported memory action: {args.action}")
458
663
 
459
664
 
665
+ def dispatch_personality(args: argparse.Namespace) -> dict[str, Any]:
666
+ if args.action == "show":
667
+ return load_personality()
668
+ if args.action == "edit":
669
+ if not any([args.agent_name, args.user_name, args.language, args.tone, args.detail_level]):
670
+ payload = load_personality()
671
+ payload["status"] = "needs-input"
672
+ payload["message"] = "Use --name, --user-name, --language, --tone or --detail-level to edit non-interactively."
673
+ return payload
674
+ return update_personality(
675
+ agent_name=args.agent_name,
676
+ user_name=args.user_name,
677
+ language=args.language,
678
+ tone=args.tone,
679
+ detail_level=args.detail_level,
680
+ )
681
+ if args.action == "reset":
682
+ return reset_personality()
683
+ raise DevKitError(f"unsupported personality action: {args.action}")
684
+
685
+
686
+ def dispatch_setup(args: argparse.Namespace) -> dict[str, Any]:
687
+ if args.action == "personality":
688
+ return setup_personality()
689
+ if args.action == "plan":
690
+ return setup_wizard(ROOT, dry_run=effective_dry_run(args), yes=args.yes)
691
+ raise DevKitError(f"unsupported setup action: {args.action}")
692
+
693
+
694
+ def dispatch_toolchain(args: argparse.Namespace) -> dict[str, Any]:
695
+ try:
696
+ if args.action == "list":
697
+ if args.tool != "all":
698
+ raise DevKitError("toolchain list does not accept a tool argument")
699
+ return list_toolchain(ROOT)
700
+ if args.action == "doctor":
701
+ return doctor_toolchain(ROOT, None if args.tool == "all" else args.tool)
702
+ if args.action == "install":
703
+ return install_toolchain(ROOT, None if args.tool == "all" else args.tool, dry_run=effective_dry_run(args), yes=args.yes)
704
+ except ValueError as exc:
705
+ raise DevKitError(str(exc)) from exc
706
+ raise DevKitError(f"unsupported toolchain action: {args.action}")
707
+
708
+
709
+ def dispatch_task(args: argparse.Namespace) -> dict[str, Any]:
710
+ try:
711
+ if args.action == "list":
712
+ return list_tasks()
713
+ if args.action == "create":
714
+ schedule = {"type": "manual"}
715
+ if args.every:
716
+ schedule = {"type": "interval", "every": args.every}
717
+ if args.cron:
718
+ schedule = {"type": "cron", "cron": args.cron}
719
+ return create_task(task_id=args.task_id, title=args.title, prompt=args.prompt, schedule=schedule)
720
+ if args.action == "show":
721
+ require_id(args.task_id, "task show")
722
+ return show_task(args.task_id)
723
+ if args.action == "history":
724
+ require_id(args.task_id, "task history")
725
+ return task_history(args.task_id)
726
+ if args.action == "run":
727
+ require_id(args.task_id, "task run")
728
+ return run_task(args.task_id, dry_run=effective_dry_run(args))
729
+ if args.action in {"pause", "disable"}:
730
+ require_id(args.task_id, f"task {args.action}")
731
+ return update_task_status(args.task_id, "paused" if args.action == "pause" else "disabled")
732
+ if args.action in {"resume", "enable"}:
733
+ require_id(args.task_id, f"task {args.action}")
734
+ return update_task_status(args.task_id, "enabled")
735
+ if args.action == "update":
736
+ require_id(args.task_id, "task update")
737
+ return update_task_schedule(args.task_id, every=args.every, cron=args.cron)
738
+ if args.action == "delete":
739
+ require_id(args.task_id, "task delete")
740
+ return delete_task(args.task_id, yes=args.yes)
741
+ except ValueError as exc:
742
+ raise DevKitError(str(exc)) from exc
743
+ raise DevKitError(f"unsupported task action: {args.action}")
744
+
745
+
746
+ def dispatch_scheduler(args: argparse.Namespace) -> dict[str, Any]:
747
+ if args.action == "run-once":
748
+ return run_scheduler_once(dry_run=effective_dry_run(args))
749
+ if args.action == "daemon":
750
+ return scheduler_daemon_plan()
751
+ raise DevKitError(f"unsupported scheduler action: {args.action}")
752
+
753
+
754
+ def dispatch_calendar(args: argparse.Namespace) -> dict[str, Any]:
755
+ try:
756
+ if args.action == "configure":
757
+ return configure_calendar(ics_path=args.ics, timezone=args.timezone)
758
+ if args.action == "today":
759
+ return calendar_today()
760
+ if args.action == "tomorrow":
761
+ return calendar_tomorrow()
762
+ if args.action == "list":
763
+ return calendar_list(args.date_from, args.date_to)
764
+ except ValueError as exc:
765
+ raise DevKitError(str(exc)) from exc
766
+ raise DevKitError(f"unsupported calendar action: {args.action}")
767
+
768
+
769
+ def dispatch_pr(args: argparse.Namespace) -> dict[str, Any]:
770
+ if args.action == "list-review-requests":
771
+ if effective_dry_run(args):
772
+ return pr_read_dry_run("list-review-requests")
773
+ return pr_list_review_requests()
774
+ if args.action == "inspect":
775
+ require_id(args.pr_ref, "pr inspect")
776
+ if effective_dry_run(args):
777
+ return pr_read_dry_run("inspect", pr_ref=args.pr_ref)
778
+ return pr_inspect(args.pr_ref)
779
+ if args.action == "review":
780
+ require_id(args.pr_ref, "pr review")
781
+ return pr_review(
782
+ args.pr_ref,
783
+ approve=args.approve,
784
+ request_changes=args.request_changes,
785
+ comment=args.comment,
786
+ allow_write=args.allow_write,
787
+ dry_run=effective_dry_run(args),
788
+ )
789
+ if args.action == "automation":
790
+ if args.automation_action not in {None, "create"}:
791
+ raise DevKitError("pr automation action must be create")
792
+ if effective_dry_run(args):
793
+ return pr_automation_dry_run(time=args.time)
794
+ return pr_create_automation(time=args.time)
795
+ raise DevKitError(f"unsupported pr action: {args.action}")
796
+
797
+
798
+ def dispatch_permissions(args: argparse.Namespace) -> dict[str, Any]:
799
+ try:
800
+ if args.action == "show":
801
+ return show_permissions()
802
+ if args.action == "grant":
803
+ if not args.agent or not args.provider or not args.level:
804
+ raise DevKitError("permissions grant requires agent, provider and level")
805
+ return grant_permission(args.agent, args.provider, args.level, project=args.project, task_id=args.task_id)
806
+ if args.action == "revoke":
807
+ if not args.agent or not args.provider:
808
+ raise DevKitError("permissions revoke requires agent and provider")
809
+ return revoke_permission(args.agent, args.provider, args.level, project=args.project, task_id=args.task_id)
810
+ except ValueError as exc:
811
+ raise DevKitError(str(exc)) from exc
812
+ raise DevKitError(f"unsupported permissions action: {args.action}")
813
+
814
+
815
+ def dispatch_audit(args: argparse.Namespace) -> dict[str, Any]:
816
+ try:
817
+ if args.action == "list":
818
+ if args.execution_id:
819
+ raise DevKitError("audit list does not accept an execution id")
820
+ return list_audits(limit=max(1, int(args.limit or 20)))
821
+ if args.action == "show":
822
+ require_id(args.execution_id, "audit show")
823
+ return show_audit(args.execution_id)
824
+ if args.action == "export":
825
+ require_id(args.execution_id, "audit export")
826
+ return export_audit(args.execution_id, fmt=args.format)
827
+ except ValueError as exc:
828
+ raise DevKitError(str(exc)) from exc
829
+ raise DevKitError(f"unsupported audit action: {args.action}")
830
+
831
+
832
+ def pr_read_dry_run(action: str, *, pr_ref: str | None = None) -> dict[str, Any]:
833
+ return {
834
+ "kind": "pr",
835
+ "status": "planned",
836
+ "ok": True,
837
+ "dry_run": True,
838
+ "mode": "report-only",
839
+ "provider": "github",
840
+ "action": action,
841
+ "pr_ref": pr_ref,
842
+ "commands": planned_pr_commands(action, pr_ref=pr_ref),
843
+ "summary": "Dry-run only. GitHub would be read through gh; no PR write action would be submitted.",
844
+ }
845
+
846
+
847
+ def pr_automation_dry_run(*, time: str) -> dict[str, Any]:
848
+ return {
849
+ "kind": "pr-automation",
850
+ "status": "planned",
851
+ "ok": True,
852
+ "dry_run": True,
853
+ "mode": "report-only",
854
+ "provider": "github",
855
+ "task": {
856
+ "id": "daily-pr-review",
857
+ "title": "Revisar PRs pendentes diariamente",
858
+ "schedule": {"type": "daily", "time": time},
859
+ "action": {
860
+ "type": "capability",
861
+ "agent": "github-pr-reviewer",
862
+ "capability": "list-review-requests",
863
+ "external_writes": False,
864
+ },
865
+ "permissions": {"mode": "report-only", "comment": False, "approve": False, "request_changes": False},
866
+ },
867
+ "summary": "Dry-run only. A local report-only PR review task would be created.",
868
+ }
869
+
870
+
871
+ def planned_pr_commands(action: str, *, pr_ref: str | None = None) -> list[list[str]]:
872
+ if action == "list-review-requests":
873
+ return [["gh", "pr", "list", "--review-requested", "@me", "--json", "number,title,url,author,headRefName,baseRefName,isDraft"]]
874
+ if action == "inspect":
875
+ return [["gh", "pr", "view", str(pr_ref), "--json", "number,title,url,author,body,headRefName,baseRefName,state,isDraft,reviewDecision,mergeable"]]
876
+ return []
877
+
878
+
879
+ def require_id(value: str | None, command: str) -> None:
880
+ if not value:
881
+ raise DevKitError(f"{command} requires an id")
882
+
883
+
884
+ def effective_dry_run(args: argparse.Namespace) -> bool:
885
+ return bool(getattr(args, "dry_run", False) or getattr(args, "global_dry_run", False))
886
+
887
+
888
+ def dispatch_alias(args: argparse.Namespace) -> dict[str, Any]:
889
+ try:
890
+ if args.action == "list":
891
+ if args.name:
892
+ raise DevKitError("alias list does not accept a name")
893
+ return list_aliases()
894
+ if args.action == "add":
895
+ if not args.name:
896
+ raise DevKitError("alias add requires a name")
897
+ return add_alias(args.name, force=args.force)
898
+ if args.action == "remove":
899
+ if not args.name:
900
+ raise DevKitError("alias remove requires a name")
901
+ return remove_alias(args.name)
902
+ if args.action == "sync":
903
+ if args.name:
904
+ raise DevKitError("alias sync does not accept a name")
905
+ return sync_aliases()
906
+ except ValueError as exc:
907
+ raise DevKitError(str(exc)) from exc
908
+ raise DevKitError(f"unsupported alias action: {args.action}")
909
+
910
+
911
+ def dispatch_session(args: argparse.Namespace) -> dict[str, Any]:
912
+ try:
913
+ if args.action == "list":
914
+ if args.session_id:
915
+ raise DevKitError("session list does not accept a session id")
916
+ return list_sessions()
917
+ if args.action == "show":
918
+ if not args.session_id:
919
+ raise DevKitError("session show requires a session id")
920
+ return show_session(args.session_id)
921
+ if args.action == "resume":
922
+ if not args.session_id:
923
+ raise DevKitError("session resume requires a session id")
924
+ return resume_session(args.session_id)
925
+ except ValueError as exc:
926
+ raise DevKitError(str(exc)) from exc
927
+ raise DevKitError(f"unsupported session action: {args.action}")
928
+
929
+
460
930
  def agent_requires_llm(args: argparse.Namespace) -> dict[str, Any]:
461
931
  prompt = " ".join(args.prompt).strip()
462
932
  if not prompt:
463
933
  raise DevKitError("agent requires a natural-language prompt")
464
- route = route_prompt(prompt)
465
- if route:
466
- return invoke_deterministic_route(prompt, route)
934
+ if effective_dry_run(args):
935
+ return build_agent_dry_run_plan(prompt, args)
467
936
  try:
468
- backend = resolve_backend(args.llm)
937
+ session = get_or_create_session(
938
+ session_id=args.session_id,
939
+ force_new=args.new_session,
940
+ prompt=prompt,
941
+ project=str(Path.cwd()),
942
+ backend=args.llm,
943
+ )
469
944
  except ValueError as exc:
470
945
  raise DevKitError(str(exc)) from exc
471
- if backend and backend.get("status") == "ok":
472
- return invoke_agent_prompt(prompt, args.llm)
473
- return {
946
+ personality = load_personality()
947
+ name = public_name(personality=personality, invoked_as=getattr(args, "prog_name", "agent"))
948
+ if is_identity_question(prompt):
949
+ result = {
950
+ "kind": "agent",
951
+ "status": "ok",
952
+ "ok": True,
953
+ "requires_llm": False,
954
+ "prompt_received": True,
955
+ "prompt_length": len(prompt),
956
+ "identity": {"name": name, "source": "local"},
957
+ "response": local_identity_response(prompt, name=name),
958
+ }
959
+ return finalize_agent_session(result, session, prompt, backend=args.llm)
960
+ natural_result = dispatch_natural_operational_prompt(prompt)
961
+ if natural_result:
962
+ return finalize_agent_session(natural_result, session, prompt, backend=args.llm)
963
+ route = route_prompt(prompt)
964
+ if route:
965
+ result = invoke_deterministic_route(prompt, route)
966
+ return finalize_agent_session(result, session, prompt, backend=args.llm)
967
+ contextual_prompt = build_contextual_prompt(str(session["id"]), prompt)
968
+ result = invoke_agent_prompt(
969
+ contextual_prompt,
970
+ args.llm,
971
+ public_name=name,
972
+ allow_fallback=not args.no_llm_fallback,
973
+ )
974
+ result["prompt_length"] = len(prompt)
975
+ result["session_context_applied"] = contextual_prompt != prompt
976
+ if result.get("response"):
977
+ result["response"] = enforce_identity_response(str(result["response"]), prompt, name=name)
978
+ result["identity"] = {"name": name, "source": "local"}
979
+ return finalize_agent_session(result, session, prompt, backend=result.get("llm_backend") or args.llm)
980
+
981
+
982
+ def dispatch_natural_operational_prompt(prompt: str) -> dict[str, Any] | None:
983
+ normalized = " ".join(prompt.lower().split())
984
+ if "agenda" in normalized:
985
+ if "amanha" in normalized or "amanhã" in normalized:
986
+ payload = calendar_tomorrow()
987
+ else:
988
+ payload = calendar_today()
989
+ payload = dict(payload)
990
+ payload["kind"] = "agent"
991
+ payload["mode"] = "calendar-route"
992
+ payload["requires_llm"] = False
993
+ payload["prompt_received"] = True
994
+ payload["prompt_length"] = len(prompt)
995
+ payload["response"] = calendar_summary(payload)
996
+ if payload.get("status") == "needs-input":
997
+ payload["ok"] = False
998
+ else:
999
+ payload["ok"] = True
1000
+ return payload
1001
+ if has_pr_intent(normalized):
1002
+ if any(marker in normalized for marker in ("diariamente", "todo dia", "diaria", "diária", "recorrente")):
1003
+ payload = pr_create_automation()
1004
+ return {
1005
+ "kind": "agent",
1006
+ "status": payload.get("status"),
1007
+ "ok": True,
1008
+ "mode": "pr-automation-route",
1009
+ "requires_llm": False,
1010
+ "prompt_received": True,
1011
+ "prompt_length": len(prompt),
1012
+ "response": "Automacao diaria de revisao de PRs criada em modo report-only.",
1013
+ "result": payload,
1014
+ }
1015
+ payload = pr_list_review_requests()
1016
+ return {
1017
+ "kind": "agent",
1018
+ "status": payload.get("status"),
1019
+ "ok": payload.get("status") == "ok",
1020
+ "mode": "pr-route",
1021
+ "requires_llm": False,
1022
+ "prompt_received": True,
1023
+ "prompt_length": len(prompt),
1024
+ "response": summarize_pr_list(payload),
1025
+ "result": payload,
1026
+ "exit_code": payload.get("exit_code", 0 if payload.get("status") == "ok" else 2),
1027
+ }
1028
+ return None
1029
+
1030
+
1031
+ def build_agent_dry_run_plan(prompt: str, args: argparse.Namespace) -> dict[str, Any]:
1032
+ normalized = " ".join(prompt.lower().split())
1033
+ route = route_prompt(prompt)
1034
+ plan: dict[str, Any] = {
474
1035
  "kind": "agent",
475
- "status": "blocked",
476
- "ok": False,
477
- "requires_llm": True,
478
- "llm_backend": args.llm,
1036
+ "status": "planned",
1037
+ "ok": True,
1038
+ "dry_run": True,
1039
+ "requires_llm": False,
479
1040
  "prompt_received": True,
480
1041
  "prompt_length": len(prompt),
481
- "message": "agent requires a configured LLM backend for natural-language tasks.",
482
- "next_steps": [
483
- "Use `agent run <agent> <capability>` for deterministic execution without LLM.",
484
- "Configure a backend with `agent llm configure <backend> --set-default`.",
485
- "Inspect available backends with `agent llm list` and their status with `agent llm doctor`.",
486
- ],
487
- "exit_code": 2,
1042
+ "mode": "dry-run",
1043
+ "intent": "llm" if not route else route.get("intent"),
1044
+ "route": route,
1045
+ "llm_backend": getattr(args, "llm", None),
1046
+ "external_writes": False,
1047
+ "providers": {"used": [], "missing": [], "skipped": []},
1048
+ "commands": [],
1049
+ "permissions": [],
1050
+ "response": "Dry-run: nenhuma chamada LLM ou escrita externa foi executada.",
488
1051
  }
1052
+ if "agenda" in normalized:
1053
+ plan.update(
1054
+ {
1055
+ "intent": "calendar",
1056
+ "mode": "calendar-dry-run",
1057
+ "providers": {"used": ["calendar"], "missing": [], "skipped": []},
1058
+ "data_reads": ["configured calendar provider, if present"],
1059
+ "response": "Dry-run: o calendario seria consultado localmente se configurado.",
1060
+ }
1061
+ )
1062
+ return plan
1063
+ if has_pr_intent(normalized):
1064
+ recurring = any(marker in normalized for marker in ("diariamente", "todo dia", "diaria", "diária", "recorrente"))
1065
+ plan.update(
1066
+ {
1067
+ "intent": "github-pr-review",
1068
+ "mode": "pr-dry-run",
1069
+ "providers": {"used": ["github"], "missing": [], "skipped": []},
1070
+ "commands": planned_pr_commands("list-review-requests"),
1071
+ "external_writes": False,
1072
+ "permissions": [{"agent": "github-pr-reviewer", "provider": "github", "required_level": "read-only"}],
1073
+ "response": (
1074
+ "Dry-run: a automacao diaria de PR seria planejada em modo report-only."
1075
+ if recurring
1076
+ else "Dry-run: PRs aguardando revisao seriam listadas via gh em modo report-only."
1077
+ ),
1078
+ }
1079
+ )
1080
+ return plan
1081
+ if route:
1082
+ plan["providers"] = {"used": [route.get("provider")], "missing": [], "skipped": []}
1083
+ plan["response"] = "Dry-run: a capability roteada seria executada somente apos validar source/provider."
1084
+ return plan
1085
+ plan["response"] = "Dry-run: o prompt exigiria LLM configurada; nenhuma chamada foi feita."
1086
+ return plan
1087
+
1088
+
1089
+ def has_pr_intent(normalized_prompt: str) -> bool:
1090
+ tokens = {token.strip(".,;:!?()[]{}\"'") for token in normalized_prompt.split()}
1091
+ return bool({"pr", "prs"} & tokens) or "pull request" in normalized_prompt or "pull requests" in normalized_prompt
1092
+
1093
+
1094
+ def summarize_pr_list(payload: dict[str, Any]) -> str:
1095
+ if payload.get("status") != "ok":
1096
+ return str(payload.get("message") or "Nao foi possivel listar PRs.")
1097
+ items = payload.get("items") or []
1098
+ if not items:
1099
+ return "Nenhuma PR aguardando sua revisao foi encontrada."
1100
+ lines = []
1101
+ for item in items:
1102
+ number = item.get("number")
1103
+ title = item.get("title") or "-"
1104
+ url = item.get("url") or ""
1105
+ lines.append(f"- #{number} {title} {url}".strip())
1106
+ return "\n".join(lines)
1107
+
1108
+
1109
+ def finalize_agent_session(
1110
+ result: dict[str, Any],
1111
+ session: dict[str, Any],
1112
+ prompt: str,
1113
+ *,
1114
+ backend: str | None = None,
1115
+ ) -> dict[str, Any]:
1116
+ try:
1117
+ result["session"] = record_exchange(str(session["id"]), prompt=prompt, result=result, backend=backend)
1118
+ except ValueError as exc:
1119
+ raise DevKitError(str(exc)) from exc
1120
+ return result
489
1121
 
490
1122
 
491
1123
  def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str, Any]:
@@ -1050,6 +1682,8 @@ def print_human(result: dict[str, Any]) -> None:
1050
1682
  print_llm_configure(result)
1051
1683
  elif kind == "llm-default":
1052
1684
  print_llm_default(result)
1685
+ elif kind == "llm-preference":
1686
+ print_llm_preference(result)
1053
1687
  elif kind == "providers":
1054
1688
  print_providers(result)
1055
1689
  elif kind == "provider-status":
@@ -1072,8 +1706,48 @@ def print_human(result: dict[str, Any]) -> None:
1072
1706
  print_source_remove(result)
1073
1707
  elif kind == "memory":
1074
1708
  print_memory(result)
1709
+ elif kind == "memory-path":
1710
+ print_memory_path(result)
1075
1711
  elif kind == "memory-reset":
1076
1712
  print_memory_reset(result)
1713
+ elif kind == "personality":
1714
+ print_personality(result)
1715
+ elif kind == "aliases":
1716
+ print_aliases(result)
1717
+ elif kind == "alias":
1718
+ print_alias(result)
1719
+ elif kind == "sessions":
1720
+ print_sessions(result)
1721
+ elif kind == "session":
1722
+ print_session(result)
1723
+ elif kind == "setup":
1724
+ print_setup(result)
1725
+ elif kind == "toolchain":
1726
+ print_toolchain(result)
1727
+ elif kind == "toolchain-doctor":
1728
+ print_toolchain_doctor(result)
1729
+ elif kind == "toolchain-install":
1730
+ print_toolchain_install(result)
1731
+ elif kind == "tasks":
1732
+ print_tasks(result)
1733
+ elif kind == "task":
1734
+ print_task(result)
1735
+ elif kind == "task-history":
1736
+ print_task_history(result)
1737
+ elif kind == "task-run":
1738
+ print_task_run(result)
1739
+ elif kind == "scheduler":
1740
+ print_scheduler(result)
1741
+ elif kind == "calendar":
1742
+ print_calendar(result)
1743
+ elif kind == "calendar-configure":
1744
+ print_calendar_configure(result)
1745
+ elif kind in {"pr", "pr-review", "pr-automation"}:
1746
+ print_pr(result)
1747
+ elif kind == "permissions":
1748
+ print_permissions(result)
1749
+ elif kind in {"audit", "audit-entry", "audit-export"}:
1750
+ print_audit(result)
1077
1751
  elif kind == "install":
1078
1752
  print_install(result)
1079
1753
  else:
@@ -1242,6 +1916,10 @@ def print_source_remove(result: dict[str, Any]) -> None:
1242
1916
 
1243
1917
  def print_memory(result: dict[str, Any]) -> None:
1244
1918
  print(f"Memory home: {result['memory_home']}")
1919
+ if result.get("files"):
1920
+ print("\nFiles:")
1921
+ for item in result["files"]:
1922
+ print(f"- {item['name']}: {item['path']}")
1245
1923
  for bucket in ("prompts", "routes", "sources"):
1246
1924
  print(f"\n{bucket.title()}:")
1247
1925
  items = result["usage"].get(bucket) or []
@@ -1257,6 +1935,218 @@ def print_memory_reset(result: dict[str, Any]) -> None:
1257
1935
  print(f"Config: {result['config_path']}")
1258
1936
 
1259
1937
 
1938
+ def print_memory_path(result: dict[str, Any]) -> None:
1939
+ print(f"Memory home: {result['home']}")
1940
+ if result.get("created"):
1941
+ print("Created:")
1942
+ for path in result["created"]:
1943
+ print(f"- {path}")
1944
+ print("Files:")
1945
+ for item in result["files"]:
1946
+ print(f"- {item['name']}: {item['path']}")
1947
+
1948
+
1949
+ def print_personality(result: dict[str, Any]) -> None:
1950
+ print(f"Personality: {result.get('status', 'ok')}")
1951
+ print(f"Path: {result['path']}")
1952
+ print(f"Agent name: {result.get('agent_name') or '-'}")
1953
+ print(f"User name: {result.get('user_name') or '-'}")
1954
+ print(f"Language: {result.get('language') or '-'}")
1955
+ print(f"Tone: {result.get('tone') or '-'}")
1956
+ print(f"Detail level: {result.get('detail_level') or '-'}")
1957
+ if result.get("message"):
1958
+ print(result["message"])
1959
+ if result.get("questions"):
1960
+ print("Setup questions:")
1961
+ for question in result["questions"]:
1962
+ print(f"- {question}")
1963
+
1964
+
1965
+ def print_aliases(result: dict[str, Any]) -> None:
1966
+ print(f"Aliases config: {result['config_path']}")
1967
+ if not result["items"]:
1968
+ print("No aliases configured.")
1969
+ return
1970
+ for item in result["items"]:
1971
+ print(f"- {item['name']}: {item['path']}")
1972
+
1973
+
1974
+ def print_alias(result: dict[str, Any]) -> None:
1975
+ print(f"Alias {result['status']}: {result['name']}")
1976
+ if result.get("path"):
1977
+ print(f"Path: {result['path']}")
1978
+ if result.get("removed_paths"):
1979
+ print("Removed:")
1980
+ for path in result["removed_paths"]:
1981
+ print(f"- {path}")
1982
+ print(f"Config: {result['config_path']}")
1983
+
1984
+
1985
+ def print_sessions(result: dict[str, Any]) -> None:
1986
+ print(f"Sessions home: {result['home']}")
1987
+ if result.get("active_session_id"):
1988
+ print(f"Active: {result['active_session_id']}")
1989
+ if not result["items"]:
1990
+ print("No sessions found.")
1991
+ return
1992
+ for item in result["items"]:
1993
+ marker = " active" if item.get("active") else ""
1994
+ print(
1995
+ f"- {item['id']}{marker} {item.get('title') or '-'} "
1996
+ f"{item.get('exchange_count', 0)} exchanges ~{item.get('token_estimate', 0)} tokens"
1997
+ )
1998
+ if item.get("project"):
1999
+ print(f" Project: {item['project']}")
2000
+
2001
+
2002
+ def print_session(result: dict[str, Any]) -> None:
2003
+ session = result["session"]
2004
+ print(f"Session {result['status']}: {session['id']}")
2005
+ print(f"Title: {session.get('title') or '-'}")
2006
+ print(f"Path: {session.get('path') or '-'}")
2007
+ print(f"Project: {session.get('project') or '-'}")
2008
+ print(f"Exchanges: {session.get('exchange_count', 0)}")
2009
+ print(f"Token estimate: {session.get('token_estimate', 0)}")
2010
+
2011
+
2012
+ def print_setup(result: dict[str, Any]) -> None:
2013
+ print(f"Setup: {result['status']}")
2014
+ print(f"Dry-run: {result.get('dry_run', False)}")
2015
+ toolchain = result.get("toolchain") or {}
2016
+ print(f"Toolchain: {toolchain.get('status', '-')}")
2017
+ missing = (toolchain.get("required_missing") or []) + (toolchain.get("optional_missing") or [])
2018
+ if missing:
2019
+ print(f"Missing: {', '.join(missing)}")
2020
+ if result.get("next_steps"):
2021
+ print("Next steps:")
2022
+ for step in result["next_steps"]:
2023
+ print(f"- {step}")
2024
+
2025
+
2026
+ def print_toolchain(result: dict[str, Any]) -> None:
2027
+ print(f"Toolchain: {result['status']}")
2028
+ print(f"Platform: {result['platform']}")
2029
+ for item in result["items"]:
2030
+ print(f"- {item['id']} {item['command']} required={item['required']}")
2031
+
2032
+
2033
+ def print_toolchain_doctor(result: dict[str, Any]) -> None:
2034
+ print(f"Toolchain doctor: {result['status']}")
2035
+ print(f"Platform: {result['platform']}")
2036
+ for item in result["items"]:
2037
+ print(f"- {item['id']}: {item['status']}")
2038
+ if item.get("binary"):
2039
+ print(f" {item['binary']}")
2040
+ if item.get("install"):
2041
+ print(f" Install: {item['install']}")
2042
+
2043
+
2044
+ def print_toolchain_install(result: dict[str, Any]) -> None:
2045
+ print(f"Toolchain install: {result['status']}")
2046
+ if result.get("message"):
2047
+ print(result["message"])
2048
+ for plan in result["plans"]:
2049
+ print(f"- {plan['id']}: {plan.get('command') or '-'}")
2050
+
2051
+
2052
+ def print_tasks(result: dict[str, Any]) -> None:
2053
+ print(f"Tasks: {len(result['items'])}")
2054
+ for item in result["items"]:
2055
+ print(f"- {item['id']} {item['status']} {item.get('title') or '-'}")
2056
+
2057
+
2058
+ def print_task(result: dict[str, Any]) -> None:
2059
+ if result.get("message"):
2060
+ print(result["message"])
2061
+ task = result.get("task") or {}
2062
+ if task:
2063
+ print(f"Task {result['status']}: {task.get('id')}")
2064
+ print(f"Title: {task.get('title') or '-'}")
2065
+ print(f"Status: {task.get('status') or '-'}")
2066
+
2067
+
2068
+ def print_task_history(result: dict[str, Any]) -> None:
2069
+ print(result.get("history") or "No history.")
2070
+
2071
+
2072
+ def print_task_run(result: dict[str, Any]) -> None:
2073
+ print(f"Task run: {result['status']}")
2074
+ if result.get("message"):
2075
+ print(result["message"])
2076
+
2077
+
2078
+ def print_scheduler(result: dict[str, Any]) -> None:
2079
+ print(f"Scheduler: {result['status']}")
2080
+ if result.get("message"):
2081
+ print(result["message"])
2082
+ if "due_count" in result:
2083
+ print(f"Due tasks: {result['due_count']}")
2084
+
2085
+
2086
+ def print_calendar(result: dict[str, Any]) -> None:
2087
+ if result.get("status") != "ok":
2088
+ print(result.get("message") or "Calendar is not available.")
2089
+ for step in result.get("next_steps") or []:
2090
+ print(f"- {step}")
2091
+ return
2092
+ print(calendar_summary(result))
2093
+
2094
+
2095
+ def print_calendar_configure(result: dict[str, Any]) -> None:
2096
+ print(f"Calendar configured: {result['provider']}")
2097
+ print(f"Source: {result['source_ref']}")
2098
+ print("Stored secret: no")
2099
+
2100
+
2101
+ def print_pr(result: dict[str, Any]) -> None:
2102
+ if result.get("message"):
2103
+ print(result["message"])
2104
+ if result.get("items") is not None:
2105
+ print(summarize_pr_list(result))
2106
+ elif result.get("summary"):
2107
+ print(result["summary"])
2108
+ elif result.get("task"):
2109
+ task = result["task"]
2110
+ print(f"PR automation {result['status']}: {task.get('id')}")
2111
+
2112
+
2113
+ def print_permissions(result: dict[str, Any]) -> None:
2114
+ print(f"Permissions: {result['status']}")
2115
+ if result.get("default_level"):
2116
+ print(f"Default: {result['default_level']}")
2117
+ if result.get("grant"):
2118
+ grant = result["grant"]
2119
+ print(f"Grant: {grant.get('agent')} / {grant.get('provider')} -> {grant.get('level')}")
2120
+ if result.get("removed") is not None:
2121
+ print(f"Removed: {len(result.get('removed') or [])}")
2122
+ grants = result.get("grants")
2123
+ if grants is not None:
2124
+ if not grants:
2125
+ print("No explicit grants.")
2126
+ for grant in grants:
2127
+ print(f"- {grant.get('agent')} / {grant.get('provider')} -> {grant.get('level')}")
2128
+ if result.get("json_path"):
2129
+ print(f"Policy: {result['json_path']}")
2130
+
2131
+
2132
+ def print_audit(result: dict[str, Any]) -> None:
2133
+ if result["kind"] == "audit":
2134
+ print(f"Audit home: {result['home']}")
2135
+ for item in result.get("items") or []:
2136
+ print(f"- {item.get('id')} {item.get('created_at')} {item.get('command')} {item.get('status')}")
2137
+ return
2138
+ if result["kind"] == "audit-entry":
2139
+ entry = result.get("entry") or {}
2140
+ print(f"Audit: {entry.get('id')}")
2141
+ print(f"Created: {entry.get('created_at')}")
2142
+ print(f"Command: {entry.get('command')}")
2143
+ print(f"Status: {(entry.get('result') or {}).get('status')}")
2144
+ print(f"JSON: {result.get('json_path')}")
2145
+ print(f"Markdown: {result.get('markdown_path')}")
2146
+ return
2147
+ print(result.get("content") or "")
2148
+
2149
+
1260
2150
  def print_llm_backends(result: dict[str, Any]) -> None:
1261
2151
  print(f"LLM config: {result['config_path']}")
1262
2152
  print(f"Default: {result.get('default') or '-'}")
@@ -1294,6 +2184,16 @@ def print_llm_default(result: dict[str, Any]) -> None:
1294
2184
  print(f"Config: {result['config_path']}")
1295
2185
 
1296
2186
 
2187
+ def print_llm_preference(result: dict[str, Any]) -> None:
2188
+ print(f"LLM preference: {result.get('status', 'ok')}")
2189
+ print(f"Primary: {result.get('primary') or '-'}")
2190
+ print(f"Fallback enabled: {result.get('fallback_enabled', True)}")
2191
+ print("Order:")
2192
+ for backend_id in result.get("order") or []:
2193
+ print(f"- {backend_id}")
2194
+ print(f"Config: {result['config_path']}")
2195
+
2196
+
1297
2197
  def print_providers(result: dict[str, Any]) -> None:
1298
2198
  if not result["items"]:
1299
2199
  print("No providers found.")