contextkeeper 0.2.0__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.
@@ -0,0 +1,4 @@
1
+
2
+ packages/npm/node_modules/
3
+ packages/npm/package-lock.json
4
+ packages/python/dist/
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: contextkeeper
3
+ Version: 0.2.0
4
+ Summary: Zero model drift between AI agents. Universal session continuity protocol and CLI for Claude, GPT, Gemini, and any LLM.
5
+ Project-URL: Homepage, https://github.com/TheRealDataBoss/workbench
6
+ Project-URL: Repository, https://github.com/TheRealDataBoss/workbench
7
+ Project-URL: Issues, https://github.com/TheRealDataBoss/workbench/issues
8
+ Author: TheRealDataBoss
9
+ License-Expression: MIT
10
+ Keywords: ai,chatgpt,claude,cli,context,continuity,developer-tools
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: gitpython>=3.1.0
24
+ Requires-Dist: httpx>=0.24.0
25
+ Requires-Dist: jsonschema>=4.0.0
26
+ Requires-Dist: questionary>=2.0.0
27
+ Requires-Dist: rich>=13.0.0
28
+ Requires-Dist: typer>=0.9.0
29
+ Description-Content-Type: text/markdown
30
+
31
+ # contextkeeper
32
+
33
+ Zero model drift between AI agents. Universal session continuity protocol and CLI for Claude, GPT, Gemini, and any LLM.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install contextkeeper
39
+ ```
40
+
41
+ ## Commands
42
+
43
+ ### `contextkeeper init`
44
+ Initialize contextkeeper state files in the current project. Auto-detects project type, generates STATE_VECTOR.json and HANDOFF.md, and creates a `.workbench` config file.
45
+
46
+ ```sh
47
+ cd my-project
48
+ contextkeeper init
49
+ ```
50
+
51
+ Options:
52
+ - `-p, --project` — Project slug (default: directory name)
53
+ - `-t, --type` — Project type override
54
+ - `--bridge` — Bridge repo (e.g. `yourname/workbench`)
55
+
56
+ ### `contextkeeper sync`
57
+ Sync state files to your bridge repo.
58
+
59
+ ```sh
60
+ contextkeeper sync
61
+ ```
62
+
63
+ Options:
64
+ - `--bridge` — Bridge repo override
65
+ - `--dry-run` — Preview without pushing
66
+
67
+ ### `contextkeeper status`
68
+ Show status of all projects in the bridge repo.
69
+
70
+ ```sh
71
+ contextkeeper status --bridge yourname/workbench
72
+ ```
73
+
74
+ Options:
75
+ - `--bridge` — Bridge repo
76
+ - `--json` — Output as JSON
77
+
78
+ ### `contextkeeper bootstrap`
79
+ Generate a paste-ready bootstrap prompt for any AI chat.
80
+
81
+ ```sh
82
+ contextkeeper bootstrap -p my-project --clipboard
83
+ ```
84
+
85
+ Options:
86
+ - `-p, --project` — Project slug (required)
87
+ - `--bridge` — Bridge repo override
88
+ - `--clipboard` — Copy to clipboard
89
+
90
+ ### `contextkeeper doctor`
91
+ Check environment and configuration health.
92
+
93
+ ```sh
94
+ contextkeeper doctor
95
+ ```
96
+
97
+ ## How It Works
98
+
99
+ 1. `contextkeeper init` creates structured state files in your project
100
+ 2. `contextkeeper sync` pushes them to a central bridge repo on GitHub
101
+ 3. `contextkeeper bootstrap` generates a prompt you paste into any AI chat
102
+ 4. The AI reads your state files and has full context in under 60 seconds
103
+
104
+ ## Requirements
105
+
106
+ - Python 3.10+
107
+ - git
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,81 @@
1
+ # contextkeeper
2
+
3
+ Zero model drift between AI agents. Universal session continuity protocol and CLI for Claude, GPT, Gemini, and any LLM.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pip install contextkeeper
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `contextkeeper init`
14
+ Initialize contextkeeper state files in the current project. Auto-detects project type, generates STATE_VECTOR.json and HANDOFF.md, and creates a `.workbench` config file.
15
+
16
+ ```sh
17
+ cd my-project
18
+ contextkeeper init
19
+ ```
20
+
21
+ Options:
22
+ - `-p, --project` — Project slug (default: directory name)
23
+ - `-t, --type` — Project type override
24
+ - `--bridge` — Bridge repo (e.g. `yourname/workbench`)
25
+
26
+ ### `contextkeeper sync`
27
+ Sync state files to your bridge repo.
28
+
29
+ ```sh
30
+ contextkeeper sync
31
+ ```
32
+
33
+ Options:
34
+ - `--bridge` — Bridge repo override
35
+ - `--dry-run` — Preview without pushing
36
+
37
+ ### `contextkeeper status`
38
+ Show status of all projects in the bridge repo.
39
+
40
+ ```sh
41
+ contextkeeper status --bridge yourname/workbench
42
+ ```
43
+
44
+ Options:
45
+ - `--bridge` — Bridge repo
46
+ - `--json` — Output as JSON
47
+
48
+ ### `contextkeeper bootstrap`
49
+ Generate a paste-ready bootstrap prompt for any AI chat.
50
+
51
+ ```sh
52
+ contextkeeper bootstrap -p my-project --clipboard
53
+ ```
54
+
55
+ Options:
56
+ - `-p, --project` — Project slug (required)
57
+ - `--bridge` — Bridge repo override
58
+ - `--clipboard` — Copy to clipboard
59
+
60
+ ### `contextkeeper doctor`
61
+ Check environment and configuration health.
62
+
63
+ ```sh
64
+ contextkeeper doctor
65
+ ```
66
+
67
+ ## How It Works
68
+
69
+ 1. `contextkeeper init` creates structured state files in your project
70
+ 2. `contextkeeper sync` pushes them to a central bridge repo on GitHub
71
+ 3. `contextkeeper bootstrap` generates a prompt you paste into any AI chat
72
+ 4. The AI reads your state files and has full context in under 60 seconds
73
+
74
+ ## Requirements
75
+
76
+ - Python 3.10+
77
+ - git
78
+
79
+ ## License
80
+
81
+ MIT
@@ -0,0 +1,3 @@
1
+ """contextkeeper: Zero model drift between AI agents. Universal session continuity protocol and CLI."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,112 @@
1
+ """contextkeeper bootstrap — generate paste-ready AI bootstrap prompt."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ from git import Repo
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+
16
+ console = Console()
17
+
18
+
19
+ def _load_config(cwd: Path) -> dict | None:
20
+ config_path = cwd / ".workbench"
21
+ if not config_path.exists():
22
+ return None
23
+ return json.loads(config_path.read_text(encoding="utf-8"))
24
+
25
+
26
+ def _copy_to_clipboard(text: str) -> bool:
27
+ """Attempt to copy text to the system clipboard. Returns True on success."""
28
+ system = platform.system()
29
+ try:
30
+ if system == "Darwin":
31
+ subprocess.run(["pbcopy"], input=text.encode(), check=True)
32
+ elif system == "Windows":
33
+ subprocess.run(["clip"], input=text.encode(), check=True)
34
+ else:
35
+ subprocess.run(
36
+ ["xclip", "-selection", "clipboard"],
37
+ input=text.encode(),
38
+ check=True,
39
+ )
40
+ return True
41
+ except (subprocess.CalledProcessError, FileNotFoundError):
42
+ return False
43
+
44
+
45
+ def generate_bootstrap(
46
+ project: str,
47
+ bridge: str | None = None,
48
+ clipboard: bool = False,
49
+ ) -> None:
50
+ """Generate a paste-ready bootstrap prompt for any AI chat."""
51
+ cwd = Path.cwd()
52
+ config = _load_config(cwd)
53
+ bridge_repo = bridge or (config and config.get("bridge_repo"))
54
+
55
+ console.print("\n [cyan]contextkeeper bootstrap[/cyan]\n")
56
+
57
+ if not bridge_repo:
58
+ console.print(" [red]No bridge repo configured. Run contextkeeper init or pass --bridge.[/red]")
59
+ raise SystemExit(1)
60
+
61
+ # Verify the project exists
62
+ tmp_dir = Path(tempfile.mkdtemp(prefix="workbench-"))
63
+ try:
64
+ bridge_url = f"https://github.com/{bridge_repo}.git"
65
+
66
+ with console.status("Verifying project in bridge repo..."):
67
+ Repo.clone_from(bridge_url, str(tmp_dir), depth=1)
68
+
69
+ project_dir = tmp_dir / "projects" / project
70
+ if not project_dir.exists():
71
+ console.print(f' [red]Project "{project}" not found in bridge repo.[/red]')
72
+ projects_dir = tmp_dir / "projects"
73
+ if projects_dir.exists():
74
+ available = [d.name for d in projects_dir.iterdir() if d.is_dir()]
75
+ if available:
76
+ console.print(f" [dim]Available: {', '.join(available)}[/dim]")
77
+ raise SystemExit(1)
78
+
79
+ sv_path = project_dir / "STATE_VECTOR.json"
80
+ if not sv_path.exists():
81
+ console.print(f" [red]No STATE_VECTOR.json found for {project}[/red]")
82
+ raise SystemExit(1)
83
+
84
+ console.print(f' [green]Project "{project}" verified[/green]')
85
+
86
+ # Build bootstrap prompt with all URLs explicit
87
+ base_url = f"https://raw.githubusercontent.com/{bridge_repo}/main"
88
+ urls = [
89
+ f"{base_url}/PROFILE.md",
90
+ f"{base_url}/projects/{project}/HANDOFF.md",
91
+ f"{base_url}/projects/{project}/STATE_VECTOR.json",
92
+ ]
93
+
94
+ prompt_lines = [
95
+ f"Fetch these URLs and bootstrap the {project} project:",
96
+ *urls,
97
+ ]
98
+ prompt = "\n".join(prompt_lines)
99
+
100
+ console.print("\n Paste this into any new AI chat:\n")
101
+ console.print(Panel(prompt, border_style="green", padding=(1, 2)))
102
+
103
+ if clipboard:
104
+ if _copy_to_clipboard(prompt):
105
+ console.print(" [green]Copied to clipboard![/green]")
106
+ else:
107
+ console.print(" [yellow]Could not copy to clipboard. Copy manually from above.[/yellow]")
108
+
109
+ console.print()
110
+
111
+ finally:
112
+ shutil.rmtree(tmp_dir, ignore_errors=True)
@@ -0,0 +1,61 @@
1
+ """Typer CLI entry point for contextkeeper."""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ app = typer.Typer(
7
+ name="contextkeeper",
8
+ help="Zero model drift between AI agents. Universal session continuity protocol and CLI for Claude, GPT, Gemini, and any LLM.",
9
+ no_args_is_help=True,
10
+ )
11
+ console = Console()
12
+
13
+
14
+ @app.command()
15
+ def init(
16
+ project: str = typer.Option(None, "--project", "-p", help="Project slug"),
17
+ project_type: str = typer.Option(None, "--type", "-t", help="Project type override"),
18
+ bridge: str = typer.Option(None, "--bridge", help="Bridge repo (e.g. user/workbench)"),
19
+ ) -> None:
20
+ """Initialize contextkeeper state files in the current project."""
21
+ from contextkeeper.init import init_project
22
+
23
+ init_project(project=project, project_type=project_type, bridge=bridge)
24
+
25
+
26
+ @app.command()
27
+ def sync(
28
+ bridge: str = typer.Option(None, "--bridge", help="Bridge repo override"),
29
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview without pushing"),
30
+ ) -> None:
31
+ """Sync state files to the bridge repo."""
32
+ from contextkeeper.sync import sync_project
33
+
34
+ sync_project(bridge=bridge, dry_run=dry_run)
35
+
36
+
37
+ @app.command()
38
+ def status(
39
+ bridge: str = typer.Option(None, "--bridge", help="Bridge repo"),
40
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
41
+ ) -> None:
42
+ """Show status of all projects in the bridge repo."""
43
+ from contextkeeper.status import show_status
44
+
45
+ show_status(bridge=bridge, json_output=json_output)
46
+
47
+
48
+ @app.command()
49
+ def bootstrap(
50
+ project: str = typer.Option(..., "--project", "-p", help="Project slug"),
51
+ bridge: str = typer.Option(None, "--bridge", help="Bridge repo override"),
52
+ clipboard: bool = typer.Option(False, "--clipboard", help="Copy to clipboard"),
53
+ ) -> None:
54
+ """Generate a paste-ready bootstrap prompt for any AI."""
55
+ from contextkeeper.bootstrap import generate_bootstrap
56
+
57
+ generate_bootstrap(project=project, bridge=bridge, clipboard=clipboard)
58
+
59
+
60
+ if __name__ == "__main__":
61
+ app()
@@ -0,0 +1,157 @@
1
+ """contextkeeper init — initialize state files in the current project."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import date
7
+ from pathlib import Path
8
+
9
+ import questionary
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+ PROJECT_TYPE_SIGNALS: list[tuple[str, str, str]] = [
15
+ ("manage.py", "web_app", "Django web app"),
16
+ ("next.config.js", "web_app", "Next.js web app"),
17
+ ("next.config.mjs", "web_app", "Next.js web app"),
18
+ ("vite.config.ts", "web_app", "Vite web app"),
19
+ ("vite.config.js", "web_app", "Vite web app"),
20
+ ("Cargo.toml", "cli_tool", "Rust project"),
21
+ ("setup.py", "library", "Python library"),
22
+ ("pyproject.toml", "library", "Python project"),
23
+ ("requirements.txt", "ml_pipeline", "Python ML pipeline"),
24
+ ("package.json", "web_app", "Node.js project"),
25
+ ]
26
+
27
+ DEFAULT_GATES: dict[str, list[str]] = {
28
+ "web_app": ["npm test", "npm run build", "git status"],
29
+ "ml_pipeline": ["python -m pytest", "git status"],
30
+ "research_notebook": ["jupyter nbconvert --execute --to notebook", "git status"],
31
+ "data_pipeline": ["python -m pytest", "git status"],
32
+ "mobile_app": ["npm test", "npm run build", "git status"],
33
+ "cli_tool": ["npm test", "npm run build", "git status"],
34
+ "library": ["python -m pytest", "git status"],
35
+ "course_module": ["jupyter nbconvert --execute --to notebook", "git status"],
36
+ "other": ["git status"],
37
+ }
38
+
39
+
40
+ def detect_project_type(directory: Path) -> tuple[str, str]:
41
+ """Detect project type by inspecting files in the directory."""
42
+ for filename, ptype, label in PROJECT_TYPE_SIGNALS:
43
+ if (directory / filename).exists():
44
+ return ptype, f"{label} (found {filename})"
45
+
46
+ # Check for notebooks
47
+ notebooks = list(directory.glob("*.ipynb"))
48
+ if notebooks:
49
+ return "research_notebook", f"Research notebook ({len(notebooks)} .ipynb files)"
50
+
51
+ return "other", "Unknown project type"
52
+
53
+
54
+ def init_project(
55
+ project: str | None = None,
56
+ project_type: str | None = None,
57
+ bridge: str | None = None,
58
+ ) -> None:
59
+ """Initialize workbench state files in the current project."""
60
+ cwd = Path.cwd()
61
+ project_name = project or cwd.name
62
+
63
+ console.print("\n [cyan]contextkeeper init[/cyan]\n")
64
+
65
+ # Detect project type
66
+ detected_type, detected_label = detect_project_type(cwd)
67
+ console.print(f" [green]Detected:[/green] {detected_label}")
68
+ resolved_type = project_type or detected_type
69
+
70
+ # Prompt for bridge repo
71
+ bridge_repo = bridge
72
+ if not bridge_repo:
73
+ bridge_repo = questionary.text(
74
+ "Bridge repo (e.g. yourname/workbench):"
75
+ ).ask()
76
+
77
+ # Create directories
78
+ handoff_dir = cwd / "handoff"
79
+ docs_dir = cwd / "docs"
80
+ handoff_dir.mkdir(parents=True, exist_ok=True)
81
+ docs_dir.mkdir(parents=True, exist_ok=True)
82
+
83
+ gates = DEFAULT_GATES.get(resolved_type, ["git status"])
84
+
85
+ # Generate STATE_VECTOR.json
86
+ state_vector = {
87
+ "schema_version": "workbench-v1.0",
88
+ "project": project_name,
89
+ "project_type": resolved_type,
90
+ "local_path": str(cwd),
91
+ "state_machine_status": "IDLE",
92
+ "active_task_id": None,
93
+ "active_task_title": None,
94
+ "current_blocker": None,
95
+ "last_verified_state": "Initial project setup",
96
+ "gates": gates,
97
+ "last_updated": date.today().isoformat(),
98
+ "repo": f"https://github.com/{bridge_repo}" if bridge_repo else "local only",
99
+ "branch": "main",
100
+ "repo_head_sha": None,
101
+ "effective_verified_sha": None,
102
+ }
103
+
104
+ sv_path = handoff_dir / "STATE_VECTOR.json"
105
+ sv_path.write_text(json.dumps(state_vector, indent=2) + "\n", encoding="utf-8")
106
+ console.print(f" [green]Created:[/green] {sv_path}")
107
+
108
+ # Generate HANDOFF.md
109
+ handoff_content = f"""# {project_name} — Project Handoff
110
+ schema_version: workbench-v1.0
111
+
112
+ ## What It Is
113
+ [FILL IN: Describe this project in one paragraph.]
114
+
115
+ ## Where It Is
116
+ - Local: {cwd}
117
+ - GitHub: {state_vector['repo']}
118
+ - Branch: main
119
+
120
+ ## Current Status
121
+ State machine: IDLE. No active task.
122
+
123
+ ## Active Blocker
124
+ None
125
+
126
+ ## Non-Negotiables
127
+ - [FILL IN: List project invariants]
128
+
129
+ ## Gates
130
+ {chr(10).join(f'- {g}' for g in gates)}
131
+
132
+ ## Environment Setup
133
+ [FILL IN: Steps to run from a clean clone]
134
+
135
+ ## Next Action
136
+ [FILL IN: First task to work on]
137
+ """
138
+
139
+ handoff_path = docs_dir / "HANDOFF.md"
140
+ handoff_path.write_text(handoff_content, encoding="utf-8")
141
+ console.print(f" [green]Created:[/green] {handoff_path}")
142
+
143
+ # Write .workbench config
144
+ config = {
145
+ "bridge_repo": bridge_repo,
146
+ "project_name": project_name,
147
+ "state_vector_path": "handoff/STATE_VECTOR.json",
148
+ "handoff_path": "docs/HANDOFF.md",
149
+ }
150
+ config_path = cwd / ".workbench"
151
+ config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
152
+ console.print(f" [green]Created:[/green] {config_path}")
153
+
154
+ console.print("\n [cyan]Next steps:[/cyan]")
155
+ console.print(" 1. Fill in the [FILL IN] sections in docs/HANDOFF.md")
156
+ console.print(" 2. Review handoff/STATE_VECTOR.json")
157
+ console.print(" 3. Run: [bold]contextkeeper sync[/bold] to push to your bridge repo\n")
@@ -0,0 +1,136 @@
1
+ """contextkeeper status — show status of all projects in the bridge repo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import tempfile
8
+ from pathlib import Path
9
+
10
+ from git import Repo
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ console = Console()
15
+
16
+ STATUS_STYLES: dict[str, str] = {
17
+ "EXECUTING": "bold yellow",
18
+ "PROTOCOL_BREACH": "bold red",
19
+ "IDLE": "dim",
20
+ "VERIFIED": "green",
21
+ "SEALED": "green",
22
+ "AWAITING_REVIEW": "blue",
23
+ "AWAITING_MANUAL_VALIDATION": "magenta",
24
+ "PROPOSED": "cyan",
25
+ "REVIEWED": "cyan",
26
+ "VALIDATED": "green",
27
+ "AWAITING_SEAL": "yellow",
28
+ }
29
+
30
+
31
+ def _load_config(cwd: Path) -> dict | None:
32
+ config_path = cwd / ".workbench"
33
+ if not config_path.exists():
34
+ return None
35
+ return json.loads(config_path.read_text(encoding="utf-8"))
36
+
37
+
38
+ def show_status(bridge: str | None = None, json_output: bool = False) -> None:
39
+ """Fetch and display status of all projects in the bridge repo."""
40
+ cwd = Path.cwd()
41
+ config = _load_config(cwd)
42
+ bridge_repo = bridge or (config and config.get("bridge_repo"))
43
+
44
+ console.print("\n [cyan]contextkeeper status[/cyan]\n")
45
+
46
+ if not bridge_repo:
47
+ console.print(" [red]No bridge repo configured. Run contextkeeper init or pass --bridge.[/red]")
48
+ raise SystemExit(1)
49
+
50
+ tmp_dir = Path(tempfile.mkdtemp(prefix="workbench-"))
51
+ try:
52
+ bridge_url = f"https://github.com/{bridge_repo}.git"
53
+
54
+ with console.status("Fetching project states..."):
55
+ Repo.clone_from(bridge_url, str(tmp_dir), depth=1)
56
+
57
+ projects_dir = tmp_dir / "projects"
58
+ if not projects_dir.exists():
59
+ console.print(" [yellow]No projects directory found in bridge repo.[/yellow]")
60
+ return
61
+
62
+ project_dirs = sorted(
63
+ [d for d in projects_dir.iterdir() if d.is_dir()]
64
+ )
65
+
66
+ if not project_dirs:
67
+ console.print(" [yellow]No projects found.[/yellow]")
68
+ return
69
+
70
+ rows: list[dict[str, str]] = []
71
+ for pdir in project_dirs:
72
+ sv_path = pdir / "STATE_VECTOR.json"
73
+ if not sv_path.exists():
74
+ rows.append({
75
+ "name": pdir.name,
76
+ "type": "?",
77
+ "status": "NO STATE",
78
+ "task": "-",
79
+ "blocker": "-",
80
+ "updated": "-",
81
+ })
82
+ continue
83
+
84
+ sv = json.loads(sv_path.read_text(encoding="utf-8"))
85
+ task_id = sv.get("active_task_id")
86
+ task_title = sv.get("active_task_title", "")
87
+ task_str = f"{task_id}: {task_title}"[:50] if task_id else "-"
88
+
89
+ blocker = sv.get("current_blocker")
90
+ blocker_str = (blocker[:40] + "...") if blocker and len(blocker) > 40 else (blocker or "-")
91
+
92
+ rows.append({
93
+ "name": pdir.name,
94
+ "type": sv.get("project_type", "?"),
95
+ "status": sv.get("state_machine_status", "?"),
96
+ "task": task_str,
97
+ "blocker": blocker_str,
98
+ "updated": sv.get("last_updated", "?"),
99
+ })
100
+
101
+ if json_output:
102
+ console.print_json(json.dumps(rows, indent=2))
103
+ return
104
+
105
+ # Build rich table
106
+ table = Table(show_header=True, header_style="bold", pad_edge=False, box=None)
107
+ table.add_column("Project", style="white", min_width=16)
108
+ table.add_column("Type", style="dim", min_width=14)
109
+ table.add_column("Status", min_width=20)
110
+ table.add_column("Active Task", style="white", min_width=30)
111
+ table.add_column("Updated", style="dim", min_width=10)
112
+
113
+ for row in rows:
114
+ status_val = row["status"]
115
+ style = STATUS_STYLES.get(status_val, "white")
116
+ table.add_row(
117
+ row["name"],
118
+ row["type"],
119
+ f"[{style}]{status_val}[/{style}]",
120
+ row["task"],
121
+ row["updated"],
122
+ )
123
+
124
+ console.print(table)
125
+
126
+ # Show blockers
127
+ blocked = [r for r in rows if r["blocker"] != "-"]
128
+ if blocked:
129
+ console.print("\n [yellow]Blockers:[/yellow]")
130
+ for row in blocked:
131
+ console.print(f" [red]{row['name']}:[/red] {row['blocker']}")
132
+
133
+ console.print()
134
+
135
+ finally:
136
+ shutil.rmtree(tmp_dir, ignore_errors=True)
@@ -0,0 +1,133 @@
1
+ """contextkeeper sync — push state files to the bridge repo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import tempfile
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ import jsonschema
12
+ from git import Repo
13
+ from rich.console import Console
14
+ from rich.spinner import Spinner
15
+
16
+ console = Console()
17
+
18
+
19
+ def _load_config(cwd: Path) -> dict | None:
20
+ config_path = cwd / ".workbench"
21
+ if not config_path.exists():
22
+ return None
23
+ return json.loads(config_path.read_text(encoding="utf-8"))
24
+
25
+
26
+ def _load_schema() -> dict | None:
27
+ # Try relative to this package (installed from repo)
28
+ candidates = [
29
+ Path(__file__).resolve().parent.parent.parent.parent / "protocol" / "workbench.schema.json",
30
+ Path.home() / ".workbench" / "src" / "protocol" / "workbench.schema.json",
31
+ ]
32
+ for path in candidates:
33
+ if path.exists():
34
+ return json.loads(path.read_text(encoding="utf-8"))
35
+ return None
36
+
37
+
38
+ def sync_project(bridge: str | None = None, dry_run: bool = False) -> None:
39
+ """Sync state files to the workbench bridge repo."""
40
+ cwd = Path.cwd()
41
+ config = _load_config(cwd)
42
+
43
+ console.print("\n [cyan]contextkeeper sync[/cyan]\n")
44
+
45
+ if not config and not bridge:
46
+ console.print(" [red]No .workbench config found. Run contextkeeper init first, or pass --bridge.[/red]")
47
+ raise SystemExit(1)
48
+
49
+ bridge_repo = bridge or (config and config.get("bridge_repo"))
50
+ project_name = config.get("project_name", "") if config else ""
51
+ sv_rel = config.get("state_vector_path", "handoff/STATE_VECTOR.json") if config else "handoff/STATE_VECTOR.json"
52
+ handoff_rel = config.get("handoff_path", "docs/HANDOFF.md") if config else "docs/HANDOFF.md"
53
+
54
+ if not bridge_repo:
55
+ console.print(" [red]No bridge repo configured. Run contextkeeper init or pass --bridge.[/red]")
56
+ raise SystemExit(1)
57
+
58
+ if not project_name:
59
+ console.print(" [red]No project_name in .workbench config.[/red]")
60
+ raise SystemExit(1)
61
+
62
+ # Read and validate STATE_VECTOR.json
63
+ sv_path = cwd / sv_rel
64
+ if not sv_path.exists():
65
+ console.print(f" [red]STATE_VECTOR.json not found at {sv_path}[/red]")
66
+ raise SystemExit(1)
67
+
68
+ with console.status("Validating STATE_VECTOR.json..."):
69
+ state_vector = json.loads(sv_path.read_text(encoding="utf-8"))
70
+ schema = _load_schema()
71
+ if schema:
72
+ try:
73
+ jsonschema.validate(instance=state_vector, schema=schema)
74
+ except jsonschema.ValidationError as e:
75
+ console.print(f" [red]Validation failed: {e.message}[/red]")
76
+ raise SystemExit(1)
77
+
78
+ console.print(" [green]STATE_VECTOR.json is valid[/green]")
79
+
80
+ if dry_run:
81
+ console.print("\n [yellow]Dry run — would sync:[/yellow]")
82
+ console.print(f" {sv_rel} → projects/{project_name}/STATE_VECTOR.json")
83
+ handoff_path = cwd / handoff_rel
84
+ if handoff_path.exists():
85
+ console.print(f" {handoff_rel} → projects/{project_name}/HANDOFF.md")
86
+ console.print()
87
+ return
88
+
89
+ # Clone bridge repo, copy files, commit, push
90
+ tmp_dir = Path(tempfile.mkdtemp(prefix="workbench-"))
91
+ try:
92
+ bridge_url = f"https://github.com/{bridge_repo}.git"
93
+
94
+ with console.status("Cloning bridge repo..."):
95
+ repo = Repo.clone_from(bridge_url, str(tmp_dir), depth=1)
96
+
97
+ console.print(" [green]Bridge repo cloned[/green]")
98
+
99
+ target_dir = tmp_dir / "projects" / project_name
100
+ target_dir.mkdir(parents=True, exist_ok=True)
101
+
102
+ shutil.copy2(str(sv_path), str(target_dir / "STATE_VECTOR.json"))
103
+ console.print(" [green]Copied: STATE_VECTOR.json[/green]")
104
+
105
+ handoff_path = cwd / handoff_rel
106
+ if handoff_path.exists():
107
+ shutil.copy2(str(handoff_path), str(target_dir / "HANDOFF.md"))
108
+ console.print(" [green]Copied: HANDOFF.md[/green]")
109
+
110
+ next_task_path = cwd / "docs" / "NEXT_TASK.md"
111
+ if next_task_path.exists():
112
+ shutil.copy2(str(next_task_path), str(target_dir / "NEXT_TASK.md"))
113
+ console.print(" [green]Copied: NEXT_TASK.md[/green]")
114
+
115
+ # Commit and push
116
+ with console.status("Pushing to bridge repo..."):
117
+ repo.index.add("*")
118
+ if not repo.is_dirty(untracked_files=True):
119
+ console.print(" [yellow]No changes to push[/yellow]")
120
+ return
121
+
122
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
123
+ commit_msg = f"chore(workbench): sync {project_name} -- {timestamp}"
124
+ repo.index.commit(commit_msg)
125
+ repo.remotes.origin.push()
126
+
127
+ sha = repo.head.commit.hexsha[:7]
128
+ console.print(f" [green]Pushed: [bold]{sha}[/bold][/green]")
129
+ console.print(f"\n [green]Sync complete for [bold]{project_name}[/bold][/green]")
130
+ console.print(f" [dim]{commit_msg}[/dim]\n")
131
+
132
+ finally:
133
+ shutil.rmtree(tmp_dir, ignore_errors=True)
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "contextkeeper"
7
+ version = "0.2.0"
8
+ description = "Zero model drift between AI agents. Universal session continuity protocol and CLI for Claude, GPT, Gemini, and any LLM."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "TheRealDataBoss" },
14
+ ]
15
+ keywords = [
16
+ "ai",
17
+ "claude",
18
+ "chatgpt",
19
+ "context",
20
+ "continuity",
21
+ "developer-tools",
22
+ "cli",
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 3 - Alpha",
26
+ "Environment :: Console",
27
+ "Intended Audience :: Developers",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Topic :: Software Development :: Libraries",
35
+ "Topic :: Software Development :: Quality Assurance",
36
+ ]
37
+ dependencies = [
38
+ "typer>=0.9.0",
39
+ "rich>=13.0.0",
40
+ "gitpython>=3.1.0",
41
+ "jsonschema>=4.0.0",
42
+ "httpx>=0.24.0",
43
+ "questionary>=2.0.0",
44
+ ]
45
+
46
+ [project.scripts]
47
+ contextkeeper = "contextkeeper.cli:app"
48
+
49
+ [project.urls]
50
+ Homepage = "https://github.com/TheRealDataBoss/workbench"
51
+ Repository = "https://github.com/TheRealDataBoss/workbench"
52
+ Issues = "https://github.com/TheRealDataBoss/workbench/issues"