subconscious-cli 6.0.1 → 6.2.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 (73) hide show
  1. package/README.md +167 -43
  2. package/bin/agent-data.js +278 -0
  3. package/bin/agent-help.js +114 -0
  4. package/bin/agent-install.js +238 -0
  5. package/bin/agent-launch.js +199 -0
  6. package/bin/agent-spawn.js +137 -0
  7. package/bin/agents.js +236 -931
  8. package/bin/auth.js +9 -1
  9. package/bin/claude-picker.js +143 -0
  10. package/bin/cli.js +62 -2
  11. package/bin/clipboard-image.js +2 -0
  12. package/bin/feedback.js +14 -3
  13. package/bin/harness-manifest.js +286 -0
  14. package/bin/headless-args.js +35 -0
  15. package/bin/launch-values.js +25 -0
  16. package/bin/model-capabilities.js +10 -9
  17. package/bin/models.js +25 -13
  18. package/bin/native/subc-tui-darwin-amd64 +0 -0
  19. package/bin/native/subc-tui-darwin-arm64 +0 -0
  20. package/bin/native/subc-tui-linux-amd64 +0 -0
  21. package/bin/native/subc-tui-linux-arm64 +0 -0
  22. package/bin/native/subc-tui-windows-amd64.exe +0 -0
  23. package/bin/native/subc-tui-windows-arm64.exe +0 -0
  24. package/bin/opencode-provider.js +21 -0
  25. package/bin/profile-settings.js +129 -0
  26. package/bin/profiles.js +29 -424
  27. package/bin/runbook/README.md +77 -23
  28. package/bin/runbook/claude-code/agent.json +300 -0
  29. package/bin/runbook/claude-code/install.sh +9 -252
  30. package/bin/runbook/codex/agent.json +462 -0
  31. package/bin/runbook/codex/hooks-lib.sh +17 -4
  32. package/bin/runbook/codex/install.sh +0 -4
  33. package/bin/runbook/codex/run.sh +16 -235
  34. package/bin/runbook/copilot/agent.json +186 -0
  35. package/bin/runbook/copilot/install.sh +45 -63
  36. package/bin/runbook/cursor/agent.json +138 -0
  37. package/bin/runbook/cursor/install.sh +8 -35
  38. package/bin/runbook/deepseek-harness/agent.json +164 -0
  39. package/bin/runbook/deepseek-harness/run.sh +20 -59
  40. package/bin/runbook/image-window/index.ts +14 -0
  41. package/bin/runbook/image-window/window.d.ts +19 -0
  42. package/bin/runbook/image-window/window.js +140 -0
  43. package/bin/runbook/lib.sh +41 -0
  44. package/bin/runbook/opencode/agent.json +159 -0
  45. package/bin/runbook/opencode/install.sh +9 -183
  46. package/bin/runbook/opencode/opencode-plugin.d.ts +10 -3
  47. package/bin/runbook/opencode/subconscious-image-window.ts +24 -0
  48. package/bin/runbook/pi/agent.json +217 -0
  49. package/bin/runbook/pi/install.sh +18 -44
  50. package/bin/runbook/pi/run.sh +6 -18
  51. package/bin/runbook/pi/subconscious-compaction.ts +3 -1
  52. package/bin/runbook/shared.json +57 -0
  53. package/bin/sessions.js +53 -48
  54. package/bin/tui.js +13 -0
  55. package/bin/update-check.js +35 -1
  56. package/bin/upgrade.js +27 -34
  57. package/bin/usage.js +21 -10
  58. package/bin/windows/agents.js +28 -60
  59. package/bin/windows/common.js +2 -28
  60. package/bin/windows/launch.js +252 -231
  61. package/bin/windows/process.js +12 -1
  62. package/bin/windows/setup.js +29 -9
  63. package/package.json +2 -4
  64. package/bin/registry.generated.json +0 -406
  65. package/bin/runbook/claude-code/run.sh +0 -254
  66. package/bin/runbook/model-capabilities.generated.sh +0 -9
  67. package/bin/runbook/opencode/provider.sh +0 -93
  68. package/bin/runbook/opencode/run.sh +0 -99
  69. package/bin/runbook/opencode/subconscious-compaction.ts +0 -100
  70. package/bin/runbook/subconscious-code/install.sh +0 -154
  71. package/bin/runbook/subconscious-code/run.sh +0 -21
  72. package/bin/windows/claude.js +0 -100
  73. package/bin/windows/install.js +0 -188
