dotdotdot-cli 1.0.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.
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // dotdotdot — plain English to terminal commands
6
+ // ─────────────────────────────────────────────────────────────────────────────
7
+
8
+ // ─── Global cleanup: restore terminal on any exit ───────────────────────────
9
+ function cleanup() {
10
+ try {
11
+ if (process.stdin.isTTY && process.stdin.isRaw) process.stdin.setRawMode(false);
12
+ process.stdin.pause();
13
+ process.stdout.write('\x1b[?25h'); // restore cursor
14
+ } catch { /* ignore */ }
15
+ }
16
+ process.on('exit', cleanup);
17
+ process.on('SIGINT', () => { cleanup(); process.exit(130); });
18
+ process.on('SIGTERM', () => { cleanup(); process.exit(143); });
19
+
20
+ const args = process.argv.slice(2);
21
+
22
+ let userInput = '';
23
+ let flags = {
24
+ task: false, help: false,
25
+ config: false, debug: false, version: false, usage: false,
26
+ resetTokens: false, clearSession: false,
27
+ provider: null,
28
+ };
29
+
30
+ for (let i = 0; i < args.length; i++) {
31
+ const a = args[i];
32
+ switch (a) {
33
+ case '-t': case '--task': flags.task = true; break;
34
+ case '-p': case '--provider': if (args[i+1]) flags.provider = args[++i]; break;
35
+ case '-u': case '--usage': flags.usage = true; break;
36
+ case '-c': case '--config': flags.config = true; break;
37
+ case '-h': case '--help': flags.help = true; break;
38
+ case '-d': case '--debug': flags.debug = true; break;
39
+ case '-v': case '--version': flags.version = true; break;
40
+ case '--reset-usage': flags.resetTokens = true; break;
41
+ case '--clear': flags.clearSession = true; break;
42
+ default: if (!a.startsWith('-')) userInput += (userInput ? ' ' : '') + a; break;
43
+ }
44
+ }
45
+
46
+ (async () => {
47
+ try {
48
+ const pkg = require('../package.json');
49
+
50
+ if (flags.version) { console.log(pkg.version); process.exit(0); }
51
+ if (flags.resetTokens) {
52
+ require('../lib/tokens').resetUsage();
53
+ require('../lib/renderer').printSuccess('Token usage stats reset.');
54
+ process.exit(0);
55
+ }
56
+ if (flags.clearSession) {
57
+ require('../lib/session').clearSession();
58
+ require('../lib/renderer').printSuccess('Session cleared. Context will be fresh on next run.');
59
+ process.exit(0);
60
+ }
61
+ if (flags.usage) {
62
+ await require('../lib/tokens').printTokenStatsInteractive();
63
+ process.exit(0);
64
+ }
65
+ if (flags.help || !userInput.trim() && !flags.config) {
66
+ require('../lib/ui').printHelp(); process.exit(0);
67
+ }
68
+
69
+ const { loadConfig, resolveProvider, getAllProviderIds } = require('../lib/config');
70
+ const config = loadConfig();
71
+ if (flags.provider) {
72
+ const resolved = resolveProvider(flags.provider);
73
+ if (!resolved) {
74
+ require('../lib/renderer').printError(`Unknown provider "${flags.provider}". Use: ${getAllProviderIds().join(', ')}`);
75
+ process.exit(1);
76
+ }
77
+ config.provider = resolved;
78
+ const a = config.providers[resolved];
79
+ if (a) { config.apiKey = a.apiKey; config.model = a.model; config.apiUrl = a.apiUrl; }
80
+ }
81
+
82
+ if (flags.config) { await require('../lib/ui').printConfig(config); process.exit(0); }
83
+
84
+ if (!config.apiKey) {
85
+ const { printError } = require('../lib/renderer');
86
+ printError(`No API key for ${config.provider}. Run ${require('../lib/colors').cyan('... -c')}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ // ─── Gather context ───────────────────────────────────────────────────
91
+ const { gatherContext } = require('../lib/context');
92
+ const { Spinner } = require('../lib/renderer');
93
+ const { detectMode, queryLLM } = require('../lib/llm');
94
+ const { setUserIntent } = require('../lib/session');
95
+ const { recordUsage } = require('../lib/tokens');
96
+
97
+ const t0 = Date.now();
98
+ const spin = new Spinner('...').start();
99
+ const context = await gatherContext();
100
+ const ctxMs = Date.now() - t0;
101
+
102
+ // ─── Detect mode ──────────────────────────────────────────────────────
103
+ const mode = flags.task ? 'task' : detectMode(userInput);
104
+
105
+ if (flags.debug) {
106
+ spin.succeed(`ctx ${ctxMs}ms | ${config.provider}/${config.model} | mode: ${mode}`);
107
+ }
108
+
109
+ // ─── Task mode ────────────────────────────────────────────────────────
110
+ if (mode === 'task') {
111
+ if (!flags.debug) spin.succeed('ready');
112
+ setUserIntent(userInput);
113
+ const { runTask } = require('../lib/planner');
114
+ await runTask(userInput, context, config, { debug: flags.debug });
115
+ return;
116
+ }
117
+
118
+ // ─── Quick mode ───────────────────────────────────────────────────────
119
+ spin.update('thinking...');
120
+ let result;
121
+ try {
122
+ result = await queryLLM(userInput, context, config, 'quick');
123
+ } catch (err) {
124
+ spin.fail(err.message);
125
+ if (flags.debug && err.debugLog) {
126
+ const { subtle } = require('../lib/renderer');
127
+ process.stderr.write(` ${subtle('log: ' + err.debugLog)}\n`);
128
+ }
129
+ process.exit(1);
130
+ }
131
+
132
+ const totalMs = Date.now() - t0;
133
+
134
+ // Record token usage
135
+ const tokenUsage = result?._tokenUsage;
136
+ if (tokenUsage) {
137
+ recordUsage(tokenUsage, config.provider, config.model);
138
+ }
139
+
140
+ if (flags.debug) {
141
+ spin.succeed(`done ${totalMs}ms`);
142
+ if (result?._debugLog) {
143
+ const { subtle } = require('../lib/renderer');
144
+ process.stderr.write(` ${subtle('log: ' + result._debugLog)}\n`);
145
+ }
146
+ } else {
147
+ spin.succeed('done');
148
+ }
149
+
150
+ if (!result?.command) {
151
+ require('../lib/renderer').printError('No command generated. Try rephrasing.');
152
+ process.exit(1);
153
+ }
154
+
155
+ setUserIntent(userInput);
156
+
157
+ if (config.autoExec) {
158
+ const { executeMode } = require('../lib/executor');
159
+ await executeMode(result, config);
160
+ } else {
161
+ const { interactiveMode } = require('../lib/executor');
162
+ await interactiveMode(result, config, context);
163
+ }
164
+
165
+ } catch (err) {
166
+ require('../lib/renderer').printError(err.message || 'Unexpected error');
167
+ if (flags.debug) console.error(err.stack);
168
+ process.exit(1);
169
+ }
170
+ })();
package/lib/colors.js ADDED
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // colors.js — Zero-dependency ANSI color & styling system
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ const isColorSupported = (() => {
8
+ if (process.env.NO_COLOR) return false;
9
+ if (process.env.FORCE_COLOR) return true;
10
+ if (!process.stdout.isTTY) return false;
11
+ if (process.platform === 'win32') return true; // Windows 10+ supports ANSI
12
+ const term = process.env.TERM || '';
13
+ return term !== 'dumb';
14
+ })();
15
+
16
+ const wrap = (open, close) => {
17
+ if (!isColorSupported) return (s) => s;
18
+ return (s) => `\x1b[${open}m${s}\x1b[${close}m`;
19
+ };
20
+
21
+ const rgb = (r, g, b) => {
22
+ if (!isColorSupported) return (s) => s;
23
+ return (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
24
+ };
25
+
26
+ const bgRgb = (r, g, b) => {
27
+ if (!isColorSupported) return (s) => s;
28
+ return (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
29
+ };
30
+
31
+ const c256 = (n) => {
32
+ if (!isColorSupported) return (s) => s;
33
+ return (s) => `\x1b[38;5;${n}m${s}\x1b[39m`;
34
+ };
35
+
36
+ const bg256 = (n) => {
37
+ if (!isColorSupported) return (s) => s;
38
+ return (s) => `\x1b[48;5;${n}m${s}\x1b[49m`;
39
+ };
40
+
41
+ // ─── Core styles ─────────────────────────────────────────────────────────────
42
+
43
+ const bold = wrap(1, 22);
44
+ const dim = wrap(2, 22);
45
+ const italic = wrap(3, 23);
46
+ const underline = wrap(4, 24);
47
+ const inverse = wrap(7, 27);
48
+ const strikethrough = wrap(9, 29);
49
+
50
+ // ─── Standard colors ─────────────────────────────────────────────────────────
51
+
52
+ const black = wrap(30, 39);
53
+ const red = wrap(31, 39);
54
+ const green = wrap(32, 39);
55
+ const yellow = wrap(33, 39);
56
+ const blue = wrap(34, 39);
57
+ const magenta = wrap(35, 39);
58
+ const cyan = wrap(36, 39);
59
+ const white = wrap(37, 39);
60
+ const gray = wrap(90, 39);
61
+
62
+ // ─── Bright colors ──────────────────────────────────────────────────────────
63
+
64
+ const brightRed = wrap(91, 39);
65
+ const brightGreen = wrap(92, 39);
66
+ const brightYellow = wrap(93, 39);
67
+ const brightBlue = wrap(94, 39);
68
+ const brightMagenta = wrap(95, 39);
69
+ const brightCyan = wrap(96, 39);
70
+ const brightWhite = wrap(97, 39);
71
+
72
+ // ─── Background colors ──────────────────────────────────────────────────────
73
+
74
+ const bgBlack = wrap(40, 49);
75
+ const bgRed = wrap(41, 49);
76
+ const bgGreen = wrap(42, 49);
77
+ const bgYellow = wrap(43, 49);
78
+ const bgBlue = wrap(44, 49);
79
+ const bgMagenta = wrap(45, 49);
80
+ const bgCyan = wrap(46, 49);
81
+ const bgWhite = wrap(47, 49);
82
+
83
+ // ─── Semantic colors (for the ... UI) ────────────────────────────────────────
84
+
85
+ const themes = {
86
+ default: {
87
+ primary: cyan,
88
+ secondary: blue,
89
+ accent: magenta,
90
+ success: green,
91
+ warning: yellow,
92
+ danger: red,
93
+ info: brightCyan,
94
+ muted: dim,
95
+ highlight: brightWhite,
96
+ command: (s) => bold(brightWhite(s)),
97
+ step: (s) => bold(cyan(s)),
98
+ stepNum: (s) => bold(brightCyan(s)),
99
+ label: (s) => bold(gray(s)),
100
+ value: brightWhite,
101
+ separator: (s) => gray(s),
102
+ box: {
103
+ border: gray,
104
+ bg: bg256(236),
105
+ title: (s) => bold(cyan(s)),
106
+ },
107
+ spinner: cyan,
108
+ prompt: (s) => bold(cyan(s)),
109
+ selected: (s) => bold(cyan(s)),
110
+ unselected: gray,
111
+ },
112
+ midnight: {
113
+ primary: brightBlue,
114
+ secondary: magenta,
115
+ accent: brightMagenta,
116
+ success: brightGreen,
117
+ warning: brightYellow,
118
+ danger: brightRed,
119
+ info: brightCyan,
120
+ muted: dim,
121
+ highlight: brightWhite,
122
+ command: (s) => bold(brightWhite(s)),
123
+ step: (s) => bold(brightBlue(s)),
124
+ stepNum: (s) => bold(brightMagenta(s)),
125
+ label: (s) => bold(gray(s)),
126
+ value: brightWhite,
127
+ separator: (s) => gray(s),
128
+ box: {
129
+ border: (s) => c256(60)(s),
130
+ bg: bg256(234),
131
+ title: (s) => bold(brightBlue(s)),
132
+ },
133
+ spinner: brightMagenta,
134
+ prompt: (s) => bold(brightBlue(s)),
135
+ selected: (s) => bold(brightMagenta(s)),
136
+ unselected: gray,
137
+ },
138
+ minimal: {
139
+ primary: white,
140
+ secondary: gray,
141
+ accent: white,
142
+ success: green,
143
+ warning: yellow,
144
+ danger: red,
145
+ info: white,
146
+ muted: dim,
147
+ highlight: bold,
148
+ command: (s) => bold(s),
149
+ step: (s) => bold(s),
150
+ stepNum: (s) => bold(s),
151
+ label: dim,
152
+ value: (s) => s,
153
+ separator: dim,
154
+ box: {
155
+ border: dim,
156
+ bg: (s) => s,
157
+ title: bold,
158
+ },
159
+ spinner: white,
160
+ prompt: bold,
161
+ selected: bold,
162
+ unselected: dim,
163
+ },
164
+ };
165
+
166
+ function getTheme(name) {
167
+ return themes[name] || themes.default;
168
+ }
169
+
170
+ // ─── Utility: strip ANSI ────────────────────────────────────────────────────
171
+
172
+ function stripAnsi(s) {
173
+ // eslint-disable-next-line no-control-regex
174
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
175
+ }
176
+
177
+ function visibleLength(s) {
178
+ return stripAnsi(s).length;
179
+ }
180
+
181
+ // ─── Symbols (with fallback for non-Unicode terminals) ──────────────────────
182
+
183
+ const isUnicode = (() => {
184
+ const enc = (process.env.LANG || '').toLowerCase();
185
+ return process.platform === 'win32' || enc.includes('utf') || enc.includes('unicode');
186
+ })();
187
+
188
+ const symbols = {
189
+ dot: isUnicode ? '\u2022' : '*', // bullet
190
+ ellipsis: isUnicode ? '\u2026' : '...',
191
+ arrow: isUnicode ? '\u276F' : '>', // ❯
192
+ arrowDown: isUnicode ? '\u25BC' : 'v',
193
+ arrowRight:isUnicode ? '\u25B6' : '>',
194
+ check: isUnicode ? '\u2714' : '+', // check mark
195
+ cross: isUnicode ? '\u2718' : 'x', // cross mark
196
+ warning: isUnicode ? '\u26A0' : '!', // warning sign
197
+ info: isUnicode ? '\u2139' : 'i', // info sign
198
+ star: isUnicode ? '\u2605' : '*',
199
+ play: isUnicode ? '\u25B6' : '>',
200
+ pause: isUnicode ? '\u23F8' : '||',
201
+ gear: isUnicode ? '\u2699' : '#',
202
+ lightning: isUnicode ? '\u26A1' : '!',
203
+ folder: isUnicode ? '\uD83D\uDCC1' : '[D]',
204
+ file: isUnicode ? '\uD83D\uDCC4' : '[F]',
205
+ lock: isUnicode ? '\uD83D\uDD12' : '[L]',
206
+ rocket: isUnicode ? '\uD83D\uDE80' : '=>',
207
+ // Box drawing
208
+ topLeft: isUnicode ? '\u256D' : '+',
209
+ topRight: isUnicode ? '\u256E' : '+',
210
+ bottomLeft: isUnicode ? '\u2570' : '+',
211
+ bottomRight:isUnicode ? '\u256F' : '+',
212
+ horizontal: isUnicode ? '\u2500' : '-',
213
+ vertical: isUnicode ? '\u2502' : '|',
214
+ teeRight: isUnicode ? '\u251C' : '|',
215
+ teeLeft: isUnicode ? '\u2524' : '|',
216
+ // Spinner frames
217
+ spinnerFrames: isUnicode
218
+ ? ['\u280B','\u2819','\u2839','\u2838','\u283C','\u2834','\u2826','\u2827','\u2807','\u280F']
219
+ : ['-', '\\', '|', '/'],
220
+ // Step indicators
221
+ stepDone: isUnicode ? '\u25C9' : '(x)', // ◉
222
+ stepCurrent: isUnicode ? '\u25CB' : '( )', // ○
223
+ stepPending: isUnicode ? '\u25CC' : '(.)', // ◌
224
+ stepSkipped: isUnicode ? '\u25CB' : '(-)',
225
+ };
226
+
227
+ module.exports = {
228
+ // Core
229
+ isColorSupported,
230
+ wrap, rgb, bgRgb, c256, bg256,
231
+ // Styles
232
+ bold, dim, italic, underline, inverse, strikethrough,
233
+ // Colors
234
+ black, red, green, yellow, blue, magenta, cyan, white, gray,
235
+ brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite,
236
+ // Backgrounds
237
+ bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite,
238
+ // Theming
239
+ themes, getTheme,
240
+ // Utility
241
+ stripAnsi, visibleLength,
242
+ // Symbols
243
+ symbols,
244
+ };
package/lib/config.js ADDED
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+ // config.js — Configuration management with multi-provider support
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const CONFIG_DIR = path.join(os.homedir(), '.dotdotdot');
12
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
13
+ const CACHE_DIR = path.join(CONFIG_DIR, 'cache');
14
+ const DEBUG_DIR = path.join(CONFIG_DIR, 'debug');
15
+ const SESSION_DIR = path.join(CONFIG_DIR, 'sessions');
16
+
17
+ // ─── Provider Defaults ──────────────────────────────────────────────────────
18
+
19
+ const PROVIDERS = {
20
+ openrouter: {
21
+ name: 'OpenRouter',
22
+ apiUrl: 'https://openrouter.ai/api/v1/chat/completions',
23
+ model: 'google/gemma-4-26b-a4b-it',
24
+ envKeys: ['DOT_OPENROUTER_KEY'],
25
+ models: [
26
+ 'google/gemma-4-26b-a4b-it',
27
+ 'google/gemini-2.5-flash-preview:thinking',
28
+ 'openai/gpt-4o-mini',
29
+ 'anthropic/claude-3.5-haiku',
30
+ 'meta-llama/llama-3.1-70b-instruct',
31
+ 'google/gemini-2.0-flash-001',
32
+ ],
33
+ },
34
+ anthropic: {
35
+ name: 'Anthropic',
36
+ apiUrl: 'https://api.anthropic.com/v1/messages',
37
+ model: 'claude-haiku-4-5-20251001',
38
+ envKeys: ['DOT_ANTHROPIC_KEY'],
39
+ models: [
40
+ 'claude-haiku-4-5-20251001',
41
+ 'claude-sonnet-4-20250514',
42
+ 'claude-3-5-haiku-20241022',
43
+ ],
44
+ },
45
+ openai: {
46
+ name: 'OpenAI',
47
+ apiUrl: 'https://api.openai.com/v1/chat/completions',
48
+ model: 'gpt-4o-mini',
49
+ envKeys: ['DOT_OPENAI_KEY'],
50
+ models: [
51
+ 'gpt-4o-mini',
52
+ 'gpt-4o',
53
+ 'gpt-4-turbo',
54
+ ],
55
+ },
56
+ google: {
57
+ name: 'Google Gemini',
58
+ apiUrl: 'https://generativelanguage.googleapis.com/v1beta/models',
59
+ model: 'gemini-2.0-flash',
60
+ envKeys: ['DOT_GOOGLE_KEY'],
61
+ models: [
62
+ 'gemini-2.0-flash',
63
+ 'gemini-2.0-flash-lite',
64
+ 'gemini-1.5-pro',
65
+ ],
66
+ },
67
+ custom: {
68
+ name: 'Custom',
69
+ apiUrl: '',
70
+ model: '',
71
+ envKeys: ['DOT_CUSTOM_KEY'],
72
+ models: [],
73
+ },
74
+ };
75
+
76
+ // ─── Ensure directories exist ────────────────────────────────────────────────
77
+
78
+ function ensureDirs() {
79
+ for (const dir of [CONFIG_DIR, CACHE_DIR, DEBUG_DIR, SESSION_DIR]) {
80
+ if (!fs.existsSync(dir)) {
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ }
83
+ }
84
+ }
85
+
86
+ // ─── Load config ────────────────────────────────────────────────────────────
87
+
88
+ function loadConfig() {
89
+ ensureDirs();
90
+
91
+ let fileData = {};
92
+ try {
93
+ const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
94
+ fileData = JSON.parse(raw);
95
+ } catch { /* no config file yet */ }
96
+
97
+ // Migrate old flat format
98
+ if (fileData.apiKey && !fileData.providers) {
99
+ fileData = {
100
+ provider: 'anthropic',
101
+ providers: {
102
+ anthropic: {
103
+ apiKey: fileData.apiKey,
104
+ model: fileData.model || PROVIDERS.anthropic.model,
105
+ apiUrl: fileData.apiUrl || PROVIDERS.anthropic.apiUrl,
106
+ },
107
+ },
108
+ };
109
+ _saveRaw(fileData);
110
+ }
111
+
112
+ // Build config
113
+ const config = {
114
+ provider: fileData.provider || 'openrouter',
115
+ autoExec: fileData.autoExec || false,
116
+ providers: {},
117
+ pricing: fileData.pricing || {},
118
+ };
119
+
120
+ // Merge provider configs
121
+ for (const [id, defaults] of Object.entries(PROVIDERS)) {
122
+ const saved = (fileData.providers || {})[id] || {};
123
+ config.providers[id] = {
124
+ apiKey: saved.apiKey || '',
125
+ model: saved.model || defaults.model,
126
+ apiUrl: saved.apiUrl || defaults.apiUrl,
127
+ };
128
+ }
129
+
130
+ // Environment variable overrides
131
+ if (process.env.DOT_PROVIDER) {
132
+ config.provider = process.env.DOT_PROVIDER;
133
+ }
134
+
135
+ for (const [id, defaults] of Object.entries(PROVIDERS)) {
136
+ for (const envKey of defaults.envKeys) {
137
+ if (process.env[envKey]) {
138
+ config.providers[id].apiKey = process.env[envKey];
139
+ break;
140
+ }
141
+ }
142
+ }
143
+
144
+ if (process.env.DOT_MODEL) {
145
+ config.providers[config.provider].model = process.env.DOT_MODEL;
146
+ }
147
+
148
+ // Resolve active provider's values to top-level for convenience
149
+ const active = config.providers[config.provider] || {};
150
+ config.apiKey = active.apiKey || '';
151
+ config.model = active.model || '';
152
+ config.apiUrl = active.apiUrl || '';
153
+
154
+ return config;
155
+ }
156
+
157
+ // ─── Save config ────────────────────────────────────────────────────────────
158
+
159
+ function saveConfig(config) {
160
+ ensureDirs();
161
+ const data = {
162
+ provider: config.provider,
163
+ autoExec: config.autoExec || false,
164
+ providers: {},
165
+ pricing: config.pricing || {},
166
+ };
167
+ for (const [id, prov] of Object.entries(config.providers)) {
168
+ data.providers[id] = {
169
+ apiKey: prov.apiKey || '',
170
+ model: prov.model || '',
171
+ apiUrl: prov.apiUrl || '',
172
+ };
173
+ }
174
+ _saveRaw(data);
175
+ }
176
+
177
+ function _saveRaw(data) {
178
+ ensureDirs();
179
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
180
+ }
181
+
182
+ // ─── Utility ────────────────────────────────────────────────────────────────
183
+
184
+ function maskKey(key) {
185
+ if (!key) return '(not set)';
186
+ if (key.length <= 8) return '****';
187
+ return key.slice(0, 4) + '...' + key.slice(-4);
188
+ }
189
+
190
+ function getProviderInfo(id) {
191
+ return PROVIDERS[id] || null;
192
+ }
193
+
194
+ function getAllProviderIds() {
195
+ return Object.keys(PROVIDERS);
196
+ }
197
+
198
+ // Resolve short names / aliases to provider id
199
+ // e.g. "anthropic" "claude" "ant" → "anthropic"
200
+ const ALIASES = {
201
+ or: 'openrouter', router: 'openrouter', openrouter: 'openrouter',
202
+ ant: 'anthropic', claude: 'anthropic', anthropic: 'anthropic',
203
+ oai: 'openai', gpt: 'openai', openai: 'openai',
204
+ gem: 'google', gemini: 'google', google: 'google',
205
+ custom: 'custom',
206
+ };
207
+
208
+ function resolveProvider(name) {
209
+ if (!name) return null;
210
+ const lower = name.toLowerCase().trim();
211
+ return ALIASES[lower] || (PROVIDERS[lower] ? lower : null);
212
+ }
213
+
214
+ module.exports = {
215
+ CONFIG_DIR, CONFIG_FILE, CACHE_DIR, DEBUG_DIR, SESSION_DIR,
216
+ PROVIDERS,
217
+ loadConfig,
218
+ saveConfig,
219
+ ensureDirs,
220
+ maskKey,
221
+ getProviderInfo,
222
+ getAllProviderIds,
223
+ resolveProvider,
224
+ };