subconscious-cli 0.1.1 → 0.2.0
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 +54 -35
- package/bin/agents.js +215 -0
- package/bin/auth.js +366 -0
- package/bin/cli.js +40 -380
- package/bin/colors.js +12 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
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
|
+
| Command | Launches | Requires (install) |
|
|
28
|
+
| -------------------------- | ----------- | ------------------------------------------------------ |
|
|
29
|
+
| `subconscious claude-code` | Claude Code | `npm i -g @anthropic-ai/claude-code` |
|
|
30
|
+
| `subconscious open-code` | OpenCode | `npm i -g opencode-ai` |
|
|
31
|
+
| `subconscious aider` | Aider | `python -m pip install aider-install && aider-install` |
|
|
32
|
+
| `subconscious codex` | Codex CLI | `npm i -g @openai/codex` |
|
|
33
|
+
|
|
34
|
+
If the underlying agent isn't installed, the CLI tells you the exact install
|
|
35
|
+
command. Anything after the agent name is forwarded straight to it:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
subconscious claude-code --resume
|
|
39
|
+
subconscious codex exec "write a test"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Choosing a model
|
|
43
|
+
|
|
44
|
+
Defaults to `subconscious/tim-qwen3.6-27b`. Override per run with `--model`, or
|
|
45
|
+
set `SUBCONSCIOUS_MODEL` in your environment:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
subconscious open-code --model subconscious/tim-qwen3.6-27b
|
|
49
|
+
export SUBCONSCIOUS_MODEL=subconscious/tim-qwen3.6-27b
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Auth commands
|
|
53
|
+
|
|
54
|
+
### `login`
|
|
55
|
+
|
|
56
|
+
Opens your browser to sign in (or create an account). After authentication, your
|
|
57
|
+
API key is automatically generated and saved.
|
|
14
58
|
|
|
15
59
|
```
|
|
16
60
|
Terminal Browser
|
|
@@ -19,41 +63,24 @@ Terminal Browser
|
|
|
19
63
|
│ 2. Open browser ───────────────►│
|
|
20
64
|
│ │ 3. Sign in / sign up via Clerk
|
|
21
65
|
│ │ 4. API key auto-created
|
|
22
|
-
│ 5. Receive key ◄────────────────│
|
|
66
|
+
│ 5. Receive key ◄────────────────│
|
|
23
67
|
│ 6. Save to ~/.subcon/config.json│
|
|
24
68
|
│ │ "You can close this tab"
|
|
25
69
|
✓ Logged in! │
|
|
26
70
|
```
|
|
27
71
|
|
|
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
72
|
### `logout`
|
|
39
73
|
|
|
40
74
|
Removes your saved API key.
|
|
41
75
|
|
|
42
|
-
```bash
|
|
43
|
-
npx @subconscious/cli logout
|
|
44
|
-
```
|
|
45
|
-
|
|
46
76
|
### `whoami`
|
|
47
77
|
|
|
48
78
|
Shows your current authentication status and which key is active.
|
|
49
79
|
|
|
50
|
-
```bash
|
|
51
|
-
npx @subconscious/cli whoami
|
|
52
|
-
```
|
|
53
|
-
|
|
54
80
|
## Where keys are stored
|
|
55
81
|
|
|
56
|
-
Keys are saved to `~/.subcon/config.json` with `600` permissions
|
|
82
|
+
Keys are saved to `~/.subcon/config.json` with `600` permissions
|
|
83
|
+
(owner-read-only). The file looks like:
|
|
57
84
|
|
|
58
85
|
```json
|
|
59
86
|
{
|
|
@@ -61,13 +88,5 @@ Keys are saved to `~/.subcon/config.json` with `600` permissions (owner-read-onl
|
|
|
61
88
|
}
|
|
62
89
|
```
|
|
63
90
|
|
|
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
|
-
```
|
|
91
|
+
Environment variable `SUBCONSCIOUS_API_KEY` takes precedence over the config
|
|
92
|
+
file — handy for CI or temporary overrides.
|
package/bin/agents.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
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. Each entry here mirrors the
|
|
7
|
+
* matching example under `examples/<agent>` (its `subconscious.agent` block),
|
|
8
|
+
* which stays the source of truth.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import { constants as fsConstants } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { c } from './colors.js';
|
|
16
|
+
import { getApiKey } from './auth.js';
|
|
17
|
+
|
|
18
|
+
// Subconscious endpoints. The Anthropic-style base has no `/v1`; the
|
|
19
|
+
// OpenAI-compatible base does.
|
|
20
|
+
const API_BASE = 'https://api.subconscious.dev';
|
|
21
|
+
const API_BASE_V1 = 'https://api.subconscious.dev/v1';
|
|
22
|
+
const DEFAULT_MODEL = 'subconscious/tim-qwen3.6-27b';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Agent registry. Each entry:
|
|
26
|
+
* name — display name
|
|
27
|
+
* aliases — accepted subcommands (first is canonical)
|
|
28
|
+
* install — how to install the underlying CLI (shown when it's missing)
|
|
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`
|
|
32
|
+
*/
|
|
33
|
+
const AGENTS = [
|
|
34
|
+
{
|
|
35
|
+
name: 'Claude Code',
|
|
36
|
+
aliases: ['claude-code', 'claude', 'claudecode'],
|
|
37
|
+
install: 'npm i -g @anthropic-ai/claude-code',
|
|
38
|
+
bin: 'claude',
|
|
39
|
+
env: (key, model) => ({
|
|
40
|
+
ANTHROPIC_BASE_URL: API_BASE,
|
|
41
|
+
ANTHROPIC_AUTH_TOKEN: key,
|
|
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
|
+
];
|
|
99
|
+
|
|
100
|
+
const BY_ALIAS = new Map();
|
|
101
|
+
for (const agent of AGENTS) {
|
|
102
|
+
for (const alias of agent.aliases) BY_ALIAS.set(alias, agent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveAgent(name) {
|
|
106
|
+
return BY_ALIAS.get(name) ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function agentList() {
|
|
110
|
+
return AGENTS.map((a) => ({ name: a.name, alias: a.aliases[0] }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Pull a `--model <value>` / `--model=<value>` flag out of the passthrough
|
|
115
|
+
* args (so it sets the Subconscious model rather than reaching the agent).
|
|
116
|
+
* Falls back to SUBCONSCIOUS_MODEL, then the default.
|
|
117
|
+
*/
|
|
118
|
+
function extractModel(argv) {
|
|
119
|
+
let model = process.env.SUBCONSCIOUS_MODEL?.trim() || DEFAULT_MODEL;
|
|
120
|
+
const rest = [];
|
|
121
|
+
for (let i = 0; i < argv.length; i++) {
|
|
122
|
+
const a = argv[i];
|
|
123
|
+
if (a === '--model') {
|
|
124
|
+
const v = argv[i + 1];
|
|
125
|
+
if (v && !v.startsWith('-')) {
|
|
126
|
+
model = v;
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (a.startsWith('--model=')) {
|
|
132
|
+
model = a.slice('--model='.length);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
rest.push(a);
|
|
136
|
+
}
|
|
137
|
+
return { model, rest };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Is `bin` an executable resolvable on PATH? */
|
|
141
|
+
async function isOnPath(bin) {
|
|
142
|
+
const dirs = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
143
|
+
const exts =
|
|
144
|
+
process.platform === 'win32'
|
|
145
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
|
|
146
|
+
: [''];
|
|
147
|
+
for (const dir of dirs) {
|
|
148
|
+
for (const ext of exts) {
|
|
149
|
+
const candidate = path.join(dir, bin + ext);
|
|
150
|
+
try {
|
|
151
|
+
await fs.access(candidate, fsConstants.F_OK);
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
// keep scanning
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Launch a coding agent against Subconscious. `argv` is everything after the
|
|
163
|
+
* agent name; unknown flags pass straight through to the underlying CLI.
|
|
164
|
+
*/
|
|
165
|
+
export async function runAgent(agent, argv) {
|
|
166
|
+
const { model, rest } = extractModel(argv);
|
|
167
|
+
|
|
168
|
+
const auth = await getApiKey();
|
|
169
|
+
if (!auth) {
|
|
170
|
+
console.error(`\n ${c.red}Not logged in.${c.reset}`);
|
|
171
|
+
console.error(
|
|
172
|
+
` Run ${c.cyan}subconscious login${c.reset} (or set ${c.dim}SUBCONSCIOUS_API_KEY${c.reset}) first.\n`,
|
|
173
|
+
);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!(await isOnPath(agent.bin))) {
|
|
178
|
+
console.error(
|
|
179
|
+
`\n ${c.red}${agent.name} isn't installed${c.reset} ${c.dim}(\`${agent.bin}\` not found on PATH).${c.reset}`,
|
|
180
|
+
);
|
|
181
|
+
console.error(` Install it with:\n`);
|
|
182
|
+
console.error(` ${c.cyan}${agent.install}${c.reset}\n`);
|
|
183
|
+
process.exit(127);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const env = { ...process.env, ...agent.env(auth.key, model) };
|
|
187
|
+
const args = [...agent.args(model), ...rest];
|
|
188
|
+
|
|
189
|
+
console.log(
|
|
190
|
+
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const child = spawn(agent.bin, args, { stdio: 'inherit', env });
|
|
194
|
+
|
|
195
|
+
child.on('error', (err) => {
|
|
196
|
+
if (err.code === 'ENOENT') {
|
|
197
|
+
console.error(
|
|
198
|
+
`\n ${c.red}Could not launch \`${agent.bin}\`.${c.reset} Install it with:\n`,
|
|
199
|
+
);
|
|
200
|
+
console.error(` ${c.cyan}${agent.install}${c.reset}\n`);
|
|
201
|
+
process.exit(127);
|
|
202
|
+
}
|
|
203
|
+
console.error(`\n ${c.red}${err.message}${c.reset}\n`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Mirror the child's exit status so callers/scripts see the real result.
|
|
208
|
+
child.on('exit', (code, signal) => {
|
|
209
|
+
if (signal) {
|
|
210
|
+
process.kill(process.pid, signal);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
process.exit(code ?? 0);
|
|
214
|
+
});
|
|
215
|
+
}
|
package/bin/auth.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication + credential storage for the Subconscious CLI.
|
|
3
|
+
*
|
|
4
|
+
* Login flow (localhost callback pattern, similar to Vercel/Supabase CLIs):
|
|
5
|
+
* 1. CLI generates a random `state` token (CSRF protection) and starts
|
|
6
|
+
* an ephemeral HTTP server on a random port bound to 127.0.0.1.
|
|
7
|
+
* 2. Opens the browser to {PLATFORM_URL}/cli/auth?port=...&state=...
|
|
8
|
+
* 3. The web app authenticates the user, generates an API key, and
|
|
9
|
+
* delivers it back to the CLI via a cross-origin fetch to
|
|
10
|
+
* localhost:{port}/callback?token=...&state=...
|
|
11
|
+
* 4. CLI verifies the `state` matches, saves the key to ~/.subcon/config.json.
|
|
12
|
+
*
|
|
13
|
+
* Override SUBCONSCIOUS_URL env var for local development.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import http from 'node:http';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
18
|
+
import { exec } from 'node:child_process';
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import os from 'node:os';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { c } from './colors.js';
|
|
23
|
+
|
|
24
|
+
const CONFIG_DIR = path.join(os.homedir(), '.subcon');
|
|
25
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
26
|
+
// Defaults to production. Developers set SUBCONSCIOUS_URL=http://localhost:3000 for local dev.
|
|
27
|
+
const PLATFORM_URL = process.env.SUBCONSCIOUS_URL || 'https://www.subconscious.dev';
|
|
28
|
+
|
|
29
|
+
// ── Config helpers ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
async function loadConfig() {
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
34
|
+
return JSON.parse(content);
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function saveConfig(config) {
|
|
41
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
42
|
+
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
43
|
+
// 0o600 = owner read/write only — the file contains an API key
|
|
44
|
+
await fs.chmod(CONFIG_FILE, 0o600);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the active API key. The env var takes precedence over the saved
|
|
49
|
+
* config so CI and per-shell overrides win. Returns null when unauthenticated.
|
|
50
|
+
*/
|
|
51
|
+
export async function getApiKey() {
|
|
52
|
+
const envKey = process.env.SUBCONSCIOUS_API_KEY?.trim();
|
|
53
|
+
if (envKey) return { key: envKey, source: 'SUBCONSCIOUS_API_KEY env var' };
|
|
54
|
+
|
|
55
|
+
const config = await loadConfig();
|
|
56
|
+
if (config.subconscious_api_key) {
|
|
57
|
+
return { key: config.subconscious_api_key, source: '~/.subcon/config.json' };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Browser opener ──────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
function openBrowser(url) {
|
|
65
|
+
const cmd =
|
|
66
|
+
process.platform === 'darwin'
|
|
67
|
+
? `open "${url}"`
|
|
68
|
+
: process.platform === 'win32'
|
|
69
|
+
? `start "" "${url}"`
|
|
70
|
+
: `xdg-open "${url}"`;
|
|
71
|
+
exec(cmd, (err) => {
|
|
72
|
+
if (err) {
|
|
73
|
+
console.log(
|
|
74
|
+
`\n${c.yellow}Could not open browser automatically.${c.reset}`,
|
|
75
|
+
);
|
|
76
|
+
console.log(`Please open this URL manually:\n`);
|
|
77
|
+
console.log(` ${c.underline}${c.cyan}${url}${c.reset}\n`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Localhost callback server ───────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function startCallbackServer(expectedState) {
|
|
85
|
+
return new Promise((resolveSetup) => {
|
|
86
|
+
let resolveToken, rejectToken;
|
|
87
|
+
const tokenPromise = new Promise((resolve, reject) => {
|
|
88
|
+
resolveToken = resolve;
|
|
89
|
+
rejectToken = reject;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const server = http.createServer((req, res) => {
|
|
93
|
+
// CORS: only allow the web app's origin (production or localhost dev).
|
|
94
|
+
// This prevents arbitrary websites from hitting this callback.
|
|
95
|
+
const origin = req.headers.origin || '';
|
|
96
|
+
const allowed =
|
|
97
|
+
origin === PLATFORM_URL ||
|
|
98
|
+
origin.startsWith('http://localhost:');
|
|
99
|
+
res.setHeader(
|
|
100
|
+
'Access-Control-Allow-Origin',
|
|
101
|
+
allowed ? origin : PLATFORM_URL,
|
|
102
|
+
);
|
|
103
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
104
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
105
|
+
res.setHeader('Connection', 'close');
|
|
106
|
+
|
|
107
|
+
if (req.method === 'OPTIONS') {
|
|
108
|
+
res.writeHead(204);
|
|
109
|
+
res.end();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const url = new URL(req.url, 'http://localhost');
|
|
114
|
+
|
|
115
|
+
if (url.pathname !== '/callback') {
|
|
116
|
+
res.writeHead(404);
|
|
117
|
+
res.end();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const token = url.searchParams.get('token');
|
|
122
|
+
const state = url.searchParams.get('state');
|
|
123
|
+
const error = url.searchParams.get('error');
|
|
124
|
+
|
|
125
|
+
// The web app sends `Accept: application/json` via fetch(); direct
|
|
126
|
+
// browser visits get the HTML fallback page.
|
|
127
|
+
const wantsJson = (req.headers.accept || '').includes('application/json');
|
|
128
|
+
const respond = (ok, msg) => {
|
|
129
|
+
if (wantsJson) {
|
|
130
|
+
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
131
|
+
res.end(JSON.stringify({ ok, error: msg || undefined }));
|
|
132
|
+
} else {
|
|
133
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
134
|
+
res.end(resultPage(ok, msg));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (error) {
|
|
139
|
+
respond(false, error);
|
|
140
|
+
server.close();
|
|
141
|
+
rejectToken(new Error(error));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// CSRF check: state token must match what the CLI generated
|
|
146
|
+
if (state !== expectedState) {
|
|
147
|
+
respond(false, 'State mismatch — possible CSRF attack. Please try again.');
|
|
148
|
+
server.close();
|
|
149
|
+
rejectToken(new Error('State mismatch'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!token) {
|
|
154
|
+
respond(false, 'No API key received.');
|
|
155
|
+
server.close();
|
|
156
|
+
rejectToken(new Error('No API key received'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
respond(true);
|
|
161
|
+
server.close();
|
|
162
|
+
resolveToken({ token });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Port 0 = OS assigns a random available port. Bound to 127.0.0.1 only.
|
|
166
|
+
server.listen(0, '127.0.0.1', () => {
|
|
167
|
+
const port = server.address().port;
|
|
168
|
+
|
|
169
|
+
const timeout = setTimeout(() => {
|
|
170
|
+
server.close();
|
|
171
|
+
rejectToken(new Error('Authentication timed out (5 min). Please try again.'));
|
|
172
|
+
}, 5 * 60 * 1000);
|
|
173
|
+
|
|
174
|
+
tokenPromise.finally(() => clearTimeout(timeout));
|
|
175
|
+
|
|
176
|
+
resolveSetup({ port, promise: tokenPromise });
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resultPage(success, message) {
|
|
182
|
+
const title = success ? 'Authenticated' : 'Authentication failed';
|
|
183
|
+
const subtitle = success
|
|
184
|
+
? 'You can close this tab and return to your terminal.'
|
|
185
|
+
: message || 'Something went wrong.';
|
|
186
|
+
|
|
187
|
+
const logoSvg = `<svg viewBox="0 0 205 199" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M-4.33e-06 99.93C-4.96e-06 85.57 11.64 73.93 26 73.93c7.88 0 14.94 3.51 19.71 9.04 5.25 6.09 11.36 12.6 19.37 13.33l3.92.36v-.01c.03.01.06.01.09.01L72 96.93c12.69 0 23.98-10.28 24-22.96-.01-7.36-3.48-13.91-8.87-18.11l-1.08-.76c-6.52-4.6-15.3-3.65-23.19-2.46-7.28 1.1-14.97-.88-20.96-6.08C31.06 37.13 29.91 20.71 39.33 9.87 48.75-.96 65.18-2.11 76.01 7.31c5.99 5.21 9.02 12.55 8.94 19.91-.09 7.97.2 16.8 5.66 22.62l.94 1 .11.1.14.12c.22.19.45.37.68.55l.17.13c.25.18.5.36.75.53.64.42 1.31.8 2.01 1.14.48.23.98.44 1.49.62.21.07.42.14.63.21.32.1.64.19.97.27.4.1.8.18 1.2.25.34.06.69.11 1.03.14.58.06 1.17.09 1.77.09 4.2 0 8.03-1.57 10.95-4.16l.94-1c5.46-5.81 5.75-14.65 5.66-22.62-.08-7.36 2.95-14.71 8.94-19.91C139.82-2.11 156.25-.96 165.67 9.87c9.42 10.84 8.27 27.26-2.56 36.68-5.99 5.21-13.69 7.18-20.96 6.08-7.88-1.19-16.67-2.14-23.19 2.46l-1.08.76c-5.39 4.2-8.86 10.75-8.87 18.11.02 12.69 11.31 22.96 24 22.96l2.91-.26.09-.02v.01l3.93-.36c8.01-.73 14.12-7.23 19.37-13.33 4.77-5.54 11.83-9.04 19.71-9.04C193.36 73.93 205 85.57 205 99.93v.07c0 .01 0 .02 0 .03 0 14.36-11.64 26-26 26-7.88 0-14.94-3.51-19.71-9.04-5.25-6.09-11.36-12.6-19.37-13.33l-3.93-.36v.01c-.03-.01-.06-.01-.09-.01L133 103c-12.69 0-23.98 10.28-24 22.97.01 7.36 3.48 13.91 8.87 18.11l1.08.76c6.52 4.6 15.3 3.65 23.19 2.46 7.28-1.1 14.97.88 20.96 6.08 10.84 9.42 11.98 25.84 2.56 36.68-9.42 10.84-25.84 11.98-36.68 2.56-5.99-5.21-9.02-12.55-8.94-19.91.09-7.97-.2-16.8-5.66-22.62l-.94-1c-2.91-2.59-6.75-4.16-10.95-4.16-.6 0-1.19.03-1.77.09-.35.04-.69.09-1.03.14-.34.07-.69.15-1.03.25-.33.08-.65.17-.97.28-.21.07-.42.14-.62.21-.51.18-1.01.39-1.49.62-.7.33-1.37.71-2.01 1.14-.26.17-.5.35-.75.53l-.17.13a11 11 0 0 0-.68.55l-.14.12-.11.1-.94 1.01c-5.46 5.81-5.75 14.65-5.66 22.62.08 7.36-2.95 14.71-8.94 19.91-10.84 9.42-27.26 8.27-36.68-2.56-9.42-10.84-8.27-27.26 2.56-36.68 5.99-5.21 13.69-7.18 20.96-6.08 7.88 1.19 16.67 2.14 23.19-2.46l1.08-.76c5.39-4.2 8.86-10.75 8.87-18.11-.02-12.69-11.31-22.97-24-22.97l-2.91.27c-.03 0-.06.01-.09.01v-.01l-3.93.36c-8.01.73-14.12 7.23-19.37 13.33C40.94 122.49 33.88 126 26 126 11.64 126 0 114.36 0 100l-4.33e-06-.07Z" fill="#FF5C28"/></svg>`;
|
|
188
|
+
|
|
189
|
+
const statusIcon = success
|
|
190
|
+
? `<div class="icon success"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></div>`
|
|
191
|
+
: `<div class="icon error"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg></div>`;
|
|
192
|
+
|
|
193
|
+
return `<!DOCTYPE html><html lang="en"><head>
|
|
194
|
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
195
|
+
<title>Subconscious CLI</title>
|
|
196
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
197
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
198
|
+
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
|
|
199
|
+
<style>
|
|
200
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
201
|
+
body{font-family:'Manrope',system-ui,sans-serif;min-height:100vh;background:#F6F3EF;color:#111}
|
|
202
|
+
header{display:flex;align-items:center;gap:10px;padding:20px 24px}
|
|
203
|
+
header svg{width:24px;height:24px}
|
|
204
|
+
header span{font-size:15px;font-weight:600;letter-spacing:-.01em}
|
|
205
|
+
main{display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 160px)}
|
|
206
|
+
.wrap{width:100%;max-width:400px;padding:0 24px}
|
|
207
|
+
.label{text-align:center;font-size:11px;font-weight:500;text-transform:uppercase;
|
|
208
|
+
letter-spacing:.1em;color:#9ca3af;margin-bottom:16px}
|
|
209
|
+
.card{background:#fff;border:1px solid rgba(0,0,0,.08);border-radius:12px;
|
|
210
|
+
padding:32px;text-align:center}
|
|
211
|
+
.icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;
|
|
212
|
+
justify-content:center;margin:0 auto 16px}
|
|
213
|
+
.icon.success{background:#f0fdf4}
|
|
214
|
+
.icon.error{background:#fef2f2}
|
|
215
|
+
h1{font-size:15px;font-weight:600;margin-bottom:4px;letter-spacing:-.01em}
|
|
216
|
+
.sub{color:#6b7280;font-size:13px;line-height:1.6;max-width:280px;margin:0 auto}
|
|
217
|
+
.footer{text-align:center;margin-top:16px;font-size:11px;color:#9ca3af}
|
|
218
|
+
</style></head><body>
|
|
219
|
+
<header>${logoSvg}<span>Subconscious</span></header>
|
|
220
|
+
<main><div class="wrap">
|
|
221
|
+
<div class="label">CLI Authentication</div>
|
|
222
|
+
<div class="card">
|
|
223
|
+
${statusIcon}
|
|
224
|
+
<h1>${title}</h1>
|
|
225
|
+
<p class="sub">${subtitle}</p>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="footer">subconscious.dev</div>
|
|
228
|
+
</div></main>
|
|
229
|
+
</body></html>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Commands ────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
export async function loginCommand() {
|
|
235
|
+
const existing = await getApiKey();
|
|
236
|
+
|
|
237
|
+
if (existing) {
|
|
238
|
+
const masked = existing.key.slice(0, 8) + '...' + existing.key.slice(-4);
|
|
239
|
+
console.log(`\n${c.yellow}Already logged in.${c.reset}`);
|
|
240
|
+
console.log(` Key: ${c.dim}${masked}${c.reset}`);
|
|
241
|
+
console.log(
|
|
242
|
+
`\n Run ${c.cyan}subconscious logout${c.reset} first to switch accounts.\n`,
|
|
243
|
+
);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log();
|
|
248
|
+
console.log(
|
|
249
|
+
` ${c.magenta}${c.bold}Subconscious${c.reset} ${c.dim}— CLI Login${c.reset}`,
|
|
250
|
+
);
|
|
251
|
+
console.log();
|
|
252
|
+
|
|
253
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
254
|
+
const { port, promise } = await startCallbackServer(state);
|
|
255
|
+
|
|
256
|
+
const authUrl = `${PLATFORM_URL}/cli/auth?port=${port}&state=${state}`;
|
|
257
|
+
|
|
258
|
+
console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
|
|
259
|
+
console.log();
|
|
260
|
+
console.log(` ${c.dim}If it doesn't open, visit:${c.reset}`);
|
|
261
|
+
console.log(` ${c.underline}${c.cyan}${authUrl}${c.reset}`);
|
|
262
|
+
console.log();
|
|
263
|
+
|
|
264
|
+
openBrowser(authUrl);
|
|
265
|
+
|
|
266
|
+
// Spinner while waiting
|
|
267
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
268
|
+
let i = 0;
|
|
269
|
+
const spinner = setInterval(() => {
|
|
270
|
+
process.stdout.write(
|
|
271
|
+
`\r ${c.cyan}${frames[i++ % frames.length]}${c.reset} Waiting for authentication...`,
|
|
272
|
+
);
|
|
273
|
+
}, 80);
|
|
274
|
+
|
|
275
|
+
process.on('SIGINT', () => {
|
|
276
|
+
clearInterval(spinner);
|
|
277
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
278
|
+
console.log(`\n ${c.dim}Login cancelled.${c.reset}\n`);
|
|
279
|
+
process.exit(0);
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
const result = await promise;
|
|
283
|
+
clearInterval(spinner);
|
|
284
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
285
|
+
|
|
286
|
+
const config = await loadConfig();
|
|
287
|
+
config.subconscious_api_key = result.token;
|
|
288
|
+
await saveConfig(config);
|
|
289
|
+
|
|
290
|
+
const masked = result.token.slice(0, 8) + '...' + result.token.slice(-4);
|
|
291
|
+
console.log(` ${c.green}${c.bold}✓ Logged in successfully!${c.reset}`);
|
|
292
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
293
|
+
console.log(` ${c.dim}Saved to ~/.subcon/config.json${c.reset}`);
|
|
294
|
+
console.log();
|
|
295
|
+
} catch (error) {
|
|
296
|
+
clearInterval(spinner);
|
|
297
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
298
|
+
console.error(` ${c.red}✗ ${error.message}${c.reset}\n`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function logoutCommand() {
|
|
304
|
+
const config = await loadConfig();
|
|
305
|
+
|
|
306
|
+
if (!config.subconscious_api_key) {
|
|
307
|
+
console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
delete config.subconscious_api_key;
|
|
312
|
+
await saveConfig(config);
|
|
313
|
+
|
|
314
|
+
console.log(
|
|
315
|
+
`\n ${c.green}✓${c.reset} Logged out. API key removed from ${c.dim}~/.subcon/config.json${c.reset}\n`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function whoamiCommand() {
|
|
320
|
+
const auth = await getApiKey();
|
|
321
|
+
|
|
322
|
+
if (!auth) {
|
|
323
|
+
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
324
|
+
console.log(
|
|
325
|
+
` Run ${c.cyan}subconscious login${c.reset} to get started.\n`,
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { key, source } = auth;
|
|
331
|
+
const masked = key.slice(0, 8) + '...' + key.slice(-4);
|
|
332
|
+
|
|
333
|
+
console.log();
|
|
334
|
+
|
|
335
|
+
// Validate the key against the server; falls back to offline display if unreachable
|
|
336
|
+
try {
|
|
337
|
+
const res = await fetch(`${PLATFORM_URL}/api/cli/whoami`, {
|
|
338
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
339
|
+
signal: AbortSignal.timeout(5000),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (res.ok) {
|
|
343
|
+
const data = await res.json();
|
|
344
|
+
console.log(` ${c.green}✓ Authenticated${c.reset}`);
|
|
345
|
+
if (data.organization) {
|
|
346
|
+
console.log(` ${c.dim}Org: ${c.reset}${data.organization}`);
|
|
347
|
+
}
|
|
348
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
349
|
+
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(` ${c.red}✗ Key is invalid or revoked${c.reset}`);
|
|
352
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
353
|
+
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
354
|
+
console.log();
|
|
355
|
+
console.log(
|
|
356
|
+
` Run ${c.cyan}subconscious logout${c.reset} then ${c.cyan}subconscious login${c.reset} to re-authenticate.`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
console.log(` ${c.green}✓ Authenticated${c.reset} ${c.dim}(offline — key not verified)${c.reset}`);
|
|
361
|
+
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
362
|
+
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log();
|
|
366
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -1,400 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Subconscious CLI —
|
|
4
|
+
* Subconscious CLI — log in, then launch coding agents on your hosted models.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* an ephemeral HTTP server on a random port bound to 127.0.0.1.
|
|
9
|
-
* 2. Opens the browser to {PLATFORM_URL}/cli/auth?port=...&state=...
|
|
10
|
-
* 3. The web app authenticates the user, generates an API key, and
|
|
11
|
-
* delivers it back to the CLI via a cross-origin fetch to
|
|
12
|
-
* localhost:{port}/callback?token=...&state=...
|
|
13
|
-
* 4. CLI verifies the `state` matches, saves the key to ~/.subcon/config.json.
|
|
6
|
+
* subconscious login | logout | whoami — manage your API key
|
|
7
|
+
* subconscious <agent> [...args] — launch a coding agent
|
|
14
8
|
*
|
|
15
|
-
*
|
|
9
|
+
* Auth lives in ./auth.js, the agent launcher + registry in ./agents.js.
|
|
16
10
|
*/
|
|
17
11
|
|
|
18
|
-
import http from 'node:http';
|
|
19
|
-
import crypto from 'node:crypto';
|
|
20
|
-
import { exec } from 'node:child_process';
|
|
21
12
|
import fs from 'node:fs/promises';
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
-
const CONFIG_DIR = path.join(os.homedir(), '.subcon');
|
|
26
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
27
|
-
// Defaults to production. Developers set SUBCONSCIOUS_URL=http://localhost:3000 for local dev.
|
|
28
|
-
const PLATFORM_URL = process.env.SUBCONSCIOUS_URL || 'https://www.subconscious.dev';
|
|
29
|
-
|
|
30
|
-
const c = {
|
|
31
|
-
reset: '\x1b[0m',
|
|
32
|
-
bold: '\x1b[1m',
|
|
33
|
-
dim: '\x1b[2m',
|
|
34
|
-
cyan: '\x1b[36m',
|
|
35
|
-
green: '\x1b[32m',
|
|
36
|
-
red: '\x1b[31m',
|
|
37
|
-
yellow: '\x1b[33m',
|
|
38
|
-
magenta: '\x1b[35m',
|
|
39
|
-
underline: '\x1b[4m',
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// ── Config helpers ──────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
async function loadConfig() {
|
|
45
|
-
try {
|
|
46
|
-
const content = await fs.readFile(CONFIG_FILE, 'utf-8');
|
|
47
|
-
return JSON.parse(content);
|
|
48
|
-
} catch {
|
|
49
|
-
return {};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function saveConfig(config) {
|
|
54
|
-
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
55
|
-
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
56
|
-
// 0o600 = owner read/write only — the file contains an API key
|
|
57
|
-
await fs.chmod(CONFIG_FILE, 0o600);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ── Browser opener ──────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
function openBrowser(url) {
|
|
63
|
-
const cmd =
|
|
64
|
-
process.platform === 'darwin'
|
|
65
|
-
? `open "${url}"`
|
|
66
|
-
: process.platform === 'win32'
|
|
67
|
-
? `start "" "${url}"`
|
|
68
|
-
: `xdg-open "${url}"`;
|
|
69
|
-
exec(cmd, (err) => {
|
|
70
|
-
if (err) {
|
|
71
|
-
console.log(
|
|
72
|
-
`\n${c.yellow}Could not open browser automatically.${c.reset}`,
|
|
73
|
-
);
|
|
74
|
-
console.log(`Please open this URL manually:\n`);
|
|
75
|
-
console.log(` ${c.underline}${c.cyan}${url}${c.reset}\n`);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── Localhost callback server ───────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
function startCallbackServer(expectedState) {
|
|
83
|
-
return new Promise((resolveSetup) => {
|
|
84
|
-
let resolveToken, rejectToken;
|
|
85
|
-
const tokenPromise = new Promise((resolve, reject) => {
|
|
86
|
-
resolveToken = resolve;
|
|
87
|
-
rejectToken = reject;
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const server = http.createServer((req, res) => {
|
|
91
|
-
// CORS: only allow the web app's origin (production or localhost dev).
|
|
92
|
-
// This prevents arbitrary websites from hitting this callback.
|
|
93
|
-
const origin = req.headers.origin || '';
|
|
94
|
-
const allowed =
|
|
95
|
-
origin === PLATFORM_URL ||
|
|
96
|
-
origin.startsWith('http://localhost:');
|
|
97
|
-
res.setHeader(
|
|
98
|
-
'Access-Control-Allow-Origin',
|
|
99
|
-
allowed ? origin : PLATFORM_URL,
|
|
100
|
-
);
|
|
101
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
102
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
103
|
-
res.setHeader('Connection', 'close');
|
|
104
|
-
|
|
105
|
-
if (req.method === 'OPTIONS') {
|
|
106
|
-
res.writeHead(204);
|
|
107
|
-
res.end();
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const url = new URL(req.url, 'http://localhost');
|
|
112
|
-
|
|
113
|
-
if (url.pathname !== '/callback') {
|
|
114
|
-
res.writeHead(404);
|
|
115
|
-
res.end();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const token = url.searchParams.get('token');
|
|
120
|
-
const state = url.searchParams.get('state');
|
|
121
|
-
const error = url.searchParams.get('error');
|
|
122
|
-
|
|
123
|
-
// The web app sends `Accept: application/json` via fetch(); direct
|
|
124
|
-
// browser visits get the HTML fallback page.
|
|
125
|
-
const wantsJson = (req.headers.accept || '').includes('application/json');
|
|
126
|
-
const respond = (ok, msg) => {
|
|
127
|
-
if (wantsJson) {
|
|
128
|
-
res.writeHead(ok ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
129
|
-
res.end(JSON.stringify({ ok, error: msg || undefined }));
|
|
130
|
-
} else {
|
|
131
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
132
|
-
res.end(resultPage(ok, msg));
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
if (error) {
|
|
137
|
-
respond(false, error);
|
|
138
|
-
server.close();
|
|
139
|
-
rejectToken(new Error(error));
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// CSRF check: state token must match what the CLI generated
|
|
144
|
-
if (state !== expectedState) {
|
|
145
|
-
respond(false, 'State mismatch — possible CSRF attack. Please try again.');
|
|
146
|
-
server.close();
|
|
147
|
-
rejectToken(new Error('State mismatch'));
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!token) {
|
|
152
|
-
respond(false, 'No API key received.');
|
|
153
|
-
server.close();
|
|
154
|
-
rejectToken(new Error('No API key received'));
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
respond(true);
|
|
159
|
-
server.close();
|
|
160
|
-
resolveToken({ token });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Port 0 = OS assigns a random available port. Bound to 127.0.0.1 only.
|
|
164
|
-
server.listen(0, '127.0.0.1', () => {
|
|
165
|
-
const port = server.address().port;
|
|
166
|
-
|
|
167
|
-
const timeout = setTimeout(() => {
|
|
168
|
-
server.close();
|
|
169
|
-
rejectToken(new Error('Authentication timed out (5 min). Please try again.'));
|
|
170
|
-
}, 5 * 60 * 1000);
|
|
171
|
-
|
|
172
|
-
tokenPromise.finally(() => clearTimeout(timeout));
|
|
173
|
-
|
|
174
|
-
resolveSetup({ port, promise: tokenPromise });
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function resultPage(success, message) {
|
|
180
|
-
const title = success ? 'Authenticated' : 'Authentication failed';
|
|
181
|
-
const subtitle = success
|
|
182
|
-
? 'You can close this tab and return to your terminal.'
|
|
183
|
-
: message || 'Something went wrong.';
|
|
184
|
-
|
|
185
|
-
const logoSvg = `<svg viewBox="0 0 205 199" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M-4.33e-06 99.93C-4.96e-06 85.57 11.64 73.93 26 73.93c7.88 0 14.94 3.51 19.71 9.04 5.25 6.09 11.36 12.6 19.37 13.33l3.92.36v-.01c.03.01.06.01.09.01L72 96.93c12.69 0 23.98-10.28 24-22.96-.01-7.36-3.48-13.91-8.87-18.11l-1.08-.76c-6.52-4.6-15.3-3.65-23.19-2.46-7.28 1.1-14.97-.88-20.96-6.08C31.06 37.13 29.91 20.71 39.33 9.87 48.75-.96 65.18-2.11 76.01 7.31c5.99 5.21 9.02 12.55 8.94 19.91-.09 7.97.2 16.8 5.66 22.62l.94 1 .11.1.14.12c.22.19.45.37.68.55l.17.13c.25.18.5.36.75.53.64.42 1.31.8 2.01 1.14.48.23.98.44 1.49.62.21.07.42.14.63.21.32.1.64.19.97.27.4.1.8.18 1.2.25.34.06.69.11 1.03.14.58.06 1.17.09 1.77.09 4.2 0 8.03-1.57 10.95-4.16l.94-1c5.46-5.81 5.75-14.65 5.66-22.62-.08-7.36 2.95-14.71 8.94-19.91C139.82-2.11 156.25-.96 165.67 9.87c9.42 10.84 8.27 27.26-2.56 36.68-5.99 5.21-13.69 7.18-20.96 6.08-7.88-1.19-16.67-2.14-23.19 2.46l-1.08.76c-5.39 4.2-8.86 10.75-8.87 18.11.02 12.69 11.31 22.96 24 22.96l2.91-.26.09-.02v.01l3.93-.36c8.01-.73 14.12-7.23 19.37-13.33 4.77-5.54 11.83-9.04 19.71-9.04C193.36 73.93 205 85.57 205 99.93v.07c0 .01 0 .02 0 .03 0 14.36-11.64 26-26 26-7.88 0-14.94-3.51-19.71-9.04-5.25-6.09-11.36-12.6-19.37-13.33l-3.93-.36v.01c-.03-.01-.06-.01-.09-.01L133 103c-12.69 0-23.98 10.28-24 22.97.01 7.36 3.48 13.91 8.87 18.11l1.08.76c6.52 4.6 15.3 3.65 23.19 2.46 7.28-1.1 14.97.88 20.96 6.08 10.84 9.42 11.98 25.84 2.56 36.68-9.42 10.84-25.84 11.98-36.68 2.56-5.99-5.21-9.02-12.55-8.94-19.91.09-7.97-.2-16.8-5.66-22.62l-.94-1c-2.91-2.59-6.75-4.16-10.95-4.16-.6 0-1.19.03-1.77.09-.35.04-.69.09-1.03.14-.34.07-.69.15-1.03.25-.33.08-.65.17-.97.28-.21.07-.42.14-.62.21-.51.18-1.01.39-1.49.62-.7.33-1.37.71-2.01 1.14-.26.17-.5.35-.75.53l-.17.13a11 11 0 0 0-.68.55l-.14.12-.11.1-.94 1.01c-5.46 5.81-5.75 14.65-5.66 22.62.08 7.36-2.95 14.71-8.94 19.91-10.84 9.42-27.26 8.27-36.68-2.56-9.42-10.84-8.27-27.26 2.56-36.68 5.99-5.21 13.69-7.18 20.96-6.08 7.88 1.19 16.67 2.14 23.19-2.46l1.08-.76c5.39-4.2 8.86-10.75 8.87-18.11-.02-12.69-11.31-22.97-24-22.97l-2.91.27c-.03 0-.06.01-.09.01v-.01l-3.93.36c-8.01.73-14.12 7.23-19.37 13.33C40.94 122.49 33.88 126 26 126 11.64 126 0 114.36 0 100l-4.33e-06-.07Z" fill="#FF5C28"/></svg>`;
|
|
186
|
-
|
|
187
|
-
const statusIcon = success
|
|
188
|
-
? `<div class="icon success"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></div>`
|
|
189
|
-
: `<div class="icon error"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg></div>`;
|
|
190
|
-
|
|
191
|
-
return `<!DOCTYPE html><html lang="en"><head>
|
|
192
|
-
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
193
|
-
<title>Subconscious CLI</title>
|
|
194
|
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
195
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
196
|
-
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&display=swap" rel="stylesheet">
|
|
197
|
-
<style>
|
|
198
|
-
*{margin:0;padding:0;box-sizing:border-box}
|
|
199
|
-
body{font-family:'Manrope',system-ui,sans-serif;min-height:100vh;background:#F6F3EF;color:#111}
|
|
200
|
-
header{display:flex;align-items:center;gap:10px;padding:20px 24px}
|
|
201
|
-
header svg{width:24px;height:24px}
|
|
202
|
-
header span{font-size:15px;font-weight:600;letter-spacing:-.01em}
|
|
203
|
-
main{display:flex;align-items:center;justify-content:center;min-height:calc(100vh - 160px)}
|
|
204
|
-
.wrap{width:100%;max-width:400px;padding:0 24px}
|
|
205
|
-
.label{text-align:center;font-size:11px;font-weight:500;text-transform:uppercase;
|
|
206
|
-
letter-spacing:.1em;color:#9ca3af;margin-bottom:16px}
|
|
207
|
-
.card{background:#fff;border:1px solid rgba(0,0,0,.08);border-radius:12px;
|
|
208
|
-
padding:32px;text-align:center}
|
|
209
|
-
.icon{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;
|
|
210
|
-
justify-content:center;margin:0 auto 16px}
|
|
211
|
-
.icon.success{background:#f0fdf4}
|
|
212
|
-
.icon.error{background:#fef2f2}
|
|
213
|
-
h1{font-size:15px;font-weight:600;margin-bottom:4px;letter-spacing:-.01em}
|
|
214
|
-
.sub{color:#6b7280;font-size:13px;line-height:1.6;max-width:280px;margin:0 auto}
|
|
215
|
-
.footer{text-align:center;margin-top:16px;font-size:11px;color:#9ca3af}
|
|
216
|
-
</style></head><body>
|
|
217
|
-
<header>${logoSvg}<span>Subconscious</span></header>
|
|
218
|
-
<main><div class="wrap">
|
|
219
|
-
<div class="label">CLI Authentication</div>
|
|
220
|
-
<div class="card">
|
|
221
|
-
${statusIcon}
|
|
222
|
-
<h1>${title}</h1>
|
|
223
|
-
<p class="sub">${subtitle}</p>
|
|
224
|
-
</div>
|
|
225
|
-
<div class="footer">subconscious.dev</div>
|
|
226
|
-
</div></main>
|
|
227
|
-
</body></html>`;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── Commands ────────────────────────────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
async function loginCommand() {
|
|
233
|
-
// Environment variable takes precedence over config file
|
|
234
|
-
const existingConfig = await loadConfig();
|
|
235
|
-
const existingKey =
|
|
236
|
-
process.env.SUBCONSCIOUS_API_KEY || existingConfig.subconscious_api_key;
|
|
237
|
-
|
|
238
|
-
if (existingKey) {
|
|
239
|
-
const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
|
|
240
|
-
console.log(`\n${c.yellow}Already logged in.${c.reset}`);
|
|
241
|
-
console.log(` Key: ${c.dim}${masked}${c.reset}`);
|
|
242
|
-
console.log(
|
|
243
|
-
`\n Run ${c.cyan}subconscious-cli logout${c.reset} first to switch accounts.\n`,
|
|
244
|
-
);
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
console.log();
|
|
249
|
-
console.log(
|
|
250
|
-
` ${c.magenta}${c.bold}Subconscious${c.reset} ${c.dim}— CLI Login${c.reset}`,
|
|
251
|
-
);
|
|
252
|
-
console.log();
|
|
253
|
-
|
|
254
|
-
const state = crypto.randomBytes(16).toString('hex');
|
|
255
|
-
const { port, promise } = await startCallbackServer(state);
|
|
256
|
-
|
|
257
|
-
const authUrl = `${PLATFORM_URL}/cli/auth?port=${port}&state=${state}`;
|
|
258
|
-
|
|
259
|
-
console.log(` ${c.dim}Opening browser to sign in...${c.reset}`);
|
|
260
|
-
console.log();
|
|
261
|
-
console.log(` ${c.dim}If it doesn't open, visit:${c.reset}`);
|
|
262
|
-
console.log(` ${c.underline}${c.cyan}${authUrl}${c.reset}`);
|
|
263
|
-
console.log();
|
|
264
|
-
|
|
265
|
-
openBrowser(authUrl);
|
|
266
|
-
|
|
267
|
-
// Spinner while waiting
|
|
268
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
269
|
-
let i = 0;
|
|
270
|
-
const spinner = setInterval(() => {
|
|
271
|
-
process.stdout.write(
|
|
272
|
-
`\r ${c.cyan}${frames[i++ % frames.length]}${c.reset} Waiting for authentication...`,
|
|
273
|
-
);
|
|
274
|
-
}, 80);
|
|
275
|
-
|
|
276
|
-
process.on('SIGINT', () => {
|
|
277
|
-
clearInterval(spinner);
|
|
278
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
279
|
-
console.log(`\n ${c.dim}Login cancelled.${c.reset}\n`);
|
|
280
|
-
process.exit(0);
|
|
281
|
-
});
|
|
282
|
-
try {
|
|
283
|
-
const result = await promise;
|
|
284
|
-
clearInterval(spinner);
|
|
285
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
286
|
-
|
|
287
|
-
const config = await loadConfig();
|
|
288
|
-
config.subconscious_api_key = result.token;
|
|
289
|
-
await saveConfig(config);
|
|
290
|
-
|
|
291
|
-
const masked = result.token.slice(0, 8) + '...' + result.token.slice(-4);
|
|
292
|
-
console.log(` ${c.green}${c.bold}✓ Logged in successfully!${c.reset}`);
|
|
293
|
-
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
294
|
-
console.log(` ${c.dim}Saved to ~/.subcon/config.json${c.reset}`);
|
|
295
|
-
console.log();
|
|
296
|
-
} catch (error) {
|
|
297
|
-
clearInterval(spinner);
|
|
298
|
-
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
299
|
-
console.error(` ${c.red}✗ ${error.message}${c.reset}\n`);
|
|
300
|
-
process.exit(1);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async function logoutCommand() {
|
|
305
|
-
const config = await loadConfig();
|
|
306
|
-
|
|
307
|
-
if (!config.subconscious_api_key) {
|
|
308
|
-
console.log(`\n ${c.dim}Not logged in.${c.reset}\n`);
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
delete config.subconscious_api_key;
|
|
313
|
-
await saveConfig(config);
|
|
314
|
-
|
|
315
|
-
console.log(
|
|
316
|
-
`\n ${c.green}✓${c.reset} Logged out. API key removed from ${c.dim}~/.subcon/config.json${c.reset}\n`,
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
async function whoamiCommand() {
|
|
321
|
-
const config = await loadConfig();
|
|
322
|
-
const envKey = process.env.SUBCONSCIOUS_API_KEY;
|
|
323
|
-
const savedKey = config.subconscious_api_key;
|
|
324
|
-
const key = envKey || savedKey;
|
|
325
|
-
|
|
326
|
-
if (!key) {
|
|
327
|
-
console.log(`\n ${c.dim}Not logged in.${c.reset}`);
|
|
328
|
-
console.log(
|
|
329
|
-
` Run ${c.cyan}subconscious-cli login${c.reset} to get started.\n`,
|
|
330
|
-
);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const masked = key.slice(0, 8) + '...' + key.slice(-4);
|
|
335
|
-
const source = envKey
|
|
336
|
-
? 'SUBCONSCIOUS_API_KEY env var'
|
|
337
|
-
: '~/.subcon/config.json';
|
|
338
|
-
|
|
339
|
-
console.log();
|
|
340
|
-
|
|
341
|
-
// Validate the key against the server; falls back to offline display if unreachable
|
|
342
|
-
try {
|
|
343
|
-
const res = await fetch(`${PLATFORM_URL}/api/cli/whoami`, {
|
|
344
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
345
|
-
signal: AbortSignal.timeout(5000),
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
if (res.ok) {
|
|
349
|
-
const data = await res.json();
|
|
350
|
-
console.log(` ${c.green}✓ Authenticated${c.reset}`);
|
|
351
|
-
if (data.organization) {
|
|
352
|
-
console.log(` ${c.dim}Org: ${c.reset}${data.organization}`);
|
|
353
|
-
}
|
|
354
|
-
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
355
|
-
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
356
|
-
} else {
|
|
357
|
-
console.log(` ${c.red}✗ Key is invalid or revoked${c.reset}`);
|
|
358
|
-
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
359
|
-
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
360
|
-
console.log();
|
|
361
|
-
console.log(
|
|
362
|
-
` Run ${c.cyan}subconscious-cli logout${c.reset} then ${c.cyan}subconscious-cli login${c.reset} to re-authenticate.`,
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
} catch {
|
|
366
|
-
console.log(` ${c.green}✓ Authenticated${c.reset} ${c.dim}(offline — key not verified)${c.reset}`);
|
|
367
|
-
console.log(` ${c.dim}Key: ${masked}${c.reset}`);
|
|
368
|
-
console.log(` ${c.dim}Source: ${source}${c.reset}`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
console.log();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// ── Help & entry ────────────────────────────────────────────────────────
|
|
13
|
+
import { c } from './colors.js';
|
|
14
|
+
import { loginCommand, logoutCommand, whoamiCommand } from './auth.js';
|
|
15
|
+
import { resolveAgent, runAgent, agentList } from './agents.js';
|
|
375
16
|
|
|
376
17
|
function printHelp() {
|
|
18
|
+
const agents = agentList()
|
|
19
|
+
.map(({ name, alias }) => ` ${c.cyan}${alias.padEnd(13)}${c.reset}${c.dim}Launch ${name}${c.reset}`)
|
|
20
|
+
.join('\n');
|
|
21
|
+
|
|
377
22
|
console.log(`
|
|
378
23
|
${c.magenta}${c.bold}Subconscious CLI${c.reset}
|
|
379
24
|
|
|
380
25
|
${c.bold}Usage${c.reset}
|
|
381
|
-
${c.cyan}subconscious
|
|
26
|
+
${c.cyan}subconscious${c.reset} <command> [...args]
|
|
27
|
+
|
|
28
|
+
${c.bold}Auth${c.reset}
|
|
29
|
+
${c.cyan}login${c.reset} Authenticate and save your API key
|
|
30
|
+
${c.cyan}logout${c.reset} Remove saved credentials
|
|
31
|
+
${c.cyan}whoami${c.reset} Show current authentication status
|
|
382
32
|
|
|
383
|
-
${c.bold}
|
|
384
|
-
|
|
385
|
-
${c.cyan}logout${c.reset} Remove saved credentials
|
|
386
|
-
${c.cyan}whoami${c.reset} Show current authentication status
|
|
33
|
+
${c.bold}Coding agents${c.reset}
|
|
34
|
+
${agents}
|
|
387
35
|
|
|
388
36
|
${c.bold}Options${c.reset}
|
|
37
|
+
${c.dim}--model <id>${c.reset} Model to use (default subconscious/tim-qwen3.6-27b)
|
|
389
38
|
${c.dim}-h, --help${c.reset} Show this help
|
|
390
39
|
${c.dim}-v, --version${c.reset} Show version
|
|
391
40
|
|
|
392
|
-
${c.bold}
|
|
393
|
-
${c.dim}$${c.reset}
|
|
41
|
+
${c.bold}Examples${c.reset}
|
|
42
|
+
${c.dim}$${c.reset} subconscious login
|
|
43
|
+
${c.dim}$${c.reset} subconscious claude-code
|
|
44
|
+
${c.dim}$${c.reset} subconscious open-code --model subconscious/tim-qwen3.6-27b
|
|
45
|
+
|
|
46
|
+
${c.dim}Anything after the agent name is forwarded to the underlying CLI.${c.reset}
|
|
394
47
|
`);
|
|
395
48
|
}
|
|
396
49
|
|
|
397
|
-
const
|
|
50
|
+
const authCommands = { login: loginCommand, logout: logoutCommand, whoami: whoamiCommand };
|
|
398
51
|
|
|
399
52
|
async function main() {
|
|
400
53
|
const args = process.argv.slice(2);
|
|
@@ -412,14 +65,21 @@ async function main() {
|
|
|
412
65
|
return;
|
|
413
66
|
}
|
|
414
67
|
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
process.exit(1);
|
|
68
|
+
const authHandler = authCommands[command];
|
|
69
|
+
if (authHandler) {
|
|
70
|
+
await authHandler(args.slice(1));
|
|
71
|
+
return;
|
|
420
72
|
}
|
|
421
73
|
|
|
422
|
-
|
|
74
|
+
const agent = resolveAgent(command);
|
|
75
|
+
if (agent) {
|
|
76
|
+
await runAgent(agent, args.slice(1));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.error(`\n ${c.red}Unknown command: ${command}${c.reset}`);
|
|
81
|
+
printHelp();
|
|
82
|
+
process.exit(1);
|
|
423
83
|
}
|
|
424
84
|
|
|
425
85
|
main().catch((error) => {
|
package/bin/colors.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ANSI color helpers shared across the CLI.
|
|
2
|
+
export const c = {
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
bold: '\x1b[1m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
cyan: '\x1b[36m',
|
|
7
|
+
green: '\x1b[32m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
magenta: '\x1b[35m',
|
|
11
|
+
underline: '\x1b[4m',
|
|
12
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subconscious-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for Subconscious —
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for Subconscious — log in and launch coding agents (Claude Code, OpenCode, Aider, Codex) on your hosted models",
|
|
5
5
|
"bin": {
|
|
6
|
+
"subconscious": "./bin/cli.js",
|
|
6
7
|
"subconscious-cli": "./bin/cli.js"
|
|
7
8
|
},
|
|
8
9
|
"files": [
|
|
@@ -22,7 +23,13 @@
|
|
|
22
23
|
"ai",
|
|
23
24
|
"cli",
|
|
24
25
|
"api-key",
|
|
25
|
-
"authentication"
|
|
26
|
+
"authentication",
|
|
27
|
+
"coding-agent",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"opencode",
|
|
30
|
+
"aider",
|
|
31
|
+
"codex",
|
|
32
|
+
"launcher"
|
|
26
33
|
],
|
|
27
34
|
"author": "Subconscious Systems",
|
|
28
35
|
"license": "MIT"
|