@@ -0,0 +1,217 @@
1
+ {
2
+ "id": "pi",
3
+ "command": "pi",
4
+ "aliases": [],
5
+ "name": "Pi",
6
+ "description": "Refresh Pi with the live Subconscious model catalog and launch the active profile model.",
7
+ "protocol": "openai",
8
+ "homepage": "https://pi.dev",
9
+ "install": {
10
+ "commands": null,
11
+ "url": "https://pi.dev",
12
+ "method": null,
13
+ "package": null,
14
+ "version": null,
15
+ "minimum_version": null,
16
+ "verified": false,
17
+ "note": "No install command is defined; subc expects pi on PATH."
18
+ },
19
+ "runbook": {
20
+ "mode": "launch",
21
+ "script": "run.sh",
22
+ "setup_script": "install.sh",
23
+ "setup_actions": ["install", "status", "uninstall"],
24
+ "after_install": "Start a fresh session with subc pi."
25
+ },
26
+ "help": {
27
+ "usage": "subc [-p NAME] pi [help|install|status|uninstall] [Pi arguments...]",
28
+ "behavior": "Refreshes the persistent Subconscious provider from the live catalog, then launches Pi.",
29
+ "options": [
30
+ ["help", "Show this help"],
31
+ ["install", "Refresh the Subconscious provider without launching Pi"],
32
+ ["status", "Inspect the persistent Pi provider"],
33
+ ["uninstall", "Remove only the Subconscious Pi provider and extension"],
34
+ ["--model MODEL", "Override the profile model for this launch"]
35
+ ],
36
+ "args": ["ARGS...", "Pass arguments directly to Pi"]
37
+ },
38
+ "sessions": { "key": "pi", "portable": true },
39
+ "prerequisites": [
40
+ {
41
+ "command": "jq",
42
+ "needed_for": "install and every launch",
43
+ "required": true
44
+ }
45
+ ],
46
+ "launch": {
47
+ "argv": [
48
+ "pi",
49
+ "--provider",
50
+ "subconscious",
51
+ "--model",
52
+ "{model}",
53
+ "{args}"
54
+ ],
55
+ "headless_argv": [
56
+ "pi",
57
+ "--provider",
58
+ "subconscious",
59
+ "--model",
60
+ "{model}",
61
+ "--print",
62
+ "{args}"
63
+ ],
64
+ "headless_stdin": "{prompt}",
65
+ "headless_platforms": ["darwin", "linux"],
66
+ "resume": ["--session", "{sessionFile}"],
67
+ "handoff": ["{prompt}"],
68
+ "persistent_setup": true,
69
+ "note": "Each launch first runs install.sh install, which rewrites the persistent files below."
70
+ },
71
+ "inputs": [
72
+ {
73
+ "name": "PI_API_KEY",
74
+ "label": "Pi API key",
75
+ "description": "Pi-specific credential override; blank uses API_KEY.",
76
+ "type": "secret"
77
+ },
78
+ {
79
+ "name": "PI_CONTEXT_WINDOW",
80
+ "default": 5000000,
81
+ "label": "Context window",
82
+ "description": "Pi context window used to determine compaction.",
83
+ "type": "integer",
84
+ "min": 1
85
+ },
86
+ {
87
+ "name": "PI_MAX_TOKENS",
88
+ "default": 65536,
89
+ "label": "Maximum output tokens",
90
+ "description": "Maximum output tokens advertised to Pi.",
91
+ "type": "integer",
92
+ "min": 1
93
+ }
94
+ ],
95
+ "env": [],
96
+ "config": [
97
+ {
98
+ "kind": "file",
99
+ "path": "~/.pi/agent/models.json",
100
+ "lifetime": "persistent",
101
+ "value": {
102
+ "providers.subconscious.baseUrl": "{baseUrlV1}",
103
+ "providers.subconscious.api": "openai-completions",
104
+ "providers.subconscious.apiKey": "{apiKey}",
105
+ "providers.subconscious.headers": {
106
+ "x-subconscious-client": "pi"
107
+ },
108
+ "providers.subconscious.models[].contextWindow": "${PI_CONTEXT_WINDOW}",
109
+ "providers.subconscious.models[].maxTokens": "${PI_MAX_TOKENS}",
110
+ "providers.subconscious.models[].compat": {
111
+ "sendSessionAffinityHeaders": true,
112
+ "sessionAffinityFormat": "openai-nosession"
113
+ }
114
+ },
115
+ "description": "Subconscious provider, written by install.sh and merged in; other providers are kept. Mode 600; holds the key in plain text."
116
+ },
117
+ {
118
+ "kind": "file",
119
+ "path": "~/.pi/agent/subconscious.env",
120
+ "lifetime": "persistent",
121
+ "description": "SUBCONSCIOUS_GATEWAY_URL and SUBCONSCIOUS_API_KEY for the compaction extension (mode 600)."
122
+ },
123
+ {
124
+ "kind": "file",
125
+ "path": "~/.pi/agent/extensions/subconscious-compaction.ts",
126
+ "lifetime": "persistent",
127
+ "description": "Compaction reporting extension."
128
+ },
129
+ {
130
+ "kind": "dir",
131
+ "path": "~/.pi/agent/extensions/subconscious-image-window",
132
+ "lifetime": "persistent",
133
+ "description": "Extension that keeps requests to the newest 30 images."
134
+ }
135
+ ],
136
+ "capabilities": {
137
+ "compaction": {
138
+ "default": "on",
139
+ "on": {
140
+ "set_by_subc": true,
141
+ "kind": "config-field",
142
+ "name": "providers.subconscious.models[].contextWindow",
143
+ "value": "${PI_CONTEXT_WINDOW}",
144
+ "verified": false,
145
+ "note": "subc sets contextWindow. That Pi compacts relative to it comes only from the profile setting description.",
146
+ "config": "~/.pi/agent/models.json"
147
+ },
148
+ "off": {
149
+ "set_by_subc": false,
150
+ "kind": "config-field",
151
+ "name": "compaction.enabled",
152
+ "value": false,
153
+ "verified": false,
154
+ "note": "Upstream Pi setting. subc never sets it, and this repo does not confirm its location or behaviour.",
155
+ "config": "Pi settings (location unverified)"
156
+ },
157
+ "threshold": {
158
+ "set_by_subc": true,
159
+ "kind": "config-field",
160
+ "name": "providers.subconscious.models[].contextWindow",
161
+ "value": "${PI_CONTEXT_WINDOW}",
162
+ "verified": false,
163
+ "note": "subc sets contextWindow. That Pi compacts relative to it comes only from the profile setting description. Pi's reserveTokens also moves the trigger; subc never sets it.",
164
+ "semantics": "window",
165
+ "config": "~/.pi/agent/models.json"
166
+ }
167
+ },
168
+ "mcp": {
169
+ "configured_by_subc": false,
170
+ "transports": null,
171
+ "verified": false,
172
+ "note": "subc configures no MCP servers."
173
+ },
174
+ "headers": [
175
+ {
176
+ "name": "x-subconscious-client",
177
+ "value": "pi",
178
+ "on": "model requests",
179
+ "set_by_subc": true,
180
+ "verified": true
181
+ },
182
+ {
183
+ "name": "x-session-affinity",
184
+ "value": "session ID",
185
+ "on": "model requests",
186
+ "set_by_subc": true,
187
+ "verified": false,
188
+ "note": "Enabled by compat.sendSessionAffinityHeaders; the header name comes from an install.sh comment."
189
+ },
190
+ {
191
+ "name": "Authorization",
192
+ "value": "Bearer {apiKey}",
193
+ "on": "extension POSTs to {baseUrl}/v1/agent-hooks",
194
+ "set_by_subc": true,
195
+ "verified": true
196
+ }
197
+ ],
198
+ "hooks": [
199
+ {
200
+ "event": "session_before_compact",
201
+ "posts": "conversation_compaction phase=start",
202
+ "endpoint": "{baseUrl}/v1/agent-hooks",
203
+ "config": "~/.pi/agent/extensions/subconscious-compaction.ts",
204
+ "installed_by": ["launch", "install"],
205
+ "verified": true
206
+ },
207
+ {
208
+ "event": "session_compact",
209
+ "posts": "conversation_compaction phase=end",
210
+ "endpoint": "{baseUrl}/v1/agent-hooks",
211
+ "config": "~/.pi/agent/extensions/subconscious-compaction.ts",
212
+ "installed_by": ["launch", "install"],
213
+ "verified": true
214
+ }
215
+ ]
216
+ }
217
+ }
@@ -47,26 +47,21 @@
47
47
  set -euo pipefail
