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,80 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
export const installPackageTool = {
|
|
4
|
+
name: 'install_package',
|
|
5
|
+
definition: {
|
|
6
|
+
name: 'install_package',
|
|
7
|
+
description: 'Install a package or dependency using npm, pip, or cargo in the current project.',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
package: { type: 'string', description: 'Package name to install (e.g. "express", "axios", "three", "pygame")' },
|
|
12
|
+
dev: { type: 'boolean', description: 'Whether to install as a dev dependency (for npm)' },
|
|
13
|
+
manager: { type: 'string', enum: ['npm', 'pip', 'yarn', 'pnpm', 'cargo'], description: 'Package manager to use (auto-detected if omitted)' }
|
|
14
|
+
},
|
|
15
|
+
required: ['package']
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
validateArgs(args) {
|
|
19
|
+
if (!args.package || typeof args.package !== 'string')
|
|
20
|
+
throw new Error('package is required');
|
|
21
|
+
},
|
|
22
|
+
async execute(args) {
|
|
23
|
+
let pkgManager = args.manager;
|
|
24
|
+
if (!pkgManager) {
|
|
25
|
+
if (fsSync.existsSync('package.json')) {
|
|
26
|
+
if (fsSync.existsSync('pnpm-lock.yaml'))
|
|
27
|
+
pkgManager = 'pnpm';
|
|
28
|
+
else if (fsSync.existsSync('yarn.lock'))
|
|
29
|
+
pkgManager = 'yarn';
|
|
30
|
+
else
|
|
31
|
+
pkgManager = 'npm';
|
|
32
|
+
}
|
|
33
|
+
else if (fsSync.existsSync('requirements.txt') || fsSync.existsSync('pyproject.toml')) {
|
|
34
|
+
pkgManager = 'pip';
|
|
35
|
+
}
|
|
36
|
+
else if (fsSync.existsSync('Cargo.toml')) {
|
|
37
|
+
pkgManager = 'cargo';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
pkgManager = 'npm';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let cmd = '';
|
|
44
|
+
if (pkgManager === 'npm') {
|
|
45
|
+
cmd = `npm install ${args.dev ? '-D ' : ''}${args.package}`;
|
|
46
|
+
}
|
|
47
|
+
else if (pkgManager === 'yarn') {
|
|
48
|
+
cmd = `yarn add ${args.dev ? '-D ' : ''}${args.package}`;
|
|
49
|
+
}
|
|
50
|
+
else if (pkgManager === 'pnpm') {
|
|
51
|
+
cmd = `pnpm add ${args.dev ? '-D ' : ''}${args.package}`;
|
|
52
|
+
}
|
|
53
|
+
else if (pkgManager === 'pip') {
|
|
54
|
+
cmd = `pip install ${args.package}`;
|
|
55
|
+
}
|
|
56
|
+
else if (pkgManager === 'cargo') {
|
|
57
|
+
cmd = `cargo add ${args.package}`;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
cmd = `npm install ${args.package}`;
|
|
61
|
+
}
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const proc = spawn(cmd, { shell: true });
|
|
64
|
+
let output = '';
|
|
65
|
+
proc.stdout.on('data', (d) => { output += d.toString(); });
|
|
66
|
+
proc.stderr.on('data', (d) => { output += d.toString(); });
|
|
67
|
+
proc.on('close', (code) => {
|
|
68
|
+
if (code === 0) {
|
|
69
|
+
resolve(`Successfully installed ${args.package} (${pkgManager})\n${output.trim()}`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
resolve(`Installation exited with code ${code}:\n${output.trim()}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
proc.on('error', (err) => {
|
|
76
|
+
resolve(`Failed to run ${cmd}: ${err.message}`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export let lastPlanReady = null;
|
|
2
|
+
export function resetPlanReady() {
|
|
3
|
+
lastPlanReady = null;
|
|
4
|
+
}
|
|
5
|
+
export const planReadyTool = {
|
|
6
|
+
name: 'plan_ready',
|
|
7
|
+
definition: {
|
|
8
|
+
name: 'plan_ready',
|
|
9
|
+
description: 'Signal that the plan is finalized and ready for user approval and execution.',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
summary: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'A brief 1-2 sentence summary of what will be built or modified.'
|
|
16
|
+
},
|
|
17
|
+
files: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: { type: 'string' },
|
|
20
|
+
description: 'List of file paths that will be created or edited.'
|
|
21
|
+
},
|
|
22
|
+
steps: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: { type: 'string' },
|
|
25
|
+
description: 'Step-by-step implementation roadmap.'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
required: ['summary']
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
validateArgs: (args) => {
|
|
32
|
+
if (!args || typeof args !== 'object') {
|
|
33
|
+
throw new Error('Arguments must be an object');
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
execute: async (args) => {
|
|
37
|
+
lastPlanReady = {
|
|
38
|
+
summary: args.summary,
|
|
39
|
+
files: args.files,
|
|
40
|
+
steps: args.steps,
|
|
41
|
+
timestamp: Date.now()
|
|
42
|
+
};
|
|
43
|
+
let output = `Plan finalized: ${args.summary}\n`;
|
|
44
|
+
if (args.files && args.files.length > 0) {
|
|
45
|
+
output += `Files: ${args.files.join(', ')}\n`;
|
|
46
|
+
}
|
|
47
|
+
if (args.steps && args.steps.length > 0) {
|
|
48
|
+
output += `Steps:\n${args.steps.map((s, i) => `${i + 1}. ${s}`).join('\n')}`;
|
|
49
|
+
}
|
|
50
|
+
return output;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { select, input } from '@inquirer/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
export const askQuestionsTool = {
|
|
4
|
+
name: 'ask_questions',
|
|
5
|
+
definition: {
|
|
6
|
+
name: 'ask_questions',
|
|
7
|
+
description: 'Interactive questionnaire modal. MUST BE USED whenever you want to ask the user clarifying questions or get choices (tech stack, game genre, features, UI design, preferences) instead of writing questions in plain markdown text.',
|
|
8
|
+
parameters: {
|
|
9
|
+
type: 'object',
|
|
10
|
+
properties: {
|
|
11
|
+
questions: {
|
|
12
|
+
type: 'array',
|
|
13
|
+
description: 'List of questions to ask the user',
|
|
14
|
+
items: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
id: { type: 'string', description: 'Short identifier or topic' },
|
|
18
|
+
question: { type: 'string', description: 'The question text' },
|
|
19
|
+
options: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
items: { type: 'string' },
|
|
22
|
+
description: '2 to 6 multiple choice options for the user to pick from'
|
|
23
|
+
},
|
|
24
|
+
allowCustom: { type: 'boolean', description: 'Whether to allow typing a custom answer (default true)' }
|
|
25
|
+
},
|
|
26
|
+
required: ['question', 'options']
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
required: ['questions']
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
validateArgs(args) {
|
|
34
|
+
if (!args || !args.questions || !Array.isArray(args.questions) || args.questions.length === 0) {
|
|
35
|
+
throw new Error('questions array is required');
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
execute: async (args) => {
|
|
39
|
+
if (!args.questions || !Array.isArray(args.questions) || args.questions.length === 0) {
|
|
40
|
+
return 'No questions provided.';
|
|
41
|
+
}
|
|
42
|
+
const results = [];
|
|
43
|
+
const total = args.questions.length;
|
|
44
|
+
console.log();
|
|
45
|
+
for (let i = 0; i < total; i++) {
|
|
46
|
+
const q = args.questions[i];
|
|
47
|
+
const stepHeader = pc.cyan(`‹ ${i + 1} of ${total} ›`);
|
|
48
|
+
const messageTitle = `${pc.bold(q.question)} ${stepHeader}`;
|
|
49
|
+
const choices = q.options.map((opt, idx) => ({
|
|
50
|
+
name: `${pc.cyan(String(idx + 1))} ${opt}`,
|
|
51
|
+
value: opt
|
|
52
|
+
}));
|
|
53
|
+
if (q.allowCustom !== false) {
|
|
54
|
+
choices.push({
|
|
55
|
+
name: `${pc.dim('✏️ Type custom answer...')}`,
|
|
56
|
+
value: '__custom__'
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
choices.push({
|
|
60
|
+
name: `${pc.dim('⏭️ Skip')}`,
|
|
61
|
+
value: '__skip__'
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
let answer = await select({
|
|
65
|
+
message: messageTitle,
|
|
66
|
+
choices,
|
|
67
|
+
pageSize: Math.min(8, choices.length)
|
|
68
|
+
});
|
|
69
|
+
if (answer === '__custom__') {
|
|
70
|
+
const customAns = await input({
|
|
71
|
+
message: pc.cyan('Your custom answer:'),
|
|
72
|
+
validate: (v) => v.trim().length > 0 || 'Answer cannot be empty'
|
|
73
|
+
});
|
|
74
|
+
answer = customAns.trim();
|
|
75
|
+
}
|
|
76
|
+
else if (answer === '__skip__') {
|
|
77
|
+
answer = '(Skipped)';
|
|
78
|
+
}
|
|
79
|
+
results.push({
|
|
80
|
+
question: q.question,
|
|
81
|
+
answer
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
results.push({
|
|
86
|
+
question: q.question,
|
|
87
|
+
answer: '(Skipped)'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Print styled Q&A block in console
|
|
92
|
+
console.log('\n' + pc.cyan('────────────────────────────────────────────'));
|
|
93
|
+
const formattedBlocks = results.map(r => {
|
|
94
|
+
console.log(pc.bold(pc.white(`Q: ${r.question}`)));
|
|
95
|
+
console.log(pc.green(`A: ${r.answer}\n`));
|
|
96
|
+
return `Q: ${r.question}\n\nA: ${r.answer}`;
|
|
97
|
+
});
|
|
98
|
+
console.log(pc.cyan('────────────────────────────────────────────\n'));
|
|
99
|
+
return formattedBlocks.join('\n\n');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const IGNORED_DIRS = new Set([
|
|
4
|
+
'node_modules',
|
|
5
|
+
'.git',
|
|
6
|
+
'.next',
|
|
7
|
+
'dist',
|
|
8
|
+
'build',
|
|
9
|
+
'coverage',
|
|
10
|
+
'.turbo',
|
|
11
|
+
'.cache',
|
|
12
|
+
'vendor'
|
|
13
|
+
]);
|
|
14
|
+
async function searchFiles(dir, query, results, maxResults = 50) {
|
|
15
|
+
if (results.length >= maxResults)
|
|
16
|
+
return;
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (results.length >= maxResults)
|
|
26
|
+
break;
|
|
27
|
+
const fullPath = path.join(dir, entry.name);
|
|
28
|
+
const relPath = path.relative(process.cwd(), fullPath) || fullPath;
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
31
|
+
await searchFiles(fullPath, query, results, maxResults);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (entry.isFile()) {
|
|
35
|
+
// Skip large binary extensions
|
|
36
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
37
|
+
if (['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.wasm'].includes(ext)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
42
|
+
if (content.toLowerCase().includes(query.toLowerCase())) {
|
|
43
|
+
const lines = content.split('\n');
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
if (results.length >= maxResults)
|
|
46
|
+
break;
|
|
47
|
+
if (lines[i].toLowerCase().includes(query.toLowerCase())) {
|
|
48
|
+
const preview = lines[i].trim();
|
|
49
|
+
results.push(`${relPath}:${i + 1}: ${preview}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export const searchTool = {
|
|
59
|
+
name: 'search',
|
|
60
|
+
definition: {
|
|
61
|
+
name: 'search',
|
|
62
|
+
description: 'Search for text or patterns in project files (grep-like)',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
query: { type: 'string', description: 'Text or substring to search for' },
|
|
67
|
+
dir: { type: 'string', description: 'Directory to search in (default: .)' }
|
|
68
|
+
},
|
|
69
|
+
required: ['query']
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
validateArgs(args) {
|
|
73
|
+
if (!args.query || typeof args.query !== 'string')
|
|
74
|
+
throw new Error('query is required');
|
|
75
|
+
},
|
|
76
|
+
async execute(args) {
|
|
77
|
+
const targetDir = args.dir || args.path || '.';
|
|
78
|
+
const results = [];
|
|
79
|
+
try {
|
|
80
|
+
await searchFiles(targetDir, args.query, results, 50);
|
|
81
|
+
if (results.length === 0) {
|
|
82
|
+
return `No matches found for "${args.query}" in ${targetDir}.`;
|
|
83
|
+
}
|
|
84
|
+
return results.join('\n');
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
return `Failed to search: ${err.message}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
function stripHtml(html) {
|
|
2
|
+
return html
|
|
3
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
4
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
|
5
|
+
.replace(/<svg\b[^<]*(?:(?!<\/svg>)<[^<]*)*<\/svg>/gi, '')
|
|
6
|
+
.replace(/<[^>]+>/g, ' ')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/'/g, "'")
|
|
12
|
+
.replace(/ /g, ' ')
|
|
13
|
+
.replace(/\s+/g, ' ')
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
16
|
+
export const webSearchTool = {
|
|
17
|
+
name: 'web_search',
|
|
18
|
+
definition: {
|
|
19
|
+
name: 'web_search',
|
|
20
|
+
description: 'Search the web using DuckDuckGo to get fresh documentation, API references, library examples, or solutions for code bugs.',
|
|
21
|
+
parameters: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
query: { type: 'string', description: 'Search query' }
|
|
25
|
+
},
|
|
26
|
+
required: ['query']
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
validateArgs(args) {
|
|
30
|
+
if (!args.query || typeof args.query !== 'string')
|
|
31
|
+
throw new Error('query is required');
|
|
32
|
+
},
|
|
33
|
+
async execute(args) {
|
|
34
|
+
try {
|
|
35
|
+
const encoded = encodeURIComponent(args.query);
|
|
36
|
+
const url = `https://html.duckduckgo.com/html/?q=${encoded}`;
|
|
37
|
+
const res = await fetch(url, {
|
|
38
|
+
headers: {
|
|
39
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
40
|
+
},
|
|
41
|
+
signal: AbortSignal.timeout(10000)
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`DuckDuckGo returned status ${res.status}`);
|
|
45
|
+
}
|
|
46
|
+
const html = await res.text();
|
|
47
|
+
const results = [];
|
|
48
|
+
// Extract results from DDG HTML
|
|
49
|
+
const resultRegex = /<a class="result__url" href="([^"]+)".*?<a class="result__snippet[^>]*>(.*?)<\/a>/gs;
|
|
50
|
+
const titleRegex = /<a class="result__a" href="([^"]+)">(.*?)<\/a>/g;
|
|
51
|
+
const titles = [];
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = titleRegex.exec(html)) !== null && titles.length < 6) {
|
|
54
|
+
let rawLink = match[1];
|
|
55
|
+
if (rawLink.includes('uddg=')) {
|
|
56
|
+
const urlParam = rawLink.split('uddg=')[1]?.split('&')[0];
|
|
57
|
+
if (urlParam)
|
|
58
|
+
rawLink = decodeURIComponent(urlParam);
|
|
59
|
+
}
|
|
60
|
+
titles.push({
|
|
61
|
+
link: rawLink,
|
|
62
|
+
title: stripHtml(match[2])
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const snippetRegex = /<a class="result__snippet[^>]*>(.*?)<\/a>/g;
|
|
66
|
+
const snippets = [];
|
|
67
|
+
while ((match = snippetRegex.exec(html)) !== null && snippets.length < 6) {
|
|
68
|
+
snippets.push(stripHtml(match[1]));
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < titles.length; i++) {
|
|
71
|
+
results.push({
|
|
72
|
+
title: titles[i].title,
|
|
73
|
+
link: titles[i].link,
|
|
74
|
+
snippet: snippets[i] || ''
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (results.length === 0) {
|
|
78
|
+
// Fallback: try Instant Answer API
|
|
79
|
+
try {
|
|
80
|
+
const instantRes = await fetch(`https://api.duckduckgo.com/?q=${encoded}&format=json`, {
|
|
81
|
+
signal: AbortSignal.timeout(5000)
|
|
82
|
+
});
|
|
83
|
+
if (instantRes.ok) {
|
|
84
|
+
const data = await instantRes.json();
|
|
85
|
+
if (data.AbstractText) {
|
|
86
|
+
return `Summary: ${data.AbstractText}\nSource: ${data.AbstractURL || ''}`;
|
|
87
|
+
}
|
|
88
|
+
if (data.RelatedTopics && data.RelatedTopics.length > 0) {
|
|
89
|
+
const items = data.RelatedTopics.slice(0, 5)
|
|
90
|
+
.filter((t) => t.Text && t.FirstURL)
|
|
91
|
+
.map((t) => `• ${t.Text}\n URL: ${t.FirstURL}`);
|
|
92
|
+
if (items.length > 0)
|
|
93
|
+
return items.join('\n\n');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { }
|
|
98
|
+
return `No web results found for "${args.query}".`;
|
|
99
|
+
}
|
|
100
|
+
const formatted = results.map((r, i) => `[${i + 1}] ${r.title}\nURL: ${r.link}\nSnippet: ${r.snippet}`).join('\n\n');
|
|
101
|
+
return formatted;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return `Web search failed: ${err.message}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export const fetchUrlTool = {
|
|
109
|
+
name: 'fetch_url',
|
|
110
|
+
definition: {
|
|
111
|
+
name: 'fetch_url',
|
|
112
|
+
description: 'Fetch and read the text content of a public URL or documentation web page.',
|
|
113
|
+
parameters: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
url: { type: 'string', description: 'Web URL (http/https) to fetch' }
|
|
117
|
+
},
|
|
118
|
+
required: ['url']
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
validateArgs(args) {
|
|
122
|
+
if (!args.url || typeof args.url !== 'string')
|
|
123
|
+
throw new Error('url is required');
|
|
124
|
+
},
|
|
125
|
+
async execute(args) {
|
|
126
|
+
try {
|
|
127
|
+
let targetUrl = args.url.trim();
|
|
128
|
+
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
|
|
129
|
+
targetUrl = `https://${targetUrl}`;
|
|
130
|
+
}
|
|
131
|
+
const res = await fetch(targetUrl, {
|
|
132
|
+
headers: {
|
|
133
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
134
|
+
},
|
|
135
|
+
signal: AbortSignal.timeout(12000)
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
throw new Error(`HTTP Error ${res.status}: ${res.statusText}`);
|
|
139
|
+
}
|
|
140
|
+
const html = await res.text();
|
|
141
|
+
const cleanText = stripHtml(html);
|
|
142
|
+
if (!cleanText) {
|
|
143
|
+
return 'Webpage returned empty text content.';
|
|
144
|
+
}
|
|
145
|
+
const maxLength = 10000;
|
|
146
|
+
if (cleanText.length > maxLength) {
|
|
147
|
+
return cleanText.substring(0, maxLength) + `\n\n[... truncated ${cleanText.length - maxLength} characters]`;
|
|
148
|
+
}
|
|
149
|
+
return cleanText;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return `Failed to fetch URL: ${err.message}`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "termux-dev",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
|
|
5
|
+
"main": "dist/cli/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"preferGlobal": true,
|
|
8
|
+
"bin": {
|
|
9
|
+
"devx": "bin/devx.js",
|
|
10
|
+
"termux-dev": "bin/devx.js"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin",
|
|
17
|
+
"dist",
|
|
18
|
+
"assets"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"start": "node ./bin/devx.js",
|
|
23
|
+
"dev": "tsc --watch",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20.0.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"ai",
|
|
31
|
+
"agent",
|
|
32
|
+
"cli",
|
|
33
|
+
"coding-assistant",
|
|
34
|
+
"vibe-coding",
|
|
35
|
+
"termux",
|
|
36
|
+
"openrouter",
|
|
37
|
+
"llm",
|
|
38
|
+
"developer-tools",
|
|
39
|
+
"autonomous-agent"
|
|
40
|
+
],
|
|
41
|
+
"author": "ApvCode",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/apvcode/Termux-Dev.git"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/apvcode/Termux-Dev/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/apvcode/Termux-Dev#readme",
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@clack/prompts": "^1.7.0",
|
|
53
|
+
"@inquirer/prompts": "^8.6.0",
|
|
54
|
+
"commander": "^12.0.0",
|
|
55
|
+
"marked": "^15.0.12",
|
|
56
|
+
"marked-terminal": "^7.3.0",
|
|
57
|
+
"picocolors": "^1.1.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@types/marked-terminal": "^6.1.1",
|
|
61
|
+
"@types/node": "^20.0.0",
|
|
62
|
+
"typescript": "^5.4.0"
|
|
63
|
+
}
|
|
64
|
+
}
|