subconscious-cli 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +135 -2
  2. package/bin/agent-data.js +242 -0
  3. package/bin/agent-help.js +106 -0
  4. package/bin/agent-install.js +354 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +284 -840
  8. package/bin/auth.js +89 -33
  9. package/bin/branding.js +3 -1
  10. package/bin/claude-picker.js +143 -0
  11. package/bin/cli.js +87 -20
  12. package/bin/clipboard-image.js +24 -5
  13. package/bin/colors.js +1 -1
  14. package/bin/feedback.js +71 -21
  15. package/bin/harness-manifest.js +286 -0
  16. package/bin/headless-args.js +35 -0
  17. package/bin/launch-values.js +25 -0
  18. package/bin/model-capabilities.js +12 -8
  19. package/bin/models.js +35 -19
  20. package/bin/native/subc-tui-darwin-amd64 +0 -0
  21. package/bin/native/subc-tui-darwin-arm64 +0 -0
  22. package/bin/native/subc-tui-linux-amd64 +0 -0
  23. package/bin/native/subc-tui-linux-arm64 +0 -0
  24. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  25. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  26. package/bin/opencode-provider.js +48 -5
  27. package/bin/profile-settings.js +129 -0
  28. package/bin/profiles.js +136 -449
  29. package/bin/runbook/README.md +66 -23
  30. package/bin/runbook/claude-code/agent.json +300 -0
  31. package/bin/runbook/claude-code/install.sh +9 -252
  32. package/bin/runbook/codex/agent.json +461 -0
  33. package/bin/runbook/codex/hooks-lib.sh +17 -4
  34. package/bin/runbook/codex/install.sh +0 -4
  35. package/bin/runbook/codex/run.sh +16 -235
  36. package/bin/runbook/copilot/agent.json +185 -0
  37. package/bin/runbook/copilot/install.sh +45 -63
  38. package/bin/runbook/cursor/agent.json +137 -0
  39. package/bin/runbook/cursor/install.sh +8 -35
  40. package/bin/runbook/deepseek-harness/agent.json +163 -0
  41. package/bin/runbook/deepseek-harness/run.sh +20 -59
  42. package/bin/runbook/image-window/index.ts +14 -0
  43. package/bin/runbook/image-window/window.d.ts +19 -0
  44. package/bin/runbook/image-window/window.js +140 -0
  45. package/bin/runbook/lib.sh +41 -0
  46. package/bin/runbook/opencode/agent.json +158 -0
  47. package/bin/runbook/opencode/install.sh +9 -183
  48. package/bin/runbook/opencode/opencode-plugin.d.ts +16 -0
  49. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  50. package/bin/runbook/pi/agent.json +216 -0
  51. package/bin/runbook/pi/install.sh +18 -44
  52. package/bin/runbook/pi/run.sh +6 -18
  53. package/bin/runbook/pi/subconscious-compaction.ts +71 -49
  54. package/bin/runbook/shared.json +58 -0
  55. package/bin/runbook/subconscious-code/agent.json +156 -0
  56. package/bin/sessions.js +173 -58
  57. package/bin/tui.js +104 -32
  58. package/bin/update-check.js +54 -21
  59. package/bin/upgrade.js +67 -43
  60. package/bin/usage.js +47 -24
  61. package/bin/windows/agents.js +141 -37
  62. package/bin/windows/common.js +36 -25
  63. package/bin/windows/install.js +116 -34
  64. package/bin/windows/launch.js +260 -77
  65. package/bin/windows/pi-compaction.ts +68 -13
  66. package/bin/windows/process.js +210 -57
  67. package/bin/windows/setup.js +251 -56
  68. package/package.json +9 -4
  69. package/bin/registry.generated.json +0 -406
  70. package/bin/runbook/claude-code/run.sh +0 -254
  71. package/bin/runbook/model-capabilities.generated.sh +0 -9
  72. package/bin/runbook/opencode/provider.sh +0 -93
  73. package/bin/runbook/opencode/run.sh +0 -99
  74. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  75. package/bin/runbook/subconscious-code/run.sh +0 -21
  76. package/bin/windows/claude.js +0 -71
