subconscious-cli 0.2.1 → 0.3.1

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,284 @@
1
+ #!/usr/bin/env bash
2
+ # ── Subconscious API Gateway — Cursor hooks setup ─────────────────────────────
3
+ # Install Cursor hooks that announce each agent handoff to your gateway so
4
+ # /v1/chat/completions requests are grouped into Conversations.
5
+ #
6
+ # Quick start:
7
+ # ./install.sh --gateway-url https://your-gateway.example --api-key sk-gw-...
8
+ # ./install.sh status # show current hook config
9
+ # ./install.sh uninstall # remove hooks
10
+ #
11
+ # Cursor model/URL are set in Cursor settings (OpenAI API Key Override), not
12
+ # here. This script installs the conversation-correlation hooks (user-wide
13
+ # under ~/.cursor). Restart Cursor after install so it reloads hooks.json.
14
+ #
15
+ # ── What this does under the hood ────────────────────────────────────────────
16
+ # Equivalent manual setup (three pieces):
17
+ #
18
+ # 1. Cursor settings → enable "OpenAI API Key Override":
19
+ # Base URL: https://your-gateway.example/v1
20
+ # API Key: sk-gw-...
21
+ #
22
+ # 2. Write ~/.cursor/hooks.json pointing at the hook script:
23
+ # {
24
+ # "version": 1,
25
+ # "hooks": {
26
+ # "beforeSubmitPrompt": [{ "command": "~/.cursor/hooks/subconscious-hook.sh", "timeout": 2 }],
27
+ # "preCompact": [{ "command": "~/.cursor/hooks/subconscious-hook.sh", "timeout": 2 }]
28
+ # }
29
+ # }
30
+ #
31
+ # 3. Write ~/.cursor/subconscious-hooks.env (mode 600):
32
+ # export SUBCONSCIOUS_GATEWAY_URL=https://your-gateway.example
33
+ # export SUBCONSCIOUS_API_KEY=sk-gw-...
34
+ #
35
+ # The hook script (hook.sh) POSTs conversation_ensure to /v1/agent-hooks with the raw
36
+ # prompt text once per submission. The gateway fingerprints the prompt itself,
37
+ # binds the first LLM request of that prompt, then chains every later turn of the
38
+ # conversation onto it -- including subagents. Unlike Claude Code / Pi /
39
+ # OpenCode, Cursor has no native session headers and hooks cannot inject headers
40
+ # into model HTTP, so this announcement is required for correlation.
41
+ # ─────────────────────────────────────────────────────────────────────────────
42
+
43
+ set -euo pipefail
44
+
45
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
+ HOOK_SRC="${SCRIPT_DIR}/hook.sh"
47
+ HOOKS_TEMPLATE="${SCRIPT_DIR}/hooks.json"
48
+
49
+ # Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
50
+ SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
51
+ [[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
52
+ if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
53
+
54
+ GATEWAY_URL="${GATEWAY_URL:-}"
55
+ API_KEY="${CURSOR_API_KEY:-${API_KEY:-}}"
56
+ MODEL="${MODEL:-subconscious/glm-5.2}"
57
+ COMMAND="install"
58
+
59
+ DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.2
60
+ subconscious/tim-qwen3.6-27b
61
+ subconscious/deepseek-v4-flash-marathon"
62
+
63
+ SUPPORTED_MODELS=()
64
+ add_supported_model() {
65
+ local model_id="$1"
66
+ for existing in "${SUPPORTED_MODELS[@]:-}"; do
67
+ [[ "$existing" == "$model_id" ]] && return
68
+ done
69
+ SUPPORTED_MODELS+=("$model_id")
70
+ }
71
+ add_supported_model "$MODEL"
72
+ while IFS= read -r model_id; do
73
+ [[ -n "$model_id" ]] && add_supported_model "$model_id"
74
+ done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
75
+
76
+ usage() {
77
+ cat <<'EOF'
78
+ Usage:
79
+ subc cursor install [--gateway-url URL] [--api-key KEY]
80
+ subc cursor uninstall
81
+ subc cursor status
82
+
83
+ Installs Cursor hooks (user-wide under ~/.cursor) that POST to /v1/agent-hooks:
84
+ conversation_ensure on each prompt submission so the gateway can group
85
+ Conversations for Cursor traffic, and conversation_compaction on preCompact so
86
+ context accounting restarts at the right turn. Existing hook entries are kept.
87
+
88
+ Requires: jq, curl. Restart Cursor after install.
89
+ EOF
90
+ }
91
+
92
+ while [[ $# -gt 0 ]]; do
93
+ case "$1" in
94
+ install|uninstall|status)
95
+ COMMAND="$1"
96
+ shift
97
+ ;;
98
+ --gateway-url)
99
+ GATEWAY_URL="${2:-}"
100
+ shift 2
101
+ ;;
102
+ --api-key)
103
+ API_KEY="${2:-}"
104
+ shift 2
105
+ ;;
106
+ -h|--help)
107
+ usage
108
+ exit 0
109
+ ;;
110
+ *)
111
+ echo "unknown argument: $1" >&2
112
+ usage >&2
113
+ exit 1
114
+ ;;
115
+ esac
116
+ done
117
+
118
+ CURSOR_DIR="${HOME}/.cursor"
119
+ HOOKS_JSON="${CURSOR_DIR}/hooks.json"
120
+ HOOK_DST="${CURSOR_DIR}/hooks/subconscious-hook.sh"
121
+ ENV_FILE="${CURSOR_DIR}/subconscious-hooks.env"
122
+ MARKER="subconscious-hook.sh"
123
+
124
+ require_cmds() {
125
+ local missing=0
126
+ for c in jq curl; do
127
+ if ! command -v "$c" >/dev/null 2>&1; then
128
+ echo "missing required command: $c" >&2
129
+ missing=1
130
+ fi
131
+ done
132
+ if [[ "$missing" -ne 0 ]]; then
133
+ exit 1
134
+ fi
135
+ }
136
+
137
+ write_env() {
138
+ umask 077
139
+ cat >"$ENV_FILE" <<EOF
140
+ # Generated by subc — do not commit secrets.
141
+ export SUBCONSCIOUS_GATEWAY_URL='${GATEWAY_URL}'
142
+ export SUBCONSCIOUS_API_KEY='${API_KEY}'
143
+ EOF
144
+ chmod 600 "$ENV_FILE"
145
+ }
146
+
147
+ install_hook_script() {
148
+ mkdir -p "$(dirname "$HOOK_DST")"
149
+ cp "$HOOK_SRC" "$HOOK_DST"
150
+ chmod +x "$HOOK_DST"
151
+ }
152
+
153
+ # merge_hook_entries <hooks_json> <marker> <command> <event> [<event>...]
154
+ #
155
+ # Additive, idempotent merge of a single hook command into one or more events
156
+ # of a hooks.json file. Preserves ALL other entries verbatim, including
157
+ # third-party hooks, prompt hooks (no `command` field), and unknown events.
158
+ #
159
+ # Invariants:
160
+ # - Only entries whose `command` contains <marker> are replaced; others untouched.
161
+ # - Running twice yields the same result as once (idempotent, order-stable).
162
+ # - Entries without a `command` field are never matched/removed.
163
+ # - Missing/empty file is initialized to {"version":1,"hooks":{}}.
164
+ merge_hook_entries() {
165
+ local hooks_json="$1" marker="$2" command="$3"
166
+ shift 3
167
+ local events
168
+ events="$(printf '%s\n' "$@" | jq -R . | jq -s .)"
169
+
170
+ if [[ ! -f "$hooks_json" ]]; then
171
+ printf '%s\n' '{"version":1,"hooks":{}}' >"$hooks_json"
172
+ fi
173
+
174
+ local tmp
175
+ tmp="$(mktemp)"
176
+ jq --arg marker "$marker" --arg cmd "$command" --argjson events "$events" '
177
+ .version = (.version // 1) |
178
+ .hooks = (.hooks // {}) |
179
+ # Strip prior entries with our marker from every event, keep everything else.
180
+ .hooks |= with_entries(
181
+ .value = ((.value // []) | map(select((.command // "") | tostring | contains($marker) | not)))
182
+ ) |
183
+ # Append our entry to each requested event (preserving any other entries).
184
+ .hooks = (.hooks | reduce ($events[]) as $e (.;
185
+ .[$e] = ((.[$e] // []) + [{"command": $cmd, "timeout": 2}])
186
+ ))
187
+ ' "$hooks_json" >"$tmp"
188
+ mv "$tmp" "$hooks_json"
189
+ }
190
+
191
+ # remove_hook_entries <hooks_json> <marker>
192
+ #
193
+ # Remove every entry whose `command` contains <marker> from all events.
194
+ # Preserves all other entries (third-party, prompt hooks, unknown events).
195
+ remove_hook_entries() {
196
+ local hooks_json="$1" marker="$2"
197
+ [[ -f "$hooks_json" ]] || return 0
198
+ local tmp
199
+ tmp="$(mktemp)"
200
+ jq --arg marker "$marker" '
201
+ .hooks //= {} |
202
+ .hooks |= with_entries(
203
+ .value = ((.value // []) | map(select((.command // "") | tostring | contains($marker) | not)))
204
+ )
205
+ ' "$hooks_json" >"$tmp"
206
+ mv "$tmp" "$hooks_json"
207
+ }
208
+
209
+ merge_hooks_json() {
210
+ if [[ ! -f "$HOOKS_JSON" ]]; then
211
+ sed "s|HOOK_SH_PATH|${HOOK_DST}|g" "$HOOKS_TEMPLATE" >"$HOOKS_JSON"
212
+ return
213
+ fi
214
+ # Replace our marker entries, then register the two lifecycle events we use.
215
+ remove_hook_entries "$HOOKS_JSON" "$MARKER"
216
+ merge_hook_entries "$HOOKS_JSON" "$MARKER" "$HOOK_DST" beforeSubmitPrompt preCompact
217
+ }
218
+
219
+ uninstall_hooks() {
220
+ remove_hook_entries "$HOOKS_JSON" "$MARKER"
221
+ rm -f "$HOOK_DST" "$ENV_FILE"
222
+ }
223
+
224
+ status() {
225
+ echo "cursor dir: $CURSOR_DIR"
226
+ echo "hooks.json: $HOOKS_JSON"
227
+ if [[ -f "$HOOKS_JSON" ]] && jq -e --arg m "$MARKER" '
228
+ [.hooks[]?[]? | select((.command // "") | tostring | contains($m))] | length > 0
229
+ ' "$HOOKS_JSON" >/dev/null 2>&1; then
230
+ echo "hooks: installed"
231
+ else
232
+ echo "hooks: not installed"
233
+ fi
234
+ if [[ -f "$ENV_FILE" ]]; then
235
+ echo "env: $ENV_FILE (present)"
236
+ # shellcheck disable=SC1090
237
+ source "$ENV_FILE"
238
+ echo "gateway: ${SUBCONSCIOUS_GATEWAY_URL:-unset}"
239
+ if [[ -n "${SUBCONSCIOUS_API_KEY:-}" ]]; then
240
+ echo "api key: set (${#SUBCONSCIOUS_API_KEY} chars)"
241
+ else
242
+ echo "api key: unset"
243
+ fi
244
+ else
245
+ echo "env: missing"
246
+ fi
247
+ if [[ -x "$HOOK_DST" ]]; then
248
+ echo "hook script: $HOOK_DST (executable)"
249
+ else
250
+ echo "hook script: missing"
251
+ fi
252
+ }
253
+
254
+ case "$COMMAND" in
255
+ install)
256
+ require_cmds
257
+ if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
258
+ echo "--gateway-url and --api-key are required for install" >&2
259
+ exit 1
260
+ fi
261
+ mkdir -p "$CURSOR_DIR"
262
+ install_hook_script
263
+ write_env
264
+ merge_hooks_json
265
+ echo "Installed Subconscious Cursor hooks into $CURSOR_DIR"
266
+ echo ""
267
+ echo "Finish in Cursor Settings"
268
+ echo " Enable OpenAI API Key Override, then use:"
269
+ echo " Base URL: ${GATEWAY_URL%/}/v1"
270
+ echo " Add the available custom models:"
271
+ for model_id in "${SUPPORTED_MODELS[@]}"; do
272
+ echo " ${model_id}"
273
+ done
274
+ echo " Select ${MODEL} for this profile."
275
+ echo " Paste your Subconscious API key there, then fully restart Cursor."
276
+ ;;
277
+ uninstall)
278
+ uninstall_hooks
279
+ echo "Removed Subconscious Cursor hooks from $CURSOR_DIR"
280
+ ;;
281
+ status)
282
+ status
283
+ ;;
284
+ esac
@@ -0,0 +1,298 @@
1
+ #!/usr/bin/env bash
2
+ # ── Subconscious API Gateway — OpenCode setup ─────────────────────────────────
3
+ # Point OpenCode at your gateway. Writes a provider config with session headers
4
+ # so the gateway can correlate OpenCode requests into Conversations.
5
+ #
6
+ # Quick start:
7
+ # ./install.sh --gateway-url https://your-gateway.example --api-key sk-gw-...
8
+ # ./install.sh status # show current config
9
+ # ./install.sh uninstall # revert to default opencode config
10
+ #
11
+ # Writes ~/.opencode/opencode.json (user config). Does not touch project-level
12
+ # opencode.json files. Restart opencode after install.
13
+ #
14
+ # ── What this does under the hood ────────────────────────────────────────────
15
+ # Equivalent manual setup (writes ~/.opencode/opencode.json + env var):
16
+ #
17
+ # export SUBCONSCIOUS_API_KEY=sk-gw-...
18
+ # cat > ~/.opencode/opencode.json <<'EOF'
19
+ # {
20
+ # "$schema": "https://opencode.ai/config.json",
21
+ # "provider": {
22
+ # "subconscious": {
23
+ # "npm": "@ai-sdk/openai-compatible",
24
+ # "name": "Subconscious Gateway",
25
+ # "options": {
26
+ # "baseURL": "https://your-gateway.example/v1",
27
+ # "apiKey": "{env:SUBCONSCIOUS_API_KEY}",
28
+ # "headers": { "x-subconscious-client": "opencode" }
29
+ # },
30
+ # "models": {
31
+ # "subconscious/glm-5.2": {
32
+ # "name": "subconscious/glm-5.2",
33
+ # "tools": true,
34
+ # "limit": { "context": 5000000, "output": 65536 }
35
+ # }
36
+ # }
37
+ # }
38
+ # },
39
+ # "model": "subconscious/subconscious/glm-5.2"
40
+ # }
41
+ # EOF
42
+ # opencode
43
+ #
44
+ # The x-subconscious-client header tells the gateway to classify traffic as
45
+ # OpenCode. OpenCode also sends native x-session-affinity / x-session-id
46
+ # headers for conversation correlation. Model limit.context drives auto
47
+ # compaction (default on); custom providers do not inherit models.dev limits.
48
+ #
49
+ # Also installs a plugin that reports compactions to the gateway. OpenCode
50
+ # summarizes through the configured provider, so without it the gateway counts
51
+ # that summarization as a normal main-thread turn.
52
+ # ─────────────────────────────────────────────────────────────────────────────
53
+
54
+ set -euo pipefail
55
+
56
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
57
+
58
+ # Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
59
+ SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
60
+ [[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
61
+ if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
62
+
63
+ COMMAND="install"
64
+ GATEWAY_URL="${GATEWAY_URL:-}"
65
+ API_KEY="${OPENCODE_API_KEY:-${API_KEY:-}}"
66
+ MODEL="${MODEL:-subconscious/glm-5.2}"
67
+ # OpenCode auto-compaction uses model limit.context. Custom openai-compatible
68
+ # providers do not get that from baseURL/models.dev; set it explicitly.
69
+ CONTEXT_LIMIT="${OPENCODE_CONTEXT_LIMIT:-5000000}"
70
+ OUTPUT_LIMIT="${OPENCODE_OUTPUT_LIMIT:-65536}"
71
+
72
+ usage() {
73
+ cat <<'EOF'
74
+ Usage:
75
+ subc opencode status
76
+ subc opencode uninstall
77
+ subc opencode help
78
+
79
+ OpenCode is launch-only: subc opencode. Uninstall removes only the Subconscious
80
+ provider, plugin, and env file. It does not delete ~/.opencode/opencode.json.
81
+ EOF
82
+ }
83
+
84
+ while [[ $# -gt 0 ]]; do
85
+ case "$1" in
86
+ install|uninstall|status)
87
+ COMMAND="$1"
88
+ shift
89
+ ;;
90
+ --gateway-url)
91
+ GATEWAY_URL="${2:-}"
92
+ shift 2
93
+ ;;
94
+ --api-key)
95
+ API_KEY="${2:-}"
96
+ shift 2
97
+ ;;
98
+ --model)
99
+ MODEL="${2:-}"
100
+ shift 2
101
+ ;;
102
+ --context-limit)
103
+ CONTEXT_LIMIT="${2:-}"
104
+ shift 2
105
+ ;;
106
+ --output-limit)
107
+ OUTPUT_LIMIT="${2:-}"
108
+ shift 2
109
+ ;;
110
+ -h|--help)
111
+ usage
112
+ exit 0
113
+ ;;
114
+ *)
115
+ echo "unknown argument: $1" >&2
116
+ usage >&2
117
+ exit 1
118
+ ;;
119
+ esac
120
+ done
121
+
122
+ DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.2
123
+ subconscious/tim-qwen3.6-27b
124
+ subconscious/deepseek-v4-flash-marathon"
125
+ SUPPORTED_MODELS=()
126
+
127
+ add_supported_model() {
128
+ local model_id="$1" existing
129
+ [[ -n "$model_id" ]] || return 0
130
+ if [[ ! "$model_id" =~ ^[-A-Za-z0-9._:/+]+$ ]]; then
131
+ echo "error: invalid model id: $model_id" >&2
132
+ exit 1
133
+ fi
134
+ if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
135
+ for existing in "${SUPPORTED_MODELS[@]}"; do
136
+ [[ "$existing" == "$model_id" ]] && return 0
137
+ done
138
+ fi
139
+ SUPPORTED_MODELS+=("$model_id")
140
+ }
141
+
142
+ add_supported_model "$MODEL"
143
+ while IFS= read -r model_id; do
144
+ add_supported_model "$model_id"
145
+ done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
146
+
147
+ MODELS_JSON=""
148
+ for model_id in "${SUPPORTED_MODELS[@]}"; do
149
+ model_json="\"${model_id}\":{\"name\":\"${model_id}\",\"tools\":true,\"limit\":{\"context\":${CONTEXT_LIMIT},\"output\":${OUTPUT_LIMIT}}}"
150
+ if [[ -n "$MODELS_JSON" ]]; then
151
+ MODELS_JSON="${MODELS_JSON},${model_json}"
152
+ else
153
+ MODELS_JSON="$model_json"
154
+ fi
155
+ done
156
+
157
+ OPENCODE_DIR="${HOME}/.opencode"
158
+ OPENCODE_CONFIG="${OPENCODE_DIR}/opencode.json"
159
+ MARKER='x-subconscious-client'
160
+ # Plugins load from the XDG config dir, which is separate from the legacy ~/.opencode
161
+ # config path above. Docs: https://opencode.ai/docs/plugins/
162
+ PLUGIN_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/opencode/plugins"
163
+ PLUGIN_FILE="${PLUGIN_DIR}/subconscious-compaction.ts"
164
+ PLUGIN_SOURCE="${SCRIPT_DIR}/subconscious-compaction.ts"
165
+
166
+ require_cmds() {
167
+ local missing=0
168
+ for c in jq; do
169
+ if ! command -v "$c" >/dev/null 2>&1; then
170
+ echo "missing required command: $c" >&2
171
+ missing=1
172
+ fi
173
+ done
174
+ if [[ "$missing" -ne 0 ]]; then
175
+ exit 1
176
+ fi
177
+ }
178
+
179
+ write_config() {
180
+ mkdir -p "$OPENCODE_DIR"
181
+ local base_url="${GATEWAY_URL%/}/v1"
182
+ local provider
183
+ provider=$(cat <<EOF
184
+ {
185
+ "npm": "@ai-sdk/openai-compatible",
186
+ "name": "Subconscious Gateway",
187
+ "options": {
188
+ "baseURL": "${base_url}",
189
+ "apiKey": "{env:SUBCONSCIOUS_API_KEY}",
190
+ "headers": {
191
+ "x-subconscious-client": "opencode"
192
+ }
193
+ },
194
+ "models": {${MODELS_JSON}}
195
+ }
196
+ EOF
197
+ )
198
+ if [[ -f "$OPENCODE_CONFIG" ]]; then
199
+ local tmp
200
+ tmp="$(mktemp)"
201
+ jq --argjson provider "$provider" --arg model "subconscious/${MODEL}" '
202
+ .provider = (.provider // {})
203
+ | .provider.subconscious = $provider
204
+ | .model = $model
205
+ ' "$OPENCODE_CONFIG" >"$tmp"
206
+ mv "$tmp" "$OPENCODE_CONFIG"
207
+ else
208
+ jq -n --argjson provider "$provider" --arg model "subconscious/${MODEL}" \
209
+ '{ "$schema": "https://opencode.ai/config.json", provider: {subconscious: $provider}, model: $model }' \
210
+ >"$OPENCODE_CONFIG"
211
+ fi
212
+ local env_file="${OPENCODE_DIR}/subconscious.env"
213
+ umask 077
214
+ cat >"$env_file" <<EOF
215
+ # Generated by subc — do not commit secrets.
216
+ export SUBCONSCIOUS_API_KEY='${API_KEY}'
217
+ export SUBCONSCIOUS_GATEWAY_URL='${GATEWAY_URL%/}'
218
+ EOF
219
+ chmod 600 "$env_file"
220
+ }
221
+
222
+ write_plugin() {
223
+ if [[ ! -f "$PLUGIN_SOURCE" ]]; then
224
+ echo "warning: ${PLUGIN_SOURCE} missing; skipping compaction plugin" >&2
225
+ return
226
+ fi
227
+ mkdir -p "$PLUGIN_DIR"
228
+ cp "$PLUGIN_SOURCE" "$PLUGIN_FILE"
229
+ }
230
+
231
+ uninstall_config() {
232
+ if [[ -f "$OPENCODE_CONFIG" ]]; then
233
+ local tmp
234
+ tmp="$(mktemp)"
235
+ jq '
236
+ del(.provider.subconscious)
237
+ | if ((.model // "") | tostring | startswith("subconscious/")) then del(.model) else . end
238
+ ' "$OPENCODE_CONFIG" >"$tmp"
239
+ mv "$tmp" "$OPENCODE_CONFIG"
240
+ echo "Removed Subconscious provider from $OPENCODE_CONFIG"
241
+ else
242
+ echo "No OpenCode config at $OPENCODE_CONFIG"
243
+ fi
244
+ rm -f "${OPENCODE_DIR}/subconscious.env"
245
+ rm -f "$PLUGIN_FILE"
246
+ }
247
+
248
+ status() {
249
+ echo "scope: user"
250
+ echo "opencode dir: $OPENCODE_DIR"
251
+ echo "config: $OPENCODE_CONFIG"
252
+ if [[ -f "$PLUGIN_FILE" ]]; then
253
+ echo "compaction plugin: $PLUGIN_FILE (installed)"
254
+ else
255
+ echo "compaction plugin: not installed"
256
+ fi
257
+ if [[ -f "$OPENCODE_CONFIG" ]] && grep -q "$MARKER" "$OPENCODE_CONFIG" 2>/dev/null; then
258
+ echo "status: installed"
259
+ echo "model: $(jq -r '.model // "unset"' "$OPENCODE_CONFIG" 2>/dev/null || echo 'unknown')"
260
+ echo "context limit: $(jq -r '
261
+ (.model // "") as $m
262
+ | ($m | sub("^subconscious/"; "")) as $id
263
+ | .provider.subconscious.models[$id].limit.context // "unset"
264
+ ' "$OPENCODE_CONFIG" 2>/dev/null || echo 'unknown')"
265
+ echo "compaction.auto: $(jq -r '.compaction.auto // true' "$OPENCODE_CONFIG" 2>/dev/null || echo 'unknown')"
266
+ else
267
+ echo "status: not installed"
268
+ fi
269
+ if [[ -f "${OPENCODE_DIR}/subconscious.env" ]]; then
270
+ echo "env: ${OPENCODE_DIR}/subconscious.env (present)"
271
+ else
272
+ echo "env: missing"
273
+ fi
274
+ }
275
+
276
+ case "$COMMAND" in
277
+ install)
278
+ require_cmds
279
+ if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
280
+ echo "--gateway-url and --api-key are required for install" >&2
281
+ exit 1
282
+ fi
283
+ write_config
284
+ write_plugin
285
+ echo "Merged Subconscious OpenCode provider into $OPENCODE_CONFIG"
286
+ if [[ -f "$PLUGIN_FILE" ]]; then
287
+ echo "Installed compaction reporting plugin at $PLUGIN_FILE"
288
+ fi
289
+ echo "Launch OpenCode with: subc opencode"
290
+ ;;
291
+ uninstall)
292
+ uninstall_config
293
+ echo "Removed Subconscious OpenCode files; left $OPENCODE_CONFIG in place"
294
+ ;;
295
+ status)
296
+ status
297
+ ;;
298
+ esac
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # Point OpenCode at the Subconscious gateway — ephemerally.
3
+ #
4
+ # Exports SUBCONSCIOUS_API_KEY and OPENCODE_CONFIG_CONTENT as env vars so
5
+ # nothing is written to ~/.opencode/opencode.json. OpenCode reads the
6
+ # config from the OPENCODE_CONFIG_CONTENT env var at startup.
7
+ #
8
+ # Usage:
9
+ # ./run.sh # uses GATEWAY_URL/API_KEY from ../.env
10
+ # ./run.sh -- auth # pass args through to opencode
11
+ #
12
+ # Config: copy ../env.example to ../.env and edit. .env is gitignored.
13
+ # Profile env is injected by subc. A sibling .env is only used when SUBC_ENV_FILE is unset.
14
+ #
15
+ # Or source it to just export the env:
16
+ # source run.sh
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+
22
+ # Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
23
+ SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
24
+ [[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
25
+ if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
26
+
27
+ GATEWAY_URL="${GATEWAY_URL:-}"
28
+ API_KEY="${OPENCODE_API_KEY:-${API_KEY:-}}"
29
+ MODEL="${MODEL:-subconscious/glm-5.2}"
30
+ CONTEXT_LIMIT="${OPENCODE_CONTEXT_LIMIT:-5000000}"
31
+ OUTPUT_LIMIT="${OPENCODE_OUTPUT_LIMIT:-65536}"
32
+
33
+ DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.2
34
+ subconscious/tim-qwen3.6-27b
35
+ subconscious/deepseek-v4-flash-marathon"
36
+ SUPPORTED_MODELS=()
37
+
38
+ add_supported_model() {
39
+ local model_id="$1" existing
40
+ [[ -n "$model_id" ]] || return 0
41
+ if [[ ! "$model_id" =~ ^[-A-Za-z0-9._:/+]+$ ]]; then
42
+ echo "error: invalid model id: $model_id" >&2
43
+ exit 1
44
+ fi
45
+ if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
46
+ for existing in "${SUPPORTED_MODELS[@]}"; do
47
+ [[ "$existing" == "$model_id" ]] && return 0
48
+ done
49
+ fi
50
+ SUPPORTED_MODELS+=("$model_id")
51
+ }
52
+
53
+ add_supported_model "$MODEL"
54
+ while IFS= read -r model_id; do
55
+ add_supported_model "$model_id"
56
+ done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
57
+
58
+ MODELS_JSON=""
59
+ for model_id in "${SUPPORTED_MODELS[@]}"; do
60
+ model_json="\"${model_id}\":{\"name\":\"${model_id}\",\"tools\":true,\"limit\":{\"context\":${CONTEXT_LIMIT},\"output\":${OUTPUT_LIMIT}}}"
61
+ if [[ -n "$MODELS_JSON" ]]; then
62
+ MODELS_JSON="${MODELS_JSON},${model_json}"
63
+ else
64
+ MODELS_JSON="$model_json"
65
+ fi
66
+ done
67
+
68
+ if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
69
+ echo "error: GATEWAY_URL and API_KEY must be set in ../.env" >&2
70
+ exit 1
71
+ fi
72
+
73
+ # Parse args (only when executed, not sourced)
74
+ PASSTHRU=()
75
+ if [[ "${BASH_SOURCE[0]:-$0}" == "${0}" ]]; then
76
+ while [[ $# -gt 0 ]]; do
77
+ case "$1" in
78
+ --)
79
+ shift
80
+ PASSTHRU+=("$@")
81
+ break
82
+ ;;
83
+ *)
84
+ PASSTHRU+=("$1")
85
+ shift
86
+ ;;
87
+ esac
88
+ done
89
+ fi
90
+
91
+ BASE_URL="${GATEWAY_URL%/}/v1"
92
+
93
+ export SUBCONSCIOUS_API_KEY="$API_KEY"
94
+ # Compaction reporting needs the plugin on disk; this launch path writes nothing.
95
+ export SUBCONSCIOUS_GATEWAY_URL="${GATEWAY_URL%/}"
96
+ export OPENCODE_CONFIG_CONTENT=$(cat <<EOF
97
+ {"\$schema":"https://opencode.ai/config.json","provider":{"subconscious":{"npm":"@ai-sdk/openai-compatible","name":"Subconscious Gateway","options":{"baseURL":"${BASE_URL}","apiKey":"{env:SUBCONSCIOUS_API_KEY}","headers":{"x-subconscious-client":"opencode"}},"models":{${MODELS_JSON}}}},"model":"subconscious/${MODEL}"}
98
+ EOF
99
+ )
100
+
101
+ # If sourced, just export env and return.
102
+ if [[ "${BASH_SOURCE[0]:-$0}" != "${0}" ]]; then
103
+ return 0 2>/dev/null || true
104
+ fi
105
+
106
+ exec opencode ${PASSTHRU[@]+"${PASSTHRU[@]}"}