subconscious-cli 0.2.0 → 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 +40 -9
- package/bin/agents.js +253 -108
- package/bin/registry.generated.json +158 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,15 +24,36 @@ subconscious open-code
|
|
|
24
24
|
point the agent at Subconscious, and exec's the real CLI. Nothing is written to
|
|
25
25
|
the agent's own config — the provider is passed in-memory for that run only.
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
36
57
|
|
|
37
58
|
```bash
|
|
38
59
|
subconscious claude-code --resume
|
|
@@ -49,6 +70,16 @@ subconscious open-code --model subconscious/tim-qwen3.6-27b
|
|
|
49
70
|
export SUBCONSCIOUS_MODEL=subconscious/tim-qwen3.6-27b
|
|
50
71
|
```
|
|
51
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
|
+
|
|
52
83
|
## Auth commands
|
|
53
84
|
|
|
54
85
|
### `login`
|
package/bin/agents.js
CHANGED
|
@@ -3,103 +3,84 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `subconscious <agent>` resolves your saved API key, injects the env vars that
|
|
5
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
|
-
*
|
|
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.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { spawn } from 'node:child_process';
|
|
13
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
12
15
|
import fs from 'node:fs/promises';
|
|
13
16
|
import { constants as fsConstants } from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
14
18
|
import path from 'node:path';
|
|
19
|
+
import readline from 'node:readline';
|
|
15
20
|
import { c } from './colors.js';
|
|
16
21
|
import { getApiKey } from './auth.js';
|
|
17
22
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
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
|
+
}
|
|
23
56
|
|
|
24
57
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* bin — executable we exec and probe on PATH
|
|
30
|
-
* env — (key, model) => extra env vars merged over process.env
|
|
31
|
-
* args — (model) => array of args passed to `bin`
|
|
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.
|
|
32
62
|
*/
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
install
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ANTHROPIC_MODEL: model,
|
|
43
|
-
ANTHROPIC_SMALL_FAST_MODEL: model,
|
|
44
|
-
// Let Subconscious manage context instead of client-side compaction.
|
|
45
|
-
DISABLE_AUTO_COMPACT: 'true',
|
|
46
|
-
}),
|
|
47
|
-
args: () => [],
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: 'OpenCode',
|
|
51
|
-
aliases: ['open-code', 'opencode'],
|
|
52
|
-
install: 'npm i -g opencode-ai',
|
|
53
|
-
bin: 'opencode',
|
|
54
|
-
env: (key, model) => ({
|
|
55
|
-
SUBCONSCIOUS_API_KEY: key,
|
|
56
|
-
// OpenCode deep-merges this at startup; nothing touches ~/.config/opencode.
|
|
57
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
58
|
-
$schema: 'https://opencode.ai/config.json',
|
|
59
|
-
provider: {
|
|
60
|
-
subconscious: {
|
|
61
|
-
npm: '@ai-sdk/openai-compatible',
|
|
62
|
-
name: 'Subconscious',
|
|
63
|
-
options: { baseURL: API_BASE_V1, apiKey: '{env:SUBCONSCIOUS_API_KEY}' },
|
|
64
|
-
models: { [model]: { name: 'Subconscious', tools: true } },
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
model: `subconscious/${model}`,
|
|
68
|
-
}),
|
|
69
|
-
}),
|
|
70
|
-
args: () => [],
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: 'Aider',
|
|
74
|
-
aliases: ['aider'],
|
|
75
|
-
install: 'python -m pip install aider-install && aider-install',
|
|
76
|
-
bin: 'aider',
|
|
77
|
-
env: (key) => ({
|
|
78
|
-
OPENAI_API_BASE: API_BASE_V1,
|
|
79
|
-
OPENAI_API_KEY: key,
|
|
80
|
-
}),
|
|
81
|
-
args: (model) => ['--model', `openai/${model}`],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'Codex CLI',
|
|
85
|
-
aliases: ['codex'],
|
|
86
|
-
install: 'npm i -g @openai/codex',
|
|
87
|
-
bin: 'codex',
|
|
88
|
-
env: (key) => ({ SUBCONSCIOUS_API_KEY: key }),
|
|
89
|
-
// `-c model=…` (not `--model`, which is codex's Ollama shortcut).
|
|
90
|
-
args: (model) => [
|
|
91
|
-
'-c', 'model_providers.subconscious.name=Subconscious',
|
|
92
|
-
'-c', `model_providers.subconscious.base_url=${API_BASE_V1}`,
|
|
93
|
-
'-c', 'model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY',
|
|
94
|
-
'-c', 'model_provider=subconscious',
|
|
95
|
-
'-c', `model=${model}`,
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
];
|
|
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
|
+
}
|
|
99
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
|
+
});
|
|
100
80
|
const BY_ALIAS = new Map();
|
|
101
81
|
for (const agent of AGENTS) {
|
|
102
|
-
|
|
82
|
+
BY_ALIAS.set(agent.id, agent);
|
|
83
|
+
for (const alias of agent.aliases || []) BY_ALIAS.set(alias, agent);
|
|
103
84
|
}
|
|
104
85
|
|
|
105
86
|
export function resolveAgent(name) {
|
|
@@ -107,16 +88,27 @@ export function resolveAgent(name) {
|
|
|
107
88
|
}
|
|
108
89
|
|
|
109
90
|
export function agentList() {
|
|
110
|
-
return AGENTS.map((a) => ({ name: a.name, alias: a.
|
|
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` };
|
|
111
103
|
}
|
|
112
104
|
|
|
113
105
|
/**
|
|
114
106
|
* Pull a `--model <value>` / `--model=<value>` flag out of the passthrough
|
|
115
107
|
* args (so it sets the Subconscious model rather than reaching the agent).
|
|
116
|
-
* Falls back to SUBCONSCIOUS_MODEL, then the default.
|
|
108
|
+
* Falls back to SUBCONSCIOUS_MODEL, then the registry default.
|
|
117
109
|
*/
|
|
118
110
|
function extractModel(argv) {
|
|
119
|
-
let model = process.env.SUBCONSCIOUS_MODEL?.trim() ||
|
|
111
|
+
let model = process.env.SUBCONSCIOUS_MODEL?.trim() || DEFAULTS.model;
|
|
120
112
|
const rest = [];
|
|
121
113
|
for (let i = 0; i < argv.length; i++) {
|
|
122
114
|
const a = argv[i];
|
|
@@ -137,25 +129,176 @@ function extractModel(argv) {
|
|
|
137
129
|
return { model, rest };
|
|
138
130
|
}
|
|
139
131
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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();
|
|
147
186
|
for (const dir of dirs) {
|
|
148
187
|
for (const ext of exts) {
|
|
149
188
|
const candidate = path.join(dir, bin + ext);
|
|
150
189
|
try {
|
|
151
190
|
await fs.access(candidate, fsConstants.F_OK);
|
|
152
|
-
return
|
|
191
|
+
return dir;
|
|
153
192
|
} catch {
|
|
154
193
|
// keep scanning
|
|
155
194
|
}
|
|
156
195
|
}
|
|
157
196
|
}
|
|
158
|
-
return
|
|
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);
|
|
159
302
|
}
|
|
160
303
|
|
|
161
304
|
/**
|
|
@@ -174,30 +317,32 @@ export async function runAgent(agent, argv) {
|
|
|
174
317
|
process.exit(1);
|
|
175
318
|
}
|
|
176
319
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
process.exit(127);
|
|
184
|
-
}
|
|
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);
|
|
185
326
|
|
|
186
|
-
|
|
187
|
-
|
|
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];
|
|
188
333
|
|
|
189
334
|
console.log(
|
|
190
335
|
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
191
336
|
);
|
|
192
337
|
|
|
193
|
-
const child = spawn(
|
|
338
|
+
const child = spawn(bin, args, { stdio: 'inherit', env });
|
|
194
339
|
|
|
195
340
|
child.on('error', (err) => {
|
|
196
341
|
if (err.code === 'ENOENT') {
|
|
197
342
|
console.error(
|
|
198
|
-
`\n ${c.red}Could not launch \`${
|
|
343
|
+
`\n ${c.red}Could not launch \`${bin}\`.${c.reset} Install it with:\n`,
|
|
199
344
|
);
|
|
200
|
-
|
|
345
|
+
printInstallCommands(agent);
|
|
201
346
|
process.exit(127);
|
|
202
347
|
}
|
|
203
348
|
console.error(`\n ${c.red}${err.message}${c.reset}\n`);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_generated": "Source of truth: agents/registry.json. Do not edit by hand.",
|
|
3
|
+
"defaults": {
|
|
4
|
+
"model": "subconscious/tim-qwen3.6-27b",
|
|
5
|
+
"baseUrl": "https://api.subconscious.dev",
|
|
6
|
+
"baseUrlV1": "https://api.subconscious.dev/v1",
|
|
7
|
+
"keyPlaceholder": "your_key",
|
|
8
|
+
"platformUrl": "https://subconscious.dev/platform"
|
|
9
|
+
},
|
|
10
|
+
"agents": [
|
|
11
|
+
{
|
|
12
|
+
"id": "claude-code",
|
|
13
|
+
"exampleDir": "claude_code",
|
|
14
|
+
"name": "Claude Code",
|
|
15
|
+
"aliases": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claudecode"
|
|
18
|
+
],
|
|
19
|
+
"displayName": "Claude Code + Subconscious",
|
|
20
|
+
"description": "Run Claude Code on your hosted Subconscious model — no code changes, just env vars.",
|
|
21
|
+
"framework": "Claude Code",
|
|
22
|
+
"language": "shell",
|
|
23
|
+
"category": "coding-agent",
|
|
24
|
+
"tags": [
|
|
25
|
+
"local",
|
|
26
|
+
"cli",
|
|
27
|
+
"agents"
|
|
28
|
+
],
|
|
29
|
+
"protocol": "anthropic",
|
|
30
|
+
"homepage": "https://docs.anthropic.com/en/docs/claude-code",
|
|
31
|
+
"bin": "claude",
|
|
32
|
+
"install": {
|
|
33
|
+
"darwin": "curl -fsSL https://claude.ai/install.sh | bash",
|
|
34
|
+
"linux": "curl -fsSL https://claude.ai/install.sh | bash",
|
|
35
|
+
"win32": "powershell -ExecutionPolicy Bypass -Command \"irm https://claude.ai/install.ps1 | iex\"",
|
|
36
|
+
"fallback": "npm i -g @anthropic-ai/claude-code"
|
|
37
|
+
},
|
|
38
|
+
"launch": "claude",
|
|
39
|
+
"env": {
|
|
40
|
+
"ANTHROPIC_BASE_URL": "{baseUrl}",
|
|
41
|
+
"ANTHROPIC_AUTH_TOKEN": "{apiKey}",
|
|
42
|
+
"ANTHROPIC_MODEL": "{model}",
|
|
43
|
+
"ANTHROPIC_SMALL_FAST_MODEL": "{model}",
|
|
44
|
+
"DISABLE_AUTO_COMPACT": "true"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "opencode",
|
|
49
|
+
"exampleDir": "opencode",
|
|
50
|
+
"name": "OpenCode",
|
|
51
|
+
"aliases": [
|
|
52
|
+
"open-code"
|
|
53
|
+
],
|
|
54
|
+
"displayName": "OpenCode + Subconscious",
|
|
55
|
+
"description": "Run OpenCode on your hosted Subconscious model — registered as a custom provider, injected non-invasively (nothing written to your OpenCode config).",
|
|
56
|
+
"framework": "OpenCode",
|
|
57
|
+
"language": "shell",
|
|
58
|
+
"category": "coding-agent",
|
|
59
|
+
"tags": [
|
|
60
|
+
"local",
|
|
61
|
+
"cli",
|
|
62
|
+
"agents"
|
|
63
|
+
],
|
|
64
|
+
"protocol": "openai",
|
|
65
|
+
"homepage": "https://opencode.ai",
|
|
66
|
+
"bin": "opencode",
|
|
67
|
+
"install": {
|
|
68
|
+
"darwin": "npm i -g opencode-ai",
|
|
69
|
+
"linux": "npm i -g opencode-ai",
|
|
70
|
+
"win32": "npm i -g opencode-ai"
|
|
71
|
+
},
|
|
72
|
+
"launch": "opencode",
|
|
73
|
+
"configEnv": "OPENCODE_CONFIG_CONTENT",
|
|
74
|
+
"env": {
|
|
75
|
+
"SUBCONSCIOUS_API_KEY": "{apiKey}",
|
|
76
|
+
"OPENCODE_CONFIG_CONTENT": {
|
|
77
|
+
"$json": {
|
|
78
|
+
"$schema": "https://opencode.ai/config.json",
|
|
79
|
+
"provider": {
|
|
80
|
+
"subconscious": {
|
|
81
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
82
|
+
"name": "Subconscious",
|
|
83
|
+
"options": {
|
|
84
|
+
"baseURL": "{baseUrlV1}",
|
|
85
|
+
"apiKey": "{env:SUBCONSCIOUS_API_KEY}"
|
|
86
|
+
},
|
|
87
|
+
"models": {
|
|
88
|
+
"{model}": {
|
|
89
|
+
"name": "Subconscious",
|
|
90
|
+
"tools": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"model": "subconscious/{model}"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"id": "aider",
|
|
102
|
+
"exampleDir": "aider",
|
|
103
|
+
"name": "Aider",
|
|
104
|
+
"aliases": [],
|
|
105
|
+
"displayName": "Aider + Subconscious",
|
|
106
|
+
"description": "Run Aider on your hosted Subconscious model — no code changes, just env vars.",
|
|
107
|
+
"framework": "Aider",
|
|
108
|
+
"language": "shell",
|
|
109
|
+
"category": "coding-agent",
|
|
110
|
+
"tags": [
|
|
111
|
+
"local",
|
|
112
|
+
"cli",
|
|
113
|
+
"agents"
|
|
114
|
+
],
|
|
115
|
+
"protocol": "openai",
|
|
116
|
+
"homepage": "https://aider.chat",
|
|
117
|
+
"bin": "aider",
|
|
118
|
+
"install": {
|
|
119
|
+
"darwin": "python3 -m pip install aider-install && aider-install",
|
|
120
|
+
"linux": "python3 -m pip install aider-install && aider-install",
|
|
121
|
+
"win32": "python -m pip install aider-install && aider-install"
|
|
122
|
+
},
|
|
123
|
+
"launch": "aider --model openai/{model}",
|
|
124
|
+
"env": {
|
|
125
|
+
"OPENAI_API_BASE": "{baseUrlV1}",
|
|
126
|
+
"OPENAI_API_KEY": "{apiKey}"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"id": "codex",
|
|
131
|
+
"exampleDir": "codex",
|
|
132
|
+
"name": "Codex CLI",
|
|
133
|
+
"aliases": [],
|
|
134
|
+
"displayName": "Codex + Subconscious",
|
|
135
|
+
"description": "Run the Codex CLI on your hosted Subconscious model via a custom OpenAI-compatible provider.",
|
|
136
|
+
"framework": "Codex",
|
|
137
|
+
"language": "shell",
|
|
138
|
+
"category": "coding-agent",
|
|
139
|
+
"tags": [
|
|
140
|
+
"local",
|
|
141
|
+
"cli",
|
|
142
|
+
"agents"
|
|
143
|
+
],
|
|
144
|
+
"protocol": "openai",
|
|
145
|
+
"homepage": "https://developers.openai.com/codex",
|
|
146
|
+
"bin": "codex",
|
|
147
|
+
"install": {
|
|
148
|
+
"darwin": "npm i -g @openai/codex",
|
|
149
|
+
"linux": "npm i -g @openai/codex",
|
|
150
|
+
"win32": "npm i -g @openai/codex"
|
|
151
|
+
},
|
|
152
|
+
"launch": "codex -c model_providers.subconscious.name=Subconscious -c model_providers.subconscious.base_url={baseUrlV1} -c model_providers.subconscious.env_key=SUBCONSCIOUS_API_KEY -c model_provider=subconscious -c model={model}",
|
|
153
|
+
"env": {
|
|
154
|
+
"SUBCONSCIOUS_API_KEY": "{apiKey}"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
package/package.json
CHANGED