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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- 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]
|
hyperclaw/deployment.py
ADDED
|
@@ -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"]
|
hyperclaw/digest/cli.py
ADDED
|
@@ -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()
|