claude-opencode-sessions 0.1.0__py3-none-any.whl

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 (33) hide show
  1. claude_opencode_sessions/__init__.py +10 -0
  2. claude_opencode_sessions/__main__.py +3 -0
  3. claude_opencode_sessions/cli.py +196 -0
  4. claude_opencode_sessions/cli_backend.py +140 -0
  5. claude_opencode_sessions/errors.py +39 -0
  6. claude_opencode_sessions/import_command.py +216 -0
  7. claude_opencode_sessions/importer.py +414 -0
  8. claude_opencode_sessions/locate.py +90 -0
  9. claude_opencode_sessions/models.py +96 -0
  10. claude_opencode_sessions/parse.py +224 -0
  11. claude_opencode_sessions/py.typed +0 -0
  12. claude_opencode_sessions/render.py +169 -0
  13. claude_opencode_sessions/scope.py +160 -0
  14. claude_opencode_sessions/service.py +149 -0
  15. claude_opencode_sessions/sqlite_backend.py +249 -0
  16. claude_opencode_sessions/tests/__init__.py +0 -0
  17. claude_opencode_sessions/tests/conftest.py +314 -0
  18. claude_opencode_sessions/tests/fixtures/schema_1x.sql +76 -0
  19. claude_opencode_sessions/tests/fixtures/schema_2x.sql +37 -0
  20. claude_opencode_sessions/tests/test_cli.py +140 -0
  21. claude_opencode_sessions/tests/test_cli_backend.py +116 -0
  22. claude_opencode_sessions/tests/test_importer.py +504 -0
  23. claude_opencode_sessions/tests/test_launcher.py +47 -0
  24. claude_opencode_sessions/tests/test_locate.py +62 -0
  25. claude_opencode_sessions/tests/test_parse.py +120 -0
  26. claude_opencode_sessions/tests/test_render.py +107 -0
  27. claude_opencode_sessions/tests/test_scope.py +121 -0
  28. claude_opencode_sessions/tests/test_sqlite_backend.py +157 -0
  29. claude_opencode_sessions-0.1.0.dist-info/METADATA +287 -0
  30. claude_opencode_sessions-0.1.0.dist-info/RECORD +33 -0
  31. claude_opencode_sessions-0.1.0.dist-info/WHEEL +4 -0
  32. claude_opencode_sessions-0.1.0.dist-info/entry_points.txt +2 -0
  33. claude_opencode_sessions-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,140 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from claude_opencode_sessions.cli import main
