subconscious-cli 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/agents.js CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Coding-agent launcher.
3
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.
4
+ * `subc <agent>` resolves your saved API key and runs the packaged integration.
5
+ * Terminal agents launch ephemerally; IDE/config-based
6
+ * agents and Pi persist a surgical integration via `subc <agent> install`.
7
7
  *
8
8
  * There is NO hardcoded agent data here: everything is read from
9
9
  * `registry.generated.json` (shipped under `bin/`), which is generated from the
@@ -17,14 +17,21 @@ import { constants as fsConstants } from 'node:fs';
17
17
  import os from 'node:os';
18
18
  import path from 'node:path';
19
19
  import readline from 'node:readline';
20
+ import { fileURLToPath } from 'node:url';
20
21
  import { c } from './colors.js';
21
22
  import { getApiKey } from './auth.js';
23
+ import { profileSettingsForAgent, resolvedProfileValues } from './profiles.js';
22
24
 
23
25
  // --- Registry (single source of truth, generated copy shipped in the package).
24
26
  const registry = JSON.parse(
25
27
  readFileSync(new URL('./registry.generated.json', import.meta.url), 'utf-8'),
26
28
  );
27
29
  const DEFAULTS = registry.defaults;
30
+ const SUPPORTED_MODELS =
31
+ Array.isArray(DEFAULTS.models) && DEFAULTS.models.length
32
+ ? DEFAULTS.models
33
+ : [DEFAULTS.model];
34
+ const RUNBOOK_DIR = fileURLToPath(new URL('./runbook/', import.meta.url));
28
35
 
29
36
  // --- Token substitution — same rules as scripts/lib/registry.js.
30
37
  // Replaces {apiKey}, {model}, {baseUrl}, {baseUrlV1}. NEVER touches {env:...}.
@@ -73,13 +80,16 @@ function resolveInstall(install) {
73
80
  // --- Build the in-memory registry + alias index.
74
81
  // Each agent gets a resolved per-OS `install` (string) plus optional
75
82
  // `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
- });
83
+ const AGENTS = registry.agents
84
+ .filter((agent) => agent.cli !== false)
85
+ .map((agent) => {
86
+ const { command, fallback } = resolveInstall(agent.install);
87
+ return { ...agent, install: command, installFallback: fallback };
88
+ });
80
89
  const BY_ALIAS = new Map();
81
90
  for (const agent of AGENTS) {
82
91
  BY_ALIAS.set(agent.id, agent);
92
+ if (agent.command) BY_ALIAS.set(agent.command, agent);
83
93
  for (const alias of agent.aliases || []) BY_ALIAS.set(alias, agent);
84
94
  }
85
95
 
@@ -88,7 +98,197 @@ export function resolveAgent(name) {
88
98
  }
89
99
 
