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.
- package/README.md +200 -76
- package/bin/agents.js +430 -23
- package/bin/auth.js +116 -24
- package/bin/branding.js +39 -0
- package/bin/cli.js +238 -18
- package/bin/colors.js +22 -10
- package/bin/profiles.js +782 -0
- package/bin/registry.generated.json +182 -44
- package/bin/runbook/README.md +22 -0
- package/bin/runbook/claude-code/install.sh +290 -0
- package/bin/runbook/claude-code/run.sh +164 -0
- package/bin/runbook/codex/hook.sh +112 -0
- package/bin/runbook/codex/hooks-lib.sh +139 -0
- package/bin/runbook/codex/hooks.json +29 -0
- package/bin/runbook/codex/install.sh +82 -0
- package/bin/runbook/codex/run.sh +248 -0
- package/bin/runbook/copilot/hook.sh +173 -0
- package/bin/runbook/copilot/hooks.json +19 -0
- package/bin/runbook/copilot/install.sh +470 -0
- package/bin/runbook/cursor/hook.sh +145 -0
- package/bin/runbook/cursor/hooks.json +17 -0
- package/bin/runbook/cursor/install.sh +284 -0
- package/bin/runbook/opencode/install.sh +298 -0
- package/bin/runbook/opencode/run.sh +106 -0
- package/bin/runbook/opencode/subconscious-compaction.ts +100 -0
- package/bin/runbook/pi/install.sh +278 -0
- package/bin/runbook/pi/run.sh +25 -0
- package/bin/runbook/pi/subconscious-compaction.ts +151 -0
- package/package.json +10 -5
|
@@ -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
|