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,764 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Productivity Connectors.
|
|
3
|
+
Integration with Obsidian, Apple Notes, Apple Calendar, Google Sheets, Google Docs, Evernote, OneNote, ClickUp, Monday.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import logging
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Note:
|
|
22
|
+
"""Note/document."""
|
|
23
|
+
id: str
|
|
24
|
+
title: str
|
|
25
|
+
content: str
|
|
26
|
+
created_at: datetime | None = None
|
|
27
|
+
updated_at: datetime | None = None
|
|
28
|
+
tags: list[str] = field(default_factory=list)
|
|
29
|
+
raw: dict = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CalendarEvent:
|
|
34
|
+
"""Calendar event."""
|
|
35
|
+
id: str
|
|
36
|
+
title: str
|
|
37
|
+
start: datetime
|
|
38
|
+
end: datetime
|
|
39
|
+
description: str = ""
|
|
40
|
+
location: str = ""
|
|
41
|
+
raw: dict = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ProductivityConnector(ABC):
|
|
45
|
+
"""Abstract base for productivity connectors."""
|
|
46
|
+
|
|
47
|
+
name: str = "base"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NoteConnector(ProductivityConnector):
|
|
51
|
+
"""Abstract base for note-taking connectors."""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def get_notes(self, limit: int = 50, **kwargs) -> list[Note]:
|
|
55
|
+
"""Get notes."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
async def create_note(self, title: str, content: str, **kwargs) -> Note:
|
|
60
|
+
"""Create a note."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
async def update_note(self, note_id: str, content: str, **kwargs) -> Note:
|
|
65
|
+
"""Update a note."""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
70
|
+
# OBSIDIAN (Local Vault)
|
|
71
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
|
|
73
|
+
class ObsidianConnector(NoteConnector):
|
|
74
|
+
"""Obsidian vault connector (local filesystem)."""
|
|
75
|
+
|
|
76
|
+
name = "obsidian"
|
|
77
|
+
|
|
78
|
+
def __init__(self, vault_path: str | None = None):
|
|
79
|
+
self.vault_path = Path(vault_path or os.environ.get("OBSIDIAN_VAULT", "~/Documents/Obsidian")).expanduser()
|
|
80
|
+
|
|
81
|
+
async def get_notes(self, limit: int = 50, **kwargs) -> list[Note]:
|
|
82
|
+
notes = []
|
|
83
|
+
for md_file in sorted(self.vault_path.rglob("*.md"), key=lambda p: p.stat().st_mtime, reverse=True)[:limit]:
|
|
84
|
+
content = md_file.read_text(encoding="utf-8")
|
|
85
|
+
stat = md_file.stat()
|
|
86
|
+
|
|
87
|
+
# Extract tags from content
|
|
88
|
+
import re
|
|
89
|
+
tags = re.findall(r"#(\w+)", content)
|
|
90
|
+
|
|
91
|
+
notes.append(
|
|
92
|
+
Note(
|
|
93
|
+
id=str(md_file.relative_to(self.vault_path)),
|
|
94
|
+
title=md_file.stem,
|
|
95
|
+
content=content,
|
|
96
|
+
created_at=datetime.fromtimestamp(stat.st_ctime),
|
|
97
|
+
updated_at=datetime.fromtimestamp(stat.st_mtime),
|
|
98
|
+
tags=list(set(tags)),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return notes
|
|
103
|
+
|
|
104
|
+
async def create_note(self, title: str, content: str, **kwargs) -> Note:
|
|
105
|
+
folder = kwargs.get("folder", "")
|
|
106
|
+
note_path = self.vault_path / folder / f"{title}.md"
|
|
107
|
+
note_path.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
note_path.write_text(content, encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
return Note(
|
|
111
|
+
id=str(note_path.relative_to(self.vault_path)),
|
|
112
|
+
title=title,
|
|
113
|
+
content=content,
|
|
114
|
+
created_at=datetime.now(),
|
|
115
|
+
updated_at=datetime.now(),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def update_note(self, note_id: str, content: str, **kwargs) -> Note:
|
|
119
|
+
note_path = self.vault_path / note_id
|
|
120
|
+
if not note_path.exists():
|
|
121
|
+
raise FileNotFoundError(f"Note not found: {note_id}")
|
|
122
|
+
|
|
123
|
+
note_path.write_text(content, encoding="utf-8")
|
|
124
|
+
|
|
125
|
+
return Note(
|
|
126
|
+
id=note_id,
|
|
127
|
+
title=note_path.stem,
|
|
128
|
+
content=content,
|
|
129
|
+
updated_at=datetime.now(),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def search(self, query: str, limit: int = 20) -> list[Note]:
|
|
133
|
+
"""Search notes by content."""
|
|
134
|
+
results = []
|
|
135
|
+
query_lower = query.lower()
|
|
136
|
+
|
|
137
|
+
for md_file in self.vault_path.rglob("*.md"):
|
|
138
|
+
content = md_file.read_text(encoding="utf-8")
|
|
139
|
+
if query_lower in content.lower() or query_lower in md_file.stem.lower():
|
|
140
|
+
results.append(
|
|
141
|
+
Note(
|
|
142
|
+
id=str(md_file.relative_to(self.vault_path)),
|
|
143
|
+
title=md_file.stem,
|
|
144
|
+
content=content[:500],
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
if len(results) >= limit:
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
return results
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
154
|
+
# APPLE NOTES (via AppleScript)
|
|
155
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
156
|
+
|
|
157
|
+
class AppleNotesConnector(NoteConnector):
|
|
158
|
+
"""Apple Notes connector (macOS only, via AppleScript)."""
|
|
159
|
+
|
|
160
|
+
name = "apple_notes"
|
|
161
|
+
|
|
162
|
+
async def get_notes(self, limit: int = 50, **kwargs) -> list[Note]:
|
|
163
|
+
import subprocess
|
|
164
|
+
|
|
165
|
+
script = f'''
|
|
166
|
+
tell application "Notes"
|
|
167
|
+
set noteList to {{}}
|
|
168
|
+
repeat with aNote in (notes 1 thru {limit})
|
|
169
|
+
set noteInfo to {{id:(id of aNote), title:(name of aNote), body:(body of aNote)}}
|
|
170
|
+
set end of noteList to noteInfo
|
|
171
|
+
end repeat
|
|
172
|
+
return noteList
|
|
173
|
+
end tell
|
|
174
|
+
'''
|
|
175
|
+
|
|
176
|
+
result = subprocess.run(
|
|
177
|
+
["osascript", "-e", script],
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if result.returncode != 0:
|
|
183
|
+
logger.error(f"AppleScript error: {result.stderr}")
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
# Parse AppleScript output (simplified)
|
|
187
|
+
return [] # TODO: Parse AppleScript list output
|
|
188
|
+
|
|
189
|
+
async def create_note(self, title: str, content: str, **kwargs) -> Note:
|
|
190
|
+
import subprocess
|
|
191
|
+
|
|
192
|
+
folder = kwargs.get("folder", "Notes")
|
|
193
|
+
|
|
194
|
+
script = f'''
|
|
195
|
+
tell application "Notes"
|
|
196
|
+
tell folder "{folder}"
|
|
197
|
+
make new note with properties {{name:"{title}", body:"{content}"}}
|
|
198
|
+
end tell
|
|
199
|
+
end tell
|
|
200
|
+
'''
|
|
201
|
+
|
|
202
|
+
result = subprocess.run(
|
|
203
|
+
["osascript", "-e", script],
|
|
204
|
+
capture_output=True,
|
|
205
|
+
text=True,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result.returncode != 0:
|
|
209
|
+
raise RuntimeError(f"Failed to create note: {result.stderr}")
|
|
210
|
+
|
|
211
|
+
return Note(id="", title=title, content=content, created_at=datetime.now())
|
|
212
|
+
|
|
213
|
+
async def update_note(self, note_id: str, content: str, **kwargs) -> Note:
|
|
214
|
+
import subprocess
|
|
215
|
+
|
|
216
|
+
script = f'''
|
|
217
|
+
tell application "Notes"
|
|
218
|
+
set theNote to note id "{note_id}"
|
|
219
|
+
set body of theNote to "{content}"
|
|
220
|
+
end tell
|
|
221
|
+
'''
|
|
222
|
+
|
|
223
|
+
result = subprocess.run(
|
|
224
|
+
["osascript", "-e", script],
|
|
225
|
+
capture_output=True,
|
|
226
|
+
text=True,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if result.returncode != 0:
|
|
230
|
+
raise RuntimeError(f"Failed to update note: {result.stderr}")
|
|
231
|
+
|
|
232
|
+
return Note(id=note_id, title="", content=content, updated_at=datetime.now())
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
236
|
+
# APPLE CALENDAR (via AppleScript)
|
|
237
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
238
|
+
|
|
239
|
+
class AppleCalendarConnector(ProductivityConnector):
|
|
240
|
+
"""Apple Calendar connector (macOS only, via AppleScript)."""
|
|
241
|
+
|
|
242
|
+
name = "apple_calendar"
|
|
243
|
+
|
|
244
|
+
async def get_events(self, days: int = 7, **kwargs) -> list[CalendarEvent]:
|
|
245
|
+
import subprocess
|
|
246
|
+
|
|
247
|
+
script = '''
|
|
248
|
+
tell application "Calendar"
|
|
249
|
+
set eventList to {}
|
|
250
|
+
set startDate to current date
|
|
251
|
+
set endDate to startDate + (%d * days)
|
|
252
|
+
|
|
253
|
+
repeat with aCal in calendars
|
|
254
|
+
set theEvents to (every event of aCal whose start date >= startDate and start date <= endDate)
|
|
255
|
+
repeat with anEvent in theEvents
|
|
256
|
+
set eventInfo to {id:(uid of anEvent), title:(summary of anEvent), startDate:(start date of anEvent), endDate:(end date of anEvent)}
|
|
257
|
+
set end of eventList to eventInfo
|
|
258
|
+
end repeat
|
|
259
|
+
end repeat
|
|
260
|
+
return eventList
|
|
261
|
+
end tell
|
|
262
|
+
''' % days
|
|
263
|
+
|
|
264
|
+
result = subprocess.run(
|
|
265
|
+
["osascript", "-e", script],
|
|
266
|
+
capture_output=True,
|
|
267
|
+
text=True,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if result.returncode != 0:
|
|
271
|
+
logger.error(f"AppleScript error: {result.stderr}")
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
return [] # TODO: Parse AppleScript output
|
|
275
|
+
|
|
276
|
+
async def create_event(
|
|
277
|
+
self,
|
|
278
|
+
title: str,
|
|
279
|
+
start: datetime,
|
|
280
|
+
end: datetime,
|
|
281
|
+
calendar: str = "Calendar",
|
|
282
|
+
**kwargs,
|
|
283
|
+
) -> CalendarEvent:
|
|
284
|
+
import subprocess
|
|
285
|
+
|
|
286
|
+
start_str = start.strftime("%B %d, %Y at %I:%M %p")
|
|
287
|
+
end_str = end.strftime("%B %d, %Y at %I:%M %p")
|
|
288
|
+
|
|
289
|
+
script = f'''
|
|
290
|
+
tell application "Calendar"
|
|
291
|
+
tell calendar "{calendar}"
|
|
292
|
+
make new event with properties {{summary:"{title}", start date:date "{start_str}", end date:date "{end_str}"}}
|
|
293
|
+
end tell
|
|
294
|
+
end tell
|
|
295
|
+
'''
|
|
296
|
+
|
|
297
|
+
result = subprocess.run(
|
|
298
|
+
["osascript", "-e", script],
|
|
299
|
+
capture_output=True,
|
|
300
|
+
text=True,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if result.returncode != 0:
|
|
304
|
+
raise RuntimeError(f"Failed to create event: {result.stderr}")
|
|
305
|
+
|
|
306
|
+
return CalendarEvent(id="", title=title, start=start, end=end)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
310
|
+
# GOOGLE SHEETS
|
|
311
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
312
|
+
|
|
313
|
+
class GoogleSheetsConnector(ProductivityConnector):
|
|
314
|
+
"""Google Sheets connector."""
|
|
315
|
+
|
|
316
|
+
name = "google_sheets"
|
|
317
|
+
|
|
318
|
+
def __init__(self, credentials: str | None = None):
|
|
319
|
+
self.credentials_path = credentials or os.environ.get("GOOGLE_CREDENTIALS_PATH")
|
|
320
|
+
self.base_url = "https://sheets.googleapis.com/v4/spreadsheets"
|
|
321
|
+
self._access_token: str | None = None
|
|
322
|
+
|
|
323
|
+
async def _get_token(self) -> str:
|
|
324
|
+
# In production, use google-auth library for proper OAuth
|
|
325
|
+
return os.environ.get("GOOGLE_SHEETS_TOKEN", "")
|
|
326
|
+
|
|
327
|
+
async def read_sheet(
|
|
328
|
+
self,
|
|
329
|
+
spreadsheet_id: str,
|
|
330
|
+
range: str = "Sheet1",
|
|
331
|
+
**kwargs,
|
|
332
|
+
) -> list[list[Any]]:
|
|
333
|
+
token = await self._get_token()
|
|
334
|
+
|
|
335
|
+
async with httpx.AsyncClient() as client:
|
|
336
|
+
response = await client.get(
|
|
337
|
+
f"{self.base_url}/{spreadsheet_id}/values/{range}",
|
|
338
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
339
|
+
timeout=30.0,
|
|
340
|
+
)
|
|
341
|
+
response.raise_for_status()
|
|
342
|
+
data = response.json()
|
|
343
|
+
|
|
344
|
+
return data.get("values", [])
|
|
345
|
+
|
|
346
|
+
async def write_sheet(
|
|
347
|
+
self,
|
|
348
|
+
spreadsheet_id: str,
|
|
349
|
+
range: str,
|
|
350
|
+
values: list[list[Any]],
|
|
351
|
+
**kwargs,
|
|
352
|
+
) -> dict:
|
|
353
|
+
token = await self._get_token()
|
|
354
|
+
|
|
355
|
+
async with httpx.AsyncClient() as client:
|
|
356
|
+
response = await client.put(
|
|
357
|
+
f"{self.base_url}/{spreadsheet_id}/values/{range}",
|
|
358
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
359
|
+
params={"valueInputOption": "USER_ENTERED"},
|
|
360
|
+
json={"values": values},
|
|
361
|
+
timeout=30.0,
|
|
362
|
+
)
|
|
363
|
+
response.raise_for_status()
|
|
364
|
+
return response.json()
|
|
365
|
+
|
|
366
|
+
async def append_row(
|
|
367
|
+
self,
|
|
368
|
+
spreadsheet_id: str,
|
|
369
|
+
range: str,
|
|
370
|
+
values: list[Any],
|
|
371
|
+
**kwargs,
|
|
372
|
+
) -> dict:
|
|
373
|
+
token = await self._get_token()
|
|
374
|
+
|
|
375
|
+
async with httpx.AsyncClient() as client:
|
|
376
|
+
response = await client.post(
|
|
377
|
+
f"{self.base_url}/{spreadsheet_id}/values/{range}:append",
|
|
378
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
379
|
+
params={"valueInputOption": "USER_ENTERED"},
|
|
380
|
+
json={"values": [values]},
|
|
381
|
+
timeout=30.0,
|
|
382
|
+
)
|
|
383
|
+
response.raise_for_status()
|
|
384
|
+
return response.json()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
388
|
+
# GOOGLE DOCS
|
|
389
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
390
|
+
|
|
391
|
+
class GoogleDocsConnector(ProductivityConnector):
|
|
392
|
+
"""Google Docs connector."""
|
|
393
|
+
|
|
394
|
+
name = "google_docs"
|
|
395
|
+
|
|
396
|
+
def __init__(self):
|
|
397
|
+
self.base_url = "https://docs.googleapis.com/v1/documents"
|
|
398
|
+
|
|
399
|
+
async def _get_token(self) -> str:
|
|
400
|
+
return os.environ.get("GOOGLE_DOCS_TOKEN", "")
|
|
401
|
+
|
|
402
|
+
async def get_document(self, document_id: str) -> dict:
|
|
403
|
+
token = await self._get_token()
|
|
404
|
+
|
|
405
|
+
async with httpx.AsyncClient() as client:
|
|
406
|
+
response = await client.get(
|
|
407
|
+
f"{self.base_url}/{document_id}",
|
|
408
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
409
|
+
timeout=30.0,
|
|
410
|
+
)
|
|
411
|
+
response.raise_for_status()
|
|
412
|
+
return response.json()
|
|
413
|
+
|
|
414
|
+
async def create_document(self, title: str) -> dict:
|
|
415
|
+
token = await self._get_token()
|
|
416
|
+
|
|
417
|
+
async with httpx.AsyncClient() as client:
|
|
418
|
+
response = await client.post(
|
|
419
|
+
self.base_url,
|
|
420
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
421
|
+
json={"title": title},
|
|
422
|
+
timeout=30.0,
|
|
423
|
+
)
|
|
424
|
+
response.raise_for_status()
|
|
425
|
+
return response.json()
|
|
426
|
+
|
|
427
|
+
async def insert_text(self, document_id: str, text: str, index: int = 1) -> dict:
|
|
428
|
+
token = await self._get_token()
|
|
429
|
+
|
|
430
|
+
async with httpx.AsyncClient() as client:
|
|
431
|
+
response = await client.post(
|
|
432
|
+
f"{self.base_url}/{document_id}:batchUpdate",
|
|
433
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
434
|
+
json={
|
|
435
|
+
"requests": [
|
|
436
|
+
{
|
|
437
|
+
"insertText": {
|
|
438
|
+
"location": {"index": index},
|
|
439
|
+
"text": text,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
},
|
|
444
|
+
timeout=30.0,
|
|
445
|
+
)
|
|
446
|
+
response.raise_for_status()
|
|
447
|
+
return response.json()
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
451
|
+
# EVERNOTE
|
|
452
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
453
|
+
|
|
454
|
+
class EvernoteConnector(NoteConnector):
|
|
455
|
+
"""Evernote connector."""
|
|
456
|
+
|
|
457
|
+
name = "evernote"
|
|
458
|
+
|
|
459
|
+
def __init__(self, token: str | None = None):
|
|
460
|
+
self.token = token or os.environ.get("EVERNOTE_TOKEN")
|
|
461
|
+
self.base_url = "https://api.evernote.com"
|
|
462
|
+
|
|
463
|
+
async def get_notes(self, limit: int = 50, **kwargs) -> list[Note]:
|
|
464
|
+
# Evernote uses a complex SDK - simplified HTTP approach
|
|
465
|
+
async with httpx.AsyncClient() as client:
|
|
466
|
+
response = await client.post(
|
|
467
|
+
f"{self.base_url}/edam/note",
|
|
468
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
469
|
+
json={"maxNotes": limit},
|
|
470
|
+
timeout=30.0,
|
|
471
|
+
)
|
|
472
|
+
response.raise_for_status()
|
|
473
|
+
data = response.json()
|
|
474
|
+
|
|
475
|
+
return [
|
|
476
|
+
Note(
|
|
477
|
+
id=note["guid"],
|
|
478
|
+
title=note["title"],
|
|
479
|
+
content=note.get("content", ""),
|
|
480
|
+
created_at=datetime.fromtimestamp(note["created"] / 1000)
|
|
481
|
+
if note.get("created") else None,
|
|
482
|
+
updated_at=datetime.fromtimestamp(note["updated"] / 1000)
|
|
483
|
+
if note.get("updated") else None,
|
|
484
|
+
tags=note.get("tagNames", []),
|
|
485
|
+
raw=note,
|
|
486
|
+
)
|
|
487
|
+
for note in data.get("notes", [])
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
async def create_note(self, title: str, content: str, **kwargs) -> Note:
|
|
491
|
+
notebook_guid = kwargs.get("notebook_guid")
|
|
492
|
+
|
|
493
|
+
async with httpx.AsyncClient() as client:
|
|
494
|
+
response = await client.post(
|
|
495
|
+
f"{self.base_url}/edam/note",
|
|
496
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
497
|
+
json={
|
|
498
|
+
"title": title,
|
|
499
|
+
"content": f'<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>{content}</en-note>',
|
|
500
|
+
"notebookGuid": notebook_guid,
|
|
501
|
+
},
|
|
502
|
+
timeout=30.0,
|
|
503
|
+
)
|
|
504
|
+
response.raise_for_status()
|
|
505
|
+
data = response.json()
|
|
506
|
+
|
|
507
|
+
return Note(id=data["guid"], title=title, content=content, created_at=datetime.now())
|
|
508
|
+
|
|
509
|
+
async def update_note(self, note_id: str, content: str, **kwargs) -> Note:
|
|
510
|
+
async with httpx.AsyncClient() as client:
|
|
511
|
+
response = await client.post(
|
|
512
|
+
f"{self.base_url}/edam/note/{note_id}",
|
|
513
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
514
|
+
json={
|
|
515
|
+
"guid": note_id,
|
|
516
|
+
"content": f'<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd"><en-note>{content}</en-note>',
|
|
517
|
+
},
|
|
518
|
+
timeout=30.0,
|
|
519
|
+
)
|
|
520
|
+
response.raise_for_status()
|
|
521
|
+
data = response.json()
|
|
522
|
+
|
|
523
|
+
return Note(id=note_id, title=data.get("title", ""), content=content, updated_at=datetime.now())
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
527
|
+
# ONENOTE
|
|
528
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
529
|
+
|
|
530
|
+
class OneNoteConnector(NoteConnector):
|
|
531
|
+
"""Microsoft OneNote connector."""
|
|
532
|
+
|
|
533
|
+
name = "onenote"
|
|
534
|
+
|
|
535
|
+
def __init__(self, access_token: str | None = None):
|
|
536
|
+
self.token = access_token or os.environ.get("MICROSOFT_ACCESS_TOKEN")
|
|
537
|
+
self.base_url = "https://graph.microsoft.com/v1.0/me/onenote"
|
|
538
|
+
|
|
539
|
+
async def get_notes(self, limit: int = 50, **kwargs) -> list[Note]:
|
|
540
|
+
async with httpx.AsyncClient() as client:
|
|
541
|
+
response = await client.get(
|
|
542
|
+
f"{self.base_url}/pages",
|
|
543
|
+
headers={"Authorization": f"Bearer {self.token}"},
|
|
544
|
+
params={"$top": limit},
|
|
545
|
+
timeout=30.0,
|
|
546
|
+
)
|
|
547
|
+
response.raise_for_status()
|
|
548
|
+
data = response.json()
|
|
549
|
+
|
|
550
|
+
return [
|
|
551
|
+
Note(
|
|
552
|
+
id=page["id"],
|
|
553
|
+
title=page["title"],
|
|
554
|
+
content="", # Content requires separate call
|
|
555
|
+
created_at=datetime.fromisoformat(page["createdDateTime"].replace("Z", "+00:00"))
|
|
556
|
+
if page.get("createdDateTime") else None,
|
|
557
|
+
updated_at=datetime.fromisoformat(page["lastModifiedDateTime"].replace("Z", "+00:00"))
|
|
558
|
+
if page.get("lastModifiedDateTime") else None,
|
|
559
|
+
raw=page,
|
|
560
|
+
)
|
|
561
|
+
for page in data.get("value", [])
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
async def create_note(self, title: str, content: str, **kwargs) -> Note:
|
|
565
|
+
section_id = kwargs.get("section_id")
|
|
566
|
+
|
|
567
|
+
html_content = f"""
|
|
568
|
+
<!DOCTYPE html>
|
|
569
|
+
<html>
|
|
570
|
+
<head><title>{title}</title></head>
|
|
571
|
+
<body>{content}</body>
|
|
572
|
+
</html>
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
async with httpx.AsyncClient() as client:
|
|
576
|
+
response = await client.post(
|
|
577
|
+
f"{self.base_url}/sections/{section_id}/pages",
|
|
578
|
+
headers={
|
|
579
|
+
"Authorization": f"Bearer {self.token}",
|
|
580
|
+
"Content-Type": "application/xhtml+xml",
|
|
581
|
+
},
|
|
582
|
+
content=html_content,
|
|
583
|
+
timeout=30.0,
|
|
584
|
+
)
|
|
585
|
+
response.raise_for_status()
|
|
586
|
+
data = response.json()
|
|
587
|
+
|
|
588
|
+
return Note(id=data["id"], title=title, content=content, created_at=datetime.now())
|
|
589
|
+
|
|
590
|
+
async def update_note(self, note_id: str, content: str, **kwargs) -> Note:
|
|
591
|
+
async with httpx.AsyncClient() as client:
|
|
592
|
+
response = await client.patch(
|
|
593
|
+
f"{self.base_url}/pages/{note_id}/content",
|
|
594
|
+
headers={
|
|
595
|
+
"Authorization": f"Bearer {self.token}",
|
|
596
|
+
"Content-Type": "application/json",
|
|
597
|
+
},
|
|
598
|
+
json=[
|
|
599
|
+
{
|
|
600
|
+
"target": "body",
|
|
601
|
+
"action": "replace",
|
|
602
|
+
"content": content,
|
|
603
|
+
}
|
|
604
|
+
],
|
|
605
|
+
timeout=30.0,
|
|
606
|
+
)
|
|
607
|
+
response.raise_for_status()
|
|
608
|
+
|
|
609
|
+
return Note(id=note_id, title="", content=content, updated_at=datetime.now())
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
613
|
+
# CLICKUP
|
|
614
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
615
|
+
|
|
616
|
+
class ClickUpConnector(ProductivityConnector):
|
|
617
|
+
"""ClickUp project management connector."""
|
|
618
|
+
|
|
619
|
+
name = "clickup"
|
|
620
|
+
|
|
621
|
+
def __init__(self, api_key: str | None = None):
|
|
622
|
+
self.api_key = api_key or os.environ.get("CLICKUP_API_KEY")
|
|
623
|
+
self.base_url = "https://api.clickup.com/api/v2"
|
|
624
|
+
|
|
625
|
+
async def get_tasks(self, list_id: str, **kwargs) -> list[dict]:
|
|
626
|
+
async with httpx.AsyncClient() as client:
|
|
627
|
+
response = await client.get(
|
|
628
|
+
f"{self.base_url}/list/{list_id}/task",
|
|
629
|
+
headers={"Authorization": self.api_key},
|
|
630
|
+
timeout=30.0,
|
|
631
|
+
)
|
|
632
|
+
response.raise_for_status()
|
|
633
|
+
return response.json().get("tasks", [])
|
|
634
|
+
|
|
635
|
+
async def create_task(self, list_id: str, name: str, **kwargs) -> dict:
|
|
636
|
+
async with httpx.AsyncClient() as client:
|
|
637
|
+
response = await client.post(
|
|
638
|
+
f"{self.base_url}/list/{list_id}/task",
|
|
639
|
+
headers={"Authorization": self.api_key},
|
|
640
|
+
json={"name": name, **kwargs},
|
|
641
|
+
timeout=30.0,
|
|
642
|
+
)
|
|
643
|
+
response.raise_for_status()
|
|
644
|
+
return response.json()
|
|
645
|
+
|
|
646
|
+
async def update_task(self, task_id: str, **kwargs) -> dict:
|
|
647
|
+
async with httpx.AsyncClient() as client:
|
|
648
|
+
response = await client.put(
|
|
649
|
+
f"{self.base_url}/task/{task_id}",
|
|
650
|
+
headers={"Authorization": self.api_key},
|
|
651
|
+
json=kwargs,
|
|
652
|
+
timeout=30.0,
|
|
653
|
+
)
|
|
654
|
+
response.raise_for_status()
|
|
655
|
+
return response.json()
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
659
|
+
# MONDAY.COM
|
|
660
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
661
|
+
|
|
662
|
+
class MondayConnector(ProductivityConnector):
|
|
663
|
+
"""Monday.com project management connector."""
|
|
664
|
+
|
|
665
|
+
name = "monday"
|
|
666
|
+
|
|
667
|
+
def __init__(self, api_key: str | None = None):
|
|
668
|
+
self.api_key = api_key or os.environ.get("MONDAY_API_KEY")
|
|
669
|
+
self.base_url = "https://api.monday.com/v2"
|
|
670
|
+
|
|
671
|
+
async def _query(self, query: str, variables: dict | None = None) -> dict:
|
|
672
|
+
async with httpx.AsyncClient() as client:
|
|
673
|
+
response = await client.post(
|
|
674
|
+
self.base_url,
|
|
675
|
+
headers={
|
|
676
|
+
"Authorization": self.api_key,
|
|
677
|
+
"Content-Type": "application/json",
|
|
678
|
+
},
|
|
679
|
+
json={"query": query, "variables": variables or {}},
|
|
680
|
+
timeout=30.0,
|
|
681
|
+
)
|
|
682
|
+
response.raise_for_status()
|
|
683
|
+
return response.json()
|
|
684
|
+
|
|
685
|
+
async def get_boards(self) -> list[dict]:
|
|
686
|
+
query = "{ boards { id name } }"
|
|
687
|
+
result = await self._query(query)
|
|
688
|
+
return result.get("data", {}).get("boards", [])
|
|
689
|
+
|
|
690
|
+
async def get_items(self, board_id: str, limit: int = 50) -> list[dict]:
|
|
691
|
+
query = f"""
|
|
692
|
+
{{
|
|
693
|
+
boards(ids: [{board_id}]) {{
|
|
694
|
+
items_page(limit: {limit}) {{
|
|
695
|
+
items {{
|
|
696
|
+
id
|
|
697
|
+
name
|
|
698
|
+
state
|
|
699
|
+
column_values {{
|
|
700
|
+
id
|
|
701
|
+
text
|
|
702
|
+
}}
|
|
703
|
+
}}
|
|
704
|
+
}}
|
|
705
|
+
}}
|
|
706
|
+
}}
|
|
707
|
+
"""
|
|
708
|
+
result = await self._query(query)
|
|
709
|
+
boards = result.get("data", {}).get("boards", [])
|
|
710
|
+
if boards:
|
|
711
|
+
return boards[0].get("items_page", {}).get("items", [])
|
|
712
|
+
return []
|
|
713
|
+
|
|
714
|
+
async def create_item(self, board_id: str, name: str, **kwargs) -> dict:
|
|
715
|
+
column_values = kwargs.get("column_values", {})
|
|
716
|
+
import json
|
|
717
|
+
|
|
718
|
+
query = """
|
|
719
|
+
mutation ($boardId: ID!, $itemName: String!, $columnValues: JSON) {
|
|
720
|
+
create_item(board_id: $boardId, item_name: $itemName, column_values: $columnValues) {
|
|
721
|
+
id
|
|
722
|
+
name
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
"""
|
|
726
|
+
result = await self._query(
|
|
727
|
+
query,
|
|
728
|
+
{
|
|
729
|
+
"boardId": board_id,
|
|
730
|
+
"itemName": name,
|
|
731
|
+
"columnValues": json.dumps(column_values),
|
|
732
|
+
},
|
|
733
|
+
)
|
|
734
|
+
return result.get("data", {}).get("create_item", {})
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
738
|
+
# PROVIDER FACTORY
|
|
739
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
740
|
+
|
|
741
|
+
PRODUCTIVITY_CONNECTORS: dict[str, type[ProductivityConnector]] = {
|
|
742
|
+
"obsidian": ObsidianConnector,
|
|
743
|
+
"apple_notes": AppleNotesConnector,
|
|
744
|
+
"apple_calendar": AppleCalendarConnector,
|
|
745
|
+
"google_sheets": GoogleSheetsConnector,
|
|
746
|
+
"sheets": GoogleSheetsConnector,
|
|
747
|
+
"google_docs": GoogleDocsConnector,
|
|
748
|
+
"docs": GoogleDocsConnector,
|
|
749
|
+
"evernote": EvernoteConnector,
|
|
750
|
+
"onenote": OneNoteConnector,
|
|
751
|
+
"clickup": ClickUpConnector,
|
|
752
|
+
"monday": MondayConnector,
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def get_productivity_connector(name: str, **kwargs) -> ProductivityConnector:
|
|
757
|
+
"""Get a productivity connector by name."""
|
|
758
|
+
name = name.lower()
|
|
759
|
+
|
|
760
|
+
if name not in PRODUCTIVITY_CONNECTORS:
|
|
761
|
+
available = ", ".join(sorted(PRODUCTIVITY_CONNECTORS.keys()))
|
|
762
|
+
raise ValueError(f"Unknown productivity connector: {name}. Available: {available}")
|
|
763
|
+
|
|
764
|
+
return PRODUCTIVITY_CONNECTORS[name](**kwargs)
|