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 +8 -3
- package/bin/agents.js +3 -0
- package/bin/cli.js +15 -3
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/profiles.js +15 -2
- package/bin/tui.js +195 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -31,9 +31,13 @@ subc pi
|
|
|
31
31
|
subc <agent> uninstall
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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"
|