@trac3er/oh-my-god 2.0.0 → 2.0.2

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 (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any
8
+
9
+ _MCP_IMPORT_ERROR: ModuleNotFoundError | None = None
10
+
11
+ try:
12
+ from fastmcp import FastMCP
13
+ from starlette.requests import Request
14
+ from starlette.responses import JSONResponse
15
+ except ModuleNotFoundError as exc:
16
+ _MCP_IMPORT_ERROR = exc
17
+ Request = Any
18
+
19
+ class JSONResponse(dict):
20
+ def __init__(self, content: dict[str, Any]):
21
+ super().__init__(content)
22
+
23
+ def _passthrough_decorator(*_args: Any, **_kwargs: Any):
24
+ def decorator(func: Any) -> Any:
25
+ return func
26
+
27
+ return decorator
28
+
29
+ class FastMCP: # type: ignore[override]
30
+ def __init__(self, *_args: Any, **_kwargs: Any) -> None:
31
+ self._import_error = _MCP_IMPORT_ERROR
32
+
33
+ custom_route = staticmethod(_passthrough_decorator)
34
+ tool = staticmethod(_passthrough_decorator)
35
+ resource = staticmethod(_passthrough_decorator)
36
+
37
+ def run(self, *_args: Any, **_kwargs: Any) -> None:
38
+ raise RuntimeError("fastmcp and starlette are required to run the OMG memory server") from self._import_error
39
+
40
+ FastMCP.__module__ = "fastmcp"
41
+
42
+ from runtime.memory_store import MemoryStore, MemoryStoreFullError
43
+
44
+ _store = MemoryStore()
45
+
46
+
47
+ def _load_state() -> None:
48
+ return None
49
+
50
+
51
+ def _save_state() -> None:
52
+ return None
53
+
54
+
55
+ @asynccontextmanager
56
+ async def lifespan(_: object) -> AsyncIterator[None]:
57
+ _load_state()
58
+ try:
59
+ yield
60
+ finally:
61
+ _save_state()
62
+
63
+
64
+ mcp = FastMCP("OMG Memory Server", lifespan=lifespan)
65
+
66
+
67
+ @mcp.custom_route("/health", methods=["GET"])
68
+ async def health(_: Request) -> JSONResponse:
69
+ return JSONResponse({"status": "ok", "version": "1.0.0"})
70
+
71
+
72
+ @mcp.tool()
73
+ def memory_store(
74
+ key: str,
75
+ content: str,
76
+ source_cli: str,
77
+ tags: list[str] | None = None,
78
+ ) -> dict[str, Any]:
79
+ try:
80
+ return _store.add(key=key, content=content, source_cli=source_cli, tags=tags)
81
+ except MemoryStoreFullError as exc:
82
+ return {"error": str(exc)}
83
+
84
+
85
+ @mcp.tool()
86
+ def memory_search(
87
+ query: str,
88
+ source_cli: str | None = None,
89
+ ) -> list[dict[str, Any]]:
90
+ return _store.search(query=query, source_cli=source_cli)
91
+
92
+
93
+ @mcp.tool()
94
+ def memory_list(
95
+ source_cli: str | None = None,
96
+ ) -> list[dict[str, Any]]:
97
+ return _store.list_all(source_cli=source_cli)
98
+
99
+
100
+ @mcp.tool()
101
+ def memory_delete(item_id: str) -> dict[str, Any]:
102
+ deleted = _store.delete(item_id)
103
+ return {"deleted": deleted, "id": item_id}
104
+
105
+
106
+ @mcp.tool()
107
+ def memory_import(items: list[dict[str, Any]]) -> dict[str, int]:
108
+ count = _store.import_items(items)
109
+ return {"imported": count}
110
+
111
+
112
+ @mcp.tool()
113
+ def memory_export() -> list[dict[str, Any]]:
114
+ return _store.export_all()
115
+
116
+
117
+ @mcp.resource("memory://all")
118
+ def memory_all_resource() -> str:
119
+ return json.dumps(_store.list_all())
120
+
121
+
122
+ def get_host() -> str:
123
+ return os.environ.get("OMG_MEMORY_HOST", "127.0.0.1")
124
+
125
+
126
+ def get_port() -> int:
127
+ return int(os.environ.get("OMG_MEMORY_PORT", "8765"))
128
+
129
+
130
+ def run_server() -> None:
131
+ mcp.run(transport="http", host=get_host(), port=get_port())
132
+
133
+
134
+ if __name__ == "__main__":
135
+ run_server()
File without changes
@@ -0,0 +1,257 @@
1
+ """ChatGPT conversations.json parser for OMG shared memory import.
2
+
3
+ Parses the ChatGPT data export format (``conversations.json``) and converts
4
+ conversations into OMG memory items compatible with :class:`MemoryStore`.
5
+
6
+ Functions:
7
+ extract_linear_conversation — traverse mapping tree to ordered message list
8
+ parse_conversations_file — read + parse the export JSON file
9
+ conversations_to_memory_items — convert parsed conversations to memory items
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import logging
16
+ import uuid
17
+ import warnings
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Maximum traversal depth to guard against circular parent references.
24
+ _MAX_DEPTH = 10_000
25
+
26
+ # Maximum content length per memory item (chars).
27
+ _MAX_CONTENT_LENGTH = 5_000
28
+
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # extract_linear_conversation
32
+ # ---------------------------------------------------------------------------
33
+
34
+
35
+ def extract_linear_conversation(
36
+ mapping: dict[str, Any],
37
+ current_node: str,
38
+ ) -> list[dict[str, Any]]:
39
+ """Walk *current_node* back to root via ``parent`` pointers.
40
+
41
+ Returns a chronologically ordered list (root -> leaf) of message dicts::
42
+
43
+ {"role": str, "content": str, "timestamp": float | None}
44
+
45
+ * Skips nodes whose ``message`` is ``None`` or has empty text.
46
+ * Skips ``system`` role messages.
47
+ * Guards against circular references with a depth limit of 10 000.
48
+ """
49
+ if current_node not in mapping:
50
+ return []
51
+
52
+ # Collect nodes from current_node back to root.
53
+ chain: list[dict[str, Any]] = []
54
+ visited: set[str] = set()
55
+ node_id: str | None = current_node
56
+
57
+ depth = 0
58
+ while node_id is not None and depth < _MAX_DEPTH:
59
+ if node_id in visited:
60
+ break
61
+ visited.add(node_id)
62
+
63
+ node = mapping.get(node_id)
64
+ if node is None:
65
+ break
66
+
67
+ chain.append(node)
68
+ node_id = node.get("parent")
69
+ depth += 1
70
+
71
+ # Reverse so root comes first (chronological order).
72
+ chain.reverse()
73
+
74
+ messages: list[dict[str, Any]] = []
75
+ for node in chain:
76
+ msg = node.get("message")
77
+ if msg is None:
78
+ continue
79
+
80
+ author = msg.get("author") or {}
81
+ role = author.get("role", "")
82
+
83
+ # Skip system messages.
84
+ if role == "system":
85
+ continue
86
+
87
+ content_obj = msg.get("content") or {}
88
+ parts = content_obj.get("parts") or []
89
+ text = "".join(str(p) for p in parts).strip()
90
+
91
+ # Skip empty content.
92
+ if not text:
93
+ continue
94
+
95
+ timestamp = msg.get("create_time")
96
+
97
+ messages.append(
98
+ {
99
+ "role": role,
100
+ "content": text,
101
+ "timestamp": timestamp,
102
+ }
103
+ )
104
+
105
+ return messages
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # parse_conversations_file
110
+ # ---------------------------------------------------------------------------
111
+
112
+
113
+ def parse_conversations_file(file_path: str) -> list[dict[str, Any]]:
114
+ """Read and parse a ChatGPT ``conversations.json`` export file.
115
+
116
+ For each conversation object, calls :func:`extract_linear_conversation`
117
+ to obtain an ordered message list.
118
+
119
+ Returns a list of conversation dicts::
120
+
121
+ {
122
+ "id": str,
123
+ "title": str,
124
+ "messages": list[dict],
125
+ "create_time": float | None,
126
+ "source": "chatgpt",
127
+ }
128
+
129
+ Gracefully handles:
130
+ * File not found -> empty list
131
+ * Invalid JSON -> empty list
132
+ * Malformed individual conversations -> logged warning, skipped
133
+ """
134
+ path = Path(file_path)
135
+ if not path.exists():
136
+ logger.warning("Conversations file not found: %s", file_path)
137
+ return []
138
+
139
+ try:
140
+ raw_text = path.read_text(encoding="utf-8")
141
+ except OSError as exc:
142
+ logger.warning("Failed to read conversations file: %s", exc)
143
+ return []
144
+
145
+ # Warn about potentially large files.
146
+ file_size = path.stat().st_size
147
+ if file_size > 50 * 1024 * 1024: # 50 MB
148
+ warnings.warn(
149
+ f"Large conversations file ({file_size / 1024 / 1024:.1f} MB). "
150
+ "Consider using streaming JSON parser (ijson) for better memory usage.",
151
+ ResourceWarning,
152
+ stacklevel=2,
153
+ )
154
+
155
+ try:
156
+ data = json.loads(raw_text)
157
+ except json.JSONDecodeError as exc:
158
+ logger.warning("Invalid JSON in conversations file: %s", exc)
159
+ return []
160
+
161
+ if not isinstance(data, list):
162
+ logger.warning("Expected JSON array, got %s", type(data).__name__)
163
+ return []
164
+
165
+ results: list[dict[str, Any]] = []
166
+
167
+ for conv in data:
168
+ try:
169
+ conv_id = conv["id"]
170
+ title = conv.get("title", "Untitled")
171
+ mapping = conv["mapping"]
172
+ current_node = conv["current_node"]
173
+ create_time = conv.get("create_time")
174
+
175
+ messages = extract_linear_conversation(mapping, current_node)
176
+
177
+ results.append(
178
+ {
179
+ "id": conv_id,
180
+ "title": title,
181
+ "messages": messages,
182
+ "create_time": create_time,
183
+ "source": "chatgpt",
184
+ }
185
+ )
186
+ except (KeyError, TypeError) as exc:
187
+ conv_id_str = conv.get("id", "<unknown>") if isinstance(conv, dict) else "<invalid>"
188
+ logger.warning(
189
+ "Skipping malformed conversation %s: %s", conv_id_str, exc
190
+ )
191
+
192
+ return results
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # conversations_to_memory_items
197
+ # ---------------------------------------------------------------------------
198
+
199
+
200
+ def conversations_to_memory_items(
201
+ conversations: list[dict[str, Any]],
202
+ ) -> list[dict[str, Any]]:
203
+ """Convert parsed conversations to OMG memory item format.
204
+
205
+ Each conversation becomes ONE memory item with::
206
+
207
+ {
208
+ "key": "chatgpt-{id[:8]}",
209
+ "content": "# {title}\\n\\n**role**: content\\n\\n...",
210
+ "source_cli": "chatgpt",
211
+ "tags": ["chatgpt", "imported"],
212
+ }
213
+
214
+ * Conversations with no messages are skipped.
215
+ * Content is truncated to 5 000 chars max.
216
+ """
217
+ items: list[dict[str, Any]] = []
218
+
219
+ for conv in conversations:
220
+ messages = conv.get("messages", [])
221
+ if not messages:
222
+ continue
223
+
224
+ conv_id = conv.get("id", "unknown")
225
+ title = conv.get("title", "Untitled")
226
+
227
+ # Build content string.
228
+ parts = [f"# {title}", ""]
229
+ for msg in messages:
230
+ role = msg.get("role", "unknown")
231
+ content = msg.get("content", "")
232
+ parts.append(f"**{role}**: {content}")
233
+
234
+ full_content = "\n\n".join(parts)
235
+
236
+ # Truncate to max length.
237
+ if len(full_content) > _MAX_CONTENT_LENGTH:
238
+ full_content = full_content[:_MAX_CONTENT_LENGTH]
239
+
240
+ items.append(
241
+ {
242
+ "id": str(uuid.uuid4()),
243
+ "key": f"chatgpt-{conv_id[:8]}",
244
+ "content": full_content,
245
+ "source_cli": "chatgpt",
246
+ "tags": ["chatgpt", "imported"],
247
+ }
248
+ )
249
+
250
+ return items
251
+
252
+
253
+ __all__ = [
254
+ "extract_linear_conversation",
255
+ "parse_conversations_file",
256
+ "conversations_to_memory_items",
257
+ ]
@@ -0,0 +1,107 @@
1
+ """Claude.ai paste-based memory import."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, TypedDict
5
+
6
+ if TYPE_CHECKING:
7
+ from runtime.memory_store import MemoryStore
8
+
9
+
10
+ class MemoryItem(TypedDict):
11
+ """Memory item structure."""
12
+
13
+ key: str
14
+ content: str
15
+ source_cli: str
16
+ tags: list[str]
17
+
18
+
19
+ EXTRACTION_PROMPT = (
20
+ "List every memory you have stored about me. "
21
+ "Format each memory as a bullet point starting with '- '. "
22
+ "Include all preferences, facts, and context you remember."
23
+ )
24
+
25
+
26
+ def parse_claude_paste(text: str) -> list[MemoryItem]:
27
+ """Parse Claude.ai paste-based memory list into structured items.
28
+
29
+ Handles multiple formats:
30
+ - Bullet points: "- item", "* item", "• item"
31
+ - Numbered lists: "1. item", "2. item"
32
+ - Plain paragraphs: one per line
33
+
34
+ Args:
35
+ text: Freeform text pasted from Claude.ai memory list
36
+
37
+ Returns:
38
+ List of memory item dicts with keys: key, content, source_cli, tags
39
+ """
40
+ if not text or not text.strip():
41
+ return []
42
+
43
+ lines = text.split("\n")
44
+ items: list[MemoryItem] = []
45
+ item_index = 1
46
+
47
+ for line in lines:
48
+ stripped = line.strip()
49
+
50
+ if not stripped:
51
+ continue
52
+
53
+ content = stripped
54
+
55
+ if content.startswith("- "):
56
+ content = content[2:].strip()
57
+ elif content.startswith("* "):
58
+ content = content[2:].strip()
59
+ elif content.startswith("• "):
60
+ content = content[2:].strip()
61
+ elif re.match(r"^\d+\.\s", content):
62
+ content = re.sub(r"^\d+\.\s+", "", content).strip()
63
+
64
+ if not content:
65
+ continue
66
+
67
+ item: MemoryItem = {
68
+ "key": f"claude-memory-{item_index}",
69
+ "content": content,
70
+ "source_cli": "claude-web",
71
+ "tags": ["claude-web", "imported"],
72
+ }
73
+ items.append(item)
74
+ item_index += 1
75
+
76
+ return items
77
+
78
+
79
+ def import_from_paste(text: str, store: "MemoryStore") -> int:
80
+ """Import memories from Claude.ai paste into a MemoryStore.
81
+
82
+ Args:
83
+ text: Pasted memory list from Claude.ai
84
+ store: MemoryStore instance to add items to
85
+
86
+ Returns:
87
+ Count of items successfully added to store
88
+ """
89
+ items = parse_claude_paste(text)
90
+ count = 0
91
+
92
+ for item in items:
93
+ content = item.get("content", "")
94
+ if content: # Skip empty content
95
+ key = item.get("key", "")
96
+ source_cli = item.get("source_cli", "claude-web")
97
+ tags = item.get("tags", [])
98
+ if isinstance(key, str) and isinstance(source_cli, str) and isinstance(tags, list):
99
+ store.add(
100
+ key=key,
101
+ content=content,
102
+ source_cli=source_cli,
103
+ tags=tags,
104
+ )
105
+ count += 1
106
+
107
+ return count
@@ -0,0 +1,97 @@
1
+ """Export memory items to markdown format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def export_to_markdown(items: list[dict[str, Any]]) -> str:
11
+ """Convert list of memory items to markdown string.
12
+
13
+ Args:
14
+ items: List of memory item dicts with keys: id, key, content, source_cli, tags, created_at, updated_at
15
+
16
+ Returns:
17
+ Markdown string with formatted memory items.
18
+ """
19
+ # Generate current timestamp in ISO format
20
+ now = datetime.now(timezone.utc).isoformat()
21
+
22
+ # Start with header
23
+ lines = [
24
+ "# OMG Shared Memory Export",
25
+ "",
26
+ f"Generated: {now}",
27
+ f"Total items: {len(items)}",
28
+ "",
29
+ "---",
30
+ "",
31
+ ]
32
+
33
+ # Add each item
34
+ for idx, item in enumerate(items, start=1):
35
+ key = item.get("key", "unknown")
36
+ content = item.get("content", "")
37
+ source_cli = item.get("source_cli", "unknown")
38
+ tags = item.get("tags", [])
39
+ created_at = item.get("created_at", "")
40
+ updated_at = item.get("updated_at", "")
41
+
42
+ # Format tags as comma-separated string
43
+ tags_str = ", ".join(tags) if tags else ""
44
+
45
+ lines.append(f"## Memory {idx}: {key}")
46
+ lines.append("")
47
+ lines.append(f"**Source**: {source_cli}")
48
+ lines.append(f"**Tags**: {tags_str}")
49
+ lines.append(f"**Created**: {created_at}")
50
+ lines.append(f"**Updated**: {updated_at}")
51
+ lines.append("")
52
+ lines.append(content)
53
+ lines.append("")
54
+ lines.append("---")
55
+ lines.append("")
56
+
57
+ return "\n".join(lines)
58
+
59
+
60
+ def export_to_file(items: list[dict[str, Any]], output_path: str) -> None:
61
+ """Write export_to_markdown(items) to the given file path.
62
+
63
+ Creates parent directories if missing. Overwrites if file exists.
64
+
65
+ Args:
66
+ items: List of memory item dicts
67
+ output_path: Path where to write the markdown file
68
+ """
69
+ path = Path(output_path)
70
+ path.parent.mkdir(parents=True, exist_ok=True)
71
+ markdown = export_to_markdown(items)
72
+ _ = path.write_text(markdown)
73
+
74
+
75
+ def export_from_store(store: Any, output_path: str | None = None) -> str: # pyright: ignore[reportExplicitAny]
76
+ """Export all items from a MemoryStore to markdown.
77
+
78
+ Gets all items from store.export_all(). If output_path is given, writes to file.
79
+ Always returns the markdown string.
80
+
81
+ Args:
82
+ store: MemoryStore instance
83
+ output_path: Optional path to write markdown file to
84
+
85
+ Returns:
86
+ Markdown string representation of all items
87
+ """
88
+ items = store.export_all()
89
+ markdown = export_to_markdown(items)
90
+
91
+ if output_path is not None:
92
+ export_to_file(items, output_path)
93
+
94
+ return markdown
95
+
96
+
97
+ __all__ = ["export_to_markdown", "export_to_file", "export_from_store"]
@@ -0,0 +1,91 @@
1
+ """Gemini Web paste-based memory import — parse and import memories from Gemini Web interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any # pyright: ignore[reportExplicitAny]
7
+
8
+ EXTRACTION_PROMPT = (
9
+ "What do you remember about me? List all preferences, facts, and context you have stored. "
10
+ "Format each as a bullet point starting with '- '."
11
+ )
12
+
13
+
14
+ def parse_gemini_paste(text: str) -> list[dict[str, Any]]: # pyright: ignore[reportExplicitAny]
15
+ """Parse memories from Gemini Web paste text.
16
+
17
+ Supports bullet points (- item) and numbered lists (1. item).
18
+
19
+ Args:
20
+ text: Raw text pasted from Gemini Web interface.
21
+
22
+ Returns:
23
+ List of memory items with keys, content, source_cli, and tags.
24
+ """
25
+ if not text or not text.strip():
26
+ return []
27
+
28
+ items: list[str] = []
29
+ lines = text.split("\n")
30
+
31
+ for line in lines:
32
+ line = line.strip()
33
+ if not line:
34
+ continue
35
+
36
+ # Try bullet point format: "- item"
37
+ if line.startswith("-"):
38
+ content = line[1:].strip()
39
+ if content:
40
+ items.append(content)
41
+ continue
42
+
43
+ # Try numbered format: "1. item", "2. item", etc.
44
+ match = re.match(r"^\d+\.\s+(.+)$", line)
45
+ if match:
46
+ content = match.group(1).strip()
47
+ if content:
48
+ items.append(content)
49
+ continue
50
+
51
+ # Convert to memory item dicts
52
+ result: list[dict[str, Any]] = []
53
+ for i, content in enumerate(items):
54
+ result.append(
55
+ {
56
+ "key": f"gemini-memory-{i+1}",
57
+ "content": content,
58
+ "source_cli": "gemini-web",
59
+ "tags": ["gemini-web", "imported"],
60
+ }
61
+ )
62
+
63
+ return result
64
+
65
+
66
+ def import_from_paste(text: str, store: Any) -> int: # pyright: ignore[reportAny]
67
+ """Import memories from Gemini Web paste into the memory store.
68
+
69
+ Args:
70
+ text: Raw text pasted from Gemini Web interface.
71
+ store: MemoryStore instance to add items to.
72
+
73
+ Returns:
74
+ Number of items successfully imported.
75
+ """
76
+ items = parse_gemini_paste(text)
77
+ count = 0
78
+
79
+ for item in items:
80
+ store.add( # pyright: ignore[reportAny]
81
+ key=item["key"],
82
+ content=item["content"],
83
+ source_cli=item["source_cli"],
84
+ tags=item["tags"],
85
+ )
86
+ count += 1
87
+
88
+ return count
89
+
90
+
91
+ __all__ = ["EXTRACTION_PROMPT", "parse_gemini_paste", "import_from_paste"]