clarity-ai 2.0.0 → 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 +51 -0
- package/bin/clarity.js +1 -1
- package/package.json +14 -28
- package/src/agent/intent.js +9 -0
- package/src/agent/loop.js +105 -0
- package/src/agent/planner.js +37 -0
- package/src/agent/runner.js +143 -0
- package/src/agent/subagent.js +55 -0
- package/src/agent/tools.js +132 -0
- package/src/agents/BaseAgent.js +54 -0
- package/src/agents/CodeAgent.js +224 -0
- package/src/agents/FileAgent.js +85 -0
- package/src/agents/MonitorAgent.js +104 -0
- package/src/agents/ShellAgent.js +91 -0
- package/src/agents/WebAgent.js +89 -0
- package/src/agents/manager.js +127 -0
- package/src/app.js +88 -0
- package/src/banner.js +14 -0
- package/src/blocks.js +100 -0
- package/src/colors.js +31 -0
- package/src/commands/chat.js +104 -0
- package/src/commands/config.js +146 -0
- package/src/commands/files.js +106 -0
- package/src/commands/index.js +144 -0
- package/src/commands/model.js +93 -0
- package/src/commands/system.js +131 -0
- package/src/commands/utility.js +332 -0
- package/src/commands/web.js +82 -0
- 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/keys.js +104 -0
- package/src/config/paths.js +22 -0
- package/src/config/settings.js +28 -0
- package/src/config.js +29 -0
- package/src/history.js +54 -0
- package/src/index.js +43 -0
- package/src/input.js +36 -0
- package/src/main.js +121 -0
- package/src/memory/context.js +38 -0
- package/src/memory/store.js +54 -0
- package/src/providers/claude.js +61 -0
- package/src/providers/deepseek.js +53 -0
- package/src/providers/gemini.js +48 -0
- package/src/providers/groq.js +56 -0
- package/src/providers/index.js +14 -0
- package/src/providers/openai.js +52 -0
- package/src/providers/openrouter.js +52 -0
- package/src/setup.js +137 -0
- package/src/store.js +68 -0
- package/src/ui/spinner.js +49 -0
- package/src/utils/logger.js +40 -0
- package/src/utils/markdown.js +25 -0
- package/src/utils/termux.js +38 -0
- package/src/utils/version-check.js +76 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# CLARITY-AI — Agent Guide
|
|
2
|
+
|
|
3
|
+
## Project
|
|
4
|
+
Single-package Node.js CLI tool (`clarity-ai`) — ESM module (`"type": "module"`).
|
|
5
|
+
Entry: `bin/clarity.js` → imports `src/index.js`.
|
|
6
|
+
Install: `npm install -g clarity-ai` → command `clarity`.
|
|
7
|
+
|
|
8
|
+
## Key Architecture
|
|
9
|
+
- **`src/providers/index.js`**: `sendMessage()` MUST be a plain `function`, NOT `async function`. Wrapping an async generator in `async` returns a Promise instead of an async iterable → `for await...of` breaks ("stream is not async iterable"). Each provider exports `async function* sendMessage(...)`. Provider model lists updated as of June 2026 — see `capabilities` object.
|
|
10
|
+
- **`src/commands/index.js`**: All 40+ slash commands in one file, routed by switch. Single-file design — do NOT split into separate command files without changing the dispatcher. `/model` uses interactive inquirer selector. `/provider` switches provider + auto-filters models.
|
|
11
|
+
- **`src/config/keys.js`**: API keys stored via `conf` package, obfuscated with base64-reverse (not real encryption). Provider regex patterns are strict — Gemini keys must match `/^AIzaSy[A-Za-z0-9_-]{20,}$/`.
|
|
12
|
+
- **`src/ui/chatbox.js`**: Prompt bar design — grey fill (`\x1b[48;5;236m`) + purple outline (`chalk.hex('#7b2ff7')`). `renderPromptBar()` draws the bar, `showPrompt()` writes ` ◆ `. Pressing `/` at the prompt shows all command suggestions via `suggestCommands()` with `rl._refreshLine()`.
|
|
13
|
+
- **`src/ui/prompt.js`**: History management only. `showPrompt()` and `showSuggestions()` moved to `chatbox.js`. `createPrompt()` creates readline interface with `terminal: true` (enables keypress events).
|
|
14
|
+
- **Default model**: `groq/llama-3.3-70b-versatile` (updated in `src/config/settings.js`).
|
|
15
|
+
|
|
16
|
+
## Termux Quirks
|
|
17
|
+
- No `node-gyp`, no native modules, no `fsevents`.
|
|
18
|
+
- `npm install -g` symlinks don't work for execution on Termux (permission denied across mount points). Postinstall creates a shell wrapper at `$PREFIX/bin/clarity`.
|
|
19
|
+
- `open` package replaced with `termux-open-url` via `exec`.
|
|
20
|
+
- Console clear: `console.clear()` at startup.
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
```bash
|
|
24
|
+
# Dev workflow (no test/lint scripts configured)
|
|
25
|
+
npm start # node bin/clarity.js
|
|
26
|
+
node bin/clarity.js /status # run individual command
|
|
27
|
+
node bin/clarity.js # interactive chat mode
|
|
28
|
+
|
|
29
|
+
# Publishing
|
|
30
|
+
npm version patch && npm publish # bump + publish (needs npm token with write access)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Provider Streaming Contract
|
|
34
|
+
Each provider file exports `{ PROVIDER, sendMessage }` where `sendMessage` is:
|
|
35
|
+
```js
|
|
36
|
+
async function* sendMessage(apiKey, messages, model, stream) → AsyncGenerator<string>
|
|
37
|
+
```
|
|
38
|
+
SSE parsing pattern: read `data: ` lines, parse JSON, yield `choices[0].delta.content`.
|
|
39
|
+
|
|
40
|
+
## Known Gotchas
|
|
41
|
+
- **Provider router must NOT be async**: `src/providers/index.js` `sendMessage` is a regular function returning the async generator directly.
|
|
42
|
+
- **Model name format**: config stores `"provider/modelname"` (e.g. `groq/llama-3.3-70b-versatile`). Router strips prefix before passing to provider.
|
|
43
|
+
- **No boxen/ora deps**: v1.2.0 removed them. Custom ANSI escape shaded boxes in `src/ui/blocks.js`. Custom spinner in `src/ui/spinner.js`.
|
|
44
|
+
- **Config paths**: XDG-compliant (`~/.config/clarity/`, `~/.local/share/clarity/`, `~/.cache/clarity/`).
|
|
45
|
+
- **No test framework**: none configured. No lint/typecheck scripts.
|
|
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
|
+
- **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
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function detectIntent(message) {
|
|
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'];
|
|
5
|
+
let score = 0;
|
|
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; }
|
|
8
|
+
return score >= 2 ? 'agent' : 'chat';
|
|
9
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { toolBox, success, errorBox } from '../blocks.js';
|
|
2
|
+
import { sendMessage } from '../providers/index.js';
|
|
3
|
+
import { executeTool } from './tools.js';
|
|
4
|
+
|
|
5
|
+
const AGENT_PROMPT = `You are CLARITY, an autonomous AI agent. You have tools available. Respond in JSON:
|
|
6
|
+
{
|
|
7
|
+
"thought": "your reasoning",
|
|
8
|
+
"tool": "tool_name",
|
|
9
|
+
"args": { "key": "value" },
|
|
10
|
+
"done": false
|
|
11
|
+
}
|
|
12
|
+
When task is complete, set done: true and provide a summary.`;
|
|
13
|
+
|
|
14
|
+
function extractJSON(text) {
|
|
15
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(match[0]);
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function agentLoop(conversation, model, apiKey) {
|
|
25
|
+
const messages = [
|
|
26
|
+
{ role: 'system', content: AGENT_PROMPT },
|
|
27
|
+
...conversation
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
let step = 0;
|
|
31
|
+
|
|
32
|
+
while (true) {
|
|
33
|
+
step++;
|
|
34
|
+
|
|
35
|
+
const gen = sendMessage(apiKey, messages, model, false);
|
|
36
|
+
let raw = '';
|
|
37
|
+
for await (const chunk of gen) {
|
|
38
|
+
raw += chunk;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let parsed = null;
|
|
42
|
+
let attempts = 0;
|
|
43
|
+
while (!parsed && attempts < 3) {
|
|
44
|
+
parsed = extractJSON(raw);
|
|
45
|
+
if (!parsed) {
|
|
46
|
+
attempts++;
|
|
47
|
+
if (attempts < 3) {
|
|
48
|
+
const retryGen = sendMessage(apiKey, [
|
|
49
|
+
{ role: 'system', content: AGENT_PROMPT },
|
|
50
|
+
...messages,
|
|
51
|
+
{ role: 'user', content: `Your previous response was not valid JSON. Please respond with valid JSON only:\n${raw}\n\nRespond ONLY with valid JSON.` }
|
|
52
|
+
], model, false);
|
|
53
|
+
raw = '';
|
|
54
|
+
for await (const chunk of retryGen) {
|
|
55
|
+
raw += chunk;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!parsed) {
|
|
62
|
+
errorBox('Agent Error', 'Failed to parse JSON response after 3 attempts');
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { thought, tool, args, done, summary } = parsed;
|
|
67
|
+
|
|
68
|
+
if (done) {
|
|
69
|
+
success('Task Complete', summary || 'Done');
|
|
70
|
+
return summary || 'Done';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!tool) {
|
|
74
|
+
errorBox('Agent Error', 'No tool specified in response');
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const start = Date.now();
|
|
79
|
+
let result;
|
|
80
|
+
let status;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
result = await executeTool(tool, args || {});
|
|
84
|
+
status = 'success';
|
|
85
|
+
} catch (err) {
|
|
86
|
+
result = err.message;
|
|
87
|
+
status = 'error';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const elapsed = Date.now() - start;
|
|
91
|
+
|
|
92
|
+
toolBox(tool, {
|
|
93
|
+
args: args || {},
|
|
94
|
+
status,
|
|
95
|
+
duration: elapsed + 'ms',
|
|
96
|
+
result: typeof result === 'string' ? result.slice(0, 500) : JSON.stringify(result).slice(0, 500)
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
messages.push({ role: 'assistant', content: JSON.stringify(parsed) });
|
|
100
|
+
messages.push({
|
|
101
|
+
role: 'user',
|
|
102
|
+
content: `Tool "${tool}" returned: ${typeof result === 'string' ? result.slice(0, 2000) : JSON.stringify(result).slice(0, 2000)}`
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { sendMessage } from '../providers/index.js';
|
|
2
|
+
|
|
3
|
+
const PLAN_PROMPT = `You are CLARITY's planning system. Given a user task, create a step-by-step plan using available tools.
|
|
4
|
+
|
|
5
|
+
Available tools: file_read, file_write, file_append, file_delete, file_list, file_exists, shell_run, shell_run_silent, npm_install, pkg_install, web_search, web_fetch, code_run, git_status, git_commit, sys_info, env_get, env_set, ai_ask, summarize
|
|
6
|
+
|
|
7
|
+
Respond in JSON:
|
|
8
|
+
{
|
|
9
|
+
"steps": [
|
|
10
|
+
{ "step": "description of what to do", "tool": "tool_name", "args": { "key": "value" } }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
Provide only the JSON, no other text.`;
|
|
15
|
+
|
|
16
|
+
export async function createPlan(task, model, apiKey) {
|
|
17
|
+
const messages = [
|
|
18
|
+
{ role: 'system', content: PLAN_PROMPT },
|
|
19
|
+
{ role: 'user', content: task }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const gen = sendMessage(apiKey, messages, model, false);
|
|
23
|
+
let raw = '';
|
|
24
|
+
for await (const chunk of gen) {
|
|
25
|
+
raw += chunk;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
29
|
+
if (!match) return [{ step: 'Failed to parse plan', tool: null, args: null }];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(match[0]);
|
|
33
|
+
return parsed.steps || [];
|
|
34
|
+
} catch {
|
|
35
|
+
return [{ step: 'Failed to parse plan', tool: null, args: null }];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
|
|
6
|
+
const run = promisify(exec);
|
|
7
|
+
|
|
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);
|
|
51
|
+
}
|
|
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
|
+
}
|
|
74
|
+
|
|
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
|
+
}
|
|
120
|
+
|
|
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);
|
|
132
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logger from '../utils/logger.js';
|
|
2
|
+
|
|
3
|
+
class BaseAgent {
|
|
4
|
+
constructor(config = {}) {
|
|
5
|
+
this.id = config.id || `agent_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
6
|
+
this.name = config.name || 'Agent';
|
|
7
|
+
this.type = config.type || 'base';
|
|
8
|
+
this.status = 'stopped';
|
|
9
|
+
this.logs = [];
|
|
10
|
+
this.config = config;
|
|
11
|
+
this._interval = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async start() {
|
|
15
|
+
this.status = 'running';
|
|
16
|
+
this.log('info', 'Agent started');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async stop() {
|
|
20
|
+
this.status = 'stopped';
|
|
21
|
+
if (this._interval) {
|
|
22
|
+
clearInterval(this._interval);
|
|
23
|
+
this._interval = null;
|
|
24
|
+
}
|
|
25
|
+
this.log('info', 'Agent stopped');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async pause() {
|
|
29
|
+
this.status = 'paused';
|
|
30
|
+
this.log('info', 'Agent paused');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async resume() {
|
|
34
|
+
this.status = 'running';
|
|
35
|
+
this.log('info', 'Agent resumed');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
log(level, msg) {
|
|
39
|
+
const entry = { level, msg, time: new Date().toISOString() };
|
|
40
|
+
this.logs.push(entry);
|
|
41
|
+
if (this.logs.length > 1000) this.logs.shift();
|
|
42
|
+
logger[level](`[${this.id}] ${msg}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
emit(event, data) {
|
|
46
|
+
this.log('info', `Event: ${event} ${data ? JSON.stringify(data) : ''}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getStatus() {
|
|
50
|
+
return { id: this.id, name: this.name, type: this.type, status: this.status, logCount: this.logs.length };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default BaseAgent;
|