7
+ from claude_opencode_sessions.tests.conftest import (
8
+ HOUR,
9
+ NOW,
10
+ DbBuilder,
11
+ assistant,
12
+ text,
13
+ tool,
14
+ user,
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def populated(db_1x: DbBuilder, git_repo, monkeypatch: pytest.MonkeyPatch):
20
+ repo, feature, other = git_repo["repo"], git_repo["feature"], git_repo["other"]
21
+ db_1x.session_1x(
22
+ "ses_newest",
23
+ str(repo / "sub"),
24
+ title="Newest in repo",
25
+ updated=NOW,
26
+ messages=[
27
+ user("fix the parser"),
28
+ assistant(text("on it"), tool("edit", {"filePath": "parser.py"})),
29
+ assistant(text("done, the needle was misplaced")),
30
+ ],
31
+ )
32
+ db_1x.session_1x(
33
+ "ses_older",
34
+ str(repo),
35
+ title="Older in repo",
36
+ updated=NOW - HOUR,
37
+ messages=[user("older question")],
38
+ )
39
+ db_1x.session_1x("ses_archived", str(repo), updated=NOW - 2 * HOUR, archived=NOW)
40
+ db_1x.session_1x(
41
+ "ses_child", str(repo), updated=NOW - 3 * HOUR, parent_id="ses_newest"
42
+ )
43
+ db_1x.session_1x(
44
+ "ses_feature", str(feature), title="Feature work", updated=NOW - 4 * HOUR
45
+ )
46
+ db_1x.session_1x(
47
+ "ses_other",
48
+ str(other),
49
+ title="Elsewhere",
50
+ updated=NOW - 5 * HOUR,
51
+ project_id="proj2",
52
+ messages=[user("unrelated")],
53
+ )
54
+ db_1x.close()
55
+ monkeypatch.setenv("OPENCODE_SESSIONS_DB", str(db_1x.path))
56
+ monkeypatch.chdir(repo / "sub")
57
+ return git_repo
58
+
59
+
60
+ def run(capsys, *argv: str) -> tuple[int, str, str]:
61
+ code = main(list(argv))
62
+ captured = capsys.readouterr()
63
+ return code, captured.out, captured.err
64
+
65
+
66
+ def test_list_default_scope_is_current_worktree(populated, capsys):
67
+ code, out, _ = run(capsys, "list")
68
+ assert code == 0
69
+ assert "Newest in repo" in out and "Older in repo" in out
70
+ assert "Feature work" not in out and "Elsewhere" not in out
71
+ assert "1 archived" in out and "1 sub-agent" in out
72
+ assert out.index("Newest in repo") < out.index("Older in repo")
73
+
74
+
75
+ def test_list_worktrees_and_all(populated, capsys):
76
+ _, out, _ = run(capsys, "list", "--worktrees")
77
+ assert "Feature work" in out and "Elsewhere" not in out
78
+ _, out, _ = run(
79
+ capsys, "list", "--all", "--include-archived", "--include-subagents"
80
+ )
81
+ for title in (
82
+ "Elsewhere",
83
+ "Feature work",
84
+ "Session ses_archived",
85
+ "Session ses_child",
86
+ ):
87
+ assert title in out
88
+
89
+
90
+ def test_list_limit_and_json(populated, capsys):
91
+ code, out, _ = run(capsys, "list", "--all", "-n", "1", "--format", "json")
92
+ payload = json.loads(out)
93
+ assert code == 0
94
+ assert payload["total"] == 4
95
+ assert len(payload["sessions"]) == 1
96
+ assert payload["sessions"][0]["id"] == "ses_newest"
97
+ assert payload["sessions"][0]["index"] == 1
98
+ assert payload["backend"] == "sqlite"
99
+
100
+
101
+ def test_list_path_option(populated, capsys):
102
+ _, out, _ = run(capsys, "list", "--path", str(populated["feature"]))
103
+ assert "Feature work" in out and "Newest in repo" not in out
104
+
105
+
106
+ def test_doctor(populated, capsys):
107
+ code, out, _ = run(capsys, "doctor")
108
+ assert code == 0
109
+ assert "layout: session · 1.x message/part" in out
110
+ assert "sessions readable: 6" in out
111
+ assert "scope worktree: git worktree" in out
112
+
113
+
114
+ def test_doctor_without_db(capsys, tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
115
+ monkeypatch.setenv("OPENCODE_SESSIONS_DB", str(tmp_path / "missing.db"))
116
+ code, out, _ = run(capsys, "doctor")
117
+ assert code == 3 and "NOT USABLE" in out
118
+
119
+
120
+ def test_sqlite_backend_missing_db_errors(capsys, tmp_path: Path):
121
+ code, _, err = run(
122
+ capsys, "list", "--backend", "sqlite", "--db", str(tmp_path / "x.db")
123
+ )
124
+ assert code == 3 and "not found" in err
125
+
126
+
127
+ def test_auto_without_db_or_cli(
128
+ capsys, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
129
+ ):
130
+ monkeypatch.setenv("PATH", str(tmp_path)) # no opencode, no git
131
+ code, _, err = run(capsys, "list", "--db", str(tmp_path / "x.db"))
132
+ assert code == 4
133
+ assert "sqlite unavailable" in err and "cli unavailable" in err
134
+
135
+
136
+ def test_version(capsys):
137
+ with pytest.raises(SystemExit) as exc:
138
+ main(["--version"])
139
+ assert exc.value.code == 0
140
+ assert "claude-opencode-sessions" in capsys.readouterr().out
@@ -0,0 +1,116 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from claude_opencode_sessions.cli import main
7
+ from claude_opencode_sessions.cli_backend import (
8
+ CliBackend,
9
+ _extract_json,
10
+ parse_version,
11
+ )
12
+ from claude_opencode_sessions.errors import BackendError
13
+ from claude_opencode_sessions.models import Message, Session
14
+ from claude_opencode_sessions.service import Context, open_context
15
+ from claude_opencode_sessions.tests.conftest import DbBuilder, text_of
16
+
17
+
18
+ def test_parse_version():
19
+ assert parse_version("opencode 1.18.32") == (1, 18, 32)
20
+ assert parse_version("2.0") == (2, 0)
21
+ assert parse_version("dev") is None
22
+
23
+
24
+ def test_extract_json_skips_log_lines():
25
+ assert _extract_json('INFO starting\n{"a": [1]}\n') == {"a": [1]}
26
+ assert _extract_json("noise [1, 2]") == [1, 2]
27
+ with pytest.raises(BackendError):
28
+ _extract_json("no json {here")
29
+
30
+
31
+ def test_cli_backend_list_and_export(fake_opencode: Path):
32
+ backend = CliBackend()
33
+ assert backend.version == (1, 18, 32)
34
+ [session] = backend.list_sessions()
35
+ assert session.id == "ses_cli_1" and session.origin == "cli"
36
+ info, messages = backend.export("ses_cli_1")
37
+ assert info is not None and info.title == "From the CLI"
38
+ assert [text_of(m) for m in messages] == ["hello from export", "hi back"]
39
+ assert messages[1].model == "openai/gpt-x"
40
+ with pytest.raises(BackendError, match="exited 1"):
41
+ backend.export("ses_missing")
42
+
43
+
44
+ def test_cli_backend_rejects_old_version(fake_opencode: Path):
45
+ data = json.loads(fake_opencode.read_text())
46
+ data["version"] = "0.9.1"
47
+ fake_opencode.write_text(json.dumps(data))
48
+ with pytest.raises(BackendError, match="too old"):
49
+ CliBackend()
50
+
51
+
52
+ def test_cli_backend_missing_executable(
53
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
54
+ ):
55
+ monkeypatch.setenv("PATH", str(tmp_path))
56
+ with pytest.raises(BackendError, match="not found on PATH"):
57
+ CliBackend()
58
+
59
+
60
+ def test_auto_falls_back_to_cli(
61
+ fake_opencode: Path, tmp_path: Path, capsys, monkeypatch
62
+ ):
63
+ project = tmp_path / "proj"
64
+ project.mkdir()
65
+ monkeypatch.setenv("OPENCODE_SESSIONS_DB", str(tmp_path / "missing.db"))
66
+ code = main(["list", "--path", str(project)])
67
+ out = capsys.readouterr().out
68
+ assert code == 0
69
+ assert "From the CLI" in out
70
+ assert "backend: cli (opencode CLI 1.18.32)" in out
71
+ assert "sqlite unavailable" in out
72
+ target = tmp_path / "claude-project"
73
+ code = main(["import", "--path", str(project), "--claude-project-dir", str(target)])
74
+ out = capsys.readouterr().out
75
+ assert code == 0 and "1 imported" in out
76
+ [imported] = target.glob("*.jsonl")
77
+ assert "hello from export" in imported.read_text()
78
+
79
+
80
+ def test_unparseable_sqlite_session_falls_back_to_export(
81
+ fake_opencode: Path, db_1x: DbBuilder
82
+ ):
83
+ db_1x.session_1x(
84
+ "ses_cli_1",
85
+ "/w",
86
+ messages=[({"role": "user"}, ["{bad"]), ({"role": "user"}, ["{bad"])],
87
+ )
88
+ db_1x.close()
89
+ ctx = open_context("auto", str(db_1x.path))
90
+ session = Session(id="ses_cli_1", title="t", directory="/w")
91
+ messages = ctx.messages(session)
92
+ assert text_of(messages[0]) == "hello from export"
93
+ assert any("fell back" in note for note in ctx.notes)
94
+
95
+
96
+ def test_unparseable_sqlite_session_without_cli_keeps_partial(
97
+ db_1x: DbBuilder, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
98
+ ):
99
+ db_1x.session_1x("ses_a", "/w", messages=[({"role": "user"}, ["{bad"])])
100
+ db_1x.close()
101
+ monkeypatch.setenv("PATH", str(tmp_path))
102
+ ctx = open_context("auto", str(db_1x.path))
103
+ messages = ctx.messages(Session(id="ses_a", title="t", directory="/w"))
104
+ assert len(messages) == 1 and isinstance(messages[0], Message)
105
+ assert any("could not be parsed" in note for note in ctx.notes)
106
+ assert any("cli unavailable" in note for note in ctx.notes)
107
+
108
+
109
+ def test_open_context_rejects_unknown_backend():
110
+ with pytest.raises(ValueError):
111
+ open_context("nope")
112
+
113
+
114
+ def test_context_describe_cli(fake_opencode: Path):
115
+ ctx = Context(mode="cli", cli=CliBackend())
116
+ assert ctx.describe().startswith("cli (opencode CLI")