codex-coach 0.1.0
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.
- package/.codex-plugin/plugin.json +25 -0
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/assets/brand/codex-coach-icon.png +0 -0
- package/assets/brand/codex-coach-icon.svg +49 -0
- package/assets/brand/codex-coach-logo.png +0 -0
- package/assets/brand/codex-coach-logo.svg +62 -0
- package/assets/examples/how-it-works.png +0 -0
- package/assets/examples/how-it-works.svg +71 -0
- package/assets/examples/project-capsules.png +0 -0
- package/assets/examples/project-capsules.svg +59 -0
- package/assets/examples/prompt-lint.png +0 -0
- package/assets/examples/prompt-lint.svg +54 -0
- package/bin/codex-coach.js +53 -0
- package/install.ps1 +18 -0
- package/install.sh +50 -0
- package/package.json +38 -0
- package/pyproject.toml +31 -0
- package/skills/codex-coach/SKILL.md +71 -0
- package/skills/codex-coach/agents/openai.yaml +8 -0
- package/skills/codex-coach/references/config-suggestions.md +35 -0
- package/skills/codex-coach/references/privacy.md +25 -0
- package/skills/codex-coach/references/prompt-rubric.md +21 -0
- package/src/codex_coach/__init__.py +3 -0
- package/src/codex_coach/cli.py +146 -0
- package/src/codex_coach/install.py +230 -0
- package/src/codex_coach/parser.py +395 -0
- package/src/codex_coach/paths.py +49 -0
- package/src/codex_coach/prompts.py +151 -0
- package/src/codex_coach/redaction.py +35 -0
- package/src/codex_coach/reports.py +284 -0
- package/src/codex_coach/timeutil.py +49 -0
package/pyproject.toml
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codex-coach"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local-first Codex usage coach and plugin"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Codex Coach Contributors" }]
|
|
13
|
+
keywords = ["codex", "ai", "developer-tools", "analytics", "prompting"]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
codex-coach = "codex_coach.cli:main"
|
|
18
|
+
|
|
19
|
+
[tool.hatch.build.targets.wheel]
|
|
20
|
+
packages = ["src/codex_coach"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.sdist]
|
|
23
|
+
include = [
|
|
24
|
+
"/assets",
|
|
25
|
+
"/src",
|
|
26
|
+
"/skills",
|
|
27
|
+
"/.codex-plugin",
|
|
28
|
+
"/README.md",
|
|
29
|
+
"/install.sh",
|
|
30
|
+
"/install.ps1",
|
|
31
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-coach
|
|
3
|
+
description: Use when the user asks to analyze their Codex usage, habits, prompt quality, model/effort choices, tool usage, project switching, context recovery, or wants suggestions for Codex custom instructions or AGENTS.md improvements.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Codex Coach
|
|
7
|
+
|
|
8
|
+
Codex Coach is a local-first coaching workflow. Use the bundled `codex-coach` command for deterministic parsing; do not manually scan large logs unless the command is unavailable.
|
|
9
|
+
|
|
10
|
+
## Quick Workflow
|
|
11
|
+
|
|
12
|
+
1. Run a health check:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
codex-coach doctor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. Generate or refresh the report:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
codex-coach report --since 7d
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. Use beginner or expert mode based on the user:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
codex-coach report --since 7d --mode beginner
|
|
28
|
+
codex-coach report --since 7d --mode expert
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
4. Read `~/.codex-coach/reports/latest.md` and summarize the highest-impact findings.
|
|
32
|
+
|
|
33
|
+
5. For config improvements, generate review files:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
codex-coach suggest-config --since 7d
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
6. For a prompt the user is drafting, lint it directly:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
codex-coach lint-prompt "fix the login bug"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
7. Explain that suggestions are reviewable and are not applied automatically.
|
|
46
|
+
|
|
47
|
+
## User Modes
|
|
48
|
+
|
|
49
|
+
- Beginner: summarize the top 3 findings in plain language, then give one habit to try this week.
|
|
50
|
+
- Expert: include model/effort mix, tool mix, verification ratio, compactions, project capsules, and skill opportunities.
|
|
51
|
+
- Project-specific: if the user names a project path, filter your discussion to that project from the report/facts if present.
|
|
52
|
+
|
|
53
|
+
## What To Surface
|
|
54
|
+
|
|
55
|
+
- Project capsules: explain the likely workflow, top friction, and suggested local instruction.
|
|
56
|
+
- Prompt rewrites: show the redacted preview and the safer template, not the raw original prompt.
|
|
57
|
+
- Confidence: keep low-confidence suggestions tentative; high-confidence suggestions can be presented as the next best habit.
|
|
58
|
+
- Skill opportunities: if the report flags a repeated workflow, suggest a small user skill with trigger, context to gather, verification commands, and resume rules.
|
|
59
|
+
|
|
60
|
+
## Guardrails
|
|
61
|
+
|
|
62
|
+
- Keep reports local. Do not upload logs.
|
|
63
|
+
- Do not paste full prompt bodies or source code into chat.
|
|
64
|
+
- Treat `~/.codex-coach/facts/latest.json` as the machine-readable source of truth.
|
|
65
|
+
- Do not edit global custom instructions or `AGENTS.md` automatically; show the generated suggestion file and ask for explicit approval before applying any change.
|
|
66
|
+
|
|
67
|
+
## Optional References
|
|
68
|
+
|
|
69
|
+
- Read `references/prompt-rubric.md` when explaining prompt quality scores.
|
|
70
|
+
- Read `references/config-suggestions.md` when discussing custom instruction or AGENTS.md changes.
|
|
71
|
+
- Read `references/privacy.md` when the user asks what data is read or stored.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Config Suggestion Policy
|
|
2
|
+
|
|
3
|
+
Codex Coach can suggest changes to global custom instructions, user-level skills, or project `AGENTS.md` files. Suggestions must be reviewable and reversible.
|
|
4
|
+
|
|
5
|
+
Recommended suggestion format:
|
|
6
|
+
|
|
7
|
+
- Evidence: one metric or repeated pattern from the report.
|
|
8
|
+
- Confidence: low, medium, or high.
|
|
9
|
+
- Change: the exact instruction text to add.
|
|
10
|
+
- Benefit: what should improve.
|
|
11
|
+
- Rollback: how to remove it if it does not help.
|
|
12
|
+
|
|
13
|
+
Common suggestions:
|
|
14
|
+
|
|
15
|
+
```md
|
|
16
|
+
When a task is simple, local, or status-oriented, default to medium reasoning. Reserve high or xhigh for ambiguous debugging, architecture, security, broad refactors, or unclear failures.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```md
|
|
20
|
+
Before saying a fix is complete, run the smallest meaningful verification command or inspect the live/runtime state when applicable.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```md
|
|
24
|
+
For long tasks, keep a short durable checklist or artifact ledger so work can resume safely after compaction or interruption.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```md
|
|
28
|
+
For multi-project work, prefer short project capsules over one large global instruction.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For repeated workflows, prefer a user skill over a long global instruction:
|
|
32
|
+
|
|
33
|
+
```md
|
|
34
|
+
When this workflow appears, gather [project context], follow [steps], verify with [commands], and resume from [ledger/checklist] after interruption.
|
|
35
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Privacy Model
|
|
2
|
+
|
|
3
|
+
Codex Coach is local-first.
|
|
4
|
+
|
|
5
|
+
Default inputs:
|
|
6
|
+
|
|
7
|
+
- `~/.codex/sessions/**/*.jsonl`
|
|
8
|
+
- `~/.codex/archived_sessions/*.jsonl`
|
|
9
|
+
- Optional Codex config and project metadata only when a user asks for deeper recommendations.
|
|
10
|
+
|
|
11
|
+
Default outputs:
|
|
12
|
+
|
|
13
|
+
- `~/.codex-coach/facts/latest.json`
|
|
14
|
+
- `~/.codex-coach/reports/latest.md`
|
|
15
|
+
- `~/.codex-coach/reports/weekly-YYYY-MM-DD.md`
|
|
16
|
+
- `~/.codex-coach/suggestions/*.patch.md`
|
|
17
|
+
|
|
18
|
+
Default redaction:
|
|
19
|
+
|
|
20
|
+
- Full prompts are not written to Markdown reports.
|
|
21
|
+
- Source code is not included.
|
|
22
|
+
- File paths, URLs, emails, secret-like tokens, and long opaque strings are replaced in previews.
|
|
23
|
+
- Prompt rewrites are generated as generic templates and do not include raw private details.
|
|
24
|
+
- Project capsules use redacted project labels instead of full local paths.
|
|
25
|
+
- Config suggestions are written as review files and are not applied automatically.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Prompt Quality Rubric
|
|
2
|
+
|
|
3
|
+
Codex Coach scores prompts from 0 to 10 using local text only.
|
|
4
|
+
|
|
5
|
+
- Excellent: clear action plus enough context, or a brief reply that is unambiguous because the previous Codex turn asked for a choice.
|
|
6
|
+
- Good: actionable but missing one optional detail.
|
|
7
|
+
- Needs work: too short to identify the target, missing the failing symptom, or using vague words like "fix", "run", "delete", or "make better" without context.
|
|
8
|
+
|
|
9
|
+
Do not treat brevity as bad by itself. Brief prompts like "yes", "proceed", "run tests", or "commit" can be high quality when Codex has enough local context.
|
|
10
|
+
|
|
11
|
+
When recommending improvements, prefer templates:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
[action] in [project/file] because [symptom]. Success means [observable result].
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
Debug [error text]. I expected [expected behavior], but got [actual behavior].
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`codex-coach lint-prompt "..."` applies the same rubric to a single draft prompt. If the score is low, present the suggested rewrite as a template, not as a claim that Codex Coach knows the missing private details.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .install import install_from_source, uninstall
|
|
10
|
+
from .parser import iter_log_paths, scan_logs
|
|
11
|
+
from .paths import default_paths
|
|
12
|
+
from .prompts import score_prompt
|
|
13
|
+
from .reports import render_markdown_report, write_json_facts, write_markdown_report, write_suggestion_files
|
|
14
|
+
from .timeutil import parse_since
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv: list[str] | None = None) -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(prog="codex-coach", description="Local-first Codex usage coach")
|
|
19
|
+
parser.add_argument("--home", help="Override HOME for testing or portable installs")
|
|
20
|
+
parser.add_argument("--codex-home", help="Override Codex home directory")
|
|
21
|
+
parser.add_argument("--coach-home", help="Override Codex Coach output directory")
|
|
22
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
|
|
24
|
+
sub.add_parser("doctor", help="Check local Codex Coach setup")
|
|
25
|
+
|
|
26
|
+
scan = sub.add_parser("scan", help="Scan Codex logs and write JSON facts")
|
|
27
|
+
scan.add_argument("--since", default="7d", help="Window such as 7d, 2w, 24h, or ISO timestamp")
|
|
28
|
+
|
|
29
|
+
report = sub.add_parser("report", help="Write a Markdown coaching report")
|
|
30
|
+
report.add_argument("--since", default="7d")
|
|
31
|
+
report.add_argument("--mode", choices=("beginner", "expert"), default="beginner")
|
|
32
|
+
report.add_argument("--expert", action="store_true", help="Include raw expert metrics")
|
|
33
|
+
|
|
34
|
+
suggest = sub.add_parser("suggest-config", help="Write reviewable config suggestion files")
|
|
35
|
+
suggest.add_argument("--since", default="7d")
|
|
36
|
+
|
|
37
|
+
lint = sub.add_parser("lint-prompt", help="Score one prompt and suggest a safer rewrite")
|
|
38
|
+
lint.add_argument("prompt", nargs="*", help="Prompt text. Reads stdin when omitted.")
|
|
39
|
+
lint.add_argument("--json", action="store_true", help="Write machine-readable JSON")
|
|
40
|
+
|
|
41
|
+
install_cmd = sub.add_parser("install", help="Install command, plugin, skill, and default config")
|
|
42
|
+
install_cmd.add_argument("--source-root", default=_repo_root(), help="Source checkout root")
|
|
43
|
+
install_cmd.add_argument("--schedule", choices=("daily", "weekly", "none"), default="weekly")
|
|
44
|
+
|
|
45
|
+
sub.add_parser("uninstall", help="Remove installed command, plugin, skill, and scheduler files")
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args(argv)
|
|
48
|
+
paths = default_paths(home=args.home, codex_home=args.codex_home, coach_home=args.coach_home)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
if args.command == "doctor":
|
|
52
|
+
return _doctor(paths)
|
|
53
|
+
if args.command == "scan":
|
|
54
|
+
facts = _scan(paths, args.since)
|
|
55
|
+
print(f"Wrote facts to {paths.facts_dir / 'latest.json'}")
|
|
56
|
+
return 0
|
|
57
|
+
if args.command == "report":
|
|
58
|
+
facts = _scan(paths, args.since)
|
|
59
|
+
generated_at = datetime.now(UTC)
|
|
60
|
+
report_text = render_markdown_report(facts, generated_at=generated_at, expert=args.expert, mode=args.mode)
|
|
61
|
+
latest, weekly = write_markdown_report(report_text, paths.reports_dir, generated_at=generated_at)
|
|
62
|
+
print(f"Wrote report to {latest}")
|
|
63
|
+
print(f"Wrote weekly copy to {weekly}")
|
|
64
|
+
return 0
|
|
65
|
+
if args.command == "suggest-config":
|
|
66
|
+
facts = _scan(paths, args.since)
|
|
67
|
+
written = write_suggestion_files(facts, paths.suggestions_dir)
|
|
68
|
+
for path in written:
|
|
69
|
+
print(f"Wrote suggestion {path}")
|
|
70
|
+
return 0
|
|
71
|
+
if args.command == "lint-prompt":
|
|
72
|
+
prompt_text = " ".join(args.prompt).strip() if args.prompt else sys.stdin.read().strip()
|
|
73
|
+
return _lint_prompt(prompt_text, as_json=args.json)
|
|
74
|
+
if args.command == "install":
|
|
75
|
+
result = install_from_source(Path(args.source_root), paths, schedule=args.schedule)
|
|
76
|
+
for key, value in result.items():
|
|
77
|
+
print(f"{key}: {value}")
|
|
78
|
+
return 0
|
|
79
|
+
if args.command == "uninstall":
|
|
80
|
+
removed = uninstall(paths)
|
|
81
|
+
if removed:
|
|
82
|
+
for path in removed:
|
|
83
|
+
print(f"Removed {path}")
|
|
84
|
+
else:
|
|
85
|
+
print("Nothing to remove")
|
|
86
|
+
return 0
|
|
87
|
+
except Exception as exc: # noqa: BLE001 - CLI should provide a direct error.
|
|
88
|
+
print(f"codex-coach: {exc}", file=sys.stderr)
|
|
89
|
+
return 1
|
|
90
|
+
return 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _scan(paths, since: str) -> dict:
|
|
94
|
+
since_dt = parse_since(since)
|
|
95
|
+
paths.ensure_output_dirs()
|
|
96
|
+
facts = scan_logs(paths.codex_home, since_dt=since_dt, since_label=since)
|
|
97
|
+
facts["generated_at"] = datetime.now(UTC).isoformat(timespec="seconds")
|
|
98
|
+
write_json_facts(facts, paths.facts_dir / "latest.json")
|
|
99
|
+
return facts
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _doctor(paths) -> int:
|
|
103
|
+
log_paths = iter_log_paths(paths.codex_home)
|
|
104
|
+
print(f"home: {paths.home}")
|
|
105
|
+
print(f"codex_home: {paths.codex_home} {'OK' if paths.codex_home.exists() else 'MISSING'}")
|
|
106
|
+
print(f"coach_home: {paths.coach_home}")
|
|
107
|
+
print(f"log_files: {len(log_paths)}")
|
|
108
|
+
print(f"reports_dir: {paths.reports_dir}")
|
|
109
|
+
command = paths.home / ".local" / "bin" / "codex-coach"
|
|
110
|
+
print(f"command: {command} {'OK' if command.exists() else 'not installed'}")
|
|
111
|
+
plugin = paths.home / "plugins" / "codex-coach" / ".codex-plugin" / "plugin.json"
|
|
112
|
+
print(f"plugin: {plugin} {'OK' if plugin.exists() else 'not installed'}")
|
|
113
|
+
skill = paths.home / ".agents" / "skills" / "codex-coach" / "SKILL.md"
|
|
114
|
+
print(f"skill: {skill} {'OK' if skill.exists() else 'not installed'}")
|
|
115
|
+
return 0 if paths.codex_home.exists() else 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _lint_prompt(prompt_text: str, *, as_json: bool) -> int:
|
|
119
|
+
score = score_prompt(prompt_text)
|
|
120
|
+
result = {
|
|
121
|
+
"score": score.score,
|
|
122
|
+
"category": score.category,
|
|
123
|
+
"reason": score.reason,
|
|
124
|
+
"missing": list(score.missing),
|
|
125
|
+
"preview": score.preview,
|
|
126
|
+
"rewrite": score.rewrite,
|
|
127
|
+
}
|
|
128
|
+
if as_json:
|
|
129
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
130
|
+
return 0 if score.category != "needs_work" else 2
|
|
131
|
+
|
|
132
|
+
print(f"Score: {score.score}/10 ({score.category})")
|
|
133
|
+
print(f"Reason: {score.reason}")
|
|
134
|
+
if score.missing:
|
|
135
|
+
print(f"Missing: {', '.join(score.missing)}")
|
|
136
|
+
print(f"Preview: {score.preview}")
|
|
137
|
+
print(f"Try: {score.rewrite}")
|
|
138
|
+
return 0 if score.category != "needs_work" else 2
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _repo_root() -> str:
|
|
142
|
+
return str(Path(__file__).resolve().parents[2])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .paths import CoachPaths
|
|
9
|
+
|
|
10
|
+
PLUGIN_NAME = "codex-coach"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def install_from_source(source_root: Path, paths: CoachPaths, *, schedule: str = "weekly") -> dict[str, str]:
|
|
14
|
+
source_root = source_root.resolve()
|
|
15
|
+
paths.ensure_output_dirs()
|
|
16
|
+
paths.coach_home.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
_write_default_config(paths, schedule=schedule)
|
|
18
|
+
_copy_app(source_root, paths.app_dir)
|
|
19
|
+
_install_command(paths)
|
|
20
|
+
plugin_path = _install_plugin(source_root, paths.home)
|
|
21
|
+
skill_paths = _install_user_skills(source_root, paths.home)
|
|
22
|
+
marketplace = _update_marketplace(paths.home, plugin_path)
|
|
23
|
+
scheduler = _write_scheduler(paths, schedule=schedule)
|
|
24
|
+
return {
|
|
25
|
+
"coach_home": str(paths.coach_home),
|
|
26
|
+
"command": str(paths.home / ".local" / "bin" / "codex-coach"),
|
|
27
|
+
"plugin": str(plugin_path),
|
|
28
|
+
"skills": ", ".join(str(path) for path in skill_paths),
|
|
29
|
+
"marketplace": str(marketplace),
|
|
30
|
+
"scheduler": str(scheduler) if scheduler else "not configured",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def uninstall(paths: CoachPaths) -> list[str]:
|
|
35
|
+
removed: list[str] = []
|
|
36
|
+
command = paths.home / ".local" / "bin" / "codex-coach"
|
|
37
|
+
if command.exists():
|
|
38
|
+
command.unlink()
|
|
39
|
+
removed.append(str(command))
|
|
40
|
+
for target in (
|
|
41
|
+
paths.home / "plugins" / PLUGIN_NAME,
|
|
42
|
+
paths.home / ".agents" / "skills" / PLUGIN_NAME,
|
|
43
|
+
paths.home / ".codex" / "skills" / PLUGIN_NAME,
|
|
44
|
+
):
|
|
45
|
+
if target.exists():
|
|
46
|
+
shutil.rmtree(target)
|
|
47
|
+
removed.append(str(target))
|
|
48
|
+
_remove_marketplace_entry(paths.home)
|
|
49
|
+
for target in _scheduler_paths(paths):
|
|
50
|
+
if target.exists():
|
|
51
|
+
target.unlink()
|
|
52
|
+
removed.append(str(target))
|
|
53
|
+
return removed
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _copy_app(source_root: Path, app_dir: Path) -> None:
|
|
57
|
+
if app_dir.exists():
|
|
58
|
+
shutil.rmtree(app_dir)
|
|
59
|
+
ignore = shutil.ignore_patterns(".git", ".venv", "__pycache__", ".pytest_cache", "dist", "build", "*.egg-info")
|
|
60
|
+
shutil.copytree(source_root, app_dir, ignore=ignore)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _install_command(paths: CoachPaths) -> None:
|
|
64
|
+
bin_dir = paths.home / ".local" / "bin"
|
|
65
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
command = bin_dir / "codex-coach"
|
|
67
|
+
python = os.environ.get("PYTHON", "python3")
|
|
68
|
+
command.write_text(
|
|
69
|
+
"#!/usr/bin/env sh\n"
|
|
70
|
+
"set -eu\n"
|
|
71
|
+
f'PYTHONPATH="{paths.app_dir / "src"}${{PYTHONPATH:+:$PYTHONPATH}}" exec {python} -m codex_coach.cli "$@"\n',
|
|
72
|
+
encoding="utf-8",
|
|
73
|
+
)
|
|
74
|
+
command.chmod(0o755)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _install_plugin(source_root: Path, home: Path) -> Path:
|
|
78
|
+
plugin_root = home / "plugins" / PLUGIN_NAME
|
|
79
|
+
if plugin_root.exists():
|
|
80
|
+
shutil.rmtree(plugin_root)
|
|
81
|
+
plugin_root.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
for name in (".codex-plugin", "skills", "README.md", "LICENSE"):
|
|
83
|
+
source = source_root / name
|
|
84
|
+
target = plugin_root / name
|
|
85
|
+
if source.is_dir():
|
|
86
|
+
shutil.copytree(source, target)
|
|
87
|
+
elif source.exists():
|
|
88
|
+
shutil.copy2(source, target)
|
|
89
|
+
return plugin_root
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _install_user_skills(source_root: Path, home: Path) -> list[Path]:
|
|
93
|
+
skill_source = source_root / "skills" / PLUGIN_NAME
|
|
94
|
+
targets = [
|
|
95
|
+
home / ".agents" / "skills" / PLUGIN_NAME,
|
|
96
|
+
home / ".codex" / "skills" / PLUGIN_NAME,
|
|
97
|
+
]
|
|
98
|
+
installed: list[Path] = []
|
|
99
|
+
for target in targets:
|
|
100
|
+
if target.exists():
|
|
101
|
+
shutil.rmtree(target)
|
|
102
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
shutil.copytree(skill_source, target)
|
|
104
|
+
installed.append(target)
|
|
105
|
+
return installed
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _update_marketplace(home: Path, plugin_path: Path) -> Path:
|
|
109
|
+
marketplace = home / ".agents" / "plugins" / "marketplace.json"
|
|
110
|
+
marketplace.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
if marketplace.exists():
|
|
112
|
+
try:
|
|
113
|
+
data = json.loads(marketplace.read_text(encoding="utf-8"))
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
data = _new_marketplace()
|
|
116
|
+
else:
|
|
117
|
+
data = _new_marketplace()
|
|
118
|
+
plugins = [item for item in data.get("plugins", []) if item.get("name") != PLUGIN_NAME]
|
|
119
|
+
plugins.append(
|
|
120
|
+
{
|
|
121
|
+
"name": PLUGIN_NAME,
|
|
122
|
+
"source": {"source": "local", "path": "./plugins/codex-coach"},
|
|
123
|
+
"policy": {"installation": "AVAILABLE", "authentication": "ON_INSTALL"},
|
|
124
|
+
"category": "Productivity",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
data["plugins"] = plugins
|
|
128
|
+
marketplace.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
129
|
+
return marketplace
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _remove_marketplace_entry(home: Path) -> None:
|
|
133
|
+
marketplace = home / ".agents" / "plugins" / "marketplace.json"
|
|
134
|
+
if not marketplace.exists():
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
data = json.loads(marketplace.read_text(encoding="utf-8"))
|
|
138
|
+
except json.JSONDecodeError:
|
|
139
|
+
return
|
|
140
|
+
data["plugins"] = [item for item in data.get("plugins", []) if item.get("name") != PLUGIN_NAME]
|
|
141
|
+
marketplace.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _new_marketplace() -> dict:
|
|
145
|
+
return {"name": "personal", "interface": {"displayName": "Personal"}, "plugins": []}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _write_default_config(paths: CoachPaths, *, schedule: str) -> None:
|
|
149
|
+
if paths.config_file.exists():
|
|
150
|
+
return
|
|
151
|
+
paths.config_file.write_text(
|
|
152
|
+
"\n".join(
|
|
153
|
+
[
|
|
154
|
+
"# Codex Coach config",
|
|
155
|
+
"redact_prompts = true",
|
|
156
|
+
"include_source_code = false",
|
|
157
|
+
f'schedule = "{schedule}"',
|
|
158
|
+
'default_since = "7d"',
|
|
159
|
+
"",
|
|
160
|
+
]
|
|
161
|
+
),
|
|
162
|
+
encoding="utf-8",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _write_scheduler(paths: CoachPaths, *, schedule: str) -> Path | None:
|
|
167
|
+
if schedule == "none":
|
|
168
|
+
return None
|
|
169
|
+
if schedule not in {"daily", "weekly"}:
|
|
170
|
+
raise ValueError(f"unsupported schedule: {schedule}")
|
|
171
|
+
systemd_dir = paths.home / ".config" / "systemd" / "user"
|
|
172
|
+
if os.name == "posix" and Path("/run/systemd").exists():
|
|
173
|
+
systemd_dir.mkdir(parents=True, exist_ok=True)
|
|
174
|
+
service = systemd_dir / "codex-coach.service"
|
|
175
|
+
timer = systemd_dir / "codex-coach.timer"
|
|
176
|
+
service.write_text(
|
|
177
|
+
"[Unit]\nDescription=Codex Coach report\n\n"
|
|
178
|
+
"[Service]\nType=oneshot\n"
|
|
179
|
+
f"ExecStart={paths.home / '.local' / 'bin' / 'codex-coach'} report --since 7d\n",
|
|
180
|
+
encoding="utf-8",
|
|
181
|
+
)
|
|
182
|
+
calendar = "09:00" if schedule == "daily" else "Sun 09:00"
|
|
183
|
+
timer.write_text(
|
|
184
|
+
f"[Unit]\nDescription=Run Codex Coach {schedule}\n\n"
|
|
185
|
+
f"[Timer]\nOnCalendar={calendar}\nPersistent=true\n\n"
|
|
186
|
+
"[Install]\nWantedBy=timers.target\n",
|
|
187
|
+
encoding="utf-8",
|
|
188
|
+
)
|
|
189
|
+
return timer
|
|
190
|
+
|
|
191
|
+
launch_agents = paths.home / "Library" / "LaunchAgents"
|
|
192
|
+
if os.name == "posix" and (paths.home / "Library").exists():
|
|
193
|
+
launch_agents.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
plist = launch_agents / f"com.codex-coach.{schedule}.plist"
|
|
195
|
+
interval = (
|
|
196
|
+
"<dict><key>Hour</key><integer>9</integer></dict>"
|
|
197
|
+
if schedule == "daily"
|
|
198
|
+
else "<dict><key>Weekday</key><integer>0</integer><key>Hour</key><integer>9</integer></dict>"
|
|
199
|
+
)
|
|
200
|
+
plist.write_text(
|
|
201
|
+
f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
202
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
203
|
+
<plist version="1.0">
|
|
204
|
+
<dict>
|
|
205
|
+
<key>Label</key><string>com.codex-coach.{schedule}</string>
|
|
206
|
+
<key>ProgramArguments</key>
|
|
207
|
+
<array>
|
|
208
|
+
<string>{paths.home / '.local' / 'bin' / 'codex-coach'}</string>
|
|
209
|
+
<string>report</string>
|
|
210
|
+
<string>--since</string>
|
|
211
|
+
<string>7d</string>
|
|
212
|
+
</array>
|
|
213
|
+
<key>StartCalendarInterval</key>
|
|
214
|
+
{interval}
|
|
215
|
+
</dict>
|
|
216
|
+
</plist>
|
|
217
|
+
""",
|
|
218
|
+
encoding="utf-8",
|
|
219
|
+
)
|
|
220
|
+
return plist
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _scheduler_paths(paths: CoachPaths) -> list[Path]:
|
|
225
|
+
return [
|
|
226
|
+
paths.home / ".config" / "systemd" / "user" / "codex-coach.service",
|
|
227
|
+
paths.home / ".config" / "systemd" / "user" / "codex-coach.timer",
|
|
228
|
+
paths.home / "Library" / "LaunchAgents" / "com.codex-coach.weekly.plist",
|
|
229
|
+
paths.home / "Library" / "LaunchAgents" / "com.codex-coach.daily.plist",
|
|
230
|
+
]
|