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.
@@ -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.