overcodex 0.1.0__py3-none-any.whl
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.
- overcodex/__init__.py +4 -0
- overcodex/cli.py +53 -0
- overcodex/payload/bin/codex-swap +320 -0
- overcodex/payload/codex/AGENTS-ULTRACODE.md +54 -0
- overcodex/payload/config/hooks-block.toml.tpl +42 -0
- overcodex/payload/hooks/overcodex-ctx-lib.sh +66 -0
- overcodex/payload/hooks/overcodex-ctx-watch.sh +94 -0
- overcodex/payload/hooks/overcodex-handoff-inject.sh +94 -0
- overcodex/payload/hooks/overcodex-notify.sh +64 -0
- overcodex/payload/hooks/overcodex-precompact-offer.sh +40 -0
- overcodex/payload/install.sh +448 -0
- overcodex/payload/prompts/handoff-cancel.md +23 -0
- overcodex/payload/prompts/handoff-status.md +26 -0
- overcodex/payload/prompts/handoff.md +57 -0
- overcodex/payload/shell/zshrc-snippet.sh +24 -0
- overcodex/payload/uninstall.sh +196 -0
- overcodex-0.1.0.dist-info/METADATA +186 -0
- overcodex-0.1.0.dist-info/RECORD +21 -0
- overcodex-0.1.0.dist-info/WHEEL +4 -0
- overcodex-0.1.0.dist-info/entry_points.txt +2 -0
- overcodex-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# overcodex-handoff-inject.sh — Codex SessionStart hook.
|
|
3
|
+
# Ported from overclaude's hooks/handoff-inject.sh. Injects a pending handoff
|
|
4
|
+
# package for this cwd into the new session's context, then archives it.
|
|
5
|
+
# Contract: always exit 0, silent on every error path, defensive jq parsing,
|
|
6
|
+
# tolerate a missing state root.
|
|
7
|
+
#
|
|
8
|
+
# Schema notes (see config/hooks.json header for the full write-up):
|
|
9
|
+
# session-start.command.input carries `source`: "startup"|"resume"|"clear"|
|
|
10
|
+
# "compact". We act ONLY on "startup" — mirroring overclaude's Claude Code
|
|
11
|
+
# matcher, which is also "startup"-only — so a mid-session /clear, a `codex
|
|
12
|
+
# resume`, or the SessionStart that fires right after PreCompact's own
|
|
13
|
+
# auto-compaction does not re-inject a stale package. This is a deliberate,
|
|
14
|
+
# possibly-too-narrow choice: needs live validation that `codex resume` does
|
|
15
|
+
# NOT also need this path (if handoff packages should survive a resume too,
|
|
16
|
+
# widen to `startup|resume`).
|
|
17
|
+
# session-start.command.output supports hookSpecificOutput.additionalContext
|
|
18
|
+
# (hookEventName must echo "SessionStart") — that's how the package reaches
|
|
19
|
+
# the new session; there is no plain-stdout passthrough the way Claude Code's
|
|
20
|
+
# UserPromptSubmit works, so output MUST be well-formed JSON.
|
|
21
|
+
exec 2>/dev/null
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
25
|
+
STATE_ROOT="$CODEX_HOME/overcodex"
|
|
26
|
+
ARCHIVE_DIR="$STATE_ROOT/handoff-archive"
|
|
27
|
+
PENDING_TTL=600
|
|
28
|
+
|
|
29
|
+
INPUT="$(cat)" || INPUT=""
|
|
30
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
31
|
+
|
|
32
|
+
source_field="$(printf '%s' "$INPUT" | jq -r '.source // empty' 2>/dev/null)"
|
|
33
|
+
[ "$source_field" = "startup" ] || exit 0
|
|
34
|
+
|
|
35
|
+
cwd="$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)"
|
|
36
|
+
[ -n "$cwd" ] || cwd="$PWD"
|
|
37
|
+
|
|
38
|
+
# Pending-file path: codex-swap would be the canonical resolver if/when it
|
|
39
|
+
# grows a `path handoff --cwd` subcommand (it does not, as of this writing —
|
|
40
|
+
# it's an account-swap tool only); fall back to the shared hash convention
|
|
41
|
+
# (identical result) unconditionally today. The probe is kept for forward
|
|
42
|
+
# compatibility and is harmless: an unknown codex-swap subcommand prints usage
|
|
43
|
+
# to stderr (redirected away) and exits non-zero, leaving P empty.
|
|
44
|
+
P=""
|
|
45
|
+
if command -v codex-swap >/dev/null 2>&1; then
|
|
46
|
+
P="$(codex-swap path handoff --cwd "$cwd" 2>/dev/null)"
|
|
47
|
+
elif [ -x "$HOME/.local/bin/codex-swap" ]; then
|
|
48
|
+
P="$("$HOME/.local/bin/codex-swap" path handoff --cwd "$cwd" 2>/dev/null)"
|
|
49
|
+
fi
|
|
50
|
+
if [ -z "$P" ]; then
|
|
51
|
+
h="$(printf '%s' "$cwd" | /usr/bin/shasum -a 256 | awk '{print $1}' | cut -c1-12)"
|
|
52
|
+
[ -n "$h" ] || exit 0
|
|
53
|
+
P="$STATE_ROOT/handoff-pending-$h.md"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
[ -f "$P" ] || exit 0
|
|
57
|
+
|
|
58
|
+
# Line 1 must be: <!-- handoff cwd="<abs>" created="<epoch-int>" -->
|
|
59
|
+
header="$(head -n 1 "$P")"
|
|
60
|
+
emb_cwd="$(printf '%s' "$header" | sed -n 's/^<!-- handoff cwd="\(.*\)" created="[0-9][0-9]*" -->[[:space:]]*$/\1/p')"
|
|
61
|
+
emb_created="$(printf '%s' "$header" | sed -n 's/^<!-- handoff cwd=".*" created="\([0-9][0-9]*\)" -->[[:space:]]*$/\1/p')"
|
|
62
|
+
|
|
63
|
+
# Malformed header -> leave file, silent.
|
|
64
|
+
{ [ -n "$emb_cwd" ] && [ -n "$emb_created" ]; } || exit 0
|
|
65
|
+
|
|
66
|
+
# cwd mismatch -> leave file, silent.
|
|
67
|
+
[ "$emb_cwd" = "$cwd" ] || exit 0
|
|
68
|
+
|
|
69
|
+
now="$(date +%s)"
|
|
70
|
+
age=$(( now - emb_created ))
|
|
71
|
+
|
|
72
|
+
# Archive name: <YYYYmmdd-HHMMSS>-<HASH>.md (UTC); hash taken from filename.
|
|
73
|
+
base="${P##*/}"
|
|
74
|
+
fhash="${base#handoff-pending-}"
|
|
75
|
+
fhash="${fhash%.md}"
|
|
76
|
+
ts="$(date -u +%Y%m%d-%H%M%S)"
|
|
77
|
+
|
|
78
|
+
if [ "$age" -ge "$PENDING_TTL" ]; then
|
|
79
|
+
# Expired -> archive with -expired suffix, no output.
|
|
80
|
+
mkdir -p "$ARCHIVE_DIR" 2>/dev/null && mv -f "$P" "$ARCHIVE_DIR/$ts-$fhash-expired.md" 2>/dev/null
|
|
81
|
+
exit 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Claim first (atomic mv), then read from the archived path — prevents a
|
|
85
|
+
# concurrent same-cwd startup from injecting a dangling header after losing
|
|
86
|
+
# the race for the pending file.
|
|
87
|
+
A="$ARCHIVE_DIR/$ts-$fhash.md"
|
|
88
|
+
{ mkdir -p "$ARCHIVE_DIR" && mv "$P" "$A"; } 2>/dev/null || exit 0
|
|
89
|
+
|
|
90
|
+
body="$(tail -n +2 "$A" 2>/dev/null)"
|
|
91
|
+
ctx="$(printf '%s\n\n%s' "## Handoff from previous session (loaded by handoff-inject)" "$body")"
|
|
92
|
+
|
|
93
|
+
jq -n --arg ctx "$ctx" '{hookSpecificOutput: {hookEventName: "SessionStart", additionalContext: $ctx}}'
|
|
94
|
+
exit 0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# overcodex-notify.sh — Codex Stop hook.
|
|
3
|
+
# Ported from overclaude's hooks/ctx-notify.sh. Passive banner: when context
|
|
4
|
+
# usage (derived from the rollout transcript — see overcodex-ctx-lib.sh) sits
|
|
5
|
+
# in a threshold band that has not been bannered yet, emit
|
|
6
|
+
# {"systemMessage": ...}. NEVER emits a "decision" field (stop.command.output
|
|
7
|
+
# supports one, but we deliberately never touch it — this hook only informs,
|
|
8
|
+
# it never blocks the turn). Contract: always exit 0, silent on every error
|
|
9
|
+
# path, defensive jq parsing, no-op when stop_hook_active.
|
|
10
|
+
exec 2>/dev/null
|
|
11
|
+
set -u
|
|
12
|
+
|
|
13
|
+
HOOKS_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" || exit 0
|
|
14
|
+
# shellcheck source=./overcodex-ctx-lib.sh
|
|
15
|
+
. "$HOOKS_DIR/overcodex-ctx-lib.sh" 2>/dev/null || exit 0
|
|
16
|
+
|
|
17
|
+
# Thresholds.
|
|
18
|
+
T1=60
|
|
19
|
+
T2=75
|
|
20
|
+
T3=85
|
|
21
|
+
|
|
22
|
+
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
23
|
+
CTX_DIR="$CODEX_HOME/overcodex/ctx"
|
|
24
|
+
|
|
25
|
+
INPUT="$(cat)" || INPUT=""
|
|
26
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
27
|
+
|
|
28
|
+
# Never act on our own stop cycle.
|
|
29
|
+
sha="$(printf '%s' "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)"
|
|
30
|
+
[ "$sha" = "true" ] && exit 0
|
|
31
|
+
|
|
32
|
+
session_id="$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)"
|
|
33
|
+
[ -n "$session_id" ] || exit 0
|
|
34
|
+
|
|
35
|
+
transcript_path="$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)"
|
|
36
|
+
|
|
37
|
+
pct="$(ctx_pct_from_transcript "$transcript_path")" || exit 0
|
|
38
|
+
case "$pct" in ''|*[!0-9]*) exit 0 ;; esac
|
|
39
|
+
|
|
40
|
+
# State (missing/corrupt -> fired=0 bannered=0). Shared state file with
|
|
41
|
+
# overcodex-ctx-watch.sh; see that script's header for the sharing contract.
|
|
42
|
+
state="$CTX_DIR/$session_id.state"
|
|
43
|
+
fired=0
|
|
44
|
+
bannered=0
|
|
45
|
+
if [ -f "$state" ]; then
|
|
46
|
+
f="$(jq -r '.fired // 0' "$state" 2>/dev/null)"
|
|
47
|
+
b="$(jq -r '.bannered // 0' "$state" 2>/dev/null)"
|
|
48
|
+
case "$f" in 0|60|75|85) fired=$f ;; esac
|
|
49
|
+
case "$b" in 0|60|75|85) bannered=$b ;; esac
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# band = highest threshold <= pct (0 if < 60).
|
|
53
|
+
band=0
|
|
54
|
+
for t in "$T1" "$T2" "$T3"; do
|
|
55
|
+
[ "$pct" -ge "$t" ] && band=$t
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [ "$pct" -ge "$T1" ] && [ "$band" -gt "$bannered" ]; then
|
|
59
|
+
jq -n --arg msg "context ${pct}% - /prompts:handoff available" '{systemMessage: $msg}'
|
|
60
|
+
mkdir -p "$CTX_DIR" 2>/dev/null || exit 0
|
|
61
|
+
tmp="$state.tmp.$$"
|
|
62
|
+
printf '{"fired":%d,"bannered":%d}\n' "$fired" "$band" > "$tmp" 2>/dev/null && mv -f "$tmp" "$state" 2>/dev/null
|
|
63
|
+
fi
|
|
64
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# overcodex-precompact-offer.sh — Codex PreCompact hook. No overclaude
|
|
3
|
+
# equivalent (Claude Code has no PreCompact-shaped hook Claude actually
|
|
4
|
+
# exposes on this axis the same way); added per spec because PreCompact is
|
|
5
|
+
# the ONE moment we know for certain context is about to be destroyed:
|
|
6
|
+
# pre-compact.command.input's `trigger` field is "auto" when Codex's OWN
|
|
7
|
+
# model_auto_compact_token_limit was just crossed, or "manual" on an explicit
|
|
8
|
+
# /compact. Since PreCompact fires only when compaction is actually about to
|
|
9
|
+
# happen, this is inherently a rare, always-worth-surfacing event — no
|
|
10
|
+
# threshold/hysteresis state is kept here (unlike ctx-watch/notify), and it
|
|
11
|
+
# fires every time.
|
|
12
|
+
#
|
|
13
|
+
# pre-compact.command.output has NO hookSpecificOutput/additionalContext
|
|
14
|
+
# field (confirmed against the embedded JSON Schema: only continue,
|
|
15
|
+
# stopReason, suppressOutput, systemMessage) — unlike SessionStart/
|
|
16
|
+
# UserPromptSubmit/PostToolUse/SubagentStart, PreCompact cannot inject
|
|
17
|
+
# context into the turn (compaction is about to happen regardless). So this
|
|
18
|
+
# is a passive systemMessage banner only, same mechanism as
|
|
19
|
+
# overcodex-notify.sh's Stop banner — it is shown to the user, not fed to the
|
|
20
|
+
# model as an instruction, so its wording addresses the human reading the
|
|
21
|
+
# transcript rather than telling the model what to say.
|
|
22
|
+
#
|
|
23
|
+
# Contract: always exit 0, silent on every error path, defensive jq parsing,
|
|
24
|
+
# never sets "decision" or "continue":false — this hook only informs.
|
|
25
|
+
exec 2>/dev/null
|
|
26
|
+
set -u
|
|
27
|
+
|
|
28
|
+
INPUT="$(cat)" || INPUT=""
|
|
29
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
30
|
+
|
|
31
|
+
trigger="$(printf '%s' "$INPUT" | jq -r '.trigger // empty' 2>/dev/null)"
|
|
32
|
+
case "$trigger" in
|
|
33
|
+
auto) why="Codex's auto-compact token limit was just reached" ;;
|
|
34
|
+
manual) why="a manual compact was requested" ;;
|
|
35
|
+
*) exit 0 ;;
|
|
36
|
+
esac
|
|
37
|
+
|
|
38
|
+
jq -n --arg msg "context precompact (${trigger}) - ${why}; older turns are about to be summarized. This is the last chance to /prompts:handoff before that detail is gone." \
|
|
39
|
+
'{systemMessage: $msg}'
|
|
40
|
+
exit 0
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# install.sh — overcodex installer.
|
|
3
|
+
# macOS /bin/bash 3.2 compatible. set -u; errors handled explicitly.
|
|
4
|
+
# Reproduces the overcodex multi-account / ultracode setup for the Codex CLI.
|
|
5
|
+
#
|
|
6
|
+
# Never rewrites config.toml wholesale: top-level `hooks` and `[tui].status_line`
|
|
7
|
+
# are only ADDED when absent (detected with python3+tomllib, grep fallback),
|
|
8
|
+
# never overwritten. AGENTS.md and ~/.zshrc use begin/end marker blocks.
|
|
9
|
+
|
|
10
|
+
set -u
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Locate the kit (this script lives at the repo root).
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
16
|
+
|
|
17
|
+
# CODEX_HOME: honor the env var (relocatable state dir), else ~/.codex.
|
|
18
|
+
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
19
|
+
LOCALBIN="$HOME/.local/bin"
|
|
20
|
+
ZSHRC="$HOME/.zshrc"
|
|
21
|
+
|
|
22
|
+
CONFIG_TOML="$CODEX_HOME/config.toml"
|
|
23
|
+
AGENTS_MD="$CODEX_HOME/AGENTS.md"
|
|
24
|
+
|
|
25
|
+
# Kit sources.
|
|
26
|
+
SRC_SWAP="$SCRIPT_DIR/bin/codex-swap"
|
|
27
|
+
SRC_HOOKS_TPL="$SCRIPT_DIR/config/hooks-block.toml.tpl"
|
|
28
|
+
SRC_AGENTS="$SCRIPT_DIR/codex/AGENTS-ULTRACODE.md"
|
|
29
|
+
SRC_ZSNIPPET="$SCRIPT_DIR/shell/zshrc-snippet.sh"
|
|
30
|
+
|
|
31
|
+
# Statusline TOML fragment (owned by the config builder). Optional; either name.
|
|
32
|
+
SRC_STATUSLINE=""
|
|
33
|
+
for c in "$SCRIPT_DIR/config/statusline.toml" "$SCRIPT_DIR/config/statusline"; do
|
|
34
|
+
[ -f "$c" ] && [ -s "$c" ] && { SRC_STATUSLINE="$c"; break; }
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
# Marker strings (must match uninstall.sh exactly).
|
|
38
|
+
AGENTS_BEGIN='# --- overcodex ultracode (begin) ---'
|
|
39
|
+
AGENTS_END='# --- overcodex ultracode (end) ---'
|
|
40
|
+
ZSH_BEGIN='# --- overcodex integration (begin) ---'
|
|
41
|
+
ZSH_END='# --- overcodex integration (end) ---'
|
|
42
|
+
HOOKS_BEGIN='# --- overcodex hooks (begin) ---'
|
|
43
|
+
HOOKS_END='# --- overcodex hooks (end) ---'
|
|
44
|
+
SL_BEGIN='# --- overcodex statusline (begin) ---'
|
|
45
|
+
SL_END='# --- overcodex statusline (end) ---'
|
|
46
|
+
|
|
47
|
+
EPOCH=$(date +%s)
|
|
48
|
+
|
|
49
|
+
# Summary accumulators (bash 3.2: plain indexed arrays).
|
|
50
|
+
DID=()
|
|
51
|
+
SKIPPED=()
|
|
52
|
+
WARNED=()
|
|
53
|
+
note_did() { DID[${#DID[@]}]="$1"; echo " [+] $1"; }
|
|
54
|
+
note_skip() { SKIPPED[${#SKIPPED[@]}]="$1"; echo " [=] $1"; }
|
|
55
|
+
note_warn() { WARNED[${#WARNED[@]}]="$1"; echo " [!] $1" >&2; }
|
|
56
|
+
die() { echo "install: ERROR: $1" >&2; exit 1; }
|
|
57
|
+
|
|
58
|
+
echo "== overcodex installer =="
|
|
59
|
+
echo " kit: $SCRIPT_DIR"
|
|
60
|
+
echo " CODEX_HOME: $CODEX_HOME"
|
|
61
|
+
echo " epoch: $EPOCH"
|
|
62
|
+
echo
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# 0. Sanity: required kit files present.
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
for f in "$SRC_SWAP" "$SRC_HOOKS_TPL" "$SRC_AGENTS" "$SRC_ZSNIPPET"; do
|
|
68
|
+
[ -f "$f" ] || die "kit file missing: $f (run from the repo root)"
|
|
69
|
+
done
|
|
70
|
+
# At least one hook script.
|
|
71
|
+
HOOK_SCRIPTS=$(ls "$SCRIPT_DIR"/hooks/*.sh 2>/dev/null)
|
|
72
|
+
[ -n "$HOOK_SCRIPTS" ] || die "no hook scripts found under $SCRIPT_DIR/hooks/*.sh"
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# 1. Dependency preflight.
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
echo "-- preflight --"
|
|
78
|
+
|
|
79
|
+
# jq is required.
|
|
80
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
81
|
+
die "jq is required but not found. Install it: brew install jq"
|
|
82
|
+
fi
|
|
83
|
+
echo " [ok] jq: $(command -v jq)"
|
|
84
|
+
|
|
85
|
+
# codex CLI: warn only (do not fail).
|
|
86
|
+
if command -v codex >/dev/null 2>&1; then
|
|
87
|
+
echo " [ok] codex: $(command -v codex)"
|
|
88
|
+
else
|
|
89
|
+
note_warn "codex CLI not found on PATH. Install it, e.g.:"
|
|
90
|
+
note_warn " brew install codex (or) npm install -g @openai/codex"
|
|
91
|
+
note_warn " The kit installs fine without it, but codex must be present to use it."
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# python3 is preferred for TOML read/decide; grep is the fallback.
|
|
95
|
+
PYTHON=""
|
|
96
|
+
if command -v python3 >/dev/null 2>&1 && python3 -c 'import tomllib' >/dev/null 2>&1; then
|
|
97
|
+
PYTHON="python3"
|
|
98
|
+
echo " [ok] python3 + tomllib: $(command -v python3)"
|
|
99
|
+
else
|
|
100
|
+
note_warn "python3 with tomllib not found — using grep-based config.toml detection (best effort)."
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# CODEX_HOME: create if missing.
|
|
104
|
+
if [ -d "$CODEX_HOME" ]; then
|
|
105
|
+
echo " [ok] CODEX_HOME exists: $CODEX_HOME"
|
|
106
|
+
else
|
|
107
|
+
mkdir -p "$CODEX_HOME" || die "could not create CODEX_HOME: $CODEX_HOME"
|
|
108
|
+
note_did "created CODEX_HOME: $CODEX_HOME"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# PATH check for ~/.local/bin (where codex-swap lands).
|
|
112
|
+
case ":$PATH:" in
|
|
113
|
+
*":$LOCALBIN:"*) echo " [ok] $LOCALBIN is on PATH" ;;
|
|
114
|
+
*) note_warn "$LOCALBIN is not on your PATH. Add it so codex-swap is found:"
|
|
115
|
+
note_warn " export PATH=\"\$HOME/.local/bin:\$PATH\"" ;;
|
|
116
|
+
esac
|
|
117
|
+
echo
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Helpers.
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
# backup_file <path> — timestamped backup if the file exists. Prints backup path.
|
|
124
|
+
backup_file() {
|
|
125
|
+
bf_path="$1"
|
|
126
|
+
if [ -f "$bf_path" ]; then
|
|
127
|
+
cp -p "$bf_path" "$bf_path.bak-$EPOCH" || die "backup failed: $bf_path"
|
|
128
|
+
echo "$bf_path.bak-$EPOCH"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# install_file <src> <dest> <mode|-> — copy with backup-on-change, chmod.
|
|
133
|
+
# No backup, no copy when the destination is already identical (idempotent).
|
|
134
|
+
install_file() {
|
|
135
|
+
if_src="$1"; if_dest="$2"; if_mode="$3"
|
|
136
|
+
mkdir -p "$(dirname "$if_dest")" || die "mkdir failed for $if_dest"
|
|
137
|
+
if [ -f "$if_dest" ]; then
|
|
138
|
+
if cmp -s "$if_src" "$if_dest"; then
|
|
139
|
+
[ "$if_mode" != "-" ] && chmod "$if_mode" "$if_dest" 2>/dev/null
|
|
140
|
+
note_skip "up-to-date: $if_dest"
|
|
141
|
+
return 0
|
|
142
|
+
fi
|
|
143
|
+
b=$(backup_file "$if_dest")
|
|
144
|
+
cp "$if_src" "$if_dest" || die "copy failed: $if_dest"
|
|
145
|
+
[ "$if_mode" != "-" ] && chmod "$if_mode" "$if_dest"
|
|
146
|
+
note_did "updated: $if_dest (backup: $b)"
|
|
147
|
+
else
|
|
148
|
+
cp "$if_src" "$if_dest" || die "copy failed: $if_dest"
|
|
149
|
+
[ "$if_mode" != "-" ] && chmod "$if_mode" "$if_dest"
|
|
150
|
+
note_did "installed: $if_dest"
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# 2. File copies (per the FIXED DESTINATIONS contract).
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
echo "-- files --"
|
|
158
|
+
|
|
159
|
+
install_file "$SRC_SWAP" "$LOCALBIN/codex-swap" 755
|
|
160
|
+
|
|
161
|
+
# hooks/*.sh -> $CODEX_HOME/hooks/ (755)
|
|
162
|
+
mkdir -p "$CODEX_HOME/hooks" || die "mkdir failed: $CODEX_HOME/hooks"
|
|
163
|
+
for h in "$SCRIPT_DIR"/hooks/*.sh; do
|
|
164
|
+
[ -f "$h" ] || continue
|
|
165
|
+
install_file "$h" "$CODEX_HOME/hooks/$(basename "$h")" 755
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# prompts/*.md -> $CODEX_HOME/prompts/ (optional)
|
|
169
|
+
PROMPTS=$(ls "$SCRIPT_DIR"/prompts/*.md 2>/dev/null)
|
|
170
|
+
if [ -n "$PROMPTS" ]; then
|
|
171
|
+
mkdir -p "$CODEX_HOME/prompts" || die "mkdir failed: $CODEX_HOME/prompts"
|
|
172
|
+
for p in "$SCRIPT_DIR"/prompts/*.md; do
|
|
173
|
+
[ -f "$p" ] || continue
|
|
174
|
+
install_file "$p" "$CODEX_HOME/prompts/$(basename "$p")" -
|
|
175
|
+
done
|
|
176
|
+
else
|
|
177
|
+
note_skip "no prompts/*.md in kit (nothing to install)"
|
|
178
|
+
fi
|
|
179
|
+
echo
|
|
180
|
+
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
# 3. config.toml — add the inline [hooks] table and [tui].status_line IF ABSENT.
|
|
183
|
+
# Never rewrites the file: python3+tomllib decides presence; the hooks block
|
|
184
|
+
# is APPENDED at EOF between markers (a [hooks] table prepended at the top
|
|
185
|
+
# would capture every top-level key after it); status_line is a [tui] insert.
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
echo "-- config.toml --"
|
|
188
|
+
|
|
189
|
+
# toml_present <config> -> prints "hooks=present|absent" and "sl=present|absent"
|
|
190
|
+
# on stdout. Exits 3 on a parse error (caller then leaves the file untouched).
|
|
191
|
+
toml_present() {
|
|
192
|
+
tp_cfg="$1"
|
|
193
|
+
if [ -n "$PYTHON" ]; then
|
|
194
|
+
"$PYTHON" - "$tp_cfg" <<'PY'
|
|
195
|
+
import sys, tomllib
|
|
196
|
+
p = sys.argv[1]
|
|
197
|
+
try:
|
|
198
|
+
with open(p, "rb") as f:
|
|
199
|
+
d = tomllib.load(f)
|
|
200
|
+
except FileNotFoundError:
|
|
201
|
+
d = {}
|
|
202
|
+
except Exception as e:
|
|
203
|
+
sys.stderr.write("parse-error: %s\n" % e)
|
|
204
|
+
sys.exit(3)
|
|
205
|
+
tui = d.get("tui")
|
|
206
|
+
tui = tui if isinstance(tui, dict) else {}
|
|
207
|
+
print("hooks=%s" % ("present" if "hooks" in d else "absent"))
|
|
208
|
+
print("sl=%s" % ("present" if "status_line" in tui else "absent"))
|
|
209
|
+
PY
|
|
210
|
+
return $?
|
|
211
|
+
fi
|
|
212
|
+
# grep fallback (heuristic: matches a top-level-looking key line).
|
|
213
|
+
if [ -f "$tp_cfg" ]; then
|
|
214
|
+
if grep -Eq '^[[:space:]]*hooks[[:space:]]*=' "$tp_cfg"; then
|
|
215
|
+
echo "hooks=present"; else echo "hooks=absent"; fi
|
|
216
|
+
if grep -Eq '^[[:space:]]*status_line[[:space:]]*=' "$tp_cfg"; then
|
|
217
|
+
echo "sl=present"; else echo "sl=absent"; fi
|
|
218
|
+
else
|
|
219
|
+
echo "hooks=absent"; echo "sl=absent"
|
|
220
|
+
fi
|
|
221
|
+
return 0
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
CFG_OK=1
|
|
225
|
+
DET=$(toml_present "$CONFIG_TOML")
|
|
226
|
+
if [ $? -eq 3 ]; then
|
|
227
|
+
CFG_OK=0
|
|
228
|
+
note_warn "config.toml is not valid TOML; leaving it completely untouched."
|
|
229
|
+
note_warn " Fix $CONFIG_TOML, then re-run ./install.sh to wire hooks/status_line."
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [ "$CFG_OK" = 1 ]; then
|
|
233
|
+
HOOKS_STATE=$(printf '%s\n' "$DET" | sed -n 's/^hooks=//p')
|
|
234
|
+
SL_STATE=$(printf '%s\n' "$DET" | sed -n 's/^sl=//p')
|
|
235
|
+
|
|
236
|
+
NEED_HOOKS=0
|
|
237
|
+
[ "$HOOKS_STATE" = absent ] && NEED_HOOKS=1
|
|
238
|
+
|
|
239
|
+
NEED_SL=0
|
|
240
|
+
if [ "$SL_STATE" = absent ]; then
|
|
241
|
+
if [ -n "$SRC_STATUSLINE" ]; then
|
|
242
|
+
NEED_SL=1
|
|
243
|
+
else
|
|
244
|
+
note_skip "no statusline fragment in kit — leaving [tui].status_line alone"
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# Report the never-overwrite decisions.
|
|
249
|
+
if [ "$HOOKS_STATE" = present ]; then
|
|
250
|
+
if [ -f "$CONFIG_TOML" ] && grep -qF "$HOOKS_BEGIN" "$CONFIG_TOML"; then
|
|
251
|
+
note_skip "config.toml hooks key already wired by overcodex"
|
|
252
|
+
else
|
|
253
|
+
note_warn "config.toml already defines a 'hooks' key — leaving it untouched."
|
|
254
|
+
note_warn " Merge the handlers from config/hooks-block.toml.tpl into your [hooks] table"
|
|
255
|
+
note_warn " manually (substitute @HOOKS_DIR@ with $CODEX_HOME/hooks)."
|
|
256
|
+
fi
|
|
257
|
+
fi
|
|
258
|
+
if [ "$SL_STATE" = present ]; then
|
|
259
|
+
if [ -f "$CONFIG_TOML" ] && grep -qF "$SL_BEGIN" "$CONFIG_TOML"; then
|
|
260
|
+
note_skip "config.toml [tui].status_line already set by overcodex"
|
|
261
|
+
else
|
|
262
|
+
note_warn "config.toml already defines [tui].status_line — leaving it untouched."
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
if [ "$NEED_HOOKS" = 1 ] || [ "$NEED_SL" = 1 ]; then
|
|
267
|
+
b=""
|
|
268
|
+
[ -f "$CONFIG_TOML" ] && b=$(backup_file "$CONFIG_TOML")
|
|
269
|
+
TMP="$CONFIG_TOML.tmp-$EPOCH"
|
|
270
|
+
: > "$TMP" || die "cannot write $TMP"
|
|
271
|
+
|
|
272
|
+
# the existing file first, verbatim.
|
|
273
|
+
[ -f "$CONFIG_TOML" ] && cat "$CONFIG_TOML" >> "$TMP"
|
|
274
|
+
|
|
275
|
+
# hooks: APPEND the inline [hooks] table at EOF between markers, with
|
|
276
|
+
# @HOOKS_DIR@ resolved. (EOF is the only safe spot for a table block —
|
|
277
|
+
# prepended, it would capture the file's top-level keys.)
|
|
278
|
+
if [ "$NEED_HOOKS" = 1 ]; then
|
|
279
|
+
if [ -s "$TMP" ]; then
|
|
280
|
+
[ -n "$(tail -c1 "$TMP")" ] && printf '\n' >> "$TMP"
|
|
281
|
+
printf '\n' >> "$TMP"
|
|
282
|
+
fi
|
|
283
|
+
{
|
|
284
|
+
printf '%s\n' "$HOOKS_BEGIN"
|
|
285
|
+
sed "s|@HOOKS_DIR@|$CODEX_HOME/hooks|g" "$SRC_HOOKS_TPL"
|
|
286
|
+
printf '%s\n' "$HOOKS_END"
|
|
287
|
+
} >> "$TMP"
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# status_line: insert our keys just after an existing [tui] header, else
|
|
291
|
+
# append a fresh [tui] table at EOF. Fragment header/comment/blank lines are
|
|
292
|
+
# dropped so only the key lines land inside our markers.
|
|
293
|
+
if [ "$NEED_SL" = 1 ]; then
|
|
294
|
+
TMP2="$CONFIG_TOML.tmp2-$EPOCH"
|
|
295
|
+
awk -v sb="$SL_BEGIN" -v se="$SL_END" -v kf="$SRC_STATUSLINE" '
|
|
296
|
+
function emitkeys( line) {
|
|
297
|
+
while ((getline line < kf) > 0) {
|
|
298
|
+
if (line == "" || line == "[tui]") continue
|
|
299
|
+
if (line ~ /^[ \t]*#/) continue
|
|
300
|
+
print line
|
|
301
|
+
}
|
|
302
|
+
close(kf)
|
|
303
|
+
}
|
|
304
|
+
{ print }
|
|
305
|
+
/^[ \t]*\[tui\][ \t]*(#.*)?$/ && done != 1 {
|
|
306
|
+
print sb; emitkeys(); print se; done = 1
|
|
307
|
+
}
|
|
308
|
+
END {
|
|
309
|
+
if (done != 1) { print ""; print sb; print "[tui]"; emitkeys(); print se }
|
|
310
|
+
}
|
|
311
|
+
' "$TMP" > "$TMP2" || die "awk failed inserting status_line"
|
|
312
|
+
mv "$TMP2" "$TMP" || die "could not stage status_line insert"
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
# Validate before committing (python3 only; grep path trusts the insert).
|
|
316
|
+
if [ -n "$PYTHON" ]; then
|
|
317
|
+
"$PYTHON" - "$TMP" <<'PY' || { rm -f "$TMP"; die "generated config.toml is not valid TOML; original left untouched (backup: kept)"; }
|
|
318
|
+
import sys, tomllib
|
|
319
|
+
with open(sys.argv[1], "rb") as f:
|
|
320
|
+
tomllib.load(f)
|
|
321
|
+
PY
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
mv "$TMP" "$CONFIG_TOML" || die "could not write $CONFIG_TOML"
|
|
325
|
+
_bmsg=""
|
|
326
|
+
[ -n "$b" ] && _bmsg=" (backup: $b)"
|
|
327
|
+
[ "$NEED_HOOKS" = 1 ] && note_did "wired the inline [hooks] table into config.toml$_bmsg"
|
|
328
|
+
[ "$NEED_SL" = 1 ] && note_did "set [tui].status_line in config.toml$_bmsg"
|
|
329
|
+
else
|
|
330
|
+
[ "$HOOKS_STATE" = absent ] || [ "$NEED_HOOKS" = 1 ] || true
|
|
331
|
+
note_skip "config.toml: no changes needed"
|
|
332
|
+
fi
|
|
333
|
+
fi
|
|
334
|
+
echo
|
|
335
|
+
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
# append_marked <target> <begin> <end> <src> <label>
|
|
338
|
+
# Append a marker-wrapped block only if the begin marker is absent. Any
|
|
339
|
+
# pre-existing overcodex marker lines in <src> are stripped so we never nest.
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
append_marked() {
|
|
342
|
+
am_target="$1"; am_begin="$2"; am_end="$3"; am_src="$4"; am_label="$5"
|
|
343
|
+
if [ -f "$am_target" ] && grep -qF "$am_begin" "$am_target"; then
|
|
344
|
+
note_skip "$am_label already present in $am_target"
|
|
345
|
+
return 0
|
|
346
|
+
fi
|
|
347
|
+
mkdir -p "$(dirname "$am_target")" || die "mkdir failed for $am_target"
|
|
348
|
+
b=""
|
|
349
|
+
[ -f "$am_target" ] && b=$(backup_file "$am_target")
|
|
350
|
+
# Separator blank line before our block when the file has content.
|
|
351
|
+
if [ -f "$am_target" ] && [ -s "$am_target" ]; then
|
|
352
|
+
[ -n "$(tail -c1 "$am_target")" ] && printf '\n' >> "$am_target"
|
|
353
|
+
printf '\n' >> "$am_target"
|
|
354
|
+
fi
|
|
355
|
+
{
|
|
356
|
+
printf '%s\n' "$am_begin"
|
|
357
|
+
# Drop any stray marker lines already in the source to avoid nesting.
|
|
358
|
+
grep -vxF "$am_begin" "$am_src" | grep -vxF "$am_end"
|
|
359
|
+
printf '%s\n' "$am_end"
|
|
360
|
+
} >> "$am_target" || die "could not append to $am_target"
|
|
361
|
+
if [ -n "$b" ]; then
|
|
362
|
+
note_did "appended $am_label to $am_target (backup: $b)"
|
|
363
|
+
else
|
|
364
|
+
note_did "created $am_target with $am_label"
|
|
365
|
+
fi
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
# 4. AGENTS.md — append the ultracode block between markers (if absent).
|
|
370
|
+
# ---------------------------------------------------------------------------
|
|
371
|
+
echo "-- AGENTS.md --"
|
|
372
|
+
append_marked "$AGENTS_MD" "$AGENTS_BEGIN" "$AGENTS_END" "$SRC_AGENTS" "overcodex ultracode block"
|
|
373
|
+
echo
|
|
374
|
+
|
|
375
|
+
# ---------------------------------------------------------------------------
|
|
376
|
+
# 5. .zshrc — append the integration snippet between markers (if absent).
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
echo "-- .zshrc --"
|
|
379
|
+
ZBEFORE_EXISTS=no
|
|
380
|
+
[ -f "$ZSHRC" ] && grep -qF "$ZSH_BEGIN" "$ZSHRC" && ZBEFORE_EXISTS=yes
|
|
381
|
+
append_marked "$ZSHRC" "$ZSH_BEGIN" "$ZSH_END" "$SRC_ZSNIPPET" "overcodex integration block"
|
|
382
|
+
[ "$ZBEFORE_EXISTS" = no ] && note_warn "Open a new shell or run: source $ZSHRC"
|
|
383
|
+
echo
|
|
384
|
+
|
|
385
|
+
# ---------------------------------------------------------------------------
|
|
386
|
+
# 6. Verify.
|
|
387
|
+
# ---------------------------------------------------------------------------
|
|
388
|
+
echo "-- verify --"
|
|
389
|
+
|
|
390
|
+
# codex-swap resolves.
|
|
391
|
+
if command -v codex-swap >/dev/null 2>&1 || [ -x "$LOCALBIN/codex-swap" ]; then
|
|
392
|
+
echo " [ok] codex-swap resolves"
|
|
393
|
+
else
|
|
394
|
+
note_warn "codex-swap does NOT resolve — is $LOCALBIN on PATH? (exec zsh, then re-check)"
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
# hooks dir populated.
|
|
398
|
+
if ls "$CODEX_HOME"/hooks/*.sh >/dev/null 2>&1; then
|
|
399
|
+
echo " [ok] hooks dir populated: $CODEX_HOME/hooks/"
|
|
400
|
+
else
|
|
401
|
+
note_warn "no hook scripts found under $CODEX_HOME/hooks/"
|
|
402
|
+
fi
|
|
403
|
+
|
|
404
|
+
# hooks key wired.
|
|
405
|
+
HOOKS_WIRED=no
|
|
406
|
+
if [ -n "$PYTHON" ] && [ -f "$CONFIG_TOML" ]; then
|
|
407
|
+
if "$PYTHON" - "$CONFIG_TOML" <<'PY' >/dev/null 2>&1
|
|
408
|
+
import sys, tomllib
|
|
409
|
+
with open(sys.argv[1], "rb") as f:
|
|
410
|
+
d = tomllib.load(f)
|
|
411
|
+
sys.exit(0 if "hooks" in d else 1)
|
|
412
|
+
PY
|
|
413
|
+
then HOOKS_WIRED=yes; fi
|
|
414
|
+
elif [ -f "$CONFIG_TOML" ] && grep -Eq '^[[:space:]]*hooks[[:space:]]*=' "$CONFIG_TOML"; then
|
|
415
|
+
HOOKS_WIRED=yes
|
|
416
|
+
fi
|
|
417
|
+
if [ "$HOOKS_WIRED" = yes ]; then
|
|
418
|
+
echo " [ok] config.toml hooks key wired"
|
|
419
|
+
else
|
|
420
|
+
note_warn "config.toml has no hooks key — hooks will not load."
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
# AGENTS marker present.
|
|
424
|
+
if [ -f "$AGENTS_MD" ] && grep -qF "$AGENTS_BEGIN" "$AGENTS_MD"; then
|
|
425
|
+
echo " [ok] AGENTS.md ultracode marker present"
|
|
426
|
+
else
|
|
427
|
+
note_warn "AGENTS.md ultracode marker missing."
|
|
428
|
+
fi
|
|
429
|
+
echo
|
|
430
|
+
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
# Summary.
|
|
433
|
+
# ---------------------------------------------------------------------------
|
|
434
|
+
echo "== summary =="
|
|
435
|
+
echo " changed: ${#DID[@]}"
|
|
436
|
+
echo " skipped: ${#SKIPPED[@]}"
|
|
437
|
+
echo " warnings: ${#WARNED[@]}"
|
|
438
|
+
if [ "${#WARNED[@]}" -gt 0 ]; then
|
|
439
|
+
echo " -- warnings --"
|
|
440
|
+
i=0
|
|
441
|
+
while [ "$i" -lt "${#WARNED[@]}" ]; do
|
|
442
|
+
echo " ! ${WARNED[$i]}"
|
|
443
|
+
i=$((i + 1))
|
|
444
|
+
done
|
|
445
|
+
fi
|
|
446
|
+
echo
|
|
447
|
+
echo "Switch accounts with: codex-swap (isolated CODEX_HOME per account)."
|
|
448
|
+
echo "Done."
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Remove a pending handoff package for this directory."
|
|
3
|
+
argument-hint: ""
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /prompts:handoff-cancel — cancel a pending handoff
|
|
7
|
+
|
|
8
|
+
## 1. Compute the state path
|
|
9
|
+
|
|
10
|
+
Run exactly this (macOS /bin/bash 3.2 compatible):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
HASH=$(printf '%s' "$PWD" | /usr/bin/shasum -a 256 | awk '{print $1}' | cut -c1-12)
|
|
14
|
+
STATE_DIR="${CODEX_HOME:-$HOME/.codex}/overcodex"
|
|
15
|
+
PENDING="$STATE_DIR/handoff-pending-$HASH.md"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 2. Cancel
|
|
19
|
+
|
|
20
|
+
- If `$PENDING` does not exist: report "no handoff pending for this directory — nothing to cancel."
|
|
21
|
+
- If it exists: read line 1 for the `created` timestamp and line 2 for the goal, report them to the user ("removing pending handoff from <age> ago: '<goal>'"), then delete it: `rm "$PENDING"`.
|
|
22
|
+
|
|
23
|
+
Only ever remove the single file for this cwd's hash — never glob-delete other pending packages under `$STATE_DIR`, they belong to other working directories.
|