crabcode 0.1.0__py3-none-any.whl → 0.1.3__py3-none-any.whl

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 (116) hide show
  1. crabcode-0.1.3.dist-info/METADATA +48 -0
  2. crabcode-0.1.3.dist-info/RECORD +130 -0
  3. crabcode_cli/__init__.py +1 -1
  4. crabcode_cli/__main__.py +4 -0
  5. crabcode_cli/app.py +308 -4
  6. crabcode_cli/pipe.py +87 -26
  7. crabcode_cli/repl.py +1611 -187
  8. crabcode_core/__init__.py +1 -1
  9. crabcode_core/agent_manager.py +597 -0
  10. crabcode_core/api/__init__.py +15 -1
  11. crabcode_core/api/anthropic_adapter.py +53 -6
  12. crabcode_core/api/azure_adapter.py +68 -0
  13. crabcode_core/api/base.py +25 -1
  14. crabcode_core/api/codex_adapter.py +290 -18
  15. crabcode_core/api/gemini_adapter.py +26 -0
  16. crabcode_core/api/model_info.py +70 -0
  17. crabcode_core/api/ollama_adapter.py +27 -0
  18. crabcode_core/api/openai_adapter.py +54 -11
  19. crabcode_core/api/registry.py +18 -0
  20. crabcode_core/compact/compact.py +77 -8
  21. crabcode_core/config/claudemd.py +6 -2
  22. crabcode_core/config/manager.py +7 -1
  23. crabcode_core/events.py +789 -30
  24. crabcode_core/hooks/__init__.py +5 -0
  25. crabcode_core/hooks/manager.py +267 -0
  26. crabcode_core/logging_utils.py +108 -0
  27. crabcode_core/lsp/__init__.py +6 -0
  28. crabcode_core/lsp/client.py +757 -0
  29. crabcode_core/lsp/diagnostics.py +130 -0
  30. crabcode_core/lsp/manager.py +582 -0
  31. crabcode_core/mcp/client.py +7 -2
  32. crabcode_core/mcp/config.py +5 -1
  33. crabcode_core/permissions/manager.py +21 -2
  34. crabcode_core/plan/__init__.py +5 -0
  35. crabcode_core/plan/executor.py +225 -0
  36. crabcode_core/plan/types.py +154 -0
  37. crabcode_core/prompts/context.py +11 -4
  38. crabcode_core/prompts/system.py +82 -5
  39. crabcode_core/prompts/templates.py +13 -0
  40. crabcode_core/query/loop.py +693 -67
  41. crabcode_core/schedule/__init__.py +5 -0
  42. crabcode_core/schedule/models.py +204 -0
  43. crabcode_core/schedule/store.py +349 -0
  44. crabcode_core/session/__init__.py +10 -1
  45. crabcode_core/session/export.py +141 -0
  46. crabcode_core/session/meta_db.py +346 -0
  47. crabcode_core/session/storage.py +483 -15
  48. crabcode_core/session/title_gen.py +52 -0
  49. crabcode_core/skills/__init__.py +3 -2
  50. crabcode_core/skills/loader.py +70 -5
  51. crabcode_core/skills/matcher.py +203 -0
  52. crabcode_core/snapshot/__init__.py +21 -0
  53. crabcode_core/snapshot/snapshot.py +422 -0
  54. crabcode_core/snapshot/tracker.py +250 -0
  55. crabcode_core/team/__init__.py +27 -0
  56. crabcode_core/team/bridge.py +66 -0
  57. crabcode_core/team/inbox.py +102 -0
  58. crabcode_core/team/manager.py +577 -0
  59. crabcode_core/team/message_bus.py +365 -0
  60. crabcode_core/team/models.py +191 -0
  61. crabcode_core/team/recovery.py +150 -0
  62. crabcode_core/tools/__init__.py +46 -0
  63. crabcode_core/tools/_input_helpers.py +39 -0
  64. crabcode_core/tools/agent.py +259 -66
  65. crabcode_core/tools/ask_user.py +122 -0
  66. crabcode_core/tools/bash.py +8 -0
  67. crabcode_core/tools/browser.py +662 -0
  68. crabcode_core/tools/checklist.py +241 -0
  69. crabcode_core/tools/checkpoint.py +66 -0
  70. crabcode_core/tools/file_edit.py +59 -3
  71. crabcode_core/tools/file_read.py +33 -2
  72. crabcode_core/tools/file_write.py +34 -3
  73. crabcode_core/tools/glob.py +11 -2
  74. crabcode_core/tools/grep.py +41 -20
  75. crabcode_core/tools/lint.py +4 -0
  76. crabcode_core/tools/memory.py +4 -1
  77. crabcode_core/tools/revert.py +90 -0
  78. crabcode_core/tools/schedule.py +612 -0
  79. crabcode_core/tools/skill.py +14 -0
  80. crabcode_core/tools/switch_mode.py +171 -0
  81. crabcode_core/tools/team.py +324 -0
  82. crabcode_core/tools/web_search.py +320 -0
  83. crabcode_core/types/__init__.py +10 -0
  84. crabcode_core/types/config.py +72 -2
  85. crabcode_core/types/event.py +135 -0
  86. crabcode_core/types/message.py +16 -1
  87. crabcode_core/types/tool.py +20 -0
  88. crabcode_core/utf8_sanitize.py +29 -0
  89. crabcode_gateway/__init__.py +3 -0
  90. crabcode_gateway/acp/__init__.py +13 -0
  91. crabcode_gateway/acp/agent.py +635 -0
  92. crabcode_gateway/acp/session.py +114 -0
  93. crabcode_gateway/acp/transport.py +39 -0
  94. crabcode_gateway/acp/types.py +82 -0
  95. crabcode_gateway/adapter.py +30 -0
  96. crabcode_gateway/event_bus.py +184 -0
  97. crabcode_gateway/grpc/__init__.py +0 -0
  98. crabcode_gateway/grpc/proto/crabcode.proto +138 -0
  99. crabcode_gateway/grpc/server.py +415 -0
  100. crabcode_gateway/middleware.py +237 -0
  101. crabcode_gateway/routes/__init__.py +0 -0
  102. crabcode_gateway/routes/agent.py +153 -0
  103. crabcode_gateway/routes/config.py +184 -0
  104. crabcode_gateway/routes/event.py +356 -0
  105. crabcode_gateway/routes/health.py +14 -0
  106. crabcode_gateway/routes/permission.py +59 -0
  107. crabcode_gateway/routes/session.py +371 -0
  108. crabcode_gateway/routes/snapshot.py +87 -0
  109. crabcode_gateway/schemas.py +563 -0
  110. crabcode_gateway/server.py +186 -0
  111. crabcode_search/embedder.py +57 -7
  112. crabcode_search/tool.py +18 -2
  113. crabcode-0.1.0.dist-info/METADATA +0 -23
  114. crabcode-0.1.0.dist-info/RECORD +0 -64
  115. {crabcode-0.1.0.dist-info → crabcode-0.1.3.dist-info}/WHEEL +0 -0
  116. {crabcode-0.1.0.dist-info → crabcode-0.1.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.4
2
+ Name: crabcode
3
+ Version: 0.1.3
4
+ Summary: CrabCode - AI coding assistant in the terminal
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: anthropic>=0.40.0
7
+ Requires-Dist: httpx>=0.27.0
8
+ Requires-Dist: mcp>=1.0.0
9
+ Requires-Dist: openai>=1.50.0
10
+ Requires-Dist: prompt-toolkit>=3.0.0
11
+ Requires-Dist: pydantic>=2.0.0
12
+ Requires-Dist: rich>=13.0.0
13
+ Requires-Dist: tiktoken>=0.7.0
14
+ Requires-Dist: typer>=0.12.0
15
+ Provides-Extra: all
16
+ Requires-Dist: agent-client-protocol>=0.9.0; extra == 'all'
17
+ Requires-Dist: anthropic[bedrock]>=0.40.0; extra == 'all'
18
+ Requires-Dist: anthropic[vertex]>=0.40.0; extra == 'all'
19
+ Requires-Dist: fastapi>=0.115.0; extra == 'all'
20
+ Requires-Dist: grpcio-health-checking>=1.65.0; extra == 'all'
21
+ Requires-Dist: grpcio>=1.65.0; extra == 'all'
22
+ Requires-Dist: httpx>=0.27.0; extra == 'all'
23
+ Requires-Dist: modelscope>=1.10.0; extra == 'all'
24
+ Requires-Dist: playwright>=1.48.0; extra == 'all'
25
+ Requires-Dist: sentence-transformers>=2.0.0; extra == 'all'
26
+ Requires-Dist: sse-starlette>=2.0.0; extra == 'all'
27
+ Requires-Dist: tree-sitter-language-pack>=1.0.0; extra == 'all'
28
+ Requires-Dist: usearch>=2.12.0; extra == 'all'
29
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'all'
30
+ Provides-Extra: bedrock
31
+ Requires-Dist: anthropic[bedrock]>=0.40.0; extra == 'bedrock'
32
+ Provides-Extra: browser
33
+ Requires-Dist: playwright>=1.48.0; extra == 'browser'
34
+ Provides-Extra: gateway
35
+ Requires-Dist: agent-client-protocol>=0.9.0; extra == 'gateway'
36
+ Requires-Dist: fastapi>=0.115.0; extra == 'gateway'
37
+ Requires-Dist: grpcio-health-checking>=1.65.0; extra == 'gateway'
38
+ Requires-Dist: grpcio>=1.65.0; extra == 'gateway'
39
+ Requires-Dist: httpx>=0.27.0; extra == 'gateway'
40
+ Requires-Dist: sse-starlette>=2.0.0; extra == 'gateway'
41
+ Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'gateway'
42
+ Provides-Extra: search
43
+ Requires-Dist: modelscope>=1.10.0; extra == 'search'
44
+ Requires-Dist: sentence-transformers>=2.0.0; extra == 'search'
45
+ Requires-Dist: tree-sitter-language-pack>=1.0.0; extra == 'search'
46
+ Requires-Dist: usearch>=2.12.0; extra == 'search'
47
+ Provides-Extra: vertex
48
+ Requires-Dist: anthropic[vertex]>=0.40.0; extra == 'vertex'
@@ -0,0 +1,130 @@
1
+ crabcode_cli/__init__.py,sha256=3OCzOxlK28hMxxTQHAdbnvbEZXEb07WWKnCjyDwhW-0,63
2
+ crabcode_cli/__main__.py,sha256=ekwH_ayJ5YpefhbXSDj-IM9mS9YrCwWpoDIH92V6tjg,75
3
+ crabcode_cli/app.py,sha256=RJXlRgXygGIg2N_uJqFOMMPFDL1HuZVP0vmF3wVI-ps,17612
4
+ crabcode_cli/banner.py,sha256=K0ak6Ng4l85C3TWnh64ulvdF4ReJSRsaWe8DOjse3Zg,1403
5
+ crabcode_cli/pipe.py,sha256=M79twqrmeCy1QxiQ-jDThYy_CzPHE5llbQsDXQRAoW8,4162
6
+ crabcode_cli/repl.py,sha256=6ApZtL7K-P14HRws6vP-1RSNGLsSP6YbJ_7xY3Y_bZo,103474
7
+ crabcode_cli/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ crabcode_cli/render/messages.py,sha256=n9OsbLIAU3FyO9gwGMMJld7zq-71N0caz9ecmE2kIjo,568
9
+ crabcode_core/__init__.py,sha256=bW4jmccyG4G7EZO1yb_4yZP4zxwS_wmyuZXhIIhlv0Y,73
10
+ crabcode_core/agent_manager.py,sha256=fu-t3xmiNRQUjJhUCdicLA6lflyD7TbGLpANckBRqnk,22869
11
+ crabcode_core/events.py,sha256=7IrCHMB8oYGzr6zcvAPnLmV4john8HGszZEbH3zU9-Y,48382
12
+ crabcode_core/logging_utils.py,sha256=H6E7lldUAeIeQzQooMl0MrQ07YlSO-tra_QlzxcOdks,3657
13
+ crabcode_core/utf8_sanitize.py,sha256=RRTGgwA9jqK-HLR1WUSFwgG4xJn7ZukTDu7M6Oi3fHI,936
14
+ crabcode_core/api/__init__.py,sha256=IXs4hLgC4cT85TLdmS4KgtPKYDytadkwvrDyBshr5ag,674
15
+ crabcode_core/api/anthropic_adapter.py,sha256=xZPxI4Z1ega0ZOSgwZBn6ttBCXybyGT8Z8XT20OhZ_o,10999
16
+ crabcode_core/api/azure_adapter.py,sha256=zJ7q3Ni3sVwK3Bbju_nO8t8kPEA_bL6scVFC-XRPRh4,2360
17
+ crabcode_core/api/base.py,sha256=AvOD8lwEc60GeuMZgvjN7xrHqkPkWhyhHDIXnJQ4XoU,2871
18
+ crabcode_core/api/codex_adapter.py,sha256=zrwVS-Nvv0ABDhrRqNTCHbFLMNdT3gqYO0_5GjMtyXU,26511
19
+ crabcode_core/api/gemini_adapter.py,sha256=RVk3zlLPqDtzG8w5psu4aSoFyFLua5qQx3subqz-5DQ,1130
20
+ crabcode_core/api/model_info.py,sha256=kzbEbZ9BqJffu8VQwPAZC5WXKs1qHNFq-MnM6h5XWDI,2002
21
+ crabcode_core/api/ollama_adapter.py,sha256=ZpTg11zOUGFE0J88qBXL7AvxDQU5PjWg2wWvm4uxUvY,1158
22
+ crabcode_core/api/openai_adapter.py,sha256=wqEdUn-hq3ht2XHOgGweRR1MxEKPgOxYglKWQtTDeLU,9855
23
+ crabcode_core/api/registry.py,sha256=tCVRE74bMUn9ukCbGp3bDtSOYZ6_EmsNfdVnnkGN6k4,2413
24
+ crabcode_core/compact/__init__.py,sha256=P5C6D0MilT3FAnmaAxc8NeMi2CZLEMB-CwEzKUGqSlY,206
25
+ crabcode_core/compact/compact.py,sha256=dsdqaA7NTHMxHdwNoBorlymJtchgiYKN_V2KS5jlSp0,7008
26
+ crabcode_core/config/__init__.py,sha256=wbSnNVCqj673phu2pvIFz-f5HM5bLvH-FdD9b01HNRA,84
27
+ crabcode_core/config/claudemd.py,sha256=eUJQRdbT3fP0lB6C-Yc2pVBt0u4Z0qBEamXtkOErJWw,1973
28
+ crabcode_core/config/manager.py,sha256=OJ1o-wBhk1NCI-09Vzv9lb1jOcB8fn9rda428CGlYjc,5263
29
+ crabcode_core/hooks/__init__.py,sha256=wfOnbLuJqtLezKYWYmBTAE9qG-eaG7wSfgx1-kS7N64,165
30
+ crabcode_core/hooks/manager.py,sha256=dsySmLq1G-7ZAdf8hq-9FhknchlA5DWKQBA_ApirNPE,9387
31
+ crabcode_core/lsp/__init__.py,sha256=np9EYVvOi94ZFxLWW_kD13AfwaIhN0Vcp8-pHaLDn6c,189
32
+ crabcode_core/lsp/client.py,sha256=AdQ2hzryM4RrPDXdi168yV3kQCkb69O1B1-MNUIFWts,26699
33
+ crabcode_core/lsp/diagnostics.py,sha256=hcQq9MJY8HqMtKPKnRWfGK7GOgqmQAi2niaIKVUWNTI,4218
34
+ crabcode_core/lsp/manager.py,sha256=morGtndY0dE-O88u4LalOnnAl8KzOWmQHqnUTCphtfs,18969
35
+ crabcode_core/mcp/__init__.py,sha256=axvOgOWHjpKs7WFEkf8Za_4mm_oNe0tYyD94dAMnWTE,182
36
+ crabcode_core/mcp/client.py,sha256=3e7LQKq8kNyE2E6-wmSa_MpysgsznuPUS6sxWgp2RiA,5233
37
+ crabcode_core/mcp/config.py,sha256=DQZ39LX-smZ11oyqf-KnJvcqEriJF68Q6LewWn0hrdk,1955
38
+ crabcode_core/permissions/__init__.py,sha256=YfNm9B3NPQnVxDvRukkW787XbOfJBh3ck47lwyblQgk,131
39
+ crabcode_core/permissions/manager.py,sha256=JngKqqKk_1rtcBSmV9aNB08jDQ-d98_Y3xwhw69bOQ0,4217
40
+ crabcode_core/plan/__init__.py,sha256=co_SEC__Z3OtijC9hxdVWIcdUBsUVsJnQ-LkxJfpAL8,169
41
+ crabcode_core/plan/executor.py,sha256=H3krzji0PYGIEDv-bLRzcVGD2C9AOoEeqKUu-pIjE-Q,8357
42
+ crabcode_core/plan/types.py,sha256=BuYxXh8jLLyQZtcNG3F_UJOLMfbGlAE-mffKl69lGgE,5178
43
+ crabcode_core/prompts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ crabcode_core/prompts/context.py,sha256=EHFdO-3_386pnFPIDTruy2xDmrIZDgHwvU5v52_vWDQ,7250
45
+ crabcode_core/prompts/profile.py,sha256=MWxdrC0vVc6QfaRa5sgKEC8Gi_s61F9ZaxjoCTiiEPc,2088
46
+ crabcode_core/prompts/system.py,sha256=XHIAOPR5m3RSiPm141wv3AJpOZLMEz5tpt7eALNuM8s,33389
47
+ crabcode_core/prompts/templates.py,sha256=XZRdGXWc1-OGksOp1r6nML7Cr0T1SmT_ktulSs1VqPw,2570
48
+ crabcode_core/query/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ crabcode_core/query/loop.py,sha256=Hf6PL_mdQClM0jFPQw-1eO-dhoSWFTHXpBAOdK5PK60,42004
50
+ crabcode_core/schedule/__init__.py,sha256=Pf1VyLKpZuvtxkUyYmJv0m79vYfN9p542A26UTBS-0E,149
51
+ crabcode_core/schedule/models.py,sha256=T3Z7wapFCkuE_ZeIDK2Clq8WGThKs88ENjXklT1_LE8,7847
52
+ crabcode_core/schedule/store.py,sha256=3NL8xKgyDvndiuw3Zc32_rt6Bze3VmTbVd9_DM67MII,12455
53
+ crabcode_core/session/__init__.py,sha256=0E_Flt_OkcFVkxbRTDVxot3TrUZ23T5qspIJqlYnwtE,441
54
+ crabcode_core/session/export.py,sha256=ElD3yd5KbuOWGK2TSUpULqUgloDpab3X2hdUbFiZ_iY,4453
55
+ crabcode_core/session/meta_db.py,sha256=xIlafhJ39Khcvoor-qzxKdnZt91G3IIPw6fZ-Zn5KxU,13294
56
+ crabcode_core/session/storage.py,sha256=zimJH4Z4aFrQaZ9PPer1TVAOtNIASs1crBxWPcLhJ3Y,24816
57
+ crabcode_core/session/title_gen.py,sha256=TrIqrWRSScZjVGOaG3h4zl7PhxPMD-86zLIYcVdElP4,1507
58
+ crabcode_core/skills/__init__.py,sha256=2A99hK6_T53EPy-hcYYu4FobhUAZjLL-O7ObIyG3yuQ,296
59
+ crabcode_core/skills/loader.py,sha256=VbT3C84xQ3AOf96MTZmyk3J71Z06M2sGajaQKciBXME,7142
60
+ crabcode_core/skills/matcher.py,sha256=gPt-nYz9YmUK_Pb3KjivrYl6izDSBiYo3HlrDhxcggs,7066
61
+ crabcode_core/snapshot/__init__.py,sha256=ACgq5C5_ChCXTW8UJWzhWz8vAJLw5MKtNyt9F1fYKQk,418
62
+ crabcode_core/snapshot/snapshot.py,sha256=RPcywyzxUYlg1fFSVxUAvpJnLLlN0rXMN6rk-NaqwJQ,17101
63
+ crabcode_core/snapshot/tracker.py,sha256=7ykjcb32vAPqAczvMmCxyRjP4lxg74ctl7s_ItRE3WI,8392
64
+ crabcode_core/team/__init__.py,sha256=llNqc6kSc5ng3WI1_-ABFe9RXc189A3MMXb1awKB4Ko,502
65
+ crabcode_core/team/bridge.py,sha256=RhKzk_KFBi7xKN1sqjhSJZ9ADXp9A458aoxgoAPl67c,2081
66
+ crabcode_core/team/inbox.py,sha256=Dkli61VNJ-PiedvqoTlQNSmKOq5GgCfpYVp5KwbZCFc,3629
67
+ crabcode_core/team/manager.py,sha256=tjOcTQDP-LcHjEyRiTquis92dXdPwSrImp0Oqcuh4Mk,20131
68
+ crabcode_core/team/message_bus.py,sha256=0Ztj9ikGIzItUuPekSOMS_5YJ7l79CG4T4HK2IPeslQ,13377
69
+ crabcode_core/team/models.py,sha256=P98f7q4oDMH6QU7Ev8DhpYkYmCknG50u_c5VPq3eBXE,5568
70
+ crabcode_core/team/recovery.py,sha256=Lwdcas-zUxqVv9y1EuM4X-4Ixbr7wkugm1dyjMe3nuk,4913
71
+ crabcode_core/tools/__init__.py,sha256=G_KC87cvsj5y1ncHgN5dCM-3glXbAC2fxM-cvMpCDiM,2336
72
+ crabcode_core/tools/_input_helpers.py,sha256=d7sketqhc2kWodNeuUVfiD40-GnKArsrmxGxfjtXEAE,1298
73
+ crabcode_core/tools/agent.py,sha256=35aLPdgDIqv-jNba2_SV6RIGrOPhxS4fNMi8kRmqt-g,14000
74
+ crabcode_core/tools/ask_user.py,sha256=6CjWaX6f-3RWPyBSSWq4r5m63vRT_wu5_dflAbvE2fY,4466
75
+ crabcode_core/tools/bash.py,sha256=nUztFdJ5a9Zt1megDo0Geda8Oa54mPjMtd4ReDzqbvc,5061
76
+ crabcode_core/tools/browser.py,sha256=3x8sRdZZetlO5uRpbWchmryLQ0EiUtTvScFPoR2GZnU,28267
77
+ crabcode_core/tools/checklist.py,sha256=fZCHi1ft_Yo1bXMBcG-wMdZOFdPQZGOdeHdwYboBDkE,9265
78
+ crabcode_core/tools/checkpoint.py,sha256=l0BJeNOJ9Q9P6AUnqqiQmYb9wG0CzFK2XD3RKeuEpuw,2554
79
+ crabcode_core/tools/diff_utils.py,sha256=IYuEe53zyvb1FAEIU0vcsEPQGCz1TZCyekMhJC5r0AE,3480
80
+ crabcode_core/tools/file_edit.py,sha256=SX9r0e7tBbAH-7xWMERaEqFQI9Fjb7L9AwfJiD8rtPU,7471
81
+ crabcode_core/tools/file_read.py,sha256=62sjx1wHNaaOF3XOAKubJ6jhT5V5XWUEnnLwhYqOsDw,5085
82
+ crabcode_core/tools/file_write.py,sha256=6iKg6kyrIcLGeyC2iwN8Z9WE1CDi7L90yFYU1O_GwzU,4829
83
+ crabcode_core/tools/glob.py,sha256=cB9DKE1r0dZqeojzxkfix4am4eI2bCqyDmgK5xc_wdY,3876
84
+ crabcode_core/tools/grep.py,sha256=ZR_9c-LehzHSV8CnVljG3u4-LoLglFhMDeTg4ey4Wk4,5299
85
+ crabcode_core/tools/lint.py,sha256=Kxu5jPq6rbf1Wfw7yXy6lQc_GpT0M4ADK87NEaW1buc,12692
86
+ crabcode_core/tools/memory.py,sha256=YuclnzEGUMgLrizmaJwg-sjMlY6Y6EYlKFhFe_kqgKo,8838
87
+ crabcode_core/tools/revert.py,sha256=uodlTPg9qyJbvwkW9zLuV35zeNc6Z4l5TJwnO03C0fY,3366
88
+ crabcode_core/tools/schedule.py,sha256=tfqCBRXM8kPzR7Mxyogly6Ui8kueqzGxaLalgyVRn3s,21954
89
+ crabcode_core/tools/skill.py,sha256=QdxSh4lRjBOR1J9ggU3_1sZtyETeYxXui9eBj-VFNk4,4763
90
+ crabcode_core/tools/switch_mode.py,sha256=qS9oo8CZhw99gO2g7wchOhezavGersprbOPHkCIO1e0,7328
91
+ crabcode_core/tools/team.py,sha256=Y4kyT5zFvd1qH1Y8tr-VW393ZyLKUfwryncBwvyLc_8,12503
92
+ crabcode_core/tools/web_search.py,sha256=SmQLjPmvLCNeAM0i1nT4NtsNGrlJXqxoipvcQoJujX4,12009
93
+ crabcode_core/types/__init__.py,sha256=gghuU5D2Zgy9AKWge55hqOTphxj2K-Q4oeQPUuaFvCU,1664
94
+ crabcode_core/types/config.py,sha256=C7NJmBpjjShYGX9Fa15xQspaBi6zW8COi4bG596xe7A,6449
95
+ crabcode_core/types/event.py,sha256=XHzl9CGj-Tn0ApkDK1C2IxWY2l9-I0YGX7Sezkcx2UU,5223
96
+ crabcode_core/types/message.py,sha256=w_pO6lZa8xVDVkWyjSF9wZ8y3avQu2V7L772VqqRdrU,5144
97
+ crabcode_core/types/tool.py,sha256=DIzWBH_aTOuEc3KLgniyEMbKluhR86OrTet2-ixcJFw,5197
98
+ crabcode_gateway/__init__.py,sha256=eURgiXIn5nv-c9MRBcgFaQgpXowvL7_Mq1AJ8a1Trf8,84
99
+ crabcode_gateway/adapter.py,sha256=jlFzke-V4LfgA20QDQuOMSVWmrrt9QYM1n8Mee_d3lQ,842
100
+ crabcode_gateway/event_bus.py,sha256=g9EpAk3HkK0MGUCq0YhYT7mJUVqmGJKveCUgBbSTvfY,6300
101
+ crabcode_gateway/middleware.py,sha256=yTjdZPJTTBbncLYU0CLbAx1NAAWqls5maZqsrlb9DfU,7782
102
+ crabcode_gateway/schemas.py,sha256=PK5PXofcCGjzzwiPVSqHnuholdpIJHskfOh3sjyf7iE,15215
103
+ crabcode_gateway/server.py,sha256=X6KZQ1OgILbBhZBxms0KIe_NPdt0wBAK1ZrTIdcwH2Y,5696
104
+ crabcode_gateway/acp/__init__.py,sha256=PJUCdGBi_ud--LoWxbYbzO7gAgdFCzY_UKeB3JhUvUA,352
105
+ crabcode_gateway/acp/agent.py,sha256=D2v7fhHHZzmRek_Gze2YZ3MUALBAZ0SSxtNsL4loM2I,24750
106
+ crabcode_gateway/acp/session.py,sha256=rbN1EcCQw2-LEr-bbW3x0TVmeL5ZGeEaSfBX6LEmjws,4049
107
+ crabcode_gateway/acp/transport.py,sha256=IWU3ih_9heY-EH-AhbprAtpcaj8ZvtWMMS76CAX82QA,1119
108
+ crabcode_gateway/acp/types.py,sha256=_hGt2o7CnhfgC_lvOTtdc_Z1hk6y3WE3RCdJ0Qz0hws,2424
109
+ crabcode_gateway/grpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
+ crabcode_gateway/grpc/server.py,sha256=JWTK0d4EjSWVPEYALHxFfO69e6B9oueDHnLUMUWwq0U,16576
111
+ crabcode_gateway/grpc/proto/crabcode.proto,sha256=J4eLfFowt5yqiDDajQF1r3_YAiAmxU-hZ-cDO2UEciQ,3435
112
+ crabcode_gateway/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
113
+ crabcode_gateway/routes/agent.py,sha256=G2AbNZUKlaupfZ_rSBmFNAdB476oQUoNrnGgGrNRlhk,4970
114
+ crabcode_gateway/routes/config.py,sha256=rVJtK0ZZD-4LMh3zWtyqqePmOBxLZZIiD5d5ItRxzLM,5985
115
+ crabcode_gateway/routes/event.py,sha256=Kc3GbCiHdRn9WdZJ6DQHlEhVpf2ro0hNI7F-pJCNwYo,13341
116
+ crabcode_gateway/routes/health.py,sha256=GzR1RAGNmOPXEyUQNRE0ANJsbDMOQHQGNRhA3dRz87Y,293
117
+ crabcode_gateway/routes/permission.py,sha256=kC114yHMC-C6FzhQ6wE6TM8R5rOTg-1ZQgviIXPaHSg,1790
118
+ crabcode_gateway/routes/session.py,sha256=mqBpdcxfPOLNINsFNSx9zHwIJxxeaZH3MILw8vjIzuU,12473
119
+ crabcode_gateway/routes/snapshot.py,sha256=Ax1782J76ES1Vh9Y4yGiHNIqcu8Be-LPKggfGd7bjU8,3403
120
+ crabcode_search/__init__.py,sha256=Fw3-cySOi83Y6WSduXUTC62ANXA5lf7PX3kSnQoXEUs,272
121
+ crabcode_search/background.py,sha256=urkJ6RV-aQvCRlaQr2n08S7NiY2sCDEjC-vOZw4vRl0,11098
122
+ crabcode_search/chunker.py,sha256=gtpMWAjNBEAmr9cfhU5uOcuXZFDhEpNRubVbttaxT14,16148
123
+ crabcode_search/embedder.py,sha256=NlNuOHS7t4JvaWXkNU-T_xxnkjRxFrr3l57dlB5IRkc,16728
124
+ crabcode_search/indexer.py,sha256=wNHrzB9oW8eRcKek6ZtXJdRB_HgFLj_1LhqqtG_tiDw,7522
125
+ crabcode_search/store.py,sha256=7E7fkQ25CgX0LlIp6wrgQNkVS9epUaqLpfoDA_V-BWg,8002
126
+ crabcode_search/tool.py,sha256=-hOW6n3gWDVq-kVKVs3ATOIOaeQtihCFi2VXNb9t00Q,9422
127
+ crabcode-0.1.3.dist-info/METADATA,sha256=v0EnVaE4fvh2C1NIGZ2N2azPba-MXkX3sju_lJw5PsE,2107
128
+ crabcode-0.1.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
129
+ crabcode-0.1.3.dist-info/entry_points.txt,sha256=tpYKME-yDLFb2eqwryWJG-d-HW4dkvDbzwogRl2jays,52
130
+ crabcode-0.1.3.dist-info/RECORD,,
crabcode_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """CrabCode CLI - terminal frontend."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -0,0 +1,4 @@
1
+ from crabcode_cli.app import entry
2
+
3
+ if __name__ == "__main__":
4
+ entry()
crabcode_cli/app.py CHANGED
@@ -8,6 +8,7 @@ import sys
8
8
  from typing import Optional
9
9
 
10
10
  import typer
11
+ from crabcode_core.logging_utils import configure_logging, get_logger
11
12
 
12
13
  app = typer.Typer(
13
14
  name="crabcode",
@@ -15,19 +16,22 @@ app = typer.Typer(
15
16
  add_completion=False,
16
17
  )
17
18
 
19
+ logger = get_logger(__name__)
20
+
18
21
 
19
22
  @app.command()
20
23
  def main(
21
24
  prompt: Optional[str] = typer.Argument(None, help="Prompt to send (pipe mode)"),
22
25
  pipe: bool = typer.Option(False, "-p", "--pipe", help="Run in pipe mode (non-interactive)"),
23
26
  model: Optional[str] = typer.Option(None, "-m", "--model", help="Model to use"),
24
- provider: Optional[str] = typer.Option(None, "--provider", help="API provider (anthropic/openai/codex/router)"),
27
+ provider: Optional[str] = typer.Option(None, "--provider", help="API provider (anthropic/openai/codex/ollama/gemini/azure/bedrock/vertex/router)"),
25
28
  base_url: Optional[str] = typer.Option(None, "--base-url", help="API base URL (for routers)"),
26
29
  api_format: Optional[str] = typer.Option(None, "--api-format", help="API format for router (anthropic/openai)"),
27
30
  model_profile: Optional[str] = typer.Option(None, "--model-profile", "-M", help="Use a named model from settings.models"),
28
31
  cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
29
32
  resume: Optional[str] = typer.Option(None, "-r", "--resume", help="Resume a session by ID"),
30
33
  continue_last: bool = typer.Option(False, "-c", "--continue", help="Continue most recent session"),
34
+ image: Optional[list[str]] = typer.Option(None, "-i", "--image", help="Image file(s) to attach (repeatable)"),
31
35
  ) -> None:
32
36
  """CrabCode — AI coding assistant in the terminal."""
33
37
  from crabcode_core.types.config import ApiConfig, CrabCodeSettings
@@ -48,12 +52,20 @@ def main(
48
52
  settings.api.base_url = file_settings.api.base_url
49
53
  if file_settings.api.api_key_env:
50
54
  settings.api.api_key_env = file_settings.api.api_key_env
55
+ if file_settings.api.http_headers:
56
+ settings.api.http_headers = dict(file_settings.api.http_headers)
51
57
  if file_settings.api.format:
52
58
  settings.api.format = file_settings.api.format
53
59
  if not file_settings.api.thinking_enabled:
54
60
  settings.api.thinking_enabled = file_settings.api.thinking_enabled
55
61
  if file_settings.api.max_tokens != 16384:
56
62
  settings.api.max_tokens = file_settings.api.max_tokens
63
+ if file_settings.api.azure_endpoint:
64
+ settings.api.azure_endpoint = file_settings.api.azure_endpoint
65
+ if file_settings.api.azure_api_version:
66
+ settings.api.azure_api_version = file_settings.api.azure_api_version
67
+ if file_settings.api.azure_deployment:
68
+ settings.api.azure_deployment = file_settings.api.azure_deployment
57
69
 
58
70
  if file_settings.models:
59
71
  settings.models = {**file_settings.models, **settings.models}
@@ -62,6 +74,9 @@ def main(
62
74
 
63
75
  if file_settings.permissions.run_everything:
64
76
  settings.permissions.run_everything = True
77
+ settings.logging = file_settings.logging.model_copy(deep=True)
78
+
79
+ configure_logging(work_dir, settings.logging)
65
80
 
66
81
  if model:
67
82
  settings.api.model = model
@@ -82,6 +97,30 @@ def main(
82
97
  err=True,
83
98
  )
84
99
 
100
+ # Read image files into base64 attachments
101
+ image_attachments: list[dict[str, str]] | None = None
102
+ if image:
103
+ import base64
104
+ import mimetypes
105
+ from pathlib import Path
106
+
107
+ image_attachments = []
108
+ for img_path_str in image:
109
+ img_path = Path(img_path_str)
110
+ if not img_path.exists():
111
+ typer.echo(f"Error: image file not found: {img_path_str}", err=True)
112
+ raise typer.Exit(1)
113
+ if img_path.stat().st_size > 20 * 1024 * 1024:
114
+ typer.echo(f"Error: image file too large (>20MB): {img_path_str}", err=True)
115
+ raise typer.Exit(1)
116
+ media_type = mimetypes.guess_type(img_path_str)[0] or "image/png"
117
+ if not media_type.startswith("image/"):
118
+ typer.echo(f"Error: not an image file: {img_path_str}", err=True)
119
+ raise typer.Exit(1)
120
+ with open(img_path, "rb") as f:
121
+ data = base64.b64encode(f.read()).decode("ascii")
122
+ image_attachments.append({"media_type": media_type, "data": data})
123
+
85
124
  stdin_text = ""
86
125
  if not sys.stdin.isatty():
87
126
  stdin_text = sys.stdin.read().strip()
@@ -93,12 +132,12 @@ def main(
93
132
  raise typer.Exit(1)
94
133
 
95
134
  from crabcode_cli.pipe import run_pipe
96
- asyncio.run(run_pipe(text, settings=settings, cwd=work_dir))
135
+ asyncio.run(run_pipe(text, settings=settings, cwd=work_dir, images=image_attachments))
97
136
  return
98
137
 
99
138
  if prompt:
100
139
  from crabcode_cli.pipe import run_pipe
101
- asyncio.run(run_pipe(prompt, settings=settings, cwd=work_dir))
140
+ asyncio.run(run_pipe(prompt, settings=settings, cwd=work_dir, images=image_attachments))
102
141
  return
103
142
 
104
143
  resume_id: str | None = None
@@ -125,7 +164,7 @@ def main(
125
164
  tool_config=file_settings.tool_settings.get("CodebaseSearch", {}),
126
165
  )
127
166
  except Exception:
128
- pass
167
+ logger.exception("Failed to start background indexer bootstrap")
129
168
 
130
169
  from crabcode_cli.repl import run_repl
131
170
  asyncio.run(run_repl(settings=settings, cwd=work_dir, resume_session_id=resume_id))
@@ -134,7 +173,272 @@ def main(
134
173
  os._exit(0)
135
174
 
136
175
 
176
+ sessions_app = typer.Typer(
177
+ name="sessions",
178
+ help="Manage CrabCode sessions",
179
+ )
180
+ app.add_typer(sessions_app, name="sessions")
181
+
182
+
183
+ @sessions_app.command("list")
184
+ def sessions_list(
185
+ all_projects: bool = typer.Option(False, "--all", "-a", help="List sessions across all projects"),
186
+ cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
187
+ limit: int = typer.Option(20, "--limit", "-n", help="Max number of sessions to show"),
188
+ ) -> None:
189
+ """List recent sessions."""
190
+ work_dir = cwd or os.getcwd()
191
+ if all_projects:
192
+ from crabcode_core.session.meta_db import SessionMetaStore
193
+ store = SessionMetaStore()
194
+ rows = store.list_recent(limit=limit)
195
+ store.close()
196
+ else:
197
+ from crabcode_core.session.storage import SessionStorage
198
+ rows = SessionStorage.list_sessions(work_dir)[:limit]
199
+ rows = [
200
+ {"id": r["session_id"], "cwd": work_dir, **{k: v for k, v in r.items() if k != "session_id"}}
201
+ for r in rows
202
+ ]
203
+
204
+ if not rows:
205
+ typer.echo("No sessions found.")
206
+ return
207
+
208
+ for i, r in enumerate(rows, 1):
209
+ sid = r.get("id", "")[:8]
210
+ cwd_col = r.get("cwd", "")
211
+ if len(cwd_col) > 30:
212
+ cwd_col = "…" + cwd_col[-29:]
213
+ model = r.get("model", "") or ""
214
+ tokens = r.get("tokens_used", 0)
215
+ tokens_str = f"{tokens // 1000}k" if tokens >= 1000 else str(tokens)
216
+ preview = r.get("title", "") or r.get("first_user_message", "") or r.get("preview", "")
217
+ project_part = f" [{cwd_col}]" if all_projects else ""
218
+ typer.echo(f" {i:>3}. {sid} {model[:16]:<16} {tokens_str:>6} tok{project_part} {preview[:50]}")
219
+
220
+
221
+ @sessions_app.command("search")
222
+ def sessions_search(
223
+ query: str = typer.Argument(..., help="Search query"),
224
+ limit: int = typer.Option(20, "--limit", "-n", help="Max results"),
225
+ ) -> None:
226
+ """Search sessions by title or first message."""
227
+ from crabcode_core.session.storage import SessionStorage
228
+ results = SessionStorage.search_sessions(query, limit=limit)
229
+ if not results:
230
+ typer.echo(f"No sessions matching \"{query}\".")
231
+ return
232
+ for i, r in enumerate(results, 1):
233
+ sid = r.get("id", "")[:8]
234
+ cwd_col = r.get("cwd", "")
235
+ if len(cwd_col) > 30:
236
+ cwd_col = "…" + cwd_col[-29:]
237
+ model = r.get("model", "") or ""
238
+ tokens = r.get("tokens_used", 0)
239
+ tokens_str = f"{tokens // 1000}k" if tokens >= 1000 else str(tokens)
240
+ preview = r.get("title", "") or r.get("first_user_message", "")
241
+ typer.echo(f" {i:>3}. {sid} {model[:16]:<16} {tokens_str:>6} tok [{cwd_col}] {preview[:50]}")
242
+
243
+
244
+ @sessions_app.command("export")
245
+ def sessions_export(
246
+ session_id: str = typer.Argument(..., help="Session ID (full or prefix)"),
247
+ fmt: str = typer.Option("md", "--format", "-f", help="Export format: md or json"),
248
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path"),
249
+ cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
250
+ ) -> None:
251
+ """Export a session transcript to Markdown or JSON."""
252
+ work_dir = cwd or os.getcwd()
253
+ from crabcode_core.session.export import export_json, export_markdown
254
+ if fmt == "json":
255
+ content = export_json(session_id, work_dir)
256
+ ext = ".json"
257
+ else:
258
+ content = export_markdown(session_id, work_dir)
259
+ ext = ".md"
260
+ out_path = output or os.path.join(work_dir, f"{session_id[:8]}{ext}")
261
+ with open(out_path, "w", encoding="utf-8") as f:
262
+ f.write(content)
263
+ typer.echo(f"Exported to {out_path}")
264
+
265
+
266
+ @sessions_app.command("prune")
267
+ def sessions_prune(
268
+ days: int = typer.Option(30, "--days", "-d", help="Archive sessions older than N days"),
269
+ delete_files: bool = typer.Option(False, "--delete-files", help="Also delete JSONL transcript files"),
270
+ ) -> None:
271
+ """Archive old sessions and optionally delete their files."""
272
+ from crabcode_core.session.meta_db import SessionMetaStore
273
+ store = SessionMetaStore()
274
+ archived = store.auto_archive(days=days)
275
+ typer.echo(f"Archived {archived} session(s) older than {days} days.")
276
+ if delete_files:
277
+ purged = store.purge_archived()
278
+ for entry in purged:
279
+ sid = entry["id"]
280
+ cwd = entry.get("cwd", "")
281
+ if cwd:
282
+ from crabcode_core.session.storage import get_transcript_path
283
+ path = get_transcript_path(cwd, sid)
284
+ try:
285
+ if path.exists():
286
+ path.unlink()
287
+ except OSError:
288
+ pass
289
+ typer.echo(f"Purged {len(purged)} archived session(s) from database and disk.")
290
+ else:
291
+ purged = store.purge_archived()
292
+ typer.echo(f"Purged {len(purged)} archived session(s) from database.")
293
+ store.close()
294
+
295
+
296
+ @app.command("gateway")
297
+ def gateway(
298
+ port: int = typer.Option(4096, "--port", "-p", help="HTTP port"),
299
+ grpc_port: Optional[int] = typer.Option(None, "--grpc-port", help="gRPC port (optional)"),
300
+ host: str = typer.Option("127.0.0.1", "--host", help="Bind address"),
301
+ password: Optional[str] = typer.Option(None, "--password", help="Basic Auth password"),
302
+ cors: Optional[str] = typer.Option(None, "--cors", help="Allowed CORS origin"),
303
+ log_level: str = typer.Option("info", "--log-level", help="Log level"),
304
+ ) -> None:
305
+ """Start the CrabCode HTTP/gRPC gateway server."""
306
+ from crabcode_core.logging_utils import configure_logging
307
+ from crabcode_core.types.config import CrabCodeSettings, LoggingSettings
308
+
309
+ log_settings = LoggingSettings(level=log_level.upper())
310
+ configure_logging(os.getcwd(), log_settings)
311
+
312
+ cors_origins = [cors] if cors else None
313
+
314
+ from crabcode_gateway.server import run_server
315
+
316
+ run_server(
317
+ host=host,
318
+ port=port,
319
+ grpc_port=grpc_port,
320
+ password=password,
321
+ cors_origins=cors_origins,
322
+ log_level=log_level,
323
+ )
324
+
325
+
326
+ @app.command("acp")
327
+ def acp_cmd(
328
+ port: int = typer.Option(4096, "--port", "-p", help="Internal HTTP server port"),
329
+ host: str = typer.Option("127.0.0.1", "--host", help="Internal HTTP server host"),
330
+ cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
331
+ log_level: str = typer.Option("warning", "--log-level", help="Log level"),
332
+ ) -> None:
333
+ """Start ACP (Agent Client Protocol) server on stdio.
334
+
335
+ This command starts an internal Gateway HTTP server and then
336
+ runs the ACP agent, communicating with the editor over
337
+ stdin/stdout via JSON-RPC (ndjson).
338
+
339
+ Used by ACP-compatible editors (Zed, JetBrains, Neovim) to
340
+ integrate CrabCode as a coding agent backend.
341
+
342
+ Example Zed settings.json::
343
+
344
+ {
345
+ "agent": {
346
+ "profiles": {
347
+ "crabcode": {
348
+ "command": "crabcode",
349
+ "args": ["acp"]
350
+ }
351
+ }
352
+ }
353
+ }
354
+ """
355
+ import asyncio
356
+
357
+ from crabcode_core.logging_utils import configure_logging
358
+ from crabcode_core.types.config import CrabCodeSettings, LoggingSettings
359
+
360
+ work_dir = cwd or os.getcwd()
361
+ log_settings = LoggingSettings(level=log_level.upper())
362
+ configure_logging(work_dir, log_settings)
363
+
364
+ async def _run() -> None:
365
+ # 1. Start internal Gateway HTTP server
366
+ from crabcode_gateway.server import GatewayServer
367
+
368
+ server = GatewayServer(host=host, port=port, log_level=log_level)
369
+ await server.start_background()
370
+
371
+ # Wait for server to be ready
372
+ import httpx
373
+ async with httpx.AsyncClient() as health_client:
374
+ for _ in range(20):
375
+ try:
376
+ resp = await health_client.get(f"http://{host}:{port}/health")
377
+ if resp.status_code == 200:
378
+ break
379
+ except Exception:
380
+ pass
381
+ await asyncio.sleep(0.25)
382
+
383
+ # 2. Run ACP agent on stdio
384
+ from crabcode_gateway.acp.transport import run_acp_server
385
+ from crabcode_gateway.acp.types import ACPConfig
386
+
387
+ config = ACPConfig(base_url=f"http://{host}:{port}")
388
+ try:
389
+ await run_acp_server(config)
390
+ finally:
391
+ await server.stop()
392
+
393
+ asyncio.run(_run())
394
+ os._exit(0)
395
+
396
+
397
+ @app.command("stats")
398
+ def stats(
399
+ project: bool = typer.Option(False, "--project", "-p", help="Show only current project stats"),
400
+ cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
401
+ ) -> None:
402
+ """Show usage statistics."""
403
+ work_dir = os.path.abspath(cwd or os.getcwd())
404
+ from crabcode_core.session.meta_db import SessionMetaStore
405
+ store = SessionMetaStore()
406
+
407
+ def _fmt_tok(n: int) -> str:
408
+ if n >= 1_000_000:
409
+ return f"{n / 1_000_000:.1f}M"
410
+ if n >= 1_000:
411
+ return f"{n / 1_000:.1f}k"
412
+ return str(n)
413
+
414
+ if project:
415
+ p = store.stats_by_project(work_dir)
416
+ typer.echo(f"Project: {work_dir}")
417
+ typer.echo(f" Sessions: {p['total_sessions']} | Tokens: {_fmt_tok(p['total_tokens'])} | Messages: {p['total_messages']}")
418
+ else:
419
+ g = store.stats_global()
420
+ p = store.stats_by_project(work_dir)
421
+ models = store.stats_by_model(limit=5)
422
+ typer.echo(f"Global: {g['total_sessions']} sessions | {_fmt_tok(g['total_tokens'])} tokens | {g['active_projects']} projects")
423
+ typer.echo(f"This week: {g['week_sessions']} sessions | {_fmt_tok(g['week_tokens'])} tokens")
424
+ typer.echo(f"This project: {p['total_sessions']} sessions | {_fmt_tok(p['total_tokens'])} tokens | {p['total_messages']} messages")
425
+ if models:
426
+ model_parts = [f"{m['model']} ({_fmt_tok(m['tokens'])})" for m in models]
427
+ typer.echo(f"Top models: {', '.join(model_parts)}")
428
+ store.close()
429
+
430
+
137
431
  def entry() -> None:
432
+ known_subcommands = {"main", "sessions", "stats", "gateway", "acp"}
433
+ args = sys.argv[1:]
434
+ # Preserve root --help so users can still discover all subcommands.
435
+ if args and args[0] in ("--help", "-h"):
436
+ app()
437
+ return
438
+ # If the first positional argument is not a known subcommand, default to main.
439
+ first_positional = next((a for a in args if not a.startswith("-")), None)
440
+ if first_positional not in known_subcommands:
441
+ sys.argv.insert(1, "main")
138
442
  app()
139
443
 
140
444