48
48
 
49
49
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
50
- source "${SCRIPT_DIR}/../model-capabilities.generated.sh"
50
+ source "${SCRIPT_DIR}/../lib.sh"
51
51
  EXTENSION_SRC="${SCRIPT_DIR}/subconscious-compaction.ts"
52
-
53
- # Load shared env from SUBC_ENV_FILE, or a sibling .env / env.example.
54
- SHARED_ENV="${SUBC_ENV_FILE:-${SCRIPT_DIR}/../.env}"
55
- [[ -f "$SHARED_ENV" ]] || SHARED_ENV="${SCRIPT_DIR}/../env.example"
56
- if [[ -f "$SHARED_ENV" ]]; then set -a; source "$SHARED_ENV"; set +a; fi
52
+ IMAGE_WINDOW_SRC="${SCRIPT_DIR}/../image-window"
57
53
 
58
54
  COMMAND="install"
59
55
  GATEWAY_URL="${GATEWAY_URL:-}"
60
56
  API_KEY="${PI_API_KEY:-${API_KEY:-}}"
61
- MODEL="${MODEL:-subconscious/glm-5.3-marathon}"
62
- CONTEXT_WINDOW="${PI_CONTEXT_WINDOW:-5000000}"
63
- MAX_TOKENS="${PI_MAX_TOKENS:-65536}"
57
+ CONTEXT_WINDOW="${PI_CONTEXT_WINDOW:-}"
58
+ MAX_TOKENS="${PI_MAX_TOKENS:-}"
64
59
 
