algo-cli-runtime 0.14.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. algo_cli/__init__.py +3 -0
  2. algo_cli/__main__.py +7 -0
  3. algo_cli/_internal/__init__.py +12 -0
  4. algo_cli/_internal/policy_chain.py +259 -0
  5. algo_cli/action_registry.py +1047 -0
  6. algo_cli/agent_blocks.py +550 -0
  7. algo_cli/agent_pipeline.py +1457 -0
  8. algo_cli/agent_threads.py +308 -0
  9. algo_cli/animations.py +316 -0
  10. algo_cli/cache_admission.py +209 -0
  11. algo_cli/capability_mask.py +66 -0
  12. algo_cli/chat_protocol.py +116 -0
  13. algo_cli/chatgpt_auth.py +510 -0
  14. algo_cli/chatgpt_client.py +657 -0
  15. algo_cli/code_rag.py +479 -0
  16. algo_cli/config.py +651 -0
  17. algo_cli/context_budget.py +679 -0
  18. algo_cli/credential_helpers.py +315 -0
  19. algo_cli/deliberation.py +29 -0
  20. algo_cli/display.py +1470 -0
  21. algo_cli/evals/__init__.py +21 -0
  22. algo_cli/evals/algorithm_effectiveness.py +560 -0
  23. algo_cli/evals/competitive_harness_rating.py +702 -0
  24. algo_cli/evals/cot_quality.py +220 -0
  25. algo_cli/evals/harness_retrieval_benchmark.py +401 -0
  26. algo_cli/evals/performance_regression.py +136 -0
  27. algo_cli/evals/scorecard_grading.py +308 -0
  28. algo_cli/evals/session_distribution.py +84 -0
  29. algo_cli/execution_guardrails.py +806 -0
  30. algo_cli/extensions_manifest.py +84 -0
  31. algo_cli/git_evidence.py +227 -0
  32. algo_cli/google_workspace.py +407 -0
  33. algo_cli/google_workspace_auth.py +523 -0
  34. algo_cli/harness.py +2587 -0
  35. algo_cli/identity.py +557 -0
  36. algo_cli/index_compute_lab.py +228 -0
  37. algo_cli/inference_harness.py +70 -0
  38. algo_cli/intelligence/__init__.py +1103 -0
  39. algo_cli/intelligence/acrobat_config.py +307 -0
  40. algo_cli/intelligence/acrobat_manifests.py +338 -0
  41. algo_cli/intelligence/acrobat_models.py +195 -0
  42. algo_cli/intelligence/acrobat_pipeline.py +295 -0
  43. algo_cli/intelligence/acrobat_runtime.py +302 -0
  44. algo_cli/intelligence/acrobat_security.py +261 -0
  45. algo_cli/intelligence/acrobat_workflows.py +226 -0
  46. algo_cli/intelligence/actionability.py +165 -0
  47. algo_cli/intelligence/adversarial_audit.py +136 -0
  48. algo_cli/intelligence/agent_arena.py +92 -0
  49. algo_cli/intelligence/agent_benchmark.py +236 -0
  50. algo_cli/intelligence/agent_runtime.py +171 -0
  51. algo_cli/intelligence/agents_as_tools.py +70 -0
  52. algo_cli/intelligence/artifact_binding.py +80 -0
  53. algo_cli/intelligence/autonomous_engineer.py +1976 -0
  54. algo_cli/intelligence/backpressure.py +99 -0
  55. algo_cli/intelligence/bloom_filter.py +186 -0
  56. algo_cli/intelligence/bonferroni.py +66 -0
  57. algo_cli/intelligence/boundary_compaction.py +98 -0
  58. algo_cli/intelligence/catalog_verifier.py +172 -0
  59. algo_cli/intelligence/cavecrew.py +118 -0
  60. algo_cli/intelligence/changelog.py +176 -0
  61. algo_cli/intelligence/checkpoint_resume.py +92 -0
  62. algo_cli/intelligence/circuit_breaker.py +88 -0
  63. algo_cli/intelligence/clarification_gate.py +101 -0
  64. algo_cli/intelligence/code_graph.py +180 -0
  65. algo_cli/intelligence/coderank.py +97 -0
  66. algo_cli/intelligence/consistent_hash.py +150 -0
  67. algo_cli/intelligence/consortium_synthesis.py +139 -0
  68. algo_cli/intelligence/construction/__init__.py +241 -0
  69. algo_cli/intelligence/construction/common.py +273 -0
  70. algo_cli/intelligence/construction/documents.py +496 -0
  71. algo_cli/intelligence/construction/labor_units.py +1395 -0
  72. algo_cli/intelligence/construction/payments.py +470 -0
  73. algo_cli/intelligence/construction/risk.py +784 -0
  74. algo_cli/intelligence/content_extractor.py +132 -0
  75. algo_cli/intelligence/context_adaptive.py +102 -0
  76. algo_cli/intelligence/context_ops.py +95 -0
  77. algo_cli/intelligence/count_min.py +145 -0
  78. algo_cli/intelligence/cow_state.py +103 -0
  79. algo_cli/intelligence/critic_loop.py +119 -0
  80. algo_cli/intelligence/cross_source.py +113 -0
  81. algo_cli/intelligence/daemon_mode.py +99 -0
  82. algo_cli/intelligence/dag_orchestration.py +151 -0
  83. algo_cli/intelligence/deep_research.py +155 -0
  84. algo_cli/intelligence/degenerate_detector.py +78 -0
  85. algo_cli/intelligence/delta_report.py +92 -0
  86. algo_cli/intelligence/discovery_event_log.py +92 -0
  87. algo_cli/intelligence/document_ingest.py +298 -0
  88. algo_cli/intelligence/dual_layer_validate.py +151 -0
  89. algo_cli/intelligence/echo_fidelity.py +73 -0
  90. algo_cli/intelligence/ema_tuning.py +104 -0
  91. algo_cli/intelligence/event_log.py +92 -0
  92. algo_cli/intelligence/evidence_graph.py +114 -0
  93. algo_cli/intelligence/extension_host.py +162 -0
  94. algo_cli/intelligence/extension_manifest.py +115 -0
  95. algo_cli/intelligence/falsification_suite.py +178 -0
  96. algo_cli/intelligence/finance/__init__.py +169 -0
  97. algo_cli/intelligence/finance/anomalies.py +135 -0
  98. algo_cli/intelligence/finance/ap_ar.py +351 -0
  99. algo_cli/intelligence/finance/cash.py +162 -0
  100. algo_cli/intelligence/finance/close.py +332 -0
  101. algo_cli/intelligence/finance/common.py +244 -0
  102. algo_cli/intelligence/finance/construction.py +135 -0
  103. algo_cli/intelligence/finance/controls.py +172 -0
  104. algo_cli/intelligence/finance/evidence.py +119 -0
  105. algo_cli/intelligence/finance/exceptions.py +157 -0
  106. algo_cli/intelligence/finance/reconciliations.py +254 -0
  107. algo_cli/intelligence/finance/revenue.py +109 -0
  108. algo_cli/intelligence/finance/tax.py +74 -0
  109. algo_cli/intelligence/finance/workpapers.py +111 -0
  110. algo_cli/intelligence/finding_record.py +120 -0
  111. algo_cli/intelligence/flow_dag.py +267 -0
  112. algo_cli/intelligence/gatherer.py +223 -0
  113. algo_cli/intelligence/golden_master.py +98 -0
  114. algo_cli/intelligence/graph_rag.py +195 -0
  115. algo_cli/intelligence/group_chat.py +143 -0
  116. algo_cli/intelligence/hash_dedup.py +145 -0
  117. algo_cli/intelligence/hyperloglog.py +128 -0
  118. algo_cli/intelligence/incremental_index.py +316 -0
  119. algo_cli/intelligence/index_store.py +16 -0
  120. algo_cli/intelligence/iteration_plan.py +133 -0
  121. algo_cli/intelligence/kernel_plugins.py +167 -0
  122. algo_cli/intelligence/lesson_catalog.py +135 -0
  123. algo_cli/intelligence/llm_fallback.py +169 -0
  124. algo_cli/intelligence/log2_histogram.py +267 -0
  125. algo_cli/intelligence/lsp_integration.py +147 -0
  126. algo_cli/intelligence/memory_evolution.py +117 -0
  127. algo_cli/intelligence/minhash_lsh.py +182 -0
  128. algo_cli/intelligence/multi_model_score.py +174 -0
  129. algo_cli/intelligence/multi_tier_grade.py +211 -0
  130. algo_cli/intelligence/negative_controls.py +113 -0
  131. algo_cli/intelligence/numeric_clamp.py +63 -0
  132. algo_cli/intelligence/occ_editor.py +66 -0
  133. algo_cli/intelligence/output_normalize.py +112 -0
  134. algo_cli/intelligence/parallel_delegation.py +98 -0
  135. algo_cli/intelligence/parallel_fanout.py +104 -0
  136. algo_cli/intelligence/permission_modes.py +105 -0
  137. algo_cli/intelligence/pre_push_gate.py +68 -0
  138. algo_cli/intelligence/prefetch.py +171 -0
  139. algo_cli/intelligence/process_framework.py +217 -0
  140. algo_cli/intelligence/project_graph.py +387 -0
  141. algo_cli/intelligence/query_expansion.py +146 -0
  142. algo_cli/intelligence/ralph_loop.py +117 -0
  143. algo_cli/intelligence/rate_limiter.py +153 -0
  144. algo_cli/intelligence/refactor_transaction.py +94 -0
  145. algo_cli/intelligence/research_workspace.py +108 -0
  146. algo_cli/intelligence/retraction_ledger.py +72 -0
  147. algo_cli/intelligence/saga_pattern.py +88 -0
  148. algo_cli/intelligence/session_fork.py +100 -0
  149. algo_cli/intelligence/shadow_editor.py +67 -0
  150. algo_cli/intelligence/shell_session.py +213 -0
  151. algo_cli/intelligence/source_registry.py +143 -0
  152. algo_cli/intelligence/spawn_scales.py +99 -0
  153. algo_cli/intelligence/stat_stability.py +104 -0
  154. algo_cli/intelligence/structural_validator.py +148 -0
  155. algo_cli/intelligence/subagent_spawner.py +111 -0
  156. algo_cli/intelligence/symmetric_verify.py +70 -0
  157. algo_cli/intelligence/task_classifier.py +129 -0
  158. algo_cli/intelligence/team_execution.py +122 -0
  159. algo_cli/intelligence/tiered_access.py +121 -0
  160. algo_cli/intelligence/utility_registry.py +159 -0
  161. algo_cli/intuition_engine.py +560 -0
  162. algo_cli/intuition_injector.py +82 -0
  163. algo_cli/kernels/__init__.py +5 -0
  164. algo_cli/kernels/manifest.py +763 -0
  165. algo_cli/main.py +3903 -0
  166. algo_cli/memory_candidates.py +541 -0
  167. algo_cli/memory_echo_veil.py +394 -0
  168. algo_cli/memory_runtime.py +112 -0
  169. algo_cli/model_info.py +548 -0
  170. algo_cli/model_profile.py +160 -0
  171. algo_cli/model_routing.py +74 -0
  172. algo_cli/oneshot.py +331 -0
  173. algo_cli/perf_telemetry.py +389 -0
  174. algo_cli/plugins.py +245 -0
  175. algo_cli/private_event_store.py +654 -0
  176. algo_cli/quantization/__init__.py +24 -0
  177. algo_cli/quantization/lloyd_max.py +98 -0
  178. algo_cli/quantization/turbo_quant.py +308 -0
  179. algo_cli/reasoning/__init__.py +46 -0
  180. algo_cli/reasoning/combinatorial.py +356 -0
  181. algo_cli/reasoning/graph_of_thought.py +297 -0
  182. algo_cli/reasoning/mcts.py +220 -0
  183. algo_cli/reasoning/neuro_symbolic.py +250 -0
  184. algo_cli/reasoning/react.py +246 -0
  185. algo_cli/reasoning/reflexion.py +225 -0
  186. algo_cli/reasoning/tree_of_thought.py +241 -0
  187. algo_cli/reasoning_bridge.py +150 -0
  188. algo_cli/reconciliation.py +284 -0
  189. algo_cli/reflex.py +385 -0
  190. algo_cli/resources/docs/ALGO.md +13958 -0
  191. algo_cli/resources/docs/algo-cli-algorithm-evidence-contract.md +60 -0
  192. algo_cli/resources/docs/algo-cli-execution-verification-contract.md +59 -0
  193. algo_cli/resources/docs/algo-cli-memory-lifecycle-contract.md +72 -0
  194. algo_cli/resources/docs/harness-extension-cleanup-recommendation.md +41 -0
  195. algo_cli/resources/docs/index-compute-lab-integration.md +32 -0
  196. algo_cli/resources/docs/inference-harness-loop-blueprint-2026-06.md +55 -0
  197. algo_cli/resources/docs/main-split-map.md +35 -0
  198. algo_cli/resources/docs/privacy-and-context.md +48 -0
  199. algo_cli/resources/docs/reflex-loop-v0.2.md +354 -0
  200. algo_cli/resources/skills/README.md +26 -0
  201. algo_cli/resources/skills/algo-cli.md +59 -0
  202. algo_cli/resources/skills/edit-file-precision.md +49 -0
  203. algo_cli/resources/skills/harness-search-first.md +47 -0
  204. algo_cli/resources/skills/memory-recall-ritual.md +51 -0
  205. algo_cli/resources/skills/qol-algorithms.md +224 -0
  206. algo_cli/resources/skills/smart-error-recovery.md +56 -0
  207. algo_cli/resources/skills/tool-selection-cheatsheet.md +65 -0
  208. algo_cli/retrieval_algorithms.py +127 -0
  209. algo_cli/runtime_qos.py +236 -0
  210. algo_cli/runtime_services.py +320 -0
  211. algo_cli/session_commands.py +95 -0
  212. algo_cli/session_mode.py +113 -0
  213. algo_cli/skills.py +430 -0
  214. algo_cli/slash_dispatch.py +1265 -0
  215. algo_cli/small_context.py +206 -0
  216. algo_cli/spawn_budget.py +89 -0
  217. algo_cli/task_ledger.py +84 -0
  218. algo_cli/task_router.py +197 -0
  219. algo_cli/tool_context.py +94 -0
  220. algo_cli/tool_contract.py +99 -0
  221. algo_cli/tool_policy.py +357 -0
  222. algo_cli/tool_runtime.py +647 -0
  223. algo_cli/tools.py +3056 -0
  224. algo_cli/url_scheme.py +174 -0
  225. algo_cli/verify.py +154 -0
  226. algo_cli/version_manifest.py +178 -0
  227. algo_cli/vision_screenshot_verify.py +76 -0
  228. algo_cli/workspace_resolver.py +68 -0
  229. algo_cli/x_account.py +209 -0
  230. algo_cli/xai_auth.py +374 -0
  231. algo_cli/xai_client.py +600 -0
  232. algo_cli_runtime-0.14.0.dist-info/METADATA +369 -0
  233. algo_cli_runtime-0.14.0.dist-info/RECORD +237 -0
  234. algo_cli_runtime-0.14.0.dist-info/WHEEL +4 -0
  235. algo_cli_runtime-0.14.0.dist-info/entry_points.txt +3 -0
  236. algo_cli_runtime-0.14.0.dist-info/licenses/LICENSE +21 -0
  237. ollama_cli/__init__.py +67 -0
