chatstrata 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {chatstrata-0.2.2 → chatstrata-0.2.3}/CHANGELOG.md +5 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/PKG-INFO +2 -2
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/codex_cli/adapter.py +14 -2
- chatstrata-0.2.3/chatstrata/sources/omp/__init__.py +3 -0
- chatstrata-0.2.3/chatstrata/sources/omp/adapter.py +323 -0
- chatstrata-0.2.3/chatstrata/sources/omp/manifest.yaml +12 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/pyproject.toml +2 -1
- {chatstrata-0.2.2 → chatstrata-0.2.3}/.gitignore +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/CONTRIBUTING.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/LICENSE +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/README.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/SECURITY.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/cli.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/queries/activity.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/queries/conversations.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/queries/example_queries.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/queries/models.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/queries/projects.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/analysis/queries/tools.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/cli.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/db.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/ingest.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/migrations/0001_initial.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/migrations/0002_fts_index.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/migrations/0003_conversation_mtime.sql +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/migrations/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/models.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/search.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/embed/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/embed/base.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/embed/cli.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/embed/local_provider.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/embed/search.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/mcp/README.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/mcp/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/mcp/safety.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/mcp/server.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/base.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/cli.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/presidio_engine.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/recognizers/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/recognizers/api_keys.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/recognizers/connection_strings.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/recognizers/paths.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/redact/recognizers/tokens.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/schedule/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/schedule/cli.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/schedule/launchd.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/base.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/claude_code/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/claude_code/adapter.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/claude_code/manifest.yaml +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/claude_export/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/claude_export/adapter.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/claude_export/manifest.yaml +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/codex_cli/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/codex_cli/manifest.yaml +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/opencode/__init__.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/opencode/adapter.py +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/sources/opencode/manifest.yaml +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/adapter-guide.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/adr/0001-use-duckdb.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/adr/0002-store-raw-alongside-normalized.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/adr/0003-adapter-pattern-entry-points.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/images/chatstrata.png +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/redaction.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/release.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/schema.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/sources/claude_export.md +0 -0
- {chatstrata-0.2.2 → chatstrata-0.2.3}/docs/sources/codex_cli.md +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.3 - 2026-09-12
|
|
4
|
+
|
|
5
|
+
- Add Oh My Pi (`omp`) source adapter: ingests session transcripts from `~/.omp/agent/sessions/`, including tool calls/results, thinking blocks, extension (`custom_message`) turns, titles, and project paths from the session header.
|
|
6
|
+
- Fix `codex_cli` adapter: newer Codex rollouts encode tool-result `output` fields as content arrays instead of plain strings; these now parse correctly.
|
|
7
|
+
|
|
3
8
|
## 0.2.2 - 2026-07-29
|
|
4
9
|
|
|
5
10
|
- Cap the `mcp` dependency at `>=1.12.0,<2`. `mcp` 2.0.0 removed the `mcp.server.fastmcp` module that `chatstrata/mcp/server.py` imports, so the previously unbounded pin resolved 2.0.0 and crashed `chatstrata-mcp` on startup with `ModuleNotFoundError: No module named 'mcp.server.fastmcp'`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: chatstrata
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A personal, queryable archive of your AI conversations across providers
|
|
5
5
|
Project-URL: Homepage, https://github.com/brandonbosch/chatstrata
|
|
6
6
|
Project-URL: Repository, https://github.com/brandonbosch/chatstrata
|
|
@@ -93,6 +93,18 @@ def _text_from_content(content: list[dict[str, Any]]) -> str | None:
|
|
|
93
93
|
return "\n".join(parts) if parts else None
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
def _text_from_value(value: Any) -> str | None:
|
|
97
|
+
"""Normalize text fields that newer Codex rollouts encode as content arrays."""
|
|
98
|
+
if isinstance(value, str):
|
|
99
|
+
return value
|
|
100
|
+
if isinstance(value, list):
|
|
101
|
+
return _text_from_content(value)
|
|
102
|
+
if isinstance(value, dict):
|
|
103
|
+
text = value.get("text")
|
|
104
|
+
return text if isinstance(text, str) else None
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
96
108
|
def _blocks_from_response_item(
|
|
97
109
|
payload: dict[str, Any],
|
|
98
110
|
) -> tuple[Role | None, list[ContentBlock]]:
|
|
@@ -139,7 +151,7 @@ def _blocks_from_response_item(
|
|
|
139
151
|
ContentBlock(
|
|
140
152
|
type=BlockType.TOOL_RESULT,
|
|
141
153
|
tool_use_id=payload.get("call_id"),
|
|
142
|
-
text=payload.get("output"),
|
|
154
|
+
text=_text_from_value(payload.get("output")),
|
|
143
155
|
)
|
|
144
156
|
]
|
|
145
157
|
|
|
@@ -172,7 +184,7 @@ def _blocks_from_response_item(
|
|
|
172
184
|
ContentBlock(
|
|
173
185
|
type=BlockType.TOOL_RESULT,
|
|
174
186
|
tool_use_id=payload.get("call_id"),
|
|
175
|
-
text=payload.get("output"),
|
|
187
|
+
text=_text_from_value(payload.get("output")),
|
|
176
188
|
)
|
|
177
189
|
]
|
|
178
190
|
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Oh My Pi (omp) adapter.
|
|
2
|
+
|
|
3
|
+
Oh My Pi writes one JSONL file per session to:
|
|
4
|
+
~/.omp/agent/sessions/<encoded-cwd>/<timestamp>_<session-id>.jsonl
|
|
5
|
+
|
|
6
|
+
Each file physically begins with a fixed-width 256-byte title slot
|
|
7
|
+
({"type": "title", ...} padded with spaces), followed by a session header
|
|
8
|
+
({"type": "session", ...}) and then append-only SessionEntry values.
|
|
9
|
+
|
|
10
|
+
Conversation content lives in "message" entries whose persisted roles are
|
|
11
|
+
camelCase: "user", "assistant", "toolResult" (plus rarer coding-agent roles
|
|
12
|
+
like "bashExecution" we skip). Tool calls are content blocks of type
|
|
13
|
+
"toolCall" inside assistant messages; results are separate "toolResult"
|
|
14
|
+
messages carrying toolCallId/toolName. Extensions may add "custom_message"
|
|
15
|
+
entries that participate in the transcript.
|
|
16
|
+
|
|
17
|
+
Entries form a tree via id/parentId (branching, compaction); this adapter
|
|
18
|
+
flattens in file order, preserving ids so lineage stays queryable.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
from collections.abc import Iterable
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from chatstrata.core.models import (
|
|
30
|
+
BlockType,
|
|
31
|
+
ContentBlock,
|
|
32
|
+
ConversationHandle,
|
|
33
|
+
ParsedConversation,
|
|
34
|
+
ParsedMessage,
|
|
35
|
+
Role,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
DEFAULT_OMP_DIR = Path("~/.omp/agent/sessions").expanduser()
|
|
39
|
+
|
|
40
|
+
_ROLE_MAP: dict[str, Role] = {
|
|
41
|
+
"user": Role.USER,
|
|
42
|
+
"assistant": Role.ASSISTANT,
|
|
43
|
+
"toolResult": Role.TOOL,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Roles persisted by omp that carry no conversational value for the archive
|
|
47
|
+
# (tool-feedback bookkeeping, standalone shell runs, legacy hook payloads).
|
|
48
|
+
_SKIPPED_ROLES = {"developer", "hookMessage", "bashExecution", "pythonExecution", "fileMention"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_timestamp(s: Any) -> datetime | None:
|
|
52
|
+
"""omp timestamps are ISO 8601 strings, usually with 'Z' suffix."""
|
|
53
|
+
if not isinstance(s, str):
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
57
|
+
except ValueError:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _text_from_content(content: Any) -> str | None:
|
|
62
|
+
"""Extract concatenated text from a content string or block list."""
|
|
63
|
+
if isinstance(content, str):
|
|
64
|
+
return content
|
|
65
|
+
if not isinstance(content, list):
|
|
66
|
+
return None
|
|
67
|
+
parts = [
|
|
68
|
+
block.get("text", "")
|
|
69
|
+
for block in content
|
|
70
|
+
if isinstance(block, dict) and block.get("type") == "text" and isinstance(block.get("text"), str)
|
|
71
|
+
]
|
|
72
|
+
return "\n".join(parts) if parts else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _blocks_from_content(content: Any) -> list[ContentBlock]:
|
|
76
|
+
"""Convert an omp message content value into ContentBlocks.
|
|
77
|
+
|
|
78
|
+
Content may be a plain string or a list of typed blocks: "text",
|
|
79
|
+
"thinking", "toolCall" (assistant-side), and "image".
|
|
80
|
+
"""
|
|
81
|
+
if content is None:
|
|
82
|
+
return []
|
|
83
|
+
if isinstance(content, str):
|
|
84
|
+
return [ContentBlock(type=BlockType.TEXT, text=content)] if content else []
|
|
85
|
+
|
|
86
|
+
blocks: list[ContentBlock] = []
|
|
87
|
+
for item in content:
|
|
88
|
+
if not isinstance(item, dict):
|
|
89
|
+
continue
|
|
90
|
+
ctype = item.get("type")
|
|
91
|
+
if ctype == "text":
|
|
92
|
+
text = item.get("text")
|
|
93
|
+
if isinstance(text, str) and text:
|
|
94
|
+
blocks.append(ContentBlock(type=BlockType.TEXT, text=text))
|
|
95
|
+
elif ctype == "thinking":
|
|
96
|
+
text = item.get("thinking")
|
|
97
|
+
if isinstance(text, str) and text:
|
|
98
|
+
blocks.append(ContentBlock(type=BlockType.THINKING, text=text))
|
|
99
|
+
elif ctype == "toolCall":
|
|
100
|
+
arguments = item.get("arguments")
|
|
101
|
+
if isinstance(arguments, str):
|
|
102
|
+
try:
|
|
103
|
+
arguments = json.loads(arguments)
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
arguments = {"raw": arguments}
|
|
106
|
+
blocks.append(
|
|
107
|
+
ContentBlock(
|
|
108
|
+
type=BlockType.TOOL_USE,
|
|
109
|
+
tool_name=item.get("name"),
|
|
110
|
+
tool_use_id=item.get("id"),
|
|
111
|
+
payload={"arguments": arguments},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
elif ctype == "image":
|
|
115
|
+
payload = {k: v for k, v in item.items() if k != "type"}
|
|
116
|
+
blocks.append(ContentBlock(type=BlockType.IMAGE, payload=payload))
|
|
117
|
+
return blocks
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _blocks_from_tool_result(message: dict[str, Any]) -> list[ContentBlock]:
|
|
121
|
+
"""Convert a toolResult message into a single TOOL_RESULT block."""
|
|
122
|
+
text = _text_from_content(message.get("content"))
|
|
123
|
+
details = message.get("details")
|
|
124
|
+
payload: dict[str, Any] = {}
|
|
125
|
+
if isinstance(details, dict):
|
|
126
|
+
payload["details"] = details
|
|
127
|
+
if message.get("isError"):
|
|
128
|
+
payload["isError"] = True
|
|
129
|
+
return [
|
|
130
|
+
ContentBlock(
|
|
131
|
+
type=BlockType.TOOL_RESULT,
|
|
132
|
+
tool_name=message.get("toolName"),
|
|
133
|
+
tool_use_id=message.get("toolCallId"),
|
|
134
|
+
text=text,
|
|
135
|
+
payload=payload,
|
|
136
|
+
)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _decode_project_dir(dir_name: str) -> str:
|
|
141
|
+
"""Best-effort decode of omp's encoded cwd bucket name back into a path.
|
|
142
|
+
|
|
143
|
+
Buckets under home are spelled "-<relative>" (e.g. "-git-chatstrata");
|
|
144
|
+
absolute fallback buckets are "--<encoded>--". parse() prefers the
|
|
145
|
+
lossless cwd from the session header; this is only a backstop.
|
|
146
|
+
"""
|
|
147
|
+
if dir_name.startswith("--") and dir_name.endswith("--"):
|
|
148
|
+
return "/" + dir_name[2:-2].replace("-", "/")
|
|
149
|
+
if dir_name.startswith("-"):
|
|
150
|
+
return str(Path.home() / dir_name[1:].replace("-", "/"))
|
|
151
|
+
return dir_name
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class OmpAdapter:
|
|
155
|
+
"""Adapter for Oh My Pi session transcripts."""
|
|
156
|
+
|
|
157
|
+
name = "omp"
|
|
158
|
+
display_name = "Oh My Pi"
|
|
159
|
+
version = "0.1.0"
|
|
160
|
+
schema_version = 1
|
|
161
|
+
|
|
162
|
+
def discover(self, config: dict | None = None) -> Iterable[ConversationHandle]:
|
|
163
|
+
"""Walk ~/.omp/agent/sessions/**/*.jsonl and yield a handle per file."""
|
|
164
|
+
root = Path((config or {}).get("path") or DEFAULT_OMP_DIR).expanduser()
|
|
165
|
+
if not root.exists():
|
|
166
|
+
return
|
|
167
|
+
for jsonl in sorted(root.glob("*/*.jsonl")):
|
|
168
|
+
stem = jsonl.stem
|
|
169
|
+
# Files are <timestamp>_<session-id>.jsonl; the id is the suffix
|
|
170
|
+
# after the first underscore in the timestamp portion.
|
|
171
|
+
session_id = stem.split("_", 1)[1] if "_" in stem else stem
|
|
172
|
+
project = _decode_project_dir(jsonl.parent.name)
|
|
173
|
+
yield ConversationHandle(
|
|
174
|
+
source_native_id=session_id,
|
|
175
|
+
path=jsonl,
|
|
176
|
+
metadata={"project": project},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def parse(self, handle: ConversationHandle) -> ParsedConversation:
|
|
180
|
+
if handle.path is None:
|
|
181
|
+
raise ValueError("OmpAdapter requires a path on the handle")
|
|
182
|
+
path = handle.path
|
|
183
|
+
|
|
184
|
+
events: list[dict[str, Any]] = []
|
|
185
|
+
with path.open("r", encoding="utf-8") as f:
|
|
186
|
+
for line in f:
|
|
187
|
+
line = line.strip()
|
|
188
|
+
if not line:
|
|
189
|
+
continue
|
|
190
|
+
try:
|
|
191
|
+
events.append(json.loads(line))
|
|
192
|
+
except json.JSONDecodeError:
|
|
193
|
+
# Skip malformed lines; one bad line should not kill an
|
|
194
|
+
# entire session import.
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
messages: list[ParsedMessage] = []
|
|
198
|
+
title: str | None = None
|
|
199
|
+
project: str | None = None
|
|
200
|
+
started_at: datetime | None = None
|
|
201
|
+
ended_at: datetime | None = None
|
|
202
|
+
|
|
203
|
+
for ev in events:
|
|
204
|
+
etype = ev.get("type")
|
|
205
|
+
|
|
206
|
+
if etype == "title":
|
|
207
|
+
# Fixed-width title slot; folded into the header below when
|
|
208
|
+
# the header lacks a title.
|
|
209
|
+
if title is None:
|
|
210
|
+
slot_title = ev.get("title")
|
|
211
|
+
if isinstance(slot_title, str) and slot_title.strip():
|
|
212
|
+
title = slot_title.strip()
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if etype == "session":
|
|
216
|
+
header_title = ev.get("title")
|
|
217
|
+
if isinstance(header_title, str) and header_title.strip():
|
|
218
|
+
title = header_title.strip()
|
|
219
|
+
cwd = ev.get("cwd")
|
|
220
|
+
if isinstance(cwd, str):
|
|
221
|
+
project = cwd
|
|
222
|
+
ts = _parse_timestamp(ev.get("timestamp"))
|
|
223
|
+
if ts:
|
|
224
|
+
started_at = ts
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if etype == "message":
|
|
228
|
+
message = ev.get("message") or {}
|
|
229
|
+
role_name = message.get("role")
|
|
230
|
+
if not isinstance(role_name, str) or role_name in _SKIPPED_ROLES:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if role_name == "toolResult":
|
|
234
|
+
blocks = _blocks_from_tool_result(message)
|
|
235
|
+
else:
|
|
236
|
+
blocks = _blocks_from_content(message.get("content"))
|
|
237
|
+
|
|
238
|
+
if not blocks:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
role = _ROLE_MAP[role_name]
|
|
242
|
+
ts = _parse_timestamp(ev.get("timestamp"))
|
|
243
|
+
if ts:
|
|
244
|
+
if started_at is None or ts < started_at:
|
|
245
|
+
started_at = ts
|
|
246
|
+
if ended_at is None or ts > ended_at:
|
|
247
|
+
ended_at = ts
|
|
248
|
+
|
|
249
|
+
metadata: dict[str, Any] = {}
|
|
250
|
+
attribution = message.get("attribution")
|
|
251
|
+
if isinstance(attribution, str):
|
|
252
|
+
metadata["attribution"] = attribution
|
|
253
|
+
|
|
254
|
+
messages.append(
|
|
255
|
+
ParsedMessage(
|
|
256
|
+
source_native_id=ev.get("id"),
|
|
257
|
+
parent_source_native_id=ev.get("parentId"),
|
|
258
|
+
role=role,
|
|
259
|
+
model=message.get("model"),
|
|
260
|
+
created_at=ts,
|
|
261
|
+
blocks=blocks,
|
|
262
|
+
metadata=metadata,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
if etype == "custom_message":
|
|
268
|
+
# Extension messages that participate in the transcript.
|
|
269
|
+
attribution = ev.get("attribution")
|
|
270
|
+
role = Role.USER if attribution == "user" else Role.ASSISTANT
|
|
271
|
+
text = _text_from_content(ev.get("content"))
|
|
272
|
+
if not text:
|
|
273
|
+
continue
|
|
274
|
+
ts = _parse_timestamp(ev.get("timestamp"))
|
|
275
|
+
if ts:
|
|
276
|
+
if started_at is None or ts < started_at:
|
|
277
|
+
started_at = ts
|
|
278
|
+
if ended_at is None or ts > ended_at:
|
|
279
|
+
ended_at = ts
|
|
280
|
+
messages.append(
|
|
281
|
+
ParsedMessage(
|
|
282
|
+
source_native_id=ev.get("id"),
|
|
283
|
+
parent_source_native_id=ev.get("parentId"),
|
|
284
|
+
role=role,
|
|
285
|
+
created_at=ts,
|
|
286
|
+
blocks=[ContentBlock(type=BlockType.TEXT, text=text)],
|
|
287
|
+
metadata=(
|
|
288
|
+
{"attribution": attribution}
|
|
289
|
+
if isinstance(attribution, str)
|
|
290
|
+
else {}
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Other entry types (compaction, model_change, custom, ...) affect
|
|
297
|
+
# replay state, not transcript content; skip them.
|
|
298
|
+
|
|
299
|
+
if project is None and handle.metadata:
|
|
300
|
+
project = handle.metadata.get("project")
|
|
301
|
+
|
|
302
|
+
# Fall back to the first user text block as a title.
|
|
303
|
+
if title is None:
|
|
304
|
+
for m in messages:
|
|
305
|
+
if m.role == Role.USER:
|
|
306
|
+
for b in m.blocks:
|
|
307
|
+
if b.type == BlockType.TEXT and b.text:
|
|
308
|
+
title = b.text.strip().splitlines()[0][:200]
|
|
309
|
+
break
|
|
310
|
+
if title:
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
return ParsedConversation(
|
|
314
|
+
source_native_id=handle.source_native_id,
|
|
315
|
+
title=title,
|
|
316
|
+
project=project,
|
|
317
|
+
started_at=started_at,
|
|
318
|
+
ended_at=ended_at,
|
|
319
|
+
messages=messages,
|
|
320
|
+
raw_path=str(path),
|
|
321
|
+
metadata={"event_count": len(events)},
|
|
322
|
+
raw_events=events,
|
|
323
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
name: omp
|
|
2
|
+
display_name: Oh My Pi
|
|
3
|
+
version: "0.1.0"
|
|
4
|
+
schema_version: 1
|
|
5
|
+
description: |
|
|
6
|
+
Ingests Oh My Pi (omp) session transcripts from ~/.omp/agent/sessions/.
|
|
7
|
+
Each session is a JSONL file beginning with a fixed-width title slot and a
|
|
8
|
+
session header, followed by append-only entries (message, custom_message,
|
|
9
|
+
compaction, model_change, ...). Messages use camelCase roles (user,
|
|
10
|
+
assistant, toolResult) with toolCall blocks inside assistant content.
|
|
11
|
+
data_source: local_filesystem
|
|
12
|
+
default_path: ~/.omp/agent/sessions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "chatstrata"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "A personal, queryable archive of your AI conversations across providers"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -62,6 +62,7 @@ claude_code = "chatstrata.sources.claude_code.adapter:ClaudeCodeAdapter"
|
|
|
62
62
|
claude_export = "chatstrata.sources.claude_export.adapter:ClaudeExportAdapter"
|
|
63
63
|
codex_cli = "chatstrata.sources.codex_cli.adapter:CodexCliAdapter"
|
|
64
64
|
opencode = "chatstrata.sources.opencode.adapter:OpenCodeAdapter"
|
|
65
|
+
omp = "chatstrata.sources.omp.adapter:OmpAdapter"
|
|
65
66
|
|
|
66
67
|
[build-system]
|
|
67
68
|
requires = ["hatchling"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chatstrata-0.2.2 → chatstrata-0.2.3}/chatstrata/core/migrations/0003_conversation_mtime.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|