sessionfs 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 (90) hide show
  1. sessionfs/__init__.py +3 -0
  2. sessionfs/audit.py +153 -0
  3. sessionfs/cli/__init__.py +1 -0
  4. sessionfs/cli/cmd_admin.py +60 -0
  5. sessionfs/cli/cmd_cloud.py +522 -0
  6. sessionfs/cli/cmd_config.py +117 -0
  7. sessionfs/cli/cmd_daemon.py +161 -0
  8. sessionfs/cli/cmd_io.py +137 -0
  9. sessionfs/cli/cmd_ops.py +230 -0
  10. sessionfs/cli/cmd_sessions.py +321 -0
  11. sessionfs/cli/common.py +84 -0
  12. sessionfs/cli/cost.py +77 -0
  13. sessionfs/cli/main.py +58 -0
  14. sessionfs/cli/sfs_to_cc.py +459 -0
  15. sessionfs/cli/sfs_to_md.py +125 -0
  16. sessionfs/cli/titles.py +159 -0
  17. sessionfs/converters/__init__.py +0 -0
  18. sessionfs/converters/codex_injector.py +162 -0
  19. sessionfs/converters/cursor_to_sfs.py +324 -0
  20. sessionfs/converters/gemini_injector.py +92 -0
  21. sessionfs/converters/gemini_to_sfs.py +269 -0
  22. sessionfs/converters/sfs_to_codex.py +383 -0
  23. sessionfs/converters/sfs_to_gemini.py +204 -0
  24. sessionfs/daemon/__init__.py +0 -0
  25. sessionfs/daemon/config.py +137 -0
  26. sessionfs/daemon/main.py +393 -0
  27. sessionfs/daemon/status.py +66 -0
  28. sessionfs/security/__init__.py +0 -0
  29. sessionfs/security/secrets.py +223 -0
  30. sessionfs/server/__init__.py +1 -0
  31. sessionfs/server/app.py +95 -0
  32. sessionfs/server/auth/__init__.py +1 -0
  33. sessionfs/server/auth/dependencies.py +70 -0
  34. sessionfs/server/auth/keys.py +24 -0
  35. sessionfs/server/auth/rate_limit.py +36 -0
  36. sessionfs/server/config.py +28 -0
  37. sessionfs/server/db/__init__.py +1 -0
  38. sessionfs/server/db/engine.py +40 -0
  39. sessionfs/server/db/migrations/env.py +70 -0
  40. sessionfs/server/db/migrations/versions/001_initial_schema.py +78 -0
  41. sessionfs/server/db/models.py +77 -0
  42. sessionfs/server/errors.py +48 -0
  43. sessionfs/server/routes/__init__.py +1 -0
  44. sessionfs/server/routes/auth.py +132 -0
  45. sessionfs/server/routes/health.py +12 -0
  46. sessionfs/server/routes/sessions.py +822 -0
  47. sessionfs/server/schemas/__init__.py +1 -0
  48. sessionfs/server/schemas/auth.py +37 -0
  49. sessionfs/server/schemas/errors.py +17 -0
  50. sessionfs/server/schemas/sessions.py +104 -0
  51. sessionfs/server/storage/__init__.py +1 -0
  52. sessionfs/server/storage/base.py +15 -0
  53. sessionfs/server/storage/local.py +41 -0
  54. sessionfs/server/storage/s3.py +49 -0
  55. sessionfs/session_id.py +32 -0
  56. sessionfs/spec/__init__.py +0 -0
  57. sessionfs/spec/convert_cc.py +634 -0
  58. sessionfs/spec/examples/complete/manifest.json +33 -0
  59. sessionfs/spec/examples/complete/messages.jsonl +8 -0
  60. sessionfs/spec/examples/complete/tools.json +9 -0
  61. sessionfs/spec/examples/complete/workspace.json +33 -0
  62. sessionfs/spec/examples/minimal/manifest.json +28 -0
  63. sessionfs/spec/examples/minimal/messages.jsonl +3 -0
  64. sessionfs/spec/examples/subagent/manifest.json +33 -0
  65. sessionfs/spec/examples/subagent/messages.jsonl +7 -0
  66. sessionfs/spec/examples/subagent/tools.json +7 -0
  67. sessionfs/spec/examples/subagent/workspace.json +24 -0
  68. sessionfs/spec/schemas/manifest.schema.json +287 -0
  69. sessionfs/spec/schemas/message.schema.json +291 -0
  70. sessionfs/spec/schemas/tools.schema.json +138 -0
  71. sessionfs/spec/schemas/workspace.schema.json +152 -0
  72. sessionfs/spec/validate.py +353 -0
  73. sessionfs/store/__init__.py +0 -0
  74. sessionfs/store/index.py +230 -0
  75. sessionfs/store/local.py +147 -0
  76. sessionfs/sync/__init__.py +1 -0
  77. sessionfs/sync/archive.py +73 -0
  78. sessionfs/sync/client.py +313 -0
  79. sessionfs/utils/__init__.py +0 -0
  80. sessionfs/utils/title_utils.py +207 -0
  81. sessionfs/watchers/__init__.py +0 -0
  82. sessionfs/watchers/base.py +71 -0
  83. sessionfs/watchers/claude_code.py +641 -0
  84. sessionfs/watchers/codex.py +573 -0
  85. sessionfs/watchers/cursor.py +202 -0
  86. sessionfs/watchers/gemini.py +206 -0
  87. sessionfs-0.1.0.dist-info/METADATA +213 -0
  88. sessionfs-0.1.0.dist-info/RECORD +90 -0
  89. sessionfs-0.1.0.dist-info/WHEEL +4 -0
  90. sessionfs-0.1.0.dist-info/entry_points.txt +3 -0
