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.
- package/LICENSE +21 -0
- package/README.md +707 -0
- package/bin/dotdotdot.js +170 -0
- package/lib/colors.js +244 -0
- package/lib/config.js +224 -0
- package/lib/context.js +265 -0
- package/lib/executor.js +274 -0
- package/lib/index.js +16 -0
- package/lib/llm.js +471 -0
- package/lib/menu.js +100 -0
- package/lib/planner.js +169 -0
- package/lib/postinstall.js +20 -0
- package/lib/renderer.js +145 -0
- package/lib/safety.js +71 -0
- package/lib/session.js +165 -0
- package/lib/tokens.js +291 -0
- package/lib/ui.js +207 -0
- package/package.json +56 -0
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// tokens.js — Token usage tracking & cumulative stats
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { CONFIG_DIR, ensureDirs } = require('./config');
|
|
10
|
+
const { dim, cyan, yellow, green, gray, bold, brightWhite, c256 } = require('./colors');
|
|
11
|
+
|
|
12
|
+
const TOKENS_FILE = path.join(CONFIG_DIR, 'token-usage.json');
|
|
13
|
+
|
|
14
|
+
// ─── Pricing per 1M tokens (USD) ───────────────────────────────────────────
|
|
15
|
+
// Format: { input: $/1M input tokens, output: $/1M output tokens }
|
|
16
|
+
// Prices sourced from provider pricing pages. Update as needed.
|
|
17
|
+
|
|
18
|
+
const PRICING = {
|
|
19
|
+
// ── OpenRouter models ──
|
|
20
|
+
'google/gemma-4-26b-a4b-it': { input: 0.10, output: 0.10 },
|
|
21
|
+
'google/gemini-2.5-flash-preview:thinking': { input: 0.15, output: 3.50 },
|
|
22
|
+
'openai/gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
23
|
+
'anthropic/claude-3.5-haiku': { input: 0.80, output: 4.00 },
|
|
24
|
+
'meta-llama/llama-3.1-70b-instruct': { input: 0.40, output: 0.40 },
|
|
25
|
+
'google/gemini-2.0-flash-001': { input: 0.10, output: 0.40 },
|
|
26
|
+
|
|
27
|
+
// ── Anthropic direct ──
|
|
28
|
+
'claude-haiku-4-5-20251001': { input: 0.80, output: 4.00 },
|
|
29
|
+
'claude-sonnet-4-20250514': { input: 3.00, output: 15.00 },
|
|
30
|
+
'claude-3-5-haiku-20241022': { input: 0.80, output: 4.00 },
|
|
31
|
+
|
|
32
|
+
// ── OpenAI direct ──
|
|
33
|
+
'gpt-4o-mini': { input: 0.15, output: 0.60 },
|
|
34
|
+
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
35
|
+
'gpt-4-turbo': { input: 10.00, output: 30.00 },
|
|
36
|
+
|
|
37
|
+
// ── Google Gemini direct ──
|
|
38
|
+
'gemini-2.0-flash': { input: 0.10, output: 0.40 },
|
|
39
|
+
'gemini-2.0-flash-lite': { input: 0.075, output: 0.30 },
|
|
40
|
+
'gemini-1.5-pro': { input: 1.25, output: 5.00 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ─── User-defined pricing (from config.json) ───────────────────────────────
|
|
44
|
+
|
|
45
|
+
function loadUserPricing() {
|
|
46
|
+
try {
|
|
47
|
+
const { loadConfig } = require('./config');
|
|
48
|
+
const config = loadConfig();
|
|
49
|
+
return config.pricing || {};
|
|
50
|
+
} catch { return {}; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check user pricing first, then hardcoded, with fuzzy fallback
|
|
54
|
+
function getModelPricing(model) {
|
|
55
|
+
// 1. User-defined exact match
|
|
56
|
+
const userPricing = loadUserPricing();
|
|
57
|
+
if (userPricing[model]) return userPricing[model];
|
|
58
|
+
|
|
59
|
+
// 2. Hardcoded exact match
|
|
60
|
+
if (PRICING[model]) return PRICING[model];
|
|
61
|
+
|
|
62
|
+
// 3. Fuzzy match across both (user first, then hardcoded)
|
|
63
|
+
for (const [key, val] of Object.entries(userPricing)) {
|
|
64
|
+
if (key.includes(model) || model.includes(key)) return val;
|
|
65
|
+
}
|
|
66
|
+
for (const [key, val] of Object.entries(PRICING)) {
|
|
67
|
+
if (key.includes(model) || model.includes(key)) return val;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveUserPricing(model, input, output) {
|
|
73
|
+
const { loadConfig, saveConfig } = require('./config');
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
if (!config.pricing) config.pricing = {};
|
|
76
|
+
config.pricing[model] = { input, output };
|
|
77
|
+
saveConfig(config);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Estimate cost for a single request ─────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function estimateCost(tokenUsage, provider, model) {
|
|
83
|
+
if (!tokenUsage) return null;
|
|
84
|
+
const pricing = getModelPricing(model);
|
|
85
|
+
if (!pricing) return null;
|
|
86
|
+
const inputCost = (tokenUsage.inputTokens / 1_000_000) * pricing.input;
|
|
87
|
+
const outputCost = (tokenUsage.outputTokens / 1_000_000) * pricing.output;
|
|
88
|
+
const total = inputCost + outputCost;
|
|
89
|
+
if (total < 0.000001) return '0.000000';
|
|
90
|
+
if (total < 0.01) return total.toFixed(6);
|
|
91
|
+
if (total < 1) return total.toFixed(4);
|
|
92
|
+
return total.toFixed(2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Load / Save ────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function loadUsage() {
|
|
98
|
+
try {
|
|
99
|
+
const raw = fs.readFileSync(TOKENS_FILE, 'utf8');
|
|
100
|
+
return JSON.parse(raw);
|
|
101
|
+
} catch {
|
|
102
|
+
return {
|
|
103
|
+
totalInputTokens: 0,
|
|
104
|
+
totalOutputTokens: 0,
|
|
105
|
+
totalTokens: 0,
|
|
106
|
+
requestCount: 0,
|
|
107
|
+
history: [], // last 50 entries
|
|
108
|
+
firstUsed: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function saveUsage(data) {
|
|
114
|
+
ensureDirs();
|
|
115
|
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Record a request ───────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function recordUsage(tokenUsage, provider, model) {
|
|
121
|
+
if (!tokenUsage) return;
|
|
122
|
+
|
|
123
|
+
const cost = estimateCost(tokenUsage, provider, model);
|
|
124
|
+
const costNum = cost ? parseFloat(cost) : 0;
|
|
125
|
+
|
|
126
|
+
const data = loadUsage();
|
|
127
|
+
const entry = {
|
|
128
|
+
inputTokens: tokenUsage.inputTokens || 0,
|
|
129
|
+
outputTokens: tokenUsage.outputTokens || 0,
|
|
130
|
+
totalTokens: tokenUsage.totalTokens || 0,
|
|
131
|
+
estimated: !!tokenUsage.estimated,
|
|
132
|
+
cost: costNum,
|
|
133
|
+
provider,
|
|
134
|
+
model,
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
data.totalInputTokens += entry.inputTokens;
|
|
139
|
+
data.totalOutputTokens += entry.outputTokens;
|
|
140
|
+
data.totalTokens += entry.totalTokens;
|
|
141
|
+
data.totalCost = (data.totalCost || 0) + costNum;
|
|
142
|
+
data.requestCount += 1;
|
|
143
|
+
if (!data.firstUsed) data.firstUsed = entry.timestamp;
|
|
144
|
+
|
|
145
|
+
// Keep last 50 entries
|
|
146
|
+
data.history.push(entry);
|
|
147
|
+
while (data.history.length > 50) data.history.shift();
|
|
148
|
+
|
|
149
|
+
saveUsage(data);
|
|
150
|
+
return entry;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Format token count for display ─────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
function formatTokens(n) {
|
|
156
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
157
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
158
|
+
return String(n);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Inline display (after a request) ───────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
const subtle = c256(240);
|
|
164
|
+
const tokenColor = c256(39);
|
|
165
|
+
|
|
166
|
+
function tokenLine(tokenUsage) {
|
|
167
|
+
if (!tokenUsage) return '';
|
|
168
|
+
const est = tokenUsage.estimated ? ' ~' : '';
|
|
169
|
+
const input = formatTokens(tokenUsage.inputTokens);
|
|
170
|
+
const output = formatTokens(tokenUsage.outputTokens);
|
|
171
|
+
const total = formatTokens(tokenUsage.totalTokens);
|
|
172
|
+
return `${subtle('tokens:')} ${tokenColor(input)}${subtle(' in')} ${tokenColor(output)}${subtle(' out')} ${subtle('(')}${tokenColor(total)}${subtle(' total')}${est}${subtle(')')}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Full stats display (for --tokens flag) ─────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function formatCost(cost) {
|
|
178
|
+
if (!cost || cost === 0) return '$0.00';
|
|
179
|
+
if (cost < 0.01) return `$${cost.toFixed(6)}`;
|
|
180
|
+
if (cost < 1) return `$${cost.toFixed(4)}`;
|
|
181
|
+
return `$${cost.toFixed(2)}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function printTokenStats() {
|
|
185
|
+
const { printBanner } = require('./renderer');
|
|
186
|
+
const data = loadUsage();
|
|
187
|
+
|
|
188
|
+
printBanner();
|
|
189
|
+
|
|
190
|
+
if (data.requestCount === 0) {
|
|
191
|
+
console.log();
|
|
192
|
+
console.log(` ${subtle('No usage recorded yet. Start with:')} ${brightWhite('... <your request>')}`);
|
|
193
|
+
console.log();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const avgInput = Math.round(data.totalInputTokens / data.requestCount);
|
|
198
|
+
const avgOutput = Math.round(data.totalOutputTokens / data.requestCount);
|
|
199
|
+
const avgTotal = Math.round(data.totalTokens / data.requestCount);
|
|
200
|
+
const totalCost = data.totalCost || 0;
|
|
201
|
+
const avgCost = totalCost / data.requestCount;
|
|
202
|
+
const costColor = c256(220); // gold
|
|
203
|
+
const headerColor = c256(39); // bright blue
|
|
204
|
+
const lineChar = '\u2500';
|
|
205
|
+
const w = 44;
|
|
206
|
+
|
|
207
|
+
console.log();
|
|
208
|
+
|
|
209
|
+
// ── Totals ──
|
|
210
|
+
console.log(` ${headerColor(bold('Totals'))}`);
|
|
211
|
+
console.log(` ${subtle(lineChar.repeat(w))}`);
|
|
212
|
+
console.log(` ${subtle('Requests')} ${brightWhite(String(data.requestCount))}`);
|
|
213
|
+
console.log(` ${subtle('Input')} ${tokenColor(formatTokens(data.totalInputTokens))} ${subtle('tokens')}`);
|
|
214
|
+
console.log(` ${subtle('Output')} ${tokenColor(formatTokens(data.totalOutputTokens))} ${subtle('tokens')}`);
|
|
215
|
+
console.log(` ${subtle('Combined')} ${tokenColor(formatTokens(data.totalTokens))} ${subtle('tokens')}`);
|
|
216
|
+
console.log(` ${subtle('Cost')} ${costColor(formatCost(totalCost))}`);
|
|
217
|
+
console.log();
|
|
218
|
+
|
|
219
|
+
// ── Averages ──
|
|
220
|
+
console.log(` ${headerColor(bold('Per Request'))}`);
|
|
221
|
+
console.log(` ${subtle(lineChar.repeat(w))}`);
|
|
222
|
+
console.log(` ${subtle('Input')} ${tokenColor(formatTokens(avgInput))}`);
|
|
223
|
+
console.log(` ${subtle('Output')} ${tokenColor(formatTokens(avgOutput))}`);
|
|
224
|
+
console.log(` ${subtle('Combined')} ${tokenColor(formatTokens(avgTotal))}`);
|
|
225
|
+
console.log(` ${subtle('Cost')} ${costColor(formatCost(avgCost))}`);
|
|
226
|
+
|
|
227
|
+
// ── Recent ──
|
|
228
|
+
if (data.history.length > 0) {
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(` ${headerColor(bold('Recent'))}`);
|
|
231
|
+
console.log(` ${subtle(lineChar.repeat(w))}`);
|
|
232
|
+
const recent = data.history.slice(-5);
|
|
233
|
+
for (let i = 0; i < recent.length; i++) {
|
|
234
|
+
const entry = recent[i];
|
|
235
|
+
const date = new Date(entry.timestamp);
|
|
236
|
+
const ts = `${(date.getMonth()+1).toString().padStart(2,'0')}/${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}`;
|
|
237
|
+
const model = entry.model.split('/').pop();
|
|
238
|
+
const est = entry.estimated ? subtle('~') : '';
|
|
239
|
+
const entryCost = entry.cost ? costColor(formatCost(entry.cost)) : subtle('—');
|
|
240
|
+
console.log(` ${subtle(ts)} ${tokenColor(formatTokens(entry.totalTokens))}${est} ${subtle('tokens')} ${entryCost} ${subtle(model)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Footer ──
|
|
245
|
+
if (data.firstUsed) {
|
|
246
|
+
console.log();
|
|
247
|
+
console.log(` ${subtle('tracking since ' + new Date(data.firstUsed).toLocaleDateString())}`);
|
|
248
|
+
}
|
|
249
|
+
console.log();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function printTokenStatsInteractive() {
|
|
253
|
+
const { selectMenu } = require('./menu');
|
|
254
|
+
const { printSuccess } = require('./renderer');
|
|
255
|
+
|
|
256
|
+
printTokenStats();
|
|
257
|
+
|
|
258
|
+
const choice = await selectMenu([
|
|
259
|
+
{ label: 'Exit', key: 'q' },
|
|
260
|
+
{ label: 'Reset usage', key: 'r' },
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
if (choice === 'r') {
|
|
264
|
+
resetUsage();
|
|
265
|
+
printSuccess('Usage stats reset.');
|
|
266
|
+
console.log();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Reset stats ────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function resetUsage() {
|
|
273
|
+
try {
|
|
274
|
+
fs.unlinkSync(TOKENS_FILE);
|
|
275
|
+
} catch { /* ignore */ }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
recordUsage,
|
|
280
|
+
tokenLine,
|
|
281
|
+
estimateCost,
|
|
282
|
+
getModelPricing,
|
|
283
|
+
saveUserPricing,
|
|
284
|
+
printTokenStats,
|
|
285
|
+
printTokenStatsInteractive,
|
|
286
|
+
resetUsage,
|
|
287
|
+
formatTokens,
|
|
288
|
+
formatCost,
|
|
289
|
+
loadUsage,
|
|
290
|
+
PRICING,
|
|
291
|
+
};
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// ui.js — Help, config display, setup wizard
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { bold, dim, cyan, gray, green, yellow, red, brightWhite, brightCyan, symbols, c256 } = require('./colors');
|
|
9
|
+
const { loadConfig, saveConfig, maskKey, PROVIDERS, getAllProviderIds, CONFIG_FILE } = require('./config');
|
|
10
|
+
const { textInput, selectMenu } = require('./menu');
|
|
11
|
+
const { printBanner, keyValue, printError, printSuccess, accent, subtle, mid, dot1, dot2, dot3 } = require('./renderer');
|
|
12
|
+
const { getModelPricing, PRICING } = require('./tokens');
|
|
13
|
+
|
|
14
|
+
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
15
|
+
|
|
16
|
+
// ─── Help ───────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function printHelp() {
|
|
19
|
+
printBanner();
|
|
20
|
+
console.log(` ${subtle('say what you need. it handles the rest.')}`);
|
|
21
|
+
console.log();
|
|
22
|
+
const pad = (s, w) => s + ' '.repeat(Math.max(0, w - s.length));
|
|
23
|
+
console.log(` ${subtle('$')} ${pad('... kill node', 26)} ${subtle('$')} ... find .tmp then delete`);
|
|
24
|
+
console.log(` ${subtle('$')} ${pad('... show disk usage', 26)} ${subtle('$')} ... read desktop, reorganize`);
|
|
25
|
+
console.log(` ${subtle('$')} ${pad('... compress this folder', 26)} ${subtle('$')} ... build then deploy`);
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(` ${accent('-t')} ${subtle('task')} ${accent('-p')} ${subtle('provider')} ${accent('-u')} ${subtle('usage')} ${accent('-c')} ${subtle('config')} ${accent('-d')} ${subtle('debug')}`);
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Provider list helper ───────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function printProviderList(config) {
|
|
34
|
+
const providerIds = getAllProviderIds();
|
|
35
|
+
for (let i = 0; i < providerIds.length; i++) {
|
|
36
|
+
const id = providerIds[i];
|
|
37
|
+
const prov = config.providers[id];
|
|
38
|
+
const info = PROVIDERS[id];
|
|
39
|
+
const isActive = id === config.provider;
|
|
40
|
+
const hasKey = !!prov.apiKey;
|
|
41
|
+
const num = subtle(`${i + 1}`);
|
|
42
|
+
const mark = isActive ? green(symbols.check) : hasKey ? subtle(symbols.check) : subtle(symbols.cross);
|
|
43
|
+
const name = isActive ? brightWhite(info.name) : hasKey ? mid(info.name) : subtle(info.name);
|
|
44
|
+
const key = hasKey ? subtle(' ' + maskKey(prov.apiKey)) : '';
|
|
45
|
+
console.log(` ${mark} ${num} ${name}${key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function showConfig(config) {
|
|
52
|
+
printBanner();
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(keyValue('provider ', `${config.provider} ${subtle('(' + config.model + ')')}`));
|
|
55
|
+
console.log(keyValue('api key ', maskKey(config.apiKey)));
|
|
56
|
+
console.log(keyValue('auto-exec', config.autoExec ? green('on') : subtle('off')));
|
|
57
|
+
console.log(keyValue('config ', CONFIG_FILE));
|
|
58
|
+
console.log();
|
|
59
|
+
printProviderList(config);
|
|
60
|
+
console.log();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function printConfig(config) {
|
|
64
|
+
showConfig(config);
|
|
65
|
+
|
|
66
|
+
const choice = await selectMenu([
|
|
67
|
+
{ label: 'Exit', key: 'q' },
|
|
68
|
+
{ label: 'Edit', key: 'e' },
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
if (choice === 'e') {
|
|
72
|
+
await runSetup();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Setup wizard ───────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
async function runSetup() {
|
|
79
|
+
const config = loadConfig();
|
|
80
|
+
|
|
81
|
+
printBanner();
|
|
82
|
+
console.log();
|
|
83
|
+
printProviderList(config);
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
const providerIds = getAllProviderIds();
|
|
87
|
+
const total = providerIds.length;
|
|
88
|
+
const selection = await textInput(
|
|
89
|
+
`Configure which? ${subtle(`(1-${total}, all, or name)`)}`,
|
|
90
|
+
'all'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
let selectedIds;
|
|
94
|
+
if (selection.toLowerCase() === 'all') {
|
|
95
|
+
selectedIds = providerIds;
|
|
96
|
+
} else {
|
|
97
|
+
selectedIds = selection.split(/[,\s]+/).map(s => {
|
|
98
|
+
const num = parseInt(s);
|
|
99
|
+
if (num >= 1 && num <= providerIds.length) return providerIds[num - 1];
|
|
100
|
+
if (providerIds.includes(s)) return s;
|
|
101
|
+
return null;
|
|
102
|
+
}).filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!selectedIds.length) { printError('No valid provider.'); return; }
|
|
106
|
+
|
|
107
|
+
const enterHint = subtle('enter to keep current');
|
|
108
|
+
|
|
109
|
+
for (const id of selectedIds) {
|
|
110
|
+
const info = PROVIDERS[id];
|
|
111
|
+
const prov = config.providers[id];
|
|
112
|
+
const isCustom = id === 'custom';
|
|
113
|
+
|
|
114
|
+
console.log(`\n ${bold(info.name)}`);
|
|
115
|
+
|
|
116
|
+
// API key — masked, never exposed
|
|
117
|
+
const newKey = await textInput(` Key ${enterHint}`, prov.apiKey, {
|
|
118
|
+
displayDefault: prov.apiKey ? maskKey(prov.apiKey) : 'not set',
|
|
119
|
+
});
|
|
120
|
+
if (newKey) config.providers[id].apiKey = newKey;
|
|
121
|
+
|
|
122
|
+
// Custom provider needs API URL
|
|
123
|
+
if (isCustom) {
|
|
124
|
+
const urlChoice = await textInput(` API URL ${enterHint}`, prov.apiUrl || '', {
|
|
125
|
+
displayDefault: prov.apiUrl || 'https://your-api.com/v1/chat/completions',
|
|
126
|
+
});
|
|
127
|
+
if (urlChoice) config.providers[id].apiUrl = urlChoice;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Model
|
|
131
|
+
const modelChoice = await textInput(` Model ${enterHint}`, prov.model);
|
|
132
|
+
if (modelChoice) config.providers[id].model = modelChoice;
|
|
133
|
+
|
|
134
|
+
// Pricing — simple "set or skip"
|
|
135
|
+
const activeModel = config.providers[id].model;
|
|
136
|
+
const existing = getModelPricing(activeModel);
|
|
137
|
+
|
|
138
|
+
const inputPrice = await textInput(
|
|
139
|
+
` Price $/1M input ${subtle('enter to skip')}`,
|
|
140
|
+
'', { displayDefault: existing ? `current: ${existing.input}` : '' }
|
|
141
|
+
);
|
|
142
|
+
if (inputPrice) {
|
|
143
|
+
const outputPrice = await textInput(
|
|
144
|
+
` Price $/1M output ${subtle('enter to skip')}`,
|
|
145
|
+
'', { displayDefault: existing ? `current: ${existing.output}` : '' }
|
|
146
|
+
);
|
|
147
|
+
const inP = parseFloat(inputPrice);
|
|
148
|
+
const outP = parseFloat(outputPrice);
|
|
149
|
+
if (!isNaN(inP) && outP && !isNaN(outP)) {
|
|
150
|
+
if (!config.pricing) config.pricing = {};
|
|
151
|
+
config.pricing[activeModel] = { input: inP, output: outP };
|
|
152
|
+
} else if (!isNaN(inP)) {
|
|
153
|
+
if (!config.pricing) config.pricing = {};
|
|
154
|
+
config.pricing[activeModel] = { input: inP, output: existing ? existing.output : 0 };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Default provider ───────────────────────────────────────────────
|
|
160
|
+
const configured = providerIds.filter(id => config.providers[id].apiKey);
|
|
161
|
+
if (configured.length > 0) {
|
|
162
|
+
console.log(`\n ${bold('Default Provider')}`);
|
|
163
|
+
for (let i = 0; i < configured.length; i++) {
|
|
164
|
+
const id = configured[i];
|
|
165
|
+
const info = PROVIDERS[id];
|
|
166
|
+
const isActive = id === config.provider;
|
|
167
|
+
const mark = isActive ? green(symbols.check) : subtle(symbols.check);
|
|
168
|
+
const name = isActive ? brightWhite(info.name) : mid(info.name);
|
|
169
|
+
console.log(` ${mark} ${subtle(String(i + 1))} ${name}`);
|
|
170
|
+
}
|
|
171
|
+
console.log();
|
|
172
|
+
const choice = await textInput(
|
|
173
|
+
`Set default ${enterHint}`,
|
|
174
|
+
'', { displayDefault: config.provider }
|
|
175
|
+
);
|
|
176
|
+
if (choice) {
|
|
177
|
+
const num = parseInt(choice);
|
|
178
|
+
if (num >= 1 && num <= configured.length) config.provider = configured[num - 1];
|
|
179
|
+
else if (configured.includes(choice)) config.provider = choice;
|
|
180
|
+
}
|
|
181
|
+
} else if (configured.length === 1) {
|
|
182
|
+
config.provider = configured[0];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Auto-execute ───────────────────────────────────────────────────
|
|
186
|
+
console.log();
|
|
187
|
+
const autoChoice = await textInput(
|
|
188
|
+
`Auto-execute ${subtle('skip menu, run commands directly')}`,
|
|
189
|
+
config.autoExec ? 'on' : 'off'
|
|
190
|
+
);
|
|
191
|
+
const lower = autoChoice.toLowerCase();
|
|
192
|
+
config.autoExec = (lower === 'on' || lower === 'yes' || lower === 'y' || lower === 'true' || lower === '1');
|
|
193
|
+
|
|
194
|
+
// ─── Save ───────────────────────────────────────────────────────────
|
|
195
|
+
const active = config.providers[config.provider];
|
|
196
|
+
config.apiKey = active?.apiKey || '';
|
|
197
|
+
config.model = active?.model || '';
|
|
198
|
+
config.apiUrl = active?.apiUrl || '';
|
|
199
|
+
saveConfig(config);
|
|
200
|
+
|
|
201
|
+
console.log();
|
|
202
|
+
const provName = PROVIDERS[config.provider]?.name || config.provider;
|
|
203
|
+
printSuccess(`Saved. Using ${bold(provName)} (${config.model})${config.autoExec ? bold(' auto-exec on') : ''}`);
|
|
204
|
+
console.log();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { printHelp, printConfig, runSetup };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dotdotdot-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Say what you need. It handles the rest. Natural language terminal commands & multi-step tasks. Zero dependencies.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"...": "bin/dotdotdot.js",
|
|
8
|
+
"dotdotdot": "bin/dotdotdot.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node lib/postinstall.js",
|
|
12
|
+
"test": "node bin/dotdotdot.js --help"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">= 18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"os": [
|
|
18
|
+
"darwin",
|
|
19
|
+
"linux",
|
|
20
|
+
"win32"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"cli",
|
|
24
|
+
"ai",
|
|
25
|
+
"terminal",
|
|
26
|
+
"shell",
|
|
27
|
+
"natural-language",
|
|
28
|
+
"llm",
|
|
29
|
+
"command-line",
|
|
30
|
+
"dotdotdot",
|
|
31
|
+
"zero-dependencies",
|
|
32
|
+
"multi-step",
|
|
33
|
+
"task-planner",
|
|
34
|
+
"openrouter",
|
|
35
|
+
"anthropic",
|
|
36
|
+
"openai",
|
|
37
|
+
"gemini",
|
|
38
|
+
"devtools"
|
|
39
|
+
],
|
|
40
|
+
"author": "Shell3Dots",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/Shell3Dots/dotdotdot.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/Shell3Dots/dotdotdot#readme",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/Shell3Dots/dotdotdot/issues"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"bin/",
|
|
52
|
+
"lib/",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
]
|
|
56
|
+
}
|