clarity-ai 2.0.1 → 3.0.1

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/AGENTS.md CHANGED
@@ -45,3 +45,7 @@ SSE parsing pattern: read `data: ` lines, parse JSON, yield `choices[0].delta.co
45
45
  - **No test framework**: none configured. No lint/typecheck scripts.
46
46
  - **Keypress events**: `process.stdin.on('keypress', ...)` works because `readline.createInterface({ terminal: true })` internally calls `emitKeypressEvents`. Set raw mode BEFORE creating readline if needed.
47
47
  - **Prompt bar**: `renderPromptBar()` draws grey-filled purple box. Call after every output cycle so the bar stays at bottom. `showPrompt()` writes ` ◆ `. Always pair: render bar, then show prompt.
48
+ - **No build step (v3)**: All Ink+React components use `React.createElement()` directly — NO JSX. Node.js cannot parse JSX without a transpiler.
49
+ - **TTY check**: `src/index.js` checks `process.stdin.isTTY` before rendering Ink — prevents `Raw mode not supported` error in non-TTY environments.
50
+ - **Global state**: `src/store.js` uses `useReducer` with a reducer function — dispatches `THOUGHT_START`, `TOOL_CALL`, `ADD_MESSAGE`, `SET_TODOS`, etc.
51
+ - **Parallel subagents**: `src/agent/subagent.js` — `shouldParallelize()` detects multi-step tasks, `planSubagents()` calls model for JSON array, `runSubagents()` runs `Promise.all` with AGENT_REGISTER/TOOLCALL dispatches.
package/bin/clarity.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import '../src/main.js';
2
+ import '../src/index.js';
package/package.json CHANGED
@@ -1,39 +1,25 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "2.0.1",
4
- "description": "Autonomous AI Agent CLI for Termux",
3
+ "version": "3.0.1",
4
+ "description": "Autonomous AI Agent Terminal — OpenCode-style TUI for Termux",
5
5
  "type": "module",
6
- "bin": {
7
- "clarity": "bin/clarity.js"
8
- },
6
+ "bin": { "clarity": "./bin/clarity.js" },
9
7
  "scripts": {
10
8
  "start": "node bin/clarity.js",
11
9
  "dev": "node --watch bin/clarity.js"
12
10
  },
13
- "engines": {
14
- "node": ">=18.0.0"
15
- },
16
11
  "dependencies": {
12
+ "ink": "^4.4.1",
13
+ "react": "^18.2.0",
17
14
  "chalk": "^5.3.0",
18
- "figlet": "^1.7.0",
19
- "gradient-string": "^2.0.2",
20
- "ora": "^8.1.0",
21
- "inquirer": "^9.2.12",
22
- "conf": "^12.0.0",
23
- "marked": "^12.0.0",
24
- "marked-terminal": "^7.1.0",
25
- "cli-table3": "^0.6.5",
26
- "wrap-ansi": "^9.0.1",
27
- "semver": "^7.6.0",
28
- "glob": "^10.3.12"
15
+ "figures": "^6.1.0",
16
+ "ora": "^8.0.1",
17
+ "dotenv": "^16.4.5",
18
+ "node-fetch": "^3.3.2",
19
+ "strip-ansi": "^7.1.0",
20
+ "wrap-ansi": "^9.0.0",
21
+ "cli-truncate": "^4.0.0",
22
+ "ink-text-input": "^5.0.1"
29
23
  },
30
- "keywords": [
31
- "ai",
32
- "cli",
33
- "termux",
34
- "agent",
35
- "autonomous",
36
- "clarity"
37
- ],
38
- "license": "MIT"
24
+ "engines": { "node": ">=18.0.0" }
39
25
  }
@@ -1,15 +1,9 @@
1
- const HIGH_WEIGHT = ['create','build','write','make','generate','fix','debug','patch','run','execute','install','setup','search and','find and','look up and'];
2
-
3
- const LOW_WEIGHT = ['can you','please','help me'];
4
-
5
1
  export function detectIntent(message) {
6
2
  const msg = message.toLowerCase();
3
+ const HIGH = ['create','build','write','make','generate','fix','debug','patch','run','execute','install','setup','search and','find and','look up and'];
4
+ const LOW = ['can you','please','help me'];
7
5
  let score = 0;
8
- for (const w of HIGH_WEIGHT) {
9
- if (msg.includes(w)) { score += 2; break; }
10
- }
11
- for (const w of LOW_WEIGHT) {
12
- if (msg.includes(w)) { score += 1; }
13
- }
6
+ for (const w of HIGH) { if (msg.includes(w)) { score += 2; break; } }
7
+ for (const w of LOW) { if (msg.includes(w)) score += 1; }
14
8
  return score >= 2 ? 'agent' : 'chat';
15
9
  }
@@ -0,0 +1,143 @@
1
+ export async function callProvider({ provider, model, apiKey, system, messages, temperature }) {
2
+ const urls = {
3
+ groq: 'https://api.groq.com/openai/v1/chat/completions',
4
+ openrouter: 'https://openrouter.ai/api/v1/chat/completions',
5
+ openai: 'https://api.openai.com/v1/chat/completions',
6
+ };
7
+ const url = urls[provider];
8
+ if (!url) throw new Error(`Unknown provider: ${provider}`);
9
+
10
+ const body = { model, messages: [{ role: 'system', content: system }, ...messages], temperature: temperature || 0.3, stream: false };
11
+ const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` };
12
+
13
+ const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
14
+ if (!res.ok) throw new Error(`API ${res.status}: ${await res.text()}`);
15
+ const data = await res.json();
16
+ return data.choices?.[0]?.message?.content || '';
17
+ }
18
+
19
+ import { TOOLS, executeTool } from './tools.js';
20
+
21
+ const AGENT_SYSTEM = `You are CLARITY, an autonomous terminal AI agent.
22
+ Respond ONLY with JSON. Never plain text. Never markdown.
23
+
24
+ For each step:
25
+ {
26
+ "thought": "what I'm doing and why (1-2 sentences)",
27
+ "todos": ["task 1", "task 2", ...], // only on first step
28
+ "tool": "tool_name",
29
+ "args": {},
30
+ "done": false
31
+ }
32
+
33
+ When complete:
34
+ { "thought": "...", "tool": null, "args": {}, "done": true, "summary": "..." }
35
+
36
+ Tools: file_read, file_write, file_append, file_delete, file_list, file_exists,
37
+ shell_run, code_run, npm_install, pkg_install,
38
+ web_search, web_fetch, sys_info, git_status, git_commit
39
+
40
+ Rules:
41
+ - Write files with file_write, not shell echo
42
+ - Run code with code_run, not shell_run
43
+ - If a tool errors, try a different approach
44
+ - Max 30 steps
45
+ - Stay focused on the original task`;
46
+
47
+ export async function runAgent(userMessage, config, dispatch) {
48
+ const history = [];
49
+ let step = 0;
50
+ const MAX = 30;
51
+
52
+ // Show thought starting
53
+ dispatch({ type: 'THOUGHT_START' });
54
+
55
+ while (step < MAX) {
56
+ step++;
57
+ const thinkStart = Date.now();
58
+
59
+ // Call model
60
+ const raw = await callProvider({
61
+ provider: config.provider,
62
+ model: config.model,
63
+ apiKey: config.apiKey,
64
+ system: AGENT_SYSTEM,
65
+ messages: [
66
+ ...history,
67
+ { role: 'user', content: step === 1 ? userMessage : 'Next step.' }
68
+ ],
69
+ temperature: 0.3,
70
+ });
71
+
72
+ const elapsed = ((Date.now() - thinkStart) / 1000).toFixed(1);
73
+
74
+ // Parse JSON
75
+ let action;
76
+ try {
77
+ const cleaned = raw.replace(/```json|```/g, '').trim();
78
+ action = JSON.parse(cleaned);
79
+ } catch {
80
+ // Retry with explicit reminder
81
+ history.push({ role: 'assistant', content: raw });
82
+ history.push({ role: 'user', content: 'ERROR: respond only with valid JSON, no other text.' });
83
+ continue;
84
+ }
85
+
86
+ // Emit thought
87
+ dispatch({ type: 'THOUGHT_DONE', payload: { text: action.thought, elapsed } });
88
+
89
+ // Emit todos on first step
90
+ if (action.todos?.length) {
91
+ dispatch({ type: 'SET_TODOS', payload: action.todos.map((t, i) => ({
92
+ id: i, text: t, status: 'pending'
93
+ }))});
94
+ }
95
+
96
+ // Done?
97
+ if (action.done) {
98
+ dispatch({ type: 'AGENT_DONE', payload: action.summary });
99
+ return;
100
+ }
101
+
102
+ // Execute tool
103
+ if (action.tool) {
104
+ dispatch({ type: 'TOOL_START', payload: { name: action.tool, args: action.args } });
105
+ dispatch({ type: 'TODO_SET_RUNNING', payload: step - 1 });
106
+
107
+ const toolStart = Date.now();
108
+ let result, status;
109
+
110
+ try {
111
+ result = await executeTool(action.tool, action.args);
112
+ status = 'done';
113
+ dispatch({ type: 'TODO_SET_DONE', payload: step - 1 });
114
+ } catch (err) {
115
+ result = err.message;
116
+ status = 'error';
117
+ dispatch({ type: 'TODO_SET_FAIL', payload: step - 1 });
118
+ }
119
+
120
+ const toolMs = Date.now() - toolStart;
121
+ dispatch({ type: 'TOOL_DONE', payload: { result, status, elapsed: toolMs } });
122
+
123
+ // Feed back to model
124
+ history.push({ role: 'assistant', content: JSON.stringify(action) });
125
+ history.push({ role: 'user', content: `Tool ${action.tool} result (${toolMs}ms):\n${String(result).slice(0, 3000)}` });
126
+
127
+ // Emit agent step message
128
+ dispatch({ type: 'ADD_MESSAGE', payload: {
129
+ type: 'tool',
130
+ tool: action.tool,
131
+ args: action.args,
132
+ result,
133
+ status,
134
+ elapsed: toolMs,
135
+ }});
136
+
137
+ // Think again
138
+ dispatch({ type: 'THOUGHT_START' });
139
+ }
140
+ }
141
+
142
+ dispatch({ type: 'AGENT_DONE', payload: `Completed ${step} steps.` });
143
+ }
@@ -0,0 +1,55 @@
1
+ import { runAgent } from './runner.js';
2
+
3
+ // Detect if task warrants parallel agents
4
+ export function shouldParallelize(task) {
5
+ const BIG_TASK = [
6
+ /rewrite (the )?(whole|entire|all|full)/i,
7
+ /create (a )?full/i,
8
+ /build (a )?(complete|entire)/i,
9
+ /rebuild/i,
10
+ ];
11
+ return BIG_TASK.some(p => p.test(task));
12
+ }
13
+
14
+ // Split a large task into parallel sub-tasks
15
+ export async function planSubagents(task, callModel) {
16
+ const plan = await callModel({
17
+ system: `Split this task into 3-5 parallel independent subtasks.
18
+ Respond ONLY with JSON array: [{"name": "short name", "task": "full task description"}, ...]
19
+ Each subtask must be fully independent — no subtask depends on another's output.`,
20
+ messages: [{ role: 'user', content: task }]
21
+ });
22
+
23
+ try {
24
+ return JSON.parse(plan.replace(/```json|```/g, '').trim());
25
+ } catch {
26
+ return null; // fall back to single agent
27
+ }
28
+ }
29
+
30
+ // Run multiple agents in parallel
31
+ export async function runSubagents(subtasks, config, dispatch) {
32
+ // Register all agents
33
+ const agentIds = subtasks.map((t, i) => {
34
+ const id = `agent_${i}`;
35
+ dispatch({ type: 'AGENT_REGISTER', payload: {
36
+ id, name: `General Task — ${t.name}`,
37
+ status: 'running', toolcalls: 0, startTime: Date.now()
38
+ }});
39
+ return id;
40
+ });
41
+
42
+ // Run all in parallel via Promise.all
43
+ await Promise.all(subtasks.map((subtask, i) =>
44
+ runAgent(subtask.task, config, (action) => {
45
+ // Intercept tool dispatches to update agent's toolcall count
46
+ if (action.type === 'TOOL_START') {
47
+ dispatch({ type: 'AGENT_TOOLCALL', payload: { id: agentIds[i] } });
48
+ dispatch({ type: 'AGENT_SET_TASK', payload: { id: agentIds[i], task: action.payload.name } });
49
+ }
50
+ dispatch(action);
51
+ })
52
+ .then(() => dispatch({ type: 'AGENT_DONE_ID', payload: agentIds[i] }))
53
+ .catch(e => dispatch({ type: 'AGENT_FAIL_ID', payload: { id: agentIds[i], error: e.message } }))
54
+ ));
55
+ }
@@ -1,254 +1,132 @@
1
+ import fs from 'fs/promises';
1
2
  import { execSync } from 'child_process';
