hyperagents 1.2.3rc5.post3__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 (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,161 @@
1
+ # HyperClaw Model Registry
2
+ # Thinking/effort per model come from hyperclaw/anthropic_params.py (capability
3
+ # table). Override per model here, e.g.
4
+ # claude-sonnet-5-5: {thinking: adaptive, effort: high}
5
+ # claude-haiku-4-5: {thinking: enabled, thinking_budget: 4000}
6
+ # thinking: adaptive | enabled | none; effort: low | medium | high | xhigh | max.
7
+ # A mode the model rejects (enabled on the 5.x generation) is never sent.
8
+ models:
9
+ claude-fable-5:
10
+ provider: anthropic
11
+ cost_per_1k_input: 0.010
12
+ cost_per_1k_output: 0.050
13
+ latency_ms: 6000
14
+ max_tokens: 16000
15
+ suitable_tasks: [research, analysis, synthesis, planning, code, health, finance, scientific]
16
+ note: "Top tier — hardest multi-step work. Thinking always on."
17
+
18
+ claude-sonnet-5:
19
+ provider: anthropic
20
+ cost_per_1k_input: 0.002
21
+ cost_per_1k_output: 0.010
22
+ latency_ms: 2000
23
+ max_tokens: 16000
24
+ suitable_tasks: [research, analysis, synthesis, planning, code, health, finance, scientific]
25
+
26
+ claude-opus-5:
27
+ provider: anthropic
28
+ cost_per_1k_input: 0.005
29
+ cost_per_1k_output: 0.025
30
+ latency_ms: 4000
31
+ max_tokens: 16000
32
+ suitable_tasks: [research, analysis, synthesis, planning, code, health, finance, scientific]
33
+
34
+ claude-haiku-4-5:
35
+ provider: anthropic
36
+ cost_per_1k_input: 0.001
37
+ cost_per_1k_output: 0.005
38
+ latency_ms: 500
39
+ max_tokens: 4096
40
+ suitable_tasks: [routing, classification, quick_lookup, summarization, draft, triage, status_check]
41
+
42
+ claude-code:
43
+ provider: anthropic
44
+ cost_per_1k_input: 0.003
45
+ cost_per_1k_output: 0.015
46
+ latency_ms: 3000
47
+ max_tokens: 8192
48
+ suitable_tasks: [code]
49
+
50
+ chatjimmy:
51
+ provider: chatjimmy
52
+ cost_per_1k_input: 0.000001
53
+ cost_per_1k_output: 0.000001
54
+ latency_ms: 50
55
+ max_tokens: 2048
56
+ suitable_tasks: [routing, classification, quick_lookup, summarization, draft, triage, status_check]
57
+ note: "Powered by Taalas HC1 silicon — Llama 3.1 8B at 17,000 tokens/sec. Never auto-certified."
58
+
59
+ nim-local:
60
+ provider: nim
61
+ cost_per_1k_input: 0.0
62
+ cost_per_1k_output: 0.0
63
+ latency_ms: 500
64
+ max_tokens: 4096
65
+ suitable_tasks: [research, analysis, code]
66
+
67
+ zimage-turbo:
68
+ provider: replicate
69
+ type: image_generation
70
+ cost_per_image: 0.004
71
+ latency_ms: 1000
72
+ max_resolution: 1024x1024
73
+ suitable_tasks: [image_generation, illustration, diagram, visualization, creative_art, mockup, logo]
74
+ note: "Zimage Turbo via Replicate — 6B params, sub-second generation, strong text rendering"
75
+
76
+ # ── Provider routing (1.2.0; V2 default 1.2.3) ───────────────────────────
77
+ # A provider is LIVE only if every *_env below resolves to a non-empty
78
+ # value (or has a package default); slot ladders are filtered to live
79
+ # providers at startup.
80
+ #
81
+ # origin: hyper -> Hyper's own model (HyperSpeed V2), the default.
82
+ # origin: customer -> the customer's own provider; off unless they add a key,
83
+ # labelled "your own provider (<label>)". A Hyper-deployed
84
+ # install (HYPERCLAW_DEPLOYMENT=hyper) loads origin: hyper only.
85
+ #
86
+ # `capabilities` DECLARES what a provider can do. tool_use is GRANTED only
87
+ # when a qualification record says that provider+model is QUALIFIED
88
+ # (hyperclaw/qualification.py); otherwise the provider is chat-only.
89
+ providers:
90
+ hyperspeed:
91
+ origin: hyper
92
+ label: HyperSpeed V2
93
+ kind: openai_compat
94
+ # Base URL and model come from hyperclaw/hyperspeed.py; the customer only
95
+ # supplies a Hyper access key. HYPERSPEED_BASE_URL / HYPERSPEED_MODEL override.
96
+ package_defaults: hyperspeed_v2
97
+ base_url_env: HYPERSPEED_BASE_URL
98
+ api_key_env: HYPERSPEED_API_KEY
99
+ model_env: HYPERSPEED_MODEL
100
+ # V2 limits as documented by the serving team (13 Oct; not yet confirmed
101
+ # against a live GET /v1/models): max-model-len 65536, max output 16384,
102
+ # plus up to 8192 reasoning tokens when thinking is on for the call. Every
103
+ # call sets its own max output, never above max_output_tokens
104
+ # (hyperclaw/cost_guard.py max_output); the cost guard reserves the max
105
+ # output plus max_reasoning_tokens for a call with thinking on.
106
+ max_output_tokens: 16384
107
+ max_reasoning_tokens: 8192
108
+ # tool_use is declared but NOT granted until a qualification record says
109
+ # QUALIFIED for the served model; until then V2 is chat-only.
110
+ capabilities: [chat, streaming, tool_use]
111
+ anthropic:
112
+ origin: customer
113
+ label: Anthropic
114
+ kind: anthropic # native Messages API
115
+ api_key_env: ANTHROPIC_API_KEY
116
+ capabilities: [chat, streaming, tool_use, images, documents, thinking]
117
+ models: {default: claude-sonnet-4-6, fast: claude-haiku-4-5-20251001, strong: claude-opus-4-8,
118
+ standard: claude-sonnet-5, premium: claude-opus-5, flagship: claude-fable-5}
119
+ chatjimmy:
120
+ origin: customer
121
+ label: ChatJimmy
122
+ kind: openai_compat
123
+ base_url_env: CHATJIMMY_BASE_URL # official Taalas API URL (by application); NOT the chatjimmy.ai demo
124
+ api_key_env: CHATJIMMY_API_KEY
125
+ model_env: CHATJIMMY_MODEL
126
+ capabilities: [chat, streaming, fast]
127
+ openai:
128
+ origin: customer
129
+ label: OpenAI
130
+ kind: openai
131
+ api_key_env: OPENAI_API_KEY
132
+ capabilities: [chat, streaming, tool_use, images, embeddings, image_generation]
133
+ models: {default: gpt-5.2, fast: gpt-5.2-mini}
134
+ xai:
135
+ origin: customer
136
+ label: xAI
137
+ kind: openai_compat # OpenAI-compatible chat completions, Bearer XAI_API_KEY
138
+ base_url_env: XAI_BASE_URL
139
+ base_url_default: https://api.x.ai/v1
140
+ api_key_env: XAI_API_KEY
141
+ model_env: XAI_MODEL
142
+ # tool_use is declared but NOT granted: no signed qualification record
143
+ # ships for any Grok model, so Grok is chat-only (hyperclaw/xai.py).
144
+ capabilities: [chat, streaming, tool_use]
145
+ models: {default: grok-4.7, fast: grok-4.20-0309-non-reasoning}
146
+ openai_default:
147
+ origin: customer
148
+ label: OpenAI-compatible
149
+ kind: openai_compat
150
+ api_key_env: OPENAI_API_KEY
151
+ base_url_env: OPENAI_BASE_URL
152
+ model_env: OPENAI_MODEL
153
+ capabilities: [chat, streaming]
154
+
155
+ slots:
156
+ primary: [hyperspeed, openai_default, anthropic, openai, xai]
157
+ tools: [anthropic, openai, hyperspeed]
158
+ vision: [anthropic, hyperspeed]
159
+ fast: [chatjimmy, anthropic:fast, hyperspeed]
160
+ embeddings: [openai, hyperspeed]
161
+ images: [openai]
@@ -0,0 +1,39 @@
1
+ """Install mode.
2
+
3
+ - ``standard``: the customer's own install. HyperSpeed V2 is the default;
4
+ the customer may add their own providers.
5
+ - ``hyper``: a Hyper-deployed install (client and team installs). It runs only the models
6
+ on its allowed list, which Hyper sets in the managed config
7
+ (hyperclaw.managed_config; default: HyperSpeed V2 only). Providers cannot
8
+ be added through the app; keys for providers not on the list are ignored
9
+ (spec §11, 2026-10-03; model policy 2026-10-12).
10
+
11
+ Set with HYPERCLAW_DEPLOYMENT=hyper (wins) or ``"deployment": "hyper"`` in
12
+ config.json.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+
18
+ HYPER = "hyper"
19
+ STANDARD = "standard"
20
+
21
+ REFUSAL = ("This is a Hyper-deployed install: Hyper sets the models it can use, "
22
+ "and providers cannot be added here.")
23
+
24
+
25
+ def mode() -> str:
26
+ env = os.environ.get("HYPERCLAW_DEPLOYMENT", "").strip().lower()
27
+ if env:
28
+ return HYPER if env == HYPER else STANDARD
29
+ try:
30
+ from hyperclaw.userconfig import load_config
31
+ if str(load_config().get("deployment", "")).strip().lower() == HYPER:
32
+ return HYPER
33
+ except Exception:
34
+ pass
35
+ return STANDARD
36
+
37
+
38
+ def is_hyper() -> bool:
39
+ return mode() == HYPER
@@ -0,0 +1,9 @@
1
+ """Connected daily digest (docs/CONNECTED-DIGEST.md).
2
+
3
+ Code reads the owner's own Slack mentions/DMs and GitLab MRs, to-dos and
4
+ activity with the owner's own read-only tokens; HyperSpeed V2 only writes the
5
+ summary. The model gets no tools and never sees a token.
6
+ """
7
+ from hyperclaw.digest.errors import DigestError, Reason
8
+
9
+ __all__ = ["DigestError", "Reason"]
@@ -0,0 +1,117 @@
1
+ """`hyperclaw digest ...` commands (local terminal = the owner).
2
+
3
+ Tokens are read with a masked prompt and never printed.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+
9
+ import typer
10
+
11
+ digest_app = typer.Typer(help="Connected daily digest: your Slack and GitLab, summarized by "
12
+ "HyperSpeed V2 (read-only)")
13
+
14
+ SOURCES = ("slack", "gitlab")
15
+ PROMPTS = {
16
+ "slack": "Slack user token (xoxp-..., read scopes only; input hidden)",
17
+ "gitlab": "GitLab personal access token (read_api; input hidden)",
18
+ }
19
+
20
+
21
+ def _env() -> None:
22
+ from hyperclaw.userconfig import load_env_into_process
23
+ load_env_into_process()
24
+
25
+
26
+ def _source(value: str, allow_all: bool = False) -> str:
27
+ v = (value or "").strip().lower()
28
+ ok = SOURCES + (("all",) if allow_all else ())
29
+ if v not in ok:
30
+ typer.echo(f"Choose one of: {', '.join(ok)}.")
31
+ raise typer.Exit(2)
32
+ return v
33
+
34
+
35
+ @digest_app.command("connect")
36
+ def connect_cmd(source: str = typer.Argument(..., help="slack | gitlab"),
37
+ gitlab_url: str = typer.Option("", "--gitlab-url",
38
+ help="Self-managed GitLab URL (default gitlab.com)")) -> None:
39
+ """Connect your own Slack or GitLab account with a read-only token."""
40
+ from hyperclaw.digest.errors import DigestError
41
+ from hyperclaw.digest.service import connect
42
+ src = _source(source)
43
+ token = typer.prompt(PROMPTS[src], hide_input=True).strip()
44
+ try:
45
+ info = connect(src, token, gitlab_url=gitlab_url)
46
+ except DigestError as e:
47
+ typer.echo(f"Not connected: {e.text}. Nothing was saved.")
48
+ raise typer.Exit(1)
49
+ finally:
50
+ token = ""
51
+ who = info.get("account", "")
52
+ where = info.get("workspace") or info.get("url") or ""
53
+ typer.echo(f"Connected {src} as {who}" + (f" ({where})" if where else "") +
54
+ f". Read-only scopes: {', '.join(info.get('scopes') or [])}. "
55
+ f"Token stored in the {info['store']}.")
56
+
57
+
58
+ @digest_app.command("disconnect")
59
+ def disconnect_cmd(source: str = typer.Argument(..., help="slack | gitlab | all")) -> None:
60
+ """Remove a stored token."""
61
+ from hyperclaw.digest.service import disconnect
62
+ src = _source(source, allow_all=True)
63
+ for s in (SOURCES if src == "all" else (src,)):
64
+ typer.echo(f"{s}: " + ("disconnected." if disconnect(s) else "was not connected."))
65
+
66
+
67
+ @digest_app.command("now")
68
+ def now_cmd() -> None:
69
+ """Build the digest once now and show it (also saved as the latest)."""
70
+ _env()
71
+ from hyperclaw.digest.service import run_digest
72
+ out = run_digest()
73
+ typer.echo(out.text)
74
+ if not out.ok:
75
+ raise typer.Exit(1)
76
+
77
+
78
+ @digest_app.command("status")
79
+ def status_cmd() -> None:
80
+ """Connections, schedule and last run. Never shows a token."""
81
+ _env()
82
+ from hyperclaw.digest.service import ready, status
83
+ st = status()
84
+ conns = st["connections"]
85
+ for s in SOURCES:
86
+ c = conns.get(s)
87
+ if c:
88
+ where = c.get("workspace") or c.get("url") or ""
89
+ typer.echo(f"{s}: connected as {c.get('account', '')}" + (f" ({where})" if where else "")
90
+ + f"; scopes {', '.join(c.get('scopes') or [])}; stored in {c.get('store', '')}"
91
+ + (f"; token expires {c['expires_at']}" if c.get("expires_at") else ""))
92
+ else:
93
+ typer.echo(f"{s}: not connected")
94
+ runnable, why = ready()
95
+ gate = ("on" if st["unattended"] else "off (needs HYPERCLAW_UNATTENDED=1)")
96
+ typer.echo(f"Schedule: job connected_digest at {st['time']} daily; unattended mode {gate}; "
97
+ f"opted in: {'yes' if st['opted_in'] else 'no (hyperclaw digest schedule on)'}"
98
+ + ("" if runnable else f"; cannot run: {why}"))
99
+ last = st["last_result"]
100
+ if st["last_run"]:
101
+ typer.echo(f"Last run: {st['last_run']} — " + ("ok" if last.get("ok") else "failed"))
102
+ typer.echo(f"Latest digest: {st['latest_path']}")
103
+ else:
104
+ typer.echo("Last run: never")
105
+
106
+
107
+ @digest_app.command("schedule")
108
+ def schedule_cmd(value: str = typer.Argument(..., help="on | off")) -> None:
109
+ """Opt the named job connected_digest in or out (runs only in unattended mode)."""
110
+ from hyperclaw.digest import state
111
+ v = value.strip().lower()
112
+ if v not in ("on", "off"):
113
+ typer.echo("Use: hyperclaw digest schedule on|off")
114
+ raise typer.Exit(2)
115
+ state.set_scheduled(v == "on")
116
+ typer.echo(f"connected_digest scheduling is {v}. It runs only when HYPERCLAW_UNATTENDED=1; "
117
+ "restart the HyperClaw server to apply.")
@@ -0,0 +1,100 @@
1
+ """Named, fixed failure reasons.
2
+
3
+ Every failure the digest can report is one of these. Notices and the
4
+ "Sources" line use the fixed text only, never exception text, so internal
5
+ error details (URLs, response bodies, tokens) cannot reach a chat.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from enum import Enum
10
+
11
+
12
+ class Reason(str, Enum):
13
+ SLACK_NOT_CONNECTED = "slack_not_connected"
14
+ SLACK_TOKEN_EXPIRED = "slack_token_expired"
15
+ SLACK_NOT_USER_TOKEN = "slack_not_user_token"
16
+ SLACK_WRITE_SCOPE = "slack_write_scope"
17
+ SLACK_MISSING_SCOPE = "slack_missing_scope"
18
+ SLACK_RATE_LIMITED = "slack_rate_limited"
19
+ SLACK_UNREACHABLE = "slack_unreachable"
20
+ GITLAB_NOT_CONNECTED = "gitlab_not_connected"
21
+ GITLAB_TOKEN_EXPIRED = "gitlab_token_expired"
22
+ GITLAB_WRITE_SCOPE = "gitlab_write_scope"
23
+ GITLAB_MISSING_SCOPE = "gitlab_missing_scope"
24
+ GITLAB_RATE_LIMITED = "gitlab_rate_limited"
25
+ GITLAB_UNREACHABLE = "gitlab_unreachable"
26
+ GITLAB_UNVERIFIED = "gitlab_unverified"
27
+ TOKEN_FORMAT = "token_format"
28
+ INSECURE_URL = "insecure_url"
29
+ TOKEN_FILE_OPEN = "token_file_open"
30
+ TOKEN_STORE = "token_store"
31
+ NO_SOURCES = "no_sources"
32
+ ALL_SOURCES_FAILED = "all_sources_failed"
33
+ V2_NOT_CONFIGURED = "v2_not_configured"
34
+ V2_THIRD_PARTY = "v2_third_party"
35
+ V2_FAILED = "v2_failed"
36
+ COST_CAP = "cost_cap"
37
+ NOT_OWNER = "not_owner"
38
+
39
+
40
+ TEXT = {
41
+ Reason.SLACK_NOT_CONNECTED: "Slack isn't connected",
42
+ Reason.SLACK_TOKEN_EXPIRED: "Slack token expired or was revoked",
43
+ Reason.SLACK_NOT_USER_TOKEN: "that is not a Slack user token (xoxp-); the digest reads only your own account",
44
+ Reason.SLACK_WRITE_SCOPE: "the Slack token has write or admin scopes; the digest accepts read-only tokens only",
45
+ Reason.SLACK_MISSING_SCOPE: "the Slack token is missing a read scope the digest needs",
46
+ Reason.SLACK_RATE_LIMITED: "Slack rate limit reached",
47
+ Reason.SLACK_UNREACHABLE: "Slack couldn't be reached",
48
+ Reason.GITLAB_NOT_CONNECTED: "GitLab isn't connected",
49
+ Reason.GITLAB_TOKEN_EXPIRED: "GitLab token expired or was revoked",
50
+ Reason.GITLAB_WRITE_SCOPE: "the GitLab token has write scopes (api, write_repository, ...); the digest accepts read_api tokens only",
51
+ Reason.GITLAB_MISSING_SCOPE: "the GitLab token doesn't have the read_api scope",
52
+ Reason.GITLAB_RATE_LIMITED: "GitLab rate limit reached",
53
+ Reason.GITLAB_UNREACHABLE: "GitLab couldn't be reached",
54
+ Reason.GITLAB_UNVERIFIED: "GitLab couldn't confirm the token's scopes (a personal access token is required)",
55
+ Reason.INSECURE_URL: "the API address must use https:// (tokens are never sent unencrypted)",
56
+ Reason.TOKEN_FORMAT: "that doesn't look like a token (unexpected characters)",
57
+ Reason.TOKEN_FILE_OPEN: "the digest token file permissions are too open (must be owner-only, 0600)",
58
+ Reason.TOKEN_STORE: "the stored token couldn't be read or saved",
59
+ Reason.NO_SOURCES: "no source is connected",
60
+ Reason.ALL_SOURCES_FAILED: "no source could be read",
61
+ Reason.V2_NOT_CONFIGURED: "HyperSpeed V2 isn't configured (the digest is written only by Hyper's own model)",
62
+ Reason.V2_THIRD_PARTY: "the digest model must be HyperSpeed V2, and this install points it at a third-party service",
63
+ Reason.V2_FAILED: "HyperSpeed V2 didn't answer",
64
+ Reason.COST_CAP: "today's model spending cap was reached",
65
+ Reason.NOT_OWNER: "the digest is available to the owner only",
66
+ }
67
+
68
+ # What the owner can do about it (appended to notices).
69
+ FIX = {
70
+ Reason.SLACK_NOT_CONNECTED: "Connect it with `hyperclaw digest connect slack`.",
71
+ Reason.SLACK_TOKEN_EXPIRED: "Reconnect with `hyperclaw digest connect slack`.",
72
+ Reason.SLACK_MISSING_SCOPE: "Add the scope to your Slack app's user token and reconnect.",
73
+ Reason.SLACK_WRITE_SCOPE: "Create a user token with read scopes only and reconnect.",
74
+ Reason.GITLAB_NOT_CONNECTED: "Connect it with `hyperclaw digest connect gitlab`.",
75
+ Reason.GITLAB_TOKEN_EXPIRED: "Create a new read_api token and run `hyperclaw digest connect gitlab`.",
76
+ Reason.GITLAB_WRITE_SCOPE: "Create a token with read_api only and reconnect.",
77
+ Reason.GITLAB_MISSING_SCOPE: "Create a token with read_api and reconnect.",
78
+ Reason.TOKEN_FILE_OPEN: "Run `chmod 600` on the digest token file, or reconnect.",
79
+ Reason.NO_SOURCES: "Connect one with `hyperclaw digest connect slack` or `... gitlab`.",
80
+ Reason.V2_NOT_CONFIGURED: "Add your Hyper access key with `hyperclaw provider hyper-key`.",
81
+ Reason.V2_FAILED: "It will try again at the next scheduled time, or run `hyperclaw digest now`.",
82
+ Reason.COST_CAP: "Raise the cap with `hyperclaw cost raise --daily <USD>` or wait for tomorrow.",
83
+ }
84
+
85
+
86
+ class DigestError(Exception):
87
+ """A named digest failure. str() is the fixed text, never a detail."""
88
+
89
+ def __init__(self, reason: Reason, detail: str = ""):
90
+ self.reason = Reason(reason)
91
+ self.detail = detail # for logs only; never sent to a chat
92
+ super().__init__(TEXT[self.reason])
93
+
94
+ @property
95
+ def text(self) -> str:
96
+ return TEXT[self.reason]
97
+
98
+ @property
99
+ def fix(self) -> str:
100
+ return FIX.get(self.reason, "")
@@ -0,0 +1,187 @@
1
+ """GitLab collector: the owner's MRs, to-dos and own activity, read-only.
2
+
3
+ Calls (GET, PRIVATE-TOKEN header): /personal_access_tokens/self, /user,
4
+ /merge_requests (assigned / reviewing / authored), /todos, /events,
5
+ /projects/:id (names only). See docs/CONNECTED-DIGEST.md §1.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import date, datetime, timedelta
10
+ from typing import Optional
11
+
12
+ from hyperclaw.digest.errors import DigestError, Reason
13
+ from hyperclaw.digest.http import ReadOnlyHttp, SourceResult, clip, parse_time
14
+ from hyperclaw.digest.redact import redact
15
+ from hyperclaw.taint import mark
16
+
17
+ DEFAULT_URL = "https://gitlab.com"
18
+ READ_SCOPES = frozenset({"read_api", "read_user", "read_repository", "read_registry"})
19
+ MAX_MRS = 30
20
+ MAX_TODOS = 30
21
+ MAX_EVENTS = 50
22
+ MAX_PROJECT_LOOKUPS = 10
23
+ MAX_PAGES = 10
24
+ TEXT_CHARS = 400
25
+
26
+
27
+ class GitLabCollector:
28
+ source = "gitlab"
29
+
30
+ def __init__(self, token: str, *, url: str = "", client=None, sleep=None):
31
+ self.url = (url or DEFAULT_URL).rstrip("/")
32
+ kw = {"client": client}
33
+ if sleep is not None:
34
+ kw["sleep"] = sleep
35
+ self.http = ReadOnlyHttp(self.url + "/api/v4", {"PRIVATE-TOKEN": token},
36
+ rate_limited=Reason.GITLAB_RATE_LIMITED,
37
+ unreachable=Reason.GITLAB_UNREACHABLE, **kw)
38
+ self.info: dict = {}
39
+ self._projects: dict = {}
40
+
41
+ def _get(self, path: str, params: Optional[dict] = None):
42
+ resp = self.http.get(path, params)
43
+ if resp.status_code == 401:
44
+ raise DigestError(Reason.GITLAB_TOKEN_EXPIRED, f"{path}: 401")
45
+ if resp.status_code == 403:
46
+ raise DigestError(Reason.GITLAB_MISSING_SCOPE, f"{path}: 403")
47
+ if resp.status_code >= 400:
48
+ raise DigestError(Reason.GITLAB_UNREACHABLE, f"{path}: HTTP {resp.status_code}")
49
+ try:
50
+ return resp.json(), resp
51
+ except ValueError:
52
+ raise DigestError(Reason.GITLAB_UNREACHABLE, f"{path}: not JSON")
53
+
54
+ def _pages(self, path: str, params: dict, cap: int) -> list:
55
+ out, page = [], 1
56
+ for _ in range(MAX_PAGES):
57
+ data, resp = self._get(path, {**params, "per_page": 100, "page": page})
58
+ if not isinstance(data, list):
59
+ break
60
+ out.extend(data)
61
+ nxt = (resp.headers.get("X-Next-Page") or "").strip()
62
+ if not nxt or len(out) >= cap:
63
+ break
64
+ page = int(nxt)
65
+ return out
66
+
67
+ def verify(self, today: Optional[date] = None) -> dict:
68
+ """Token scopes must be read-only and include read_api."""
69
+ resp = self.http.get("personal_access_tokens/self")
70
+ if resp.status_code == 401:
71
+ raise DigestError(Reason.GITLAB_TOKEN_EXPIRED, "self: 401")
72
+ if resp.status_code in (403, 404):
73
+ raise DigestError(Reason.GITLAB_UNVERIFIED, f"self: {resp.status_code}")
74
+ if resp.status_code >= 400:
75
+ raise DigestError(Reason.GITLAB_UNREACHABLE, f"self: {resp.status_code}")
76
+ try:
77
+ tok = resp.json()
78
+ except ValueError:
79
+ raise DigestError(Reason.GITLAB_UNREACHABLE, "self: not JSON")
80
+ scopes = sorted(str(s) for s in (tok.get("scopes") or []))
81
+ bad = sorted(set(scopes) - READ_SCOPES)
82
+ if bad:
83
+ raise DigestError(Reason.GITLAB_WRITE_SCOPE, ",".join(bad))
84
+ if "read_api" not in scopes:
85
+ raise DigestError(Reason.GITLAB_MISSING_SCOPE, "read_api")
86
+ if tok.get("revoked") or tok.get("active") is False:
87
+ raise DigestError(Reason.GITLAB_TOKEN_EXPIRED, "inactive")
88
+ exp = tok.get("expires_at")
89
+ if exp:
90
+ try:
91
+ if date.fromisoformat(str(exp)[:10]) < (today or date.today()):
92
+ raise DigestError(Reason.GITLAB_TOKEN_EXPIRED, "past expires_at")
93
+ except ValueError:
94
+ pass
95
+ user, _ = self._get("user")
96
+ self.info = {"user_id": user.get("id"), "account": str(user.get("username") or ""),
97
+ "name": str(user.get("name") or ""), "scopes": scopes, "url": self.url,
98
+ "expires_at": str(exp or "")}
99
+ return self.info
100
+
101
+ def collect(self, since: datetime) -> SourceResult:
102
+ info = self.info or self.verify()
103
+ res = SourceResult(self.source, account=f"{info['account']} ({self.url})")
104
+ lists = (
105
+ ("mr_assigned", {"scope": "assigned_to_me", "state": "opened"}),
106
+ ("mr_reviewing", {"scope": "all", "reviewer_username": info["account"],
107
+ "state": "opened"}),
108
+ ("mr_authored", {"scope": "created_by_me", "state": "opened"}),
109
+ )
110
+ for kind, params in lists:
111
+ mrs = self._pages("merge_requests", params, MAX_MRS)
112
+ if len(mrs) > MAX_MRS:
113
+ res.notes.append(f"{len(mrs) - MAX_MRS}+ more {kind.replace('_', ' ')} MRs not shown.")
114
+ for mr in mrs[:MAX_MRS]:
115
+ updated = parse_time(mr.get("updated_at"))
116
+ res.items.append({
117
+ "kind": kind,
118
+ "ref": ((mr.get("references") or {}).get("full") or f"!{mr.get('iid', '')}"),
119
+ "title": redact(clip(mr.get("title", ""), 200)),
120
+ "author": ((mr.get("author") or {}).get("username") or ""),
121
+ "draft": bool(mr.get("draft") or mr.get("work_in_progress")),
122
+ "updated": updated.isoformat(timespec="minutes") if updated else "",
123
+ "changed_since_last_digest": bool(updated and updated >= since),
124
+ "link": mr.get("web_url", ""),
125
+ })
126
+ todos = self._pages("todos", {"state": "pending"}, MAX_TODOS * 3)
127
+ older = 0
128
+ new = []
129
+ for t in todos:
130
+ created = parse_time(t.get("created_at"))
131
+ if created and created >= since:
132
+ new.append((created, t))
133
+ else:
134
+ older += 1
135
+ for created, t in new[:MAX_TODOS]:
136
+ target = t.get("target") or {}
137
+ res.items.append({
138
+ "kind": "todo", "action": t.get("action_name", ""),
139
+ "target_type": t.get("target_type", ""),
140
+ "title": redact(clip(target.get("title", ""), 200)),
141
+ "from": ((t.get("author") or {}).get("username") or ""),
142
+ "when": created.isoformat(timespec="minutes"),
143
+ "text": redact(clip(t.get("body", ""), TEXT_CHARS)),
144
+ "link": t.get("target_url", ""),
145
+ })
146
+ if len(new) > MAX_TODOS:
147
+ res.notes.append(f"{len(new) - MAX_TODOS} more new to-dos not shown.")
148
+ if older:
149
+ res.notes.append(f"{older} older pending to-dos (from before this digest's window).")
150
+ after = (since - timedelta(days=1)).date().isoformat()
151
+ events = [e for e in self._pages("events", {"after": after}, MAX_EVENTS * 2)
152
+ if (parse_time(e.get("created_at")) or since) >= since]
153
+ if len(events) > MAX_EVENTS:
154
+ res.notes.append(f"{len(events) - MAX_EVENTS} more activity events not shown.")
155
+ for e in events[:MAX_EVENTS]:
156
+ push = e.get("push_data") or {}
157
+ note = e.get("note") or {}
158
+ res.items.append({
159
+ "kind": "activity", "action": e.get("action_name", ""),
160
+ "target_type": e.get("target_type") or ("push" if push else ""),
161
+ "title": redact(clip(e.get("target_title") or push.get("commit_title") or "", 200)),
162
+ "ref": push.get("ref", ""),
163
+ "commits": push.get("commit_count", 0) if push else 0,
164
+ "comment": redact(clip(note.get("body", ""), TEXT_CHARS)) if note else "",
165
+ "project": self._project_name(e.get("project_id")),
166
+ "when": (parse_time(e.get("created_at")) or since).isoformat(timespec="minutes"),
167
+ })
168
+ res.items = mark(res.items, "gitlab", "gitlab") # connector content (hyperclaw.taint)
169
+ return res
170
+
171
+ def _project_name(self, pid) -> str:
172
+ if not pid:
173
+ return ""
174
+ if pid not in self._projects:
175
+ name = str(pid)
176
+ if len(self._projects) < MAX_PROJECT_LOOKUPS:
177
+ try:
178
+ data, _ = self._get(f"projects/{int(pid)}")
179
+ name = str(data.get("path_with_namespace") or pid)
180
+ except DigestError as e:
181
+ if e.reason in (Reason.GITLAB_TOKEN_EXPIRED, Reason.GITLAB_RATE_LIMITED):
182
+ raise
183
+ self._projects[pid] = name
184
+ return self._projects[pid]
185
+
186
+ def close(self) -> None:
187
+ self.http.close()