65
60
  usage() {
66
61
  cat <<'EOF'
67
62
  Usage:
68
- subc pi install [--gateway-url URL] [--api-key KEY] [--model MODEL]
69
- [--context-window N] [--max-tokens N]
63
+ subc pi install [--gateway-url URL] [--api-key KEY] [--context-window N]
64
+ [--max-tokens N]
70
65
  subc pi uninstall
71
66
  subc pi status
72
67
 
@@ -91,10 +86,6 @@ while [[ $# -gt 0 ]]; do
91
86
  API_KEY="${2:-}"
92
87
  shift 2
93
88
  ;;
94
- --model)
95
- MODEL="${2:-}"
96
- shift 2
97
- ;;
98
89
  --context-window)
99
90
  CONTEXT_WINDOW="${2:-}"
100
91
  shift 2
@@ -115,35 +106,8 @@ while [[ $# -gt 0 ]]; do
115
106
  esac
116
107
  done
117
108
 
118
- DEFAULT_SUBCONSCIOUS_MODELS="subconscious/glm-5.3-marathon
119
- subconscious/glm-5.2
120
- subconscious/tim-qwen3.6-27b
121
- subconscious/deepseek-v4-flash-marathon
122
- subconscious/deepseek-v4.1-flash-marathon"
123
- SUPPORTED_MODELS=()
124
-
125
- add_supported_model() {
126
- local model_id="$1" existing
127
- [[ -n "$model_id" ]] || return 0
128
- if [[ ! "$model_id" =~ ^[-A-Za-z0-9._:/+]+$ ]]; then
129
- echo "error: invalid model id: $model_id" >&2
130
- exit 1
131
- fi
132
- if [[ "${#SUPPORTED_MODELS[@]}" -gt 0 ]]; then
133
- for existing in "${SUPPORTED_MODELS[@]}"; do
134
- [[ "$existing" == "$model_id" ]] && return 0
135
- done
136
- fi
137
- SUPPORTED_MODELS+=("$model_id")
138
- }
139
-
140
- add_supported_model "$MODEL"
141
- while IFS= read -r model_id; do
142
- add_supported_model "$model_id"
143
- done <<< "${SUBCONSCIOUS_MODELS:-$DEFAULT_SUBCONSCIOUS_MODELS}"
144
-
145
109
  MODEL_ENTRIES_JSON=""
146
- for model_id in "${SUPPORTED_MODELS[@]}"; do
110
+ while IFS= read -r model_id; do
147
111
  vision_fields=""
148
112
  if subc_model_supports_vision "$model_id"; then
149
113
  vision_fields=',"input":["text","image"]'
@@ -154,12 +118,14 @@ for model_id in "${SUPPORTED_MODELS[@]}"; do
154
118
  else
155
119
  MODEL_ENTRIES_JSON="$model_json"
156
120
  fi
157
- done
121
+ done <<< "${SUBC_MODEL_IDS:-}"
158
122
 
159
123
  PI_DIR="${PI_CODING_AGENT_DIR:-${HOME}/.pi/agent}"
160
124
  MODELS_JSON="${PI_DIR}/models.json"
161
125
  EXTENSIONS_DIR="${PI_DIR}/extensions"
162
126
  EXTENSION_DST="${EXTENSIONS_DIR}/subconscious-compaction.ts"
127
+ # Keeps requests at the newest 30 images so a long session's upload stops growing.
128
+ IMAGE_WINDOW_DST="${EXTENSIONS_DIR}/subconscious-image-window"
163
129
  ENV_FILE="${PI_DIR}/subconscious.env"
164
130
  MARKER='x-subconscious-client'
165
131
 
@@ -222,6 +188,8 @@ write_extension() {
222
188
  fi
223
189
  mkdir -p "$EXTENSIONS_DIR"
224
190
  cp "$EXTENSION_SRC" "$EXTENSION_DST"
191
+ rm -rf "$IMAGE_WINDOW_DST"
192
+ cp -R "$IMAGE_WINDOW_SRC" "$IMAGE_WINDOW_DST"
225
193
  }
226
194
 
227
195
  uninstall_config() {
@@ -235,6 +203,7 @@ uninstall_config() {
235
203
  echo "No Pi models.json at $MODELS_JSON"
236
204
  fi
237
205
  rm -f "$EXTENSION_DST" "$ENV_FILE"
206
+ rm -rf "$IMAGE_WINDOW_DST"
238
207
  }
239
208
 
240
209
  status() {
@@ -253,6 +222,11 @@ status() {
253
222
  else
254
223
  echo "compaction extension: not installed"
255
224
  fi
225
+ if [[ -d "$IMAGE_WINDOW_DST" ]]; then
226
+ echo "image window extension: $IMAGE_WINDOW_DST (installed)"
227
+ else
228
+ echo "image window extension: not installed"
229
+ fi
256
230
  if [[ -f "$ENV_FILE" ]]; then
257
231
  echo "env: $ENV_FILE (present)"
258
232
  else
@@ -1,25 +1,13 @@
1
1
  #!/usr/bin/env bash
2
- # Refresh the Subconscious provider from the live catalog, then launch Pi.
3
- # Other providers in models.json are preserved.
2
+ # Refresh the Subconscious provider from the live catalog, then start the pi
3
+ # argv subc built. Other providers in models.json are preserved.
4
4
 
5
5
  set -euo pipefail
6
6
 
7
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
- GATEWAY_URL="${GATEWAY_URL:-}"
9
- API_KEY="${PI_API_KEY:-${API_KEY:-}}"
10
- MODEL="${MODEL:-subconscious/glm-5.3-marathon}"
11
- PI_DIR="${PI_CODING_AGENT_DIR:-${HOME}/.pi/agent}"
12
- MODELS_JSON="${PI_DIR}/models.json"
8
+ source "${SCRIPT_DIR}/../lib.sh"
13
9
 
14
- if [[ -z "$GATEWAY_URL" || -z "$API_KEY" ]]; then
15
- echo "error: GATEWAY_URL and API_KEY are required to configure Pi" >&2
16
- exit 1
17
- fi
10
+ # stdin can hold a headless prompt, which belongs to Pi.
11
+ "${SCRIPT_DIR}/install.sh" install </dev/null >/dev/null
18
12
 
19
- "${SCRIPT_DIR}/install.sh" install \
20
- --gateway-url "$GATEWAY_URL" \
21
- --api-key "$API_KEY" \
22
- --model "$MODEL" \
23
- >/dev/null
24
-
25
- exec pi --provider subconscious --model "$MODEL" "$@"
13
+ subc_exec -- "$@"
@@ -51,7 +51,9 @@ function loadSubconsciousEnvFile(): void {
51
51
  return;
52
52
  }
53
53
  try {
54
- const path = join(homedir(), '.pi', 'agent', 'subconscious.env');
54
+ const directory =
55
+ process.env.PI_CODING_AGENT_DIR || join(homedir(), '.pi', 'agent');
56
+ const path = join(directory, 'subconscious.env');
55
57
  const text = readFileSync(path, 'utf8');
56
58
  for (const line of text.split('\n')) {
57
59
  const trimmed = line.trim();
@@ -0,0 +1,57 @@
1
+ {
2
+ "agents": [
3
+ "opencode",
4
+ "claude-code",
5
+ "codex",
6
+ "cursor",
7
+ "copilot",
8
+ "pi",
9
+ "deepseek-harness"
10
+ ],
11
+ "defaults": {
12
+ "model": "subconscious/glm-5.3-marathon",
13
+ "models": [
14
+ "subconscious/glm-5.3-marathon",
15
+ "subconscious/glm-5.2",
16
+ "subconscious/tim-qwen3.6-27b",
17
+ "subconscious/deepseek-v4-flash-marathon",
18
+ "subconscious/deepseek-v4.1-flash-marathon"
19
+ ],
20
+ "baseUrl": "https://api.subconscious.dev"
21
+ },
22
+ "model_capabilities": {
23
+ "subconscious/deepseek-v4.1-flash-marathon": { "vision": true }
24
+ },
25
+ "inputs": [
26
+ {
27
+ "name": "GATEWAY_URL",
28
+ "group": "shared",
29
+ "label": "Gateway URL",
30
+ "description": "Subconscious gateway origin shared by every integration.",
31
+ "type": "url",
32
+ "required": true
33
+ },
34
+ {
35
+ "name": "API_KEY",
36
+ "group": "shared",
37
+ "label": "Shared API key",
38
+ "description": "Default credential used when an agent-specific key is blank.",
39
+ "type": "secret"
40
+ },
41
+ {
42
+ "name": "MODEL",
43
+ "group": "shared",
44
+ "label": "Default model",
45
+ "description": "Initial model used by coding-agent launches and setup. UNSET uses the first live catalog model.",
46
+ "type": "model"
47
+ },
48
+ {
49
+ "name": "MAX_CONCURRENT_SUBAGENTS",
50
+ "default": 4,
51
+ "label": "Concurrent subagents",
52
+ "description": "Maximum concurrent Claude/Codex subagent threads.",
53
+ "type": "integer",
54
+ "min": 1
55
+ }
56
+ ]
57
+ }
package/bin/sessions.js CHANGED
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { AGENTS, agentById, agentCommandName } from './agent-data.js';
5
6
  import { resolveAgent, runAgent } from './agents.js';
6
7
 
7
8
  import { c } from './colors.js';
@@ -14,15 +15,25 @@ const MAX_INDEX_BYTES = 4 * 1024 * 1024;
14
15
  const MAX_HANDOFF_MESSAGES = 24;
15
16
  const MAX_HANDOFF_CHARS = 24_000;
16
17
  const MAX_MESSAGE_CHARS = 4_000;
17
- const SESSION_KEY_PATTERN = /^(claude|codex|opencode|pi|sc):[-A-Za-z0-9._]+$/;
18
-
19
- export const SESSION_HARNESSES = Object.freeze({
20
- claude: { name: 'Claude Code', command: 'claude', portable: true },
21
- codex: { name: 'Codex CLI', command: 'codex', portable: true },
22
- opencode: { name: 'OpenCode', command: 'opencode', portable: true },
23
- pi: { name: 'Pi', command: 'pi', portable: true },
24
- sc: { name: 'Marathon', command: 'marathon', portable: false },
25
- });
18
+ const SESSION_KEY_PATTERN = new RegExp(
19
+ `^(${AGENTS.filter((agent) => agent.sessions)
20
+ .map((agent) => agent.sessions.key)
21
+ .join('|')}):[-A-Za-z0-9._]+$`,
22
+ );
23
+
24
+ export const SESSION_HARNESSES = Object.freeze(
25
+ Object.fromEntries(
26
+ AGENTS.filter((agent) => agent.sessions).map((agent) => [
27
+ agent.sessions.key,
28
+ Object.freeze({
29
+ name: agent.name,
30
+ command: agentCommandName(agent),
31
+ portable: agent.sessions.portable,
32
+ agentId: agent.id,
33
+ }),
34
+ ]),
35
+ ),
36
+ );
26
37
 
27
38
  function cleanText(value) {
28
39
  return String(value || '')
@@ -56,7 +67,10 @@ function parseLines(text) {
56
67
  for (const line of text.split('\n')) {
57
68
  if (!line.trim()) continue;
58
69
  try {
59
- records.push(JSON.parse(line));
70
+ const record = JSON.parse(line);
71
+ if (record && typeof record === 'object' && !Array.isArray(record)) {
72
+ records.push(record);
73
+ }
60
74
  } catch {
61
75
  // Session writers can leave a partial final line after an interrupted run.
62
76
  }
@@ -217,14 +231,6 @@ function piMessages(records) {
217
231
  });
218
232
  }
219
233
 
220
- function scMessages(records) {
221
- return records.flatMap((record) => {
222
- if (!['user', 'assistant'].includes(record.type)) return [];
223
- const text = cleanText(record.text ?? record.content);
224
- return text ? [{ role: record.type, text }] : [];
225
- });
226
- }
227
-
228
234
  async function parseFileSession(entry, harness) {
229
235
  const records = parseLines(await readBounded(entry.file, MAX_METADATA_BYTES));
230
236
  if (!records.length) return null;
@@ -290,19 +296,6 @@ async function parseFileSession(entry, harness) {
290
296
  });
291
297
  }
292
298
 
293
- if (harness === 'sc') {
294
- const meta = records[0] || {};
295
- const messages = scMessages(records);
296
- const id = String(meta.id || filename).replace(/^session-/, '');
297
- return sessionRecord(harness, id, {
298
- title: messages.find((message) => message.role === 'user')?.text,
299
- cwd: meta.cwd,
300
- model: meta.model,
301
- updated: entry.updated,
302
- sourcePath: entry.file,
303
- });
304
- }
305
-
306
299
  return null;
307
300
  }
308
301
 
@@ -466,7 +459,6 @@ export async function discoverSessions(options = {}) {
466
459
  claude: path.join(home, '.claude', 'projects'),
467
460
  codex: path.join(home, '.codex', 'sessions'),
468
461
  pi: path.join(home, '.pi', 'agent', 'sessions'),
469
- sc: path.join(home, '.sc', 'sessions'),
470
462
  };
471
463
  const indexes =
472
464
  options.indexes ||
@@ -486,7 +478,6 @@ export async function discoverSessions(options = {}) {
486
478
  codex || discoverFileSessions(roots.codex, 'codex', max),
487
479
  Promise.resolve(discoverOpenCodeSessions(execute, max)),
488
480
  discoverFileSessions(roots.pi, 'pi', max),
489
- discoverFileSessions(roots.sc, 'sc', max),
490
481
  ]);
491
482
  const seen = new Set();
492
483
  return groups
@@ -541,7 +532,6 @@ export async function readSessionMessages(session, options = {}) {
541
532
  if (session.harness === 'claude') return claudeMessages(records);
542
533
  if (session.harness === 'codex') return codexMessages(records);
543
534
  if (session.harness === 'pi') return piMessages(records);
544
- if (session.harness === 'sc') return scMessages(records);
545
535
  return [];
546
536
  }
547
537
 
@@ -594,22 +584,38 @@ export function buildHandoffPrompt(session, messages) {
594
584
  .join('\n\n');
595
585
  }
596
586
 
587
+ function sessionLaunch(harness) {
588
+ return agentById(SESSION_HARNESSES[harness]?.agentId)?.launch ?? {};
589
+ }
590
+
591
+ // One pass, so a prompt that mentions a placeholder is passed unchanged.
592
+ function fillWords(words, values) {
593
+ return words.map((word) =>
594
+ word.replace(/\{(sessionId|sessionFile|prompt)\}/g, (_, name) =>
595
+ String(values[name]),
596
+ ),
597
+ );
598
+ }
599
+
597
600
  export function nativeResumeArgs(session) {
598
- if (session.harness === 'claude') return ['--resume', session.id];
599
- if (session.harness === 'codex') return ['resume', session.id];
600
- if (session.harness === 'opencode') return ['--session', session.id];
601
- if (session.harness === 'pi')
602
- return ['--session', session.sourcePath || session.id];
603
- if (session.harness === 'sc') return ['--resume', session.sourcePath];
604
- throw new Error(`Native resume is not supported for ${session.harnessName}.`);
601
+ const { resume } = sessionLaunch(session.harness);
602
+ if (!resume)
603
+ throw new Error(
604
+ `Native resume is not supported for ${session.harnessName}.`,
605
+ );
606
+ return fillWords(resume, {
607
+ sessionId: session.id,
608
+ sessionFile: session.sourcePath || session.id,
609
+ });
605
610
  }
606
611
 
607
612
  export function handoffLaunchArgs(targetHarness, prompt) {
608
- if (targetHarness === 'opencode') return ['--prompt', prompt];
609
- if (['claude', 'codex', 'pi'].includes(targetHarness)) return [prompt];
610
- throw new Error(
611
- `${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`,
612
- );
613
+ const { handoff } = sessionLaunch(targetHarness);
614
+ if (!handoff)
615
+ throw new Error(
616
+ `${SESSION_HARNESSES[targetHarness]?.name || targetHarness} cannot start an interactive handoff yet.`,
617
+ );
618
+ return fillWords(handoff, { prompt });
613
619
  }
614
620
 
615
621
  function sessionByKey(sessions, key) {
@@ -665,8 +671,7 @@ export async function sessionsCommand(argv, options = {}) {
665
671
  const harnessIndex = argv.indexOf('--harness');
666
672
  const requestedHarness =
667
673
  harnessIndex >= 0 ? argv[harnessIndex + 1] : session.harness;
668
- const targetHarness =
669
- requestedHarness === 'marathon' ? 'sc' : requestedHarness;
674
+ const targetHarness = requestedHarness;
670
675
  const target = SESSION_HARNESSES[targetHarness];
671
676
  if (!target) throw new Error(`Unknown destination harness: ${targetHarness}`);
672
677
 
package/bin/tui.js CHANGED
@@ -422,6 +422,19 @@ function spawnAndWait(command, args, options = {}) {
422
422
  }
423
423
 
424
424
  export async function runTui(options = {}) {
425
+ for (;;) {
426
+ const result = await runTuiOnce(options);
427
+ if (result?.args?.length !== 2 || result.args[0] !== '-p') return result;
428
+ options = {
429
+ ...options,
430
+ profileName: result.args[1],
431
+ profile: undefined,
432
+ state: undefined,
433
+ };
434
+ }
435
+ }
436
+
437
+ async function runTuiOnce(options) {
425
438
  const executable = await resolveTuiExecutable(options);
426
439
  if (!executable) return null;
427
440