Skip to content

Commit bdd4f74

Browse files
committed
Release v0.10.0
1 parent 59b4051 commit bdd4f74

6 files changed

Lines changed: 253 additions & 17 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,22 @@ codexapi agent whoami
158158
codexapi agent install-cron
159159
```
160160

161+
If you skip `install-cron`, `codexapi agent start` warns on stderr because
162+
background wakes will not run until the scheduler hook is installed.
163+
When `gh` is installed and authenticated, `agent start` also captures a
164+
background-safe `GH_TOKEN` automatically if your shell did not already export
165+
`GH_TOKEN` or `GITHUB_TOKEN`.
166+
161167
Start a goal-directed agent that decides for itself when it is done:
162168

163169
```bash
164170
codexapi agent start --name ci-fixer \
165171
"Watch CI, fix failing tests, open or update a PR, and stop when the work is done."
166172
```
167173

174+
Add `--wait` if you want `start` to block for the first local wake instead of
175+
just scheduling it.
176+
168177
Start a persistent watcher that keeps running until you stop it:
169178

170179
```bash
@@ -182,9 +191,12 @@ codexapi agent show ci-fixer
182191
codexapi agent read ci-fixer
183192
codexapi agent book ci-fixer
184193
codexapi agent send ci-fixer "Prefer the smallest safe fix."
194+
codexapi agent send --wait ci-fixer "Reply now if you can handle this immediately."
185195
codexapi agent wake ci-fixer
196+
codexapi agent wake --wait ci-fixer
186197
codexapi agent pause ci-fixer
187198
codexapi agent resume ci-fixer
199+
codexapi agent resume --wait ci-fixer
188200
codexapi agent cancel ci-fixer
189201
codexapi agent delete ci-fixer
190202
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "codexapi"
7-
version = "0.9.0"
7+
version = "0.10.0"
88
description = "Minimal Python API for running the Codex CLI."
99
readme = "README.md"
1010
requires-python = ">=3.8"

src/codexapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@
2727
"task_result",
2828
"lead",
2929
]
30-
__version__ = "0.9.0"
30+
__version__ = "0.10.0"

src/codexapi/agents.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,17 @@ def install_cron(home=None, hostname=None, python_executable=None, path_value=No
412412
}
413413

414414

415+
def cron_installed(home=None, hostname=None):
416+
"""Return whether this home and host have an installed scheduler hook."""
417+
home = _resolve_home(home)
418+
host = hostname or current_hostname()
419+
tag = _cron_tag(home, host)
420+
wrapper = home / "bin" / "agent-tick"
421+
crontab = _read_crontab()
422+
installed = any(raw.strip().endswith(f"# {tag}") for raw in crontab.splitlines())
423+
return installed and wrapper.exists()
424+
425+
415426
def uninstall_cron(home=None, hostname=None):
416427
"""Remove the cron entry for this home and host."""
417428
home = _resolve_home(home)
@@ -915,13 +926,31 @@ def _resolve_cwd(cwd):
915926

916927
def _capture_env():
917928
env = {}
918-
for key in ("PATH", "VIRTUAL_ENV"):
919-
value = os.environ.get(key)
920-
if value:
921-
env[key] = value
929+
for key, value in os.environ.items():
930+
if key in ("CODEXAPI_AGENT_ID", "CODEXAPI_AGENT_NAME", "CODEXAPI_AGENT_PARENT_ID"):
931+
continue
932+
env[key] = value
933+
if not (env.get("GH_TOKEN") or env.get("GITHUB_TOKEN")):
934+
gh_token = _gh_auth_token()
935+
if gh_token:
936+
env["GH_TOKEN"] = gh_token
922937
return env
923938

924939

940+
def _gh_auth_token():
941+
"""Return the active gh auth token when available."""
942+
if shutil.which("gh") is None:
943+
return ""
944+
result = subprocess.run(
945+
["gh", "auth", "token"],
946+
capture_output=True,
947+
text=True,
948+
)
949+
if result.returncode != 0:
950+
return ""
951+
return (result.stdout or "").strip()
952+
953+
925954
def _parent_identity(home, parent_ref):
926955
"""Return the resolved parent agent id and name, if any."""
927956
if parent_ref is not None and str(parent_ref).strip():