@@ -0,0 +1,137 @@
1
+ {
2
+ "id": "cursor",
3
+ "command": "cursor",
4
+ "aliases": [],
5
+ "name": "Cursor",
6
+ "description": "Install Cursor conversation and compaction hooks, then finish model setup in Cursor Settings.",
7
+ "protocol": "openai",
8
+ "homepage": "https://cursor.com",
9
+ "install": {
10
+ "commands": null,
11
+ "method": "user",
12
+ "package": null,
13
+ "version": null,
14
+ "minimum_version": null,
15
+ "verified": true,
16
+ "note": "IDE installed by the user. The model endpoint is set by hand in Cursor Settings (OpenAI API Key Override)."
17
+ },
18
+ "runbook": {
19
+ "mode": "setup",
20
+ "setup_script": "install.sh",
21
+ "setup_actions": ["install", "status", "uninstall"]
22
+ },
23
+ "help": {
24
+ "usage": "subc [-p NAME] cursor [help|install|status|uninstall]",
25
+ "behavior": "Manages Cursor correlation hooks; model endpoint setup is completed in Cursor Settings.",
26
+ "options": [
27
+ ["help", "Show this help"],
28
+ ["install", "Install or update the Cursor hooks (default action)"],
29
+ ["status", "Inspect the installed hook configuration"],
30
+ ["uninstall", "Remove only the Subconscious Cursor hooks"]
31
+ ]
32
+ },
33
+ "prerequisites": [
34
+ { "command": "jq", "needed_for": "install", "required": true },
35
+ { "command": "curl", "needed_for": "install", "required": true }
36
+ ],
37
+ "launch": null,
38
+ "inputs": [
39
+ {
40
+ "name": "CURSOR_API_KEY",
41
+ "label": "Cursor hook API key",
42
+ "description": "Cursor hook credential override; blank uses API_KEY.",
43
+ "type": "secret"
44
+ }
45
+ ],
46
+ "env": [],
47
+ "config": [
48
+ {
49
+ "kind": "file",
50
+ "path": "~/.cursor/hooks.json",
51
+ "lifetime": "persistent",
52
+ "description": "beforeSubmitPrompt and preCompact entries are merged in; other entries are kept."
53
+ },
54
+ {
55
+ "kind": "file",
56
+ "path": "~/.cursor/hooks/subconscious-hook.sh",
57
+ "lifetime": "persistent",
58
+ "description": "Hook script."
59
+ },
60
+ {
61
+ "kind": "file",
62
+ "path": "~/.cursor/subconscious-hooks.env",
63
+ "lifetime": "persistent",
64
+ "description": "SUBCONSCIOUS_GATEWAY_URL and SUBCONSCIOUS_API_KEY for the hook (mode 600)."
65
+ }
66
+ ],
67
+ "capabilities": {
68
+ "compaction": {
69
+ "default": "unknown",
70
+ "on": {
71
+ "set_by_subc": false,
72
+ "kind": null,
73
+ "name": null,
74
+ "value": null,
75
+ "verified": false,
76
+ "note": "subc does not configure Cursor compaction."
77
+ },
78
+ "off": {
79
+ "set_by_subc": false,
80
+ "kind": null,
81
+ "name": null,
82
+ "value": null,
83
+ "verified": false,
84
+ "note": "subc does not configure Cursor compaction."
85
+ },
86
+ "threshold": {
87
+ "set_by_subc": false,
88
+ "kind": null,
89
+ "name": null,
90
+ "value": null,
91
+ "verified": false,
92
+ "note": "subc does not configure Cursor compaction."
93
+ }
94
+ },
95
+ "mcp": {
96
+ "configured_by_subc": false,
97
+ "transports": null,
98
+ "verified": false,
99
+ "note": "subc configures no MCP servers."
100
+ },
101
+ "headers": [
102
+ {
103
+ "name": "x-subconscious-client",
104
+ "value": "cursor",
105
+ "on": "hook POSTs to {baseUrl}/v1/agent-hooks",
106
+ "set_by_subc": true,
107
+ "verified": true,
108
+ "note": "Cursor cannot add headers to model requests."
109
+ },
110
+ {
111
+ "name": "Authorization",
112
+ "value": "Bearer {apiKey}",
113
+ "on": "hook POSTs to {baseUrl}/v1/agent-hooks",
114
+ "set_by_subc": true,
115
+ "verified": true
116
+ }
117
+ ],
118
+ "hooks": [
119
+ {
120
+ "event": "beforeSubmitPrompt",
121
+ "posts": "conversation_ensure",
122
+ "endpoint": "{baseUrl}/v1/agent-hooks",
123
+ "config": "~/.cursor/hooks.json",
124
+ "installed_by": ["install"],
125
+ "verified": true
126
+ },
127
+ {
128
+ "event": "preCompact",
129
+ "posts": "conversation_compaction phase=point",
130
+ "endpoint": "{baseUrl}/v1/agent-hooks",
131
+ "config": "~/.cursor/hooks.json",
132
+ "installed_by": ["install"],
133
+ "verified": true
134
+ }
135
+ ]
136
+ }
137
+ }
@@ -44,37 +44,12 @@ set -euo pipefail
44
44
 