@@ -0,0 +1,407 @@
1
+ """Google Workspace REST client.
2
+
3
+ Read/write access to Drive, Docs, Sheets, and Calendar plus Gmail read/draft
4
+ access over HTTPS using only the standard library (urllib.request /
5
+ urllib.parse). Mirrors the zero deps posture of google_workspace_auth.py.
6
+
7
+ The client auto-refreshes access tokens via google_workspace_auth and raises
8
+ GoogleWorkspaceError on non-2xx responses. Methods return parsed JSON dicts
9
+ or raw bytes for media downloads; they never write to disk.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import base64
15
+ from email.message import EmailMessage
16
+ from email.utils import formatdate
17
+ import json
18
+ import urllib.error
19
+ import urllib.parse
20
+ import urllib.request
21
+ from typing import Any
22
+
23
+ from . import google_workspace_auth as auth
24
+
25
+ API_BASE = "https://www.googleapis.com"
26
+ DRIVE_BASE = f"{API_BASE}/drive/v3"
27
+ DOCS_BASE = f"{API_BASE}/documents/v1"
28
+ SHEETS_BASE = f"{API_BASE}/spreadsheets/v4"
29
+ CALENDAR_BASE = f"{API_BASE}/calendar/v3"
30
+ GMAIL_BASE = f"{API_BASE}/gmail/v1"
31
+
32
+
33
+ class GoogleWorkspaceError(RuntimeError):
34
+ """Raised on a non-2xx Google API response."""
35
+
36
+ def __init__(self, status: int, detail: str, *, body: dict[str, Any] | None = None):
37
+ super().__init__(f"Google API HTTP {status}: {detail}")
38
+ self.status = status
39
+ self.detail = detail
40
+ self.body = body or {}
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Client
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ class GoogleWorkspaceClient:
49
+ """Thin urllib wrapper around Google Workspace REST APIs."""
50
+
51
+ def __init__(self) -> None:
52
+ self.last_user_info: dict[str, Any] | None = None
53
+
54
+ # -- HTTP plumbing ----------------------------------------------------
55
+
56
+ def _request(
57
+ self,
58
+ method: str,
59
+ url: str,
60
+ *,
61
+ params: dict[str, Any] | None = None,
62
+ json_body: Any = None,
63
+ raw_body: bytes | None = None,
64
+ headers: dict[str, str] | None = None,
65
+ accept: str = "application/json",
66
+ timeout: float = 30.0,
67
+ ) -> tuple[int, bytes, dict[str, str]]:
68
+ if params:
69
+ # Drop empty values so we don't pollute URLs with `key=`.
70
+ cleaned = {k: v for k, v in params.items() if v not in (None, "")}
71
+ if cleaned:
72
+ url = f"{url}?{urllib.parse.urlencode(cleaned, doseq=True)}"
73
+ token = auth.get_valid_token()
74
+ if not token:
75
+ raise GoogleWorkspaceError(
76
+ 401,
77
+ "Not authenticated. Run /google-login to start the OAuth flow.",
78
+ )
79
+ req_headers: dict[str, str] = {
80
+ "Authorization": f"Bearer {token}",
81
+ "Accept": accept,
82
+ }
83
+ if headers:
84
+ req_headers.update(headers)
85
+ data: bytes | None = None
86
+ if json_body is not None:
87
+ data = json.dumps(json_body).encode("utf-8")
88
+ req_headers.setdefault("Content-Type", "application/json; charset=utf-8")
89
+ elif raw_body is not None:
90
+ data = raw_body
91
+ request = urllib.request.Request(url, data=data, method=method, headers=req_headers)
92
+ try:
93
+ with urllib.request.urlopen(request, timeout=timeout) as response:
94
+ return response.status, response.read(), dict(response.headers.items())
95
+ except urllib.error.HTTPError as exc:
96
+ body_bytes = exc.read() or b""
97
+ try:
98
+ body_obj = json.loads(body_bytes.decode("utf-8") or "{}")
99
+ except (UnicodeDecodeError, json.JSONDecodeError):
100
+ body_obj = None
101
+ detail = body_obj.get("error", {}).get("message") if isinstance(body_obj, dict) else None
102
+ raise GoogleWorkspaceError(
103
+ exc.code,
104
+ detail or body_bytes.decode("utf-8", errors="replace") or exc.reason,
105
+ body=body_obj if isinstance(body_obj, dict) else None,
106
+ ) from exc
107
+ except urllib.error.URLError as exc:
108
+ raise GoogleWorkspaceError(0, f"Network error: {exc.reason}") from exc
109
+
110
+ def _get_json(
111
+ self,
112
+ url: str,
113
+ *,
114
+ params: dict[str, Any] | None = None,
115
+ timeout: float = 30.0,
116
+ ) -> dict[str, Any]:
117
+ status, body, _ = self._request("GET", url, params=params, timeout=timeout)
118
+ if not (200 <= status < 300):
119
+ detail = body.decode("utf-8", errors="replace")
120
+ raise GoogleWorkspaceError(status, detail)
121
+ if not body:
122
+ return {}
123
+ try:
124
+ return json.loads(body.decode("utf-8"))
125
+ except json.JSONDecodeError as exc:
126
+ raise GoogleWorkspaceError(status, f"Non-JSON response: {body!r}") from exc
127
+
128
+ def _get_bytes(
129
+ self,
130
+ url: str,
131
+ *,
132
+ params: dict[str, Any] | None = None,
133
+ accept: str = "*/*",
134
+ timeout: float = 60.0,
135
+ ) -> tuple[bytes, dict[str, str]]:
136
+ status, body, headers = self._request(
137
+ "GET", url, params=params, accept=accept, timeout=timeout
138
+ )
139
+ if not (200 <= status < 300):
140
+ detail = body.decode("utf-8", errors="replace")
141
+ raise GoogleWorkspaceError(status, detail)
142
+ return body, headers
143
+
144
+ # -- Identity ---------------------------------------------------------
145
+
146
+ def user_info(self) -> dict[str, Any]:
147
+ info = self._get_json(auth.GOOGLE_USERINFO_URL)
148
+ self.last_user_info = info
149
+ return info
150
+
151
+ # -- Drive ------------------------------------------------------------
152
+
153
+ def drive_list(
154
+ self,
155
+ *,
156
+ query: str | None = None,
157
+ page_size: int = 20,
158
+ fields: str = "files(id,name,mimeType,size,modifiedTime,webViewLink),nextPageToken",
159
+ ) -> dict[str, Any]:
160
+ params: dict[str, Any] = {
161
+ "pageSize": max(1, min(int(page_size), 1000)),
162
+ "fields": fields,
163
+ }
164
+ if query:
165
+ params["q"] = query
166
+ return self._get_json(f"{DRIVE_BASE}/files", params=params)
167
+
168
+ def drive_get(
169
+ self,
170
+ file_id: str,
171
+ *,
172
+ fields: str = "id,name,mimeType,size,modifiedTime,createdTime,webViewLink,owners,parents,description",
173
+ ) -> dict[str, Any]:
174
+ return self._get_json(
175
+ f"{DRIVE_BASE}/files/{urllib.parse.quote(file_id, safe='')}",
176
+ params={"fields": fields, "supportsAllDrives": "true"},
177
+ )
178
+
179
+ def drive_search(
180
+ self,
181
+ name_contains: str,
182
+ *,
183
+ mime_type: str | None = None,
184
+ page_size: int = 20,
185
+ ) -> dict[str, Any]:
186
+ # Escape single quotes for the Drive query language.
187
+ safe = name_contains.replace("\\", "\\\\").replace("'", "\\'")
188
+ clauses = [f"name contains '{safe}'", "trashed = false"]
189
+ if mime_type:
190
+ clauses.append(f"mimeType = '{mime_type}'")
191
+ return self.drive_list(query=" and ".join(clauses), page_size=page_size)
192
+
193
+ def drive_download(self, file_id: str) -> tuple[bytes, dict[str, str]]:
194
+ return self._get_bytes(
195
+ f"{DRIVE_BASE}/files/{urllib.parse.quote(file_id, safe='')}",
196
+ params={"alt": "media", "supportsAllDrives": "true"},
197
+ )
198
+
199
+ def drive_export(
200
+ self,
201
+ file_id: str,
202
+ *,
203
+ mime_type: str = "text/plain",
204
+ ) -> tuple[bytes, dict[str, str]]:
205
+ return self._get_bytes(
206
+ f"{DRIVE_BASE}/files/{urllib.parse.quote(file_id, safe='')}/export",
207
+ params={"mimeType": mime_type},
208
+ )
209
+
210
+ # -- Docs -------------------------------------------------------------
211
+
212
+ def docs_get(self, document_id: str) -> dict[str, Any]:
213
+ return self._get_json(
214
+ f"{DOCS_BASE}/documents/{urllib.parse.quote(document_id, safe='')}"
215
+ )
216
+
217
+ def docs_to_plain_text(self, document: dict[str, Any]) -> str:
218
+ """Flatten a docs.get response into a plain-text body for display."""
219
+ body = document.get("body") or {}
220
+ chunks: list[str] = []
221
+
222
+ def walk(element: dict[str, Any]) -> None:
223
+ for child in element.get("content", []) or []:
224
+ kind = child.get("type")
225
+ if kind == "paragraph":
226
+ walk(child)
227
+ chunks.append("")
228
+ elif kind == "table":
229
+ for row in child.get("tableRows", []) or []:
230
+ for cell in row.get("tableCells", []) or []:
231
+ walk(cell)
232
+ chunks.append("")
233
+ elif kind == "textRun":
234
+ content = (child.get("textRun") or {}).get("content", "")
235
+ if content:
236
+ chunks.append(content)
237
+
238
+ walk(body)
239
+ return "\n".join(chunks).rstrip() + "\n"
240
+
241
+ # -- Sheets -----------------------------------------------------------
242
+
243
+ def sheets_values_get(
244
+ self,
245
+ spreadsheet_id: str,
246
+ range_a1: str,
247
+ ) -> dict[str, Any]:
248
+ return self._get_json(
249
+ f"{SHEETS_BASE}/spreadsheets/{urllib.parse.quote(spreadsheet_id, safe='')}"
250
+ f"/values/{urllib.parse.quote(range_a1, safe='')}"
251
+ )
252
+
253
+ # -- Calendar ---------------------------------------------------------
254
+
255
+ def calendar_events_list(
256
+ self,
257
+ *,
258
+ time_min: str | None = None,
259
+ time_max: str | None = None,
260
+ max_results: int = 20,
261
+ ) -> dict[str, Any]:
262
+ params: dict[str, Any] = {
263
+ "maxResults": max(1, min(int(max_results), 250)),
264
+ "singleEvents": "true",
265
+ "orderBy": "startTime",
266
+ }
267
+ if time_min:
268
+ params["timeMin"] = time_min
269
+ if time_max:
270
+ params["timeMax"] = time_max
271
+ return self._get_json(f"{CALENDAR_BASE}/calendars/primary/events", params=params)
272
+
273
+ # -- Gmail ------------------------------------------------------------
274
+
275
+ def gmail_list(
276
+ self,
277
+ *,
278
+ query: str | None = None,
279
+ max_results: int = 20,
280
+ ) -> dict[str, Any]:
281
+ params: dict[str, Any] = {"maxResults": max(1, min(int(max_results), 500))}
282
+ if query:
283
+ params["q"] = query
284
+ return self._get_json(f"{GMAIL_BASE}/users/me/messages", params=params)
285
+
286
+ def gmail_get(
287
+ self,
288
+ message_id: str,
289
+ *,
290
+ fmt: str = "metadata",
291
+ ) -> dict[str, Any]:
292
+ allowed = {"metadata", "full", "raw", "minimal"}
293
+ if fmt not in allowed:
294
+ raise ValueError(f"fmt must be one of {sorted(allowed)}")
295
+ return self._get_json(
296
+ f"{GMAIL_BASE}/users/me/messages/{urllib.parse.quote(message_id, safe='')}",
297
+ params={"format": fmt},
298
+ )
299
+
300
+ def gmail_create_draft(
301
+ self,
302
+ *,
303
+ to: str,
304
+ subject: str,
305
+ html_body: str | None = None,
306
+ text_body: str | None = None,
307
+ cc: str | None = None,
308
+ bcc: str | None = None,
309
+ ) -> dict[str, Any]:
310
+ """Create a Gmail draft. Does not send mail."""
311
+ if not to.strip():
312
+ raise ValueError("to is required")
313
+ if not subject.strip():
314
+ raise ValueError("subject is required")
315
+ body_text = text_body or ""
316
+ body_html = html_body or ""
317
+ if not body_text and not body_html:
318
+ raise ValueError("html_body or text_body is required")
319
+
320
+ msg = EmailMessage()
321
+ msg["To"] = to
322
+ msg["From"] = "me"
323
+ msg["Subject"] = subject
324
+ msg["Date"] = formatdate(localtime=True)
325
+ if cc:
326
+ msg["Cc"] = cc
327
+ if bcc:
328
+ msg["Bcc"] = bcc
329
+ if body_html:
330
+ msg.set_content(body_text or "This email contains an HTML body.")
331
+ msg.add_alternative(body_html, subtype="html")
332
+ else:
333
+ msg.set_content(body_text)
334
+ raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii")
335
+ status, body, _headers = self._request(
336
+ "POST",
337
+ f"{GMAIL_BASE}/users/me/drafts",
338
+ json_body={"message": {"raw": raw}},
339
+ timeout=60.0,
340
+ )
341
+ if not (200 <= status < 300):
342
+ detail = body.decode("utf-8", errors="replace")
343
+ raise GoogleWorkspaceError(status, detail)
344
+ return json.loads(body.decode("utf-8") or "{}")
345
+
346
+
347
+ # ---------------------------------------------------------------------------
348
+ # Convenience formatters for slash-command display
349
+ # ---------------------------------------------------------------------------
350
+
351
+
352
+ def format_drive_files(payload: dict[str, Any]) -> list[str]:
353
+ lines: list[str] = []
354
+ for item in payload.get("files", []) or []:
355
+ if not isinstance(item, dict):
356
+ continue
357
+ fid = item.get("id", "?")
358
+ name = item.get("name", "(unnamed)")
359
+ mime = item.get("mimeType", "")
360
+ size = item.get("size")
361
+ size_s = f" {int(size):,}B" if size else ""
362
+ lines.append(f" - {name} [muted]({mime}{size_s})[/] id={fid}")
363
+ if not lines:
364
+ lines.append(" (no files)")
365
+ return lines
366
+
367
+
368
+ def format_docs_plain_text(document: dict[str, Any], client: GoogleWorkspaceClient) -> str:
369
+ return client.docs_to_plain_text(document)
370
+
371
+
372
+ def format_sheet_values(payload: dict[str, Any]) -> str:
373
+ rows = payload.get("values", []) or []
374
+ if not rows:
375
+ return "(empty range)"
376
+ width = max(len(row) for row in rows)
377
+ out: list[str] = []
378
+ for row in rows:
379
+ padded = list(row) + [""] * (width - len(row))
380
+ out.append(" | ".join(str(cell) for cell in padded))
381
+ return "\n".join(out)
382
+
383
+
384
+ def format_calendar_events(payload: dict[str, Any]) -> list[str]:
385
+ lines: list[str] = []
386
+ for item in payload.get("items", []) or []:
387
+ if not isinstance(item, dict):
388
+ continue
389
+ summary = item.get("summary", "(no title)")
390
+ start = (item.get("start") or {}).get("dateTime") or (item.get("start") or {}).get("date")
391
+ end = (item.get("end") or {}).get("dateTime") or (item.get("end") or {}).get("date")
392
+ eid = item.get("id", "")
393
+ when = f"{start} → {end}" if start and end else (start or "?")
394
+ lines.append(f" - {summary} [muted]({when})[/] id={eid}")
395
+ if not lines:
396
+ lines.append(" (no events)")
397
+ return lines
398
+
399
+
400
+ def format_gmail_message(message: dict[str, Any]) -> str:
401
+ headers = message.get("payload", {}).get("headers", []) or []
402
+ by_name = {h.get("name", "").lower(): h.get("value", "") for h in headers if isinstance(h, dict)}
403
+ subject = by_name.get("subject", "(no subject)")
404
+ frm = by_name.get("from", "?")
405
+ date = by_name.get("date", "?")
406
+ snippet = message.get("snippet", "")
407
+ return f" Subject: {subject}\n From: {frm}\n Date: {date}\n ID: {message.get('id', '?')}\n Snippet: {snippet}"