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.
- contextkeeper-0.2.0/.gitignore +4 -0
- contextkeeper-0.2.0/PKG-INFO +111 -0
- contextkeeper-0.2.0/README.md +81 -0
- contextkeeper-0.2.0/contextkeeper/__init__.py +3 -0
- contextkeeper-0.2.0/contextkeeper/bootstrap.py +112 -0
- contextkeeper-0.2.0/contextkeeper/cli.py +61 -0
- contextkeeper-0.2.0/contextkeeper/init.py +157 -0
- contextkeeper-0.2.0/contextkeeper/status.py +136 -0
- contextkeeper-0.2.0/contextkeeper/sync.py +133 -0
- contextkeeper-0.2.0/pyproject.toml +52 -0
|
@@ -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,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"
|