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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report OpenCode compactions to the Subconscious API Gateway.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: OpenCode summarizes by issuing an ordinary completion through the
|
|
5
|
+
* configured provider, which is the gateway, carrying the same session headers as every
|
|
6
|
+
* other turn. Without a signal the gateway would count that summarization as the largest
|
|
7
|
+
* main-thread turn of the conversation, inflating peak context and the traditional
|
|
8
|
+
* comparison at exactly the turn where accuracy matters most.
|
|
9
|
+
*
|
|
10
|
+
* Two callbacks bracket it:
|
|
11
|
+
* experimental.session.compacting -> phase "start" (awaited, so it is ordered before
|
|
12
|
+
* the summarization request reaches the gateway)
|
|
13
|
+
* session.compacted event -> phase "end"
|
|
14
|
+
*
|
|
15
|
+
* Requests between them are the compaction. `sessionID` is the same value OpenCode sends
|
|
16
|
+
* as x-session-affinity / x-session-id, which is already the gateway's grouping key, so
|
|
17
|
+
* no fingerprinting is involved.
|
|
18
|
+
*
|
|
19
|
+
* Fail open: a gateway that is down or slow must never disturb a coding session.
|
|
20
|
+
*
|
|
21
|
+
* Install: copy to ~/.config/opencode/plugins/ (global) or .opencode/plugins/ (project).
|
|
22
|
+
* `opencode/install.sh` does this for you.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
26
|
+
|
|
27
|
+
const TIMEOUT_MS = 2000
|
|
28
|
+
|
|
29
|
+
function gatewayUrl(): string | undefined {
|
|
30
|
+
const raw = process.env.SUBCONSCIOUS_GATEWAY_URL ?? process.env.GATEWAY_URL
|
|
31
|
+
return raw ? raw.replace(/\/+$/, "") : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function apiKey(): string | undefined {
|
|
35
|
+
return process.env.SUBCONSCIOUS_API_KEY ?? process.env.API_KEY
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function report(
|
|
39
|
+
sessionID: string,
|
|
40
|
+
phase: "start" | "end",
|
|
41
|
+
hookEventName: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
const url = gatewayUrl()
|
|
44
|
+
const key = apiKey()
|
|
45
|
+
if (!url || !key || !sessionID) return
|
|
46
|
+
|
|
47
|
+
// Idempotency key for this one event. It must be unique per compaction but stable for
|
|
48
|
+
// a redelivery of this same POST, so it is derived from the wall clock rather than any
|
|
49
|
+
// in-process counter: a counter resets when OpenCode restarts, and a resumed session
|
|
50
|
+
// compacting again would then reuse a key the gateway has already seen and silently
|
|
51
|
+
// drop the signal. Computed once here, before the request, so a transport-level retry
|
|
52
|
+
// carries the identical body.
|
|
53
|
+
const dedupeKey = `${sessionID}:${phase}:${Date.now()}`
|
|
54
|
+
|
|
55
|
+
const controller = new AbortController()
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
57
|
+
try {
|
|
58
|
+
await fetch(`${url}/v1/agent-hooks`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
authorization: `Bearer ${key}`,
|
|
63
|
+
"x-subconscious-client": "opencode",
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
event: "conversation_compaction",
|
|
67
|
+
conversation_id: sessionID,
|
|
68
|
+
phase,
|
|
69
|
+
hook_event_name: hookEventName,
|
|
70
|
+
dedupe_key: dedupeKey,
|
|
71
|
+
}),
|
|
72
|
+
signal: controller.signal,
|
|
73
|
+
})
|
|
74
|
+
} catch {
|
|
75
|
+
// Fail open on timeout, offline gateway, or auth failure.
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timer)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const SubconsciousCompaction: Plugin = async () => {
|
|
82
|
+
return {
|
|
83
|
+
"experimental.session.compacting": async (input) => {
|
|
84
|
+
// Deliberately does not touch `output`: mutating `context` would change the
|
|
85
|
+
// customer's compaction prompt, and setting `prompt` would replace it entirely.
|
|
86
|
+
await report(input.sessionID, "start", "session.compacting")
|
|
87
|
+
},
|
|
88
|
+
event: async ({ event }) => {
|
|
89
|
+
if (event.type === "session.compacted") {
|
|
90
|
+
const sessionID = (event as { properties?: { sessionID?: string } }).properties
|
|
91
|
+
?.sessionID
|
|
92
|
+
if (sessionID) {
|
|
93
|
+
// No shared id with the matching `start` is needed: the gateway pairs a start
|
|
94
|
+
// with the next end by server-stamped time, not by key.
|
|
95
|
+
await report(sessionID, "end", "session.compacted")
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ── Subconscious API Gateway — Pi setup ───────────────────────────────────────
|
|
3
|
+
# Point the Pi CLI at your gateway. Writes a model config with session headers
|
|
4
|
+
# so the gateway can correlate Pi 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 model config
|
|
10
|
+
#
|
|
11
|
+
# Writes ~/.pi/agent/models.json (user config). Restart Pi after install.
|
|
12
|
+
#
|
|
13
|
+
# ── What this does under the hood ────────────────────────────────────────────
|
|
14
|
+
# Equivalent manual setup (writes ~/.pi/agent/models.json):
|
|
15
|
+
#
|
|
16
|
+
# {
|
|
17
|
+
# "providers": {
|
|
18
|
+
# "subconscious": {
|
|
19
|
+
# "baseUrl": "https://your-gateway.example/v1",
|
|
20
|
+
# "api": "openai-completions",
|
|
21
|
+
# "apiKey": "sk-gw-...",
|
|
22
|
+
# "headers": { "x-subconscious-client": "pi" },
|
|
23
|
+
# "models": [{
|
|
24
|
+
# "id": "subconscious/glm-5.2",
|
|
25
|
+
# "contextWindow": 5000000,
|
|
26
|
+
# "maxTokens": 65536,
|
|
27
|
+
# "compat": {
|
|
28
|
+
# "sendSessionAffinityHeaders": true,
|
|
29
|
+
# "sessionAffinityFormat": "openai-nosession"
|
|
30
|
+
# }
|
|
31
|
+
# }]
|
|
32
|
+
# }
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
#
|
|
36
|
+
# The compat flags make Pi send x-session-affinity headers (openai-nosession)
|
|
37
|
+
# so the gateway groups requests into Conversations.
|
|
38
|
+
#
|
|
39
|
+
# Also installs ~/.pi/agent/extensions/subconscious-compaction.ts so Pi reports
|
|
40
|
+
# session_before_compact / session_compact to /v1/agent-hooks. Pi summarizes
|
|
41
|
+
# through the configured provider; without the extension that turn looks like
|
|
42
|
+
# a normal main-thread peak.
|
|
43
|
+
# Docs: https://pi.dev/docs/latest/compaction
|
|
44
|
+
# https://pi.dev/docs/latest/extensions
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
|
|
49
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
50
|
+
EXTENSION_SRC="${SCRIPT_DIR}/subconscious-compaction.ts"
|
|
51
|
+
|
|
52
|
+
# Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
|
|
53
|
+
SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
|
|
54
|
+
[[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
|
|
55
|
+
if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
|
|
56
|
+
|
|
57
|
+
COMMAND="install"
|
|
58
|
+
GATEWAY_URL="${GATEWAY_URL:-}"
|
|
59
|
+
API_KEY="${PI_API_KEY:-${API_KEY:-}}"
|
|
60
|
+
MODEL="${MODEL:-subconscious/glm-5.2}"
|
|
61
|
+
CONTEXT_WINDOW="${PI_CONTEXT_WINDOW:-5000000}"
|
|
62
|
+
MAX_TOKENS="${PI_MAX_TOKENS:-65536}"
|
|
63
|
+
|
|
64
|
+
usage() {
|
|
65
|
+
cat <<'EOF'
|
|
66
|
+
Usage:
|
|
67
|
+
subc pi install [--gateway-url URL] [--api-key KEY] [--model MODEL]
|
|
68
|
+
[--context-window N] [--max-tokens N]
|
|
69
|
+
subc pi uninstall
|
|
70
|
+
subc pi status
|
|
71
|
+
|
|
72
|
+
Merges a Subconscious provider into ~/.pi/agent/models.json without replacing
|
|
73
|
+
other providers. Launch Pi with subc pi after install.
|
|
74
|
+
|
|
75
|
+
Requires: jq. Restart Pi after install (or /reload for the extension).
|
|
76
|
+
EOF
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
while [[ $# -gt 0 ]]; do
|
|
80
|
+
case "$1" in
|
|
81
|
+
install|uninstall|status)
|
|
82
|
+
COMMAND="$1"
|
|
83
|
+
shift
|
|
84
|
+
;;
|
|
85
|
+
--gateway-url)
|
|
86
|
+
GATEWAY_URL="${2:-}"
|
|
87
|
+
shift 2
|
|
88
|
+
;;
|
|
89
|
+
--api-key)
|
|
90
|
+
API_KEY="${2:-}"
|
|
91
|
+
shift 2
|
|
92
|
+
;;
|
|
93
|
+
--model)
|
|
94
|
+
MODEL="${2:-}"
|
|
95
|
+
shift 2
|
|
96
|
+
;;
|
|
97
|
+
--context-window)
|
|
98
|
+
CONTEXT_WINDOW="${2:-}"
|
|
99
|
+
shift 2
|
|
100
|
+
;;
|
|
101
|
+
--max-tokens)
|
|
102
|
+
MAX_TOKENS="${2:-}"
|
|
103
|
+
shift 2
|
|
104
|
+
;;
|
|
105
|
+
-h|--help|help)
|
|
106
|
+
usage
|
|
107
|
+
exit 0
|
|
108
|
+
;;
|
|
109
|
+
*)
|
|
110
|
+
echo "unknown argument: $1" >&2
|
|
111
|
+
usage >&2
|
|
112
|
+
exit 1
|
|
113
|
+
;;
|
|
114
|
+
esac
|
|
115
|
+
done
|
|
116
|
+
|
|
117
|
+
DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.2
|
|
118
|
+
subconscious/tim-qwen3.6-27b
|
|
119
|
+
subconscious/deepseek-v4-flash-marathon"
|
|
120
|
+
SUPPORTED_MODELS=()
|
|
121
|
+
|
|
122
|
+
add_supported_model() {
|
|
123
|
+
local model_id="$1" existing
|
|
124
|
+
[[ -n "$model_id" ]] || return 0
|
|
125
|
+
if [[ ! "$model_id" =~ ^[-A-Za-z0-9._:/+]+$ ]]; then
|
|
126
|
+
echo "error: invalid model id: $model_id" >&2
|
|
127
|
+
exit 1
|
|
128
|
+
fi
|
|
129
|
+
if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
|
|
130
|
+
for existing in "${SUPPORTED_MODELS[@]}"; do
|
|
131
|
+
[[ "$existing" == "$model_id" ]] && return 0
|
|
132
|
+
done
|
|
133
|
+
fi
|
|
134
|
+
SUPPORTED_MODELS+=("$model_id")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
add_supported_model "$MODEL"
|
|
138
|
+
while IFS= read -r model_id; do
|
|
139
|
+
add_supported_model "$model_id"
|
|
140
|
+
done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
|
|
141
|
+
|
|
142
|
+
MODEL_ENTRIES_JSON=""
|
|
143
|
+
for model_id in "${SUPPORTED_MODELS[@]}"; do
|
|
144
|
+
model_json="{\"id\":\"${model_id}\",\"contextWindow\":${CONTEXT_WINDOW},\"maxTokens\":${MAX_TOKENS},\"compat\":{\"sendSessionAffinityHeaders\":true,\"sessionAffinityFormat\":\"openai-nosession\"}}"
|
|
145
|
+
if [[ -n "$MODEL_ENTRIES_JSON" ]]; then
|
|
146
|
+
MODEL_ENTRIES_JSON="${MODEL_ENTRIES_JSON},${model_json}"
|
|
147
|
+
else
|
|
148
|
+
MODEL_ENTRIES_JSON="$model_json"
|
|
149
|
+
fi
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
PI_DIR="${HOME}/.pi/agent"
|
|
153
|
+
MODELS_JSON="${PI_DIR}/models.json"
|
|
154
|
+
EXTENSIONS_DIR="${PI_DIR}/extensions"
|
|
155
|
+
EXTENSION_DST="${EXTENSIONS_DIR}/subconscious-compaction.ts"
|
|
156
|
+
ENV_FILE="${PI_DIR}/subconscious.env"
|
|
157
|
+
MARKER='x-subconscious-client'
|
|
158
|
+
|
|
159
|
+
require_cmds() {
|
|
160
|
+
local missing=0
|
|
161
|
+
for c in jq; do
|
|
162
|
+
if ! command -v "$c" >/dev/null 2>&1; then
|
|
163
|
+
echo "missing required command: $c" >&2
|
|
164
|
+
missing=1
|
|
165
|
+
fi
|
|
166
|
+
done
|
|
167
|
+
if [[ "$missing" -ne 0 ]]; then
|
|
168
|
+
exit 1
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
write_config() {
|
|
173
|
+
mkdir -p "$PI_DIR"
|
|
174
|
+
local base_url="${GATEWAY_URL%/}/v1"
|
|
175
|
+
local provider
|
|
176
|
+
provider=$(cat <<EOF
|
|
177
|
+
{
|
|
178
|
+
"baseUrl": "${base_url}",
|
|
179
|
+
"api": "openai-completions",
|
|
180
|
+
"apiKey": "${API_KEY}",
|
|
181
|
+
"headers": {
|
|
182
|
+
"x-subconscious-client": "pi"
|
|
183
|
+
},
|
|
184
|
+
"models": [${MODEL_ENTRIES_JSON}]
|
|
185
|
+
}
|
|
186
|
+
EOF
|
|
187
|
+
)
|
|
188
|
+
if [[ -f "$MODELS_JSON" ]]; then
|
|
189
|
+
local tmp
|
|
190
|
+
tmp="$(mktemp)"
|
|
191
|
+
jq --argjson provider "$provider" '
|
|
192
|
+
.providers = (.providers // {})
|
|
193
|
+
| .providers.subconscious = $provider
|
|
194
|
+
' "$MODELS_JSON" >"$tmp"
|
|
195
|
+
mv "$tmp" "$MODELS_JSON"
|
|
196
|
+
else
|
|
197
|
+
jq -n --argjson provider "$provider" '{providers: {subconscious: $provider}}' >"$MODELS_JSON"
|
|
198
|
+
fi
|
|
199
|
+
chmod 600 "$MODELS_JSON"
|
|
200
|
+
|
|
201
|
+
umask 077
|
|
202
|
+
cat >"$ENV_FILE" <<EOF
|
|
203
|
+
# Generated by subc — do not commit secrets.
|
|
204
|
+
# Loaded by subconscious-compaction.ts if process env is unset.
|
|
205
|
+
export SUBCONSCIOUS_GATEWAY_URL='${GATEWAY_URL%/}'
|
|
206
|
+
export SUBCONSCIOUS_API_KEY='${API_KEY}'
|
|
207
|
+
EOF
|
|
208
|
+
chmod 600 "$ENV_FILE"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
write_extension() {
|
|
212
|
+
if [[ ! -f "$EXTENSION_SRC" ]]; then
|
|
213
|
+
echo "warning: ${EXTENSION_SRC} missing; skipping compaction extension" >&2
|
|
214
|
+
return
|
|
215
|
+
fi
|
|
216
|
+
mkdir -p "$EXTENSIONS_DIR"
|
|
217
|
+
cp "$EXTENSION_SRC" "$EXTENSION_DST"
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
uninstall_config() {
|
|
221
|
+
if [[ -f "$MODELS_JSON" ]]; then
|
|
222
|
+
local tmp
|
|
223
|
+
tmp="$(mktemp)"
|
|
224
|
+
jq 'del(.providers.subconscious)' "$MODELS_JSON" >"$tmp"
|
|
225
|
+
mv "$tmp" "$MODELS_JSON"
|
|
226
|
+
echo "Removed Subconscious provider from $MODELS_JSON"
|
|
227
|
+
else
|
|
228
|
+
echo "No Pi models.json at $MODELS_JSON"
|
|
229
|
+
fi
|
|
230
|
+
rm -f "$EXTENSION_DST" "$ENV_FILE"
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
status() {
|
|
234
|
+
echo "scope: user"
|
|
235
|
+
echo "pi dir: $PI_DIR"
|
|
236
|
+
echo "config: $MODELS_JSON"
|
|
237
|
+
if [[ -f "$MODELS_JSON" ]] && grep -q "$MARKER" "$MODELS_JSON" 2>/dev/null; then
|
|
238
|
+
echo "status: installed"
|
|
239
|
+
echo "models: $(jq -r '[.providers.subconscious.models[].id] | join(", ")' "$MODELS_JSON" 2>/dev/null || echo 'unknown')"
|
|
240
|
+
echo "contextWindow: $(jq -r '.providers.subconscious.models[0].contextWindow // "unset"' "$MODELS_JSON" 2>/dev/null || echo 'unknown')"
|
|
241
|
+
else
|
|
242
|
+
echo "status: not installed"
|
|
243
|
+
fi
|
|
244
|
+
if [[ -f "$EXTENSION_DST" ]]; then
|
|
245
|
+
echo "compaction extension: $EXTENSION_DST (installed)"
|
|
246
|
+
else
|
|
247
|
+
echo "compaction extension: not installed"
|
|
248
|
+
fi
|
|
249
|
+
if [[ -f "$ENV_FILE" ]]; then
|
|
250
|
+
echo "env: $ENV_FILE (present)"
|
|
251
|
+
else
|
|
252
|
+
echo "env: missing"
|
|
253
|
+
fi
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
case "$COMMAND" in
|
|
257
|
+
install)
|
|
258
|
+
require_cmds
|
|
259
|
+
if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
|
|
260
|
+
echo "--gateway-url and --api-key are required for install" >&2
|
|
261
|
+
exit 1
|
|
262
|
+
fi
|
|
263
|
+
write_config
|
|
264
|
+
write_extension
|
|
265
|
+
echo "Merged Subconscious Pi provider into $MODELS_JSON"
|
|
266
|
+
if [[ -f "$EXTENSION_DST" ]]; then
|
|
267
|
+
echo "Installed compaction reporting extension at $EXTENSION_DST"
|
|
268
|
+
fi
|
|
269
|
+
echo "Compaction extension reads ${ENV_FILE} automatically (sourcing optional)."
|
|
270
|
+
echo "Restart any running Pi sessions (or /reload for the extension)."
|
|
271
|
+
;;
|
|
272
|
+
uninstall)
|
|
273
|
+
uninstall_config
|
|
274
|
+
;;
|
|
275
|
+
status)
|
|
276
|
+
status
|
|
277
|
+
;;
|
|
278
|
+
esac
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Launch Pi with the Subconscious provider configured by `subc pi install`.
|
|
3
|
+
# This script is deliberately read-only: it never installs Pi, writes config,
|
|
4
|
+
# or updates the persistent integration.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
MODEL="${MODEL:-subconscious/glm-5.2}"
|
|
9
|
+
PI_DIR="${PI_CODING_AGENT_DIR:-${HOME}/.pi/agent}"
|
|
10
|
+
MODELS_JSON="${PI_DIR}/models.json"
|
|
11
|
+
|
|
12
|
+
if [[ ! -f "$MODELS_JSON" ]] || ! grep -q 'x-subconscious-client' "$MODELS_JSON" 2>/dev/null; then
|
|
13
|
+
echo "Pi is not configured for Subconscious. Run 'subc pi install' first." >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if command -v jq >/dev/null 2>&1 && ! jq -e \
|
|
18
|
+
--arg model "$MODEL" \
|
|
19
|
+
'.providers.subconscious.models[]? | select(.id == $model)' \
|
|
20
|
+
"$MODELS_JSON" >/dev/null 2>&1; then
|
|
21
|
+
echo "Model '$MODEL' is not present in the Pi catalog. Run 'subc pi install' to refresh it." >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
exec pi --provider subconscious --model "$MODEL" "$@"
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Pi compactions to the Subconscious API Gateway.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: Pi summarizes by calling the configured provider (the
|
|
5
|
+
* gateway) with a structured compaction prompt. Without a signal the gateway
|
|
6
|
+
* counts that summarization as the largest main-thread turn, inflating peak
|
|
7
|
+
* context and the traditional comparison at the boundary.
|
|
8
|
+
*
|
|
9
|
+
* Two extension events bracket it:
|
|
10
|
+
* session_before_compact -> phase "start"
|
|
11
|
+
* session_compact -> phase "end"
|
|
12
|
+
*
|
|
13
|
+
* Observational only: never return { cancel } or a custom compaction summary.
|
|
14
|
+
* conversation_id is ctx.sessionManager.getSessionId(), which should match the
|
|
15
|
+
* x-session-affinity value used for conversation grouping when
|
|
16
|
+
* sendSessionAffinityHeaders is enabled (openai-nosession).
|
|
17
|
+
*
|
|
18
|
+
* Capture note (2026-08): Pi compaction/branch-summary requests use a *fresh*
|
|
19
|
+
* routing session id on the wire, so the summarization HTTP call lands in a
|
|
20
|
+
* separate Conversations row. start/end still open the epoch on the parent
|
|
21
|
+
* session via getSessionId(); the boundary charge on the parent may be empty.
|
|
22
|
+
* Split-turn /compact (huge mid-turn) may only drop a few k tokens because
|
|
23
|
+
* keepRecentTokens still retains most of the active turn.
|
|
24
|
+
*
|
|
25
|
+
* Fail open: a gateway that is down or slow must never disturb a coding session.
|
|
26
|
+
*
|
|
27
|
+
* Install: copy to ~/.pi/agent/extensions/ (global). `pi/install.sh` does this.
|
|
28
|
+
* Credentials: process env, or ~/.pi/agent/subconscious.env (loaded here so
|
|
29
|
+
* launching Pi without `source` still works).
|
|
30
|
+
* Docs: https://pi.dev/docs/latest/compaction
|
|
31
|
+
* https://pi.dev/docs/latest/extensions
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync } from "node:fs"
|
|
35
|
+
import { homedir } from "node:os"
|
|
36
|
+
import { join } from "node:path"
|
|
37
|
+
|
|
38
|
+
const TIMEOUT_MS = 2000
|
|
39
|
+
|
|
40
|
+
type CompactPhase = "start" | "end"
|
|
41
|
+
|
|
42
|
+
let envFileLoaded = false
|
|
43
|
+
|
|
44
|
+
function loadSubconsciousEnvFile(): void {
|
|
45
|
+
if (envFileLoaded) return
|
|
46
|
+
envFileLoaded = true
|
|
47
|
+
if (process.env.SUBCONSCIOUS_GATEWAY_URL && process.env.SUBCONSCIOUS_API_KEY) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const path = join(homedir(), ".pi", "agent", "subconscious.env")
|
|
52
|
+
const text = readFileSync(path, "utf8")
|
|
53
|
+
for (const line of text.split("\n")) {
|
|
54
|
+
const trimmed = line.trim()
|
|
55
|
+
if (!trimmed || trimmed.startsWith("#")) continue
|
|
56
|
+
const m = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
|
|
57
|
+
if (!m) continue
|
|
58
|
+
const key = m[1]
|
|
59
|
+
let val = m[2]
|
|
60
|
+
if (
|
|
61
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
62
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
63
|
+
) {
|
|
64
|
+
val = val.slice(1, -1)
|
|
65
|
+
}
|
|
66
|
+
if (process.env[key] === undefined) {
|
|
67
|
+
process.env[key] = val
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Missing file or unreadable: fall through to process env / no-op report.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function gatewayUrl(): string | undefined {
|
|
76
|
+
loadSubconsciousEnvFile()
|
|
77
|
+
const raw = process.env.SUBCONSCIOUS_GATEWAY_URL ?? process.env.GATEWAY_URL
|
|
78
|
+
return raw ? raw.replace(/\/+$/, "") : undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function apiKey(): string | undefined {
|
|
82
|
+
loadSubconsciousEnvFile()
|
|
83
|
+
return process.env.SUBCONSCIOUS_API_KEY ?? process.env.API_KEY ?? process.env.PI_API_KEY
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function report(
|
|
87
|
+
sessionID: string,
|
|
88
|
+
phase: CompactPhase,
|
|
89
|
+
hookEventName: string,
|
|
90
|
+
metadata?: Record<string, unknown>,
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
const url = gatewayUrl()
|
|
93
|
+
const key = apiKey()
|
|
94
|
+
if (!url || !key || !sessionID) return
|
|
95
|
+
|
|
96
|
+
// Unique per event, stable for a transport-level retry of this same POST.
|
|
97
|
+
const dedupeKey = `${sessionID}:${phase}:${Date.now()}`
|
|
98
|
+
|
|
99
|
+
const controller = new AbortController()
|
|
100
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS)
|
|
101
|
+
try {
|
|
102
|
+
await fetch(`${url}/v1/agent-hooks`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"content-type": "application/json",
|
|
106
|
+
authorization: `Bearer ${key}`,
|
|
107
|
+
"x-subconscious-client": "pi",
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
event: "conversation_compaction",
|
|
111
|
+
conversation_id: sessionID,
|
|
112
|
+
phase,
|
|
113
|
+
hook_event_name: hookEventName,
|
|
114
|
+
dedupe_key: dedupeKey,
|
|
115
|
+
metadata: metadata ?? undefined,
|
|
116
|
+
}),
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
})
|
|
119
|
+
} catch {
|
|
120
|
+
// Fail open on timeout, offline gateway, or auth failure.
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pi auto-discovers default-exported extension factories from
|
|
127
|
+
// ~/.pi/agent/extensions/*.ts. See https://pi.dev/docs/latest/extensions
|
|
128
|
+
export default function (pi: {
|
|
129
|
+
on: (event: string, handler: (...args: any[]) => any) => void
|
|
130
|
+
}) {
|
|
131
|
+
pi.on("session_before_compact", async (event: any, ctx: any) => {
|
|
132
|
+
const sessionID = ctx?.sessionManager?.getSessionId?.()
|
|
133
|
+
if (sessionID) {
|
|
134
|
+
await report(sessionID, "start", "session_before_compact", {
|
|
135
|
+
reason: event?.reason ?? null,
|
|
136
|
+
tokens_before: event?.preparation?.tokensBefore ?? null,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
// Deliberately return nothing: do not cancel or replace summarization.
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
pi.on("session_compact", async (event: any, ctx: any) => {
|
|
143
|
+
const sessionID = ctx?.sessionManager?.getSessionId?.()
|
|
144
|
+
if (sessionID) {
|
|
145
|
+
await report(sessionID, "end", "session_compact", {
|
|
146
|
+
reason: event?.reason ?? null,
|
|
147
|
+
from_extension: event?.fromExtension ?? null,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subconscious-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for Subconscious —
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "CLI for Subconscious — run Claude Code, Codex, OpenCode, Cursor, Copilot, and Pi",
|
|
5
5
|
"bin": {
|
|
6
|
-
"
|
|
7
|
-
|
|
6
|
+
"subc": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test"
|
|
8
10
|
},
|
|
9
11
|
"files": [
|
|
10
12
|
"bin"
|
|
@@ -24,11 +26,14 @@
|
|
|
24
26
|
"cli",
|
|
25
27
|
"api-key",
|
|
26
28
|
"authentication",
|
|
29
|
+
"profiles",
|
|
27
30
|
"coding-agent",
|
|
28
31
|
"claude-code",
|
|
29
32
|
"opencode",
|
|
30
|
-
"aider",
|
|
31
33
|
"codex",
|
|
34
|
+
"cursor",
|
|
35
|
+
"copilot",
|
|
36
|
+
"pi",
|
|
32
37
|
"launcher"
|
|
33
38
|
],
|
|
34
39
|
"author": "Subconscious Systems",
|