45
45
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
46
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
47
 
54
48
  GATEWAY_URL="${GATEWAY_URL:-}"
55
49
  API_KEY="${CURSOR_API_KEY:-${API_KEY:-}}"
56
- MODEL="${MODEL:-subconscious/glm-5.3-marathon}"
50
+ MODEL="${MODEL:-}"
57
51
  COMMAND="install"
58
52
 
59
- DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.3-marathon
60
- subconscious/glm-5.2
61
- subconscious/tim-qwen3.6-27b
62
- subconscious/deepseek-v4-flash-marathon
63
- subconscious/deepseek-v4.1-flash-marathon"
64
-
65
- SUPPORTED_MODELS=()
66
- add_supported_model() {
67
- local model_id="$1"
68
- for existing in "${SUPPORTED_MODELS[@]:-}"; do
69
- [[ "$existing" == "$model_id" ]] && return
70
- done
71
- SUPPORTED_MODELS+=("$model_id")
72
- }
73
- add_supported_model "$MODEL"
74
- while IFS= read -r model_id; do
75
- [[ -n "$model_id" ]] && add_supported_model "$model_id"
76
- done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
77
-
78
53
  usage() {
79
54
  cat <<'EOF'
80
55
  Usage:
@@ -209,13 +184,11 @@ remove_hook_entries() {
209
184
  }
210
185
 
211
186
  merge_hooks_json() {
212
- if [[ ! -f "$HOOKS_JSON" ]]; then
213
- sed "s|HOOK_SH_PATH|${HOOK_DST}|g" "$HOOKS_TEMPLATE" >"$HOOKS_JSON"
214
- return
215
- fi
216
- # Replace our marker entries, then register the two lifecycle events we use.
217
- remove_hook_entries "$HOOKS_JSON" "$MARKER"
218
- merge_hook_entries "$HOOKS_JSON" "$MARKER" "$HOOK_DST" beforeSubmitPrompt preCompact
187
+ local command
188
+ command="$(jq -rn --arg path "$HOOK_DST" '$path | @sh')"
189
+ # The merge handles both first install and refresh; jq escapes JSON while
190
+ # @sh keeps spaces and shell characters inside the executable path.
191
+ merge_hook_entries "$HOOKS_JSON" "$MARKER" "$command" beforeSubmitPrompt preCompact
219
192
  }
220
193
 
221
194
  uninstall_hooks() {
@@ -270,9 +243,9 @@ case "$COMMAND" in
270
243
  echo " Enable OpenAI API Key Override, then use:"
271
244
  echo " Base URL: ${GATEWAY_URL%/}/v1"
272
245
  echo " Add the available custom models:"
273
- for model_id in "${SUPPORTED_MODELS[@]}"; do
246
+ while IFS= read -r model_id; do
274
247
  echo " ${model_id}"
275
- done
248
+ done <<< "$SUBC_MODEL_IDS"
276
249
  echo " Select ${MODEL} for this profile."
277
250
  echo " Paste your Subconscious API key there, then fully restart Cursor."
278
251
  ;;
@@ -0,0 +1,163 @@
1
+ {
2
+ "id": "deepseek-harness",
3
+ "command": "dsh",
4
+ "aliases": ["deepseek-harness", "deepseek"],
5
+ "name": "DeepSeek Harness",
6
+ "description": "Launch DeepSeek Harness with an ephemeral Subconscious provider populated from the live model catalog.",
7
+ "protocol": "openai",
8
+ "homepage": "https://www.deepseek.com/harness/en/",
9
+ "install": {
10
+ "commands": {
11
+ "darwin": "npm i -g {package}",
12
+ "linux": "npm i -g {package}",
13
+ "win32": "npm i -g {package}"
14
+ },
15
+ "method": "npm",
16
+ "package": "@deepseek-ai/dsh",
17
+ "version": null,
18
+ "channel": "latest (unpinned)",
19
+ "minimum_version": null,
20
+ "verified": true
21
+ },
22
+ "runbook": {
23
+ "mode": "launch",
24
+ "script": "run.sh"
25
+ },
26
+ "help": {
27
+ "usage": "subc [-p NAME] dsh [web|headless] [--model MODEL] [Harness arguments...]",
28
+ "behavior": "Launches DeepSeek Harness with a temporary Subconscious provider containing every live gateway model. Web mode is the default.",
29
+ "options": [
30
+ ["help", "Show this help"],
31
+ ["web", "Launch the DeepSeek Harness Web UI (default)"],
32
+ ["headless PROMPT", "Run one headless task and exit"],
33
+ ["--model MODEL", "Set the initial model for new Harness sessions"]
34
+ ],
35
+ "args": ["ARGS...", "Pass remaining arguments to the selected Harness mode"]
36
+ },
37
+ "prerequisites": [],
38
+ "launch": {
39
+ "argv": ["dsh", "web", "--patch", "{tempFile}", "{args}"],
40
+ "headless_argv": [
41
+ "dsh",
42
+ "--profile",
43
+ "headless",
44
+ "--patch",
45
+ "{tempFile}",
46
+ "{args}"
47
+ ],
48
+ "headless_stdin": "{prompt}",
49
+ "headless_platforms": ["darwin", "linux", "win32"],
50
+ "mode_word": "web",
51
+ "persistent_setup": false
52
+ },
53
+ "inputs": [
54
+ {
55
+ "name": "DEEPSEEK_HARNESS_API_KEY",
56
+ "label": "DeepSeek Harness API key",
57
+ "description": "DeepSeek Harness credential override; blank uses API_KEY.",
58
+ "type": "secret"
59
+ },
60
+ {
61
+ "name": "DEEPSEEK_HARNESS_CONTEXT_WINDOW",
62
+ "default": 5000000,
63
+ "label": "Context window",
64
+ "description": "Context capacity advertised to DeepSeek Harness.",
65
+ "type": "integer",
66
+ "min": 1
67
+ },
68
+ {
69
+ "name": "DEEPSEEK_HARNESS_MAX_TOKENS",
70
+ "default": 65536,
71
+ "label": "Maximum output tokens",
72
+ "description": "Output capacity advertised to DeepSeek Harness.",
73
+ "type": "integer",
74
+ "min": 1
75
+ }
76
+ ],
77
+ "env": [
78
+ {
79
+ "name": "SUBCONSCIOUS_API_KEY",
80
+ "value": "{apiKey}",
81
+ "description": "Credential, read through apiKeyEnv in the overlay."
82
+ },
83
+ {
84
+ "name": "SUBCONSCIOUS_DSH_BASE_URL",
85
+ "value": "{baseUrlV1}",
86
+ "description": "OpenAI-compatible base URL, read by the overlay."
87
+ }
88
+ ],
89
+ "config": [
90
+ {
91
+ "kind": "file",
92
+ "path": "{tmp}/subc-dsh.XXXXXX/subconscious.cordis.yml",
93
+ "lifetime": "launch",
94
+ "value": {
95
+ "llm-pi-ai.providers.subconscious.api": "openai-completions",
96
+ "llm-pi-ai.providers.subconscious.apiKeyEnv": "SUBCONSCIOUS_API_KEY",
97
+ "llm-pi-ai.providers.subconscious.headers": {
98
+ "x-subconscious-client": "deepseek-harness"
99
+ },
100
+ "llm-pi-ai.providers.subconscious.compat": {
101
+ "supportsDeveloperRole": false,
102
+ "maxTokensField": "max_tokens"
103
+ },
104
+ "llm-pi-ai.providers.subconscious.defaultContextWindow": "${DEEPSEEK_HARNESS_CONTEXT_WINDOW}",
105
+ "llm-pi-ai.providers.subconscious.defaultMaxTokens": "${DEEPSEEK_HARNESS_MAX_TOKENS}",
106
+ "agent-default-model": {
107
+ "provider": "subconscious",
108
+ "model": "{model}"
109
+ },
110
+ "llm-pi-ai.providers.subconscious.models[].contextWindow": "${DEEPSEEK_HARNESS_CONTEXT_WINDOW}",
111
+ "llm-pi-ai.providers.subconscious.models[].maxTokens": "${DEEPSEEK_HARNESS_MAX_TOKENS}"
112
+ },
113
+ "description": "Cordis patch the runbook writes and passes with --patch; holds no API key; removed once dsh exits."
114
+ }
115
+ ],
116
+ "capabilities": {
117
+ "compaction": {
118
+ "default": "unknown",
119
+ "on": {
120
+ "set_by_subc": false,
121
+ "kind": null,
122
+ "name": null,
123
+ "value": null,
124
+ "verified": false,
125
+ "note": "subc sets contextWindow only; DeepSeek Harness compaction behaviour is not described in this repo."
126
+ },
127
+ "off": {
128
+ "set_by_subc": false,
129
+ "kind": null,
130
+ "name": null,
131
+ "value": null,
132
+ "verified": false,
133
+ "note": "No off knob is set or documented by subc."
134
+ },
135
+ "threshold": {
136
+ "set_by_subc": true,
137
+ "kind": "config-field",
138
+ "name": "llm-pi-ai.providers.subconscious.models[].contextWindow",
139
+ "value": "${DEEPSEEK_HARNESS_CONTEXT_WINDOW}",
140
+ "verified": false,
141
+ "note": "subc sets it; whether it drives compaction is not confirmed here.",
142
+ "semantics": "window",
143
+ "config": "{tmp}/subc-dsh.XXXXXX/subconscious.cordis.yml"
144
+ }
145
+ },
146
+ "mcp": {
147
+ "configured_by_subc": false,
148
+ "transports": null,
149
+ "verified": false,
150
+ "note": "subc configures no MCP servers."
151
+ },
152
+ "headers": [
153
+ {
154
+ "name": "x-subconscious-client",
155
+ "value": "deepseek-harness",
156
+ "on": "model requests",
157
+ "set_by_subc": true,
158
+ "verified": true
159
+ }
160
+ ],
161
+ "hooks": []
162
+ }
163
+ }
@@ -1,28 +1,15 @@
1
1
  #!/usr/bin/env bash
2
- # Launch DeepSeek Harness with a temporary Cordis overlay that adds the live
3
- # Subconscious catalog. The user's DSH settings and profiles are not rewritten.
2
+ # Write a temporary Cordis overlay that adds the live Subconscious catalog,
3
+ # then start the dsh argv subc built. The user's DSH settings and profiles are
4
+ # not rewritten.
4
5
 
5
6
  set -euo pipefail
6
7
 
7
8
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
- source "${SCRIPT_DIR}/../model-capabilities.generated.sh"
9
+ source "${SCRIPT_DIR}/../lib.sh"
9
10
 
10
- GATEWAY_URL="${GATEWAY_URL:-}"
11
- API_KEY="${DEEPSEEK_HARNESS_API_KEY:-${API_KEY:-}}"
12
- MODEL="${MODEL:-subconscious/glm-5.3-marathon}"
13
- CONTEXT_WINDOW="${DEEPSEEK_HARNESS_CONTEXT_WINDOW:-5000000}"
14
- MAX_TOKENS="${DEEPSEEK_HARNESS_MAX_TOKENS:-65536}"
15
-
16
- DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.3-marathon
17
- subconscious/glm-5.2
18
- subconscious/tim-qwen3.6-27b
19
- subconscious/deepseek-v4-flash-marathon
20
- subconscious/deepseek-v4.1-flash-marathon"
21
-
22
- if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
23
- echo "error: GATEWAY_URL and API_KEY are required to launch DeepSeek Harness" >&2
24
- exit 1
25
- fi
11
+ CONTEXT_WINDOW="$DEEPSEEK_HARNESS_CONTEXT_WINDOW"
12
+ MAX_TOKENS="$DEEPSEEK_HARNESS_MAX_TOKENS"
26
13
  if [[ ! "$CONTEXT_WINDOW" =~ ^[1-9][0-9]*$ ]]; then
27
14
  echo "error: DEEPSEEK_HARNESS_CONTEXT_WINDOW must be a positive integer" >&2
28
15
  exit 1
@@ -32,27 +19,6 @@ if [[ ! "$MAX_TOKENS" =~ ^[1-9][0-9]*$ ]]; then
32
19
  exit 1
33
20
  fi
34
21
 
35
- SUPPORTED_MODELS=()
36
- add_supported_model() {
37
- local model_id="$1" existing
38
- [[ -n "$model_id" ]] || return 0
39
- if [[ ! "$model_id" =~ ^[-A-Za-z0-9._:/+]+$ ]]; then
40
- echo "error: invalid model id: $model_id" >&2
41
- exit 1
42
- fi
43
- if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
44
- for existing in "${SUPPORTED_MODELS[@]}"; do
45
- [[ "$existing" == "$model_id" ]] && return 0
46
- done
47
- fi
48
- SUPPORTED_MODELS+=("$model_id")
49
- }
50
-
51
- add_supported_model "$MODEL"
52
- while IFS= read -r model_id; do
53
- add_supported_model "$model_id"
54
- done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
55
-
56
22
  OVERLAY_DIR="$(mktemp -d "${TMPDIR:-/tmp}/subc-dsh.XXXXXX")"
57
23
  OVERLAY_FILE="${OVERLAY_DIR}/subconscious.cordis.yml"
58
24
  cleanup() {
@@ -80,7 +46,7 @@ trap cleanup EXIT HUP INT TERM
80
46
  " defaultContextWindow: ${CONTEXT_WINDOW}" \
81
47
  " defaultMaxTokens: ${MAX_TOKENS}" \
82
48
  ' models:'
83
- for model_id in "${SUPPORTED_MODELS[@]}"; do
49
+ while IFS= read -r model_id; do
84
50
  printf " - id: '%s'\n" "$model_id"
85
51
  printf " name: '%s'\n" "$model_id"
86
52
  if subc_model_supports_vision "$model_id"; then
@@ -88,7 +54,7 @@ trap cleanup EXIT HUP INT TERM
88
54
  fi
89
55
  printf " contextWindow: %s\n" "$CONTEXT_WINDOW"
90
56
  printf " maxTokens: %s\n" "$MAX_TOKENS"
91
- done
57
+ done <<< "$SUBC_MODEL_IDS"
92
58
  printf '%s\n' \
93
59
  '- id: agent-default-model' \
94
60
  ' config:' \
@@ -97,21 +63,16 @@ trap cleanup EXIT HUP INT TERM
97
63
  } >"$OVERLAY_FILE"
98
64
  chmod 600 "$OVERLAY_FILE"
99
65
 
100
- export SUBCONSCIOUS_API_KEY="$API_KEY"
101
- export SUBCONSCIOUS_DSH_BASE_URL="${GATEWAY_URL%/}/v1"
66
+ # dsh replaces this shell, keeping its PID, so signals and the exit status
67
+ # are dsh's own. A detached watcher removes the overlay once that PID exits.
68
+ runbook_pid=$$
69
+ (
70
+ # A stop sent to the whole process group must not kill the watcher first.
71
+ trap '' HUP INT TERM
72
+ set +e
73
+ while kill -0 "$runbook_pid" 2>/dev/null; do sleep 1; done
74
+ cleanup
75
+ ) </dev/null >/dev/null 2>&1 &
76
+ trap - EXIT HUP INT TERM
102
77
 
103
- mode="web"
104
- if [[ "${1:-}" == "web" || "${1:-}" == "headless" ]]; then
105
- mode="$1"
106
- shift
107
- fi
108
-
109
- set +e
110
- if [[ "$mode" == "headless" ]]; then
111
- dsh --profile headless --patch "$OVERLAY_FILE" "$@"
112
- else
113
- dsh web --patch "$OVERLAY_FILE" "$@"
114
- fi
115
- status=$?
116
- set -e
117
- exit "$status"
78
+ subc_exec "{tempFile}" "$OVERLAY_FILE" -- "$@"
@@ -0,0 +1,14 @@
1
+ // Pi extension (Pi loads a folder's index.ts): keep requests to Subconscious at the newest 30 images, so a
2
+ // long screenshot session's upload stops growing. The rule lives in
3
+ // window.js (plain JS so the CLI's tests can import it).
4
+
5
+ import { windowImages } from './window.js';
6
+
7
+ export default function (pi: {
8
+ on: (
9
+ event: 'before_provider_request',
10
+ handler: (event: { payload: unknown }) => unknown,
11
+ ) => void;
12
+ }) {
13
+ pi.on('before_provider_request', (event) => windowImages(event.payload));
14
+ }
@@ -0,0 +1,19 @@
1
+ export declare const MAX_IMAGES: number;
2
+ export declare const IMAGE_LABEL: string;
3
+ export declare const DEFAULT_SOFT_IMAGE_MIB: number;
4
+ export declare const DEFAULT_HARD_IMAGE_MIB: number;
5
+ export declare const SOFT_IMAGE_BYTES: number;
6
+ export declare const HARD_IMAGE_BYTES: number;
7
+ export declare function imagesToDrop(
8
+ sizes: number[],
9
+ maxImages?: number,
10
+ softBytes?: number,
11
+ hardBytes?: number,
12
+ ): number;
13
+ export declare function windowImages(
14
+ payload: unknown,
15
+ maxImages?: number,
16
+ softBytes?: number,
17
+ hardBytes?: number,
18
+ ): unknown;
19
+ export declare function windowFetch(fetchFn: typeof fetch): typeof fetch;
@@ -0,0 +1,140 @@
1
+ // The image window: keep a request to Subconscious at the newest screenshots.
2
+ //
3
+ // Every coding agent resends its whole history, every screenshot included, on
4
+ // every turn, so a long screenshot session's upload grows without end. The
5
+ // Subconscious gateway keeps only the newest images before it forwards a
6
+ // request, but that does not stop the agent uploading all of them. subc's Pi
7
+ // extension (index.ts) and OpenCode plugin apply this rule before the request
8
+ // leaves the machine:
9
+ //
10
+ // keep the newest screenshots, up to MAX_IMAGES and about SOFT_IMAGE_BYTES
11
+ // in total; replace every older one with the text IMAGE_LABEL.
12
+ //
13
+ // The label is what keeps Subconscious Cache working. The gateway puts the
14
+ // same label before every image, so replacing an image with it removes only
15
+ // the image's own tokens and nothing around it moves; the cache then reuses
16
+ // the rest of the conversation and each turn only reads the new screenshot.
17
+ //
18
+ // The cache reuses across one removed image per turn, not several, so the
19
+ // window never removes more than one screenshot per turn. A big screenshot can
20
+ // take the total over SOFT_IMAGE_BYTES for a few turns while the window
21
+ // catches up one screenshot at a time. Only if a request would pass
22
+ // HARD_IMAGE_BYTES does it drop
23
+ // straight back to the soft limit, which costs that turn's cache hit instead
24
+ // of failing the request.
25
+
26
+ export const MAX_IMAGES = 30;
27
+ export const IMAGE_LABEL = 'image';
28
+ const MIB = 1024 * 1024;
29
+ /** Where trimming starts; room below the hard limit to catch up. */
30
+ export const DEFAULT_SOFT_IMAGE_MIB = 25;
31
+ /** Never forwarded above this. */
32
+ export const DEFAULT_HARD_IMAGE_MIB = 30;
33
+ export const SOFT_IMAGE_BYTES =
34
+ positiveNumber(
35
+ process.env.SUBCONSCIOUS_IMAGE_WINDOW_SOFT_MIB,
36
+ DEFAULT_SOFT_IMAGE_MIB,
37
+ ) * MIB;
38
+ export const HARD_IMAGE_BYTES =
39
+ positiveNumber(
40
+ process.env.SUBCONSCIOUS_IMAGE_WINDOW_HARD_MIB,
41
+ DEFAULT_HARD_IMAGE_MIB,
42
+ ) * MIB;
43
+ const SUBCONSCIOUS_MODEL = /^subconscious\//;
44
+ const LABEL_PART = Object.freeze({ type: 'text', text: IMAGE_LABEL });
45
+
46
+ function positiveNumber(raw, fallback) {
47
+ const value = Number(raw);
48
+ return Number.isFinite(value) && value > 0 ? value : fallback;
49
+ }
50
+
51
+ /**
52
+ * How many of the oldest images to drop, given each image's size in order
53
+ * (oldest first). The answer for a request is worked out turn by turn over
54
+ * its history, as if each earlier screenshot had just arrived, so it depends
55
+ * only on the request and consecutive turns differ by at most one removal
56
+ * (except at the hard limit). The newest image is always kept.
57
+ */
58
+ export function imagesToDrop(
59
+ sizes,
60
+ maxImages = MAX_IMAGES,
61
+ softBytes = SOFT_IMAGE_BYTES,
62
+ hardBytes = HARD_IMAGE_BYTES,
63
+ ) {
64
+ const total = [0];
65
+ for (const size of sizes) total.push(total.at(-1) + size);
66
+ const kept = (from, to) => total[to] - total[from];
67
+ let dropped = 0;
68
+ // Smallest drop count that meets both soft limits for the first k images.
69
+ let needed = 0;
70
+ for (let k = 1; k <= sizes.length; k++) {
71
+ needed = Math.max(needed, k - maxImages);
72
+ while (needed < k - 1 && kept(needed, k) > softBytes) needed += 1;
73
+ dropped = Math.max(dropped, Math.min(dropped + 1, needed));
74
+ if (kept(dropped, k) > hardBytes) dropped = needed;
75
+ }
76
+ return dropped;
77
+ }
78
+
79
+ function contentParts(message) {
80
+ return Array.isArray(message.content) ? message.content : [];
81
+ }
82
+
83
+ function isChatImage(part) {
84
+ return part?.type === 'image_url';
85
+ }
86
+
87
+ function chatImageSize(part) {
88
+ const url =
89
+ typeof part.image_url === 'string' ? part.image_url : part.image_url?.url;
90
+ return typeof url === 'string' ? url.length : 0;
91
+ }
92
+
93
+ /**
94
+ * Returns a new OpenAI chat payload with the oldest images replaced by the
95
+ * label, or undefined when there is nothing to change.
96
+ */
97
+ export function windowImages(
98
+ payload,
99
+ maxImages = MAX_IMAGES,
100
+ softBytes = SOFT_IMAGE_BYTES,
101
+ hardBytes = HARD_IMAGE_BYTES,
102
+ ) {
103
+ if (!SUBCONSCIOUS_MODEL.test(payload?.model ?? '')) return undefined;
104
+ if (!Array.isArray(payload.messages)) return undefined;
105
+ const sizes = payload.messages
106
+ .flatMap(contentParts)
107
+ .filter(isChatImage)
108
+ .map(chatImageSize);
109
+ let drop = imagesToDrop(sizes, maxImages, softBytes, hardBytes);
110
+ if (drop === 0) return undefined;
111
+ const messages = payload.messages.map((message) => {
112
+ if (drop === 0 || !Array.isArray(message.content)) return message;
113
+ const content = message.content.map((part) => {
114
+ if (drop === 0 || !isChatImage(part)) return part;
115
+ drop -= 1;
116
+ return LABEL_PART;
117
+ });
118
+ return { ...message, content };
119
+ });
120
+ return { ...payload, messages };
121
+ }
122
+
123
+ /**
124
+ * Wraps a fetch so every JSON request body goes through windowImages first.
125
+ * Anything else, or a body that is not JSON, is sent unchanged: a problem here
126
+ * must never break a coding session.
127
+ */
128
+ export function windowFetch(fetchFn) {
129
+ return async (input, init) => {
130
+ if (typeof init?.body !== 'string') return fetchFn(input, init);
131
+ let windowed;
132
+ try {
133
+ windowed = windowImages(JSON.parse(init.body));
134
+ } catch {
135
+ return fetchFn(input, init);
136
+ }
137
+ if (!windowed) return fetchFn(input, init);
138
+ return fetchFn(input, { ...init, body: JSON.stringify(windowed) });
139
+ };
140
+ }