@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.
- package/.claude-plugin/marketplace.json +8 -8
- package/.claude-plugin/plugin.json +5 -4
- package/.claude-plugin/scripts/uninstall.sh +74 -3
- package/.claude-plugin/scripts/update.sh +78 -3
- package/.coveragerc +26 -0
- package/.mcp.json +4 -4
- package/CHANGELOG.md +14 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +62 -0
- package/OMG-setup.sh +1201 -355
- package/README.md +77 -56
- package/SECURITY.md +25 -0
- package/agents/__init__.py +1 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-architect-mode.md +3 -5
- package/agents/omg-backend-engineer.md +3 -5
- package/agents/omg-database-engineer.md +3 -5
- package/agents/omg-frontend-designer.md +4 -5
- package/agents/omg-implement-mode.md +4 -5
- package/agents/omg-infra-engineer.md +3 -5
- package/agents/omg-research-mode.md +4 -6
- package/agents/omg-security-auditor.md +3 -5
- package/agents/omg-testing-engineer.md +3 -5
- package/build/lib/yaml.py +321 -0
- package/commands/OMG:ai-commit.md +101 -14
- package/commands/OMG:arch.md +302 -19
- package/commands/OMG:ccg.md +12 -7
- package/commands/OMG:compat.md +25 -17
- package/commands/OMG:cost.md +173 -13
- package/commands/OMG:crazy.md +1 -1
- package/commands/OMG:create-agent.md +170 -20
- package/commands/OMG:deps.md +235 -17
- package/commands/OMG:domain-init.md +1 -1
- package/commands/OMG:escalate.md +41 -12
- package/commands/OMG:health-check.md +37 -13
- package/commands/OMG:init.md +122 -14
- package/commands/OMG:project-init.md +1 -1
- package/commands/OMG:session-branch.md +76 -9
- package/commands/OMG:session-fork.md +42 -5
- package/commands/OMG:session-merge.md +124 -8
- package/commands/OMG:setup.md +69 -12
- package/commands/OMG:stats.md +215 -14
- package/commands/OMG:teams.md +19 -10
- package/config/lsp_languages.yaml +8 -0
- package/hooks/__init__.py +0 -0
- package/hooks/_agent_registry.py +423 -0
- package/hooks/_analytics.py +291 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +569 -0
- package/hooks/_compression_optimizer.py +119 -0
- package/hooks/_cost_ledger.py +176 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/_protected_context.py +150 -0
- package/hooks/_token_counter.py +221 -0
- package/hooks/branch_manager.py +236 -0
- package/hooks/budget_governor.py +232 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/compression_feedback.py +254 -0
- package/hooks/config-guard.py +216 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +1020 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +505 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +219 -0
- package/hooks/post_write.py +46 -0
- package/hooks/pre-compact.py +398 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/query.py +512 -0
- package/hooks/secret-guard.py +61 -0
- package/hooks/secret_audit.py +144 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +277 -0
- package/hooks/setup_wizard.py +582 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +225 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +945 -0
- package/hooks/test-validator.py +361 -0
- package/hooks/test_generator_hook.py +123 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +149 -0
- package/hooks/trust_review.py +585 -0
- package/hud/omg-hud.mjs +31 -1
- package/lab/__init__.py +1 -0
- package/lab/pipeline.py +75 -0
- package/lab/policies.py +52 -0
- package/package.json +7 -18
- package/plugins/README.md +33 -61
- package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
- package/plugins/advanced/commands/OMG:learn.md +1 -1
- package/plugins/advanced/commands/OMG:security-review.md +3 -3
- package/plugins/advanced/commands/OMG:ship.md +1 -1
- package/plugins/advanced/plugin.json +1 -1
- package/plugins/core/plugin.json +8 -3
- package/plugins/dephealth/__init__.py +0 -0
- package/plugins/dephealth/cve_scanner.py +188 -0
- package/plugins/dephealth/license_checker.py +135 -0
- package/plugins/dephealth/manifest_detector.py +423 -0
- package/plugins/dephealth/vuln_analyzer.py +169 -0
- package/plugins/testgen/__init__.py +0 -0
- package/plugins/testgen/codamosa_engine.py +402 -0
- package/plugins/testgen/edge_case_synthesizer.py +184 -0
- package/plugins/testgen/framework_detector.py +271 -0
- package/plugins/testgen/skeleton_generator.py +219 -0
- package/plugins/viz/__init__.py +0 -0
- package/plugins/viz/ast_parser.py +139 -0
- package/plugins/viz/diagram_generator.py +192 -0
- package/plugins/viz/graph_builder.py +444 -0
- package/plugins/viz/native_parsers.py +259 -0
- package/plugins/viz/regex_parser.py +112 -0
- package/pyproject.toml +81 -0
- package/rules/contextual/write-verify.md +2 -2
- package/rules/core/00-truth.md +1 -1
- package/rules/core/01-surgical.md +1 -1
- package/rules/core/02-circuit-breaker.md +2 -2
- package/rules/core/03-ensemble.md +3 -3
- package/rules/core/04-testing.md +3 -3
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/adoption.py +212 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/cli_provider.py +85 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/mcp_config_writers.py +115 -0
- package/runtime/mcp_lifecycle.py +153 -0
- package/runtime/mcp_memory_server.py +135 -0
- package/runtime/memory_parsers/__init__.py +0 -0
- package/runtime/memory_parsers/chatgpt_parser.py +257 -0
- package/runtime/memory_parsers/claude_import.py +107 -0
- package/runtime/memory_parsers/export.py +97 -0
- package/runtime/memory_parsers/gemini_import.py +91 -0
- package/runtime/memory_parsers/kimi_import.py +91 -0
- package/runtime/memory_store.py +215 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/providers/__init__.py +0 -0
- package/runtime/providers/codex_provider.py +112 -0
- package/runtime/providers/gemini_provider.py +128 -0
- package/runtime/providers/kimi_provider.py +151 -0
- package/runtime/providers/opencode_provider.py +144 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +1167 -0
- package/runtime/tmux_session_manager.py +169 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-contract-snapshot.py +12 -0
- package/scripts/check-omg-public-ready.py +193 -0
- package/scripts/check-omg-standalone-clean.py +103 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-legacy.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +492 -0
- package/scripts/settings-merge.py +283 -0
- package/scripts/verify-standalone.sh +8 -4
- package/settings.json +126 -29
- package/templates/profile.yaml +1 -1
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +347 -0
- package/tools/commit_splitter.py +746 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/dashboard_generator.py +300 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/pr_generator.py +404 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
- package/yaml.py +321 -0
- package/.claude-plugin/scripts/install.sh +0 -9
- package/bun.lock +0 -23
- package/bunfig.toml +0 -3
- package/hooks/_budget.ts +0 -1
- package/hooks/_common.ts +0 -63
- package/hooks/circuit-breaker.ts +0 -101
- package/hooks/config-guard.ts +0 -4
- package/hooks/firewall.ts +0 -20
- package/hooks/policy_engine.ts +0 -156
- package/hooks/post-tool-failure.ts +0 -22
- package/hooks/post-write.ts +0 -4
- package/hooks/pre-tool-inject.ts +0 -4
- package/hooks/prompt-enhancer.ts +0 -46
- package/hooks/quality-runner.ts +0 -24
- package/hooks/secret-guard.ts +0 -4
- package/hooks/session-end-capture.ts +0 -19
- package/hooks/session-start.ts +0 -19
- package/hooks/shadow_manager.ts +0 -81
- package/hooks/stop-gate.ts +0 -22
- package/hooks/stop_dispatcher.ts +0 -147
- package/hooks/test-generator-hook.ts +0 -4
- package/hooks/tool-ledger.ts +0 -27
- package/hooks/trust_review.ts +0 -175
- package/lab/pipeline.ts +0 -75
- package/lab/policies.ts +0 -68
- package/runtime/common.ts +0 -111
- package/runtime/compat.ts +0 -174
- package/runtime/dispatcher.ts +0 -25
- package/runtime/ecosystem.ts +0 -186
- package/runtime/provider_bootstrap.ts +0 -99
- package/runtime/provider_smoke.ts +0 -34
- package/runtime/release_readiness.ts +0 -186
- package/runtime/team_router.ts +0 -144
- package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
- package/scripts/check-omg-standalone-clean.ts +0 -12
- package/scripts/check-runtime-clean.ts +0 -94
- package/scripts/omg.ts +0 -352
- package/scripts/settings-merge.ts +0 -93
- package/tools/commit_splitter.ts +0 -23
- package/tools/git_inspector.ts +0 -18
- package/tools/session_snapshot.ts +0 -47
- package/trac3er-oh-my-god-2.0.0.tgz +0 -0
- 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"]
|