subconscious-cli 0.2.0 → 0.3.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.
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env bash
2
+ # Point the `codex` CLI at the Subconscious gateway — ephemerally.
3
+ #
4
+ # Uses `codex -c key=value` overrides so nothing is written to ~/.codex/config.toml.
5
+ # web_search is disabled so Codex doesn't send hosted tools the gateway can't execute.
6
+ # The model catalog (needed to suppress the "model metadata not found" warning)
7
+ # is written to a temp file that is cleaned up on exit.
8
+ #
9
+ # Subagents are OFF by default. Current Codex (>=0.144) wraps subagent tools in a
10
+ # `type: "namespace"` wire format that non-OpenAI providers can't resolve, causing
11
+ # "unsupported call: spawn_agent" (upstream #32318, #26977). The fix (PR #29602) is
12
+ # not yet merged.
13
+ #
14
+ # To run with subagents, pass --subagents — this runs the pinned legacy
15
+ # codex@0.132.0 with multi-agent v1 config (plain tool names) that the model can
16
+ # resolve. Requires npx.
17
+ #
18
+ # Usage:
19
+ # ./run.sh # uses GATEWAY_URL/API_KEY from ../.env
20
+ # ./run.sh --context-window 5000000 -- --resume
21
+ # ./run.sh --subagents # run codex@0.132.0 with subagents enabled
22
+ # ./run.sh --subagents -- --resume # subagents + passthrough args
23
+ # ./run.sh --external-tools # include Codex apps/plugins (may exceed gateway tool limits)
24
+ #
25
+ # Config: copy ../env.example to ../.env and edit. .env is gitignored.
26
+ # All agents share one coding-agents/.env file.
27
+ #
28
+ # Or source it to just export the env:
29
+ # source run.sh
30
+
31
+ set -euo pipefail
32
+
33
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+
35
+ # Load shared env from coding-agents/.env (gitignored) or env.example.
36
+ SHARED_ENV="${MBTA_ENV_FILE:-${SCRIPT_DIR}/../.env}"
37
+ [[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
38
+ if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
39
+
40
+ GATEWAY_URL="${GATEWAY_URL:-}"
41
+ API_KEY="${CODEX_API_KEY:-${API_KEY:-}}"
42
+ MODEL="${MODEL:-subconscious/glm-5.2}"
43
+ MAX_CONCURRENT_SUBAGENTS="${MAX_CONCURRENT_SUBAGENTS:-4}"
44
+ SUBAGENTS=false
45
+ EXTERNAL_TOOLS="${CODEX_EXTERNAL_TOOLS:-false}"
46
+ CODEX_CONTEXT_WINDOW="${CODEX_CONTEXT_WINDOW:-5000000}"
47
+ CODEX_MAX_CONTEXT_WINDOW="${CODEX_MAX_CONTEXT_WINDOW:-}"
48
+ CODEX_AUTO_COMPACT_TOKEN_LIMIT="${CODEX_AUTO_COMPACT_TOKEN_LIMIT:-4500000}"
49
+ CODEX_REASONING_EFFORT="${CODEX_REASONING_EFFORT:-max}"
50
+
51
+ # Codex version that supports the legacy multi-agent v1 config (plain tool names).
52
+ SUBAGENT_CODEX_VERSION="0.132.0"
53
+
54
+ # Parse args (only when executed, not sourced)
55
+ PASSTHRU=()
56
+ if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
57
+ while [[ $# -gt 0 ]]; do
58
+ case "$1" in
59
+ --context-window)
60
+ CODEX_CONTEXT_WINDOW="${2:-}"
61
+ shift 2
62
+ ;;
63
+ --max-context-window)
64
+ CODEX_MAX_CONTEXT_WINDOW="${2:-}"
65
+ shift 2
66
+ ;;
67
+ --auto-compact-token-limit)
68
+ CODEX_AUTO_COMPACT_TOKEN_LIMIT="${2:-}"
69
+ shift 2
70
+ ;;
71
+ --reasoning-effort)
72
+ CODEX_REASONING_EFFORT="${2:-}"
73
+ shift 2
74
+ ;;
75
+ --subagents)
76
+ SUBAGENTS=true
77
+ shift
78
+ ;;
79
+ --external-tools)
80
+ EXTERNAL_TOOLS=true
81
+ shift
82
+ ;;
83
+ --)
84
+ shift
85
+ PASSTHRU+=("$@")
86
+ break
87
+ ;;
88
+ *)
89
+ PASSTHRU+=("$1")
90
+ shift
91
+ ;;
92
+ esac
93
+ done
94
+ fi
95
+
96
+ CODEX_MAX_CONTEXT_WINDOW="${CODEX_MAX_CONTEXT_WINDOW:-${CODEX_CONTEXT_WINDOW}}"
97
+ case "$CODEX_REASONING_EFFORT" in
98
+ none|low|medium|high|max) ;;
99
+ *)
100
+ echo "error: reasoning effort must be one of: none, low, medium, high, max" >&2
101
+ exit 1
102
+ ;;
103
+ esac
104
+
105
+ DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.2
106
+ subconscious/tim-qwen3.6-27b
107
+ subconscious/deepseek-v4-flash-marathon"
108
+ SUPPORTED_MODELS=()
109
+
110
+ add_supported_model() {
111
+ local model_id="$1" existing
112
+ [[ -n "$model_id" ]] || return 0
113
+ if [[ ! "$model_id" =~ ^[-A-Za-z0-9._:/+]+$ ]]; then
114
+ echo "error: invalid model id: $model_id" >&2
115
+ exit 1
116
+ fi
117
+ if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
118
+ for existing in "${SUPPORTED_MODELS[@]}"; do
119
+ [[ "$existing" == "$model_id" ]] && return 0
120
+ done
121
+ fi
122
+ SUPPORTED_MODELS+=("$model_id")
123
+ }
124
+
125
+ # Keep the requested model first while exposing the complete Subconscious
126
+ # catalog in Codex's /model picker. The CLI supplies this list from the registry;
127
+ # the fallback keeps the vendored runbook useful on its own.
128
+ add_supported_model "$MODEL"
129
+ while IFS= read -r model_id; do
130
+ add_supported_model "$model_id"
131
+ done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
132
+
133
+ write_model_catalog() {
134
+ local catalog_file="$1" model_id index=0
135
+ {
136
+ printf '{\n "models": [\n'
137
+ for model_id in "${SUPPORTED_MODELS[@]}"; do
138
+ if [[ "$index" -gt 0 ]]; then
139
+ printf ',\n'
140
+ fi
141
+ cat <<EOF
142
+ {
143
+ "slug": "${model_id}",
144
+ "display_name": "${model_id}",
145
+ "description": "Subconscious API Gateway model ${model_id}",
146
+ "context_window": ${CODEX_CONTEXT_WINDOW},
147
+ "max_context_window": ${CODEX_MAX_CONTEXT_WINDOW},
148
+ "auto_compact_token_limit": ${CODEX_AUTO_COMPACT_TOKEN_LIMIT},
149
+ "effective_context_window_percent": 95,
150
+ "supported_reasoning_levels": [],
151
+ "shell_type": "shell_command",
152
+ "visibility": "list",
153
+ "supported_in_api": true,
154
+ "priority": 0,
155
+ "availability_nux": null,
156
+ "upgrade": null,
157
+ "base_instructions": "You are Codex, a coding agent.",
158
+ "supports_reasoning_summaries": false,
159
+ "support_verbosity": false,
160
+ "default_verbosity": null,
161
+ "apply_patch_tool_type": "freeform",
162
+ "truncation_policy": { "mode": "tokens", "limit": 10000 },
163
+ "supports_parallel_tool_calls": true,
164
+ "experimental_supported_tools": []
165
+ }
166
+ EOF
167
+ index=$((index + 1))
168
+ done
169
+ printf '\n ]\n}\n'
170
+ } >"$catalog_file"
171
+ }
172
+
173
+ # The gateway accepts at most 128 tools per request. Codex apps and installed
174
+ # plugins can collectively exceed that before any core coding tools are added,
175
+ # so keep those external catalogs off for Subconscious by default. Users can
176
+ # opt back in with --external-tools if their gateway supports a larger limit.
177
+ EXTERNAL_TOOL_ARGS=()
178
+ if [[ "$EXTERNAL_TOOLS" != "true" ]]; then
179
+ EXTERNAL_TOOL_ARGS=(
180
+ -c features.apps=false
181
+ -c features.plugins=false
182
+ -c apps._default.enabled=false
183
+ )
184
+ fi
185
+
186
+ if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
187
+ echo "error: GATEWAY_URL and API_KEY must be set in ../.env" >&2
188
+ exit 1
189
+ fi
190
+
191
+ export SUBCONSCIOUS_API_KEY="$API_KEY"
192
+ export SUBCONSCIOUS_GATEWAY_URL="${GATEWAY_URL%/}"
193
+
194
+ # Ensure compaction hooks are present for ephemeral runs too. Codex discovers
195
+ # ~/.codex/hooks.json alongside config layers; trust them once via /hooks.
196
+ HOOK_SRC="${SCRIPT_DIR}/hook.sh"
197
+ HOOKS_TEMPLATE="${SCRIPT_DIR}/hooks.json"
198
+ CODEX_DIR="${HOME}/.codex"
199
+ HOOK_DST="${CODEX_DIR}/subconscious-hook.sh"
200
+ HOOKS_JSON="${CODEX_DIR}/hooks.json"
201
+ HOOKS_ENV_FILE="${CODEX_DIR}/subconscious-hooks.env"
202
+ if [[ -f "$HOOK_SRC" && -f "$HOOKS_TEMPLATE" ]]; then
203
+ mkdir -p "$CODEX_DIR"
204
+ cp "$HOOK_SRC" "$HOOK_DST"
205
+ chmod +x "$HOOK_DST"
206
+ sed "s|HOOK_SH_PATH|${HOOK_DST}|g" "$HOOKS_TEMPLATE" >"$HOOKS_JSON"
207
+ umask 077
208
+ cat >"$HOOKS_ENV_FILE" <<EOF
209
+ # Generated by ol-runbook/coding-agents/codex/run.sh — do not commit secrets.
210
+ export SUBCONSCIOUS_GATEWAY_URL='${GATEWAY_URL%/}'
211
+ export SUBCONSCIOUS_API_KEY='${API_KEY}'
212
+ EOF
213
+ chmod 600 "$HOOKS_ENV_FILE"
214
+ fi
215
+
216
+ # Write a temp model catalog so Codex doesn't print "model metadata not found".
217
+ # This is the one thing that can't be passed via -c flags.
218
+ CATALOG_FILE="$(mktemp -t codex-model-catalog.XXXXXX.json)"
219
+ cleanup() { rm -f "$CATALOG_FILE"; }
220
+ trap cleanup EXIT
221
+ write_model_catalog "$CATALOG_FILE"
222
+
223
+ # If sourced, just export env and return.
224
+ if [[ "${BASH_SOURCE[0]:-$0}" != "${0}" ]]; then
225
+ export GATEWAY_URL CATALOG_FILE MAX_CONCURRENT_SUBAGENTS SUBAGENTS
226
+ return 0 2>/dev/null || true
227
+ fi
228
+
229
+ # Ephemeral config via -c flags — nothing is written to ~/.codex/config.toml.
230
+ if [[ "$SUBAGENTS" == "true" ]]; then
231
+ # Legacy multi-agent v1 config — plain tool names the model can resolve.
232
+ echo "Starting codex@${SUBAGENT_CODEX_VERSION} with subagents enabled (max ${MAX_CONCURRENT_SUBAGENTS} threads)..." >&2
233
+ exec npx -y "@openai/codex@${SUBAGENT_CODEX_VERSION}" \
234
+ -c model="${MODEL}" \
235
+ -c model_provider=subconscious \
236
+ -c model_catalog_json="${CATALOG_FILE}" \
237
+ -c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
238
+ -c web_search=disabled \
239
+ ${EXTERNAL_TOOL_ARGS[@]+"${EXTERNAL_TOOL_ARGS[@]}"} \
240
+ -c features.multi_agent=true \
241
+ -c agents.max_threads="${MAX_CONCURRENT_SUBAGENTS}" \
242
+ -c agents.max_depth=1 \
243
+ -c agents.interrupt_message=true \
244
+ -c model_providers.subconscious.name=Subconscious \
245
+ -c model_providers.subconscious.base_url="${GATEWAY_URL}/v1" \
246
+ -c model_providers.subconscious.wire_api=responses \
247
+ -c model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY \
248
+ -c model_providers.subconscious.stream_idle_timeout_ms=300000 \
249
+ ${PASSTHRU[@]+"${PASSTHRU[@]}"}
250
+ else
251
+ exec codex \
252
+ -c model="${MODEL}" \
253
+ -c model_provider=subconscious \
254
+ -c model_catalog_json="${CATALOG_FILE}" \
255
+ -c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
256
+ -c web_search=disabled \
257
+ ${EXTERNAL_TOOL_ARGS[@]+"${EXTERNAL_TOOL_ARGS[@]}"} \
258
+ -c model_providers.subconscious.name=Subconscious \
259
+ -c model_providers.subconscious.base_url="${GATEWAY_URL}/v1" \
260
+ -c model_providers.subconscious.wire_api=responses \
261
+ -c model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY \
262
+ -c model_providers.subconscious.stream_idle_timeout_ms=300000 \
263
+ ${PASSTHRU[@]+"${PASSTHRU[@]}"}
264
+ fi
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env bash
2
+ # Fail-open VS Code Copilot hook: announce prompts and auto-compactions to the
3
+ # gateway. Never blocks the agent.
4
+ # Stdin: VS Code hook JSON. Stdout: permissive JSON for VS Code.
5
+ #
6
+ # Docs: https://code.visualstudio.com/docs/copilot/customization/hooks
7
+ # PreCompact: https://code.visualstudio.com/docs/agents/reference/hooks-reference#precompact
8
+ #
9
+ # Two events:
10
+ # UserPromptSubmit -> conversation_ensure { conversation_id, prompt }
11
+ # + conversation_compaction phase "end" when a pending
12
+ # auto-compact marker exists for this session
13
+ # PreCompact -> conversation_compaction { conversation_id, phase: start }
14
+ #
15
+ # Copilot compaction is an LLM request through the Custom Endpoint. There is no
16
+ # PostCompact, so PreCompact opens a window and the next UserPromptSubmit closes
17
+ # it. That brackets the summarization turn between the two signals.
18
+ #
19
+ # Known gap: PreCompact does not fire for manual compact. Auto-compact only.
20
+ #
21
+ # Deliberately NOT registered: SessionStart / SubagentStart / SubagentStop /
22
+ # Stop / PreToolUse / PostToolUse. UserPromptSubmit already fires for subagent
23
+ # prompts, and subagents are correlated gateway-side from the parent's
24
+ # runSubagent tool call.
25
+
26
+ set -u
27
+
28
+ CONFIG="${SUBCONSCIOUS_HOOKS_ENV:-${HOME}/.copilot/subconscious-hooks.env}"
29
+ if [[ -f "$CONFIG" ]]; then
30
+ # shellcheck disable=SC1090
31
+ source "$CONFIG"
32
+ fi
33
+
34
+ GATEWAY_URL="${SUBCONSCIOUS_GATEWAY_URL:-}"
35
+ API_KEY="${SUBCONSCIOUS_API_KEY:-}"
36
+ PENDING_DIR="${SUBCONSCIOUS_COMPACT_PENDING_DIR:-${HOME}/.copilot/subconscious-compact-pending}"
37
+
38
+ fail_open() {
39
+ printf '%s\n' '{"continue":true}'
40
+ exit 0
41
+ }
42
+
43
+ post_hook() {
44
+ local payload="$1"
45
+ if [[ -z "$payload" ]]; then
46
+ return 0
47
+ fi
48
+ curl -sS -m 2 \
49
+ -H "Authorization: Bearer ${API_KEY}" \
50
+ -H "Content-Type: application/json" \
51
+ -H "x-subconscious-client: copilot" \
52
+ -d "$payload" \
53
+ "${GATEWAY_URL%/}/v1/agent-hooks" >/dev/null 2>&1 || true
54
+ }
55
+
56
+ pending_path() {
57
+ local session_id="$1"
58
+ # Session ids are opaque strings; keep the filename filesystem-safe.
59
+ local safe
60
+ safe="$(printf '%s' "$session_id" | tr -c 'A-Za-z0-9._-' '_')"
61
+ printf '%s/%s' "$PENDING_DIR" "$safe"
62
+ }
63
+
64
+ if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
65
+ echo "subconscious hook: missing SUBCONSCIOUS_GATEWAY_URL or SUBCONSCIOUS_API_KEY" >&2
66
+ fail_open
67
+ fi
68
+
69
+ for tool in jq curl; do
70
+ if ! command -v "$tool" >/dev/null 2>&1; then
71
+ echo "subconscious hook: ${tool} is required" >&2
72
+ fail_open
73
+ fi
74
+ done
75
+
76
+ INPUT="$(cat || true)"
77
+ if [[ -z "$INPUT" ]]; then
78
+ fail_open
79
+ fi
80
+
81
+ HOOK_EVENT="$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null || true)"
82
+ case "$HOOK_EVENT" in
83
+ UserPromptSubmit | PreCompact) ;;
84
+ *) fail_open ;;
85
+ esac
86
+
87
+ SESSION_ID="$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null || true)"
88
+ if [[ -z "$SESSION_ID" ]]; then
89
+ fail_open
90
+ fi
91
+
92
+ if [[ "$HOOK_EVENT" == "PreCompact" ]]; then
93
+ TRIGGER="$(printf '%s' "$INPUT" | jq -r '.trigger // "auto"' 2>/dev/null || true)"
94
+ # Unique per event; wall clock alone can collide within one second.
95
+ NOW_MS="$(date +%s)-$$-$RANDOM"
96
+ PAYLOAD="$(jq -n \
97
+ --arg conversation_id "$SESSION_ID" \
98
+ --arg hook_event_name "$HOOK_EVENT" \
99
+ --arg trigger "$TRIGGER" \
100
+ --arg dedupe_key "${SESSION_ID}:start:${TRIGGER}:${NOW_MS}" \
101
+ '{
102
+ event: "conversation_compaction",
103
+ conversation_id: $conversation_id,
104
+ phase: "start",
105
+ hook_event_name: $hook_event_name,
106
+ dedupe_key: $dedupe_key,
107
+ metadata: { trigger: $trigger }
108
+ }'
109
+ )"
110
+ post_hook "$PAYLOAD"
111
+
112
+ mkdir -p "$PENDING_DIR"
113
+ # Marker tells the next UserPromptSubmit to close this compaction window.
114
+ printf '%s\n' "$NOW_MS" >"$(pending_path "$SESSION_ID")" 2>/dev/null || true
115
+ fail_open
116
+ fi
117
+
118
+ # UserPromptSubmit
119
+ PROMPT="$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)"
120
+ CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || true)"
121
+ WORKSPACE=""
122
+ if [[ -n "$CWD" ]]; then
123
+ WORKSPACE="$(basename "$CWD")"
124
+ fi
125
+
126
+ # Nothing to anchor on without a prompt.
127
+ if [[ -z "$PROMPT" ]]; then
128
+ fail_open
129
+ fi
130
+
131
+ ENSURE_PAYLOAD="$(jq -n \
132
+ --arg event "conversation_ensure" \
133
+ --arg conversation_id "$SESSION_ID" \
134
+ --arg prompt "$PROMPT" \
135
+ --arg workspace "$WORKSPACE" \
136
+ --arg hook_event_name "$HOOK_EVENT" \
137
+ '{
138
+ event: $event,
139
+ conversation_id: $conversation_id,
140
+ prompt: $prompt,
141
+ workspace: (if $workspace == "" then null else $workspace end),
142
+ hook_event_name: $hook_event_name
143
+ } | with_entries(select(.value != null))'
144
+ )"
145
+
146
+ # Response body is intentionally ignored: the gateway resolves the VS Code
147
+ # session id to its own UUID on every call, so there is no mapping to cache.
148
+ post_hook "$ENSURE_PAYLOAD"
149
+
150
+ MARKER="$(pending_path "$SESSION_ID")"
151
+ if [[ -f "$MARKER" ]]; then
152
+ START_TOKEN="$(tr -d '[:space:]' <"$MARKER" 2>/dev/null || true)"
153
+ rm -f "$MARKER" 2>/dev/null || true
154
+ NOW_MS="$(date +%s)-$$-$RANDOM"
155
+ END_PAYLOAD="$(jq -n \
156
+ --arg conversation_id "$SESSION_ID" \
157
+ --arg hook_event_name "$HOOK_EVENT" \
158
+ --arg start_token "${START_TOKEN:-}" \
159
+ --arg dedupe_key "${SESSION_ID}:end:${START_TOKEN:-0}:${NOW_MS}" \
160
+ '{
161
+ event: "conversation_compaction",
162
+ conversation_id: $conversation_id,
163
+ phase: "end",
164
+ hook_event_name: $hook_event_name,
165
+ dedupe_key: $dedupe_key,
166
+ metadata: { closes_start_token: (if $start_token == "" then null else $start_token end) }
167
+ | with_entries(select(.value != null))
168
+ }'
169
+ )"
170
+ post_hook "$END_PAYLOAD"
171
+ fi
172
+
173
+ fail_open
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "UserPromptSubmit": [
5
+ {
6
+ "type": "command",
7
+ "command": "HOOK_SH_PATH",
8
+ "timeout": 2
9
+ }
10
+ ],
11
+ "PreCompact": [
12
+ {
13
+ "type": "command",
14
+ "command": "HOOK_SH_PATH",
15
+ "timeout": 2
16
+ }
17
+ ]
18
+ }
19
+ }