termux-dev 1.0.2
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 +290 -0
- package/assets/banner.svg +33 -0
- package/assets/preview.png +0 -0
- package/bin/devx.js +2 -0
- package/dist/cli/clipboard.js +136 -0
- package/dist/cli/files.js +93 -0
- package/dist/cli/index.js +1506 -0
- package/dist/cli/markdown.js +147 -0
- package/dist/cli/prompt.js +553 -0
- package/dist/cli/providers.js +892 -0
- package/dist/cli/server.js +137 -0
- package/dist/cli/updater.js +245 -0
- package/dist/core/history.js +121 -0
- package/dist/core/loop.js +164 -0
- package/dist/core/memory.js +68 -0
- package/dist/core/models.js +72 -0
- package/dist/core/pricing.js +65 -0
- package/dist/core/session.js +129 -0
- package/dist/core/snapshot.js +88 -0
- package/dist/core/types.js +1 -0
- package/dist/permissions/guard.js +104 -0
- package/dist/prompts/builder.js +69 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai.js +318 -0
- package/dist/tools/bash.js +51 -0
- package/dist/tools/diagnostics.js +63 -0
- package/dist/tools/fs.js +185 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/packages.js +80 -0
- package/dist/tools/plan.js +52 -0
- package/dist/tools/questions.js +101 -0
- package/dist/tools/search.js +90 -0
- package/dist/tools/web.js +155 -0
- package/package.json +64 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
const MEMORY_DIR = path.join(process.cwd(), '.devx');
|
|
5
|
+
const MEMORY_FILE = path.join(MEMORY_DIR, 'memory.md');
|
|
6
|
+
export class MemoryManager {
|
|
7
|
+
static async loadMemory() {
|
|
8
|
+
if (!fsSync.existsSync(MEMORY_FILE)) {
|
|
9
|
+
return '';
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return await fs.readFile(MEMORY_FILE, 'utf8');
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
static async addFact(fact) {
|
|
19
|
+
if (!fsSync.existsSync(MEMORY_DIR)) {
|
|
20
|
+
await fs.mkdir(MEMORY_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
let current = '';
|
|
23
|
+
if (fsSync.existsSync(MEMORY_FILE)) {
|
|
24
|
+
current = await fs.readFile(MEMORY_FILE, 'utf8');
|
|
25
|
+
}
|
|
26
|
+
const trimmedFact = fact.trim();
|
|
27
|
+
if (!trimmedFact)
|
|
28
|
+
return;
|
|
29
|
+
if (!current.includes(trimmedFact)) {
|
|
30
|
+
const entry = `• ${trimmedFact}\n`;
|
|
31
|
+
if (!current) {
|
|
32
|
+
current = `# Project Memory Bank\n\n## Established Rules & Architecture\n`;
|
|
33
|
+
}
|
|
34
|
+
current += entry;
|
|
35
|
+
await fs.writeFile(MEMORY_FILE, current, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
static async clearMemory() {
|
|
39
|
+
if (fsSync.existsSync(MEMORY_FILE)) {
|
|
40
|
+
await fs.unlink(MEMORY_FILE);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
static isMemoryPresent() {
|
|
44
|
+
return fsSync.existsSync(MEMORY_FILE);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export const saveMemoryTool = {
|
|
48
|
+
name: 'save_memory',
|
|
49
|
+
definition: {
|
|
50
|
+
name: 'save_memory',
|
|
51
|
+
description: 'Save an important project fact, architectural decision, user preference, or coding guideline into long-term project memory (.devx/memory.md) so you will remember it in future sessions.',
|
|
52
|
+
parameters: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
fact: { type: 'string', description: 'The concise fact, decision, or preference to remember' }
|
|
56
|
+
},
|
|
57
|
+
required: ['fact']
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
validateArgs(args) {
|
|
61
|
+
if (!args.fact || typeof args.fact !== 'string')
|
|
62
|
+
throw new Error('fact is required');
|
|
63
|
+
},
|
|
64
|
+
async execute(args) {
|
|
65
|
+
await MemoryManager.addFact(args.fact);
|
|
66
|
+
return `Saved to project memory: "${args.fact}"`;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
let limitsCache = null;
|
|
5
|
+
function loadLimits() {
|
|
6
|
+
if (limitsCache)
|
|
7
|
+
return limitsCache;
|
|
8
|
+
try {
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const jsonPath = path.join(__dirname, 'model-limits.json');
|
|
11
|
+
if (fs.existsSync(jsonPath)) {
|
|
12
|
+
limitsCache = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
13
|
+
return limitsCache;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
try {
|
|
18
|
+
const fallbackPath = path.join(process.cwd(), 'src/core/model-limits.json');
|
|
19
|
+
if (fs.existsSync(fallbackPath)) {
|
|
20
|
+
limitsCache = JSON.parse(fs.readFileSync(fallbackPath, 'utf8'));
|
|
21
|
+
return limitsCache;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
limitsCache = {};
|
|
26
|
+
return limitsCache;
|
|
27
|
+
}
|
|
28
|
+
export function getModelContextLimit(modelName) {
|
|
29
|
+
const cache = loadLimits();
|
|
30
|
+
const clean = (modelName || '').trim().toLowerCase();
|
|
31
|
+
const short = clean.split('/').pop() || clean;
|
|
32
|
+
if (cache[clean])
|
|
33
|
+
return cache[clean];
|
|
34
|
+
if (cache[short])
|
|
35
|
+
return cache[short];
|
|
36
|
+
const baseName = short.replace(/\-\d {4,8}$/, '').replace(/:latest$/, '');
|
|
37
|
+
if (cache[baseName])
|
|
38
|
+
return cache[baseName];
|
|
39
|
+
if (clean.includes('kimi-k2') || clean.includes('kimi'))
|
|
40
|
+
return 256000;
|
|
41
|
+
if (clean.includes('ox-alpha') || clean.includes('stealth'))
|
|
42
|
+
return 1048576;
|
|
43
|
+
if (clean.includes('gemini-3') || clean.includes('gemini-2') || clean.includes('gemini-1.5') || clean.includes('gemini'))
|
|
44
|
+
return 1048576;
|
|
45
|
+
if (clean.includes('claude-opus-4') || clean.includes('claude-4'))
|
|
46
|
+
return 1000000;
|
|
47
|
+
if (clean.includes('claude-3-5') || clean.includes('claude-3.5') || clean.includes('claude-3-7') || clean.includes('claude-3.7') || clean.includes('claude'))
|
|
48
|
+
return 200000;
|
|
49
|
+
if (clean.includes('gpt-5'))
|
|
50
|
+
return 1050000;
|
|
51
|
+
if (clean.includes('gpt-4o') || clean.includes('gpt-4-turbo') || clean.includes('o1') || clean.includes('o3'))
|
|
52
|
+
return 128000;
|
|
53
|
+
if (clean.includes('deepseek-v4') || clean.includes('deepseek-v3') || clean.includes('deepseek-r1') || clean.includes('deepseek'))
|
|
54
|
+
return 128000;
|
|
55
|
+
if (clean.includes('qwen3.5') || clean.includes('qwen-3.5'))
|
|
56
|
+
return 262144;
|
|
57
|
+
if (clean.includes('qwen2.5') || clean.includes('qwen-2.5') || clean.includes('qwen'))
|
|
58
|
+
return 131072;
|
|
59
|
+
if (clean.includes('glm-5') || clean.includes('glm-4.7') || clean.includes('glm-4.6'))
|
|
60
|
+
return 200000;
|
|
61
|
+
if (clean.includes('glm-4'))
|
|
62
|
+
return 128000;
|
|
63
|
+
if (clean.includes('minimax-m1') || clean.includes('minimax'))
|
|
64
|
+
return 1000000;
|
|
65
|
+
if (clean.includes('doubao'))
|
|
66
|
+
return 256000;
|
|
67
|
+
if (clean.includes('llama-3.3') || clean.includes('llama-3.1') || clean.includes('llama-3'))
|
|
68
|
+
return 128000;
|
|
69
|
+
if (clean.includes('mistral-large') || clean.includes('codestral'))
|
|
70
|
+
return 128000;
|
|
71
|
+
return 128000;
|
|
72
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
let pricingCache = null;
|
|
5
|
+
function loadPricing() {
|
|
6
|
+
if (pricingCache)
|
|
7
|
+
return pricingCache;
|
|
8
|
+
try {
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const jsonPath = path.join(__dirname, 'model-pricing.json');
|
|
11
|
+
if (fs.existsSync(jsonPath)) {
|
|
12
|
+
pricingCache = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
13
|
+
return pricingCache;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
try {
|
|
18
|
+
const fallbackPath = path.join(process.cwd(), 'src/core/model-pricing.json');
|
|
19
|
+
if (fs.existsSync(fallbackPath)) {
|
|
20
|
+
pricingCache = JSON.parse(fs.readFileSync(fallbackPath, 'utf8'));
|
|
21
|
+
return pricingCache;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch { }
|
|
25
|
+
pricingCache = {};
|
|
26
|
+
return pricingCache;
|
|
27
|
+
}
|
|
28
|
+
export function getModelPricing(modelName) {
|
|
29
|
+
const clean = (modelName || '').trim().toLowerCase();
|
|
30
|
+
if (clean.includes(':free') ||
|
|
31
|
+
clean.includes('/free') ||
|
|
32
|
+
clean.includes('free') ||
|
|
33
|
+
clean.includes('stealth') ||
|
|
34
|
+
clean.includes('local') ||
|
|
35
|
+
clean.includes('ollama') ||
|
|
36
|
+
clean.includes('lmstudio')) {
|
|
37
|
+
return { input: 0, output: 0 };
|
|
38
|
+
}
|
|
39
|
+
const cache = loadPricing();
|
|
40
|
+
const short = clean.split('/').pop() || clean;
|
|
41
|
+
if (cache[clean])
|
|
42
|
+
return cache[clean];
|
|
43
|
+
if (cache[short])
|
|
44
|
+
return cache[short];
|
|
45
|
+
const baseName = short.replace(/\-\d {4,8}$/, '').replace(/:latest$/, '');
|
|
46
|
+
if (cache[baseName])
|
|
47
|
+
return cache[baseName];
|
|
48
|
+
if (clean.includes('gpt-4o-mini'))
|
|
49
|
+
return { input: 0.15, output: 0.60 };
|
|
50
|
+
if (clean.includes('gpt-4o'))
|
|
51
|
+
return { input: 2.5, output: 10.0 };
|
|
52
|
+
if (clean.includes('deepseek'))
|
|
53
|
+
return { input: 0.14, output: 0.28 };
|
|
54
|
+
if (clean.includes('claude-3-5-sonnet') || clean.includes('claude-3.5-sonnet'))
|
|
55
|
+
return { input: 3.0, output: 15.0 };
|
|
56
|
+
if (clean.includes('gemini-2.0-flash') || clean.includes('gemini-1.5-flash'))
|
|
57
|
+
return { input: 0.1, output: 0.4 };
|
|
58
|
+
return { input: 0, output: 0 };
|
|
59
|
+
}
|
|
60
|
+
export function calculateCost(modelName, promptTokens, completionTokens) {
|
|
61
|
+
const { input, output } = getModelPricing(modelName);
|
|
62
|
+
if (input === 0 && output === 0)
|
|
63
|
+
return 0;
|
|
64
|
+
return (promptTokens * (input / 1_000_000)) + (completionTokens * (output / 1_000_000));
|
|
65
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
const SESSIONS_DIR = path.join(os.homedir(), '.devx', 'sessions');
|
|
6
|
+
export class SessionManager {
|
|
7
|
+
currentSession;
|
|
8
|
+
constructor(model, planMode) {
|
|
9
|
+
this.currentSession = this.createEmptySession(model, planMode);
|
|
10
|
+
}
|
|
11
|
+
createEmptySession(model, planMode) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const id = `sess_${now}_${Math.random().toString(36).substring(2, 7)}`;
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
title: 'New Session',
|
|
17
|
+
createdAt: now,
|
|
18
|
+
updatedAt: now,
|
|
19
|
+
model,
|
|
20
|
+
planMode,
|
|
21
|
+
messages: [],
|
|
22
|
+
totalCost: 0
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
getSession() {
|
|
26
|
+
return this.currentSession;
|
|
27
|
+
}
|
|
28
|
+
startNewSession(model, planMode) {
|
|
29
|
+
this.currentSession = this.createEmptySession(model, planMode);
|
|
30
|
+
return this.currentSession;
|
|
31
|
+
}
|
|
32
|
+
async save(messages, totalCost, model, planMode) {
|
|
33
|
+
if (!fsSync.existsSync(SESSIONS_DIR)) {
|
|
34
|
+
await fs.mkdir(SESSIONS_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
this.currentSession.messages = messages;
|
|
37
|
+
this.currentSession.totalCost = totalCost;
|
|
38
|
+
this.currentSession.updatedAt = Date.now();
|
|
39
|
+
if (model)
|
|
40
|
+
this.currentSession.model = model;
|
|
41
|
+
if (planMode !== undefined)
|
|
42
|
+
this.currentSession.planMode = planMode;
|
|
43
|
+
if (this.currentSession.title === 'New Session') {
|
|
44
|
+
const firstUserMsg = messages.find(m => m.role === 'user');
|
|
45
|
+
if (firstUserMsg && firstUserMsg.content) {
|
|
46
|
+
let title = firstUserMsg.content.trim().split('\n')[0];
|
|
47
|
+
if (title.length > 50) {
|
|
48
|
+
title = title.substring(0, 47) + '...';
|
|
49
|
+
}
|
|
50
|
+
this.currentSession.title = title || 'Session';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const filePath = path.join(SESSIONS_DIR, `${this.currentSession.id}.json`);
|
|
54
|
+
await fs.writeFile(filePath, JSON.stringify(this.currentSession, null, 2), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
static async listSessions() {
|
|
57
|
+
if (!fsSync.existsSync(SESSIONS_DIR)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const files = await fs.readdir(SESSIONS_DIR);
|
|
62
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
63
|
+
const sessions = [];
|
|
64
|
+
for (const file of jsonFiles) {
|
|
65
|
+
try {
|
|
66
|
+
const content = await fs.readFile(path.join(SESSIONS_DIR, file), 'utf8');
|
|
67
|
+
const sess = JSON.parse(content);
|
|
68
|
+
if (sess.id && sess.messages) {
|
|
69
|
+
sessions.push(sess);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
}
|
|
74
|
+
sessions.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
75
|
+
return sessions;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
static async loadSession(sessionId) {
|
|
82
|
+
const filePath = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
83
|
+
if (!fsSync.existsSync(filePath))
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
87
|
+
return JSON.parse(content);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
static async deleteSession(sessionId) {
|
|
94
|
+
const filePath = path.join(SESSIONS_DIR, `${sessionId}.json`);
|
|
95
|
+
try {
|
|
96
|
+
if (fsSync.existsSync(filePath)) {
|
|
97
|
+
await fs.unlink(filePath);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
static async deleteAllSessions() {
|
|
107
|
+
if (!fsSync.existsSync(SESSIONS_DIR))
|
|
108
|
+
return 0;
|
|
109
|
+
try {
|
|
110
|
+
const files = await fs.readdir(SESSIONS_DIR);
|
|
111
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
112
|
+
let count = 0;
|
|
113
|
+
for (const file of jsonFiles) {
|
|
114
|
+
try {
|
|
115
|
+
await fs.unlink(path.join(SESSIONS_DIR, file));
|
|
116
|
+
count++;
|
|
117
|
+
}
|
|
118
|
+
catch { }
|
|
119
|
+
}
|
|
120
|
+
return count;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
setLoadedSession(session) {
|
|
127
|
+
this.currentSession = session;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export class SnapshotManager {
|
|
5
|
+
history = [];
|
|
6
|
+
currentTurn = null;
|
|
7
|
+
beginTurn() {
|
|
8
|
+
this.currentTurn = {
|
|
9
|
+
id: `turn_${Date.now()}`,
|
|
10
|
+
timestamp: Date.now(),
|
|
11
|
+
files: new Map()
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async recordFileBeforeChange(filePath) {
|
|
15
|
+
if (!this.currentTurn) {
|
|
16
|
+
this.beginTurn();
|
|
17
|
+
}
|
|
18
|
+
const resolved = path.resolve(filePath);
|
|
19
|
+
if (this.currentTurn.files.has(resolved)) {
|
|
20
|
+
return; // Already recorded original state for this turn
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
if (fsSync.existsSync(resolved)) {
|
|
24
|
+
const content = await fs.readFile(resolved, 'utf8');
|
|
25
|
+
this.currentTurn.files.set(resolved, {
|
|
26
|
+
filePath: resolved,
|
|
27
|
+
existed: true,
|
|
28
|
+
content
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.currentTurn.files.set(resolved, {
|
|
33
|
+
filePath: resolved,
|
|
34
|
+
existed: false,
|
|
35
|
+
content: null
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
this.currentTurn.files.set(resolved, {
|
|
41
|
+
filePath: resolved,
|
|
42
|
+
existed: false,
|
|
43
|
+
content: null
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finishTurn() {
|
|
48
|
+
if (this.currentTurn && this.currentTurn.files.size > 0) {
|
|
49
|
+
this.history.push(this.currentTurn);
|
|
50
|
+
}
|
|
51
|
+
this.currentTurn = null;
|
|
52
|
+
}
|
|
53
|
+
async undoLastTurn() {
|
|
54
|
+
const lastTurn = this.history.pop();
|
|
55
|
+
if (!lastTurn || lastTurn.files.size === 0) {
|
|
56
|
+
return { revertedFiles: [], count: 0 };
|
|
57
|
+
}
|
|
58
|
+
const revertedFiles = [];
|
|
59
|
+
for (const [filePath, snap] of lastTurn.files.entries()) {
|
|
60
|
+
try {
|
|
61
|
+
const relPath = path.relative(process.cwd(), filePath) || filePath;
|
|
62
|
+
if (snap.existed && snap.content !== null) {
|
|
63
|
+
const parentDir = path.dirname(filePath);
|
|
64
|
+
if (!fsSync.existsSync(parentDir)) {
|
|
65
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
await fs.writeFile(filePath, snap.content, 'utf8');
|
|
68
|
+
revertedFiles.push(relPath);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// File was newly created in this turn, remove it
|
|
72
|
+
if (fsSync.existsSync(filePath)) {
|
|
73
|
+
await fs.unlink(filePath);
|
|
74
|
+
revertedFiles.push(`${relPath} (deleted)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(`Failed to revert ${filePath}: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { revertedFiles, count: revertedFiles.length };
|
|
83
|
+
}
|
|
84
|
+
getUndoCount() {
|
|
85
|
+
return this.history.length;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const globalSnapshotManager = new SnapshotManager();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
/**
|
|
5
|
+
* Robust path escape check: returns true if target path resolves outside current working directory
|
|
6
|
+
*/
|
|
7
|
+
function isPathOutsideCwd(targetPath) {
|
|
8
|
+
if (!targetPath)
|
|
9
|
+
return false;
|
|
10
|
+
try {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const resolved = path.resolve(cwd, targetPath);
|
|
13
|
+
const relative = path.relative(cwd, resolved);
|
|
14
|
+
return relative.startsWith('..') || path.isAbsolute(relative);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Checks for destructive or dangerous shell commands
|
|
22
|
+
*/
|
|
23
|
+
function isDangerousBashCommand(commandStr) {
|
|
24
|
+
if (!commandStr)
|
|
25
|
+
return false;
|
|
26
|
+
const lower = commandStr.toLowerCase().trim();
|
|
27
|
+
return (lower.includes('rm -rf /') ||
|
|
28
|
+
lower.includes('rm -rf ~') ||
|
|
29
|
+
lower.includes('rm -rf *') ||
|
|
30
|
+
lower.includes('mkfs') ||
|
|
31
|
+
lower.includes('dd if=') ||
|
|
32
|
+
lower.includes(':(){ :|:& };:') ||
|
|
33
|
+
lower.includes('chmod -r 777 /') ||
|
|
34
|
+
lower.includes('> /dev/sda') ||
|
|
35
|
+
lower.includes('format c:'));
|
|
36
|
+
}
|
|
37
|
+
export class CLIConsoleGuard {
|
|
38
|
+
autoApprove;
|
|
39
|
+
constructor(autoApprove = false) {
|
|
40
|
+
this.autoApprove = autoApprove;
|
|
41
|
+
}
|
|
42
|
+
check(toolName, args) {
|
|
43
|
+
const t = (toolName || '').toLowerCase();
|
|
44
|
+
const cmd = args?.command || args?.cmd || '';
|
|
45
|
+
// Critical safety net: even in YOLO mode, warn and confirm destructive system commands
|
|
46
|
+
if (this.autoApprove) {
|
|
47
|
+
if (t === 'bash' && isDangerousBashCommand(cmd)) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// 1. Shell commands always require confirmation in safe mode
|
|
53
|
+
if (t === 'bash' || t === 'exec' || t === 'run_command') {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// 2. Package installations require confirmation
|
|
57
|
+
if (t === 'install_package' || t === 'packages') {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// 3. File deletions require confirmation
|
|
61
|
+
if (t === 'delete_file' || t === 'remove_file') {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
// 4. File writes, edits, or directory creations outside project CWD require confirmation
|
|
65
|
+
if (t === 'write_file' || t === 'edit_file' || t === 'make_dir' || t === 'mkdir') {
|
|
66
|
+
const target = args?.path || args?.dir || args?.targetFile || args?.filePath;
|
|
67
|
+
if (isPathOutsideCwd(target)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
async askUser(toolName, args) {
|
|
74
|
+
const t = (toolName || '').toLowerCase();
|
|
75
|
+
const cmd = args?.command || args?.cmd || '';
|
|
76
|
+
const isDangerous = t === 'bash' && isDangerousBashCommand(cmd);
|
|
77
|
+
if (isDangerous) {
|
|
78
|
+
p.log.error(pc.bold(pc.red('⚠️ [SECURITY WARNING] Agent requested a potentially dangerous system command!')));
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
p.log.warn(pc.bold(pc.yellow(`🛡️ [PERMISSION GUARD] Agent wants to execute: ${pc.cyan(toolName)}`)));
|
|
82
|
+
}
|
|
83
|
+
// Pretty preview of key arguments
|
|
84
|
+
if (t === 'bash') {
|
|
85
|
+
console.log(` ${pc.bold('Command:')} ${pc.green(cmd)}`);
|
|
86
|
+
}
|
|
87
|
+
else if (t === 'install_package') {
|
|
88
|
+
console.log(` ${pc.bold('Packages:')} ${pc.green((args.packages || []).join(', '))}`);
|
|
89
|
+
}
|
|
90
|
+
else if (args?.path || args?.targetFile) {
|
|
91
|
+
const pth = args.path || args.targetFile;
|
|
92
|
+
const isOutside = isPathOutsideCwd(pth);
|
|
93
|
+
console.log(` ${pc.bold('Target File:')} ${isOutside ? pc.red(pth + ' (OUTSIDE PROJECT)') : pc.green(pth)}`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
console.log(` ${pc.dim('Arguments:')} ${JSON.stringify(args, null, 2)}`);
|
|
97
|
+
}
|
|
98
|
+
const allowed = await p.confirm({
|
|
99
|
+
message: isDangerous ? pc.red('Confirm executing this dangerous command?') : 'Allow execution?',
|
|
100
|
+
initialValue: !isDangerous
|
|
101
|
+
});
|
|
102
|
+
return allowed === true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import { MemoryManager } from '../core/memory.js';
|
|
5
|
+
export async function buildSystemPrompt(planMode) {
|
|
6
|
+
const isTermux = process.env.PREFIX?.includes('com.termux') || false;
|
|
7
|
+
const envDesc = isTermux ? 'Android (Termux, ARM64)' : `${os.platform()} (${os.arch()})`;
|
|
8
|
+
let prompt = `You are devx, an expert AI developer.\n`;
|
|
9
|
+
prompt += `Operating System: ${envDesc}\n`;
|
|
10
|
+
prompt += `Current Working Directory: ${process.cwd()}\n`;
|
|
11
|
+
prompt += `Date: ${new Date().toISOString()}\n\n`;
|
|
12
|
+
if (planMode) {
|
|
13
|
+
prompt += `MODE: PLAN (STRICT ARCHITECT & PLANNER MODE)\n`;
|
|
14
|
+
prompt += `You are in PLAN mode. Your role is strictly to act as an expert Software Architect and Planner.\n\n`;
|
|
15
|
+
prompt += `STRICT PLAN MODE RULES:\n`;
|
|
16
|
+
prompt += `1. ABSOLUTELY NO FULL CODE DUMPS: You must NEVER output full code files, HTML, CSS, or script contents in your response! Do NOT say "скопируйте этот код", "вот полный код" or write full file implementations. Your output must strictly be a high-level architectural plan and structure.\n`;
|
|
17
|
+
prompt += `2. NO MODIFYING TOOLS: You cannot write, edit, delete files or execute bash commands. Modifying tools are disabled.\n`;
|
|
18
|
+
prompt += `3. EXPLORATION & RESEARCH: Inspect the existing codebase using 'readFile', 'listDir', 'search', and 'webSearch' to thoroughly understand architecture, existing code patterns, and dependencies.\n`;
|
|
19
|
+
prompt += `4. MANDATORY INTERACTIVE QUESTIONS VIA 'ask_questions' TOOL:\n`;
|
|
20
|
+
prompt += ` - Whenever you want to ask the user ANY question, clarify requirements, or get choices (genre, stack, mechanics, UI, design), you MUST NEVER write questions as plain text in your markdown output!\n`;
|
|
21
|
+
prompt += ` - You MUST call the 'ask_questions' tool function call with the list of questions and options.\n`;
|
|
22
|
+
prompt += ` - Outputting numbered questions in plain markdown text without calling 'ask_questions' is strictly prohibited.\n`;
|
|
23
|
+
prompt += `5. FORMULATING THE PLAN: Deeply analyze the problem, consider edge cases, and design a solid, clean, step-by-step plan. Present a structured **FINAL PLAN** in your response:\n`;
|
|
24
|
+
prompt += ` - **🎯 Цель и архитектурный обзор**\n`;
|
|
25
|
+
prompt += ` - **📁 Пошаговый список изменений по файлам (какие файлы создаём/правим и какую логику реализуем в них, БЕЗ написания полного кода)**\n`;
|
|
26
|
+
prompt += ` - **🧪 План тестирования и проверки**\n`;
|
|
27
|
+
prompt += `6. CALL 'plan_ready' TOOL: When your architectural plan is finalized and ready for execution, you MUST call the 'plan_ready' tool with the summary, target files list, and roadmap steps. Do NOT ask the user to type trigger words (like "начинай", "делай", "выполняй") — devx automatically presents the user with an interactive [🚀 Go / ✏️ Other] selection!\n`;
|
|
28
|
+
prompt += `7. EXECUTION TRIGGER: When the user confirms with [🚀 Go], devx switches to AGENT mode and sends "Go!" to begin implementing the approved plan.\n`;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
prompt += `MODE: AGENT (CODER / EXECUTOR MODE)\n`;
|
|
32
|
+
prompt += `You are in AGENT mode. You have full access to bash, file creation, editing, package installation, diagnostics, and search tools.\n`;
|
|
33
|
+
prompt += `Execute tasks and approved plans step by step with high code quality.\n`;
|
|
34
|
+
prompt += `SELF-HEALING & VERIFICATION:\n`;
|
|
35
|
+
prompt += `- Whenever you modify or create files, use the 'diagnose_code' tool to check for any syntax or type errors.\n`;
|
|
36
|
+
prompt += `- If any error is found, automatically fix it with 'edit_file' until all diagnostics pass cleanly.\n`;
|
|
37
|
+
prompt += `- If you need dependencies, use 'install_package' to install them cleanly.\n`;
|
|
38
|
+
prompt += `- Use 'save_memory' to remember important architectural decisions, user preferences, or project rules.\n`;
|
|
39
|
+
prompt += `Always explain your actions briefly before using tools.\n`;
|
|
40
|
+
}
|
|
41
|
+
// Load Project Memory Bank
|
|
42
|
+
try {
|
|
43
|
+
const memory = await MemoryManager.loadMemory();
|
|
44
|
+
if (memory.trim()) {
|
|
45
|
+
prompt += `\n--- Project Knowledge & Memory Bank (.devx/memory.md) ---\n${memory}\n-------------------------------------------------------\n`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
let currentDir = process.cwd();
|
|
50
|
+
try {
|
|
51
|
+
while (true) {
|
|
52
|
+
const agentsPath = path.join(currentDir, 'AGENTS.md');
|
|
53
|
+
try {
|
|
54
|
+
const content = await fs.readFile(agentsPath, 'utf8');
|
|
55
|
+
prompt += `\n--- Project Instructions (from AGENTS.md) ---\n${content}\n------------------------------------------\n`;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
const parent = path.dirname(currentDir);
|
|
60
|
+
if (parent === currentDir)
|
|
61
|
+
break;
|
|
62
|
+
currentDir = parent;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
}
|
|
68
|
+
return prompt;
|
|
69
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { OpenAIProvider } from './openai.js';
|
|
2
|
+
export function createProvider(config) {
|
|
3
|
+
const baseUrl = config.baseUrl || 'https://api.openai.com/v1';
|
|
4
|
+
const apiKey = config.apiKey || process.env.OPENAI_API_KEY || '';
|
|
5
|
+
const model = config.model || 'gpt-4o-mini';
|
|
6
|
+
return new OpenAIProvider(baseUrl, apiKey, model);
|
|
7
|
+
}
|