@smilintux/skcapstone 0.9.0 → 0.12.5

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 (284) hide show
  1. package/.env.example +10 -4
  2. package/.github/workflows/ci.yml +2 -2
  3. package/.github/workflows/publish.yml +9 -2
  4. package/.openclaw-workspace.json +2 -2
  5. package/CLAUDE.md +37 -0
  6. package/MISSION.md +17 -2
  7. package/README.md +282 -3
  8. package/docker/Dockerfile +7 -7
  9. package/docker/compose-templates/dev-team.yml +12 -12
  10. package/docker/compose-templates/mini-team.yml +9 -9
  11. package/docker/compose-templates/ops-team.yml +10 -10
  12. package/docker/compose-templates/research-team.yml +10 -10
  13. package/docker/entrypoint.sh +4 -4
  14. package/docs/ADR-optional-integration-backbone.md +181 -0
  15. package/docs/ARCHITECTURE.md +186 -43
  16. package/docs/BOND_WITH_GROK.md +6 -6
  17. package/docs/CUSTOM_AGENT.md +278 -1
  18. package/docs/DREAMING.md +70 -0
  19. package/docs/GETTING_STARTED.md +10 -7
  20. package/docs/QUICKSTART.md +10 -6
  21. package/docs/SKJOULE_ARCHITECTURE.md +3 -3
  22. package/docs/SOUL_SWAPPER.md +5 -5
  23. package/docs/hammertime-audit.md +402 -0
  24. package/docs/sk-integration-HANDOFF.md +117 -0
  25. package/docs/skscheduler.md +155 -0
  26. package/docs/superpowers/examples/jobs.yaml +31 -0
  27. package/docs/superpowers/plans/2026-06-08-skscheduler.md +1265 -0
  28. package/docs/superpowers/specs/2026-06-08-skscheduler-design.md +186 -0
  29. package/examples/custom-bond-template.json +1 -1
  30. package/examples/grok-feb.json +1 -1
  31. package/examples/queen-ava-feb.json +1 -1
  32. package/launchd/com.skcapstone.daemon.plist +52 -0
  33. package/launchd/com.skcapstone.memory-compress.plist +45 -0
  34. package/launchd/com.skcapstone.skcomms-heartbeat.plist +33 -0
  35. package/launchd/com.skcapstone.skcomms-queue-drain.plist +34 -0
  36. package/launchd/install-launchd.sh +156 -0
  37. package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/index.ts +3 -2
  38. package/package.json +1 -1
  39. package/pyproject.toml +16 -10
  40. package/scripts/archive-sessions.sh +95 -0
  41. package/scripts/check-updates.py +4 -4
  42. package/scripts/install-bundle.sh +8 -8
  43. package/scripts/install.ps1 +12 -11
  44. package/scripts/install.sh +196 -11
  45. package/scripts/model-fallback-monitor.sh +102 -0
  46. package/scripts/notion-api.py +259 -0
  47. package/scripts/nvidia-proxy.mjs +908 -0
  48. package/scripts/proxy-monitor.sh +89 -0
  49. package/scripts/refresh-anthropic-token.sh +172 -0
  50. package/scripts/release.sh +98 -0
  51. package/scripts/session-to-memory.py +219 -0
  52. package/scripts/skgateway.mjs +856 -0
  53. package/scripts/telegram-catchup-all.sh +147 -0
  54. package/scripts/verify_install.sh +2 -2
  55. package/scripts/wargov-ufo-capture/README.md +43 -0
  56. package/scripts/wargov-ufo-capture/cdp_capture_release2.py +273 -0
  57. package/scripts/wargov-ufo-capture/cdp_capture_splc_doj.py +246 -0
  58. package/scripts/wargov-ufo-capture/cdp_finish.py +271 -0
  59. package/scripts/wargov-ufo-capture/cdp_probe.py +188 -0
  60. package/scripts/wargov-ufo-capture/cdp_splc_pressrelease.py +101 -0
  61. package/scripts/wargov-ufo-capture/parse_csv.py +95 -0
  62. package/scripts/wargov-ufo-capture/pull_dvids.sh +107 -0
  63. package/scripts/watch-anthropic-token.sh +212 -0
  64. package/scripts/windows/install-tasks.ps1 +7 -7
  65. package/scripts/windows/skcapstone-task.xml +1 -1
  66. package/src/skcapstone/__init__.py +45 -3
  67. package/src/skcapstone/_cli_monolith.py +20 -15
  68. package/src/skcapstone/activity.py +5 -1
  69. package/src/skcapstone/agent_card.py +3 -2
  70. package/src/skcapstone/api.py +41 -40
  71. package/src/skcapstone/auction.py +14 -11
  72. package/src/skcapstone/backup.py +2 -1
  73. package/src/skcapstone/blueprint_registry.py +4 -3
  74. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  75. package/src/skcapstone/brain_first.py +238 -0
  76. package/src/skcapstone/changelog.py +1 -1
  77. package/src/skcapstone/chat.py +22 -17
  78. package/src/skcapstone/cli/__init__.py +9 -1
  79. package/src/skcapstone/cli/_common.py +1 -0
  80. package/src/skcapstone/cli/agents_spawner.py +5 -2
  81. package/src/skcapstone/cli/alerts.py +25 -4
  82. package/src/skcapstone/cli/bench.py +15 -15
  83. package/src/skcapstone/cli/chat.py +7 -4
  84. package/src/skcapstone/cli/consciousness.py +5 -2
  85. package/src/skcapstone/cli/context_cmd.py +18 -4
  86. package/src/skcapstone/cli/daemon.py +121 -42
  87. package/src/skcapstone/cli/gtd.py +26 -1
  88. package/src/skcapstone/cli/housekeeping.py +3 -3
  89. package/src/skcapstone/cli/identity_cmd.py +378 -0
  90. package/src/skcapstone/cli/joule_cmd.py +7 -3
  91. package/src/skcapstone/cli/memory.py +8 -6
  92. package/src/skcapstone/cli/peers_dir.py +1 -1
  93. package/src/skcapstone/cli/register_cmd.py +29 -3
  94. package/src/skcapstone/cli/scheduler_cmd.py +167 -0
  95. package/src/skcapstone/cli/session.py +25 -0
  96. package/src/skcapstone/cli/setup.py +96 -29
  97. package/src/skcapstone/cli/shell_cmd.py +53 -1
  98. package/src/skcapstone/cli/skills_cmd.py +2 -2
  99. package/src/skcapstone/cli/soul.py +8 -5
  100. package/src/skcapstone/cli/status.py +37 -11
  101. package/src/skcapstone/cli/telegram.py +21 -0
  102. package/src/skcapstone/cli/test_cmd.py +5 -5
  103. package/src/skcapstone/cli/test_connection.py +2 -2
  104. package/src/skcapstone/cli/upgrade_cmd.py +23 -14
  105. package/src/skcapstone/cli/version_cmd.py +1 -1
  106. package/src/skcapstone/cli/watch_cmd.py +9 -6
  107. package/src/skcapstone/cloud9_bridge.py +14 -14
  108. package/src/skcapstone/codex_setup.py +255 -0
  109. package/src/skcapstone/config_validator.py +7 -4
  110. package/src/skcapstone/consciousness_config.py +5 -1
  111. package/src/skcapstone/consciousness_loop.py +313 -273
  112. package/src/skcapstone/context_loader.py +121 -0
  113. package/src/skcapstone/coord_federation.py +2 -1
  114. package/src/skcapstone/coordination.py +23 -6
  115. package/src/skcapstone/crush_integration.py +2 -1
  116. package/src/skcapstone/daemon.py +151 -88
  117. package/src/skcapstone/dashboard.py +10 -10
  118. package/src/skcapstone/data/sk-agent-picker.sh +421 -0
  119. package/src/skcapstone/data/systemd/skcapstone-api.socket +9 -0
  120. package/src/skcapstone/data/systemd/skcapstone-memory-compress.service +18 -0
  121. package/src/skcapstone/data/systemd/skcapstone-memory-compress.timer +11 -0
  122. package/src/skcapstone/data/systemd/skcapstone.service +37 -0
  123. package/src/skcapstone/data/systemd/skcapstone@.service +50 -0
  124. package/src/skcapstone/data/systemd/skcomms-heartbeat.service +18 -0
  125. package/{systemd/skcomm-heartbeat.timer → src/skcapstone/data/systemd/skcomms-heartbeat.timer} +2 -2
  126. package/src/skcapstone/data/systemd/skcomms-queue-drain.service +17 -0
  127. package/{systemd/skcomm-queue-drain.timer → src/skcapstone/data/systemd/skcomms-queue-drain.timer} +2 -2
  128. package/src/skcapstone/defaults/claude/CLAUDE.md +67 -0
  129. package/src/skcapstone/defaults/claude/settings.json +74 -0
  130. package/src/skcapstone/defaults/lumina/config/claude-hooks.md +57 -0
  131. package/src/skcapstone/defaults/lumina/config/skgraph.yaml +55 -10
  132. package/src/skcapstone/defaults/lumina/config/skmemory.yaml +79 -13
  133. package/src/skcapstone/defaults/lumina/config/skvector.yaml +60 -9
  134. package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +2 -2
  135. package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
  136. package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
  137. package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +2 -2
  138. package/src/skcapstone/defaults/unhinged.json +13 -0
  139. package/src/skcapstone/discovery.py +43 -20
  140. package/src/skcapstone/doctor.py +941 -22
  141. package/src/skcapstone/dreaming.py +1183 -109
  142. package/src/skcapstone/emotion_tracker.py +2 -2
  143. package/src/skcapstone/export.py +4 -3
  144. package/src/skcapstone/fuse_mount.py +35 -25
  145. package/src/skcapstone/gui_installer.py +2 -2
  146. package/src/skcapstone/heartbeat.py +34 -30
  147. package/src/skcapstone/housekeeping.py +14 -14
  148. package/src/skcapstone/install_wizard.py +209 -7
  149. package/src/skcapstone/itil.py +13 -4
  150. package/src/skcapstone/kms_scheduler.py +10 -8
  151. package/src/skcapstone/launchd.py +426 -0
  152. package/src/skcapstone/mcp_launcher.py +15 -1
  153. package/src/skcapstone/mcp_server.py +341 -49
  154. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  155. package/src/skcapstone/mcp_tools/_helpers.py +2 -2
  156. package/src/skcapstone/mcp_tools/ansible_tools.py +7 -4
  157. package/src/skcapstone/mcp_tools/brain_first_tools.py +90 -0
  158. package/src/skcapstone/mcp_tools/capauth_tools.py +7 -4
  159. package/src/skcapstone/mcp_tools/comm_tools.py +10 -10
  160. package/src/skcapstone/mcp_tools/coord_tools.py +8 -4
  161. package/src/skcapstone/mcp_tools/did_tools.py +11 -8
  162. package/src/skcapstone/mcp_tools/gtd_tools.py +4 -4
  163. package/src/skcapstone/mcp_tools/memory_tools.py +6 -2
  164. package/src/skcapstone/mcp_tools/notification_tools.py +22 -6
  165. package/src/skcapstone/mcp_tools/{skcomm_tools.py → skcomms_tools.py} +14 -14
  166. package/src/skcapstone/mcp_tools/soul_tools.py +8 -2
  167. package/src/skcapstone/mdns_discovery.py +2 -2
  168. package/src/skcapstone/memory_curator.py +1 -1
  169. package/src/skcapstone/memory_engine.py +10 -3
  170. package/src/skcapstone/metrics.py +30 -16
  171. package/src/skcapstone/migrate_memories.py +4 -3
  172. package/src/skcapstone/migrate_multi_agent.py +8 -7
  173. package/src/skcapstone/models.py +47 -5
  174. package/src/skcapstone/notifications.py +42 -18
  175. package/src/skcapstone/onboard.py +1000 -126
  176. package/src/skcapstone/operator_link.py +170 -0
  177. package/src/skcapstone/peer_directory.py +4 -4
  178. package/src/skcapstone/peers.py +19 -19
  179. package/src/skcapstone/pillars/__init__.py +7 -5
  180. package/src/skcapstone/pillars/consciousness.py +191 -0
  181. package/src/skcapstone/pillars/identity.py +51 -7
  182. package/src/skcapstone/pillars/memory.py +9 -3
  183. package/src/skcapstone/pillars/sync.py +2 -2
  184. package/src/skcapstone/preflight.py +3 -3
  185. package/src/skcapstone/providers/docker.py +28 -28
  186. package/src/skcapstone/register.py +6 -6
  187. package/src/skcapstone/registry_client.py +5 -4
  188. package/src/skcapstone/runtime.py +14 -3
  189. package/src/skcapstone/scheduled_tasks.py +254 -19
  190. package/src/skcapstone/scheduler_jobs.py +456 -0
  191. package/src/skcapstone/scheduler_runner.py +239 -0
  192. package/src/skcapstone/scheduler_state.py +162 -0
  193. package/src/skcapstone/sdk.py +310 -0
  194. package/src/skcapstone/service_health.py +279 -39
  195. package/src/skcapstone/session_briefing.py +108 -0
  196. package/src/skcapstone/session_capture.py +1 -1
  197. package/src/skcapstone/shell.py +7 -1
  198. package/src/skcapstone/soul.py +3 -1
  199. package/src/skcapstone/soul_switch.py +3 -1
  200. package/src/skcapstone/summary.py +6 -6
  201. package/src/skcapstone/sync_engine.py +15 -15
  202. package/src/skcapstone/sync_watcher.py +2 -2
  203. package/src/skcapstone/systemd.py +72 -21
  204. package/src/skcapstone/team_comms.py +8 -8
  205. package/src/skcapstone/team_engine.py +1 -1
  206. package/src/skcapstone/testrunner.py +3 -3
  207. package/src/skcapstone/trust_graph.py +40 -5
  208. package/src/skcapstone/unified_search.py +15 -6
  209. package/src/skcapstone/uninstall_wizard.py +11 -3
  210. package/src/skcapstone/version_check.py +8 -4
  211. package/src/skcapstone/warmth_anchor.py +4 -2
  212. package/src/skcapstone/whoami.py +4 -4
  213. package/systemd/skcapstone.service +4 -6
  214. package/systemd/skcapstone@.service +7 -8
  215. package/systemd/skcomms-heartbeat.service +21 -0
  216. package/systemd/skcomms-heartbeat.timer +12 -0
  217. package/systemd/skcomms-queue-drain.service +17 -0
  218. package/systemd/skcomms-queue-drain.timer +12 -0
  219. package/tests/conftest.py +39 -0
  220. package/tests/integration/test_consciousness_e2e.py +39 -39
  221. package/tests/test_agent_card.py +1 -1
  222. package/tests/test_agent_home_scaffold.py +34 -0
  223. package/tests/test_alerts_consumer_topics.py +27 -0
  224. package/tests/test_backup.py +2 -1
  225. package/tests/test_chat.py +6 -6
  226. package/tests/test_claude_md.py +2 -2
  227. package/tests/test_cli_skills.py +10 -10
  228. package/tests/test_cli_test_cmd.py +4 -4
  229. package/tests/test_cli_test_connection.py +1 -1
  230. package/tests/test_cloud9_bridge.py +6 -6
  231. package/tests/test_consciousness_e2e.py +1 -1
  232. package/tests/test_consciousness_loop.py +10 -10
  233. package/tests/test_coordination.py +25 -0
  234. package/tests/test_cross_package.py +21 -21
  235. package/tests/test_daemon.py +4 -4
  236. package/tests/test_daemon_shutdown.py +1 -1
  237. package/tests/test_docker_provider.py +29 -29
  238. package/tests/test_doctor.py +400 -0
  239. package/tests/test_doctor_skscheduler.py +50 -0
  240. package/tests/test_dreaming_engine.py +147 -0
  241. package/tests/test_dreaming_gtd_capture.py +35 -0
  242. package/tests/test_e2e_automated.py +8 -5
  243. package/tests/test_fuse_mount.py +10 -10
  244. package/tests/test_gtd_brief.py +46 -0
  245. package/tests/test_gtd_malformed_tolerance.py +31 -0
  246. package/tests/test_housekeeping.py +15 -15
  247. package/tests/test_identity_migrate.py +251 -0
  248. package/tests/test_integration_backbone.py +598 -0
  249. package/tests/test_itil_gtd_lifecycle.py +37 -0
  250. package/tests/test_jobs_dropins.py +84 -0
  251. package/tests/test_mcp_server.py +82 -37
  252. package/tests/test_models.py +48 -4
  253. package/tests/test_multi_agent.py +31 -29
  254. package/tests/test_notifications.py +122 -32
  255. package/tests/test_onboard.py +63 -75
  256. package/tests/test_operator_link.py +78 -0
  257. package/tests/test_peers.py +14 -14
  258. package/tests/test_pillars.py +98 -0
  259. package/tests/test_preflight.py +3 -3
  260. package/tests/test_runtime.py +21 -0
  261. package/tests/test_scheduled_tasks.py +11 -6
  262. package/tests/test_scheduler_cli.py +47 -0
  263. package/tests/test_scheduler_features.py +133 -0
  264. package/tests/test_scheduler_integration.py +87 -0
  265. package/tests/test_scheduler_jobs.py +155 -0
  266. package/tests/test_scheduler_runner.py +64 -0
  267. package/tests/test_scheduler_state.py +57 -0
  268. package/tests/test_sdk.py +70 -0
  269. package/tests/test_service_health_incidents.py +34 -0
  270. package/tests/test_service_registry.py +52 -0
  271. package/tests/test_session_briefing.py +130 -0
  272. package/tests/test_snapshots.py +4 -4
  273. package/tests/test_sync_pipeline.py +26 -26
  274. package/tests/test_team_comms.py +2 -2
  275. package/tests/test_testrunner.py +2 -2
  276. package/tests/test_trust_graph.py +18 -0
  277. package/tests/test_unified_search.py +2 -2
  278. package/tests/test_version_check.py +10 -0
  279. package/tests/test_version_cmd.py +8 -8
  280. package/tests/test_whoami.py +1 -1
  281. package/systemd/skcomm-heartbeat.service +0 -18
  282. package/systemd/skcomm-queue-drain.service +0 -17
  283. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/package.json +0 -0
  284. /package/{openclaw-plugin → openclaw-plugin.archived-2026-04-23}/src/openclaw.plugin.json +0 -0
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ notion-api — Simple Notion API wrapper for AI agents.
4
+
5
+ Lets agents read and update Notion pages via CLI without needing
6
+ to construct raw curl/JSON. Uses NOTION_API_KEY from environment.
7
+
8
+ Usage:
9
+ notion-api read <page-id>
10
+ notion-api append <page-id> <markdown-text>
11
+ notion-api replace <page-id> <markdown-text>
12
+ notion-api add-todo <page-id> <text> [--checked]
13
+ notion-api list-blocks <page-id>
14
+
15
+ Examples:
16
+ notion-api read 31e2be82-a3a1-8178-820c-e6eeb11b15c1
17
+ notion-api append 31e2be82-a3a1-8178-820c-e6eeb11b15c1 "## New Section\nContent here"
18
+ notion-api add-todo 31e2be82-a3a1-8178-820c-e6eeb11b15c1 "Follow up with John"
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import os
25
+ import sys
26
+ import urllib.request
27
+ import urllib.error
28
+ from typing import Any
29
+
30
+ API_KEY = os.environ.get("NOTION_API_KEY", "")
31
+ API_VERSION = "2022-06-28"
32
+ BASE_URL = "https://api.notion.com/v1"
33
+
34
+
35
+ def _request(method: str, path: str, body: dict | None = None) -> dict:
36
+ """Make an authenticated Notion API request."""
37
+ if not API_KEY:
38
+ print("Error: NOTION_API_KEY not set in environment", file=sys.stderr)
39
+ sys.exit(1)
40
+
41
+ url = f"{BASE_URL}{path}"
42
+ data = json.dumps(body).encode() if body else None
43
+ req = urllib.request.Request(url, data=data, method=method)
44
+ req.add_header("Authorization", f"Bearer {API_KEY}")
45
+ req.add_header("Notion-Version", API_VERSION)
46
+ if data:
47
+ req.add_header("Content-Type", "application/json")
48
+
49
+ try:
50
+ resp = urllib.request.urlopen(req)
51
+ return json.loads(resp.read())
52
+ except urllib.error.HTTPError as e:
53
+ err = e.read().decode()
54
+ print(f"Notion API error {e.code}: {err}", file=sys.stderr)
55
+ sys.exit(1)
56
+
57
+
58
+ def _rich_text(text: str, bold: bool = False) -> list[dict]:
59
+ """Create a rich_text array from plain text."""
60
+ rt: dict[str, Any] = {"type": "text", "text": {"content": text}}
61
+ if bold:
62
+ rt["annotations"] = {"bold": True}
63
+ return [rt]
64
+
65
+
66
+ def _md_to_blocks(markdown: str) -> list[dict]:
67
+ """Convert simple markdown to Notion blocks.
68
+
69
+ Supports: ## headings, - bullets, [ ] todos, [x] todos, plain paragraphs.
70
+ """
71
+ blocks: list[dict] = []
72
+ for line in markdown.strip().split("\n"):
73
+ stripped = line.strip()
74
+ if not stripped:
75
+ continue
76
+
77
+ if stripped.startswith("### "):
78
+ blocks.append({
79
+ "object": "block",
80
+ "type": "heading_3",
81
+ "heading_3": {"rich_text": _rich_text(stripped[4:])}
82
+ })
83
+ elif stripped.startswith("## "):
84
+ blocks.append({
85
+ "object": "block",
86
+ "type": "heading_2",
87
+ "heading_2": {"rich_text": _rich_text(stripped[3:])}
88
+ })
89
+ elif stripped.startswith("# "):
90
+ blocks.append({
91
+ "object": "block",
92
+ "type": "heading_1",
93
+ "heading_1": {"rich_text": _rich_text(stripped[2:])}
94
+ })
95
+ elif stripped.startswith("- [x] ") or stripped.startswith("- [X] "):
96
+ blocks.append({
97
+ "object": "block",
98
+ "type": "to_do",
99
+ "to_do": {"rich_text": _rich_text(stripped[6:]), "checked": True}
100
+ })
101
+ elif stripped.startswith("- [ ] "):
102
+ blocks.append({
103
+ "object": "block",
104
+ "type": "to_do",
105
+ "to_do": {"rich_text": _rich_text(stripped[6:]), "checked": False}
106
+ })
107
+ elif stripped.startswith("- ") or stripped.startswith("* "):
108
+ blocks.append({
109
+ "object": "block",
110
+ "type": "bulleted_list_item",
111
+ "bulleted_list_item": {"rich_text": _rich_text(stripped[2:])}
112
+ })
113
+ elif stripped.startswith("---"):
114
+ blocks.append({"object": "block", "type": "divider", "divider": {}})
115
+ else:
116
+ blocks.append({
117
+ "object": "block",
118
+ "type": "paragraph",
119
+ "paragraph": {"rich_text": _rich_text(stripped)}
120
+ })
121
+
122
+ return blocks
123
+
124
+
125
+ def cmd_read(page_id: str) -> None:
126
+ """Read a page's properties and content."""
127
+ page = _request("GET", f"/pages/{page_id}")
128
+ title = ""
129
+ for prop in page.get("properties", {}).values():
130
+ if prop.get("type") == "title":
131
+ title = "".join(t.get("plain_text", "") for t in prop.get("title", []))
132
+ break
133
+
134
+ print(f"Page: {title}")
135
+ print(f"URL: {page.get('url', '')}")
136
+ print(f"Last edited: {page.get('last_edited_time', '')}")
137
+ print()
138
+
139
+ # Get blocks
140
+ blocks = _request("GET", f"/blocks/{page_id}/children?page_size=100")
141
+ for block in blocks.get("results", []):
142
+ btype = block["type"]
143
+ if btype == "divider":
144
+ print("---")
145
+ elif btype == "child_database":
146
+ print(f"[Database: {block['child_database'].get('title', '')}]")
147
+ else:
148
+ content = block.get(btype, {})
149
+ rt = content.get("rich_text", [])
150
+ text = "".join(t.get("plain_text", "") for t in rt)
151
+ prefix = ""
152
+ if btype == "heading_1":
153
+ prefix = "# "
154
+ elif btype == "heading_2":
155
+ prefix = "## "
156
+ elif btype == "heading_3":
157
+ prefix = "### "
158
+ elif btype == "bulleted_list_item":
159
+ prefix = "- "
160
+ elif btype == "numbered_list_item":
161
+ prefix = "1. "
162
+ elif btype == "to_do":
163
+ checked = content.get("checked", False)
164
+ prefix = "[x] " if checked else "[ ] "
165
+ elif btype == "callout":
166
+ icon = content.get("icon", {}).get("emoji", "")
167
+ prefix = f"{icon} " if icon else "> "
168
+ print(f"{prefix}{text}")
169
+
170
+
171
+ def cmd_list_blocks(page_id: str) -> None:
172
+ """List all block IDs and types on a page."""
173
+ blocks = _request("GET", f"/blocks/{page_id}/children?page_size=100")
174
+ for block in blocks.get("results", []):
175
+ btype = block["type"]
176
+ rt = block.get(btype, {}).get("rich_text", [])
177
+ preview = "".join(t.get("plain_text", "") for t in rt)[:60]
178
+ print(f"{block['id']} {btype:20s} {preview}")
179
+
180
+
181
+ def cmd_append(page_id: str, markdown: str) -> None:
182
+ """Append markdown content as new blocks to a page."""
183
+ blocks = _md_to_blocks(markdown)
184
+ if not blocks:
185
+ print("No content to append.")
186
+ return
187
+
188
+ result = _request("PATCH", f"/blocks/{page_id}/children", {"children": blocks})
189
+ count = len(result.get("results", []))
190
+ print(f"Appended {count} blocks to page.")
191
+
192
+
193
+ def cmd_replace(page_id: str, markdown: str) -> None:
194
+ """Replace all content on a page with new markdown."""
195
+ # Delete existing blocks
196
+ existing = _request("GET", f"/blocks/{page_id}/children?page_size=100")
197
+ for block in existing.get("results", []):
198
+ if block["type"] != "child_database": # Don't delete databases
199
+ try:
200
+ _request("DELETE", f"/blocks/{block['id']}")
201
+ except SystemExit:
202
+ pass # Skip blocks that can't be deleted
203
+
204
+ # Append new content
205
+ cmd_append(page_id, markdown)
206
+
207
+
208
+ def cmd_add_todo(page_id: str, text: str, checked: bool = False) -> None:
209
+ """Add a single todo item to a page."""
210
+ blocks = [{
211
+ "object": "block",
212
+ "type": "to_do",
213
+ "to_do": {"rich_text": _rich_text(text), "checked": checked}
214
+ }]
215
+ result = _request("PATCH", f"/blocks/{page_id}/children", {"children": blocks})
216
+ status = "checked" if checked else "unchecked"
217
+ print(f"Added todo ({status}): {text}")
218
+
219
+
220
+ def main() -> None:
221
+ if len(sys.argv) < 3:
222
+ print(__doc__)
223
+ sys.exit(1)
224
+
225
+ cmd = sys.argv[1]
226
+ page_id = sys.argv[2]
227
+
228
+ if cmd == "read":
229
+ cmd_read(page_id)
230
+ elif cmd == "list-blocks":
231
+ cmd_list_blocks(page_id)
232
+ elif cmd == "append":
233
+ if len(sys.argv) < 4:
234
+ # Read from stdin
235
+ text = sys.stdin.read()
236
+ else:
237
+ text = sys.argv[3]
238
+ cmd_append(page_id, text)
239
+ elif cmd == "replace":
240
+ if len(sys.argv) < 4:
241
+ text = sys.stdin.read()
242
+ else:
243
+ text = sys.argv[3]
244
+ cmd_replace(page_id, text)
245
+ elif cmd == "add-todo":
246
+ if len(sys.argv) < 4:
247
+ print("Usage: notion-api add-todo <page-id> <text> [--checked]")
248
+ sys.exit(1)
249
+ text = sys.argv[3]
250
+ checked = "--checked" in sys.argv
251
+ cmd_add_todo(page_id, text, checked)
252
+ else:
253
+ print(f"Unknown command: {cmd}")
254
+ print(__doc__)
255
+ sys.exit(1)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ main()