2
- import { readFileSync, writeFileSync, appendFileSync, unlinkSync, readdirSync, existsSync } from 'fs';
3
- import { cpus, totalmem } from 'os';
3
+ import { exec } from 'child_process';
4
+ import { promisify } from 'util';
4
5
 
5
- const tools = [];
6
+ const run = promisify(exec);
6
7
 
7
- tools.push({
8
- name: 'file_read',
9
- description: 'Read file contents',
10
- args: [{ name: 'path', type: 'string', required: true, description: 'File path to read' }],
11
- execute({ path }) {
12
- return readFileSync(path, 'utf-8');
13
- }
14
- });
15
-
16
- tools.push({
17
- name: 'file_write',
18
- description: 'Write or create a file',
19
- args: [
20
- { name: 'path', type: 'string', required: true, description: 'File path' },
21
- { name: 'content', type: 'string', required: true, description: 'Content to write' }
22
- ],
23
- execute({ path, content }) {
24
- writeFileSync(path, content, 'utf-8');
25
- return `Written ${path}`;
26
- }
27
- });
28
-
29
- tools.push({
30
- name: 'file_append',
31
- description: 'Append content to a file',
32
- args: [
33
- { name: 'path', type: 'string', required: true, description: 'File path' },
34
- { name: 'content', type: 'string', required: true, description: 'Content to append' }
35
- ],
36
- execute({ path, content }) {
37
- appendFileSync(path, content + '\n', 'utf-8');
38
- return `Appended to ${path}`;
39
- }
40
- });
41
-
42
- tools.push({
43
- name: 'file_delete',
44
- description: 'Delete a file',
45
- args: [{ name: 'path', type: 'string', required: true, description: 'File path' }],
46
- execute({ path }) {
47
- unlinkSync(path);
48
- return `Deleted ${path}`;
49
- }
50
- });
51
-
52
- tools.push({
53
- name: 'file_list',
54
- description: 'List directory contents',
55
- args: [{ name: 'dir', type: 'string', required: false, description: 'Directory path (default: current)' }],
56
- execute({ dir }) {
57
- return readdirSync(dir || '.').join('\n');
58
- }
59
- });
60
-
61
- tools.push({
62
- name: 'file_exists',
63
- description: 'Check if a file exists',
64
- args: [{ name: 'path', type: 'string', required: true, description: 'File path' }],
65
- execute({ path }) {
66
- return String(existsSync(path));
67
- }
68
- });
69
-
70
- tools.push({
71
- name: 'shell_run',
72
- description: 'Run a bash command and capture stdout+stderr',
73
- args: [{ name: 'command', type: 'string', required: true, description: 'Bash command to run' }],
74
- execute({ command }) {
75
- return execSync(command, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }) || '(no output)';
76
- }
77
- });
78
-
79
- tools.push({
80
- name: 'shell_run_silent',
81
- description: 'Run a bash command with no output captured',
82
- args: [{ name: 'command', type: 'string', required: true, description: 'Bash command to run' }],
83
- execute({ command }) {
84
- execSync(command, { encoding: 'utf-8', stdio: 'ignore' });
85
- return 'done';
86
- }
87
- });
88
-
89
- tools.push({
90
- name: 'npm_install',
91
- description: 'Install npm packages',
92
- args: [{ name: 'packages', type: 'array', required: true, description: 'Package names to install' }],
93
- execute({ packages }) {
94
- const pkgs = Array.isArray(packages) ? packages.join(' ') : packages;
95
- return execSync(`npm install ${pkgs}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }) || 'done';
96
- }
97
- });
98
-
99
- tools.push({
100
- name: 'pkg_install',
101
- description: 'Install Termux packages via pkg',
102
- args: [{ name: 'packages', type: 'array', required: true, description: 'Package names to install' }],
103
- execute({ packages }) {
104
- const pkgs = Array.isArray(packages) ? packages.join(' ') : packages;
105
- return execSync(`pkg install -y ${pkgs}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }) || 'done';
106
- }
107
- });
108
-
109
- tools.push({
110
- name: 'web_search',
111
- description: 'Search DuckDuckGo for instant answers',
112
- args: [{ name: 'query', type: 'string', required: true, description: 'Search query' }],
113
- async execute({ query }) {
114
- const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1`;
115
- const res = await fetch(url);
116
- const data = await res.json();
117
- return data.AbstractText || (data.RelatedTopics || []).slice(0, 5).map(t => t.Text || t).join('\n') || 'No results found';
118
- }
119
- });
120
-
121
- tools.push({
122
- name: 'web_fetch',
123
- description: 'Fetch a URL and return text content',
124
- args: [{ name: 'url', type: 'string', required: true, description: 'URL to fetch' }],
125
- async execute({ url }) {
126
- const res = await fetch(url);
127
- const html = await res.text();
128
- return html.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim().slice(0, 10000);
129
- }
130
- });
131
-
132
- tools.push({
133
- name: 'code_run',
134
- description: 'Write code to temp file and execute it',
135
- args: [
136
- { name: 'language', type: 'string', required: true, description: 'Programming language (js, py, sh, rb, go, rs)' },
137
- { name: 'code', type: 'string', required: true, description: 'Source code to run' }
138
- ],
139
- execute({ language, code }) {
140
- const extMap = { js: '.js', py: '.py', sh: '.sh', rb: '.rb', go: '.go', rs: '.rs' };
141
- const ext = extMap[language] || '.tmp';
142
- const tmp = `/tmp/clarity-code-${Date.now()}${ext}`;
143
- writeFileSync(tmp, code, 'utf-8');
144
- const runner = {
145
- js: `node ${tmp}`, py: `python3 ${tmp}`, sh: `bash ${tmp}`,
146
- rb: `ruby ${tmp}`, go: `go run ${tmp}`,
147
- rs: `rustc ${tmp} -o /tmp/clarity-bin && /tmp/clarity-bin`
148
- };
149
- return execSync(runner[language] || `bash ${tmp}`, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
150
- }
151
- });
152
-
153
- tools.push({
154
- name: 'git_status',
155
- description: 'Show current git repository status',
156
- args: [],
157
- execute() {
158
- return execSync('git status', { encoding: 'utf-8' });
159
- }
160
- });
161
-
162
- tools.push({
163
- name: 'git_commit',
164
- description: 'Stage all changes and commit',
165
- args: [{ name: 'message', type: 'string', required: true, description: 'Commit message' }],
166
- execute({ message }) {
167
- execSync('git add -A', { encoding: 'utf-8' });
168
- const safeMsg = message.replace(/'/g, "'\\''");
169
- return execSync(`git commit -m '${safeMsg}'`, { encoding: 'utf-8' });
170
- }
171
- });
172
-
173
- tools.push({
174
- name: 'sys_info',
175
- description: 'Get CPU, memory, Node version, platform info',
176
- args: [],
177
- execute() {
178
- const info = {
179
- platform: process.platform,
180
- node: process.version,
181
- cpu: cpus().length + ' cores',
182
- memory: Math.round(totalmem() / 1024 / 1024 / 1024) + ' GB',
183
- arch: process.arch,
184
- cwd: process.cwd()
185
- };
186
- return Object.entries(info).map(([k, v]) => `${k}: ${v}`).join('\n');
187
- }
188
- });
189
-
190
- tools.push({
191
- name: 'env_get',
192
- description: 'Get an environment variable',
193
- args: [{ name: 'key', type: 'string', required: true, description: 'Variable name' }],
194
- execute({ key }) {
195
- return process.env[key] || '(not set)';
196
- }
197
- });
198
-
199
- tools.push({
200
- name: 'env_set',
201
- description: 'Set an environment variable in .env file',
202
- args: [
203
- { name: 'key', type: 'string', required: true, description: 'Variable name' },
204
- { name: 'value', type: 'string', required: true, description: 'Variable value' }
205
- ],
206
- execute({ key, value }) {
207
- appendFileSync('.env', `${key}=${value}\n`, 'utf-8');
208
- return `Set ${key}=${value}`;
209
- }
210
- });
211
-
212
- tools.push({
213
- name: 'ai_ask',
214
- description: 'Ask another AI model a question',
215
- args: [
216
- { name: 'prompt', type: 'string', required: true, description: 'Prompt to send' },
217
- { name: 'model', type: 'string', required: false, description: 'Model name (e.g. groq/llama-3.3-70b-versatile)' }
218
- ],
219
- async execute({ prompt, model }) {
220
- const { sendMessage } = await import('../providers/index.js');
221
- const m = model || 'groq/llama-3.3-70b-versatile';
222
- const messages = [{ role: 'user', content: prompt }];
223
- const gen = sendMessage(process.env.SECRET_API_KEY || '', messages, m, false);
224
- let result = '';
225
- for await (const chunk of gen) {
226
- result += chunk;
227
- }
228
- return result;
229
- }
230
- });
231
-
232
- tools.push({
233
- name: 'summarize',
234
- description: 'Summarize a long text using AI',
235
- args: [{ name: 'text', type: 'string', required: true, description: 'Text to summarize' }],
236
- async execute({ text }) {
237
- const { sendMessage } = await import('../providers/index.js');
238
- const messages = [{ role: 'user', content: `Summarize this text concisely:\n\n${text}` }];
239
- const gen = sendMessage(process.env.SECRET_API_KEY || '', messages, 'groq/llama-3.3-70b-versatile', false);
240
- let result = '';
241
- for await (const chunk of gen) {
242
- result += chunk;
243
- }
244
- return result;
8
+ // ── File Tools ─────────────────────────────────────────────────
9
+ export async function file_read({ path }) {
10
+ return (await fs.readFile(path, 'utf8')).slice(0, 8000);
11
+ }
12
+
13
+ export async function file_write({ path, content }) {
14
+ const dir = path.includes('/') ? path.split('/').slice(0,-1).join('/') : null;
15
+ if (dir) await fs.mkdir(dir, { recursive: true });
16
+ await fs.writeFile(path, content, 'utf8');
17
+ return `Written ${path} (${content.length} bytes)`;
18
+ }
19
+
20
+ export async function file_append({ path, content }) {
21
+ await fs.appendFile(path, content, 'utf8');
22
+ return `Appended to ${path}`;
23
+ }
24
+
25
+ export async function file_delete({ path }) {
26
+ await fs.unlink(path);
27
+ return `Deleted ${path}`;
28
+ }
29
+
30
+ export async function file_list({ dir = '.' }) {
31
+ const entries = await fs.readdir(dir, { withFileTypes: true });
32
+ return entries.map(e => `${e.isDirectory() ? '📁' : '📄'} ${e.name}`).join('\n');
33
+ }
34
+
35
+ export async function file_exists({ path }) {
36
+ try { await fs.access(path); return 'exists'; }
37
+ catch { return 'not found'; }
38
+ }
39
+
40
+ // ── Shell Tools ─────────────────────────────────────────────────
41
+ export async function shell_run({ command, cwd, timeout = 30000 }) {
42
+ try {
43
+ const { stdout, stderr } = await run(command, {
44
+ cwd: cwd || process.cwd(),
45
+ timeout,
46
+ maxBuffer: 2 * 1024 * 1024
47
+ });
48
+ return ((stdout || '') + (stderr || '')).trim().slice(0, 5000);
49
+ } catch (e) {
50
+ return `EXIT ${e.code || 1}: ${e.message}`.slice(0, 2000);
245
51
  }
246
- });
52
+ }
53
+
54
+ export async function code_run({ language, code }) {
55
+ const exts = { python: 'py', javascript: 'js', node: 'js', bash: 'sh', python3: 'py' };
56
+ const cmds = { python: 'python3', javascript: 'node', node: 'node', bash: 'bash', python3: 'python3' };
57
+ const ext = exts[language] || 'sh';
58
+ const cmd = cmds[language] || 'bash';
59
+ const tmp = `/tmp/clarity_${Date.now()}.${ext}`;
60
+ await fs.writeFile(tmp, code);
61
+ const result = await shell_run({ command: `${cmd} ${tmp}`, timeout: 15000 });
62
+ try { await fs.unlink(tmp); } catch {}
63
+ return result;
64
+ }
65
+
66
+ export async function npm_install({ packages, cwd, flags = '' }) {
67
+ const pkgStr = Array.isArray(packages) ? packages.join(' ') : packages;
68
+ // Termux-safe: always use --no-bin-links
69
+ return await shell_run({
70
+ command: `npm install --no-bin-links ${flags} ${pkgStr}`,
71
+ cwd: cwd || process.cwd()
72
+ });
73
+ }
247
74
 
248
- export { tools };
75
+ export async function pkg_install({ packages }) {
76
+ const pkgStr = Array.isArray(packages) ? packages.join(' ') : packages;
77
+ return await shell_run({ command: `pkg install -y ${pkgStr}` });
78
+ }
79
+
80
+ // ── Web Tools ─────────────────────────────────────────────────
81
+ export async function web_search({ query }) {
82
+ const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
83
+ const r = await fetch(url, { headers: { 'User-Agent': 'clarity-ai/3.0' } });
84
+ const d = await r.json();
85
+ const results = [
86
+ d.AbstractText && `Summary: ${d.AbstractText}`,
87
+ ...(d.RelatedTopics||[]).slice(0,6).map(t => t.Text).filter(Boolean),
88
+ ].filter(Boolean).join('\n\n');
89
+ return results || `No results for: ${query}`;
90
+ }
91
+
92
+ export async function web_fetch({ url, selector }) {
93
+ const r = await fetch(url, { headers: { 'User-Agent': 'clarity-ai/3.0' } });
94
+ const html = await r.text();
95
+ // Strip tags + collapse whitespace
96
+ const text = html
97
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
98
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
99
+ .replace(/<[^>]+>/g, ' ')
100
+ .replace(/\s+/g, ' ')
101
+ .trim()
102
+ .slice(0, 6000);
103
+ return text;
104
+ }
105
+
106
+ // ── System Tools ─────────────────────────────────────────────
107
+ export async function sys_info() {
108
+ return await shell_run({ command: 'uname -a && node -v && free -h 2>/dev/null | head -3 || true' });
109
+ }
110
+
111
+ export async function git_status() {
112
+ return await shell_run({ command: 'git status --short && git log --oneline -5' });
113
+ }
114
+
115
+ export async function git_commit({ message, add = '.' }) {
116
+ const r1 = await shell_run({ command: `git add ${add}` });
117
+ const r2 = await shell_run({ command: `git commit -m "${message.replace(/"/g, '\\"')}"` });
118
+ return r1 + '\n' + r2;
119
+ }
249
120
 
250
- export async function executeTool(name, argsObj) {
251
- const tool = tools.find(t => t.name === name);
252
- if (!tool) throw new Error(`Unknown tool: ${name}`);
253
- return await tool.execute(argsObj);
121
+ // ── Registry ─────────────────────────────────────────────────
122
+ export const TOOLS = {
123
+ file_read, file_write, file_append, file_delete, file_list, file_exists,
124
+ shell_run, code_run, npm_install, pkg_install,
125
+ web_search, web_fetch,
126
+ sys_info, git_status, git_commit,
127
+ };
128
+
129
+ export async function executeTool(name, args) {
130
+ if (!TOOLS[name]) throw new Error(`Unknown tool: ${name}. Available: ${Object.keys(TOOLS).join(', ')}`);
131
+ return await TOOLS[name](args);
254
132
  }