90
100
  export function agentList() {
91
- return AGENTS.map((a) => ({ name: a.name, alias: a.id }));
101
+ return AGENTS.map((a) => ({
102
+ name: a.name,
103
+ alias: a.command || a.id,
104
+ action: a.runbook?.mode === 'setup' ? 'Configure' : 'Launch',
105
+ }));
106
+ }
107
+
108
+ const SETUP_ACTIONS = new Set(['install', 'status', 'uninstall']);
109
+ const DROPPED_SETUP_HELPERS = new Set(['use', 'env', 'unset']);
110
+
111
+ export function agentSetupActions(agent) {
112
+ return Array.isArray(agent.runbook?.setupActions) ? agent.runbook.setupActions : [];
113
+ }
114
+
115
+ export function agentCommandName(agent) {
116
+ return agent.command || agent.id;
117
+ }
118
+
119
+ export function parseAgentAction(agent, argv = []) {
120
+ const command = agentCommandName(agent);
121
+ const first = argv[0];
122
+ const actions = agentSetupActions(agent);
123
+
124
+ if (DROPPED_SETUP_HELPERS.has(first)) {
125
+ throw new Error(
126
+ `${agent.name} no longer supports '${first}'. Launch with subc ${command}.`,
127
+ );
128
+ }
129
+
130
+ if (SETUP_ACTIONS.has(first)) {
131
+ if (!agent.runbook?.setupScript || !actions.includes(first)) {
132
+ if (first === 'install') {
133
+ const uninstallHint = actions.includes('uninstall')
134
+ ? ` To remove leftover files: subc ${command} uninstall.`
135
+ : '';
136
+ throw new Error(
137
+ `${agent.name} is launch-only. Run subc ${command}.${uninstallHint}`,
138
+ );
139
+ }
140
+ throw new Error(
141
+ `${agent.name} does not support '${first}'. Try subc ${command} help.`,
142
+ );
143
+ }
144
+ return { action: first, args: argv };
145
+ }
146
+
147
+ if (agent.runbook?.mode === 'setup') {
148
+ return { action: 'install', args: argv };
149
+ }
150
+
151
+ return { action: 'launch', args: argv };
152
+ }
153
+
154
+ const AGENT_HELP = {
155
+ 'claude-code': {
156
+ usage: 'subc [-p NAME] claude [help|status|uninstall] [Claude arguments...]',
157
+ behavior:
158
+ 'Launches Claude Code with the active Subconscious profile and model picker. Persistent files are leftover-only: subc claude uninstall.',
159
+ options: [
160
+ ['help', 'Show this help'],
161
+ ['status', 'Inspect leftover ~/.claude/subconscious-gateway.env'],
162
+ ['uninstall', 'Remove leftover ~/.claude/subconscious-gateway.env'],
163
+ ['--model MODEL', 'Override the profile model for this launch'],
164
+ ['--compact-window N', 'Override the Claude auto-compact window'],
165
+ ['--max-context-tokens N', 'Override the maximum context tokens'],
166
+ ['-- ARGS...', 'Pass remaining arguments to Claude Code'],
167
+ ],
168
+ },
169
+ codex: {
170
+ usage: 'subc [-p NAME] codex [help|install|status|uninstall] [Codex arguments...]',
171
+ behavior:
172
+ 'Launches Codex with a temporary Subconscious provider catalog. Compaction hooks are merged into ~/.codex/hooks.json and removed with subc codex uninstall.',
173
+ options: [
174
+ ['help', 'Show this help'],
175
+ ['install', 'Install only the Subconscious compaction hooks'],
176
+ ['status', 'Inspect the installed compaction hooks'],
177
+ ['uninstall', 'Remove only the Subconscious Codex hooks'],
178
+ ['--model MODEL', 'Override the profile model for this launch'],
179
+ ['--context-window N', 'Override catalog context_window'],
180
+ ['--max-context-window N', 'Override catalog max_context_window'],
181
+ ['--auto-compact-token-limit N', 'Override the automatic compaction threshold'],
182
+ ['--reasoning-effort LEVEL', 'Use none, low, medium, high, or max'],
183
+ ['--external-tools', 'Enable Codex apps/plugins for this launch'],
184
+ ['--subagents', 'Use the pinned legacy Codex subagent mode'],
185
+ ['-- ARGS...', 'Pass remaining arguments to Codex'],
186
+ ],
187
+ },
188
+ opencode: {
189
+ usage: 'subc [-p NAME] opencode [help|status|uninstall] [OpenCode arguments...]',
190
+ behavior:
191
+ 'Launches OpenCode with an ephemeral provider containing every Subconscious model. Persistent files are leftover-only: subc opencode uninstall.',
192
+ options: [
193
+ ['help', 'Show this help'],
194
+ ['status', 'Inspect leftover OpenCode Subconscious config'],
195
+ ['uninstall', 'Remove only the Subconscious OpenCode provider and plugin'],
196
+ ['--model MODEL', 'Override the profile model for this launch'],
197
+ ['ARGS...', 'Pass arguments directly to OpenCode'],
198
+ ],
199
+ },
200
+ cursor: {
201
+ usage: 'subc [-p NAME] cursor [help|install|status|uninstall]',
202
+ behavior:
203
+ 'Manages Cursor correlation hooks; model endpoint setup is completed in Cursor Settings.',
204
+ options: [
205
+ ['help', 'Show this help'],
206
+ ['install', 'Install or update the Cursor hooks (default action)'],
207
+ ['status', 'Inspect the installed hook configuration'],
208
+ ['uninstall', 'Remove only the Subconscious Cursor hooks'],
209
+ ],
210
+ },
211
+ copilot: {
212
+ usage: 'subc [-p NAME] copilot [help|install|status|uninstall]',
213
+ behavior: 'Manages the VS Code model provider and Copilot correlation hooks.',
214
+ options: [
215
+ ['help', 'Show this help'],
216
+ ['install', 'Install or update the provider and hooks (default action)'],
217
+ ['status', 'Inspect the installed provider and hooks'],
218
+ ['uninstall', 'Remove the Subconscious provider and hooks'],
219
+ ],
220
+ },
221
+ pi: {
222
+ usage: 'subc [-p NAME] pi [help|install|status|uninstall] [Pi arguments...]',
223
+ behavior:
224
+ 'Launches Pi read-only against the provider previously configured by subc pi install.',
225
+ options: [
226
+ ['help', 'Show this help'],
227
+ ['install', 'Merge the Subconscious provider into ~/.pi/agent/models.json'],
228
+ ['status', 'Inspect the persistent Pi provider'],
229
+ ['uninstall', 'Remove only the Subconscious Pi provider and extension'],
230
+ ['--model MODEL', 'Override the profile model for this launch'],
231
+ ['ARGS...', 'Pass arguments directly to Pi'],
232
+ ],
233
+ },
234
+ };
235
+
236
+ export function isAgentHelpRequest(argv = []) {
237
+ return ['help', '-h', '--help'].includes(argv[0]);
238
+ }
239
+
240
+ function displayProfileValue(setting, value, values) {
241
+ if (setting.type === 'secret') {
242
+ if (value) return '(set)';
243
+ return setting.key !== 'API_KEY' && values.API_KEY ? '(shared key)' : '(not set)';
244
+ }
245
+ return value || '(auto)';
246
+ }
247
+
248
+ export function printAgentHelp(agent, profile) {
249
+ const details = AGENT_HELP[agent.id] || {
250
+ usage: `subc [--profile NAME] ${agent.command || agent.id} [arguments...]`,
251
+ behavior: agent.description,
252
+ options: [],
253
+ };
254
+ const settings = profileSettingsForAgent(agent.id);
255
+ const values = resolvedProfileValues(profile);
256
+ const optionWidth = Math.max(0, ...details.options.map(([option]) => option.length));
257
+ const settingWidth = Math.max(0, ...settings.map((setting) => setting.key.length));
258
+
259
+ console.log(`\n ${c.bold}${agent.name} + Subconscious${c.reset}\n`);
260
+ console.log(` ${details.behavior}\n`);
261
+ console.log(` ${c.bold}Usage${c.reset}\n ${details.usage}\n`);
262
+ if (details.options.length) {
263
+ console.log(` ${c.bold}Commands and options${c.reset}`);
264
+ for (const [option, description] of details.options) {
265
+ console.log(` ${c.cyan}${option.padEnd(optionWidth)}${c.reset} ${description}`);
266
+ }
267
+ console.log();
268
+ }
269
+ console.log(` ${c.bold}Profile settings${c.reset} ${c.dim}(${profile?.name || 'default'})${c.reset}`);
270
+ for (const setting of settings) {
271
+ const value = displayProfileValue(setting, values[setting.key], values);
272
+ console.log(` ${c.cyan}${setting.key.padEnd(settingWidth)}${c.reset} ${value}`);
273
+ console.log(` ${' '.repeat(settingWidth)} ${c.dim}${setting.description}${c.reset}`);
274
+ }
275
+ const command = agentCommandName(agent);
276
+ const profileFlag = profile?.name || 'default';
277
+ console.log(
278
+ `\n Edit the env file with ${c.cyan}subc -p ${profileFlag} config edit${c.reset}.`,
279
+ );
280
+ const actions = agentSetupActions(agent);
281
+ if (actions.includes('install')) {
282
+ console.log(
283
+ ` Install the persistent integration with ${c.cyan}subc ${command} install${c.reset}.`,
284
+ );
285
+ }
286
+ if (actions.includes('uninstall')) {
287
+ console.log(
288
+ ` Remove it with ${c.cyan}subc ${command} uninstall${c.reset}.`,
289
+ );
290
+ }
291
+ console.log();
92
292
  }
