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,470 @@
1
+ #!/usr/bin/env bash
2
+ # ── Subconscious API Gateway — GitHub Copilot in VS Code setup ────────────────
3
+ # Point GitHub Copilot Chat in VS Code at your gateway and install hooks for
4
+ # conversation correlation.
5
+ #
6
+ # Quick start:
7
+ # ./install.sh # reads GATEWAY_URL + API_KEY from ../.env
8
+ # ./install.sh --gateway-url https://your-gateway.example --api-key sk-gw-...
9
+ # ./install.sh status # show current config + hooks
10
+ # ./install.sh uninstall # remove provider + hooks
11
+ #
12
+ # `install` is the default subcommand. It does two things:
13
+ # 1. Writes a Custom Endpoint provider into VS Code's chatLanguageModels.json
14
+ # 2. Installs VS Code agent hooks (~/.copilot/hooks/) for conversation correlation
15
+ #
16
+ # The API key for the model provider is NOT passed on the command line — VS Code
17
+ # requires it to be entered through its UI (the script writes a stable
18
+ # ${input:chat.lm.secret.*} reference, and you paste the key once via Manage
19
+ # Language Models). The API key for the hooks is read from .env or --api-key
20
+ # and stored in ~/.copilot/subconscious-hooks.env (mode 600).
21
+ #
22
+ # Assumes VS Code is installed globally (Code or Code - Insiders). Restart
23
+ # VS Code after install so it reloads chatLanguageModels.json and hooks.
24
+ #
25
+ # ── What this does under the hood ────────────────────────────────────────────
26
+ # Writes a Custom Endpoint provider entry into chatLanguageModels.json:
27
+ #
28
+ # [
29
+ # {
30
+ # "name": "Subconscious Gateway",
31
+ # "vendor": "customendpoint",
32
+ # "apiKey": "${input:chat.lm.secret.subconscious-gateway}",
33
+ # "apiType": "chat-completions",
34
+ # "models": [
35
+ # {
36
+ # "id": "subconscious/glm-5.2",
37
+ # "name": "Subconscious GLM 5.2",
38
+ # "url": "https://your-gateway.example/v1/chat/completions",
39
+ # "toolCalling": true,
40
+ # "vision": false,
41
+ # "maxInputTokens": 5000000,
42
+ # "maxOutputTokens": 65536,
43
+ # "streaming": true,
44
+ # "requestHeaders": { "x-subconscious-client": "copilot" }
45
+ # }
46
+ # ]
47
+ # }
48
+ # ]
49
+ #
50
+ # And installs VS Code agent hooks (~/.copilot/hooks/) that POST to
51
+ # /v1/agent-hooks:
52
+ # UserPromptSubmit -> conversation_ensure (+ compaction end if pending)
53
+ # PreCompact -> conversation_compaction phase start
54
+ # There is no PostCompact; the next UserPromptSubmit closes the window so the
55
+ # summarization LLM turn between them is billed as compaction. Manual compact
56
+ # does not fire PreCompact (accepted gap). The gateway fingerprints prompts and
57
+ # chains turns; subagents need no extra hooks. The x-subconscious-client:
58
+ # copilot header (hook script + model requestHeaders) classifies the traffic.
59
+ # hooks: https://code.visualstudio.com/docs/copilot/customization/hooks
60
+ # PreCompact: https://code.visualstudio.com/docs/agents/reference/hooks-reference#precompact
61
+ # ─────────────────────────────────────────────────────────────────────────────
62
+
63
+ set -euo pipefail
64
+
65
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
66
+ HOOK_SRC="${SCRIPT_DIR}/hook.sh"
67
+ HOOKS_TEMPLATE="${SCRIPT_DIR}/hooks.json"
68
+
69
+ # Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
70
+ SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
71
+ [[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
72
+ if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
73
+
74
+ # Default to `install` so `./install.sh --gateway-url URL` works without an
75
+ # explicit `install` subcommand. `status` / `uninstall` still work.
76
+ COMMAND="install"
77
+ GATEWAY_URL="${GATEWAY_URL:-}"
78
+ API_KEY="${COPILOT_API_KEY:-${API_KEY:-}}"
79
+ MODEL="${MODEL:-subconscious/glm-5.2}"
80
+ MAX_INPUT_TOKENS="${COPILOT_MAX_INPUT_TOKENS:-5000000}"
81
+ MAX_OUTPUT_TOKENS="${COPILOT_MAX_OUTPUT_TOKENS:-65536}"
82
+ VSCODE_APP="${VSCODE_APP:-}" # auto-detected: Code | Code - Insiders | VSCodium
83
+
84
+ # VS Code's customendpoint provider requires the apiKey to be a
85
+ # ${input:chat.lm.secret.<id>} reference into its OS secret store — a plaintext
86
+ # key is silently dropped (microsoft/vscode#322299). The secret itself must be
87
+ # entered through VS Code's UI ("Add Models" → "Custom Endpoint"). The script
88
+ # writes a stable secret id so you only enter the key once.
89
+ SECRET_ID="chat.lm.secret.subconscious-gateway"
90
+
91
+ # Hooks are installed user-wide under ~/.copilot (VS Code user hooks dir).
92
+ COPILOT_DIR="${HOME}/.copilot"
93
+ HOOKS_DIR="${COPILOT_DIR}/hooks"
94
+ HOOK_DST="${HOOKS_DIR}/subconscious-hook.sh"
95
+ HOOKS_JSON="${HOOKS_DIR}/subconscious-hooks.json"
96
+ ENV_FILE="${COPILOT_DIR}/subconscious-hooks.env"
97
+ MARKER="subconscious-hook.sh"
98
+
99
+ usage() {
100
+ cat <<'EOF'
101
+ Usage:
102
+ subc copilot install [--gateway-url URL] [--api-key KEY] [--model MODEL]
103
+ subc copilot uninstall
104
+ subc copilot status
105
+
106
+ `install` is the default subcommand. It writes a "Subconscious Gateway"
107
+ Custom Endpoint provider into VS Code's user-wide chatLanguageModels.json
108
+ AND installs VS Code agent hooks for conversation correlation.
109
+
110
+ The API key for the model provider is NOT passed on the command line — VS Code
111
+ requires it to be entered through its UI (the script writes a stable
112
+ ${input:chat.lm.secret.*} reference into chatLanguageModels.json, and you
113
+ paste the key once via Manage Language Models).
114
+
115
+ The API key for the hooks is read from the profile env or --api-key and
116
+ stored in ~/.copilot/subconscious-hooks.env (mode 600).
117
+
118
+ Reads GATEWAY_URL, API_KEY, and MODEL from the profile env by
119
+ default. Restart VS Code after install.
120
+
121
+ Options:
122
+ --gateway-url URL Gateway origin (default: $GATEWAY_URL from .env)
123
+ --api-key KEY Gateway API key for hooks (default: $API_KEY from .env)
124
+ --model MODEL Model id (default: subconscious/glm-5.2)
125
+ --max-input-tokens N Model context window input tokens (default: 5000000)
126
+ --max-output-tokens N Model max output tokens (default: 65536)
127
+ --vscode-app APP Code, Code - Insiders, or VSCodium (auto-detected)
128
+
129
+ Requires: jq, curl. Restart VS Code after install, then enter your model API
130
+ key once via Manage Language Models.
131
+ EOF
132
+ }
133
+
134
+ while [[ $# -gt 0 ]]; do
135
+ case "$1" in
136
+ install|uninstall|status)
137
+ COMMAND="$1"
138
+ shift
139
+ ;;
140
+ --gateway-url)
141
+ GATEWAY_URL="${2:-}"
142
+ shift 2
143
+ ;;
144
+ --api-key)
145
+ API_KEY="${2:-}"
146
+ shift 2
147
+ ;;
148
+ --model)
149
+ MODEL="${2:-}"
150
+ shift 2
151
+ ;;
152
+ --max-input-tokens)
153
+ MAX_INPUT_TOKENS="${2:-}"
154
+ shift 2
155
+ ;;
156
+ --max-output-tokens)
157
+ MAX_OUTPUT_TOKENS="${2:-}"
158
+ shift 2
159
+ ;;
160
+ --vscode-app)
161
+ VSCODE_APP="${2:-}"
162
+ shift 2
163
+ ;;
164
+ -h|--help)
165
+ usage
166
+ exit 0
167
+ ;;
168
+ *)
169
+ echo "unknown argument: $1" >&2
170
+ usage >&2
171
+ exit 1
172
+ ;;
173
+ esac
174
+ done
175
+
176
+ DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.2
177
+ subconscious/tim-qwen3.6-27b
178
+ subconscious/deepseek-v4-flash-marathon"
179
+ SUPPORTED_MODELS=()
180
+
181
+ add_supported_model() {
182
+ local model_id="$1" existing
183
+ [[ -n "$model_id" ]] || return 0
184
+ if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
185
+ for existing in "${SUPPORTED_MODELS[@]}"; do
186
+ [[ "$existing" == "$model_id" ]] && return 0
187
+ done
188
+ fi
189
+ SUPPORTED_MODELS+=("$model_id")
190
+ }
191
+
192
+ add_supported_model "$MODEL"
193
+ while IFS= read -r model_id; do
194
+ add_supported_model "$model_id"
195
+ done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
196
+
197
+ PROVIDER_NAME="Subconscious Gateway"
198
+ MARKER='Subconscious Gateway'
199
+
200
+ require_cmds() {
201
+ local missing=0
202
+ for c in jq curl; do
203
+ if ! command -v "$c" >/dev/null 2>&1; then
204
+ echo "missing required command: $c" >&2
205
+ missing=1
206
+ fi
207
+ done
208
+ if [[ "$missing" -ne 0 ]]; then
209
+ exit 1
210
+ fi
211
+ }
212
+
213
+ # Resolve the VS Code User directory across platforms.
214
+ # macOS: ~/Library/Application Support/Code/User
215
+ # Linux: ~/.config/Code/User
216
+ # Windows: %APPDATA%\Code\User (Git Bash: ~/AppData/Roaming/Code/User)
217
+ detect_vscode_dir() {
218
+ local app="${VSCODE_APP:-}"
219
+ if [[ -z "$app" ]]; then
220
+ if [[ -d "${HOME}/Library/Application Support/Code/User" ]]; then
221
+ app="Code"
222
+ elif [[ -d "${HOME}/Library/Application Support/Code - Insiders/User" ]]; then
223
+ app="Code - Insiders"
224
+ elif [[ -d "${HOME}/.config/Code/User" ]]; then
225
+ app="Code"
226
+ elif [[ -d "${HOME}/.config/Code - Insiders/User" ]]; then
227
+ app="Code - Insiders"
228
+ elif [[ -d "${HOME}/.config/VSCodium/User" ]]; then
229
+ app="VSCodium"
230
+ elif [[ -d "${HOME}/AppData/Roaming/Code/User" ]]; then
231
+ app="Code"
232
+ else
233
+ echo "could not find a VS Code User directory; pass --vscode-app explicitly" >&2
234
+ exit 1
235
+ fi
236
+ fi
237
+
238
+ local user_dir=""
239
+ case "$(uname -s)" in
240
+ Darwin)
241
+ user_dir="${HOME}/Library/Application Support/${app}/User"
242
+ ;;
243
+ Linux)
244
+ user_dir="${HOME}/.config/${app}/User"
245
+ ;;
246
+ MINGW*|MSYS*|CYGWIN*)
247
+ user_dir="${HOME}/AppData/Roaming/${app}/User"
248
+ ;;
249
+ *)
250
+ echo "unsupported platform: $(uname -s)" >&2
251
+ exit 1
252
+ ;;
253
+ esac
254
+
255
+ if [[ ! -d "$user_dir" ]]; then
256
+ echo "VS Code User directory not found: $user_dir" >&2
257
+ echo "pass --vscode-app with the correct app name" >&2
258
+ exit 1
259
+ fi
260
+ echo "$user_dir"
261
+ }
262
+
263
+ # The ${input:...} prefix VS Code uses to reference secrets in chatLanguageModels.json.
264
+ INPUT_PREFIX='${input:'
265
+
266
+ # Strip any prior Subconscious provider entry, preserving other providers.
267
+ strip_subconscious() {
268
+ local file="$1"
269
+ if [[ ! -f "$file" ]]; then
270
+ echo "[]"
271
+ return
272
+ fi
273
+ jq --arg name "$PROVIDER_NAME" \
274
+ 'map(select(.name != $name and ((.name // "") | test("subconscious"; "i") | not)))' \
275
+ "$file" 2>/dev/null || echo "[]"
276
+ }
277
+
278
+ write_config() {
279
+ local user_dir="$1"
280
+ local models_json="${user_dir}/chatLanguageModels.json"
281
+ local base_url="${GATEWAY_URL%/}"
282
+ local chat_url="${base_url}/v1/chat/completions"
283
+
284
+ mkdir -p "$user_dir"
285
+ local existing
286
+ existing="$(strip_subconscious "$models_json")"
287
+
288
+ local provider_models='[]' model_id
289
+ for model_id in "${SUPPORTED_MODELS[@]}"; do
290
+ provider_models=$(jq -cn \
291
+ --argjson models "$provider_models" \
292
+ --arg modelId "$model_id" \
293
+ --arg modelName "Subconscious ${model_id}" \
294
+ --arg url "$chat_url" \
295
+ --argjson maxIn "$MAX_INPUT_TOKENS" \
296
+ --argjson maxOut "$MAX_OUTPUT_TOKENS" \
297
+ '$models + [{
298
+ id: $modelId,
299
+ name: $modelName,
300
+ url: $url,
301
+ toolCalling: true,
302
+ vision: false,
303
+ maxInputTokens: $maxIn,
304
+ maxOutputTokens: $maxOut,
305
+ streaming: true,
306
+ requestHeaders: { "x-subconscious-client": "copilot" }
307
+ }]')
308
+ done
309
+
310
+ local new_provider
311
+ new_provider=$(jq -n \
312
+ --arg name "$PROVIDER_NAME" \
313
+ --arg apiKeyRef "${INPUT_PREFIX}${SECRET_ID}}" \
314
+ --argjson models "$provider_models" \
315
+ '{
316
+ name: $name,
317
+ vendor: "customendpoint",
318
+ apiKey: $apiKeyRef,
319
+ apiType: "chat-completions",
320
+ models: $models
321
+ }')
322
+
323
+ local merged
324
+ merged=$(echo "$existing" | jq --argjson p "$new_provider" '. + [$p]')
325
+
326
+ umask 077
327
+ echo "$merged" >"$models_json"
328
+ umask 022
329
+ echo "$models_json"
330
+ }
331
+
332
+ uninstall_config() {
333
+ local user_dir="$1"
334
+ local models_json="${user_dir}/chatLanguageModels.json"
335
+ if [[ -f "$models_json" ]]; then
336
+ if grep -qi "$MARKER" "$models_json" 2>/dev/null; then
337
+ local tmp
338
+ tmp="$(mktemp)"
339
+ jq --arg name "$PROVIDER_NAME" \
340
+ 'map(select(.name != $name and ((.name // "") | test("subconscious"; "i") | not)))' \
341
+ "$models_json" >"$tmp"
342
+ mv "$tmp" "$models_json"
343
+ echo "Removed Subconscious provider from $models_json"
344
+ else
345
+ echo "No Subconscious provider found in $models_json"
346
+ fi
347
+ else
348
+ echo "No chatLanguageModels.json at $models_json"
349
+ fi
350
+ }
351
+
352
+ # ── Hooks installation ────────────────────────────────────────────────────────
353
+
354
+ write_hooks_env() {
355
+ umask 077
356
+ cat >"$ENV_FILE" <<EOF
357
+ # Generated by subc — do not commit secrets.
358
+ export SUBCONSCIOUS_GATEWAY_URL='${GATEWAY_URL}'
359
+ export SUBCONSCIOUS_API_KEY='${API_KEY}'
360
+ EOF
361
+ chmod 600 "$ENV_FILE"
362
+ }
363
+
364
+ install_hook_script() {
365
+ mkdir -p "$HOOKS_DIR"
366
+ cp "$HOOK_SRC" "$HOOK_DST"
367
+ chmod +x "$HOOK_DST"
368
+ }
369
+
370
+ write_hooks_json() {
371
+ sed "s|HOOK_SH_PATH|${HOOK_DST}|g" "$HOOKS_TEMPLATE" >"$HOOKS_JSON"
372
+ }
373
+
374
+ uninstall_hooks() {
375
+ rm -f "$HOOK_DST" "$HOOKS_JSON" "$ENV_FILE"
376
+ rm -rf "${COPILOT_DIR}/subconscious-compact-pending"
377
+ }
378
+
379
+ hooks_status() {
380
+ echo "hooks dir: $HOOKS_DIR"
381
+ if [[ -x "$HOOK_DST" ]]; then
382
+ echo "hook script: $HOOK_DST (executable)"
383
+ else
384
+ echo "hook script: missing"
385
+ fi
386
+ if [[ -f "$HOOKS_JSON" ]]; then
387
+ echo "hooks json: $HOOKS_JSON (present)"
388
+ else
389
+ echo "hooks json: missing"
390
+ fi
391
+ if [[ -f "$ENV_FILE" ]]; then
392
+ echo "hooks env: $ENV_FILE (present)"
393
+ # shellcheck disable=SC1090
394
+ source "$ENV_FILE"
395
+ echo "gateway: ${SUBCONSCIOUS_GATEWAY_URL:-unset}"
396
+ if [[ -n "${SUBCONSCIOUS_API_KEY:-}" ]]; then
397
+ echo "hooks api key: set (${#SUBCONSCIOUS_API_KEY} chars)"
398
+ else
399
+ echo "hooks api key: unset"
400
+ fi
401
+ else
402
+ echo "hooks env: missing"
403
+ fi
404
+ }
405
+
406
+ status() {
407
+ local user_dir="$1"
408
+ echo "scope: user"
409
+ echo "vscode user dir: $user_dir"
410
+ local models_json="${user_dir}/chatLanguageModels.json"
411
+ echo "config: $models_json"
412
+ if [[ -f "$models_json" ]] && grep -qi "$MARKER" "$models_json" 2>/dev/null; then
413
+ echo "model provider: installed"
414
+ echo "models: $(jq -r --arg name "$PROVIDER_NAME" '.[] | select(.name == $name) | [.models[].id] | join(", ")' "$models_json" 2>/dev/null || echo 'unknown')"
415
+ echo "url: $(jq -r --arg name "$PROVIDER_NAME" '.[] | select(.name == $name) | .models[0].url' "$models_json" 2>/dev/null || echo 'unknown')"
416
+ else
417
+ echo "model provider: not installed"
418
+ fi
419
+ echo ""
420
+ hooks_status
421
+ }
422
+
423
+ case "$COMMAND" in
424
+ install)
425
+ require_cmds
426
+ if [[ -z "$GATEWAY_URL" ]]; then
427
+ echo "--gateway-url is required for install (or set GATEWAY_URL in .env)" >&2
428
+ exit 1
429
+ fi
430
+ if [[ -z "$API_KEY" ]]; then
431
+ echo "--api-key is required for hooks (or set API_KEY in .env)" >&2
432
+ exit 1
433
+ fi
434
+ USER_DIR="$(detect_vscode_dir)"
435
+ WRITTEN="$(write_config "$USER_DIR")"
436
+ echo "Installed Subconscious Copilot provider into $WRITTEN"
437
+ echo "Gateway base URL: ${GATEWAY_URL%/}/v1"
438
+ install_hook_script
439
+ write_hooks_env
440
+ write_hooks_json
441
+ echo "Installed Subconscious Copilot hooks into $COPILOT_DIR"
442
+ echo ""
443
+ echo "IMPORTANT: VS Code requires the model API key to be stored in its"
444
+ echo "secret store — a plaintext key in the JSON is silently dropped."
445
+ echo "Enter the key once through the VS Code UI:"
446
+ echo ""
447
+ echo " 1. Restart VS Code (fully quit, not just reload)."
448
+ echo " 2. Open Chat → model picker (gear) → Manage Language Models."
449
+ echo " 3. Find 'Subconscious Gateway' → click the key icon to set the API key."
450
+ echo " 4. Paste your gateway API key (sk-gw-...)."
451
+ echo ""
452
+ echo "The hooks use a separate API key from ~/.copilot/subconscious-hooks.env."
453
+ echo "Both keys can be the same gateway API key."
454
+ echo ""
455
+ echo "After restart, choose a Subconscious model from the model picker."
456
+ ;;
457
+ uninstall)
458
+ require_cmds
459
+ USER_DIR="$(detect_vscode_dir)"
460
+ uninstall_config "$USER_DIR"
461
+ uninstall_hooks
462
+ echo "Removed Subconscious Copilot hooks from $COPILOT_DIR"
463
+ echo "Restart VS Code for the change to take effect."
464
+ ;;
465
+ status)
466
+ require_cmds
467
+ USER_DIR="$(detect_vscode_dir)"
468
+ status "$USER_DIR"
469
+ ;;
470
+ esac
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ # Fail-open Cursor hook: announce prompts and compactions to the gateway. Never
3
+ # blocks the agent.
4
+ # Stdin: Cursor hook JSON. Stdout: permissive JSON for Cursor.
5
+ #
6
+ # Docs: https://cursor.com/docs/hooks
7
+ #
8
+ # Two events, one call each:
9
+ # beforeSubmitPrompt -> conversation_ensure { conversation_id, prompt }
10
+ # preCompact -> conversation_compaction { conversation_id, phase: point }
11
+ #
12
+ # The gateway fingerprints the raw prompt itself and chains every later turn of
13
+ # the conversation onto the first one, so this hook needs no hashing and no local
14
+ # state. Cursor cannot inject headers into model HTTP, which is why this
15
+ # announcement exists at all.
16
+ #
17
+ # preCompact is observational and Cursor has no postCompact, so compaction is
18
+ # reported as a zero-width "point": the gateway places the context boundary on the
19
+ # first following turn whose retained context actually shrinks. Cursor compacts
20
+ # mid-turn and keeps working without a new prompt, so a window closed at the next
21
+ # beforeSubmitPrompt would wrongly swallow real turns.
22
+ #
23
+ # Deliberately NOT registered: afterAgentThought / afterAgentResponse /
24
+ # preToolUse / stop / subagentStart / subagentStop. Subagents are correlated
25
+ # gateway-side from the parent's tool-call prompt. Tool-execution hooks are also
26
+ # the ones Cursor is known to emit with an empty conversation_id after a mid-turn
27
+ # compaction; conversation-lifecycle hooks like the two above are unaffected.
28
+
29
+ set -u
30
+
31
+ CONFIG="${SUBCONSCIOUS_HOOKS_ENV:-${HOME}/.cursor/subconscious-hooks.env}"
32
+ if [[ -f "$CONFIG" ]]; then
33
+ # shellcheck disable=SC1090
34
+ source "$CONFIG"
35
+ fi
36
+
37
+ GATEWAY_URL="${SUBCONSCIOUS_GATEWAY_URL:-}"
38
+ API_KEY="${SUBCONSCIOUS_API_KEY:-}"
39
+
40
+ # Cursor reads `user_message` on preCompact and a permission object on
41
+ # beforeSubmitPrompt, so the reply shape depends on the event.
42
+ HOOK_EVENT=""
43
+
44
+ fail_open() {
45
+ if [[ "$HOOK_EVENT" == "preCompact" ]]; then
46
+ printf '%s\n' '{}'
47
+ else
48
+ printf '%s\n' '{"continue":true,"permission":"allow"}'
49
+ fi
50
+ exit 0
51
+ }
52
+
53
+ if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
54
+ echo "subconscious hook: missing SUBCONSCIOUS_GATEWAY_URL or SUBCONSCIOUS_API_KEY" >&2
55
+ fail_open
56
+ fi
57
+
58
+ for tool in jq curl; do
59
+ if ! command -v "$tool" >/dev/null 2>&1; then
60
+ echo "subconscious hook: ${tool} is required" >&2
61
+ fail_open
62
+ fi
63
+ done
64
+
65
+ INPUT="$(cat || true)"
66
+ if [[ -z "$INPUT" ]]; then
67
+ fail_open
68
+ fi
69
+
70
+ HOOK_EVENT="$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null || true)"
71
+ case "$HOOK_EVENT" in
72
+ beforeSubmitPrompt | preCompact) ;;
73
+ *) fail_open ;;
74
+ esac
75
+
76
+ CONVERSATION_ID="$(printf '%s' "$INPUT" | jq -r '.conversation_id // .session_id // empty' 2>/dev/null || true)"
77
+ if [[ -z "$CONVERSATION_ID" ]]; then
78
+ fail_open
79
+ fi
80
+
81
+ if [[ "$HOOK_EVENT" == "preCompact" ]]; then
82
+ # generation_id plus is_first_compaction keeps successive compactions distinct
83
+ # while making a redelivery of the same one idempotent server-side.
84
+ PAYLOAD="$(printf '%s' "$INPUT" | jq -c \
85
+ --arg conversation_id "$CONVERSATION_ID" \
86
+ --arg hook_event_name "$HOOK_EVENT" \
87
+ '{
88
+ event: "conversation_compaction",
89
+ conversation_id: $conversation_id,
90
+ phase: "point",
91
+ hook_event_name: $hook_event_name,
92
+ dedupe_key: ($conversation_id + ":" + ((.generation_id // "gen") | tostring)
93
+ + ":" + ((.is_first_compaction // false) | tostring)),
94
+ metadata: {
95
+ trigger: .trigger,
96
+ context_tokens: .context_tokens,
97
+ context_window_size: .context_window_size,
98
+ context_usage_percent: .context_usage_percent,
99
+ message_count: .message_count,
100
+ messages_to_compact: .messages_to_compact,
101
+ is_first_compaction: .is_first_compaction
102
+ } | with_entries(select(.value != null))
103
+ }' 2>/dev/null || true)"
104
+ else
105
+ PROMPT="$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null || true)"
106
+ WORKSPACE="$(printf '%s' "$INPUT" | jq -r '(.workspace_roots // [])[0] // empty' 2>/dev/null || true)"
107
+ if [[ -n "$WORKSPACE" ]]; then
108
+ WORKSPACE="$(basename "$WORKSPACE")"
109
+ fi
110
+
111
+ # Nothing to anchor on without a prompt.
112
+ if [[ -z "$PROMPT" ]]; then
113
+ fail_open
114
+ fi
115
+
116
+ PAYLOAD="$(jq -n \
117
+ --arg event "conversation_ensure" \
118
+ --arg conversation_id "$CONVERSATION_ID" \
119
+ --arg prompt "$PROMPT" \
120
+ --arg workspace "$WORKSPACE" \
121
+ --arg hook_event_name "$HOOK_EVENT" \
122
+ '{
123
+ event: $event,
124
+ conversation_id: $conversation_id,
125
+ prompt: $prompt,
126
+ workspace: (if $workspace == "" then null else $workspace end),
127
+ hook_event_name: $hook_event_name
128
+ } | with_entries(select(.value != null))'
129
+ )"
130
+ fi
131
+
132
+ if [[ -z "$PAYLOAD" ]]; then
133
+ fail_open
134
+ fi
135
+
136
+ # Response body is intentionally ignored: the gateway resolves the Cursor
137
+ # conversation id to its own UUID on every call, so there is no mapping to cache.
138
+ curl -sS -m 2 \
139
+ -H "Authorization: Bearer ${API_KEY}" \
140
+ -H "Content-Type: application/json" \
141
+ -H "x-subconscious-client: cursor" \
142
+ -d "$PAYLOAD" \
143
+ "${GATEWAY_URL%/}/v1/agent-hooks" >/dev/null 2>&1 || true
144
+
145
+ fail_open
@@ -0,0 +1,17 @@
1
+ {
2
+ "version": 1,
3
+ "hooks": {
4
+ "beforeSubmitPrompt": [
5
+ {
6
+ "command": "HOOK_SH_PATH",
7
+ "timeout": 2
8
+ }
9
+ ],
10
+ "preCompact": [
11
+ {
12
+ "command": "HOOK_SH_PATH",
13
+ "timeout": 2
14
+ }
15
+ ]
16
+ }
17
+ }