holdspeak 0.2.1__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.
- holdspeak/__init__.py +40 -0
- holdspeak/activity_candidates.py +120 -0
- holdspeak/activity_connector_preview.py +242 -0
- holdspeak/activity_connectors.py +140 -0
- holdspeak/activity_context.py +154 -0
- holdspeak/activity_entities.py +139 -0
- holdspeak/activity_extension.py +253 -0
- holdspeak/activity_github.py +293 -0
- holdspeak/activity_history.py +394 -0
- holdspeak/activity_jira.py +265 -0
- holdspeak/activity_mapping.py +114 -0
- holdspeak/agent_context/__init__.py +152 -0
- holdspeak/agent_context/_common.py +26 -0
- holdspeak/agent_context/hooks.py +132 -0
- holdspeak/agent_context/hs_context.py +341 -0
- holdspeak/agent_context/models.py +184 -0
- holdspeak/agent_context/sessions.py +791 -0
- holdspeak/agent_device.py +174 -0
- holdspeak/agent_summarizer.py +345 -0
- holdspeak/artifacts.py +64 -0
- holdspeak/audio.py +232 -0
- holdspeak/audio_devices.py +383 -0
- holdspeak/commands/__init__.py +1 -0
- holdspeak/commands/actions.py +61 -0
- holdspeak/commands/agent_hook.py +172 -0
- holdspeak/commands/backup.py +77 -0
- holdspeak/commands/device.py +47 -0
- holdspeak/commands/dictation.py +297 -0
- holdspeak/commands/doctor.py +1104 -0
- holdspeak/commands/history.py +157 -0
- holdspeak/commands/intel.py +403 -0
- holdspeak/config.py +556 -0
- holdspeak/connector_fixtures.py +347 -0
- holdspeak/connector_pack_loader.py +423 -0
- holdspeak/connector_packs/__init__.py +42 -0
- holdspeak/connector_packs/calendar_activity.py +111 -0
- holdspeak/connector_packs/firefox_ext.py +66 -0
- holdspeak/connector_packs/github_cli.py +148 -0
- holdspeak/connector_packs/jira_cli.py +123 -0
- holdspeak/connector_packs/meeting_context.py +299 -0
- holdspeak/connector_runtime.py +429 -0
- holdspeak/connector_sdk.py +762 -0
- holdspeak/db/__init__.py +25 -0
- holdspeak/db/activity.py +1553 -0
- holdspeak/db/actuators.py +294 -0
- holdspeak/db/base.py +49 -0
- holdspeak/db/core.py +903 -0
- holdspeak/db/corrections.py +110 -0
- holdspeak/db/intel.py +394 -0
- holdspeak/db/journal.py +215 -0
- holdspeak/db/meetings.py +890 -0
- holdspeak/db/milestones.py +79 -0
- holdspeak/db/models.py +444 -0
- holdspeak/db/plugins.py +814 -0
- holdspeak/db/projects.py +463 -0
- holdspeak/desktop_presence.py +367 -0
- holdspeak/desktop_presence_cocoa.py +287 -0
- holdspeak/desktop_presence_freedesktop.py +267 -0
- holdspeak/desktop_presence_gtk.py +243 -0
- holdspeak/device_audio.py +602 -0
- holdspeak/device_audio_ws.py +572 -0
- holdspeak/device_meeting_stats.py +154 -0
- holdspeak/device_recording_tick.py +153 -0
- holdspeak/device_status.py +411 -0
- holdspeak/dictation_learning.py +272 -0
- holdspeak/dictation_telemetry.py +230 -0
- holdspeak/hotkey.py +164 -0
- holdspeak/intel/__init__.py +111 -0
- holdspeak/intel/engine.py +547 -0
- holdspeak/intel/models.py +88 -0
- holdspeak/intel/parsing.py +209 -0
- holdspeak/intel/providers.py +267 -0
- holdspeak/intel_queue.py +482 -0
- holdspeak/intent_timeline.py +112 -0
- holdspeak/logging_config.py +47 -0
- holdspeak/main.py +562 -0
- holdspeak/meeting.py +703 -0
- holdspeak/meeting_aftercare.py +355 -0
- holdspeak/meeting_exports.py +245 -0
- holdspeak/meeting_session.py +1659 -0
- holdspeak/plugin_pack_loader.py +420 -0
- holdspeak/plugin_packs/__init__.py +26 -0
- holdspeak/plugin_sdk.py +330 -0
- holdspeak/plugins/__init__.py +100 -0
- holdspeak/plugins/actuator_executor.py +170 -0
- holdspeak/plugins/actuators.py +140 -0
- holdspeak/plugins/builtin/__init__.py +195 -0
- holdspeak/plugins/builtin/action_owner_enforcer.py +237 -0
- holdspeak/plugins/builtin/adr_drafter.py +231 -0
- holdspeak/plugins/builtin/customer_signal_extractor.py +211 -0
- holdspeak/plugins/builtin/decision_announcement_drafter.py +174 -0
- holdspeak/plugins/builtin/decision_capture.py +200 -0
- holdspeak/plugins/builtin/dependency_mapper.py +173 -0
- holdspeak/plugins/builtin/followup_ticket_actuator.py +165 -0
- holdspeak/plugins/builtin/github_issue_actuator.py +245 -0
- holdspeak/plugins/builtin/incident_timeline.py +174 -0
- holdspeak/plugins/builtin/mermaid_architecture.py +288 -0
- holdspeak/plugins/builtin/milestone_planner.py +218 -0
- holdspeak/plugins/builtin/requirements_extractor.py +226 -0
- holdspeak/plugins/builtin/risk_heatmap.py +237 -0
- holdspeak/plugins/builtin/runbook_delta.py +198 -0
- holdspeak/plugins/builtin/scope_guard.py +207 -0
- holdspeak/plugins/builtin/stakeholder_update_drafter.py +175 -0
- holdspeak/plugins/builtin/webhook_post_actuator.py +205 -0
- holdspeak/plugins/contracts.py +153 -0
- holdspeak/plugins/dictation/__init__.py +6 -0
- holdspeak/plugins/dictation/assembly.py +142 -0
- holdspeak/plugins/dictation/blocks.py +353 -0
- holdspeak/plugins/dictation/builtin/__init__.py +7 -0
- holdspeak/plugins/dictation/builtin/intent_router.py +241 -0
- holdspeak/plugins/dictation/builtin/kb_enricher.py +187 -0
- holdspeak/plugins/dictation/builtin/project_rewriter.py +441 -0
- holdspeak/plugins/dictation/contracts.py +64 -0
- holdspeak/plugins/dictation/corrections.py +242 -0
- holdspeak/plugins/dictation/grammars.py +217 -0
- holdspeak/plugins/dictation/guidance.py +235 -0
- holdspeak/plugins/dictation/journal.py +137 -0
- holdspeak/plugins/dictation/pipeline.py +145 -0
- holdspeak/plugins/dictation/project_kb.py +122 -0
- holdspeak/plugins/dictation/project_root.py +166 -0
- holdspeak/plugins/dictation/runtime.py +228 -0
- holdspeak/plugins/dictation/runtime_counters.py +303 -0
- holdspeak/plugins/dictation/runtime_llama_cpp.py +184 -0
- holdspeak/plugins/dictation/runtime_mlx.py +191 -0
- holdspeak/plugins/dictation/runtime_openai_compatible.py +277 -0
- holdspeak/plugins/dictation/telemetry_store.py +102 -0
- holdspeak/plugins/dispatch.py +237 -0
- holdspeak/plugins/gated_connector.py +299 -0
- holdspeak/plugins/host.py +610 -0
- holdspeak/plugins/persistence.py +165 -0
- holdspeak/plugins/pipeline.py +225 -0
- holdspeak/plugins/project_detector.py +111 -0
- holdspeak/plugins/queue.py +179 -0
- holdspeak/plugins/router.py +206 -0
- holdspeak/plugins/scoring.py +99 -0
- holdspeak/plugins/segment_probe.py +165 -0
- holdspeak/plugins/signals.py +64 -0
- holdspeak/plugins/synthesis.py +749 -0
- holdspeak/project_doc_suggestions.py +315 -0
- holdspeak/runtime_activity.py +156 -0
- holdspeak/setup_runtime.py +111 -0
- holdspeak/setup_status.py +198 -0
- holdspeak/speaker_intel.py +403 -0
- holdspeak/static/_built/_astro/AppLayout.BQCpCpOb.css +1 -0
- holdspeak/static/_built/_astro/activity.DA_tfAUv.css +1 -0
- holdspeak/static/_built/_astro/activity.astro_astro_type_script_index_0_lang.DuqbHxsc.js +792 -0
- holdspeak/static/_built/_astro/arc.C9-mI4Co.js +1 -0
- holdspeak/static/_built/_astro/architectureDiagram-3BPJPVTR.DWncxEGY.js +36 -0
- holdspeak/static/_built/_astro/blockDiagram-GPEHLZMM.BcmuLtcp.js +132 -0
- holdspeak/static/_built/_astro/briefing-markdown.CG6qhLV7.js +72 -0
- holdspeak/static/_built/_astro/c4Diagram-AAUBKEIU.D0YM09iU.js +10 -0
- holdspeak/static/_built/_astro/channel.BHojkgFI.js +1 -0
- holdspeak/static/_built/_astro/chunk-2J33WTMH.CJ33DlQS.js +1 -0
- holdspeak/static/_built/_astro/chunk-4BX2VUAB.B7RJHp_O.js +1 -0
- holdspeak/static/_built/_astro/chunk-55IACEB6.BDSs_AEM.js +1 -0
- holdspeak/static/_built/_astro/chunk-727SXJPM.DPJ0Pqm_.js +206 -0
- holdspeak/static/_built/_astro/chunk-AQP2D5EJ.Css1Km4r.js +231 -0
- holdspeak/static/_built/_astro/chunk-FMBD7UC4.bzIS3Sbr.js +15 -0
- holdspeak/static/_built/_astro/chunk-ND2GUHAM.240nFkAJ.js +1 -0
- holdspeak/static/_built/_astro/chunk-QZHKN3VN.B4EOMkxi.js +1 -0
- holdspeak/static/_built/_astro/classDiagram-4FO5ZUOK.BJJlpiT3.js +1 -0
- holdspeak/static/_built/_astro/classDiagram-v2-Q7XG4LA2.BJJlpiT3.js +1 -0
- holdspeak/static/_built/_astro/companion.-9QC2i0c.css +1 -0
- holdspeak/static/_built/_astro/companion.astro_astro_type_script_index_0_lang.BjniKhot.js +193 -0
- holdspeak/static/_built/_astro/components.Ck31oQ9K.css +1 -0
- holdspeak/static/_built/_astro/cose-bilkent-S5V4N54A.DTSBLznh.js +1 -0
- holdspeak/static/_built/_astro/cytoscape.esm.CkSuTymj.js +321 -0
- holdspeak/static/_built/_astro/dagre-BM42HDAG.zt96dTxi.js +4 -0
- holdspeak/static/_built/_astro/defaultLocale.DX6XiGOO.js +1 -0
- holdspeak/static/_built/_astro/diagram-2AECGRRQ.D9XBzHxF.js +43 -0
- holdspeak/static/_built/_astro/diagram-5GNKFQAL.sNM7LTUu.js +10 -0
- holdspeak/static/_built/_astro/diagram-KO2AKTUF.Ca2PRDWQ.js +3 -0
- holdspeak/static/_built/_astro/diagram-LMA3HP47.DsT__rnS.js +24 -0
- holdspeak/static/_built/_astro/diagram-OG6HWLK6.TL8Yqa7l.js +24 -0
- holdspeak/static/_built/_astro/dictation.BvC1-5I-.css +1 -0
- holdspeak/static/_built/_astro/dictation.astro_astro_type_script_index_0_lang.DnK0bob4.js +2703 -0
- holdspeak/static/_built/_astro/erDiagram-TEJ5UH35.BX4Bu8K2.js +85 -0
- holdspeak/static/_built/_astro/flowDiagram-I6XJVG4X.DLX_YAKZ.js +162 -0
- holdspeak/static/_built/_astro/ganttDiagram-6RSMTGT7.CUgTcpLU.js +292 -0
- holdspeak/static/_built/_astro/gitGraphDiagram-PVQCEYII.D4CrYgXJ.js +106 -0
- holdspeak/static/_built/_astro/global.BJQBsoEX.css +1 -0
- holdspeak/static/_built/_astro/graph.-OzhPTMs.js +1 -0
- holdspeak/static/_built/_astro/history.CEex34w_.css +1 -0
- holdspeak/static/_built/_astro/history.astro_astro_type_script_index_0_lang.C2uhSDck.js +1442 -0
- holdspeak/static/_built/_astro/index.COQeraZI.css +1 -0
- holdspeak/static/_built/_astro/index.astro_astro_type_script_index_0_lang.Dl3tsaO-.js +1529 -0
- holdspeak/static/_built/_astro/infoDiagram-5YYISTIA.C7cseoqe.js +2 -0
- holdspeak/static/_built/_astro/init.Gi6I4Gst.js +1 -0
- holdspeak/static/_built/_astro/inter-cyrillic-400-normal.HOLc17fK.woff +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-400-normal.obahsSVq.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-500-normal.BasfLYem.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-500-normal.CxZf_p3X.woff +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-600-normal.4D_pXhcN.woff +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-600-normal.CWCymEST.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-ext-400-normal.BQZuk6qB.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-ext-400-normal.DQukG94-.woff +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-ext-500-normal.B0yAr1jD.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-ext-500-normal.BmqWE9Dz.woff +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-ext-600-normal.Bcila6Z-.woff +0 -0
- holdspeak/static/_built/_astro/inter-cyrillic-ext-600-normal.Dfes3d0z.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-greek-400-normal.B4URO6DV.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-greek-400-normal.q2sYcFCs.woff +0 -0
- holdspeak/static/_built/_astro/inter-greek-500-normal.BIZE56-Y.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-greek-500-normal.Xzm54t5V.woff +0 -0
- holdspeak/static/_built/_astro/inter-greek-600-normal.BZpKdvQh.woff +0 -0
- holdspeak/static/_built/_astro/inter-greek-600-normal.plRanbMR.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-greek-ext-400-normal.DGGRlc-M.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-greek-ext-400-normal.KugGGMne.woff +0 -0
- holdspeak/static/_built/_astro/inter-greek-ext-500-normal.2j5mBUwD.woff +0 -0
- holdspeak/static/_built/_astro/inter-greek-ext-500-normal.C4iEst2y.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-greek-ext-600-normal.B8X0CLgF.woff +0 -0
- holdspeak/static/_built/_astro/inter-greek-ext-600-normal.DRtmH8MT.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-latin-400-normal.C38fXH4l.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-latin-400-normal.CyCys3Eg.woff +0 -0
- holdspeak/static/_built/_astro/inter-latin-500-normal.BL9OpVg8.woff +0 -0
- holdspeak/static/_built/_astro/inter-latin-500-normal.Cerq10X2.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-latin-600-normal.CiBQ2DWP.woff +0 -0
- holdspeak/static/_built/_astro/inter-latin-600-normal.LgqL8muc.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-latin-ext-400-normal.77YHD8bZ.woff +0 -0
- holdspeak/static/_built/_astro/inter-latin-ext-400-normal.C1nco2VV.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-latin-ext-500-normal.BxGbmqWO.woff +0 -0
- holdspeak/static/_built/_astro/inter-latin-ext-500-normal.CV4jyFjo.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-latin-ext-600-normal.CIVaiw4L.woff +0 -0
- holdspeak/static/_built/_astro/inter-latin-ext-600-normal.D2bJ5OIk.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-vietnamese-400-normal.Bbgyi5SW.woff +0 -0
- holdspeak/static/_built/_astro/inter-vietnamese-400-normal.DMkecbls.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-vietnamese-500-normal.DOriooB6.woff2 +0 -0
- holdspeak/static/_built/_astro/inter-vietnamese-500-normal.mJboJaSs.woff +0 -0
- holdspeak/static/_built/_astro/inter-vietnamese-600-normal.BuLX-rYi.woff +0 -0
- holdspeak/static/_built/_astro/inter-vietnamese-600-normal.Cc8MFFhd.woff2 +0 -0
- holdspeak/static/_built/_astro/ishikawaDiagram-YF4QCWOH.BdfUqAt-.js +70 -0
- holdspeak/static/_built/_astro/jetbrains-mono-cyrillic-400-normal.BEIGL1Tu.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-cyrillic-400-normal.ugxPyKxw.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-cyrillic-500-normal.DJqRU3vO.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-cyrillic-500-normal.DmUKJPL_.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-greek-400-normal.B9oWc5Lo.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-greek-400-normal.C190GLew.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-greek-500-normal.D7SFKleX.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-greek-500-normal.JpySY46c.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-400-normal.6-qcROiO.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-400-normal.V6pRDFza.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-500-normal.BWZEU5yA.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-500-normal.CJOVTJB7.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-ext-400-normal.Bc8Ftmh3.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-ext-400-normal.fXTG6kC5.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-ext-500-normal.Cut-4mMH.woff2 +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-latin-ext-500-normal.ckzbgY84.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-vietnamese-400-normal.CqNFfHCs.woff +0 -0
- holdspeak/static/_built/_astro/jetbrains-mono-vietnamese-500-normal.DNRqzVM1.woff +0 -0
- holdspeak/static/_built/_astro/journeyDiagram-JHISSGLW.WZS9VdE2.js +139 -0
- holdspeak/static/_built/_astro/kanban-definition-UN3LZRKU.CiBJq3eg.js +89 -0
- holdspeak/static/_built/_astro/katex.HP8lGamR.js +257 -0
- holdspeak/static/_built/_astro/layout.owoKPs3z.js +1 -0
- holdspeak/static/_built/_astro/linear.CnSHR4tB.js +1 -0
- holdspeak/static/_built/_astro/mermaid.core.Dk3RQ27w.js +303 -0
- holdspeak/static/_built/_astro/mindmap-definition-RKZ34NQL.D6ToJLIk.js +96 -0
- holdspeak/static/_built/_astro/module.esm.DLuZkmyn.js +5 -0
- holdspeak/static/_built/_astro/ordinal.BYWQX77i.js +1 -0
- holdspeak/static/_built/_astro/pieDiagram-4H26LBE5.Bjq6EVuj.js +30 -0
- holdspeak/static/_built/_astro/quadrantDiagram-W4KKPZXB.DWImXuIw.js +7 -0
- holdspeak/static/_built/_astro/requirementDiagram-4Y6WPE33.DcoqCJGO.js +84 -0
- holdspeak/static/_built/_astro/sankeyDiagram-5OEKKPKP.CF7MFHI6.js +40 -0
- holdspeak/static/_built/_astro/sequenceDiagram-3UESZ5HK.DOtyfyfd.js +162 -0
- holdspeak/static/_built/_astro/settings.DzjNJCZS.css +1 -0
- holdspeak/static/_built/_astro/settings.astro_astro_type_script_index_0_lang.BzMA_Ke9.js +243 -0
- holdspeak/static/_built/_astro/setup.BPUW9MXF.css +1 -0
- holdspeak/static/_built/_astro/setup.astro_astro_type_script_index_0_lang.C9WPmx9P.js +216 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-500-normal.CNSSEhBt.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-500-normal.lFbtlQH6.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-600-normal.BflQw4A9.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-600-normal.DjKNqYRj.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-700-normal.CwsQ-cCU.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-700-normal.RjhwGPKo.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-ext-500-normal.3dgZTiw9.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-ext-500-normal.DUe3BAxM.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-ext-600-normal.DxxdqCpr.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-ext-600-normal.VcznFIpX.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-ext-700-normal.BQnZhY3m.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-latin-ext-700-normal.HVCqSBdx.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-vietnamese-500-normal.BTqKIpxg.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-vietnamese-500-normal.BmEvtly_.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-vietnamese-600-normal.D6zpsUhD.woff +0 -0
- holdspeak/static/_built/_astro/space-grotesk-vietnamese-600-normal.DUi7WF5p.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-vietnamese-700-normal.DMty7AZE.woff2 +0 -0
- holdspeak/static/_built/_astro/space-grotesk-vietnamese-700-normal.Duxec5Rn.woff +0 -0
- holdspeak/static/_built/_astro/stateDiagram-AJRCARHV.05fu2X9t.js +1 -0
- holdspeak/static/_built/_astro/stateDiagram-v2-BHNVJYJU.DroQ3LdL.js +1 -0
- holdspeak/static/_built/_astro/timeline-definition-PNZ67QCA.CeLSEqD8.js +120 -0
- holdspeak/static/_built/_astro/vennDiagram-CIIHVFJN.DfWLwODp.js +34 -0
- holdspeak/static/_built/_astro/wardley-L42UT6IY.DO5nHI8o.js +161 -0
- holdspeak/static/_built/_astro/wardleyDiagram-YWT4CUSO.C6Y_RHFZ.js +78 -0
- holdspeak/static/_built/_astro/welcome.BIz1gKPz.css +1 -0
- holdspeak/static/_built/_astro/welcome.astro_astro_type_script_index_0_lang.C8G7sNS7.js +285 -0
- holdspeak/static/_built/_astro/xychartDiagram-2RQKCTM6.CtZMLnKX.js +7 -0
- holdspeak/static/_built/activity/index.html +123 -0
- holdspeak/static/_built/apple-touch-icon.png +0 -0
- holdspeak/static/_built/companion/index.html +118 -0
- holdspeak/static/_built/design/check/index.html +122 -0
- holdspeak/static/_built/design/components/index.html +184 -0
- holdspeak/static/_built/dictation/index.html +255 -0
- holdspeak/static/_built/docs/dictation-runtime/index.html +192 -0
- holdspeak/static/_built/favicon.svg +7 -0
- holdspeak/static/_built/history/index.html +129 -0
- holdspeak/static/_built/holdspeak-mark.png +0 -0
- holdspeak/static/_built/index.html +149 -0
- holdspeak/static/_built/presence/index.html +3 -0
- holdspeak/static/_built/settings/index.html +114 -0
- holdspeak/static/_built/setup/index.html +118 -0
- holdspeak/static/_built/welcome/index.html +8 -0
- holdspeak/target_profile.py +402 -0
- holdspeak/text_processor.py +134 -0
- holdspeak/tmux_transport.py +62 -0
- holdspeak/transcribe.py +371 -0
- holdspeak/typer.py +140 -0
- holdspeak/voice_typing.py +190 -0
- holdspeak/web/__init__.py +7 -0
- holdspeak/web/context.py +84 -0
- holdspeak/web/routes/__init__.py +31 -0
- holdspeak/web/routes/activity/__init__.py +45 -0
- holdspeak/web/routes/activity/candidates.py +228 -0
- holdspeak/web/routes/activity/enrichment.py +614 -0
- holdspeak/web/routes/activity/ledger.py +189 -0
- holdspeak/web/routes/activity/plugin_jobs.py +205 -0
- holdspeak/web/routes/activity/rules.py +176 -0
- holdspeak/web/routes/core.py +37 -0
- holdspeak/web/routes/dictation/__init__.py +61 -0
- holdspeak/web/routes/dictation/_helpers.py +710 -0
- holdspeak/web/routes/dictation/agent.py +201 -0
- holdspeak/web/routes/dictation/blocks.py +342 -0
- holdspeak/web/routes/dictation/intents.py +99 -0
- holdspeak/web/routes/dictation/kb.py +167 -0
- holdspeak/web/routes/dictation/pipeline.py +637 -0
- holdspeak/web/routes/dictation/project_docs.py +145 -0
- holdspeak/web/routes/meetings.py +1229 -0
- holdspeak/web/routes/pages.py +262 -0
- holdspeak/web/routes/projects.py +396 -0
- holdspeak/web/routes/setup.py +56 -0
- holdspeak/web/routes/system.py +960 -0
- holdspeak/web/runtime_support.py +79 -0
- holdspeak/web_auth.py +120 -0
- holdspeak/web_requests.py +163 -0
- holdspeak/web_runtime.py +2341 -0
- holdspeak/web_server.py +556 -0
- holdspeak-0.2.1.dist-info/METADATA +276 -0
- holdspeak-0.2.1.dist-info/RECORD +348 -0
- holdspeak-0.2.1.dist-info/WHEEL +4 -0
- holdspeak-0.2.1.dist-info/entry_points.txt +2 -0
- holdspeak-0.2.1.dist-info/licenses/LICENSE +201 -0
holdspeak/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""HoldSpeak - Voice typing for macOS and Linux. Hold, speak, release."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _resolve_version() -> str:
|
|
7
|
+
"""Return the one true version.
|
|
8
|
+
|
|
9
|
+
The package metadata written from `pyproject.toml` is the single source of
|
|
10
|
+
truth. An editable install (`uv pip install -e .`) registers that metadata,
|
|
11
|
+
so this resolves correctly for both installed and source-tree runs. The
|
|
12
|
+
fallback only matters when running from a raw checkout that was never
|
|
13
|
+
installed; there we read the version straight out of `pyproject.toml`.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
return version("holdspeak")
|
|
20
|
+
except PackageNotFoundError:
|
|
21
|
+
pass
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import re
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
30
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
31
|
+
match = re.search(r'(?m)^\s*version\s*=\s*"([^"]+)"', text)
|
|
32
|
+
if match:
|
|
33
|
+
return match.group(1)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
return "0.0.0+unknown"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__version__ = _resolve_version()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Local meeting-candidate extraction from activity records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Iterable, Optional
|
|
9
|
+
from urllib.parse import unquote
|
|
10
|
+
|
|
11
|
+
from .db import ActivityRecord
|
|
12
|
+
|
|
13
|
+
CALENDAR_CONNECTOR_ID = "calendar_activity"
|
|
14
|
+
|
|
15
|
+
CALENDAR_DOMAINS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"calendar.google.com",
|
|
18
|
+
"meet.google.com",
|
|
19
|
+
"outlook.live.com",
|
|
20
|
+
"outlook.office.com",
|
|
21
|
+
"outlook.office365.com",
|
|
22
|
+
"teams.microsoft.com",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DATE_TIME_RE = re.compile(
|
|
27
|
+
r"\b(?P<date>\d{4}-\d{2}-\d{2})[T ]+"
|
|
28
|
+
r"(?P<start>\d{1,2}:\d{2})"
|
|
29
|
+
r"(?:\s*(?:-|to)\s*(?P<end>\d{1,2}:\d{2}))?\b",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ActivityMeetingCandidatePreview:
|
|
36
|
+
"""Preview of a meeting candidate derived from local activity only."""
|
|
37
|
+
|
|
38
|
+
title: str
|
|
39
|
+
starts_at: Optional[datetime]
|
|
40
|
+
ends_at: Optional[datetime]
|
|
41
|
+
meeting_url: str
|
|
42
|
+
source_activity_record_id: int
|
|
43
|
+
source_connector_id: str
|
|
44
|
+
confidence: float
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def preview_calendar_meeting_candidates(
|
|
48
|
+
records: Iterable[ActivityRecord],
|
|
49
|
+
*,
|
|
50
|
+
source_connector_id: str = CALENDAR_CONNECTOR_ID,
|
|
51
|
+
limit: int = 50,
|
|
52
|
+
) -> list[ActivityMeetingCandidatePreview]:
|
|
53
|
+
"""Derive meeting-candidate previews from existing local activity records."""
|
|
54
|
+
previews: list[ActivityMeetingCandidatePreview] = []
|
|
55
|
+
for record in records:
|
|
56
|
+
if not _is_calendar_record(record):
|
|
57
|
+
continue
|
|
58
|
+
title = _candidate_title(record)
|
|
59
|
+
starts_at, ends_at = _candidate_time_hints(record)
|
|
60
|
+
previews.append(
|
|
61
|
+
ActivityMeetingCandidatePreview(
|
|
62
|
+
title=title,
|
|
63
|
+
starts_at=starts_at,
|
|
64
|
+
ends_at=ends_at,
|
|
65
|
+
meeting_url=record.url,
|
|
66
|
+
source_activity_record_id=record.id,
|
|
67
|
+
source_connector_id=source_connector_id,
|
|
68
|
+
confidence=_candidate_confidence(record),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
if len(previews) >= max(1, min(int(limit), 500)):
|
|
72
|
+
break
|
|
73
|
+
return previews
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _is_calendar_record(record: ActivityRecord) -> bool:
|
|
77
|
+
domain = str(record.domain or "").strip().lower()
|
|
78
|
+
if domain in CALENDAR_DOMAINS:
|
|
79
|
+
return True
|
|
80
|
+
return any(domain.endswith(f".{candidate}") for candidate in CALENDAR_DOMAINS)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _candidate_title(record: ActivityRecord) -> str:
|
|
84
|
+
title = str(record.title or "").strip()
|
|
85
|
+
if title:
|
|
86
|
+
return title
|
|
87
|
+
if "teams.microsoft.com" in record.domain:
|
|
88
|
+
return "Microsoft Teams meeting"
|
|
89
|
+
if "meet.google.com" in record.domain:
|
|
90
|
+
return "Google Meet meeting"
|
|
91
|
+
if "outlook" in record.domain:
|
|
92
|
+
return "Outlook calendar event"
|
|
93
|
+
return "Calendar event"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _candidate_confidence(record: ActivityRecord) -> float:
|
|
97
|
+
title = str(record.title or "").lower()
|
|
98
|
+
if "meeting" in title or "calendar" in title:
|
|
99
|
+
return 0.75
|
|
100
|
+
if "teams.microsoft.com" in record.domain or "meet.google.com" in record.domain:
|
|
101
|
+
return 0.7
|
|
102
|
+
return 0.55
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _candidate_time_hints(record: ActivityRecord) -> tuple[Optional[datetime], Optional[datetime]]:
|
|
106
|
+
text = f"{record.title or ''} {unquote(record.url or '')}"
|
|
107
|
+
match = DATE_TIME_RE.search(text)
|
|
108
|
+
if match is None:
|
|
109
|
+
return None, None
|
|
110
|
+
try:
|
|
111
|
+
starts_at = datetime.fromisoformat(f"{match.group('date')}T{match.group('start')}:00")
|
|
112
|
+
end_text = match.group("end")
|
|
113
|
+
if not end_text:
|
|
114
|
+
return starts_at, None
|
|
115
|
+
ends_at = datetime.fromisoformat(f"{match.group('date')}T{end_text}:00")
|
|
116
|
+
if ends_at < starts_at:
|
|
117
|
+
ends_at += timedelta(days=1)
|
|
118
|
+
return starts_at, ends_at
|
|
119
|
+
except ValueError:
|
|
120
|
+
return None, None
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Shared dry-run harness for activity-enrichment connectors.
|
|
2
|
+
|
|
3
|
+
HS-9-13. Each known connector (gh, jira, calendar_activity) describes
|
|
4
|
+
what it *would* do via this harness without writing to the database.
|
|
5
|
+
The result shape is the same for every connector so the browser can
|
|
6
|
+
render a single dry-run preview surface.
|
|
7
|
+
|
|
8
|
+
Mutation-free guarantee:
|
|
9
|
+
|
|
10
|
+
- The harness only reads from the database (`list_activity_records`).
|
|
11
|
+
- It calls each connector's preview helper, which itself does not
|
|
12
|
+
mutate state (`preview_github_cli_enrichment`, `preview_jira_cli_enrichment`,
|
|
13
|
+
`preview_calendar_meeting_candidates`).
|
|
14
|
+
- It does not call any *_run_* helper.
|
|
15
|
+
|
|
16
|
+
Tests in `tests/integration/test_web_activity_api.py` assert that
|
|
17
|
+
the row counts of `activity_annotations` and
|
|
18
|
+
`activity_meeting_candidates` are unchanged after a dry-run.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Mapping, Optional
|
|
25
|
+
|
|
26
|
+
from .activity_candidates import (
|
|
27
|
+
CALENDAR_CONNECTOR_ID,
|
|
28
|
+
preview_calendar_meeting_candidates,
|
|
29
|
+
)
|
|
30
|
+
from .activity_connectors import KNOWN_CONNECTORS, get_descriptor
|
|
31
|
+
from .activity_github import (
|
|
32
|
+
CONNECTOR_ID as GH_CONNECTOR_ID,
|
|
33
|
+
SUPPORTED_ENTITY_TYPES as GH_SUPPORTED_ENTITY_TYPES,
|
|
34
|
+
preview_github_cli_enrichment,
|
|
35
|
+
)
|
|
36
|
+
from .activity_jira import (
|
|
37
|
+
CONNECTOR_ID as JIRA_CONNECTOR_ID,
|
|
38
|
+
SUPPORTED_ENTITY_TYPES as JIRA_SUPPORTED_ENTITY_TYPES,
|
|
39
|
+
preview_jira_cli_enrichment,
|
|
40
|
+
)
|
|
41
|
+
from .db import Database
|
|
42
|
+
|
|
43
|
+
DEFAULT_LIMIT = 25
|
|
44
|
+
MAX_LIMIT = 100
|
|
45
|
+
|
|
46
|
+
# A safety cap on the per-section length of the dry-run payload so a
|
|
47
|
+
# pathological local dataset cannot blow up the API response. Each
|
|
48
|
+
# section (commands / proposed_annotations / proposed_candidates) is
|
|
49
|
+
# truncated to this length and the result is flagged `truncated=True`.
|
|
50
|
+
PAYLOAD_SECTION_CAP = 100
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class ConnectorDryRunResult:
|
|
55
|
+
"""Uniform dry-run preview produced by `dry_run()`.
|
|
56
|
+
|
|
57
|
+
Every connector returns this shape. The browser renders the same
|
|
58
|
+
surface (commands → CommandPreview, proposed_* → list of cards,
|
|
59
|
+
warnings + permission_notes → inline messages) regardless of the
|
|
60
|
+
underlying connector kind.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
connector_id: str
|
|
64
|
+
kind: str
|
|
65
|
+
capabilities: tuple[str, ...]
|
|
66
|
+
enabled: bool
|
|
67
|
+
cli_required: Optional[str]
|
|
68
|
+
cli_available: Optional[bool]
|
|
69
|
+
commands: tuple[Mapping[str, Any], ...] = field(default_factory=tuple)
|
|
70
|
+
proposed_annotations: tuple[Mapping[str, Any], ...] = field(default_factory=tuple)
|
|
71
|
+
proposed_candidates: tuple[Mapping[str, Any], ...] = field(default_factory=tuple)
|
|
72
|
+
warnings: tuple[str, ...] = field(default_factory=tuple)
|
|
73
|
+
permission_notes: tuple[str, ...] = field(default_factory=tuple)
|
|
74
|
+
truncated: bool = False
|
|
75
|
+
|
|
76
|
+
def to_payload(self) -> dict[str, Any]:
|
|
77
|
+
return {
|
|
78
|
+
"connector_id": self.connector_id,
|
|
79
|
+
"kind": self.kind,
|
|
80
|
+
"capabilities": list(self.capabilities),
|
|
81
|
+
"enabled": self.enabled,
|
|
82
|
+
"cli_required": self.cli_required,
|
|
83
|
+
"cli_available": self.cli_available,
|
|
84
|
+
"commands": list(self.commands),
|
|
85
|
+
"proposed_annotations": list(self.proposed_annotations),
|
|
86
|
+
"proposed_candidates": list(self.proposed_candidates),
|
|
87
|
+
"warnings": list(self.warnings),
|
|
88
|
+
"permission_notes": list(self.permission_notes),
|
|
89
|
+
"truncated": self.truncated,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class UnknownConnectorError(ValueError):
|
|
94
|
+
"""Raised when `dry_run()` is given a connector id that isn't registered."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def dry_run(
|
|
98
|
+
db: Database,
|
|
99
|
+
connector_id: str,
|
|
100
|
+
*,
|
|
101
|
+
limit: int = DEFAULT_LIMIT,
|
|
102
|
+
) -> ConnectorDryRunResult:
|
|
103
|
+
"""Build a uniform mutation-free preview for one connector.
|
|
104
|
+
|
|
105
|
+
Returns a `ConnectorDryRunResult`. The caller is responsible for
|
|
106
|
+
serializing it via `to_payload()`. The harness does not raise on
|
|
107
|
+
a disabled connector or a missing CLI — those land in
|
|
108
|
+
`permission_notes` so the browser can render the would-do plan
|
|
109
|
+
alongside the reason it can't currently run.
|
|
110
|
+
"""
|
|
111
|
+
descriptor = get_descriptor(connector_id)
|
|
112
|
+
if descriptor is None:
|
|
113
|
+
raise UnknownConnectorError(connector_id)
|
|
114
|
+
|
|
115
|
+
capped_limit = max(1, min(int(limit), MAX_LIMIT))
|
|
116
|
+
state = db.activity.get_activity_enrichment_connector(descriptor.id)
|
|
117
|
+
enabled = bool(state.enabled) if state is not None else False
|
|
118
|
+
|
|
119
|
+
cli_required = descriptor.requires_cli
|
|
120
|
+
cli_status = descriptor.cli_status() if cli_required else None
|
|
121
|
+
cli_available = bool(cli_status.get("available")) if cli_status else None
|
|
122
|
+
|
|
123
|
+
permission_notes: list[str] = []
|
|
124
|
+
if not enabled:
|
|
125
|
+
permission_notes.append(
|
|
126
|
+
f"{descriptor.label} is currently disabled. Dry-run shows what "
|
|
127
|
+
"the connector would do if you enabled it; nothing runs."
|
|
128
|
+
)
|
|
129
|
+
if cli_required and cli_available is False:
|
|
130
|
+
permission_notes.append(
|
|
131
|
+
f"`{cli_required}` CLI was not found on PATH. Install and "
|
|
132
|
+
"authenticate it before enabling this connector."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
warnings: list[str] = []
|
|
136
|
+
commands: list[Mapping[str, Any]] = []
|
|
137
|
+
proposed_annotations: list[Mapping[str, Any]] = []
|
|
138
|
+
proposed_candidates: list[Mapping[str, Any]] = []
|
|
139
|
+
|
|
140
|
+
if descriptor.id == GH_CONNECTOR_ID:
|
|
141
|
+
records = []
|
|
142
|
+
for entity_type in GH_SUPPORTED_ENTITY_TYPES:
|
|
143
|
+
records.extend(
|
|
144
|
+
db.activity.list_activity_records(entity_type=entity_type, limit=capped_limit)
|
|
145
|
+
)
|
|
146
|
+
records = records[:capped_limit]
|
|
147
|
+
if not records:
|
|
148
|
+
warnings.append(
|
|
149
|
+
"No GitHub PR or issue activity has been imported yet. "
|
|
150
|
+
"Visit a PR or issue page in your browser, then refresh "
|
|
151
|
+
"the activity ledger."
|
|
152
|
+
)
|
|
153
|
+
preview = preview_github_cli_enrichment(records, limit=capped_limit)
|
|
154
|
+
commands = list(preview.get("commands", []))
|
|
155
|
+
for command in commands:
|
|
156
|
+
proposed_annotations.append(
|
|
157
|
+
{
|
|
158
|
+
"annotation_type": command.get("annotation_type"),
|
|
159
|
+
"activity_record_id": command.get("activity_record_id"),
|
|
160
|
+
"entity_id": command.get("entity_id"),
|
|
161
|
+
"title": f"Local {command.get('annotation_type')} annotation",
|
|
162
|
+
"from_command": list(command.get("command", [])),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
elif descriptor.id == JIRA_CONNECTOR_ID:
|
|
167
|
+
records = []
|
|
168
|
+
for entity_type in JIRA_SUPPORTED_ENTITY_TYPES:
|
|
169
|
+
records.extend(
|
|
170
|
+
db.activity.list_activity_records(entity_type=entity_type, limit=capped_limit)
|
|
171
|
+
)
|
|
172
|
+
records = records[:capped_limit]
|
|
173
|
+
if not records:
|
|
174
|
+
warnings.append(
|
|
175
|
+
"No Jira ticket activity has been imported yet. Visit "
|
|
176
|
+
"an Atlassian ticket in your browser, then refresh the "
|
|
177
|
+
"activity ledger."
|
|
178
|
+
)
|
|
179
|
+
preview = preview_jira_cli_enrichment(records, limit=capped_limit)
|
|
180
|
+
commands = list(preview.get("commands", []))
|
|
181
|
+
for command in commands:
|
|
182
|
+
proposed_annotations.append(
|
|
183
|
+
{
|
|
184
|
+
"annotation_type": command.get("annotation_type"),
|
|
185
|
+
"activity_record_id": command.get("activity_record_id"),
|
|
186
|
+
"entity_id": command.get("entity_id"),
|
|
187
|
+
"title": f"Local {command.get('annotation_type')} annotation",
|
|
188
|
+
"from_command": list(command.get("command", [])),
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
elif descriptor.id == CALENDAR_CONNECTOR_ID:
|
|
193
|
+
records = db.activity.list_activity_records(limit=max(capped_limit * 4, 50))
|
|
194
|
+
previews = preview_calendar_meeting_candidates(records, limit=capped_limit)
|
|
195
|
+
if not previews:
|
|
196
|
+
warnings.append(
|
|
197
|
+
"No calendar / video-call activity has been imported "
|
|
198
|
+
"yet. Visit a Google Calendar event, Outlook event, or "
|
|
199
|
+
"Meet/Teams link, then refresh the activity ledger."
|
|
200
|
+
)
|
|
201
|
+
for preview in previews:
|
|
202
|
+
proposed_candidates.append(
|
|
203
|
+
{
|
|
204
|
+
"title": preview.title,
|
|
205
|
+
"starts_at": preview.starts_at.isoformat() if preview.starts_at else None,
|
|
206
|
+
"ends_at": preview.ends_at.isoformat() if preview.ends_at else None,
|
|
207
|
+
"meeting_url": preview.meeting_url,
|
|
208
|
+
"source_activity_record_id": preview.source_activity_record_id,
|
|
209
|
+
"source_connector_id": preview.source_connector_id,
|
|
210
|
+
"confidence": preview.confidence,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
truncated = False
|
|
215
|
+
if len(commands) > PAYLOAD_SECTION_CAP:
|
|
216
|
+
commands = commands[:PAYLOAD_SECTION_CAP]
|
|
217
|
+
truncated = True
|
|
218
|
+
if len(proposed_annotations) > PAYLOAD_SECTION_CAP:
|
|
219
|
+
proposed_annotations = proposed_annotations[:PAYLOAD_SECTION_CAP]
|
|
220
|
+
truncated = True
|
|
221
|
+
if len(proposed_candidates) > PAYLOAD_SECTION_CAP:
|
|
222
|
+
proposed_candidates = proposed_candidates[:PAYLOAD_SECTION_CAP]
|
|
223
|
+
truncated = True
|
|
224
|
+
|
|
225
|
+
return ConnectorDryRunResult(
|
|
226
|
+
connector_id=descriptor.id,
|
|
227
|
+
kind=descriptor.kind,
|
|
228
|
+
capabilities=descriptor.capabilities,
|
|
229
|
+
enabled=enabled,
|
|
230
|
+
cli_required=cli_required,
|
|
231
|
+
cli_available=cli_available,
|
|
232
|
+
commands=tuple(commands),
|
|
233
|
+
proposed_annotations=tuple(proposed_annotations),
|
|
234
|
+
proposed_candidates=tuple(proposed_candidates),
|
|
235
|
+
warnings=tuple(warnings),
|
|
236
|
+
permission_notes=tuple(permission_notes),
|
|
237
|
+
truncated=truncated,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def known_connector_ids() -> tuple[str, ...]:
|
|
242
|
+
return tuple(c.id for c in KNOWN_CONNECTORS)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Pack-derived registry of activity connectors known to the runtime.
|
|
2
|
+
|
|
3
|
+
HS-13-01 + HS-13-04. The runtime registry now derives from
|
|
4
|
+
both first-party packs (`connector_packs.ALL_PACKS`) and any
|
|
5
|
+
user packs discovered under `~/.holdspeak/connector_packs/`
|
|
6
|
+
via `connector_pack_loader.build_registry`. The descriptor
|
|
7
|
+
surface is unchanged for downstream consumers; only the source
|
|
8
|
+
field on `ConnectorDescriptor` and the new
|
|
9
|
+
`reload_registry()`/`discovery_errors()` helpers are new.
|
|
10
|
+
|
|
11
|
+
`ConnectorDescriptor` carries:
|
|
12
|
+
|
|
13
|
+
- `id`, `label`, `kind`, `capabilities`, `requires_cli`,
|
|
14
|
+
`description` — sourced from the manifest.
|
|
15
|
+
- `source` — `"first-party"` or `"user"` so the API + doctor
|
|
16
|
+
can label each connector by provenance.
|
|
17
|
+
- `manifest` — the underlying `ConnectorManifest`.
|
|
18
|
+
- `cli_status()` — dispatches by id (gh / jira only).
|
|
19
|
+
|
|
20
|
+
The descriptor's `capabilities` is the manifest's
|
|
21
|
+
`capabilities` filtered to row-producing capabilities so the
|
|
22
|
+
manifest's `commands` / pure-preview capabilities don't leak
|
|
23
|
+
into API consumers expecting a row-shape.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
from .activity_github import CONNECTOR_ID as GH_CONNECTOR_ID, github_cli_status
|
|
33
|
+
from .activity_jira import CONNECTOR_ID as JIRA_CONNECTOR_ID, jira_cli_status
|
|
34
|
+
from .connector_pack_loader import (
|
|
35
|
+
DiscoveryResult,
|
|
36
|
+
RegisteredPack,
|
|
37
|
+
build_registry,
|
|
38
|
+
)
|
|
39
|
+
from .connector_sdk import ConnectorManifest
|
|
40
|
+
|
|
41
|
+
_ROW_CAPABILITIES: frozenset[str] = frozenset({"records", "annotations", "candidates"})
|
|
42
|
+
|
|
43
|
+
ENRICHMENT_KINDS: frozenset[str] = frozenset(
|
|
44
|
+
{"cli_enrichment", "candidate_inference"}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ConnectorDescriptor:
|
|
50
|
+
"""Runtime view of one pack-registered connector."""
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
label: str
|
|
54
|
+
kind: str
|
|
55
|
+
capabilities: tuple[str, ...]
|
|
56
|
+
requires_cli: Optional[str]
|
|
57
|
+
description: str
|
|
58
|
+
source: str
|
|
59
|
+
manifest: ConnectorManifest
|
|
60
|
+
|
|
61
|
+
def cli_status(self) -> Optional[dict]:
|
|
62
|
+
if self.id == GH_CONNECTOR_ID:
|
|
63
|
+
return github_cli_status()
|
|
64
|
+
if self.id == JIRA_CONNECTOR_ID:
|
|
65
|
+
return jira_cli_status()
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _descriptor_from_pack(pack: RegisteredPack) -> ConnectorDescriptor:
|
|
70
|
+
manifest = pack.manifest
|
|
71
|
+
return ConnectorDescriptor(
|
|
72
|
+
id=manifest.id,
|
|
73
|
+
label=manifest.label,
|
|
74
|
+
kind=manifest.kind,
|
|
75
|
+
capabilities=tuple(c for c in manifest.capabilities if c in _ROW_CAPABILITIES),
|
|
76
|
+
requires_cli=manifest.requires_cli,
|
|
77
|
+
description=manifest.description,
|
|
78
|
+
source=pack.source,
|
|
79
|
+
manifest=manifest,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ───────────────────── Module-level registry state ────────────────────
|
|
84
|
+
#
|
|
85
|
+
# Populated at import time and refreshable via `reload_registry`.
|
|
86
|
+
# The web API + the dry-run harness + the fixture runner all
|
|
87
|
+
# read these globals; tests that exercise user-pack discovery
|
|
88
|
+
# call `reload_registry(user_packs_dir=tmp_path)` to swap them.
|
|
89
|
+
|
|
90
|
+
_DISCOVERY: DiscoveryResult = DiscoveryResult()
|
|
91
|
+
KNOWN_CONNECTORS: tuple[ConnectorDescriptor, ...] = ()
|
|
92
|
+
KNOWN_CONNECTOR_IDS: frozenset[str] = frozenset()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _apply_discovery(result: DiscoveryResult) -> None:
|
|
96
|
+
global _DISCOVERY, KNOWN_CONNECTORS, KNOWN_CONNECTOR_IDS
|
|
97
|
+
_DISCOVERY = result
|
|
98
|
+
KNOWN_CONNECTORS = tuple(_descriptor_from_pack(p) for p in result.packs)
|
|
99
|
+
KNOWN_CONNECTOR_IDS = frozenset(c.id for c in KNOWN_CONNECTORS)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def reload_registry(
|
|
103
|
+
user_packs_dir: Optional[Path] = None,
|
|
104
|
+
) -> DiscoveryResult:
|
|
105
|
+
"""Recompute the registry. Returns the resulting
|
|
106
|
+
`DiscoveryResult` for callers that want to inspect errors.
|
|
107
|
+
|
|
108
|
+
Tests use this to swap in a tmp_path-scoped user-pack dir.
|
|
109
|
+
Production code calls it implicitly at module import; a
|
|
110
|
+
runtime restart re-discovers any new files dropped into
|
|
111
|
+
`~/.holdspeak/connector_packs/`.
|
|
112
|
+
"""
|
|
113
|
+
result = build_registry(user_packs_dir=user_packs_dir)
|
|
114
|
+
_apply_discovery(result)
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def discovery_errors() -> tuple:
|
|
119
|
+
"""Return the discovery errors from the most recent registry
|
|
120
|
+
load. Doctor + the API surface these so a malformed user
|
|
121
|
+
pack is visible without grepping logs."""
|
|
122
|
+
return _DISCOVERY.errors
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def get_descriptor(connector_id: str) -> Optional[ConnectorDescriptor]:
|
|
126
|
+
for descriptor in KNOWN_CONNECTORS:
|
|
127
|
+
if descriptor.id == connector_id:
|
|
128
|
+
return descriptor
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def enrichment_descriptors() -> tuple[ConnectorDescriptor, ...]:
|
|
133
|
+
"""Subset of the registry that drives the activity-enrichment
|
|
134
|
+
surface. Records-ingesters (firefox_ext) live in the registry
|
|
135
|
+
but not on this surface."""
|
|
136
|
+
return tuple(c for c in KNOWN_CONNECTORS if c.kind in ENRICHMENT_KINDS)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# Initial load at module-import time.
|
|
140
|
+
reload_registry()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Shared activity context bundles for HoldSpeak plugins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from threading import Lock
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from .activity_history import import_browser_history
|
|
13
|
+
from .db import ActivityRecord, Database, get_database
|
|
14
|
+
from .logging_config import get_logger
|
|
15
|
+
|
|
16
|
+
log = get_logger("activity_context")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ActivityContextBundle:
|
|
21
|
+
"""Serializable local activity context for plugin consumers."""
|
|
22
|
+
|
|
23
|
+
records: list[dict[str, Any]]
|
|
24
|
+
entity_counts: dict[str, int]
|
|
25
|
+
domain_counts: dict[str, int]
|
|
26
|
+
source_counts: dict[str, int]
|
|
27
|
+
generated_at: str
|
|
28
|
+
project_id: Optional[str] = None
|
|
29
|
+
refreshed: bool = False
|
|
30
|
+
refresh_errors: list[str] | None = None
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> dict[str, Any]:
|
|
33
|
+
return {
|
|
34
|
+
"records": list(self.records),
|
|
35
|
+
"entity_counts": dict(self.entity_counts),
|
|
36
|
+
"domain_counts": dict(self.domain_counts),
|
|
37
|
+
"source_counts": dict(self.source_counts),
|
|
38
|
+
"generated_at": self.generated_at,
|
|
39
|
+
"project_id": self.project_id,
|
|
40
|
+
"refreshed": self.refreshed,
|
|
41
|
+
"refresh_errors": list(self.refresh_errors or []),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ActivityContextProvider:
|
|
46
|
+
"""Callable context provider that injects local activity into plugins."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
db: Database | None = None,
|
|
52
|
+
limit: int = 20,
|
|
53
|
+
refresh: bool = False,
|
|
54
|
+
refresh_once: bool = True,
|
|
55
|
+
importer: Callable[..., Any] = import_browser_history,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._db = db
|
|
58
|
+
self._limit = max(1, min(int(limit), 200))
|
|
59
|
+
self._refresh = bool(refresh)
|
|
60
|
+
self._refresh_once = bool(refresh_once)
|
|
61
|
+
self._importer = importer
|
|
62
|
+
self._lock = Lock()
|
|
63
|
+
self._refreshed = False
|
|
64
|
+
|
|
65
|
+
def __call__(self, context: dict[str, Any]) -> dict[str, Any]:
|
|
66
|
+
project_id = _project_id_from_context(context)
|
|
67
|
+
bundle = build_activity_context(
|
|
68
|
+
db=self._db,
|
|
69
|
+
project_id=project_id,
|
|
70
|
+
limit=self._limit,
|
|
71
|
+
refresh=self._should_refresh(),
|
|
72
|
+
importer=self._importer,
|
|
73
|
+
)
|
|
74
|
+
return {"activity": bundle.to_dict()}
|
|
75
|
+
|
|
76
|
+
def _should_refresh(self) -> bool:
|
|
77
|
+
if not self._refresh:
|
|
78
|
+
return False
|
|
79
|
+
with self._lock:
|
|
80
|
+
if self._refresh_once and self._refreshed:
|
|
81
|
+
return False
|
|
82
|
+
self._refreshed = True
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_activity_context(
|
|
87
|
+
*,
|
|
88
|
+
db: Database | None = None,
|
|
89
|
+
project_id: Optional[str] = None,
|
|
90
|
+
limit: int = 20,
|
|
91
|
+
refresh: bool = False,
|
|
92
|
+
importer: Callable[..., Any] = import_browser_history,
|
|
93
|
+
) -> ActivityContextBundle:
|
|
94
|
+
"""Build a plugin-safe local activity context bundle."""
|
|
95
|
+
database = db or get_database()
|
|
96
|
+
refresh_errors: list[str] = []
|
|
97
|
+
did_refresh = False
|
|
98
|
+
if refresh:
|
|
99
|
+
try:
|
|
100
|
+
results = importer(db=database)
|
|
101
|
+
did_refresh = True
|
|
102
|
+
refresh_errors = [
|
|
103
|
+
str(result.error)
|
|
104
|
+
for result in results
|
|
105
|
+
if getattr(result, "error", None)
|
|
106
|
+
]
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
refresh_errors.append(f"{type(exc).__name__}: {exc}")
|
|
109
|
+
log.warning("Activity context refresh failed: %s", exc)
|
|
110
|
+
|
|
111
|
+
records = database.activity.list_activity_records(
|
|
112
|
+
project_id=project_id,
|
|
113
|
+
limit=max(1, min(int(limit), 200)),
|
|
114
|
+
)
|
|
115
|
+
serialized = [_serialize_activity_record(record) for record in records]
|
|
116
|
+
return ActivityContextBundle(
|
|
117
|
+
records=serialized,
|
|
118
|
+
entity_counts=dict(Counter(item["entity_type"] for item in serialized if item["entity_type"])),
|
|
119
|
+
domain_counts=dict(Counter(item["domain"] for item in serialized if item["domain"])),
|
|
120
|
+
source_counts=dict(Counter(item["source_browser"] for item in serialized if item["source_browser"])),
|
|
121
|
+
generated_at=datetime.now().isoformat(),
|
|
122
|
+
project_id=project_id,
|
|
123
|
+
refreshed=did_refresh,
|
|
124
|
+
refresh_errors=refresh_errors,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _serialize_activity_record(record: ActivityRecord) -> dict[str, Any]:
|
|
129
|
+
return {
|
|
130
|
+
"id": record.id,
|
|
131
|
+
"source_browser": record.source_browser,
|
|
132
|
+
"source_profile": record.source_profile,
|
|
133
|
+
"url": record.url,
|
|
134
|
+
"title": record.title,
|
|
135
|
+
"domain": record.domain,
|
|
136
|
+
"visit_count": record.visit_count,
|
|
137
|
+
"first_seen_at": record.first_seen_at.isoformat() if record.first_seen_at else None,
|
|
138
|
+
"last_seen_at": record.last_seen_at.isoformat() if record.last_seen_at else None,
|
|
139
|
+
"entity_type": record.entity_type,
|
|
140
|
+
"entity_id": record.entity_id,
|
|
141
|
+
"project_id": record.project_id,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _project_id_from_context(context: dict[str, Any]) -> Optional[str]:
|
|
146
|
+
raw_project_id = context.get("project_id")
|
|
147
|
+
if raw_project_id not in (None, ""):
|
|
148
|
+
return str(raw_project_id)
|
|
149
|
+
project = context.get("project")
|
|
150
|
+
if isinstance(project, dict):
|
|
151
|
+
raw_project_id = project.get("id") or project.get("project_id")
|
|
152
|
+
if raw_project_id not in (None, ""):
|
|
153
|
+
return str(raw_project_id)
|
|
154
|
+
return None
|