93
293
 
94
294
  /**
@@ -97,8 +297,12 @@ export function agentList() {
97
297
  * baseUrl — SUBCONSCIOUS_BASE_URL → registry default
98
298
  * baseUrlV1 — `${baseUrl}/v1` (so an override flows to both)
99
299
  */
100
- function buildContext(apiKey, model) {
101
- const baseUrl = process.env.SUBCONSCIOUS_BASE_URL?.trim() || DEFAULTS.baseUrl;
300
+ function buildContext(apiKey, model, profile) {
301
+ const baseUrl = (
302
+ process.env.SUBCONSCIOUS_BASE_URL?.trim() ||
303
+ profile?.values?.GATEWAY_URL?.trim() ||
304
+ DEFAULTS.baseUrl
305
+ ).replace(/\/+$/, '');
102
306
  return { apiKey, model, baseUrl, baseUrlV1: `${baseUrl}/v1` };
103
307
  }
104
308
 
@@ -107,8 +311,9 @@ function buildContext(apiKey, model) {
107
311
  * args (so it sets the Subconscious model rather than reaching the agent).
108
312
  * Falls back to SUBCONSCIOUS_MODEL, then the registry default.
109
313
  */
110
- function extractModel(argv) {
111
- let model = process.env.SUBCONSCIOUS_MODEL?.trim() || DEFAULTS.model;
314
+ function extractModel(argv, profile) {
315
+ let model =
316
+ process.env.SUBCONSCIOUS_MODEL?.trim() || profile?.values?.MODEL?.trim() || DEFAULTS.model;
112
317
  const rest = [];
113
318
  for (let i = 0; i < argv.length; i++) {
114
319
  const a = argv[i];
@@ -253,6 +458,16 @@ async function ensureInstalled(agent) {
253
458
  const existing = await resolveBinPath(agent.bin);
254
459
  if (existing) return existing;
255
460
 
461
+ // Agents without an installer are launch-only. Their setup integration may
462
+ // configure the provider, but `subc <agent>` must never install the binary.
463
+ if (!agent.install) {
464
+ console.error(
465
+ `\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
466
+ );
467
+ console.error(` Install ${agent.name} separately, then re-run ${c.cyan}subc ${agent.command || agent.id}${c.reset}.\n`);
468
+ process.exit(127);
469
+ }
470
+
256
471
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
257
472
 
258
473
  if (!interactive) {
@@ -296,30 +511,217 @@ async function ensureInstalled(agent) {
296
511
 
297
512
  console.error(
298
513
  `\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`,
514
+ `Open a new terminal (or add a bin dir to PATH) and re-run \`subc ${agent.command || agent.id}\`.${c.reset}\n`,
300
515
  );
301
516
  process.exit(0);
302
517
  }
303
518
 
519
+ /** Resolve and validate a script inside the packaged runbook directory. */
520
+ function runbookScriptPath(agent, relativeScript = agent.runbook.script) {
521
+ const script = path.resolve(RUNBOOK_DIR, relativeScript);
522
+ const relative = path.relative(RUNBOOK_DIR, script);
523
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
524
+ throw new Error(`Invalid runbook script path for ${agent.name}`);
525
+ }
526
+ return script;
527
+ }
528
+
529
+ /** Spawn a runbook script and mirror its exit status/signals. */
530
+ function spawnRunbook(agent, args, env, relativeScript) {
531
+ return new Promise((resolve, reject) => {
532
+ const script = runbookScriptPath(agent, relativeScript);
533
+ const child = spawn('bash', [script, ...args], { stdio: 'inherit', env });
534
+
535
+ child.on('error', (error) => {
536
+ if (error.code === 'ENOENT') {
537
+ reject(
538
+ new Error('These coding-agent integrations require `bash`, but it was not found on PATH.'),
539
+ );
540
+ return;
541
+ }
542
+ reject(error);
543
+ });
544
+
545
+ child.on('exit', (code, signal) => {
546
+ if (signal) {
547
+ process.kill(process.pid, signal);
548
+ return;
549
+ }
550
+ if (code) process.exitCode = code;
551
+ resolve(code ?? 0);
552
+ });
553
+ });
554
+ }
555
+
556
+ function isSetupWithoutAuth(argv) {
557
+ return ['status', 'uninstall', '-h', '--help', 'help'].includes(argv[0]);
558
+ }
559
+
560
+ function optionValue(argv, name) {
561
+ const index = argv.indexOf(name);
562
+ return index >= 0 ? argv[index + 1]?.trim() || null : null;
563
+ }
564
+
565
+ function agentApiKeySetting(agent) {
566
+ return profileSettingsForAgent(agent.id).find(
567
+ (setting) => setting.key !== 'API_KEY' && setting.key.endsWith('_API_KEY'),
568
+ );
569
+ }
570
+
571
+ export async function getAgentApiKey(profile, agent) {
572
+ const specificSetting = agentApiKeySetting(agent);
573
+ const specificKey = specificSetting?.key;
574
+ const specificEnvKey = specificKey && process.env[specificKey]?.trim();
575
+ if (specificEnvKey) return { key: specificEnvKey, source: `${specificKey} env var` };
576
+
577
+ const sharedEnvKey = process.env.SUBCONSCIOUS_API_KEY?.trim();
578
+ if (sharedEnvKey) {
579
+ return { key: sharedEnvKey, source: 'SUBCONSCIOUS_API_KEY env var' };
580
+ }
581
+
582
+ const profileKey = specificKey && profile?.values?.[specificKey]?.trim();
583
+ if (profileKey) return { key: profileKey, source: profile.path };
584
+
585
+ return getApiKey(profile);
586
+ }
587
+
588
+ async function requireApiKey(profile, agent) {
589
+ const auth = await getAgentApiKey(profile, agent);
590
+ if (auth) return auth.key;
591
+ const login =
592
+ profile?.name && profile.name !== 'default'
593
+ ? `subc --profile ${profile.name} login`
594
+ : 'subc login';
595
+
596
+ console.error(`\n ${c.red}Not logged in.${c.reset}`);
597
+ console.error(
598
+ ` Run ${c.cyan}${login}${c.reset} (or set ${c.dim}SUBCONSCIOUS_API_KEY${c.reset}) first.\n`,
599
+ );
600
+ process.exitCode = 1;
601
+ return null;
602
+ }
603
+
604
+ const CLAUDE_MODEL_PICKER_KEYS = [
605
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
606
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_NAME',
607
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION',
608
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
609
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_NAME',
610
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION',
611
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
612
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME',
613
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION',
614
+ ];
615
+
616
+ function claudeModelPickerEnv(agent, ctx) {
617
+ if (agent.id !== 'claude-code') return {};
618
+ const configured = substitute(agent.env || {}, ctx);
619
+ return Object.fromEntries(
620
+ CLAUDE_MODEL_PICKER_KEYS.map((key) => [key, configured[key]]).filter(([, value]) => value),
621
+ );
622
+ }
623
+
624
+ export function runbookEnv(apiKey, model, binDir, profile, agent) {
625
+ const ctx = buildContext(apiKey, model, profile);
626
+ const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
627
+ const specificApiKey = agentApiKeySetting(agent)?.key;
628
+ return {
629
+ ...claudeModelPickerEnv(agent, ctx),
630
+ ...(profile?.values || {}),
631
+ ...process.env,
632
+ GATEWAY_URL: ctx.baseUrl,
633
+ API_KEY: apiKey,
634
+ ...(specificApiKey ? { [specificApiKey]: apiKey } : {}),
635
+ MODEL: model,
636
+ SUBCONSCIOUS_MODELS: SUPPORTED_MODELS.join('\n'),
637
+ SUBC_ENV_FILE: os.devNull,
638
+ PATH: augmentPath(extraDirs),
639
+ };
640
+ }
641
+
642
+ async function runRunbookSetup(agent, argv, profile, relativeScript = agent.runbook.script) {
643
+ if (isSetupWithoutAuth(argv)) {
644
+ return spawnRunbook(agent, argv, {
645
+ ...(profile?.values || {}),
646
+ ...process.env,
647
+ SUBC_ENV_FILE: os.devNull,
648
+ }, relativeScript);
649
+ }
650
+
651
+ const { model, rest } = extractModel(argv, profile);
652
+ const apiKey = optionValue(rest, '--api-key') || (await requireApiKey(profile, agent));
653
+ if (!apiKey) return 1;
654
+ const ctx = buildContext(apiKey, model, profile);
655
+ const authArgs = substitute(agent.runbook.authArgs || [], ctx);
656
+
657
+ console.log(
658
+ ` ${c.dim}Configuring ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}for Subconscious (${model})${c.reset}\n`,
659
+ );
660
+ const code = await spawnRunbook(
661
+ agent,
662
+ [...authArgs, ...rest],
663
+ runbookEnv(apiKey, model, undefined, profile, agent),
664
+ relativeScript,
665
+ );
666
+ const installed = !['status', 'uninstall'].includes(rest[0]);
667
+ if (code !== 0 || !installed) return code;
668
+
669
+ if (agent.id === 'pi') {
670
+ console.log(`\n ${c.dim}Start a fresh session with ${c.reset}${c.cyan}subc pi${c.reset}${c.dim}.${c.reset}\n`);
671
+ }
672
+ return code;
673
+ }
674
+
304
675
  /**
305
676
  * Launch a coding agent against Subconscious. `argv` is everything after the
306
677
  * agent name; unknown flags pass straight through to the underlying CLI.
307
678
  */
308
- export async function runAgent(agent, argv) {
309
- const { model, rest } = extractModel(argv);
679
+ export async function runAgent(agent, argv, options = {}) {
680
+ const profile = options.profile;
681
+ if (isAgentHelpRequest(argv)) {
682
+ printAgentHelp(agent, profile);
683
+ return 0;
684
+ }
310
685
 
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`,
686
+ const parsed = parseAgentAction(agent, argv);
687
+ if (parsed.action !== 'launch') {
688
+ if (!agent.runbook?.setupScript) {
689
+ throw new Error(`No persistent integration is available for ${agent.name}`);
690
+ }
691
+ const setupArgs =
692
+ parsed.args[0] === parsed.action ? parsed.args : [parsed.action, ...parsed.args];
693
+ const code = await runRunbookSetup(
694
+ agent,
695
+ setupArgs,
696
+ profile,
697
+ agent.runbook.setupScript,
316
698
  );
317
- process.exit(1);
699
+ if (code === 0) {
700
+ const message =
701
+ parsed.action === 'status'
702
+ ? `${agent.name} status check complete.`
703
+ : parsed.action === 'uninstall'
704
+ ? `${agent.name} integration removed.`
705
+ : `${agent.name} setup complete.`;
706
+ console.log(`\n ${c.green}${c.bold}✓ ${message}${c.reset}\n`);
707
+ }
708
+ return code;
318
709
  }
319
710
 
711
+ const { model, rest } = extractModel(argv, profile);
712
+ const apiKey = await requireApiKey(profile, agent);
713
+ if (!apiKey) return 1;
714
+
320
715
  const binDir = await ensureInstalled(agent);
321
716
 
322
- const ctx = buildContext(auth.key, model);
717
+ if (agent.runbook?.mode === 'launch') {
718
+ console.log(
719
+ ` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
720
+ );
721
+ return spawnRunbook(agent, rest, runbookEnv(apiKey, model, binDir, profile, agent));
722
+ }
723
+
724
+ const ctx = buildContext(apiKey, model, profile);
323
725
  const launch = substituteString(agent.launch, ctx);
324
726
  const [bin, ...launchArgs] = launch.split(' ').filter(Boolean);
325
727
  const envMap = substitute(agent.env, ctx);
@@ -328,7 +730,12 @@ export async function runAgent(agent, argv) {
328
730
  // agent (and any subprocess it spawns) resolves correctly this session, even
329
731
  // if it was installed into a dir not yet on the parent shell's PATH.
330
732
  const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
331
- const env = { ...process.env, ...envMap, PATH: augmentPath(extraDirs) };
733
+ const env = {
734
+ ...envMap,
735
+ ...(profile?.values || {}),
736
+ ...process.env,
737
+ PATH: augmentPath(extraDirs),
738
+ };
332
739
  const args = [...launchArgs, ...rest];
333
740
 
334
741
  console.log(