@smilintux/skcapstone 0.4.6 → 0.10.0

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 (35) hide show
  1. package/docs/CUSTOM_AGENT.md +184 -0
  2. package/docs/GETTING_STARTED.md +3 -0
  3. package/launchd/com.skcapstone.daemon.plist +52 -0
  4. package/launchd/com.skcapstone.memory-compress.plist +45 -0
  5. package/launchd/com.skcapstone.skcomm-heartbeat.plist +33 -0
  6. package/launchd/com.skcapstone.skcomm-queue-drain.plist +34 -0
  7. package/launchd/install-launchd.sh +156 -0
  8. package/package.json +1 -1
  9. package/scripts/archive-sessions.sh +88 -0
  10. package/scripts/install.sh +39 -8
  11. package/scripts/notion-api.py +259 -0
  12. package/scripts/nvidia-proxy.mjs +856 -0
  13. package/scripts/proxy-monitor.sh +89 -0
  14. package/scripts/skgateway.mjs +856 -0
  15. package/scripts/telegram-catchup-all.sh +136 -0
  16. package/src/skcapstone/blueprints/builtins/itil-operations.yaml +40 -0
  17. package/src/skcapstone/cli/__init__.py +2 -0
  18. package/src/skcapstone/cli/daemon.py +116 -41
  19. package/src/skcapstone/cli/itil.py +434 -0
  20. package/src/skcapstone/consciousness_config.py +27 -0
  21. package/src/skcapstone/coordination.py +1 -0
  22. package/src/skcapstone/daemon.py +19 -11
  23. package/src/skcapstone/dreaming.py +761 -0
  24. package/src/skcapstone/fuse_mount.py +21 -13
  25. package/src/skcapstone/heartbeat.py +33 -29
  26. package/src/skcapstone/itil.py +1104 -0
  27. package/src/skcapstone/launchd.py +426 -0
  28. package/src/skcapstone/mcp_server.py +258 -0
  29. package/src/skcapstone/mcp_tools/__init__.py +2 -0
  30. package/src/skcapstone/mcp_tools/gtd_tools.py +1 -1
  31. package/src/skcapstone/mcp_tools/itil_tools.py +657 -0
  32. package/src/skcapstone/onboard.py +130 -10
  33. package/src/skcapstone/scheduled_tasks.py +107 -0
  34. package/src/skcapstone/service_health.py +81 -2
  35. package/src/skcapstone/systemd.py +17 -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()