clarity-ai 1.0.0
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/README.md +108 -0
- package/bin/clarity.js +2 -0
- package/package.json +51 -0
- package/scripts/postinstall.js +53 -0
- package/src/agents/BaseAgent.js +54 -0
- package/src/agents/CodeAgent.js +57 -0
- package/src/agents/FileAgent.js +39 -0
- package/src/agents/MonitorAgent.js +54 -0
- package/src/agents/ShellAgent.js +31 -0
- package/src/agents/WebAgent.js +39 -0
- package/src/agents/manager.js +116 -0
- package/src/commands/index.js +725 -0
- package/src/config/keys.js +104 -0
- package/src/config/paths.js +22 -0
- package/src/config/settings.js +28 -0
- package/src/index.js +86 -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 +52 -0
- package/src/providers/index.js +39 -0
- package/src/providers/openai.js +52 -0
- package/src/providers/openrouter.js +52 -0
- package/src/tools/bash.js +25 -0
- package/src/tools/code.js +52 -0
- package/src/tools/files.js +62 -0
- package/src/tools/git.js +67 -0
- package/src/tools/index.js +40 -0
- package/src/tools/pkg.js +46 -0
- package/src/tools/search.js +29 -0
- package/src/tools/system.js +29 -0
- package/src/tools/web.js +24 -0
- package/src/ui/banner.js +15 -0
- package/src/ui/blocks.js +55 -0
- package/src/ui/chatbox.js +126 -0
- package/src/ui/colors.js +22 -0
- package/src/ui/prompt.js +49 -0
- package/src/ui/spinner.js +43 -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 +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# CLARITY — AI Agent CLI
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
██████╗██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗
|
|
5
|
+
██╔════╝██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝
|
|
6
|
+
██║ ██║ ███████║██████╔╝██║ ██║ ╚████╔╝
|
|
7
|
+
██║ ██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝
|
|
8
|
+
╚██████╗███████╗██║ ██║██║ ██║██║ ██║ ██║
|
|
9
|
+
╚═════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
AI agent CLI for Termux and terminal — chat, code, automate.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g clarity-ai
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Termux Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pkg update && pkg install nodejs-lts
|
|
24
|
+
npm install -g clarity-ai
|
|
25
|
+
clarity init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Interactive setup
|
|
32
|
+
clarity init
|
|
33
|
+
|
|
34
|
+
# Start chatting
|
|
35
|
+
clarity
|
|
36
|
+
|
|
37
|
+
# One-shot question
|
|
38
|
+
clarity /ask "What is the weather?"
|
|
39
|
+
|
|
40
|
+
# Run commands
|
|
41
|
+
clarity /bash ls -la
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
### Chat & AI
|
|
47
|
+
| Command | Description |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `/chat` | Start interactive chat session |
|
|
50
|
+
| `/ask <q>` | One-shot question |
|
|
51
|
+
| `/clear` | Clear screen and conversation |
|
|
52
|
+
| `/history show [n]` | Show last N messages |
|
|
53
|
+
| `/memory show\|add\|clear` | AI memory management |
|
|
54
|
+
|
|
55
|
+
### Configuration
|
|
56
|
+
| Command | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `/init` | Interactive setup wizard |
|
|
59
|
+
| `/keys set\|list\|remove\|test` | API key management |
|
|
60
|
+
| `/model set\|list` | Switch AI model |
|
|
61
|
+
| `/config show\|reset` | Settings management |
|
|
62
|
+
| `/theme set\|list` | Color themes |
|
|
63
|
+
|
|
64
|
+
### Files & Code
|
|
65
|
+
| Command | Description |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `/file create\|read\|delete\|list\|edit` | File operations |
|
|
68
|
+
| `/bash <command>` | Execute shell commands |
|
|
69
|
+
| `/code run\|write\|explain\|fix` | Code operations |
|
|
70
|
+
| `/git <action>` | Git operations |
|
|
71
|
+
|
|
72
|
+
### Web & Search
|
|
73
|
+
| Command | Description |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `/web <url>` | Fetch and display URL |
|
|
76
|
+
| `/search <query>` | Web search |
|
|
77
|
+
| `/translate <text>` | Translate text |
|
|
78
|
+
| `/summarize <file\|url>` | Summarize content |
|
|
79
|
+
|
|
80
|
+
### Agents & Tools
|
|
81
|
+
| Command | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `/agent start\|stop\|list\|logs` | Background agents |
|
|
84
|
+
| `/tools list\|run` | Tool management |
|
|
85
|
+
|
|
86
|
+
### System
|
|
87
|
+
| Command | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `/status` | System status |
|
|
90
|
+
| `/version` | Version info |
|
|
91
|
+
| `/env show\|set` | Environment variables |
|
|
92
|
+
| `/help [command]` | Command help |
|
|
93
|
+
| `/exit` | Exit CLARITY |
|
|
94
|
+
|
|
95
|
+
## Provider Comparison
|
|
96
|
+
|
|
97
|
+
| Provider | Free Tier | Streaming | Priority |
|
|
98
|
+
|---|---|---|---|
|
|
99
|
+
| Groq | ✓ | ✓ | 1 (fastest) |
|
|
100
|
+
| Google Gemini | ✓ | ✓ | 2 |
|
|
101
|
+
| DeepSeek | Cheap | ✓ | 3 |
|
|
102
|
+
| OpenRouter | ✓ | ✓ | 4 |
|
|
103
|
+
| OpenAI | Paid | ✓ | 5 |
|
|
104
|
+
| Anthropic | Paid | ✓ | 6 |
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/bin/clarity.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clarity-ai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI agent CLI for Termux and terminal — chat, code, automate",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"clarity": "bin/clarity.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/clarity.js",
|
|
12
|
+
"postinstall": "node scripts/postinstall.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"chalk": "^5.3.0",
|
|
19
|
+
"inquirer": "^9.2.12",
|
|
20
|
+
"node-fetch": "^3.3.2",
|
|
21
|
+
"conf": "^12.0.0",
|
|
22
|
+
"marked": "^12.0.0",
|
|
23
|
+
"marked-terminal": "^7.1.0",
|
|
24
|
+
"ora": "^8.0.1",
|
|
25
|
+
"boxen": "^7.1.1",
|
|
26
|
+
"figlet": "^1.7.0",
|
|
27
|
+
"gradient-string": "^2.0.2",
|
|
28
|
+
"cli-table3": "^0.6.3",
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"dotenv": "^16.4.5",
|
|
31
|
+
"axios": "^1.6.8",
|
|
32
|
+
"fs-extra": "^11.2.0",
|
|
33
|
+
"glob": "^10.3.12",
|
|
34
|
+
"open": "^10.1.0",
|
|
35
|
+
"update-notifier": "^7.0.0",
|
|
36
|
+
"semver": "^7.6.0",
|
|
37
|
+
"strip-ansi": "^7.1.0",
|
|
38
|
+
"wrap-ansi": "^9.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"ai",
|
|
42
|
+
"cli",
|
|
43
|
+
"termux",
|
|
44
|
+
"agent",
|
|
45
|
+
"clarity",
|
|
46
|
+
"chatgpt",
|
|
47
|
+
"gemini",
|
|
48
|
+
"groq"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT"
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import figlet from 'figlet';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import boxen from 'boxen';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
const GRADIENT = gradient(['#00d2ff', '#7b2ff7']);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const text = figlet.textSync('CLARITY', { font: 'ANSI Shadow' });
|
|
10
|
+
console.log(GRADIENT(text));
|
|
11
|
+
} catch {
|
|
12
|
+
console.log(GRADIENT('CLARITY'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const nodeVersion = process.versions.node;
|
|
16
|
+
const [major] = nodeVersion.split('.').map(Number);
|
|
17
|
+
|
|
18
|
+
if (major < 18) {
|
|
19
|
+
console.log(boxen(
|
|
20
|
+
chalk.hex('#ff4466').bold(`Node.js 18+ required. You have v${nodeVersion}`) + '\n' +
|
|
21
|
+
chalk.hex('#ffcc00')('Termux: pkg install nodejs-lts'),
|
|
22
|
+
{ padding: 1, borderColor: 'red', borderStyle: 'round' }
|
|
23
|
+
));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const isTermux = process.env.PREFIX?.includes('com.termux');
|
|
28
|
+
|
|
29
|
+
console.log(boxen(
|
|
30
|
+
chalk.hex('#00ff88').bold('CLARITY installed successfully!') + '\n' +
|
|
31
|
+
chalk.hex('#00d2ff')('Run: clarity init'),
|
|
32
|
+
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
|
33
|
+
));
|
|
34
|
+
|
|
35
|
+
console.log(chalk.hex('#666888')("Run 'clarity init' to configure your AI keys and get started."));
|
|
36
|
+
|
|
37
|
+
const binDir = process.env.npm_config_prefix ? `${process.env.npm_config_prefix}/bin` : null;
|
|
38
|
+
if (isTermux && binDir) {
|
|
39
|
+
const { existsSync, writeFileSync, chmodSync, unlinkSync } = await import('fs');
|
|
40
|
+
const { resolve } = await import('path');
|
|
41
|
+
const wrapperPath = resolve(binDir, 'clarity');
|
|
42
|
+
const targetPath = resolve(process.argv[1] || '.', '..', 'bin', 'clarity.js');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const stat = await import('fs').then(fs => fs.lstatSync(wrapperPath));
|
|
46
|
+
if (stat.isSymbolicLink()) {
|
|
47
|
+
unlinkSync(wrapperPath);
|
|
48
|
+
writeFileSync(wrapperPath, `#!/data/data/com.termux/files/usr/bin/bash\nexec node "${resolve(process.env.PREFIX || '/usr', 'lib/node_modules/clarity-ai/bin/clarity.js')}" "$@"\n`);
|
|
49
|
+
chmodSync(wrapperPath, '755');
|
|
50
|
+
console.log(chalk.hex('#00ff88')('✓ Termux wrapper created at:'), chalk.hex('#00d2ff')(wrapperPath));
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import BaseAgent from './BaseAgent.js';
|
|
2
|
+
import { watch, readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
class CodeAgent extends BaseAgent {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super({ ...config, type: 'code', name: config.name || 'Code Watcher' });
|
|
8
|
+
this.file = config.file;
|
|
9
|
+
this.language = config.language || 'auto';
|
|
10
|
+
this.autoFix = config.autoFix || false;
|
|
11
|
+
this._watcher = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.file) {
|
|
16
|
+
this.log('error', 'No file specified');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await super.start();
|
|
20
|
+
const filePath = resolve(this.file);
|
|
21
|
+
this.log('info', `Watching ${filePath} for errors`);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(filePath, 'utf8');
|
|
25
|
+
this.analyze(content);
|
|
26
|
+
} catch {}
|
|
27
|
+
|
|
28
|
+
this._watcher = watch(filePath, (eventType) => {
|
|
29
|
+
if (eventType === 'change') {
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(filePath, 'utf8');
|
|
32
|
+
this.analyze(content);
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
analyze(content) {
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
const errors = [];
|
|
41
|
+
lines.forEach((line, i) => {
|
|
42
|
+
if (line.length > 200) errors.push({ line: i + 1, msg: 'Line too long' });
|
|
43
|
+
if (line.includes('\t')) errors.push({ line: i + 1, msg: 'Tab character detected' });
|
|
44
|
+
});
|
|
45
|
+
if (errors.length > 0) {
|
|
46
|
+
this.log('warn', `Found ${errors.length} issues in ${this.file}`);
|
|
47
|
+
errors.forEach(e => this.log('warn', ` L${e.line}: ${e.msg}`));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async stop() {
|
|
52
|
+
if (this._watcher) { this._watcher.close(); this._watcher = null; }
|
|
53
|
+
await super.stop();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default CodeAgent;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import BaseAgent from './BaseAgent.js';
|
|
2
|
+
import { watch } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
|
|
5
|
+
class FileAgent extends BaseAgent {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
super({ ...config, type: 'file', name: config.name || 'File Watcher' });
|
|
8
|
+
this.watchDir = config.watchDir || '.';
|
|
9
|
+
this.pattern = config.pattern || '*';
|
|
10
|
+
this.onChangePrompt = config.onChangePrompt || 'File changed: {path}';
|
|
11
|
+
this._watcher = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async start() {
|
|
15
|
+
await super.start();
|
|
16
|
+
try {
|
|
17
|
+
const dir = resolve(this.watchDir);
|
|
18
|
+
this._watcher = watch(dir, { recursive: true }, (eventType, filename) => {
|
|
19
|
+
if (filename && filename.match(this.pattern.replace('*', '.*'))) {
|
|
20
|
+
this.log('info', `File ${eventType}: ${filename}`);
|
|
21
|
+
this.emit('fileChange', { eventType, filename });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
this.log('info', `Watching ${dir} for changes`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
this.log('error', `Watch error: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async stop() {
|
|
31
|
+
if (this._watcher) {
|
|
32
|
+
this._watcher.close();
|
|
33
|
+
this._watcher = null;
|
|
34
|
+
}
|
|
35
|
+
await super.stop();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default FileAgent;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import BaseAgent from './BaseAgent.js';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
class MonitorAgent extends BaseAgent {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super({ ...config, type: 'monitor', name: config.name || 'System Monitor' });
|
|
7
|
+
this.intervalMs = config.intervalMs || 5000;
|
|
8
|
+
this.alertThresholds = config.alertThresholds || { cpu: 90, memory: 90 };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async start() {
|
|
12
|
+
await super.start();
|
|
13
|
+
this.log('info', `Monitoring system every ${this.intervalMs}ms`);
|
|
14
|
+
this._interval = setInterval(() => this.check(), this.intervalMs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
check() {
|
|
18
|
+
const mem = process.memoryUsage();
|
|
19
|
+
const memPct = Math.round((mem.heapUsed / mem.heapTotal) * 100);
|
|
20
|
+
const load = os.loadavg()[0];
|
|
21
|
+
const totalMem = os.totalmem();
|
|
22
|
+
const freeMem = os.freemem();
|
|
23
|
+
const memUsage = Math.round(((totalMem - freeMem) / totalMem) * 100);
|
|
24
|
+
|
|
25
|
+
const stats = {
|
|
26
|
+
cpu: load.toFixed(2),
|
|
27
|
+
memory: memPct,
|
|
28
|
+
systemMem: memUsage,
|
|
29
|
+
heap: Math.round(mem.heapUsed / 1024 / 1024) + 'MB',
|
|
30
|
+
uptime: Math.round(process.uptime()) + 's',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (memPct > this.alertThresholds.memory) {
|
|
34
|
+
this.log('warn', `High memory usage: ${memPct}%`);
|
|
35
|
+
}
|
|
36
|
+
if (load > this.alertThresholds.cpu / 100) {
|
|
37
|
+
this.log('warn', `High CPU load: ${load}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.emit('stats', stats);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getStats() {
|
|
44
|
+
const mem = process.memoryUsage();
|
|
45
|
+
return {
|
|
46
|
+
cpu: os.loadavg()[0].toFixed(2),
|
|
47
|
+
memory: Math.round((mem.heapUsed / mem.heapTotal) * 100),
|
|
48
|
+
uptime: Math.round(process.uptime()),
|
|
49
|
+
os: `${os.platform()} ${os.arch()}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default MonitorAgent;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import BaseAgent from './BaseAgent.js';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
|
|
7
|
+
class ShellAgent extends BaseAgent {
|
|
8
|
+
constructor(config) {
|
|
9
|
+
super({ ...config, type: 'shell', name: config.name || 'Shell Runner' });
|
|
10
|
+
this.command = config.command || 'echo hello';
|
|
11
|
+
this.intervalMs = config.intervalMs || 60000;
|
|
12
|
+
this.storeOutput = config.storeOutput !== false;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async start() {
|
|
16
|
+
await super.start();
|
|
17
|
+
this.log('info', `Running command every ${this.intervalMs}ms: ${this.command}`);
|
|
18
|
+
this._interval = setInterval(async () => {
|
|
19
|
+
try {
|
|
20
|
+
const { stdout, stderr } = await execAsync(this.command);
|
|
21
|
+
if (this.storeOutput) this.log('info', `Output: ${stdout.trim().slice(0, 200)}`);
|
|
22
|
+
if (stderr) this.log('warn', `Stderr: ${stderr.trim().slice(0, 200)}`);
|
|
23
|
+
this.emit('commandComplete', { command: this.command, stdout, stderr });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
this.log('error', `Command failed: ${err.message}`);
|
|
26
|
+
}
|
|
27
|
+
}, this.intervalMs);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default ShellAgent;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import BaseAgent from './BaseAgent.js';
|
|
2
|
+
|
|
3
|
+
class WebAgent extends BaseAgent {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super({ ...config, type: 'web', name: config.name || 'Web Fetcher' });
|
|
6
|
+
this.url = config.url;
|
|
7
|
+
this.intervalMs = config.intervalMs || 300000;
|
|
8
|
+
this.lastContent = '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async start() {
|
|
12
|
+
if (!this.url) {
|
|
13
|
+
this.log('error', 'No URL configured');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await super.start();
|
|
17
|
+
this.log('info', `Fetching ${this.url} every ${this.intervalMs}ms`);
|
|
18
|
+
await this.fetch();
|
|
19
|
+
this._interval = setInterval(() => this.fetch(), this.intervalMs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async fetch() {
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(this.url);
|
|
25
|
+
const text = await res.text();
|
|
26
|
+
const clean = text.replace(/<[^>]+>/g, '').trim().slice(0, 500);
|
|
27
|
+
if (this.lastContent && clean !== this.lastContent) {
|
|
28
|
+
this.log('info', 'Content changed!');
|
|
29
|
+
this.emit('contentChange', { url: this.url, preview: clean.slice(0, 100) });
|
|
30
|
+
}
|
|
31
|
+
this.lastContent = clean;
|
|
32
|
+
this.log('info', `Fetched ${this.url} (${text.length} bytes)`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
this.log('error', `Fetch failed: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default WebAgent;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import BaseAgent from './BaseAgent.js';
|
|
2
|
+
import FileAgent from './FileAgent.js';
|
|
3
|
+
import ShellAgent from './ShellAgent.js';
|
|
4
|
+
import WebAgent from './WebAgent.js';
|
|
5
|
+
import CodeAgent from './CodeAgent.js';
|
|
6
|
+
import MonitorAgent from './MonitorAgent.js';
|
|
7
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { resolve, dirname } from 'path';
|
|
9
|
+
import paths from '../config/paths.js';
|
|
10
|
+
import blocks from '../ui/blocks.js';
|
|
11
|
+
|
|
12
|
+
const AGENT_MAP = {
|
|
13
|
+
file: FileAgent,
|
|
14
|
+
shell: ShellAgent,
|
|
15
|
+
web: WebAgent,
|
|
16
|
+
code: CodeAgent,
|
|
17
|
+
monitor: MonitorAgent,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class AgentManager {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.agents = new Map();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async start(agentType, config = {}) {
|
|
26
|
+
const AgentClass = AGENT_MAP[agentType];
|
|
27
|
+
if (!AgentClass) throw new Error(`Unknown agent type: ${agentType}`);
|
|
28
|
+
|
|
29
|
+
const agent = new AgentClass(config);
|
|
30
|
+
this.agents.set(agent.id, agent);
|
|
31
|
+
await agent.start();
|
|
32
|
+
this.save();
|
|
33
|
+
return agent.id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async stop(agentId) {
|
|
37
|
+
const agent = this.agents.get(agentId);
|
|
38
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
39
|
+
await agent.stop();
|
|
40
|
+
this.agents.delete(agentId);
|
|
41
|
+
this.save();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
list() {
|
|
45
|
+
return Array.from(this.agents.values()).map(a => a.getStatus());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getLogs(agentId, lines = 50) {
|
|
49
|
+
const agent = this.agents.get(agentId);
|
|
50
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
51
|
+
return agent.logs.slice(-lines);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async restart(agentId) {
|
|
55
|
+
const agent = this.agents.get(agentId);
|
|
56
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
57
|
+
await agent.stop();
|
|
58
|
+
await agent.start();
|
|
59
|
+
this.save();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
save() {
|
|
63
|
+
try {
|
|
64
|
+
const dir = dirname(paths.agents);
|
|
65
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
66
|
+
const data = Array.from(this.agents.values()).map(a => ({
|
|
67
|
+
type: a.type, config: a.config, id: a.id, name: a.name, status: a.status
|
|
68
|
+
}));
|
|
69
|
+
writeFileSync(paths.agents, JSON.stringify(data, null, 2));
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async restore() {
|
|
74
|
+
try {
|
|
75
|
+
if (!existsSync(paths.agents)) return;
|
|
76
|
+
const data = JSON.parse(readFileSync(paths.agents, 'utf8'));
|
|
77
|
+
for (const item of data) {
|
|
78
|
+
const AgentClass = AGENT_MAP[item.type];
|
|
79
|
+
if (AgentClass && item.status === 'running') {
|
|
80
|
+
const agent = new AgentClass(item.config);
|
|
81
|
+
this.agents.set(agent.id, agent);
|
|
82
|
+
await agent.start();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
showStatus() {
|
|
89
|
+
const list = this.list();
|
|
90
|
+
if (list.length === 0) {
|
|
91
|
+
blocks.info('Agents', 'No agents running.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const headers = ['ID', 'Name', 'Type', 'Status', 'Logs'];
|
|
95
|
+
const rows = list.map(a => [
|
|
96
|
+
a.id.slice(0, 12),
|
|
97
|
+
a.name,
|
|
98
|
+
a.type,
|
|
99
|
+
statusBadge(a.status),
|
|
100
|
+
String(a.logCount),
|
|
101
|
+
]);
|
|
102
|
+
blocks.table(headers, rows);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function statusBadge(status) {
|
|
107
|
+
switch (status) {
|
|
108
|
+
case 'running': return '● running';
|
|
109
|
+
case 'error': return '✗ error';
|
|
110
|
+
case 'idle': return '○ idle';
|
|
111
|
+
default: return '– stopped';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const agentManager = new AgentManager();
|
|
116
|
+
export default agentManager;
|