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.
Files changed (63) hide show
  1. package/AGENTS.md +51 -0
  2. package/bin/clarity.js +1 -1
  3. package/package.json +14 -28
  4. package/src/agent/intent.js +9 -0
  5. package/src/agent/loop.js +105 -0
  6. package/src/agent/planner.js +37 -0
  7. package/src/agent/runner.js +143 -0
  8. package/src/agent/subagent.js +55 -0
  9. package/src/agent/tools.js +132 -0
  10. package/src/agents/BaseAgent.js +54 -0
  11. package/src/agents/CodeAgent.js +224 -0
  12. package/src/agents/FileAgent.js +85 -0
  13. package/src/agents/MonitorAgent.js +104 -0
  14. package/src/agents/ShellAgent.js +91 -0
  15. package/src/agents/WebAgent.js +89 -0
  16. package/src/agents/manager.js +127 -0
  17. package/src/app.js +88 -0
  18. package/src/banner.js +14 -0
  19. package/src/blocks.js +100 -0
  20. package/src/colors.js +31 -0
  21. package/src/commands/chat.js +104 -0
  22. package/src/commands/config.js +146 -0
  23. package/src/commands/files.js +106 -0
  24. package/src/commands/index.js +144 -0
  25. package/src/commands/model.js +93 -0
  26. package/src/commands/system.js +131 -0
  27. package/src/commands/utility.js +332 -0
  28. package/src/commands/web.js +82 -0
  29. package/src/components/AIMessage.js +21 -0
  30. package/src/components/AgentTree.js +42 -0
  31. package/src/components/Banner.js +15 -0
  32. package/src/components/InputBar.js +40 -0
  33. package/src/components/MessageList.js +23 -0
  34. package/src/components/Spinner.js +13 -0
  35. package/src/components/StatusBar.js +24 -0
  36. package/src/components/ThoughtBox.js +23 -0
  37. package/src/components/TodoTree.js +31 -0
  38. package/src/components/ToolCall.js +69 -0
  39. package/src/components/UserMessage.js +9 -0
  40. package/src/config/keys.js +104 -0
  41. package/src/config/paths.js +22 -0
  42. package/src/config/settings.js +28 -0
  43. package/src/config.js +29 -0
  44. package/src/history.js +54 -0
  45. package/src/index.js +43 -0
  46. package/src/input.js +36 -0
  47. package/src/main.js +121 -0
  48. package/src/memory/context.js +38 -0
  49. package/src/memory/store.js +54 -0
  50. package/src/providers/claude.js +61 -0
  51. package/src/providers/deepseek.js +53 -0
  52. package/src/providers/gemini.js +48 -0
  53. package/src/providers/groq.js +56 -0
  54. package/src/providers/index.js +14 -0
  55. package/src/providers/openai.js +52 -0
  56. package/src/providers/openrouter.js +52 -0
  57. package/src/setup.js +137 -0
  58. package/src/store.js +68 -0
  59. package/src/ui/spinner.js +49 -0
  60. package/src/utils/logger.js +40 -0
  61. package/src/utils/markdown.js +25 -0
  62. package/src/utils/termux.js +38 -0
  63. 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/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.0",
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
  }
@@ -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;