src/codexapi/cli.py

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import re
55
import select
6+
import shlex
67
import shutil
78
import subprocess
89
import sys
@@ -17,6 +18,7 @@
1718
from .agents import (
1819
codexapi_home,
1920
control_agent,
21+
cron_installed as agent_cron_installed,
2022
current_hostname,
2123
delete_agent as delete_managed_agent,
2224
install_cron as install_agent_cron,
@@ -194,6 +196,39 @@ def _print_managed_agent_identity():
194196
print(f"Home: {codexapi_home()}")
195197

196198

199+
def _agent_install_cron_command():
200+
parts = []
201+
home = os.environ.get("CODEXAPI_HOME", "").strip()
202+
host = os.environ.get("CODEXAPI_HOSTNAME", "").strip()
203+
if home:
204+
parts.append(f"CODEXAPI_HOME={shlex.quote(home)}")
205+
if host:
206+
parts.append(f"CODEXAPI_HOSTNAME={shlex.quote(host)}")
207+
parts.extend(["codexapi", "agent", "install-cron"])
208+
return " ".join(parts)
209+
210+
211+
def _warn_agent_scheduler_missing():
212+
try:
213+
installed = agent_cron_installed()
214+
except Exception as exc:
215+
print(
216+
"Warning: could not verify whether the codexapi agent scheduler hook is installed.",
217+
file=sys.stderr,
218+
)
219+
print(f"Reason: {exc}", file=sys.stderr)
220+
print(f"Install it with: {_agent_install_cron_command()}", file=sys.stderr)
221+
return
222+
if installed:
223+
return
224+
print(
225+
"Warning: no codexapi agent scheduler hook is installed for this CODEXAPI_HOME. "
226+
"Background agent wakes will not run until you install it.",
227+
file=sys.stderr,
228+
)
229+
print(f"Install it with: {_agent_install_cron_command()}", file=sys.stderr)
230+
231+
197232
def _send_reply_info(agent_ref, message_id):
198233
"""Return the matching run reply for one sent message, if already delivered."""
199234
shown = show_managed_agent(agent_ref)
@@ -1401,7 +1436,7 @@ def main(argv=None):
14011436

14021437
agent_start = agent_subparsers.add_parser(
14031438
"start",
1404-
help="Create a durable agent.",
1439+
help="Create a durable agent and return immediately unless --wait is set.",
14051440
)
14061441
agent_start.add_argument(
14071442
"prompt",
@@ -1445,6 +1480,11 @@ def main(argv=None):
14451480
"--flags",
14461481
help="Additional raw CLI flags to pass to the backend.",
14471482
)
1483+
agent_start.add_argument(
1484+
"--wait",
1485+
action="store_true",
1486+
help="Wait for the first local wake to finish instead of just scheduling it.",
1487+
)
14481488

14491489
agent_subparsers.add_parser(
14501490
"list",
@@ -1481,21 +1521,32 @@ def main(argv=None):
14811521

14821522
agent_send = agent_subparsers.add_parser(
14831523
"send",
1484-
help="Queue a message for an agent.",
1524+
help="Queue a message for an agent and return immediately unless --wait is set.",
14851525
)
14861526
agent_send.add_argument("agent_ref", help="Agent id, unique prefix, or name.")
14871527
agent_send.add_argument("message", help="Message to queue.")
14881528
agent_send.add_argument("--author", help="Author label for the message.")
1529+
agent_send.add_argument(
1530+
"--wait",
1531+
action="store_true",
1532+
help="Wait for a local wake after queueing the message.",
1533+
)
14891534

14901535
for subcommand, help_text in (
1491-
("wake", "Request an extra wake for an agent."),
1536+
("wake", "Request an extra wake for an agent and return immediately unless --wait is set."),
14921537
("pause", "Pause an agent."),
1493-
("resume", "Resume a paused agent."),
1538+
("resume", "Resume a paused agent and return immediately unless --wait is set."),
14941539
("cancel", "Cancel an agent."),
14951540
):
14961541
subparser = agent_subparsers.add_parser(subcommand, help=help_text)
14971542
subparser.add_argument("agent_ref", help="Agent id, unique prefix, or name.")
14981543
subparser.add_argument("--author", help="Author label for the command.")
1544+
if subcommand in ("wake", "resume"):
1545+
subparser.add_argument(
1546+
"--wait",
1547+
action="store_true",
1548+
help="Wait for a local wake after queueing the command.",
1549+
)
14991550

15001551
agent_delete = agent_subparsers.add_parser(
15011552
"delete",
@@ -1834,6 +1885,10 @@ def main(argv=None):
18341885
args.yolo,
18351886
args.flags,
18361887
)
1888+
result["waited"] = bool(args.wait)
1889+
if args.wait:
1890+
result["nudge"] = nudge_agent(result["id"])
1891+
_warn_agent_scheduler_missing()
18371892
print(json.dumps(result, indent=2, sort_keys=True))
18381893
return
18391894
if args.agent_command == "list":
@@ -1855,18 +1910,32 @@ def main(argv=None):
18551910
return
18561911
if args.agent_command == "send":
18571912
result = send_agent(args.agent_ref, args.message, args.author)
1858-
result["nudge"] = nudge_agent(args.agent_ref)
1859-
reply_info = _send_reply_info(args.agent_ref, result["id"])
1860-
if reply_info:
1861-
result.update(reply_info)
1913+
result["waited"] = bool(args.wait)
1914+
if args.wait:
1915+
result["nudge"] = nudge_agent(args.agent_ref)
1916+
reply_info = _send_reply_info(args.agent_ref, result["id"])
1917+
if reply_info:
1918+
result.update(reply_info)
1919+
print(json.dumps(result, indent=2, sort_keys=True))
1920+
return
1921+
if args.agent_command in ("wake", "resume"):
1922+
result = control_agent(
1923+
args.agent_ref,
1924+
args.agent_command,
1925+
args.author,
1926+
)
1927+
result["waited"] = bool(args.wait)
1928+
if args.wait:
1929+
result["nudge"] = nudge_agent(args.agent_ref)
18621930
print(json.dumps(result, indent=2, sort_keys=True))
18631931
return
1864-
if args.agent_command in ("wake", "pause", "resume", "cancel"):
1932+
if args.agent_command in ("pause", "cancel"):
18651933
result = control_agent(
18661934
args.agent_ref,
18671935
args.agent_command,
18681936
args.author,
18691937
)
1938+
result["waited"] = False
18701939
result["nudge"] = nudge_agent(args.agent_ref)
18711940
print(json.dumps(result, indent=2, sort_keys=True))
18721941
return

0 commit comments

Comments
 (0)