subconscious-cli 4.0.6 → 4.0.7

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 CHANGED
@@ -31,9 +31,13 @@ subc pi
31
31
  subc <agent> uninstall
32
32
  ```
33
33
 
34
- Top-level help (`subc`, `subc help`, or `subc --help`) displays the
35
- Subconscious logo as portable ASCII art. Set `NO_COLOR=1` for a
36
- monochrome version; redirected output automatically uses a plain wordmark.
34
+ Running `subc` with no arguments in a terminal opens the native Go TUI. Use
35
+ the arrow keys and Enter to launch agents or manage the active profile, `p` to
36
+ switch profiles, and `q` to quit. The menu includes dedicated **Create profile**,
37
+ **Set default model**, and **Update base URL** actions. Model and URL changes
38
+ are validated and saved to the active profile without leaving the TUI.
39
+ `subc help` and `subc --help` continue to print script-friendly command help.
40
+ Non-interactive `subc` also prints regular help.
37
41
 
38
42
  Every command accepts `help` as a subcommand. These only read the selected
39
43
  profile; they do not authenticate, install, configure, or launch anything:
@@ -135,6 +139,7 @@ DeepSeek Harness context and output settings used by the packaged runbook script
135
139
 
136
140
  ```bash
137
141
  subc config # list every profile and its file path
142
+ subc -p staging config create # create a profile with default settings
138
143
  subc -p staging config # print that path and env file
139
144
  subc -p staging config --model subconscious/glm-5.2
140
145
  subc config --gateway-url https://gateway.example
