termux-dev 1.2.2 → 1.3.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 +46 -0
- package/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/dist/cli/export.js +62 -0
- package/dist/cli/headless.js +110 -0
- package/dist/cli/index.js +238 -77
- package/dist/cli/prompt.js +15 -2
- package/dist/cli/server.js +1 -1
- package/dist/core/commands.js +100 -0
- package/dist/core/repomap.js +135 -0
- package/dist/core/usage.js +119 -0
- package/dist/permissions/guard.js +35 -5
- package/dist/prompts/builder.js +9 -0
- package/dist/providers/openai.js +23 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,20 @@ npm install
|
|
|
85
85
|
npm link
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
### 🗑️ Complete Uninstallation
|
|
89
|
+
|
|
90
|
+
To completely remove `devx` along with all configuration, saved sessions, and data:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 1. Uninstall the global npm package
|
|
94
|
+
npm uninstall -g termux-dev
|
|
95
|
+
|
|
96
|
+
# 2. Remove configuration and session history (Linux / macOS / Termux)
|
|
97
|
+
rm -rf ~/.devx ~/.devxrc.json
|
|
98
|
+
|
|
99
|
+
# (Windows PowerShell)
|
|
100
|
+
Remove-Item -Recurse -Force "$HOME\.devx", "$HOME\.devxrc.json" -ErrorAction SilentlyContinue
|
|
101
|
+
```
|
|
88
102
|
|
|
89
103
|
---
|
|
90
104
|
|
|
@@ -101,6 +115,11 @@ devx
|
|
|
101
115
|
```bash
|
|
102
116
|
devx --plan
|
|
103
117
|
```
|
|
118
|
+
* Run in **Headless One-Shot Mode** (scripts, CI/CD, Termux:Widget):
|
|
119
|
+
```bash
|
|
120
|
+
devx -p "review latest git diff and find security bugs"
|
|
121
|
+
devx -p "fix build errors" --yolo
|
|
122
|
+
```
|
|
104
123
|
|
|
105
124
|
---
|
|
106
125
|
|
|
@@ -112,6 +131,10 @@ Type `/` in the prompt to open the autocomplete command palette:
|
|
|
112
131
|
| :--- | :--- |
|
|
113
132
|
| **`/plan`** | Switch to **PLAN** mode (architect & requirements planner) |
|
|
114
133
|
| **`/agent`** | Switch to **AGENT** mode (coder & autonomous executor) |
|
|
134
|
+
| **`/usage`** | View live network bandwidth (KB/MB), token costs, and API request counts |
|
|
135
|
+
| **`/export`** | Export session conversation to a clean GitHub-Flavored Markdown transcript |
|
|
136
|
+
| **`/theme`** | Switch UI color theme (Cyan, Purple, Matrix, Amber, Crimson, Monochrome) |
|
|
137
|
+
| **`/doctor`** | Run environment health diagnostics (Node, Termux, Compilers, API keys) |
|
|
115
138
|
| **`/image`** | Paste image from clipboard as `[1.png 203kb]` |
|
|
116
139
|
| **`/serve [port]`** | Start local HTTP server for web/game preview (`/serve stop` to halt) |
|
|
117
140
|
| **`/memory`** | View, add (`/memory add <fact>`), or clear project memory bank |
|
|
@@ -213,6 +236,20 @@ npm install
|
|
|
213
236
|
npm link
|
|
214
237
|
```
|
|
215
238
|
|
|
239
|
+
### 🗑️ Полное удаление
|
|
240
|
+
|
|
241
|
+
Чтобы полностью удалить `devx` вместе с конфигурацией, историей сессий и сохранёнными данными:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# 1. Удаляем глобальный npm-пакет
|
|
245
|
+
npm uninstall -g termux-dev
|
|
246
|
+
|
|
247
|
+
# 2. Удаляем конфиг и историю сессий (Linux / macOS / Termux)
|
|
248
|
+
rm -rf ~/.devx ~/.devxrc.json
|
|
249
|
+
|
|
250
|
+
# (Windows PowerShell)
|
|
251
|
+
Remove-Item -Recurse -Force "$HOME\.devx", "$HOME\.devxrc.json" -ErrorAction SilentlyContinue
|
|
252
|
+
```
|
|
216
253
|
|
|
217
254
|
---
|
|
218
255
|
|
|
@@ -229,6 +266,11 @@ devx
|
|
|
229
266
|
```bash
|
|
230
267
|
devx --plan
|
|
231
268
|
```
|
|
269
|
+
* Запуск в **Headless One-Shot режиме** (скрипты, CI/CD, Termux:Widget):
|
|
270
|
+
```bash
|
|
271
|
+
devx -p "проверь последний git diff и найди баги"
|
|
272
|
+
devx -p "исправь ошибки сборки" --yolo
|
|
273
|
+
```
|
|
232
274
|
|
|
233
275
|
---
|
|
234
276
|
|
|
@@ -240,6 +282,10 @@ devx
|
|
|
240
282
|
| :--- | :--- |
|
|
241
283
|
| **`/plan`** | Переключиться в режим архитектора (**PLAN**) |
|
|
242
284
|
| **`/agent`** | Переключиться в режим исполнителя (**AGENT**) |
|
|
285
|
+
| **`/usage`** | Показать сетевой трафик (КБ/МБ), токены, число запросов и расходы ($) |
|
|
286
|
+
| **`/export`** | Экспортировать сессию диалога в чистый GitHub Markdown файл |
|
|
287
|
+
| **`/theme`** | Сменить цветовую тему (Cyan, Purple, Matrix, Amber, Crimson, Monochrome) |
|
|
288
|
+
| **`/doctor`** | Запустить системную диагностику (Node, Termux, Компиляторы, Ключи) |
|
|
243
289
|
| **`/image`** | Вставить картинку из буфера обмена как `[1.png 203kb]` |
|
|
244
290
|
| **`/serve [порт]`** | Запустить локальный веб-сервер (`/serve stop` для остановки) |
|
|
245
291
|
| **`/memory`** | Посмотреть, добавить (`/memory add <факт>`) или очистить банк памяти |
|
package/assets/banner.svg
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Tagline & Badge -->
|
|
27
27
|
<text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
|
|
28
|
-
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.
|
|
28
|
+
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.3.0</tspan>
|
|
29
29
|
</text>
|
|
30
30
|
|
|
31
31
|
<!-- Top Accent Line -->
|
package/assets/preview.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export class SessionExporter {
|
|
4
|
+
static async exportToMarkdown(messages, model, targetFilename) {
|
|
5
|
+
try {
|
|
6
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
7
|
+
let fileName = targetFilename?.trim() || `devx-session-${timestamp.slice(0, 19)}.md`;
|
|
8
|
+
if (!fileName.endsWith('.md'))
|
|
9
|
+
fileName += '.md';
|
|
10
|
+
const filePath = path.resolve(process.cwd(), fileName);
|
|
11
|
+
const lines = [
|
|
12
|
+
`# 📝 devx Session Transcript`,
|
|
13
|
+
``,
|
|
14
|
+
`- **Date:** ${new Date().toLocaleString()}`,
|
|
15
|
+
`- **Model:** \`${model}\``,
|
|
16
|
+
`- **Total Messages:** ${messages.length}`,
|
|
17
|
+
`- **Working Directory:** \`${process.cwd()}\``,
|
|
18
|
+
``,
|
|
19
|
+
`---`,
|
|
20
|
+
``
|
|
21
|
+
];
|
|
22
|
+
for (const msg of messages) {
|
|
23
|
+
if (msg.role === 'system')
|
|
24
|
+
continue; // Skip raw system prompt
|
|
25
|
+
if (msg.role === 'user') {
|
|
26
|
+
lines.push(`## 👤 User\n`);
|
|
27
|
+
lines.push(msg.content || '');
|
|
28
|
+
if (msg.images && msg.images.length > 0) {
|
|
29
|
+
lines.push(`\n*Attached Images: ${msg.images.map(i => i.path).join(', ')}*`);
|
|
30
|
+
}
|
|
31
|
+
lines.push(`\n---\n`);
|
|
32
|
+
}
|
|
33
|
+
else if (msg.role === 'assistant') {
|
|
34
|
+
lines.push(`## 🤖 Assistant\n`);
|
|
35
|
+
if (msg.content) {
|
|
36
|
+
lines.push(msg.content);
|
|
37
|
+
}
|
|
38
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
39
|
+
lines.push(`\n**Executed Tools:**`);
|
|
40
|
+
for (const tc of msg.tool_calls) {
|
|
41
|
+
lines.push(`- \`${tc.name}\`: \`\`\`json\n${JSON.stringify(tc.arguments, null, 2)}\n\`\`\``);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push(`\n---\n`);
|
|
45
|
+
}
|
|
46
|
+
else if (msg.role === 'tool') {
|
|
47
|
+
lines.push(`> **Tool Result (${msg.name || 'tool'}):**\n`);
|
|
48
|
+
let displayContent = msg.content || '';
|
|
49
|
+
if (displayContent.length > 2000) {
|
|
50
|
+
displayContent = displayContent.slice(0, 2000) + '\n... [truncated]';
|
|
51
|
+
}
|
|
52
|
+
lines.push('```\n' + displayContent + '\n```\n');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await fs.writeFile(filePath, lines.join('\n'), 'utf8');
|
|
56
|
+
return { success: true, filePath };
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return { success: false, filePath: '', error: err.message };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { History } from '../core/history.js';
|
|
3
|
+
import { Agent } from '../core/loop.js';
|
|
4
|
+
import { buildSystemPrompt } from '../prompts/builder.js';
|
|
5
|
+
import { createProvider } from '../providers/index.js';
|
|
6
|
+
import { getTools } from '../tools/index.js';
|
|
7
|
+
import { CLIConsoleGuard } from '../permissions/guard.js';
|
|
8
|
+
import { globalSnapshotManager } from '../core/snapshot.js';
|
|
9
|
+
import { UsageTracker } from '../core/usage.js';
|
|
10
|
+
export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
11
|
+
const planMode = !!options.planMode;
|
|
12
|
+
const isYolo = !!options.yolo || !!config.autoApprove;
|
|
13
|
+
const isQuiet = !!options.quiet;
|
|
14
|
+
const isJson = !!options.json;
|
|
15
|
+
let history = new History();
|
|
16
|
+
const sysPrompt = await buildSystemPrompt(planMode);
|
|
17
|
+
history.addMessage({ role: 'system', content: sysPrompt });
|
|
18
|
+
history.addMessage({ role: 'user', content: userPrompt });
|
|
19
|
+
const provider = createProvider(config);
|
|
20
|
+
const tools = getTools(planMode);
|
|
21
|
+
const guard = new CLIConsoleGuard(isYolo, config.bashAllowlist || []);
|
|
22
|
+
const agent = new Agent(config, provider, tools, history, guard);
|
|
23
|
+
const executedTools = [];
|
|
24
|
+
let accumulatedText = '';
|
|
25
|
+
let accumulatedReasoning = '';
|
|
26
|
+
let isFatalError = false;
|
|
27
|
+
let errorMessage = '';
|
|
28
|
+
const abortController = new AbortController();
|
|
29
|
+
process.on('SIGINT', () => {
|
|
30
|
+
abortController.abort();
|
|
31
|
+
if (!isJson) {
|
|
32
|
+
console.error(pc.yellow('\n[devx headless] Aborted by user (SIGINT)'));
|
|
33
|
+
}
|
|
34
|
+
process.exit(130);
|
|
35
|
+
});
|
|
36
|
+
try {
|
|
37
|
+
if (!isQuiet && !isJson) {
|
|
38
|
+
console.log(pc.bold(pc.cyan(`⚡ devx v1.3.0 (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
|
|
39
|
+
console.log(pc.dim(`Task: ${userPrompt}\n`));
|
|
40
|
+
}
|
|
41
|
+
for await (const event of agent.run(abortController.signal)) {
|
|
42
|
+
if (event.type === 'reasoning_delta') {
|
|
43
|
+
accumulatedReasoning += event.delta;
|
|
44
|
+
if (!isQuiet && !isJson) {
|
|
45
|
+
process.stdout.write(pc.dim(event.delta));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (event.type === 'text_delta') {
|
|
49
|
+
accumulatedText += event.delta;
|
|
50
|
+
if (!isJson) {
|
|
51
|
+
process.stdout.write(event.delta);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (event.type === 'tool_start') {
|
|
55
|
+
if (!isQuiet && !isJson) {
|
|
56
|
+
console.log('\n' + pc.bold(pc.white(`› ${event.actionDesc}`)));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (event.type === 'tool_end') {
|
|
60
|
+
executedTools.push({ id: event.id, name: event.name, result: event.result });
|
|
61
|
+
}
|
|
62
|
+
else if (event.type === 'error') {
|
|
63
|
+
errorMessage = event.message;
|
|
64
|
+
if (event.isFatal) {
|
|
65
|
+
isFatalError = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
globalSnapshotManager.finishTurn();
|
|
71
|
+
if (isJson) {
|
|
72
|
+
const summary = UsageTracker.getInstance().getSummary();
|
|
73
|
+
const output = {
|
|
74
|
+
success: !isFatalError,
|
|
75
|
+
model: config.model,
|
|
76
|
+
prompt: userPrompt,
|
|
77
|
+
result: accumulatedText,
|
|
78
|
+
reasoning: accumulatedReasoning || undefined,
|
|
79
|
+
executedTools,
|
|
80
|
+
usage: {
|
|
81
|
+
requests: summary.requestsCount,
|
|
82
|
+
totalTokens: summary.totalTokens,
|
|
83
|
+
cost: summary.totalCost,
|
|
84
|
+
totalBytes: summary.totalBytes
|
|
85
|
+
},
|
|
86
|
+
error: isFatalError ? errorMessage : undefined
|
|
87
|
+
};
|
|
88
|
+
console.log(JSON.stringify(output, null, 2));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (accumulatedText && !accumulatedText.endsWith('\n')) {
|
|
92
|
+
process.stdout.write('\n');
|
|
93
|
+
}
|
|
94
|
+
if (isFatalError) {
|
|
95
|
+
console.error(pc.red(`\n❌ Error: ${errorMessage}`));
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return isFatalError ? 1 : 0;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (isJson) {
|
|
103
|
+
console.log(JSON.stringify({ success: false, error: err.message }, null, 2));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error(pc.red(`\n❌ Fatal Headless Execution Error: ${err.message}`));
|
|
107
|
+
}
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -27,6 +27,10 @@ import { askPrompt } from './prompt.js';
|
|
|
27
27
|
import { resolveAtMentions } from './files.js';
|
|
28
28
|
import { runStartupUpdateCheck, checkForUpdates, performSelfUpdate } from './updater.js';
|
|
29
29
|
import { setActiveTheme, getCurrentTheme, listThemes, findTheme } from './theme.js';
|
|
30
|
+
import { runHeadlessMode } from './headless.js';
|
|
31
|
+
import { CustomCommandManager } from '../core/commands.js';
|
|
32
|
+
import { UsageTracker } from '../core/usage.js';
|
|
33
|
+
import { SessionExporter } from './export.js';
|
|
30
34
|
const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
|
|
31
35
|
function maskApiKey(key) {
|
|
32
36
|
if (!key)
|
|
@@ -316,23 +320,110 @@ async function runOnboarding() {
|
|
|
316
320
|
p.outro(pc.green('Setup complete! Configuration saved to ~/.devxrc.json'));
|
|
317
321
|
return res;
|
|
318
322
|
}
|
|
319
|
-
async function loadConfig() {
|
|
323
|
+
async function loadConfig(interactive = true) {
|
|
324
|
+
let globalConfig = {};
|
|
320
325
|
try {
|
|
321
326
|
const data = await fs.readFile(CONFIG_PATH, 'utf8');
|
|
322
|
-
|
|
323
|
-
if (!
|
|
327
|
+
globalConfig = JSON.parse(data);
|
|
328
|
+
if (!globalConfig.model || !globalConfig.baseUrl)
|
|
324
329
|
throw new Error('Invalid config');
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
globalConfig.maxContextTokens = globalConfig.maxContextTokens || getModelContextLimit(globalConfig.model);
|
|
331
|
+
globalConfig.apiKeys = globalConfig.apiKeys || {};
|
|
332
|
+
globalConfig.baseUrls = globalConfig.baseUrls || {};
|
|
333
|
+
globalConfig.trustedProjects = globalConfig.trustedProjects || {};
|
|
334
|
+
if (globalConfig.provider && globalConfig.apiKey) {
|
|
335
|
+
globalConfig.apiKeys[globalConfig.provider] = globalConfig.apiKey;
|
|
330
336
|
}
|
|
331
|
-
return parsed;
|
|
332
337
|
}
|
|
333
338
|
catch {
|
|
334
|
-
|
|
339
|
+
if (interactive) {
|
|
340
|
+
globalConfig = await runOnboarding();
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
throw new Error('No configuration found in ~/.devxrc.json. Please run devx interactively first to set up your AI provider.');
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Check for local project config (.devx.json or .devxrc.json)
|
|
347
|
+
const localConfigPaths = [
|
|
348
|
+
path.join(process.cwd(), '.devx.json'),
|
|
349
|
+
path.join(process.cwd(), '.devxrc.json')
|
|
350
|
+
];
|
|
351
|
+
let localConfig = null;
|
|
352
|
+
for (const lp of localConfigPaths) {
|
|
353
|
+
try {
|
|
354
|
+
if (fsSync.existsSync(lp)) {
|
|
355
|
+
const raw = await fs.readFile(lp, 'utf8');
|
|
356
|
+
localConfig = JSON.parse(raw);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch { }
|
|
361
|
+
}
|
|
362
|
+
const effectiveConfig = { ...globalConfig };
|
|
363
|
+
if (localConfig && typeof localConfig === 'object') {
|
|
364
|
+
const cwd = process.cwd();
|
|
365
|
+
const hasElevatedPermissions = localConfig.autoApprove === true || (Array.isArray(localConfig.bashAllowlist) && localConfig.bashAllowlist.length > 0);
|
|
366
|
+
let isTrusted = globalConfig.trustedProjects?.[cwd];
|
|
367
|
+
if (hasElevatedPermissions && isTrusted === undefined) {
|
|
368
|
+
if (interactive) {
|
|
369
|
+
console.log('');
|
|
370
|
+
p.log.warn(pc.bold(pc.yellow(`🛡️ [PROJECT TRUST] Local configuration (.devx.json) requests elevated permissions:`)));
|
|
371
|
+
if (localConfig.autoApprove === true) {
|
|
372
|
+
console.log(pc.yellow(` • Auto-approval (YOLO mode): enabled`));
|
|
373
|
+
}
|
|
374
|
+
if (localConfig.bashAllowlist) {
|
|
375
|
+
console.log(pc.yellow(` • Bash allowlist: ${localConfig.bashAllowlist.join(', ')}`));
|
|
376
|
+
}
|
|
377
|
+
console.log(pc.dim(` Repository: ${cwd}`));
|
|
378
|
+
const answer = await p.confirm({
|
|
379
|
+
message: 'Do you trust this repository and allow its elevated permissions?',
|
|
380
|
+
initialValue: false
|
|
381
|
+
});
|
|
382
|
+
isTrusted = answer === true;
|
|
383
|
+
globalConfig.trustedProjects = globalConfig.trustedProjects || {};
|
|
384
|
+
globalConfig.trustedProjects[cwd] = isTrusted;
|
|
385
|
+
await saveConfig(globalConfig);
|
|
386
|
+
if (isTrusted) {
|
|
387
|
+
p.log.success(pc.green('Repository marked as trusted. Elevated permissions applied.'));
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
p.log.info(pc.cyan('Repository not trusted. Elevated permissions ignored.'));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
isTrusted = false; // Never auto-trust in non-interactive headless mode
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Apply safe configuration overrides
|
|
398
|
+
if (localConfig.model)
|
|
399
|
+
effectiveConfig.model = localConfig.model;
|
|
400
|
+
if (localConfig.provider)
|
|
401
|
+
effectiveConfig.provider = localConfig.provider;
|
|
402
|
+
if (localConfig.apiKey)
|
|
403
|
+
effectiveConfig.apiKey = localConfig.apiKey;
|
|
404
|
+
if (localConfig.baseUrl)
|
|
405
|
+
effectiveConfig.baseUrl = localConfig.baseUrl;
|
|
406
|
+
if (localConfig.maxIterations)
|
|
407
|
+
effectiveConfig.maxIterations = localConfig.maxIterations;
|
|
408
|
+
if (localConfig.maxContextTokens)
|
|
409
|
+
effectiveConfig.maxContextTokens = localConfig.maxContextTokens;
|
|
410
|
+
if (localConfig.dataSaverLimitMB)
|
|
411
|
+
effectiveConfig.dataSaverLimitMB = localConfig.dataSaverLimitMB;
|
|
412
|
+
if (localConfig.pureBlackTheme !== undefined)
|
|
413
|
+
effectiveConfig.pureBlackTheme = localConfig.pureBlackTheme;
|
|
414
|
+
// Apply elevated permissions only if explicitly trusted
|
|
415
|
+
if (isTrusted) {
|
|
416
|
+
if (localConfig.autoApprove !== undefined)
|
|
417
|
+
effectiveConfig.autoApprove = localConfig.autoApprove;
|
|
418
|
+
if (Array.isArray(localConfig.bashAllowlist))
|
|
419
|
+
effectiveConfig.bashAllowlist = localConfig.bashAllowlist;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Configure Data Saver threshold
|
|
423
|
+
if (effectiveConfig.dataSaverLimitMB) {
|
|
424
|
+
UsageTracker.getInstance().setLimit(effectiveConfig.dataSaverLimitMB);
|
|
335
425
|
}
|
|
426
|
+
return effectiveConfig;
|
|
336
427
|
}
|
|
337
428
|
function enableDarkTheme(enabled = true) {
|
|
338
429
|
if (!enabled)
|
|
@@ -391,7 +482,7 @@ function drawLogo() {
|
|
|
391
482
|
theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
|
|
392
483
|
theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
|
|
393
484
|
theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
|
|
394
|
-
' ' + theme.boldFn('v1.
|
|
485
|
+
' ' + theme.boldFn('v1.3.0'),
|
|
395
486
|
''
|
|
396
487
|
];
|
|
397
488
|
for (const line of logo) {
|
|
@@ -406,7 +497,7 @@ function drawLogo() {
|
|
|
406
497
|
indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
|
|
407
498
|
indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
|
|
408
499
|
indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
|
|
409
|
-
indent + theme.boldFn('v1.
|
|
500
|
+
indent + theme.boldFn('v1.3.0'),
|
|
410
501
|
''
|
|
411
502
|
];
|
|
412
503
|
for (const line of logo) {
|
|
@@ -527,12 +618,37 @@ export async function main() {
|
|
|
527
618
|
const program = new Command();
|
|
528
619
|
program
|
|
529
620
|
.name('devx')
|
|
530
|
-
.description('
|
|
531
|
-
.
|
|
621
|
+
.description('Terminal-native AI coding assistant and vibe-coding agent')
|
|
622
|
+
.version('1.3.0')
|
|
623
|
+
.option('-p, --prompt <task>', 'Run one-shot task non-interactively (headless mode)')
|
|
624
|
+
.option('-y, --yolo', 'Automatically approve all tool executions without confirmation')
|
|
625
|
+
.option('-m, --model <model>', 'Specify AI model to use for this execution')
|
|
626
|
+
.option('-q, --quiet', 'Quiet output in headless mode (suppress banners and tool logs)')
|
|
627
|
+
.option('--json', 'Output structured JSON in headless mode')
|
|
628
|
+
.option('--plan', 'Start in plan mode (architect & planner)')
|
|
532
629
|
.parse(process.argv);
|
|
533
630
|
const options = program.opts();
|
|
534
|
-
|
|
535
|
-
|
|
631
|
+
const planModeInitial = !!options.plan;
|
|
632
|
+
const isHeadless = !!options.prompt;
|
|
633
|
+
let config = await loadConfig(!isHeadless);
|
|
634
|
+
if (options.model) {
|
|
635
|
+
config.model = options.model;
|
|
636
|
+
config.maxContextTokens = getModelContextLimit(options.model);
|
|
637
|
+
}
|
|
638
|
+
if (options.yolo) {
|
|
639
|
+
config.autoApprove = true;
|
|
640
|
+
}
|
|
641
|
+
// If -p / --prompt is provided, execute one-shot headless mode and exit!
|
|
642
|
+
if (isHeadless) {
|
|
643
|
+
const exitCode = await runHeadlessMode(options.prompt, config, {
|
|
644
|
+
planMode: planModeInitial,
|
|
645
|
+
yolo: !!options.yolo,
|
|
646
|
+
quiet: !!options.quiet,
|
|
647
|
+
json: !!options.json
|
|
648
|
+
});
|
|
649
|
+
process.exit(exitCode);
|
|
650
|
+
}
|
|
651
|
+
let planMode = planModeInitial;
|
|
536
652
|
if (!config.onboarded) {
|
|
537
653
|
const cols = Math.min(process.stdout.columns || 40, 42);
|
|
538
654
|
const fill = Math.max(2, cols - 16);
|
|
@@ -685,7 +801,7 @@ export async function main() {
|
|
|
685
801
|
const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
|
|
686
802
|
const currentTh = getCurrentTheme();
|
|
687
803
|
const choice = await select({
|
|
688
|
-
message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.
|
|
804
|
+
message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.3.0 • theme: ${currentTh.name})`)}`,
|
|
689
805
|
choices: [
|
|
690
806
|
{
|
|
691
807
|
name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
|
|
@@ -726,7 +842,7 @@ export async function main() {
|
|
|
726
842
|
description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
|
|
727
843
|
},
|
|
728
844
|
{
|
|
729
|
-
name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.
|
|
845
|
+
name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.3.0 by ApvCode)')}`,
|
|
730
846
|
value: 'about',
|
|
731
847
|
description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
|
|
732
848
|
},
|
|
@@ -742,7 +858,7 @@ export async function main() {
|
|
|
742
858
|
continue;
|
|
743
859
|
}
|
|
744
860
|
if (choice === 'about') {
|
|
745
|
-
p.note(`⚡ devx v1.
|
|
861
|
+
p.note(`⚡ devx v1.3.0 — Terminal-Native AI Coding Agent\n` +
|
|
746
862
|
`🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
|
|
747
863
|
`👤 Author: ApvCode (https://github.com/apvcode)\n` +
|
|
748
864
|
`🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
|
|
@@ -806,65 +922,83 @@ export async function main() {
|
|
|
806
922
|
process.stdin.resume();
|
|
807
923
|
return config;
|
|
808
924
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
{ name: '/update - Check and install updates from GitHub', value: '/update' },
|
|
826
|
-
{ name: '/model - Switch model for current provider', value: '/model' },
|
|
827
|
-
{ name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
|
|
828
|
-
{ name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
|
|
829
|
-
{ name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
|
|
830
|
-
{ name: '/serve - Start local web server for web preview', value: '/serve' },
|
|
831
|
-
{ name: '/memory - View or edit project memory bank', value: '/memory' },
|
|
832
|
-
{ name: '/undo - Revert last file changes made by AI', value: '/undo' },
|
|
833
|
-
{ name: '/diff - Show git diff of modified files', value: '/diff' },
|
|
834
|
-
{ name: '/commit - AI-generated git commit message', value: '/commit' },
|
|
835
|
-
{ name: '/status - Show git repository status', value: '/status' },
|
|
836
|
-
{ name: '/compact - Compact conversation context', value: '/compact' },
|
|
837
|
-
{ name: '/config - View current configuration', value: '/config' },
|
|
838
|
-
{ name: '/clear - Clear message history', value: '/clear' },
|
|
839
|
-
{ name: '/help - Show commands overview', value: '/help' },
|
|
840
|
-
{ name: '/exit - Exit devx', value: '/exit' },
|
|
925
|
+
// 1. Check for custom slash commands (.devx/commands/*.md)
|
|
926
|
+
const customCmd = await CustomCommandManager.findCommand(cmd);
|
|
927
|
+
if (customCmd) {
|
|
928
|
+
const cmdArgs = answer.slice(cmd.length).trim();
|
|
929
|
+
const expandedPrompt = CustomCommandManager.expandTemplate(customCmd.promptTemplate, cmdArgs);
|
|
930
|
+
answer = expandedPrompt;
|
|
931
|
+
p.log.info(pc.cyan(`⚡ Executing custom command: ${pc.bold(customCmd.cmd)} (${customCmd.desc})`));
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
const VALID_COMMANDS = [
|
|
935
|
+
'/new', '/reset', '/resume', '/session', '/sessions', '/history',
|
|
936
|
+
'/theme', '/themes', '/usage', '/export',
|
|
937
|
+
'/settings', '/update', '/model', '/provider', '/providers',
|
|
938
|
+
'/plan', '/agent', '/image', '/serve', '/memory', '/undo',
|
|
939
|
+
'/diff', '/commit', '/status', '/compact', '/init', '/doctor',
|
|
940
|
+
'/config', '/clear', '/exit', '/quit', '/help'
|
|
841
941
|
];
|
|
842
|
-
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
},
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
942
|
+
if (!VALID_COMMANDS.includes(cmd)) {
|
|
943
|
+
const customList = await CustomCommandManager.listCommands();
|
|
944
|
+
const customSlashItems = customList.map(c => ({
|
|
945
|
+
name: `${c.cmd.padEnd(14)} - ${c.desc}`,
|
|
946
|
+
value: c.cmd
|
|
947
|
+
}));
|
|
948
|
+
const SLASH_COMMANDS = [
|
|
949
|
+
{ name: '/new - Start a new clean chat session', value: '/new' },
|
|
950
|
+
{ name: '/resume - Resume a previous chat session', value: '/resume' },
|
|
951
|
+
{ name: '/session del - Select and delete saved sessions', value: '/session del' },
|
|
952
|
+
{ name: '/usage - Show network bandwidth, data saver & token cost', value: '/usage' },
|
|
953
|
+
{ name: '/export - Export session conversation to Markdown', value: '/export' },
|
|
954
|
+
{ name: '/theme - Switch UI color theme', value: '/theme' },
|
|
955
|
+
{ name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
|
|
956
|
+
{ name: '/settings - Configure permissions & auto-approval', value: '/settings' },
|
|
957
|
+
{ name: '/update - Check and install updates from GitHub', value: '/update' },
|
|
958
|
+
{ name: '/model - Switch model for current provider', value: '/model' },
|
|
959
|
+
{ name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
|
|
960
|
+
{ name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
|
|
961
|
+
{ name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
|
|
962
|
+
{ name: '/serve - Start local web server for web preview', value: '/serve' },
|
|
963
|
+
{ name: '/memory - View or edit project memory bank', value: '/memory' },
|
|
964
|
+
{ name: '/undo - Revert last file changes made by AI', value: '/undo' },
|
|
965
|
+
{ name: '/diff - Show git diff of modified files', value: '/diff' },
|
|
966
|
+
{ name: '/commit - AI-generated git commit message', value: '/commit' },
|
|
967
|
+
{ name: '/status - Show git repository status', value: '/status' },
|
|
968
|
+
{ name: '/compact - Compact conversation context', value: '/compact' },
|
|
969
|
+
{ name: '/config - View current configuration', value: '/config' },
|
|
970
|
+
{ name: '/clear - Clear message history', value: '/clear' },
|
|
971
|
+
{ name: '/help - Show commands overview', value: '/help' },
|
|
972
|
+
{ name: '/exit - Exit devx', value: '/exit' },
|
|
973
|
+
...customSlashItems
|
|
974
|
+
];
|
|
975
|
+
try {
|
|
976
|
+
const picked = await search({
|
|
977
|
+
message: 'Commands (type to search or select):',
|
|
978
|
+
source: async (term) => {
|
|
979
|
+
const q = (term || '').trim().toLowerCase();
|
|
980
|
+
const list = [
|
|
981
|
+
{ name: pc.yellow('⬅️ Cancel'), value: '__cancel__' },
|
|
982
|
+
...SLASH_COMMANDS
|
|
983
|
+
];
|
|
984
|
+
if (!q)
|
|
985
|
+
return list;
|
|
986
|
+
return list.filter(item => item.name.toLowerCase().includes(q) || item.value.toLowerCase().includes(q));
|
|
987
|
+
},
|
|
988
|
+
pageSize: 12
|
|
989
|
+
});
|
|
990
|
+
if (!picked || picked === '__cancel__') {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (picked === '/session del') {
|
|
994
|
+
await handleSessionDelete();
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
cmd = picked;
|
|
859
998
|
}
|
|
860
|
-
|
|
861
|
-
await handleSessionDelete();
|
|
999
|
+
catch {
|
|
862
1000
|
continue;
|
|
863
1001
|
}
|
|
864
|
-
cmd = picked;
|
|
865
|
-
}
|
|
866
|
-
catch {
|
|
867
|
-
continue;
|
|
868
1002
|
}
|
|
869
1003
|
}
|
|
870
1004
|
if (cmd === '/settings') {
|
|
@@ -1322,8 +1456,33 @@ export async function main() {
|
|
|
1322
1456
|
}
|
|
1323
1457
|
continue;
|
|
1324
1458
|
}
|
|
1459
|
+
if (cmd === '/usage') {
|
|
1460
|
+
const card = UsageTracker.getInstance().renderUsageCard();
|
|
1461
|
+
console.log('\n' + card);
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (cmd === '/export') {
|
|
1465
|
+
const targetName = parts.slice(1).join(' ').trim();
|
|
1466
|
+
const res = await SessionExporter.exportToMarkdown(history.getMessages(), config.model, targetName || undefined);
|
|
1467
|
+
if (res.success) {
|
|
1468
|
+
p.log.success(pc.bold(pc.green(`📄 Session exported to: ${res.filePath}`)));
|
|
1469
|
+
}
|
|
1470
|
+
else {
|
|
1471
|
+
p.log.error(`Failed to export session: ${res.error}`);
|
|
1472
|
+
}
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1325
1475
|
if (cmd === '/config') {
|
|
1326
|
-
|
|
1476
|
+
const masked = { ...config };
|
|
1477
|
+
if (masked.apiKey)
|
|
1478
|
+
masked.apiKey = maskApiKey(masked.apiKey);
|
|
1479
|
+
if (masked.apiKeys) {
|
|
1480
|
+
masked.apiKeys = { ...masked.apiKeys };
|
|
1481
|
+
for (const k of Object.keys(masked.apiKeys)) {
|
|
1482
|
+
masked.apiKeys[k] = maskApiKey(masked.apiKeys[k]);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
p.note(JSON.stringify(masked, null, 2), 'Configuration (Secrets Masked)');
|
|
1327
1486
|
continue;
|
|
1328
1487
|
}
|
|
1329
1488
|
if (cmd === '/model') {
|
|
@@ -1408,10 +1567,12 @@ export async function main() {
|
|
|
1408
1567
|
});
|
|
1409
1568
|
const provider = createProvider(config);
|
|
1410
1569
|
const tools = getTools(planMode);
|
|
1411
|
-
const guard = new CLIConsoleGuard(config.autoApprove);
|
|
1570
|
+
const guard = new CLIConsoleGuard(config.autoApprove, config.bashAllowlist || []);
|
|
1412
1571
|
const agentConfig = {
|
|
1413
1572
|
maxContextTokens: config.maxContextTokens || 100000,
|
|
1414
|
-
maxIterations: config.maxIterations || 100
|
|
1573
|
+
maxIterations: config.maxIterations || 100,
|
|
1574
|
+
autoApprove: config.autoApprove,
|
|
1575
|
+
bashAllowlist: config.bashAllowlist
|
|
1415
1576
|
};
|
|
1416
1577
|
const agent = new Agent(agentConfig, provider, tools, history, guard);
|
|
1417
1578
|
const taskStartTime = Date.now();
|
package/dist/cli/prompt.js
CHANGED
|
@@ -2,11 +2,14 @@ import pc from 'picocolors';
|
|
|
2
2
|
import { scanProjectFiles } from './files.js';
|
|
3
3
|
import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
|
|
4
4
|
import { getCurrentTheme, listThemes } from './theme.js';
|
|
5
|
+
import { CustomCommandManager } from '../core/commands.js';
|
|
5
6
|
export const SLASH_COMMANDS = [
|
|
6
7
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
7
8
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
8
9
|
{ cmd: '/session', desc: 'Show active session ID, stats, and info' },
|
|
9
10
|
{ cmd: '/session del', desc: 'Select and delete saved sessions' },
|
|
11
|
+
{ cmd: '/usage', desc: 'Show network bandwidth, data saver & token cost' },
|
|
12
|
+
{ cmd: '/export', desc: 'Export session conversation to Markdown' },
|
|
10
13
|
{ cmd: '/theme', desc: 'Switch UI theme (Cyan, Purple, Matrix, Amber, etc.)' },
|
|
11
14
|
{ cmd: '/doctor', desc: 'Run system & environment health diagnostics' },
|
|
12
15
|
{ cmd: '/settings', desc: 'Configure permissions & auto-approval' },
|
|
@@ -43,10 +46,14 @@ export function askPrompt(opts = {}) {
|
|
|
43
46
|
let historyIndex = GLOBAL_PROMPT_HISTORY.length;
|
|
44
47
|
let tempDraft = '';
|
|
45
48
|
let availableFiles = [];
|
|
46
|
-
|
|
49
|
+
let customCommandsList = [];
|
|
50
|
+
// Preload project files and custom slash commands
|
|
47
51
|
scanProjectFiles().then(files => {
|
|
48
52
|
availableFiles = files;
|
|
49
53
|
}).catch(() => { });
|
|
54
|
+
CustomCommandManager.listCommands().then(cmds => {
|
|
55
|
+
customCommandsList = cmds.map(c => ({ cmd: c.cmd, desc: c.desc }));
|
|
56
|
+
}).catch(() => { });
|
|
50
57
|
const pastes = [];
|
|
51
58
|
const imageAttachments = [];
|
|
52
59
|
const usedImageNames = new Set();
|
|
@@ -89,7 +96,13 @@ export function askPrompt(opts = {}) {
|
|
|
89
96
|
}
|
|
90
97
|
if (input.startsWith('/')) {
|
|
91
98
|
const q = input.trim().toLowerCase();
|
|
92
|
-
const
|
|
99
|
+
const baseList = [...SLASH_COMMANDS];
|
|
100
|
+
for (const cc of customCommandsList) {
|
|
101
|
+
if (!baseList.some(b => b.cmd === cc.cmd)) {
|
|
102
|
+
baseList.push({ cmd: cc.cmd, desc: cc.desc });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const filtered = baseList.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
|
|
93
106
|
return filtered.map(c => ({
|
|
94
107
|
label: c.cmd,
|
|
95
108
|
desc: c.desc,
|
package/dist/cli/server.js
CHANGED
|
@@ -139,7 +139,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
|
|
|
139
139
|
${parentLink}
|
|
140
140
|
${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
|
|
141
141
|
</ul>
|
|
142
|
-
<div class="footer">devx v1.
|
|
142
|
+
<div class="footer">devx v1.3.0 • Terminal-Native AI Assistant</div>
|
|
143
143
|
</div>
|
|
144
144
|
</body>
|
|
145
145
|
</html>`;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
export class CustomCommandManager {
|
|
6
|
+
static cachedCommands = null;
|
|
7
|
+
static lastScanTime = 0;
|
|
8
|
+
/**
|
|
9
|
+
* Scans for custom commands in project and global directories
|
|
10
|
+
*/
|
|
11
|
+
static async listCommands(forceRefresh = false) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (!forceRefresh && this.cachedCommands && now - this.lastScanTime < 5000) {
|
|
14
|
+
return this.cachedCommands;
|
|
15
|
+
}
|
|
16
|
+
const commandMap = new Map();
|
|
17
|
+
// 1. Search directories (higher priority overrides lower priority)
|
|
18
|
+
const searchDirs = [
|
|
19
|
+
path.join(os.homedir(), '.devx', 'commands'),
|
|
20
|
+
path.join(process.cwd(), '.claude', 'commands'),
|
|
21
|
+
path.join(process.cwd(), '.devx', 'commands')
|
|
22
|
+
];
|
|
23
|
+
for (const dir of searchDirs) {
|
|
24
|
+
try {
|
|
25
|
+
if (!fsSync.existsSync(dir))
|
|
26
|
+
continue;
|
|
27
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.prompt'))) {
|
|
30
|
+
const rawName = entry.name.replace(/\.(md|prompt)$/i, '').toLowerCase();
|
|
31
|
+
const filePath = path.join(dir, entry.name);
|
|
32
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
33
|
+
const { description, template } = this.parseCommandFile(content, rawName);
|
|
34
|
+
commandMap.set(rawName, {
|
|
35
|
+
name: rawName,
|
|
36
|
+
cmd: `/${rawName}`,
|
|
37
|
+
desc: description || `Custom command (${path.basename(dir)}/${entry.name})`,
|
|
38
|
+
promptTemplate: template,
|
|
39
|
+
sourcePath: filePath
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
this.cachedCommands = Array.from(commandMap.values());
|
|
47
|
+
this.lastScanTime = now;
|
|
48
|
+
return this.cachedCommands;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Finds a custom command by slash name (e.g. "/deploy" or "deploy")
|
|
52
|
+
*/
|
|
53
|
+
static async findCommand(cmdName) {
|
|
54
|
+
const clean = cmdName.replace(/^\//, '').toLowerCase().trim();
|
|
55
|
+
const list = await this.listCommands();
|
|
56
|
+
return list.find(c => c.name === clean) || null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Expands arguments into the prompt template
|
|
60
|
+
*/
|
|
61
|
+
static expandTemplate(template, args) {
|
|
62
|
+
const trimmedArgs = args.trim();
|
|
63
|
+
if (!template.includes('$ARG') && !template.includes('$*') && !template.includes('$1')) {
|
|
64
|
+
return trimmedArgs ? `${template}\n\nUser Arguments: ${trimmedArgs}` : template;
|
|
65
|
+
}
|
|
66
|
+
let expanded = template;
|
|
67
|
+
expanded = expanded.replace(/\$ARG/g, trimmedArgs);
|
|
68
|
+
expanded = expanded.replace(/\$\*/g, trimmedArgs);
|
|
69
|
+
// Support positional parameters: $1, $2, etc.
|
|
70
|
+
const parts = trimmedArgs.split(/\s+/);
|
|
71
|
+
for (let i = 0; i < parts.length; i++) {
|
|
72
|
+
expanded = expanded.replace(new RegExp(`\\$${i + 1}`, 'g'), parts[i]);
|
|
73
|
+
}
|
|
74
|
+
return expanded;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parses frontmatter (YAML description) and template content
|
|
78
|
+
*/
|
|
79
|
+
static parseCommandFile(rawContent, defaultName) {
|
|
80
|
+
let description = '';
|
|
81
|
+
let template = rawContent;
|
|
82
|
+
const frontmatterMatch = rawContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
83
|
+
if (frontmatterMatch) {
|
|
84
|
+
const frontmatter = frontmatterMatch[1];
|
|
85
|
+
template = frontmatterMatch[2].trim();
|
|
86
|
+
const descMatch = frontmatter.match(/description:\s*([^\r\n]+)/i);
|
|
87
|
+
if (descMatch) {
|
|
88
|
+
description = descMatch[1].trim().replace(/^["']|["']$/g, '');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
// Check for first-line Markdown heading or comment: # Description
|
|
93
|
+
const firstLine = rawContent.split('\n')[0].trim();
|
|
94
|
+
if (firstLine.startsWith('# ')) {
|
|
95
|
+
description = firstLine.replace(/^#\s*/, '').trim();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { description, template };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const IGNORED_DIRS = new Set([
|
|
4
|
+
'.git', 'node_modules', 'dist', 'build', 'out', 'coverage',
|
|
5
|
+
'.next', '.nuxt', '.cache', '.gemini', '.antigravity', '.vscode', '.idea'
|
|
6
|
+
]);
|
|
7
|
+
const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py', '.mjs', '.cjs', '.rs', '.go', '.c', '.cpp', '.h', '.java']);
|
|
8
|
+
export class RepoMapGenerator {
|
|
9
|
+
/**
|
|
10
|
+
* Generates a compact, compressed tree representation of project files and key symbols
|
|
11
|
+
*/
|
|
12
|
+
static async generate(rootDir = process.cwd(), maxFiles = 60) {
|
|
13
|
+
try {
|
|
14
|
+
const files = await this.scanFiles(rootDir, maxFiles);
|
|
15
|
+
if (files.length === 0)
|
|
16
|
+
return '';
|
|
17
|
+
const lines = ['Project Structure & Key Symbols (Repo Map):'];
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const symbolStr = file.symbols.length > 0 ? ` (${file.symbols.slice(0, 5).join(', ')}${file.symbols.length > 5 ? ', ...' : ''})` : '';
|
|
20
|
+
lines.push(`• ${file.relPath}${symbolStr}`);
|
|
21
|
+
}
|
|
22
|
+
return lines.join('\n');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
static async scanFiles(dir, maxFiles) {
|
|
29
|
+
const results = [];
|
|
30
|
+
async function walk(currentDir) {
|
|
31
|
+
if (results.length >= maxFiles)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (results.length >= maxFiles)
|
|
37
|
+
break;
|
|
38
|
+
if (entry.isDirectory()) {
|
|
39
|
+
if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
40
|
+
await walk(path.join(currentDir, entry.name));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (entry.isFile()) {
|
|
44
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
45
|
+
if (EXTENSIONS.has(ext)) {
|
|
46
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
47
|
+
const relPath = path.relative(dir, fullPath).replace(/\\/g, '/');
|
|
48
|
+
const symbols = await RepoMapGenerator.extractSymbols(fullPath, ext);
|
|
49
|
+
results.push({ relPath, symbols });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
await walk(dir);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
static async extractSymbols(filePath, ext) {
|
|
60
|
+
const symbols = [];
|
|
61
|
+
try {
|
|
62
|
+
// Read at most 16KB of each file for super-fast symbol extraction
|
|
63
|
+
const stat = await fs.stat(filePath);
|
|
64
|
+
if (stat.size > 256 * 1024)
|
|
65
|
+
return []; // Skip giant generated files
|
|
66
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
67
|
+
const lines = content.split('\n').slice(0, 300); // Only examine top 300 lines
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.js' || ext === '.jsx' || ext === '.mjs') {
|
|
71
|
+
// JS/TS exports: function, class, interface, type, const
|
|
72
|
+
const fnMatch = trimmed.match(/^export\s+(?:async\s+)?function\s+([a-zA-Z0-9_$]+)/);
|
|
73
|
+
if (fnMatch) {
|
|
74
|
+
symbols.push(`fn ${fnMatch[1]}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const classMatch = trimmed.match(/^export\s+(?:abstract\s+)?class\s+([a-zA-Z0-9_$]+)/);
|
|
78
|
+
if (classMatch) {
|
|
79
|
+
symbols.push(`class ${classMatch[1]}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const ifaceMatch = trimmed.match(/^export\s+interface\s+([a-zA-Z0-9_$]+)/);
|
|
83
|
+
if (ifaceMatch) {
|
|
84
|
+
symbols.push(`interface ${ifaceMatch[1]}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const typeMatch = trimmed.match(/^export\s+type\s+([a-zA-Z0-9_$]+)/);
|
|
88
|
+
if (typeMatch) {
|
|
89
|
+
symbols.push(`type ${typeMatch[1]}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const constMatch = trimmed.match(/^export\s+const\s+([a-zA-Z0-9_$]+)/);
|
|
93
|
+
if (constMatch && !constMatch[1].startsWith('_')) {
|
|
94
|
+
symbols.push(constMatch[1]);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (ext === '.py') {
|
|
99
|
+
// Python classes and functions
|
|
100
|
+
const pyClass = trimmed.match(/^class\s+([a-zA-Z0-9_]+)/);
|
|
101
|
+
if (pyClass) {
|
|
102
|
+
symbols.push(`class ${pyClass[1]}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const pyFn = trimmed.match(/^(?:async\s+)?def\s+([a-zA-Z0-9_]+)/);
|
|
106
|
+
if (pyFn && !pyFn[1].startsWith('__')) {
|
|
107
|
+
symbols.push(`def ${pyFn[1]}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (ext === '.rs') {
|
|
112
|
+
const rsFn = trimmed.match(/^pub\s+(?:async\s+)?fn\s+([a-zA-Z0-9_]+)/);
|
|
113
|
+
if (rsFn) {
|
|
114
|
+
symbols.push(`fn ${rsFn[1]}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const rsStruct = trimmed.match(/^pub\s+struct\s+([a-zA-Z0-9_]+)/);
|
|
118
|
+
if (rsStruct) {
|
|
119
|
+
symbols.push(`struct ${rsStruct[1]}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (ext === '.go') {
|
|
124
|
+
const goFn = trimmed.match(/^func\s+(?:\([^\)]+\)\s+)?([a-zA-Z0-9_]+)/);
|
|
125
|
+
if (goFn) {
|
|
126
|
+
symbols.push(`func ${goFn[1]}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch { }
|
|
133
|
+
return symbols.slice(0, 8);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getCurrentTheme } from '../cli/theme.js';
|
|
3
|
+
export class UsageTracker {
|
|
4
|
+
static instance;
|
|
5
|
+
requestsCount = 0;
|
|
6
|
+
bytesSent = 0;
|
|
7
|
+
bytesReceived = 0;
|
|
8
|
+
promptTokens = 0;
|
|
9
|
+
completionTokens = 0;
|
|
10
|
+
totalCost = 0;
|
|
11
|
+
dataSaverLimitMB;
|
|
12
|
+
warnedThreshold = false;
|
|
13
|
+
constructor() { }
|
|
14
|
+
static getInstance() {
|
|
15
|
+
if (!UsageTracker.instance) {
|
|
16
|
+
UsageTracker.instance = new UsageTracker();
|
|
17
|
+
}
|
|
18
|
+
return UsageTracker.instance;
|
|
19
|
+
}
|
|
20
|
+
setLimit(limitMB) {
|
|
21
|
+
this.dataSaverLimitMB = limitMB;
|
|
22
|
+
}
|
|
23
|
+
recordRequest(bytes) {
|
|
24
|
+
this.requestsCount++;
|
|
25
|
+
this.bytesSent += Math.max(0, bytes);
|
|
26
|
+
}
|
|
27
|
+
recordResponseChunk(bytes) {
|
|
28
|
+
this.bytesReceived += Math.max(0, bytes);
|
|
29
|
+
}
|
|
30
|
+
recordTokens(prompt, completion, cost = 0) {
|
|
31
|
+
this.promptTokens += Math.max(0, prompt);
|
|
32
|
+
this.completionTokens += Math.max(0, completion);
|
|
33
|
+
this.totalCost += Math.max(0, cost);
|
|
34
|
+
}
|
|
35
|
+
getSummary() {
|
|
36
|
+
return {
|
|
37
|
+
requestsCount: this.requestsCount,
|
|
38
|
+
bytesSent: this.bytesSent,
|
|
39
|
+
bytesReceived: this.bytesReceived,
|
|
40
|
+
totalBytes: this.bytesSent + this.bytesReceived,
|
|
41
|
+
promptTokens: this.promptTokens,
|
|
42
|
+
completionTokens: this.completionTokens,
|
|
43
|
+
totalTokens: this.promptTokens + this.completionTokens,
|
|
44
|
+
totalCost: this.totalCost,
|
|
45
|
+
dataSaverLimitMB: this.dataSaverLimitMB
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
static formatBytes(bytes) {
|
|
49
|
+
if (bytes <= 0)
|
|
50
|
+
return '0 B';
|
|
51
|
+
if (bytes < 1024)
|
|
52
|
+
return `${bytes} B`;
|
|
53
|
+
if (bytes < 1024 * 1024)
|
|
54
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
55
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
56
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
57
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
58
|
+
}
|
|
59
|
+
static formatTokens(n) {
|
|
60
|
+
if (n >= 1_000_000)
|
|
61
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
62
|
+
if (n >= 1000)
|
|
63
|
+
return `${(n / 1000).toFixed(1)}k`;
|
|
64
|
+
return `${n}`;
|
|
65
|
+
}
|
|
66
|
+
renderUsageCard() {
|
|
67
|
+
const theme = getCurrentTheme();
|
|
68
|
+
const summary = this.getSummary();
|
|
69
|
+
const cols = Math.min(process.stdout.columns || 80, 75);
|
|
70
|
+
const boxWidth = Math.max(34, cols - 4);
|
|
71
|
+
const innerWidth = boxWidth - 4;
|
|
72
|
+
const padRow = (label, value) => {
|
|
73
|
+
const plainLen = label.length + value.length;
|
|
74
|
+
const spaces = Math.max(1, innerWidth - plainLen);
|
|
75
|
+
return `│ ${pc.bold(label)}${' '.repeat(spaces)}${value} │`;
|
|
76
|
+
};
|
|
77
|
+
const header = theme.colorFn('┌─ ') + pc.bold('📊 Session Bandwidth & Usage Monitor') + ' ' + theme.colorFn('─'.repeat(Math.max(2, boxWidth - 41)) + '┐');
|
|
78
|
+
const divider = theme.colorFn('├' + '─'.repeat(boxWidth - 2) + '┤');
|
|
79
|
+
const footer = theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘');
|
|
80
|
+
const totalBandwidthStr = pc.cyan(UsageTracker.formatBytes(summary.totalBytes));
|
|
81
|
+
const sentStr = pc.dim(UsageTracker.formatBytes(summary.bytesSent));
|
|
82
|
+
const recvStr = pc.dim(UsageTracker.formatBytes(summary.bytesReceived));
|
|
83
|
+
const reqStr = pc.yellow(`${summary.requestsCount}`);
|
|
84
|
+
const tokenStr = pc.magenta(`${UsageTracker.formatTokens(summary.totalTokens)} tokens`);
|
|
85
|
+
const promptTokenStr = pc.dim(`${UsageTracker.formatTokens(summary.promptTokens)} in`);
|
|
86
|
+
const compTokenStr = pc.dim(`${UsageTracker.formatTokens(summary.completionTokens)} out`);
|
|
87
|
+
const costStr = pc.green(`$${summary.totalCost.toFixed(4)}`);
|
|
88
|
+
const lines = [
|
|
89
|
+
header,
|
|
90
|
+
padRow('Total Network Traffic:', totalBandwidthStr),
|
|
91
|
+
padRow(' • Upload (Sent):', sentStr),
|
|
92
|
+
padRow(' • Download (Recv):', recvStr),
|
|
93
|
+
padRow('API Requests Count:', reqStr),
|
|
94
|
+
divider,
|
|
95
|
+
padRow('Tokens Consumed:', tokenStr),
|
|
96
|
+
padRow(' • Breakdown:', `${promptTokenStr} / ${compTokenStr}`),
|
|
97
|
+
padRow('Session Cost:', costStr)
|
|
98
|
+
];
|
|
99
|
+
if (summary.dataSaverLimitMB) {
|
|
100
|
+
const usedMB = summary.totalBytes / (1024 * 1024);
|
|
101
|
+
const pct = Math.min(100, Math.round((usedMB / summary.dataSaverLimitMB) * 100));
|
|
102
|
+
const limitStr = pct >= 90 ? pc.red(`${usedMB.toFixed(1)} / ${summary.dataSaverLimitMB} MB (${pct}%)`) : pc.cyan(`${usedMB.toFixed(1)} / ${summary.dataSaverLimitMB} MB (${pct}%)`);
|
|
103
|
+
lines.push(divider);
|
|
104
|
+
lines.push(padRow('Data Saver Limit:', limitStr));
|
|
105
|
+
}
|
|
106
|
+
lines.push(footer);
|
|
107
|
+
return lines.join('\n') + '\n';
|
|
108
|
+
}
|
|
109
|
+
checkThresholdWarning() {
|
|
110
|
+
if (!this.dataSaverLimitMB || this.warnedThreshold)
|
|
111
|
+
return null;
|
|
112
|
+
const usedMB = (this.bytesSent + this.bytesReceived) / (1024 * 1024);
|
|
113
|
+
if (usedMB >= this.dataSaverLimitMB) {
|
|
114
|
+
this.warnedThreshold = true;
|
|
115
|
+
return `⚠️ [DATA SAVER WARNING] Session network traffic has reached ${usedMB.toFixed(1)} MB (Limit: ${this.dataSaverLimitMB} MB)!`;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -34,14 +34,34 @@ function isDangerousBashCommand(commandStr) {
|
|
|
34
34
|
lower.includes('> /dev/sda') ||
|
|
35
35
|
lower.includes('format c:'));
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Checks if a command contains chaining, redirection, or subshell operators.
|
|
39
|
+
* Allowlist matches MUST be clean, single commands without hidden side-effects.
|
|
40
|
+
*/
|
|
41
|
+
function hasComplexChainingOrRedirection(commandStr) {
|
|
42
|
+
if (!commandStr)
|
|
43
|
+
return false;
|
|
44
|
+
return (commandStr.includes('&&') ||
|
|
45
|
+
commandStr.includes('||') ||
|
|
46
|
+
commandStr.includes(';') ||
|
|
47
|
+
commandStr.includes('|') ||
|
|
48
|
+
commandStr.includes('>') ||
|
|
49
|
+
commandStr.includes('<') ||
|
|
50
|
+
commandStr.includes('`') ||
|
|
51
|
+
commandStr.includes('$(') ||
|
|
52
|
+
commandStr.includes('\n') ||
|
|
53
|
+
commandStr.includes('\r'));
|
|
54
|
+
}
|
|
37
55
|
export class CLIConsoleGuard {
|
|
38
56
|
autoApprove;
|
|
39
|
-
|
|
57
|
+
bashAllowlist;
|
|
58
|
+
constructor(autoApprove = false, bashAllowlist = []) {
|
|
40
59
|
this.autoApprove = autoApprove;
|
|
60
|
+
this.bashAllowlist = bashAllowlist;
|
|
41
61
|
}
|
|
42
62
|
check(toolName, args) {
|
|
43
63
|
const t = (toolName || '').toLowerCase();
|
|
44
|
-
const cmd = args?.command || args?.cmd || '';
|
|
64
|
+
const cmd = (args?.command || args?.cmd || '').trim();
|
|
45
65
|
// Critical safety net: even in YOLO mode, warn and confirm destructive system commands
|
|
46
66
|
if (this.autoApprove) {
|
|
47
67
|
if (t === 'bash' && isDangerousBashCommand(cmd)) {
|
|
@@ -49,9 +69,19 @@ export class CLIConsoleGuard {
|
|
|
49
69
|
}
|
|
50
70
|
return false;
|
|
51
71
|
}
|
|
52
|
-
//
|
|
53
|
-
if (t === 'bash' || t === 'exec' || t === 'run_command') {
|
|
54
|
-
|
|
72
|
+
// Check Bash Allowlist: only allow if command is NOT dangerous AND does not use chaining/redirection
|
|
73
|
+
if ((t === 'bash' || t === 'exec' || t === 'run_command') && cmd) {
|
|
74
|
+
if (!isDangerousBashCommand(cmd) && !hasComplexChainingOrRedirection(cmd)) {
|
|
75
|
+
const isAllowed = this.bashAllowlist.some(pattern => {
|
|
76
|
+
const p = pattern.trim().toLowerCase();
|
|
77
|
+
const lowerCmd = cmd.toLowerCase();
|
|
78
|
+
return lowerCmd === p || lowerCmd.startsWith(p + ' ');
|
|
79
|
+
});
|
|
80
|
+
if (isAllowed) {
|
|
81
|
+
return false; // Automatically allowed!
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return true; // Requires user confirmation
|
|
55
85
|
}
|
|
56
86
|
// 2. Package installations require confirmation
|
|
57
87
|
if (t === 'install_package' || t === 'packages') {
|
package/dist/prompts/builder.js
CHANGED
|
@@ -72,5 +72,14 @@ export async function buildSystemPrompt(planMode) {
|
|
|
72
72
|
}
|
|
73
73
|
catch (e) {
|
|
74
74
|
}
|
|
75
|
+
// Load Compact Repo Map (AST structure & exported symbols)
|
|
76
|
+
try {
|
|
77
|
+
const { RepoMapGenerator } = await import('../core/repomap.js');
|
|
78
|
+
const repoMap = await RepoMapGenerator.generate(process.cwd(), 50);
|
|
79
|
+
if (repoMap.trim()) {
|
|
80
|
+
prompt += `\n--- Codebase Map & Exported Symbols ---\n${repoMap}\n----------------------------------------\n`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { }
|
|
75
84
|
return prompt;
|
|
76
85
|
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { calculateCost } from '../core/pricing.js';
|
|
2
|
+
import { UsageTracker } from '../core/usage.js';
|
|
2
3
|
export class OpenAIProvider {
|
|
3
4
|
baseUrl;
|
|
4
5
|
apiKey;
|
|
@@ -66,20 +67,23 @@ export class OpenAIProvider {
|
|
|
66
67
|
async chat(request) {
|
|
67
68
|
const url = `${this.baseUrl}/chat/completions`;
|
|
68
69
|
const payload = this.buildPayload(request, false);
|
|
70
|
+
const bodyStr = JSON.stringify(payload);
|
|
69
71
|
const headers = {
|
|
70
72
|
'Content-Type': 'application/json',
|
|
71
73
|
};
|
|
72
74
|
if (this.apiKey) {
|
|
73
75
|
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
74
76
|
}
|
|
77
|
+
UsageTracker.getInstance().recordRequest(Buffer.byteLength(bodyStr, 'utf8'));
|
|
75
78
|
const res = await fetch(url, {
|
|
76
79
|
method: 'POST',
|
|
77
80
|
headers,
|
|
78
|
-
body:
|
|
81
|
+
body: bodyStr,
|
|
79
82
|
signal: request.signal
|
|
80
83
|
});
|
|
81
84
|
if (!res.ok) {
|
|
82
85
|
const errText = await res.text();
|
|
86
|
+
UsageTracker.getInstance().recordResponseChunk(Buffer.byteLength(errText, 'utf8'));
|
|
83
87
|
let errMsg = errText;
|
|
84
88
|
try {
|
|
85
89
|
const parsed = JSON.parse(errText);
|
|
@@ -95,7 +99,15 @@ export class OpenAIProvider {
|
|
|
95
99
|
err.status = res.status;
|
|
96
100
|
throw err;
|
|
97
101
|
}
|
|
98
|
-
const
|
|
102
|
+
const rawResText = await res.text();
|
|
103
|
+
UsageTracker.getInstance().recordResponseChunk(Buffer.byteLength(rawResText, 'utf8'));
|
|
104
|
+
let data = {};
|
|
105
|
+
try {
|
|
106
|
+
data = JSON.parse(rawResText);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new Error(`Invalid JSON response from provider`);
|
|
110
|
+
}
|
|
99
111
|
if (data.error) {
|
|
100
112
|
const msg = data.error.message || data.error.code || JSON.stringify(data.error);
|
|
101
113
|
throw new Error(`Provider Error: ${msg}`);
|
|
@@ -115,6 +127,7 @@ export class OpenAIProvider {
|
|
|
115
127
|
totalTokens,
|
|
116
128
|
cost
|
|
117
129
|
};
|
|
130
|
+
UsageTracker.getInstance().recordTokens(promptTokens, completionTokens, cost);
|
|
118
131
|
}
|
|
119
132
|
return {
|
|
120
133
|
content: choice.content || null,
|
|
@@ -129,20 +142,23 @@ export class OpenAIProvider {
|
|
|
129
142
|
async *chatStream(request) {
|
|
130
143
|
const url = `${this.baseUrl}/chat/completions`;
|
|
131
144
|
const payload = this.buildPayload(request, true);
|
|
145
|
+
const bodyStr = JSON.stringify(payload);
|
|
132
146
|
const headers = {
|
|
133
147
|
'Content-Type': 'application/json',
|
|
134
148
|
};
|
|
135
149
|
if (this.apiKey) {
|
|
136
150
|
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
137
151
|
}
|
|
152
|
+
UsageTracker.getInstance().recordRequest(Buffer.byteLength(bodyStr, 'utf8'));
|
|
138
153
|
const res = await fetch(url, {
|
|
139
154
|
method: 'POST',
|
|
140
155
|
headers,
|
|
141
|
-
body:
|
|
156
|
+
body: bodyStr,
|
|
142
157
|
signal: request.signal
|
|
143
158
|
});
|
|
144
159
|
if (!res.ok) {
|
|
145
160
|
const errText = await res.text();
|
|
161
|
+
UsageTracker.getInstance().recordResponseChunk(Buffer.byteLength(errText, 'utf8'));
|
|
146
162
|
let errMsg = errText;
|
|
147
163
|
try {
|
|
148
164
|
const parsed = JSON.parse(errText);
|
|
@@ -184,6 +200,9 @@ export class OpenAIProvider {
|
|
|
184
200
|
const { done, value } = await reader.read();
|
|
185
201
|
if (done || request.signal?.aborted)
|
|
186
202
|
break;
|
|
203
|
+
if (value) {
|
|
204
|
+
UsageTracker.getInstance().recordResponseChunk(value.byteLength);
|
|
205
|
+
}
|
|
187
206
|
buffer += decoder.decode(value, { stream: true });
|
|
188
207
|
const lines = buffer.split('\n');
|
|
189
208
|
buffer = lines.pop() || '';
|
|
@@ -333,6 +352,7 @@ export class OpenAIProvider {
|
|
|
333
352
|
totalTokens,
|
|
334
353
|
cost
|
|
335
354
|
};
|
|
355
|
+
UsageTracker.getInstance().recordTokens(promptTokens, completionTokens, cost);
|
|
336
356
|
const finalResponse = {
|
|
337
357
|
content: accumulatedContent || null,
|
|
338
358
|
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
package/package.json
CHANGED