subconscious-cli 0.1.1 → 0.2.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 +85 -35
- package/bin/agents.js +360 -0
- package/bin/auth.js +366 -0
- package/bin/cli.js +40 -380
- package/bin/colors.js +12 -0
- package/bin/registry.generated.json +158 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,16 +1,91 @@
|
|
|
1
|
-
#
|
|
1
|
+
# subconscious-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Log in to Subconscious from your terminal, then launch your favorite coding
|
|
4
|
+
agent against your hosted Subconscious model — no per-agent config required.
|
|
4
5
|
|
|
5
6
|
## Quick start
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
|
-
npx
|
|
9
|
+
npx subconscious-cli login # sign in, saves your API key
|
|
10
|
+
npx subconscious-cli claude-code # launch Claude Code on Subconscious
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Installed globally it's just `subconscious <command>`:
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g subconscious-cli
|
|
17
|
+
subconscious login
|
|
18
|
+
subconscious open-code
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Launching coding agents
|
|
22
|
+
|
|
23
|
+
`subconscious <agent>` resolves your saved API key, injects the env vars that
|
|
24
|
+
point the agent at Subconscious, and exec's the real CLI. Nothing is written to
|
|
25
|
+
the agent's own config — the provider is passed in-memory for that run only.
|
|
26
|
+
|
|
27
|
+
Install commands are **OS-specific** — the CLI picks the right one for your
|
|
28
|
+
platform automatically. The table below shows the macOS/Linux command; on
|
|
29
|
+
Windows the equivalent native installer is used instead.
|
|
30
|
+
|
|
31
|
+
| Command | Launches | Requires (install, macOS/Linux) |
|
|
32
|
+
| -------------------------- | ----------- | ------------------------------------------------------- |
|
|
33
|
+
| `subconscious claude-code` | Claude Code | `curl -fsSL https://claude.ai/install.sh \| bash` |
|
|
34
|
+
| `subconscious open-code` | OpenCode | `npm i -g opencode-ai` |
|
|
35
|
+
| `subconscious aider` | Aider | `python3 -m pip install aider-install && aider-install` |
|
|
36
|
+
| `subconscious codex` | Codex CLI | `npm i -g @openai/codex` |
|
|
37
|
+
|
|
38
|
+
Claude Code uses its **native installer** (the `curl`/`irm` script above), with
|
|
39
|
+
`npm i -g @anthropic-ai/claude-code` kept as an automatic fallback if the native
|
|
40
|
+
installer fails.
|
|
41
|
+
|
|
42
|
+
If the underlying agent isn't installed and you're in an interactive terminal,
|
|
43
|
+
the CLI offers to install it for you (just press Enter), runs the right
|
|
44
|
+
installer for your OS (trying the fallback if the primary fails), and launches
|
|
45
|
+
it once the install succeeds. In a non-interactive context (CI) it instead
|
|
46
|
+
prints the exact install command (and any fallback) and exits without running
|
|
47
|
+
anything.
|
|
48
|
+
|
|
49
|
+
Freshly-installed binaries (e.g. Aider and Claude Code land in `~/.local/bin`,
|
|
50
|
+
npm globals in the npm prefix) often aren't on your current shell's `PATH` yet.
|
|
51
|
+
The CLI looks in those common locations and launches the agent anyway. If it
|
|
52
|
+
still can't find the binary right after install, it tells you to **open a new
|
|
53
|
+
terminal** (or add the printed dir to `PATH`) and re-run the command — the
|
|
54
|
+
install itself succeeded.
|
|
55
|
+
|
|
56
|
+
Anything after the agent name is forwarded straight to it:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
subconscious claude-code --resume
|
|
60
|
+
subconscious codex exec "write a test"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Choosing a model
|
|
64
|
+
|
|
65
|
+
Defaults to `subconscious/tim-qwen3.6-27b`. Override per run with `--model`, or
|
|
66
|
+
set `SUBCONSCIOUS_MODEL` in your environment:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
subconscious open-code --model subconscious/tim-qwen3.6-27b
|
|
70
|
+
export SUBCONSCIOUS_MODEL=subconscious/tim-qwen3.6-27b
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Pointing at a different endpoint
|
|
74
|
+
|
|
75
|
+
By default the CLI targets `https://api.subconscious.dev`. Override the base URL
|
|
76
|
+
per run (or for a whole session) with `SUBCONSCIOUS_BASE_URL` — it flows to both
|
|
77
|
+
the Anthropic-style base and the OpenAI-compatible `/v1` base:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
SUBCONSCIOUS_BASE_URL=http://localhost:9999 subconscious claude-code
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Auth commands
|
|
84
|
+
|
|
85
|
+
### `login`
|
|
86
|
+
|
|
87
|
+
Opens your browser to sign in (or create an account). After authentication, your
|
|
88
|
+
API key is automatically generated and saved.
|
|
14
89
|
|
|
15
90
|
```
|
|
16
91
|
Terminal Browser
|
|
@@ -19,41 +94,24 @@ Terminal Browser
|
|
|
19
94
|
│ 2. Open browser ───────────────►│
|
|
20
95
|
│ │ 3. Sign in / sign up via Clerk
|
|
21
96
|
│ │ 4. API key auto-created
|
|
22
|
-
│ 5. Receive key ◄────────────────│
|
|
97
|
+
│ 5. Receive key ◄────────────────│
|
|
23
98
|
│ 6. Save to ~/.subcon/config.json│
|
|
24
99
|
│ │ "You can close this tab"
|
|
25
100
|
✓ Logged in! │
|
|
26
101
|
```
|
|
27
102
|
|
|
28
|
-
## Commands
|
|
29
|
-
|
|
30
|
-
### `login`
|
|
31
|
-
|
|
32
|
-
Opens your browser to sign in (or create an account). After authentication, your API key is automatically generated and saved.
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx @subconscious/cli login
|
|
36
|
-
```
|
|
37
|
-
|
|
38
103
|
### `logout`
|
|
39
104
|
|
|
40
105
|
Removes your saved API key.
|
|
41
106
|
|
|
42
|
-
```bash
|
|
43
|
-
npx @subconscious/cli logout
|
|
44
|
-
```
|
|
45
|
-
|
|
46
107
|
### `whoami`
|
|
47
108
|
|
|
48
109
|
Shows your current authentication status and which key is active.
|
|
49
110
|
|
|
50
|
-
```bash
|
|
51
|
-
npx @subconscious/cli whoami
|
|
52
|
-
```
|
|
53
|
-
|
|
54
111
|
## Where keys are stored
|
|
55
112
|
|
|
56
|
-
Keys are saved to `~/.subcon/config.json` with `600` permissions
|
|
113
|
+
Keys are saved to `~/.subcon/config.json` with `600` permissions
|
|
114
|
+
(owner-read-only). The file looks like:
|
|
57
115
|
|
|
58
116
|
```json
|
|
59
117
|
{
|
|
@@ -61,13 +119,5 @@ Keys are saved to `~/.subcon/config.json` with `600` permissions (owner-read-onl
|
|
|
61
119
|
}
|
|
62
120
|
```
|
|
63
121
|
|
|
64
|
-
Environment variable `SUBCONSCIOUS_API_KEY` takes precedence over the config
|
|
65
|
-
|
|
66
|
-
## Global install
|
|
67
|
-
|
|
68
|
-
If you prefer a persistent command:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm install -g @subconscious/cli
|
|
72
|
-
subconscious login
|
|
73
|
-
```
|
|
122
|
+
Environment variable `SUBCONSCIOUS_API_KEY` takes precedence over the config
|
|
123
|
+
file — handy for CI or temporary overrides.
|
package/bin/agents.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coding-agent launcher.
|
|
3
|
+
*
|
|
4
|
+
* `subconscious <agent>` resolves your saved API key, injects the env vars that
|
|
5
|
+
* point the agent at your hosted Subconscious model, and exec's the real CLI —
|
|
6
|
+
* nothing is written to the agent's own config.
|
|
7
|
+
*
|
|
8
|
+
* There is NO hardcoded agent data here: everything is read from
|
|
9
|
+
* `registry.generated.json` (shipped under `bin/`), which is generated from the
|
|
10
|
+
* single source of truth `agents/registry.json`. Run `pnpm generate` to update.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import fs from 'node:fs/promises';
|
|
16
|
+
import { constants as fsConstants } from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import readline from 'node:readline';
|
|
20
|
+
import { c } from './colors.js';
|
|
21
|
+
import { getApiKey } from './auth.js';
|
|
22
|
+
|
|
23
|
+
// --- Registry (single source of truth, generated copy shipped in the package).
|
|
24
|
+
const registry = JSON.parse(
|
|
25
|
+
readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
|
|
26
|
+
);
|
|
27
|
+
const DEFAULTS = registry.defaults;
|
|
28
|
+
|
|
29
|
+
// --- Token substitution — same rules as scripts/lib/registry.js.
|
|
30
|
+
// Replaces {apiKey}, {model}, {baseUrl}, {baseUrlV1}. NEVER touches {env:...}.
|
|
31
|
+
const TOKENS = ['apiKey', 'model', 'baseUrl', 'baseUrlV1'];
|
|
32
|
+
|
|
33
|
+
function substituteString(str, ctx) {
|
|
34
|
+
let out = str;
|
|
35
|
+
for (const token of TOKENS) {
|
|
36
|
+
if (ctx[token] === undefined) continue;
|
|
37
|
+
out = out.split(`{${token}}`).join(ctx[token]);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function substitute(value, ctx) {
|
|
43
|
+
if (typeof value === 'string') return substituteString(value, ctx);
|
|
44
|
+
if (Array.isArray(value)) return value.map((v) => substitute(v, ctx));
|
|
45
|
+
if (value && typeof value === 'object') {
|
|
46
|
+
const keys = Object.keys(value);
|
|
47
|
+
if (keys.length === 1 && keys[0] === '$json') {
|
|
48
|
+
return JSON.stringify(substitute(value.$json, ctx));
|
|
49
|
+
}
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const key of keys) out[substituteString(key, ctx)] = substitute(value[key], ctx);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the install command for the current OS from a per-OS install object.
|
|
59
|
+
* Falls back to the linux command, then any string value present, if the exact
|
|
60
|
+
* `process.platform` key is missing. Tolerates a legacy plain-string `install`.
|
|
61
|
+
* Returns `{ command, fallback }` where `fallback` may be undefined.
|
|
62
|
+
*/
|
|
63
|
+
function resolveInstall(install) {
|
|
64
|
+
if (typeof install === 'string') return { command: install, fallback: undefined };
|
|
65
|
+
if (!install || typeof install !== 'object') return { command: undefined, fallback: undefined };
|
|
66
|
+
const command =
|
|
67
|
+
install[process.platform] ||
|
|
68
|
+
install.linux ||
|
|
69
|
+
Object.values(install).find((v) => typeof v === 'string');
|
|
70
|
+
return { command, fallback: install.fallback };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- Build the in-memory registry + alias index.
|
|
74
|
+
// Each agent gets a resolved per-OS `install` (string) plus optional
|
|
75
|
+
// `installFallback`, while keeping the original per-OS object available.
|
|
76
|
+
const AGENTS = registry.agents.map((agent) => {
|
|
77
|
+
const { command, fallback } = resolveInstall(agent.install);
|
|
78
|
+
return { ...agent, install: command, installFallback: fallback };
|
|
79
|
+
});
|
|
80
|
+
const BY_ALIAS = new Map();
|
|
81
|
+
for (const agent of AGENTS) {
|
|
82
|
+
BY_ALIAS.set(agent.id, agent);
|
|
83
|
+
for (const alias of agent.aliases || []) BY_ALIAS.set(alias, agent);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveAgent(name) {
|
|
87
|
+
return BY_ALIAS.get(name) ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function agentList() {
|
|
91
|
+
return AGENTS.map((a) => ({ name: a.name, alias: a.id }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the substitution context for a launch:
|
|
96
|
+
* model — --model flag → SUBCONSCIOUS_MODEL → registry default
|
|
97
|
+
* baseUrl — SUBCONSCIOUS_BASE_URL → registry default
|
|
98
|
+
* baseUrlV1 — `${baseUrl}/v1` (so an override flows to both)
|
|
99
|
+
*/
|
|
100
|
+
function buildContext(apiKey, model) {
|
|
101
|
+
const baseUrl = process.env.SUBCONSCIOUS_BASE_URL?.trim() || DEFAULTS.baseUrl;
|
|
102
|
+
return { apiKey, model, baseUrl, baseUrlV1: `${baseUrl}/v1` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Pull a `--model <value>` / `--model=<value>` flag out of the passthrough
|
|
107
|
+
* args (so it sets the Subconscious model rather than reaching the agent).
|
|
108
|
+
* Falls back to SUBCONSCIOUS_MODEL, then the registry default.
|
|
109
|
+
*/
|
|
110
|
+
function extractModel(argv) {
|
|
111
|
+
let model = process.env.SUBCONSCIOUS_MODEL?.trim() || DEFAULTS.model;
|
|
112
|
+
const rest = [];
|
|
113
|
+
for (let i = 0; i < argv.length; i++) {
|
|
114
|
+
const a = argv[i];
|
|
115
|
+
if (a === '--model') {
|
|
116
|
+
const v = argv[i + 1];
|
|
117
|
+
if (v && !v.startsWith('-')) {
|
|
118
|
+
model = v;
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (a.startsWith('--model=')) {
|
|
124
|
+
model = a.slice('--model='.length);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
rest.push(a);
|
|
128
|
+
}
|
|
129
|
+
return { model, rest };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Common locations a freshly-installed coding-agent binary lands in but which
|
|
134
|
+
* are often NOT on the current process's PATH (e.g. aider/claude install into
|
|
135
|
+
* `~/.local/bin`; npm globals into the npm prefix bin). Best-effort, deduped.
|
|
136
|
+
*/
|
|
137
|
+
function candidateBinDirs() {
|
|
138
|
+
const home = os.homedir();
|
|
139
|
+
const dirs = [];
|
|
140
|
+
|
|
141
|
+
if (process.platform === 'win32') {
|
|
142
|
+
if (process.env.APPDATA) dirs.push(path.join(process.env.APPDATA, 'npm'));
|
|
143
|
+
if (process.env.USERPROFILE) {
|
|
144
|
+
dirs.push(path.join(process.env.USERPROFILE, '.local', 'bin'));
|
|
145
|
+
}
|
|
146
|
+
if (home) dirs.push(path.join(home, '.local', 'bin'));
|
|
147
|
+
} else {
|
|
148
|
+
dirs.push(path.join(home, '.local', 'bin'));
|
|
149
|
+
dirs.push('/opt/homebrew/bin');
|
|
150
|
+
dirs.push('/usr/local/bin');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// npm global bin (best-effort — npm may be absent).
|
|
154
|
+
try {
|
|
155
|
+
const prefix = execFileSync('npm', ['prefix', '-g'], {
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
158
|
+
}).trim();
|
|
159
|
+
if (prefix) {
|
|
160
|
+
dirs.push(process.platform === 'win32' ? prefix : path.join(prefix, 'bin'));
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// npm not available — skip.
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Dedupe, drop empties.
|
|
167
|
+
return [...new Set(dirs.filter(Boolean))];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Executable extensions to probe (Windows uses PATHEXT). */
|
|
171
|
+
function binExts() {
|
|
172
|
+
return process.platform === 'win32'
|
|
173
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
174
|
+
: [''];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Resolve `bin` against PATH plus the candidate bin dirs. Returns the directory
|
|
179
|
+
* containing the executable if found, otherwise null. Searching the candidate
|
|
180
|
+
* dirs lets us find binaries installed this session that aren't on PATH yet.
|
|
181
|
+
*/
|
|
182
|
+
async function resolveBinPath(bin) {
|
|
183
|
+
const pathDirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
184
|
+
const dirs = [...pathDirs, ...candidateBinDirs()];
|
|
185
|
+
const exts = binExts();
|
|
186
|
+
for (const dir of dirs) {
|
|
187
|
+
for (const ext of exts) {
|
|
188
|
+
const candidate = path.join(dir, bin + ext);
|
|
189
|
+
try {
|
|
190
|
+
await fs.access(candidate, fsConstants.F_OK);
|
|
191
|
+
return dir;
|
|
192
|
+
} catch {
|
|
193
|
+
// keep scanning
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build a PATH string with `extraDirs` prepended (deduped against PATH).
|
|
202
|
+
* Returns the augmented PATH value for use in a child env.
|
|
203
|
+
*/
|
|
204
|
+
function augmentPath(extraDirs) {
|
|
205
|
+
const current = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
206
|
+
const seen = new Set(current);
|
|
207
|
+
const prepend = extraDirs.filter((d) => d && !seen.has(d));
|
|
208
|
+
return [...prepend, ...current].join(path.delimiter);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Ask a yes/no question on the TTY. Empty answer counts as yes. */
|
|
212
|
+
function askYesNo(question) {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
215
|
+
rl.question(question, (answer) => {
|
|
216
|
+
rl.close();
|
|
217
|
+
const a = answer.trim().toLowerCase();
|
|
218
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Run the agent's install command (may contain `&&`, so shell:true). */
|
|
224
|
+
function runInstaller(install) {
|
|
225
|
+
return new Promise((resolve) => {
|
|
226
|
+
const child = spawn(install, { shell: true, stdio: 'inherit' });
|
|
227
|
+
child.on('error', () => resolve(false));
|
|
228
|
+
child.on('exit', (code) => resolve(code === 0));
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Print the resolved install command (plus any fallback) for an agent. */
|
|
233
|
+
function printInstallCommands(agent) {
|
|
234
|
+
console.error(` ${c.cyan}${agent.install}${c.reset}`);
|
|
235
|
+
if (agent.installFallback) {
|
|
236
|
+
console.error(` ${c.dim}or, as a fallback:${c.reset}`);
|
|
237
|
+
console.error(` ${c.cyan}${agent.installFallback}${c.reset}`);
|
|
238
|
+
}
|
|
239
|
+
console.error('');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Ensure the agent's binary is resolvable. If missing:
|
|
244
|
+
* - interactive TTY: offer to run the per-OS installer (with fallback), then
|
|
245
|
+
* re-resolve against PATH + candidate dirs.
|
|
246
|
+
* - non-interactive: print the resolved install command (+ fallback) and
|
|
247
|
+
* exit 127 without running anything.
|
|
248
|
+
*
|
|
249
|
+
* Returns the directory containing the bin (to prepend to the child's PATH) on
|
|
250
|
+
* success. May exit the process on failure or when manual action is needed.
|
|
251
|
+
*/
|
|
252
|
+
async function ensureInstalled(agent) {
|
|
253
|
+
const existing = await resolveBinPath(agent.bin);
|
|
254
|
+
if (existing) return existing;
|
|
255
|
+
|
|
256
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
257
|
+
|
|
258
|
+
if (!interactive) {
|
|
259
|
+
console.error(
|
|
260
|
+
`\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
|
|
261
|
+
);
|
|
262
|
+
console.error(` Install it with:\n`);
|
|
263
|
+
printInstallCommands(agent);
|
|
264
|
+
process.exit(127);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.error(`\n ${c.bold}${agent.name}${c.reset} isn't installed.`);
|
|
268
|
+
const ok = await askYesNo(` Install it now? ${c.dim}[Y/n]${c.reset} `);
|
|
269
|
+
if (!ok) {
|
|
270
|
+
console.error(`\n No problem. Install it yourself with:\n`);
|
|
271
|
+
printInstallCommands(agent);
|
|
272
|
+
process.exit(127);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.error(`\n ${c.dim}Running ${c.reset}${c.cyan}${agent.install}${c.reset}\n`);
|
|
276
|
+
let installed = await runInstaller(agent.install);
|
|
277
|
+
|
|
278
|
+
// Primary failed and a fallback exists — try it once.
|
|
279
|
+
if (!installed && agent.installFallback) {
|
|
280
|
+
console.error(
|
|
281
|
+
`\n ${c.dim}That didn't work. Trying the fallback: ${c.reset}${c.cyan}${agent.installFallback}${c.reset}\n`,
|
|
282
|
+
);
|
|
283
|
+
installed = await runInstaller(agent.installFallback);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!installed) {
|
|
287
|
+
console.error(`\n ${c.red}Install failed.${c.reset} Try it manually:\n`);
|
|
288
|
+
printInstallCommands(agent);
|
|
289
|
+
process.exit(127);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// PATH hardening: the freshly-installed binary is often not on the current
|
|
293
|
+
// process's PATH. Re-resolve against PATH + candidate dirs.
|
|
294
|
+
const found = await resolveBinPath(agent.bin);
|
|
295
|
+
if (found) return found;
|
|
296
|
+
|
|
297
|
+
console.error(
|
|
298
|
+
`\n ${c.dim}Installed ${agent.name}, but it isn't on this shell's PATH yet. ` +
|
|
299
|
+
`Open a new terminal (or add a bin dir to PATH) and re-run \`subconscious ${agent.id}\`.${c.reset}\n`,
|
|
300
|
+
);
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Launch a coding agent against Subconscious. `argv` is everything after the
|
|
306
|
+
* agent name; unknown flags pass straight through to the underlying CLI.
|
|
307
|
+
*/
|
|
308
|
+
export async function runAgent(agent, argv) {
|
|
309
|
+
const { model, rest } = extractModel(argv);
|
|
310
|
+
|
|
311
|
+
const auth = await getApiKey();
|
|
312
|
+
if (!auth) {
|
|
313
|
+
console.error(`\n ${c.red}Not logged in.${c.reset}`);
|
|
314
|
+
console.error(
|
|
315
|
+
` Run ${c.cyan}subconscious login${c.reset} (or set ${c.dim}SUBCONSCIOUS_API_KEY${c.reset}) first.\n`,
|
|
316
|
+
);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const binDir = await ensureInstalled(agent);
|
|
321
|
+
|
|
322
|
+
const ctx = buildContext(auth.key, model);
|
|
323
|
+
const launch = substituteString(agent.launch, ctx);
|
|
324
|
+
const [bin, ...launchArgs] = launch.split(' ').filter(Boolean);
|
|
325
|
+
const envMap = substitute(agent.env, ctx);
|
|
326
|
+
|
|
327
|
+
// Prepend the resolved bin dir + candidate dirs to the child's PATH so the
|
|
328
|
+
// agent (and any subprocess it spawns) resolves correctly this session, even
|
|
329
|
+
// if it was installed into a dir not yet on the parent shell's PATH.
|
|
330
|
+
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
331
|
+
const env = { ...process.env, ...envMap, PATH: augmentPath(extraDirs) };
|
|
332
|
+
const args = [...launchArgs, ...rest];
|
|
333
|
+
|
|
334
|
+
console.log(
|
|
335
|
+
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const child = spawn(bin, args, { stdio: 'inherit', env });
|
|
339
|
+
|
|
340
|
+
child.on('error', (err) => {
|
|
341
|
+
if (err.code === 'ENOENT') {
|
|
342
|
+
console.error(
|
|
343
|
+
`\n ${c.red}Could not launch \`${bin}\`.${c.reset} Install it with:\n`,
|
|
344
|
+
);
|
|
345
|
+
printInstallCommands(agent);
|
|
346
|
+
process.exit(127);
|
|
347
|
+
}
|
|
348
|
+
console.error(`\n ${c.red}${err.message}${c.reset}\n`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// Mirror the child's exit status so callers/scripts see the real result.
|
|
353
|
+
child.on('exit', (code, signal) => {
|
|
354
|
+
if (signal) {
|
|
355
|
+
process.kill(process.pid, signal);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
process.exit(code ?? 0);
|
|
359
|
+
});
|
|
360
|
+
}
|