Add portable Codex project skills
This commit is contained in:
48
.codex/skills/codex-thread-cwd/SKILL.md
Normal file
48
.codex/skills/codex-thread-cwd/SKILL.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: codex-thread-cwd
|
||||
description: Change a Codex thread working directory/cwd on the Codex side, not in downstream tool databases. Use when asked to change, move, fix, align, or sync a Codex thread workspace/cwd, especially for app-server threads or client integrations where dependent tools should sync from Codex afterward.
|
||||
---
|
||||
|
||||
# Codex Thread CWD
|
||||
|
||||
Use this skill to change a Codex thread's stored working directory (`cwd`) in Codex state, then let clients such as a Telegram bot sync from Codex. Do not edit downstream client databases as the source of truth.
|
||||
|
||||
## Required Inputs
|
||||
|
||||
- Target `thread_id`, usually a Codex UUID. If the user refers to a client-specific current thread, inspect that client's state only to discover the Codex `thread_id`; do not edit that client state unless explicitly asked.
|
||||
- Target absolute `cwd`. Do not assume a machine-specific path; derive it from the user's request, the current repo, or an explicit argument.
|
||||
|
||||
## Preferred Workflow
|
||||
|
||||
1. Resolve the target thread ID.
|
||||
- For this repo's Telegram bot, read its configured SQLite DB only to discover `sessions.active_thread_id -> threads.codex_thread_id` for the relevant Telegram user.
|
||||
- If ambiguous, list candidates and ask the user to choose.
|
||||
2. Confirm the target cwd is absolute and exists, unless the user intentionally wants a path that does not exist yet.
|
||||
3. Run the bundled script from the skill directory:
|
||||
|
||||
```bash
|
||||
python3 path/to/codex-thread-cwd/scripts/set_thread_cwd.py THREAD_ID /absolute/new/cwd
|
||||
```
|
||||
|
||||
Use `--codex-home`, `--state-db`, or `CODEX_HOME` when the Codex state is not under the current user's default Codex home.
|
||||
|
||||
4. Verify Codex-side state, preferably with app-server `thread/read` if a socket is available. DB-level verification from the script is acceptable when app-server is unavailable.
|
||||
5. Trigger the dependent tool's normal sync path, such as asking a bot for `/workspace` or `/threads`. Do not manually insert or update downstream workspace rows unless the user explicitly asks for emergency repair.
|
||||
|
||||
## Notes
|
||||
|
||||
- The app-server schema exposes `cwd` on `thread/resume`, `thread/fork`, and `turn/start`; `thread/metadata/update` does not patch `cwd`.
|
||||
- In observed behavior, `thread/resume` with `cwd` may return the original cwd and not mutate existing stored thread cwd.
|
||||
- `turn/start.cwd` is schema-supported as an override for that turn and subsequent turns, but it starts a new turn.
|
||||
- The proven no-turn approach is to update Codex's own `state_*.sqlite` `threads.cwd` row and the first `session_meta.payload.cwd` line in the rollout JSONL. This is Codex-side state, not downstream client state.
|
||||
- The script preserves a rollout backup by default; it writes `<rollout>.bak.<timestamp>` unless `--no-backup` is passed.
|
||||
|
||||
## Verification Examples
|
||||
|
||||
Inspect Codex state directly:
|
||||
|
||||
```bash
|
||||
python3 path/to/codex-thread-cwd/scripts/set_thread_cwd.py THREAD_ID /absolute/new/cwd --verify-only
|
||||
```
|
||||
|
||||
Read through app-server and check `result.thread.cwd` equals the requested path.
|
||||
4
.codex/skills/codex-thread-cwd/agents/openai.yaml
Normal file
4
.codex/skills/codex-thread-cwd/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Codex Thread CWD"
|
||||
short_description: "Change Codex thread cwd safely."
|
||||
default_prompt: "Change the active Codex thread cwd/workspace to the requested directory, then verify app-server or Codex state reports it."
|
||||
149
.codex/skills/codex-thread-cwd/scripts/set_thread_cwd.py
Executable file
149
.codex/skills/codex-thread-cwd/scripts/set_thread_cwd.py
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Set a Codex app-server thread cwd in Codex-side state.
|
||||
|
||||
Updates:
|
||||
- <CODEX_HOME>/state_*.sqlite threads.cwd for the thread id
|
||||
- the rollout JSONL first session_meta payload.cwd, using rollout_path from state DB
|
||||
|
||||
This intentionally does not touch downstream client databases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def codex_home(explicit: str | None) -> Path:
|
||||
if explicit:
|
||||
return Path(explicit).expanduser()
|
||||
return Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex").expanduser()
|
||||
|
||||
|
||||
def find_state_db(home: Path, explicit: str | None) -> Path:
|
||||
if explicit:
|
||||
path = Path(explicit).expanduser()
|
||||
if not path.exists():
|
||||
raise SystemExit(f"state DB does not exist: {path}")
|
||||
return path
|
||||
candidates = sorted(home.glob("state_*.sqlite"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if not candidates:
|
||||
raise SystemExit(f"no state_*.sqlite found under {home}; pass --codex-home or --state-db")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def validate_cwd(cwd: str, require_exists: bool) -> str:
|
||||
path = Path(cwd).expanduser()
|
||||
if not path.is_absolute():
|
||||
raise SystemExit(f"cwd must be absolute: {cwd}")
|
||||
clean = str(path.resolve() if path.exists() else path)
|
||||
if clean == "/":
|
||||
raise SystemExit("refusing to set cwd to filesystem root")
|
||||
if require_exists and not Path(clean).is_dir():
|
||||
raise SystemExit(f"cwd is not an existing directory: {clean}")
|
||||
return clean
|
||||
|
||||
|
||||
def read_thread(conn: sqlite3.Connection, thread_id: str) -> sqlite3.Row:
|
||||
conn.row_factory = sqlite3.Row
|
||||
row = conn.execute(
|
||||
"select id, cwd, rollout_path, updated_at, updated_at_ms from threads where id = ?",
|
||||
(thread_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise SystemExit(f"thread not found in Codex state DB: {thread_id}")
|
||||
return row
|
||||
|
||||
|
||||
def update_state_db(db: Path, thread_id: str, cwd: str) -> sqlite3.Row:
|
||||
now_ms = int(time.time() * 1000)
|
||||
now_s = now_ms // 1000
|
||||
conn = sqlite3.connect(str(db), timeout=10)
|
||||
try:
|
||||
changed = conn.execute(
|
||||
"update threads set cwd = ?, updated_at = ?, updated_at_ms = ? where id = ?",
|
||||
(cwd, now_s, now_ms, thread_id),
|
||||
).rowcount
|
||||
if changed != 1:
|
||||
raise SystemExit(f"updated {changed} Codex thread rows, expected 1")
|
||||
conn.commit()
|
||||
return read_thread(conn, thread_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def update_rollout(path: Path, thread_id: str, cwd: str, backup: bool) -> None:
|
||||
if not path.exists():
|
||||
raise SystemExit(f"rollout JSONL does not exist: {path}")
|
||||
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
if not lines:
|
||||
raise SystemExit(f"rollout JSONL is empty: {path}")
|
||||
first = json.loads(lines[0])
|
||||
payload = first.get("payload") or {}
|
||||
if first.get("type") != "session_meta" or payload.get("id") != thread_id:
|
||||
raise SystemExit(f"first rollout line is not session_meta for {thread_id}: {path}")
|
||||
payload["cwd"] = cwd
|
||||
first["payload"] = payload
|
||||
lines[0] = json.dumps(first, separators=(",", ":")) + "\n"
|
||||
|
||||
if backup:
|
||||
backup_path = path.with_name(path.name + f".bak.{int(time.time())}")
|
||||
shutil.copy2(path, backup_path)
|
||||
print(f"backup: {backup_path}")
|
||||
|
||||
fd, tmp_name = tempfile.mkstemp(dir=str(path.parent), prefix=path.name + ".", text=True)
|
||||
tmp = Path(tmp_name)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
handle.writelines(lines)
|
||||
os.chmod(tmp, path.stat().st_mode & 0o777)
|
||||
os.replace(tmp, path)
|
||||
finally:
|
||||
if tmp.exists():
|
||||
tmp.unlink()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Set Codex thread cwd in Codex-side state")
|
||||
parser.add_argument("thread_id")
|
||||
parser.add_argument("cwd")
|
||||
parser.add_argument("--codex-home", help="Codex home directory; defaults to CODEX_HOME or ~/.codex")
|
||||
parser.add_argument("--state-db", help="Path to Codex state_*.sqlite; defaults to newest under Codex home")
|
||||
parser.add_argument("--allow-missing-cwd", action="store_true", help="Allow cwd path that does not exist yet")
|
||||
parser.add_argument("--no-backup", action="store_true", help="Do not create rollout JSONL backup")
|
||||
parser.add_argument("--verify-only", action="store_true", help="Only print current Codex cwd for thread")
|
||||
args = parser.parse_args()
|
||||
|
||||
home = codex_home(args.codex_home)
|
||||
db = find_state_db(home, args.state_db)
|
||||
cwd = validate_cwd(args.cwd, require_exists=not args.allow_missing_cwd)
|
||||
|
||||
conn = sqlite3.connect(str(db), timeout=10)
|
||||
try:
|
||||
row = read_thread(conn, args.thread_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
rollout = Path(row["rollout_path"]).expanduser()
|
||||
print(f"state_db: {db}")
|
||||
print(f"thread_id: {args.thread_id}")
|
||||
print(f"before_cwd: {row['cwd']}")
|
||||
print(f"rollout: {rollout}")
|
||||
|
||||
if args.verify_only:
|
||||
return 0 if row["cwd"] == cwd else 1
|
||||
|
||||
after = update_state_db(db, args.thread_id, cwd)
|
||||
update_rollout(rollout, args.thread_id, cwd, backup=not args.no_backup)
|
||||
print(f"after_cwd: {after['cwd']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
4
.codex/skills/thread-naming/agents/openai.yaml
Normal file
4
.codex/skills/thread-naming/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Thread Naming"
|
||||
short_description: "Choose concise Codex thread names."
|
||||
default_prompt: "Rename the current thread with a short useful title that matches the current task."
|
||||
Reference in New Issue
Block a user