sessionfs/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """SessionFS — Portable AI coding sessions."""
2
+
3
+ __version__ = "0.1.0"
sessionfs/audit.py ADDED
@@ -0,0 +1,153 @@
1
+ """M9: Audit logging module.
2
+
3
+ Provides structured audit logging to both local file (~/.sessionfs/audit.log)
4
+ and the server audit_events table. All significant events are logged in JSON
5
+ lines format.
6
+
7
+ IMPORTANT: Never log session content, blob data, or API key values.
8
+ Log metadata only.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import stat
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ logger = logging.getLogger("sessionfs.audit")
22
+
23
+ # Event types that can be logged
24
+ EVENT_TYPES = frozenset({
25
+ "session_captured",
26
+ "session_synced",
27
+ "session_pulled",
28
+ "session_resumed",
29
+ "session_exported",
30
+ "session_handoff",
31
+ "session_deleted",
32
+ "session_forked",
33
+ "session_checkpoint_created",
34
+ "api_key_created",
35
+ "api_key_revoked",
36
+ "auth_failed",
37
+ "auth_success",
38
+ "sync_conflict",
39
+ "sync_error",
40
+ })
41
+
42
+
43
+ class AuditLogger:
44
+ """Writes audit events to a JSON lines file."""
45
+
46
+ def __init__(self, audit_log_path: Path | None = None) -> None:
47
+ if audit_log_path is None:
48
+ audit_log_path = Path.home() / ".sessionfs" / "audit.log"
49
+ self._path = audit_log_path
50
+
51
+ def _ensure_file(self) -> None:
52
+ """Ensure the audit log file exists with correct permissions."""
53
+ self._path.parent.mkdir(parents=True, exist_ok=True)
54
+ if not self._path.exists():
55
+ self._path.touch()
56
+ os.chmod(self._path, stat.S_IRUSR | stat.S_IWUSR) # 0o600
57
+
58
+ def log(
59
+ self,
60
+ event_type: str,
61
+ *,
62
+ user_id: str | None = None,
63
+ session_id: str | None = None,
64
+ details: dict[str, Any] | None = None,
65
+ source_ip: str | None = None,
66
+ ) -> None:
67
+ """Write an audit event to the log file.
68
+
69
+ Args:
70
+ event_type: One of the EVENT_TYPES constants.
71
+ user_id: User who performed the action (None for daemon events).
72
+ session_id: Session involved (if applicable).
73
+ details: Additional context (never include secret values).
74
+ source_ip: Client IP (server-side events only).
75
+ """
76
+ entry = {
77
+ "timestamp": datetime.now(timezone.utc).isoformat(),
78
+ "event_type": event_type,
79
+ }
80
+ if user_id is not None:
81
+ entry["user_id"] = user_id
82
+ if session_id is not None:
83
+ entry["session_id"] = session_id
84
+ if details:
85
+ entry["details"] = details
86
+ if source_ip:
87
+ entry["source_ip"] = source_ip
88
+
89
+ try:
90
+ self._ensure_file()
91
+ with open(self._path, "a", encoding="utf-8") as f:
92
+ f.write(json.dumps(entry, separators=(",", ":")) + "\n")
93
+ except OSError as e:
94
+ logger.error("Failed to write audit log: %s", e)
95
+
96
+ def read_events(
97
+ self,
98
+ event_type: str | None = None,
99
+ session_id: str | None = None,
100
+ limit: int = 100,
101
+ ) -> list[dict[str, Any]]:
102
+ """Read audit events from the log file with optional filters."""
103
+ if not self._path.exists():
104
+ return []
105
+
106
+ events: list[dict[str, Any]] = []
107
+ with open(self._path, "r", encoding="utf-8") as f:
108
+ for line in f:
109
+ line = line.strip()
110
+ if not line:
111
+ continue
112
+ try:
113
+ event = json.loads(line)
114
+ except json.JSONDecodeError:
115
+ continue
116
+ if event_type and event.get("event_type") != event_type:
117
+ continue
118
+ if session_id and event.get("session_id") != session_id:
119
+ continue
120
+ events.append(event)
121
+
122
+ # Return most recent first, limited
123
+ return events[-limit:][::-1]
124
+
125
+
126
+ # Module-level default instance
127
+ _default_logger: AuditLogger | None = None
128
+
129
+
130
+ def get_audit_logger(audit_log_path: Path | None = None) -> AuditLogger:
131
+ """Get or create the default AuditLogger instance."""
132
+ global _default_logger
133
+ if _default_logger is None or audit_log_path is not None:
134
+ _default_logger = AuditLogger(audit_log_path)
135
+ return _default_logger
136
+
137
+
138
+ def audit_event(
139
+ event_type: str,
140
+ *,
141
+ user_id: str | None = None,
142
+ session_id: str | None = None,
143
+ details: dict[str, Any] | None = None,
144
+ source_ip: str | None = None,
145
+ ) -> None:
146
+ """Convenience function to log an audit event using the default logger."""
147
+ get_audit_logger().log(
148
+ event_type,
149
+ user_id=user_id,
150
+ session_id=session_id,
151
+ details=details,
152
+ source_ip=source_ip,
153
+ )
@@ -0,0 +1 @@
1
+ """SessionFS CLI."""
@@ -0,0 +1,60 @@
1
+ """Admin commands: sfs admin reindex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import typer
8
+
9
+ from sessionfs.cli.common import console, err_console
10
+
11
+ admin_app = typer.Typer(name="admin", help="Server administration commands.")
12
+
13
+
14
+ def _get_sync_client():
15
+ """Create a SyncClient from stored config."""
16
+ from sessionfs.cli.cmd_cloud import _get_sync_client
17
+ return _get_sync_client()
18
+
19
+
20
+ @admin_app.command("reindex")
21
+ def reindex() -> None:
22
+ """Re-extract metadata from all session archives on the server.
23
+
24
+ Backfills title, source_tool, model, stats, and tags for sessions
25
+ that were pushed before metadata extraction was deployed.
26
+ """
27
+ from sessionfs.sync.client import SyncClient, SyncAuthError, SyncError
28
+
29
+ client = _get_sync_client()
30
+
31
+ async def _reindex():
32
+ try:
33
+ http = await client._get_client()
34
+ resp = await http.post(
35
+ f"{client.api_url}/api/v1/sessions/admin/reindex",
36
+ timeout=120.0,
37
+ )
38
+ if resp.status_code in (401, 403):
39
+ raise SyncAuthError(f"Authentication failed: {resp.status_code}")
40
+ resp.raise_for_status()
41
+ return resp.json()
42
+ finally:
43
+ await client.close()
44
+
45
+ console.print("Reindexing all sessions on the server...")
46
+ try:
47
+ result = asyncio.run(_reindex())
48
+ except SyncAuthError:
49
+ err_console.print("[red]Authentication failed. Run 'sfs auth login' first.[/red]")
50
+ raise SystemExit(1)
51
+ except Exception as exc:
52
+ err_console.print(f"[red]Reindex failed: {exc}[/red]")
53
+ raise SystemExit(1)
54
+
55
+ console.print(
56
+ f"[green]Reindex complete:[/green] "
57
+ f"{result['reindexed']} sessions, "
58
+ f"{result['updated']} updated, "
59
+ f"{result['errors']} errors"
60
+ )