package/bin/agents.js CHANGED
@@ -100,9 +100,12 @@ export function resolveAgent(name) {
100
100
 
101
101
  export function agentList() {
102
102
  return AGENTS.map((a) => ({
103
+ id: a.id,
103
104
  name: a.name,
104
105
  alias: a.command || a.id,
105
106
  action: a.runbook?.mode === 'setup' ? 'Configure' : 'Launch',
107
+ description: a.description || '',
108
+ launch: a.runbook?.mode !== 'setup',
106
109
  }));
107
110
  }
108
111
 
package/bin/cli.js CHANGED
@@ -41,6 +41,7 @@ import {
41
41
  } from './profiles.js';
42
42
  import { resolveModelCatalog } from './models.js';
43
43
  import { showUpdateNotice } from './update-check.js';
44
+ import { runTui } from './tui.js';
44
45
 
45
46
  function isHelpArg(arg) {
46
47
  return arg === 'help' || arg === '-h' || arg === '--help';
@@ -228,9 +229,20 @@ async function main() {
228
229
  const update = await showUpdateNotice();
229
230
  if (update?.action === 'updated' || update?.action === 'cancel') return;
230
231
 
231
- const parsed = extractProfile(process.argv.slice(2));
232
- const { args, profileName, profileExplicit } = parsed;
233
- const command = args[0];
232
+ let parsed = extractProfile(process.argv.slice(2));
233
+ let { args, profileName, profileExplicit } = parsed;
234
+ let command = args[0];
235
+
236
+ if (!command && process.stdin.isTTY === true && process.stdout.isTTY === true && process.env.TERM !== 'dumb') {
237
+ const selection = await runTui({ profileName });
238
+ if (!selection?.args?.length) return;
239
+ if (selection.baseUrl?.trim()) {
240
+ process.env.SUBCONSCIOUS_BASE_URL = selection.baseUrl.trim();
241
+ }
242
+ parsed = extractProfile(selection.args);
243
+ ({ args, profileName, profileExplicit } = parsed);
244
+ command = args[0];
245
+ }
234
246
 
235
247
  if (!command || command === '--help' || command === '-h' || (command === 'help' && !args[1])) {
236
248
  printHelp();
Binary file
Binary file
Binary file
Binary file
package/bin/profiles.js CHANGED
@@ -678,7 +678,7 @@ Usage:
678
678
  subc -p NAME config
679
679
  subc config edit [vim|nano]
680
680
  subc -p NAME config edit [vim|nano]
681
- subc config [show|path|list|delete]
681
+ subc config [show|path|list|create|delete]
682
682
  [--gateway-url URL] [--api-key KEY] [--model MODEL]
683
683
 
684
684
  subc config List every profile and its file path
@@ -687,6 +687,7 @@ Usage:
687
687
  subc config edit vim Open the selected profile in vim
688
688
  subc config edit nano Open the selected profile in nano
689
689
  subc config path Print the selected profile path
690
+ subc -p NAME config create Create a new profile with default settings
690
691
  subc -p NAME config delete Delete a non-default profile
691
692
  `);
692
693
  }
@@ -715,7 +716,7 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
715
716
  if (arg === 'help' || arg === '-h' || arg === '--help') {
716
717
  printConfigHelp();
717
718
  return;
718
- } else if (['show', 'path', 'list', 'delete', 'edit', 'interactive'].includes(arg)) {
719
+ } else if (['show', 'path', 'list', 'create', 'delete', 'edit', 'interactive'].includes(arg)) {
719
720
  action = arg === 'interactive' ? 'edit' : arg;
720
721
  } else if (arg === '--gateway-url' || arg === '--api-key' || arg === '--model') {
721
722
  const value = argv[++i];
@@ -754,6 +755,18 @@ export async function configCommand(argv, profileName = DEFAULT_PROFILE, options
754
755
  return;
755
756
  }
756
757
 
758
+ if (action === 'create') {
759
+ if (Object.keys(updates).length) {
760
+ throw new Error('config create cannot be combined with profile updates');
761
+ }
762
+ const existing = await loadProfile(profileName);
763
+ if (existing.exists) throw new Error(`Profile '${profileName}' already exists`);
764
+ const profile = await ensureProfile(profileName);
765
+ console.log(`Created profile '${profileName}'.`);
766
+ await printProfile(profile);
767
+ return;
768
+ }
769
+
757
770
  if (action === 'delete') {
758
771
  if (profileName === DEFAULT_PROFILE) {
759
772
  throw new Error('Refusing to delete the default profile; use subc logout to clear its key');
package/bin/tui.js ADDED
@@ -0,0 +1,195 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import { agentList } from './agents.js';
8
+ import { getApiKey } from './auth.js';
9
+ import { resolveModelCatalog } from './models.js';
10
+ import {
11
+ DEFAULT_PROFILE,
12
+ listProfiles,
13
+ loadProfile,
14
+ RUNBOOK_DEFAULTS,
15
+ SUPPORTED_MODELS as PACKAGED_MODELS,
16
+ } from './profiles.js';
17
+
18
+ const BIN_DIR = path.dirname(fileURLToPath(import.meta.url));
19
+ const TUI_SOURCE_DIR = path.resolve(BIN_DIR, '../tui');
20
+
21
+ export function nativeTargetName(platform = process.platform, arch = process.arch) {
22
+ const goArch = { x64: 'amd64', arm64: 'arm64' }[arch];
23
+ if (!goArch || !['darwin', 'linux', 'win32'].includes(platform)) return null;
24
+ const goOS = platform === 'win32' ? 'windows' : platform;
25
+ const extension = platform === 'win32' ? '.exe' : '';
26
+ return `subc-tui-${goOS}-${goArch}${extension}`;
27
+ }
28
+
29
+ export function isTuiResult(result) {
30
+ return (
31
+ result !== null &&
32
+ typeof result === 'object' &&
33
+ Array.isArray(result.args) &&
34
+ result.args.every((arg) => typeof arg === 'string') &&
35
+ (result.baseUrl === undefined || typeof result.baseUrl === 'string')
36
+ );
37
+ }
38
+
39
+ async function pathExists(file) {
40
+ try {
41
+ await fs.access(file);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export async function resolveTuiExecutable(options = {}) {
49
+ const override = options.binary || process.env.SUBC_TUI_BIN?.trim();
50
+ if (override) return { command: override, args: [], cwd: undefined };
51
+
52
+ const target = nativeTargetName(options.platform, options.arch);
53
+ if (target) {
54
+ const packaged = path.join(BIN_DIR, 'native', target);
55
+ if (await pathExists(packaged)) {
56
+ return { command: packaged, args: [], cwd: undefined };
57
+ }
58
+ }
59
+
60
+ // Source checkouts can run the TUI without committing native build output.
61
+ // Published packages always contain a prebuilt platform binary.
62
+ if (await pathExists(path.join(TUI_SOURCE_DIR, 'go.mod'))) {
63
+ return {
64
+ command: 'go',
65
+ args: ['run', './cmd/subc-tui'],
66
+ cwd: TUI_SOURCE_DIR,
67
+ };
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function selectedModelFor(profile) {
73
+ return (
74
+ process.env.SUBCONSCIOUS_MODEL?.trim() ||
75
+ profile.values.MODEL?.trim() ||
76
+ RUNBOOK_DEFAULTS.MODEL
77
+ );
78
+ }
79
+
80
+ function gatewayFor(profile) {
81
+ return (
82
+ process.env.SUBCONSCIOUS_BASE_URL?.trim() ||
83
+ profile.values.GATEWAY_URL?.trim() ||
84
+ RUNBOOK_DEFAULTS.GATEWAY_URL
85
+ ).replace(/\/+$/, '');
86
+ }
87
+
88
+ async function packageVersion() {
89
+ const pkg = JSON.parse(
90
+ await fs.readFile(new URL('../package.json', import.meta.url), 'utf-8'),
91
+ );
92
+ return pkg.version;
93
+ }
94
+
95
+ export async function createTuiState(profileName = DEFAULT_PROFILE, options = {}) {
96
+ const activeProfile = options.profile || (await loadProfile(profileName));
97
+ const names = [...new Set([profileName, ...(await listProfiles())])].sort((a, b) => {
98
+ if (a === profileName) return -1;
99
+ if (b === profileName) return 1;
100
+ return a.localeCompare(b);
101
+ });
102
+ const profiles = await Promise.all(
103
+ names.map(async (name) => {
104
+ const profile = name === profileName ? activeProfile : await loadProfile(name);
105
+ const auth = await getApiKey(profile);
106
+ return {
107
+ name,
108
+ model: selectedModelFor(profile),
109
+ authenticated: Boolean(auth?.key),
110
+ };
111
+ }),
112
+ );
113
+
114
+ const auth = await getApiKey(activeProfile);
115
+ const selectedModel = selectedModelFor(activeProfile);
116
+ const gatewayUrl = gatewayFor(activeProfile);
117
+ const catalog = await resolveModelCatalog({
118
+ baseUrl: gatewayUrl,
119
+ apiKey: auth?.key,
120
+ selectedModel,
121
+ fallbackModels: PACKAGED_MODELS,
122
+ });
123
+
124
+ return {
125
+ version: await packageVersion(),
126
+ activeProfile: profileName,
127
+ profilePath: activeProfile.path,
128
+ profiles,
129
+ models: catalog.models,
130
+ selectedModel,
131
+ gatewayUrl,
132
+ savedGatewayUrl:
133
+ activeProfile.values.GATEWAY_URL?.trim().replace(/\/+$/, '') ||
134
+ RUNBOOK_DEFAULTS.GATEWAY_URL,
135
+ gatewayOverridden: Boolean(process.env.SUBCONSCIOUS_BASE_URL?.trim()),
136
+ modelError: catalog.error?.message || '',
137
+ agents: agentList().map((agent) => ({
138
+ command: agent.alias,
139
+ name: agent.name,
140
+ action: agent.action,
141
+ description: agent.description,
142
+ launch: agent.launch,
143
+ })),
144
+ };
145
+ }
146
+
147
+ function spawnAndWait(command, args, options = {}) {
148
+ return new Promise((resolve, reject) => {
149
+ const child = spawn(command, args, {
150
+ cwd: options.cwd,
151
+ env: process.env,
152
+ stdio: 'inherit',
153
+ });
154
+ child.once('error', reject);
155
+ child.once('exit', (code, signal) => {
156
+ if (signal) {
157
+ reject(new Error(`Subconscious TUI exited with signal ${signal}`));
158
+ return;
159
+ }
160
+ resolve(code ?? 1);
161
+ });
162
+ });
163
+ }
164
+
165
+ export async function runTui(options = {}) {
166
+ const executable = await resolveTuiExecutable(options);
167
+ if (!executable) return null;
168
+
169
+ const state = options.state || (await createTuiState(options.profileName, options));
170
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'subc-tui-'));
171
+ const statePath = path.join(tempDir, 'state.json');
172
+ const resultPath = path.join(tempDir, 'result.json');
173
+
174
+ try {
175
+ // This state intentionally contains only display data. API keys are used
176
+ // by the Node command engine and are never passed into the TUI process.
177
+ await fs.writeFile(statePath, `${JSON.stringify(state)}\n`, { mode: 0o600 });
178
+ const code = await spawnAndWait(
179
+ executable.command,
180
+ [...executable.args, '--state', statePath, '--result', resultPath],
181
+ { cwd: executable.cwd },
182
+ );
183
+ if (code !== 0) throw new Error(`Subconscious TUI exited with status ${code}`);
184
+
185
+ try {
186
+ const result = JSON.parse(await fs.readFile(resultPath, 'utf-8'));
187
+ return isTuiResult(result) ? result : null;
188
+ } catch (error) {
189
+ if (error.code === 'ENOENT') return null;
190
+ throw error;
191
+ }
192
+ } finally {
193
+ await fs.rm(tempDir, { recursive: true, force: true });
194
+ }
195
+ }
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "subconscious-cli",
3
- "version": "4.0.6",
3
+ "version": "4.0.7",
4
4
  "description": "CLI for Subconscious — run Claude Code, Codex, OpenCode, DeepSeek Harness, Cursor, Copilot, and Pi",
5
5
  "bin": {
6
6
  "subc": "bin/cli.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "node --test"
9
+ "build:tui": "node scripts/build-tui.js",
10
+ "build:tui:host": "node scripts/build-tui.js --host",
11
+ "test": "node --test",
12
+ "test:tui": "go -C tui test ./...",
13
+ "prepack": "npm run build:tui"
10
14
  },
11
15
  "files": [
12
16
  "bin"