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 +4 -0
- package/bin/clarity.js +1 -1
- package/package.json +14 -28
- package/src/agent/intent.js +4 -10
- package/src/agent/runner.js +143 -0
- package/src/agent/subagent.js +55 -0
- package/src/agent/tools.js +125 -247
- package/src/app.js +88 -0
- package/src/commands/index.js +137 -14
- package/src/components/AIMessage.js +21 -0
- package/src/components/AgentTree.js +42 -0
- package/src/components/Banner.js +15 -0
- package/src/components/InputBar.js +40 -0
- package/src/components/MessageList.js +23 -0
- package/src/components/Spinner.js +13 -0
- package/src/components/StatusBar.js +24 -0
- package/src/components/ThoughtBox.js +23 -0
- package/src/components/TodoTree.js +31 -0
- package/src/components/ToolCall.js +69 -0
- package/src/components/UserMessage.js +9 -0
- package/src/config.js +24 -39
- package/src/index.js +43 -0
- package/src/providers/index.js +11 -36
- package/src/store.js +68 -0
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/
|
|
2
|
+
import '../src/index.js';
|
package/package.json
CHANGED
|
@@ -1,39 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clarity-ai",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Autonomous AI Agent
|
|
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
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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
|
-
"
|
|
31
|
-
"ai",
|
|
32
|
-
"cli",
|
|
33
|
-
"termux",
|
|
34
|
-
"agent",
|
|
35
|
-
"autonomous",
|
|
36
|
-
"clarity"
|
|
37
|
-
],
|
|
38
|
-
"license": "MIT"
|
|
24
|
+
"engines": { "node": ">=18.0.0" }
|
|
39
25
|
}
|
package/src/agent/intent.js
CHANGED
|
@@ -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
|
|
9
|
-
|
|
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
|
+
}
|
package/src/agent/tools.js
CHANGED
|
@@ -1,254 +1,132 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
1
2
|
import { execSync } from 'child_process';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const run = promisify(exec);
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 {
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
}
|