notoken-core 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/config/file-hints.json +255 -0
- package/config/hosts.json +14 -0
- package/config/intents.json +3920 -0
- package/config/playbooks.json +112 -0
- package/config/rules.json +100 -0
- package/dist/agents/agentSpawner.d.ts +56 -0
- package/dist/agents/agentSpawner.js +180 -0
- package/dist/agents/planner.d.ts +40 -0
- package/dist/agents/planner.js +175 -0
- package/dist/agents/playbookRunner.d.ts +45 -0
- package/dist/agents/playbookRunner.js +120 -0
- package/dist/agents/taskRunner.d.ts +61 -0
- package/dist/agents/taskRunner.js +142 -0
- package/dist/context/history.d.ts +36 -0
- package/dist/context/history.js +115 -0
- package/dist/conversation/coreference.d.ts +27 -0
- package/dist/conversation/coreference.js +147 -0
- package/dist/conversation/secrets.d.ts +43 -0
- package/dist/conversation/secrets.js +129 -0
- package/dist/conversation/store.d.ts +94 -0
- package/dist/conversation/store.js +184 -0
- package/dist/execution/git.d.ts +11 -0
- package/dist/execution/git.js +146 -0
- package/dist/execution/ssh.d.ts +2 -0
- package/dist/execution/ssh.js +17 -0
- package/dist/handlers/executor.d.ts +8 -0
- package/dist/handlers/executor.js +216 -0
- package/dist/healing/claudeHealer.d.ts +17 -0
- package/dist/healing/claudeHealer.js +300 -0
- package/dist/healing/patchPromoter.d.ts +25 -0
- package/dist/healing/patchPromoter.js +118 -0
- package/dist/healing/ruleBuilder.d.ts +5 -0
- package/dist/healing/ruleBuilder.js +111 -0
- package/dist/healing/ruleRepairer.d.ts +8 -0
- package/dist/healing/ruleRepairer.js +29 -0
- package/dist/healing/ruleValidator.d.ts +22 -0
- package/dist/healing/ruleValidator.js +145 -0
- package/dist/healing/runHealer.d.ts +11 -0
- package/dist/healing/runHealer.js +74 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +62 -0
- package/dist/intents/catalog.d.ts +4 -0
- package/dist/intents/catalog.js +7 -0
- package/dist/nlp/disambiguate.d.ts +2 -0
- package/dist/nlp/disambiguate.js +46 -0
- package/dist/nlp/fuzzyResolver.d.ts +14 -0
- package/dist/nlp/fuzzyResolver.js +108 -0
- package/dist/nlp/llmFallback.d.ts +63 -0
- package/dist/nlp/llmFallback.js +338 -0
- package/dist/nlp/llmParser.d.ts +8 -0
- package/dist/nlp/llmParser.js +118 -0
- package/dist/nlp/multiClassifier.d.ts +39 -0
- package/dist/nlp/multiClassifier.js +181 -0
- package/dist/nlp/parseIntent.d.ts +2 -0
- package/dist/nlp/parseIntent.js +34 -0
- package/dist/nlp/ruleParser.d.ts +2 -0
- package/dist/nlp/ruleParser.js +234 -0
- package/dist/nlp/semantic.d.ts +104 -0
- package/dist/nlp/semantic.js +419 -0
- package/dist/nlp/uncertainty.d.ts +42 -0
- package/dist/nlp/uncertainty.js +103 -0
- package/dist/parsers/apacheParser.d.ts +50 -0
- package/dist/parsers/apacheParser.js +152 -0
- package/dist/parsers/bindParser.d.ts +40 -0
- package/dist/parsers/bindParser.js +189 -0
- package/dist/parsers/envFile.d.ts +39 -0
- package/dist/parsers/envFile.js +128 -0
- package/dist/parsers/fileFinder.d.ts +30 -0
- package/dist/parsers/fileFinder.js +226 -0
- package/dist/parsers/index.d.ts +27 -0
- package/dist/parsers/index.js +193 -0
- package/dist/parsers/jsonParser.d.ts +16 -0
- package/dist/parsers/jsonParser.js +57 -0
- package/dist/parsers/nginxParser.d.ts +47 -0
- package/dist/parsers/nginxParser.js +161 -0
- package/dist/parsers/passwd.d.ts +25 -0
- package/dist/parsers/passwd.js +41 -0
- package/dist/parsers/shadow.d.ts +23 -0
- package/dist/parsers/shadow.js +50 -0
- package/dist/parsers/yamlParser.d.ts +13 -0
- package/dist/parsers/yamlParser.js +54 -0
- package/dist/policy/confirm.d.ts +2 -0
- package/dist/policy/confirm.js +29 -0
- package/dist/policy/safety.d.ts +4 -0
- package/dist/policy/safety.js +32 -0
- package/dist/types/intent.d.ts +205 -0
- package/dist/types/intent.js +32 -0
- package/dist/types/rules.d.ts +237 -0
- package/dist/types/rules.js +50 -0
- package/dist/utils/analysis.d.ts +25 -0
- package/dist/utils/analysis.js +307 -0
- package/dist/utils/autoBackup.d.ts +43 -0
- package/dist/utils/autoBackup.js +144 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +32 -0
- package/dist/utils/dirAnalysis.d.ts +23 -0
- package/dist/utils/dirAnalysis.js +192 -0
- package/dist/utils/explain.d.ts +8 -0
- package/dist/utils/explain.js +145 -0
- package/dist/utils/logger.d.ts +5 -0
- package/dist/utils/logger.js +29 -0
- package/dist/utils/output.d.ts +2 -0
- package/dist/utils/output.js +26 -0
- package/dist/utils/paths.d.ts +26 -0
- package/dist/utils/paths.js +47 -0
- package/dist/utils/permissions.d.ts +64 -0
- package/dist/utils/permissions.js +298 -0
- package/dist/utils/platform.d.ts +53 -0
- package/dist/utils/platform.js +253 -0
- package/dist/utils/smartFile.d.ts +29 -0
- package/dist/utils/smartFile.js +188 -0
- package/dist/utils/spinner.d.ts +53 -0
- package/dist/utils/spinner.js +140 -0
- package/dist/utils/verbose.d.ts +27 -0
- package/dist/utils/verbose.js +131 -0
- package/dist/utils/wslPaths.d.ts +31 -0
- package/dist/utils/wslPaths.js +145 -0
- package/package.json +39 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_description": "Reusable multi-step playbooks. Run with: mycli> :play <name> [env]",
|
|
3
|
+
"playbooks": [
|
|
4
|
+
{
|
|
5
|
+
"name": "health-check",
|
|
6
|
+
"description": "Full server health check — disk, memory, load, processes, services",
|
|
7
|
+
"steps": [
|
|
8
|
+
{ "command": "uptime", "label": "Uptime & load" },
|
|
9
|
+
{ "command": "free -h", "label": "Memory usage" },
|
|
10
|
+
{ "command": "df -h", "label": "Disk usage" },
|
|
11
|
+
{ "command": "ps aux --sort=-%cpu | head -10", "label": "Top CPU processes" },
|
|
12
|
+
{ "command": "ps aux --sort=-%mem | head -10", "label": "Top memory processes" },
|
|
13
|
+
{ "command": "ss -tlnp | head -20", "label": "Listening ports" },
|
|
14
|
+
{ "command": "systemctl list-units --state=failed --no-pager 2>/dev/null || echo 'N/A'", "label": "Failed services" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "disk-analysis",
|
|
19
|
+
"description": "Deep disk usage analysis — find what's consuming space",
|
|
20
|
+
"steps": [
|
|
21
|
+
{ "command": "df -h", "label": "Filesystem overview" },
|
|
22
|
+
{ "command": "du -sh /var/* 2>/dev/null | sort -rh | head -10", "label": "Largest dirs in /var" },
|
|
23
|
+
{ "command": "du -sh /home/* 2>/dev/null | sort -rh | head -10", "label": "Largest dirs in /home" },
|
|
24
|
+
{ "command": "du -sh /tmp/* 2>/dev/null | sort -rh | head -10", "label": "Largest in /tmp" },
|
|
25
|
+
{ "command": "find /var/log -type f -size +100M 2>/dev/null", "label": "Large log files (>100MB)" },
|
|
26
|
+
{ "command": "find /tmp -type f -mtime +7 2>/dev/null | wc -l", "label": "Old temp files (>7 days)" },
|
|
27
|
+
{ "command": "journalctl --disk-usage 2>/dev/null || echo 'N/A'", "label": "Journal disk usage" }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "load-analysis",
|
|
32
|
+
"description": "Analyze what's causing high load — CPU, memory, I/O",
|
|
33
|
+
"steps": [
|
|
34
|
+
{ "command": "uptime", "label": "Load average" },
|
|
35
|
+
{ "command": "cat /proc/loadavg", "label": "Load detail" },
|
|
36
|
+
{ "command": "nproc", "label": "CPU cores" },
|
|
37
|
+
{ "command": "ps aux --sort=-%cpu | head -15", "label": "Top CPU consumers" },
|
|
38
|
+
{ "command": "ps aux --sort=-%mem | head -15", "label": "Top memory consumers" },
|
|
39
|
+
{ "command": "iostat -x 1 3 2>/dev/null || echo 'iostat not installed (sysstat)'", "label": "I/O stats" },
|
|
40
|
+
{ "command": "vmstat 1 5 2>/dev/null || echo 'vmstat not available'", "label": "VM stats" }
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "security-audit",
|
|
45
|
+
"description": "Basic security audit — open ports, auth, updates, suspicious activity",
|
|
46
|
+
"steps": [
|
|
47
|
+
{ "command": "ss -tlnp", "label": "Open listening ports" },
|
|
48
|
+
{ "command": "cat /etc/passwd | grep -v nologin | grep -v false | cut -d: -f1", "label": "Login-capable users" },
|
|
49
|
+
{ "command": "lastlog 2>/dev/null | grep -v 'Never' | head -20", "label": "Recent logins" },
|
|
50
|
+
{ "command": "last -n 10 2>/dev/null", "label": "Last 10 logins" },
|
|
51
|
+
{ "command": "grep 'Failed password' /var/log/auth.log 2>/dev/null | tail -10 || grep 'Failed password' /var/log/secure 2>/dev/null | tail -10 || echo 'No auth log found'", "label": "Failed login attempts" },
|
|
52
|
+
{ "command": "find / -perm -4000 -type f 2>/dev/null | head -20", "label": "SUID binaries" },
|
|
53
|
+
{ "command": "apt list --upgradable 2>/dev/null | head -20 || yum check-update 2>/dev/null | head -20 || echo 'N/A'", "label": "Pending updates" },
|
|
54
|
+
{ "command": "grep PermitRootLogin /etc/ssh/sshd_config 2>/dev/null || echo 'sshd_config not found'", "label": "SSH root login setting" }
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "virus-scan",
|
|
59
|
+
"description": "Run ClamAV virus scan (installs if missing)",
|
|
60
|
+
"steps": [
|
|
61
|
+
{ "command": "command -v clamscan >/dev/null || (echo 'Installing ClamAV...' && sudo apt-get install -y clamav 2>/dev/null || sudo yum install -y clamav 2>/dev/null || echo 'Install clamav manually')", "label": "Ensure ClamAV installed" },
|
|
62
|
+
{ "command": "sudo freshclam 2>/dev/null || echo 'Could not update virus definitions'", "label": "Update virus definitions" },
|
|
63
|
+
{ "command": "sudo clamscan -r --bell --max-filesize=100M --max-scansize=500M /home /tmp /var/www 2>/dev/null | tail -20 || echo 'Scan failed'", "label": "Scan /home, /tmp, /var/www" },
|
|
64
|
+
{ "command": "sudo clamscan -r --bell --infected /etc 2>/dev/null | tail -10 || echo 'N/A'", "label": "Scan /etc (infected only)" }
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "letsencrypt-setup",
|
|
69
|
+
"description": "Set up Let's Encrypt SSL certificate with certbot",
|
|
70
|
+
"steps": [
|
|
71
|
+
{ "command": "command -v certbot >/dev/null || (echo 'Installing certbot...' && sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx 2>/dev/null || sudo dnf install -y certbot python3-certbot-nginx 2>/dev/null || echo 'Install certbot manually')", "label": "Ensure certbot installed" },
|
|
72
|
+
{ "command": "certbot --version 2>/dev/null", "label": "Certbot version" },
|
|
73
|
+
{ "command": "ls /etc/letsencrypt/live/ 2>/dev/null || echo 'No existing certificates'", "label": "Existing certificates" },
|
|
74
|
+
{ "command": "echo 'Run: sudo certbot --nginx -d yourdomain.com'", "label": "Next step — run certbot" },
|
|
75
|
+
{ "command": "echo 'Run: sudo certbot renew --dry-run'", "label": "Test renewal" }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "nginx-check",
|
|
80
|
+
"description": "Nginx configuration check and status",
|
|
81
|
+
"steps": [
|
|
82
|
+
{ "command": "nginx -v 2>&1", "label": "Nginx version" },
|
|
83
|
+
{ "command": "sudo nginx -t 2>&1", "label": "Config syntax check" },
|
|
84
|
+
{ "command": "systemctl status nginx --no-pager 2>/dev/null || service nginx status", "label": "Service status" },
|
|
85
|
+
{ "command": "ls -la /etc/nginx/sites-enabled/ 2>/dev/null || ls -la /etc/nginx/conf.d/ 2>/dev/null", "label": "Enabled sites/configs" },
|
|
86
|
+
{ "command": "tail -20 /var/log/nginx/error.log 2>/dev/null || echo 'No error log'", "label": "Recent errors" }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "docker-check",
|
|
91
|
+
"description": "Docker status, containers, images, and disk usage",
|
|
92
|
+
"steps": [
|
|
93
|
+
{ "command": "docker --version 2>/dev/null || echo 'Docker not installed'", "label": "Docker version" },
|
|
94
|
+
{ "command": "docker ps --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' 2>/dev/null", "label": "Running containers" },
|
|
95
|
+
{ "command": "docker ps -a --filter 'status=exited' --format 'table {{.Names}}\\t{{.Status}}' 2>/dev/null | head -10", "label": "Stopped containers" },
|
|
96
|
+
{ "command": "docker images --format 'table {{.Repository}}\\t{{.Tag}}\\t{{.Size}}' 2>/dev/null | head -15", "label": "Images" },
|
|
97
|
+
{ "command": "docker system df 2>/dev/null", "label": "Disk usage" }
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "backup-full",
|
|
102
|
+
"description": "Full server backup — configs, data, databases",
|
|
103
|
+
"steps": [
|
|
104
|
+
{ "command": "BACKUP_DIR=/backup/$(hostname)-$(date +%Y%m%d) && mkdir -p $BACKUP_DIR && echo $BACKUP_DIR", "label": "Create backup directory" },
|
|
105
|
+
{ "command": "BACKUP_DIR=/backup/$(hostname)-$(date +%Y%m%d) && tar -czf $BACKUP_DIR/etc.tar.gz /etc/ 2>/dev/null && echo 'OK'", "label": "Backup /etc" },
|
|
106
|
+
{ "command": "BACKUP_DIR=/backup/$(hostname)-$(date +%Y%m%d) && tar -czf $BACKUP_DIR/var-www.tar.gz /var/www/ 2>/dev/null && echo 'OK' || echo 'No /var/www'", "label": "Backup /var/www" },
|
|
107
|
+
{ "command": "BACKUP_DIR=/backup/$(hostname)-$(date +%Y%m%d) && tar -czf $BACKUP_DIR/home.tar.gz /home/ 2>/dev/null && echo 'OK'", "label": "Backup /home" },
|
|
108
|
+
{ "command": "BACKUP_DIR=/backup/$(hostname)-$(date +%Y%m%d) && ls -lh $BACKUP_DIR/", "label": "Backup summary" }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0.1",
|
|
3
|
+
"intentSynonyms": {},
|
|
4
|
+
"environmentAliases": {
|
|
5
|
+
"local": [
|
|
6
|
+
"local",
|
|
7
|
+
"localhost",
|
|
8
|
+
"127.0.0.1"
|
|
9
|
+
],
|
|
10
|
+
"dev": [
|
|
11
|
+
"dev",
|
|
12
|
+
"development"
|
|
13
|
+
],
|
|
14
|
+
"staging": [
|
|
15
|
+
"staging",
|
|
16
|
+
"stage",
|
|
17
|
+
"stg",
|
|
18
|
+
"preprod",
|
|
19
|
+
"pre-prod"
|
|
20
|
+
],
|
|
21
|
+
"prod": [
|
|
22
|
+
"prod",
|
|
23
|
+
"production",
|
|
24
|
+
"live",
|
|
25
|
+
"prd"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"serviceAliases": {
|
|
29
|
+
"nginx": [
|
|
30
|
+
"nginx",
|
|
31
|
+
"web",
|
|
32
|
+
"webserver",
|
|
33
|
+
"web server",
|
|
34
|
+
"proxy",
|
|
35
|
+
"reverse proxy"
|
|
36
|
+
],
|
|
37
|
+
"redis": [
|
|
38
|
+
"redis",
|
|
39
|
+
"cache",
|
|
40
|
+
"redis-server"
|
|
41
|
+
],
|
|
42
|
+
"api": [
|
|
43
|
+
"api",
|
|
44
|
+
"backend",
|
|
45
|
+
"backend-api",
|
|
46
|
+
"server",
|
|
47
|
+
"app"
|
|
48
|
+
],
|
|
49
|
+
"worker": [
|
|
50
|
+
"worker",
|
|
51
|
+
"background",
|
|
52
|
+
"jobs",
|
|
53
|
+
"sidekiq",
|
|
54
|
+
"queue"
|
|
55
|
+
],
|
|
56
|
+
"postgres": [
|
|
57
|
+
"postgres",
|
|
58
|
+
"postgresql",
|
|
59
|
+
"pg",
|
|
60
|
+
"database",
|
|
61
|
+
"db"
|
|
62
|
+
],
|
|
63
|
+
"mysql": [
|
|
64
|
+
"mysql",
|
|
65
|
+
"mariadb",
|
|
66
|
+
"maria"
|
|
67
|
+
],
|
|
68
|
+
"mongo": [
|
|
69
|
+
"mongo",
|
|
70
|
+
"mongodb"
|
|
71
|
+
],
|
|
72
|
+
"docker": [
|
|
73
|
+
"docker",
|
|
74
|
+
"container",
|
|
75
|
+
"containers"
|
|
76
|
+
],
|
|
77
|
+
"sshd": [
|
|
78
|
+
"sshd",
|
|
79
|
+
"ssh",
|
|
80
|
+
"openssh"
|
|
81
|
+
],
|
|
82
|
+
"apache": [
|
|
83
|
+
"apache",
|
|
84
|
+
"apache2",
|
|
85
|
+
"httpd"
|
|
86
|
+
],
|
|
87
|
+
"certbot": [
|
|
88
|
+
"certbot",
|
|
89
|
+
"letsencrypt"
|
|
90
|
+
],
|
|
91
|
+
"fail2ban": [
|
|
92
|
+
"fail2ban"
|
|
93
|
+
],
|
|
94
|
+
"clamav": [
|
|
95
|
+
"clamav",
|
|
96
|
+
"clamscan",
|
|
97
|
+
"freshclam"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
export type AgentStatus = "running" | "completed" | "failed" | "killed";
|
|
3
|
+
export interface AgentHandle {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
status: AgentStatus;
|
|
8
|
+
pid?: number;
|
|
9
|
+
startedAt: Date;
|
|
10
|
+
completedAt?: Date;
|
|
11
|
+
output: string[];
|
|
12
|
+
error?: string;
|
|
13
|
+
acknowledged: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* AgentSpawner — spawns long-running child processes (agents) in the background.
|
|
17
|
+
*
|
|
18
|
+
* Unlike TaskRunner (which runs async functions in-process), AgentSpawner
|
|
19
|
+
* forks actual child processes that can run independently. Useful for:
|
|
20
|
+
* - Long SSH sessions
|
|
21
|
+
* - Tailing logs in real-time
|
|
22
|
+
* - Monitoring tasks
|
|
23
|
+
* - Parallel multi-server operations
|
|
24
|
+
*/
|
|
25
|
+
export declare class AgentSpawner extends EventEmitter {
|
|
26
|
+
private agents;
|
|
27
|
+
private processes;
|
|
28
|
+
private nextId;
|
|
29
|
+
/**
|
|
30
|
+
* Spawn a new agent that runs a shell command.
|
|
31
|
+
*/
|
|
32
|
+
spawnCommand(name: string, description: string, command: string, options?: {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
env?: Record<string, string>;
|
|
35
|
+
}): AgentHandle;
|
|
36
|
+
/**
|
|
37
|
+
* Spawn a command using exec (simpler, no worker needed).
|
|
38
|
+
*/
|
|
39
|
+
spawnShell(name: string, description: string, command: string): AgentHandle;
|
|
40
|
+
/** Kill an agent by ID. */
|
|
41
|
+
kill(id: number): boolean;
|
|
42
|
+
/** Get an agent by ID. */
|
|
43
|
+
get(id: number): AgentHandle | undefined;
|
|
44
|
+
/** List all agents. */
|
|
45
|
+
list(filter?: AgentStatus): AgentHandle[];
|
|
46
|
+
/** Get agents that finished since the user last checked. */
|
|
47
|
+
getUnacknowledged(): AgentHandle[];
|
|
48
|
+
/** Mark an agent as acknowledged. */
|
|
49
|
+
acknowledge(id: number): void;
|
|
50
|
+
acknowledgeAll(): void;
|
|
51
|
+
/** Get count of running agents. */
|
|
52
|
+
get active(): number;
|
|
53
|
+
/** Get last N lines of output from an agent. */
|
|
54
|
+
getOutput(id: number, lines?: number): string[];
|
|
55
|
+
}
|
|
56
|
+
export declare const agentSpawner: AgentSpawner;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
/**
|
|
7
|
+
* AgentSpawner — spawns long-running child processes (agents) in the background.
|
|
8
|
+
*
|
|
9
|
+
* Unlike TaskRunner (which runs async functions in-process), AgentSpawner
|
|
10
|
+
* forks actual child processes that can run independently. Useful for:
|
|
11
|
+
* - Long SSH sessions
|
|
12
|
+
* - Tailing logs in real-time
|
|
13
|
+
* - Monitoring tasks
|
|
14
|
+
* - Parallel multi-server operations
|
|
15
|
+
*/
|
|
16
|
+
export class AgentSpawner extends EventEmitter {
|
|
17
|
+
agents = new Map();
|
|
18
|
+
processes = new Map();
|
|
19
|
+
nextId = 1;
|
|
20
|
+
/**
|
|
21
|
+
* Spawn a new agent that runs a shell command.
|
|
22
|
+
*/
|
|
23
|
+
spawnCommand(name, description, command, options = {}) {
|
|
24
|
+
const agent = {
|
|
25
|
+
id: this.nextId++,
|
|
26
|
+
name,
|
|
27
|
+
description,
|
|
28
|
+
status: "running",
|
|
29
|
+
startedAt: new Date(),
|
|
30
|
+
output: [],
|
|
31
|
+
acknowledged: false,
|
|
32
|
+
};
|
|
33
|
+
this.agents.set(agent.id, agent);
|
|
34
|
+
// Use a worker script that executes the command
|
|
35
|
+
const workerPath = resolve(__dirname, "worker.ts");
|
|
36
|
+
const child = fork(workerPath, [command], {
|
|
37
|
+
cwd: options.cwd,
|
|
38
|
+
env: { ...process.env, ...options.env },
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
40
|
+
execArgv: ["--import", "tsx"],
|
|
41
|
+
});
|
|
42
|
+
agent.pid = child.pid;
|
|
43
|
+
this.processes.set(agent.id, child);
|
|
44
|
+
child.stdout?.on("data", (data) => {
|
|
45
|
+
const line = data.toString().trim();
|
|
46
|
+
if (line) {
|
|
47
|
+
agent.output.push(line);
|
|
48
|
+
this.emit("agent:output", agent, line);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
child.stderr?.on("data", (data) => {
|
|
52
|
+
const line = data.toString().trim();
|
|
53
|
+
if (line) {
|
|
54
|
+
agent.output.push(`[err] ${line}`);
|
|
55
|
+
this.emit("agent:output", agent, `[err] ${line}`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
child.on("close", (code) => {
|
|
59
|
+
if (agent.status === "killed")
|
|
60
|
+
return;
|
|
61
|
+
agent.status = code === 0 ? "completed" : "failed";
|
|
62
|
+
agent.completedAt = new Date();
|
|
63
|
+
if (code !== 0)
|
|
64
|
+
agent.error = `Exit code: ${code}`;
|
|
65
|
+
this.processes.delete(agent.id);
|
|
66
|
+
this.emit("agent:done", agent);
|
|
67
|
+
});
|
|
68
|
+
child.on("error", (err) => {
|
|
69
|
+
agent.status = "failed";
|
|
70
|
+
agent.error = err.message;
|
|
71
|
+
agent.completedAt = new Date();
|
|
72
|
+
this.processes.delete(agent.id);
|
|
73
|
+
this.emit("agent:done", agent);
|
|
74
|
+
});
|
|
75
|
+
this.emit("agent:started", agent);
|
|
76
|
+
return agent;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Spawn a command using exec (simpler, no worker needed).
|
|
80
|
+
*/
|
|
81
|
+
spawnShell(name, description, command) {
|
|
82
|
+
const { exec } = require("node:child_process");
|
|
83
|
+
const agent = {
|
|
84
|
+
id: this.nextId++,
|
|
85
|
+
name,
|
|
86
|
+
description,
|
|
87
|
+
status: "running",
|
|
88
|
+
startedAt: new Date(),
|
|
89
|
+
output: [],
|
|
90
|
+
acknowledged: false,
|
|
91
|
+
};
|
|
92
|
+
this.agents.set(agent.id, agent);
|
|
93
|
+
const child = exec(command, { timeout: 300_000 });
|
|
94
|
+
agent.pid = child.pid;
|
|
95
|
+
child.stdout?.on("data", (data) => {
|
|
96
|
+
const line = data.toString().trim();
|
|
97
|
+
if (line) {
|
|
98
|
+
agent.output.push(line);
|
|
99
|
+
this.emit("agent:output", agent, line);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
child.stderr?.on("data", (data) => {
|
|
103
|
+
const line = data.toString().trim();
|
|
104
|
+
if (line) {
|
|
105
|
+
agent.output.push(`[err] ${line}`);
|
|
106
|
+
this.emit("agent:output", agent, `[err] ${line}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
child.on("close", (code) => {
|
|
110
|
+
if (agent.status === "killed")
|
|
111
|
+
return;
|
|
112
|
+
agent.status = code === 0 ? "completed" : "failed";
|
|
113
|
+
agent.completedAt = new Date();
|
|
114
|
+
if (code !== 0)
|
|
115
|
+
agent.error = `Exit code: ${code}`;
|
|
116
|
+
this.emit("agent:done", agent);
|
|
117
|
+
});
|
|
118
|
+
this.emit("agent:started", agent);
|
|
119
|
+
return agent;
|
|
120
|
+
}
|
|
121
|
+
/** Kill an agent by ID. */
|
|
122
|
+
kill(id) {
|
|
123
|
+
const agent = this.agents.get(id);
|
|
124
|
+
if (!agent || agent.status !== "running")
|
|
125
|
+
return false;
|
|
126
|
+
const proc = this.processes.get(id);
|
|
127
|
+
if (proc) {
|
|
128
|
+
proc.kill("SIGTERM");
|
|
129
|
+
this.processes.delete(id);
|
|
130
|
+
}
|
|
131
|
+
// Also try killing by PID for shell-spawned agents
|
|
132
|
+
if (agent.pid) {
|
|
133
|
+
try {
|
|
134
|
+
process.kill(agent.pid, "SIGTERM");
|
|
135
|
+
}
|
|
136
|
+
catch { }
|
|
137
|
+
}
|
|
138
|
+
agent.status = "killed";
|
|
139
|
+
agent.completedAt = new Date();
|
|
140
|
+
this.emit("agent:done", agent);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
/** Get an agent by ID. */
|
|
144
|
+
get(id) {
|
|
145
|
+
return this.agents.get(id);
|
|
146
|
+
}
|
|
147
|
+
/** List all agents. */
|
|
148
|
+
list(filter) {
|
|
149
|
+
const all = Array.from(this.agents.values());
|
|
150
|
+
return filter ? all.filter((a) => a.status === filter) : all;
|
|
151
|
+
}
|
|
152
|
+
/** Get agents that finished since the user last checked. */
|
|
153
|
+
getUnacknowledged() {
|
|
154
|
+
return Array.from(this.agents.values()).filter((a) => !a.acknowledged && a.status !== "running");
|
|
155
|
+
}
|
|
156
|
+
/** Mark an agent as acknowledged. */
|
|
157
|
+
acknowledge(id) {
|
|
158
|
+
const agent = this.agents.get(id);
|
|
159
|
+
if (agent)
|
|
160
|
+
agent.acknowledged = true;
|
|
161
|
+
}
|
|
162
|
+
acknowledgeAll() {
|
|
163
|
+
for (const agent of this.agents.values()) {
|
|
164
|
+
if (agent.status !== "running")
|
|
165
|
+
agent.acknowledged = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Get count of running agents. */
|
|
169
|
+
get active() {
|
|
170
|
+
return Array.from(this.agents.values()).filter((a) => a.status === "running").length;
|
|
171
|
+
}
|
|
172
|
+
/** Get last N lines of output from an agent. */
|
|
173
|
+
getOutput(id, lines = 20) {
|
|
174
|
+
const agent = this.agents.get(id);
|
|
175
|
+
if (!agent)
|
|
176
|
+
return [];
|
|
177
|
+
return agent.output.slice(-lines);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export const agentSpawner = new AgentSpawner();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal planner.
|
|
3
|
+
*
|
|
4
|
+
* Breaks complex multi-step requests into a sequence of tasks.
|
|
5
|
+
*
|
|
6
|
+
* Handles patterns like:
|
|
7
|
+
* "check if api is down and restart it"
|
|
8
|
+
* "deploy to staging then check the logs"
|
|
9
|
+
* "show disk, check memory, and tail logs on prod"
|
|
10
|
+
* "restart nginx on prod and then rollback if it fails"
|
|
11
|
+
* "copy the config to /backup and restart nginx"
|
|
12
|
+
*/
|
|
13
|
+
export interface PlanStep {
|
|
14
|
+
id: number;
|
|
15
|
+
rawText: string;
|
|
16
|
+
intent: string | null;
|
|
17
|
+
fields: Record<string, unknown>;
|
|
18
|
+
confidence: number;
|
|
19
|
+
/** Condition for execution */
|
|
20
|
+
condition?: "always" | "if_success" | "if_failure";
|
|
21
|
+
/** Step IDs this step depends on */
|
|
22
|
+
dependsOn: number[];
|
|
23
|
+
status: "pending" | "running" | "completed" | "failed" | "skipped";
|
|
24
|
+
result?: string;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ExecutionPlan {
|
|
28
|
+
originalText: string;
|
|
29
|
+
steps: PlanStep[];
|
|
30
|
+
isMultiStep: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Analyze text and determine if it's a multi-step request.
|
|
34
|
+
* If so, break it into a plan.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createPlan(rawText: string): ExecutionPlan;
|
|
37
|
+
/**
|
|
38
|
+
* Format a plan for display.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatPlan(plan: ExecutionPlan): string;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import nlp from "compromise";
|
|
2
|
+
import { parseByRules } from "../nlp/ruleParser.js";
|
|
3
|
+
// Conjunctions and sequencing words that split multi-step commands
|
|
4
|
+
const SPLIT_PATTERNS = [
|
|
5
|
+
/\band then\b/i,
|
|
6
|
+
/\bthen\b/i,
|
|
7
|
+
/\bafter that\b/i,
|
|
8
|
+
/\bfollowed by\b/i,
|
|
9
|
+
/\bnext\b/i,
|
|
10
|
+
/,\s*(?:and\s+)?/,
|
|
11
|
+
/\band\b/i,
|
|
12
|
+
];
|
|
13
|
+
// Conditional patterns
|
|
14
|
+
const CONDITIONAL_PATTERNS = [
|
|
15
|
+
{ pattern: /\bif (?:it |that )?(?:works|succeeds|passes|is up|is running)\b/i, condition: "if_success" },
|
|
16
|
+
{ pattern: /\bif (?:it |that )?(?:fails|breaks|is down|crashes|errors)\b/i, condition: "if_failure" },
|
|
17
|
+
{ pattern: /\botherwise\b/i, condition: "if_failure" },
|
|
18
|
+
{ pattern: /\bif not\b/i, condition: "if_failure" },
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Analyze text and determine if it's a multi-step request.
|
|
22
|
+
* If so, break it into a plan.
|
|
23
|
+
*/
|
|
24
|
+
export function createPlan(rawText) {
|
|
25
|
+
const clauses = splitIntoClauses(rawText);
|
|
26
|
+
if (clauses.length <= 1) {
|
|
27
|
+
// Single step — still create a plan for consistency
|
|
28
|
+
const parsed = parseByRules(rawText);
|
|
29
|
+
return {
|
|
30
|
+
originalText: rawText,
|
|
31
|
+
isMultiStep: false,
|
|
32
|
+
steps: [{
|
|
33
|
+
id: 1,
|
|
34
|
+
rawText,
|
|
35
|
+
intent: parsed?.intent ?? null,
|
|
36
|
+
fields: parsed?.fields ?? {},
|
|
37
|
+
confidence: parsed?.confidence ?? 0,
|
|
38
|
+
condition: "always",
|
|
39
|
+
dependsOn: [],
|
|
40
|
+
status: "pending",
|
|
41
|
+
}],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Multi-step: parse each clause
|
|
45
|
+
const steps = [];
|
|
46
|
+
let lastEntityContext = {};
|
|
47
|
+
for (let i = 0; i < clauses.length; i++) {
|
|
48
|
+
const clause = clauses[i];
|
|
49
|
+
const condition = detectCondition(clause.text);
|
|
50
|
+
// Inherit entities from previous steps for coreference
|
|
51
|
+
let textToParse = clause.text;
|
|
52
|
+
// Replace "it" with last known service
|
|
53
|
+
if (/\bit\b/i.test(textToParse) && lastEntityContext.service) {
|
|
54
|
+
textToParse = textToParse.replace(/\bit\b/i, lastEntityContext.service);
|
|
55
|
+
}
|
|
56
|
+
const parsed = parseByRules(textToParse);
|
|
57
|
+
// Carry forward environment and service context
|
|
58
|
+
const fields = { ...parsed?.fields ?? {} };
|
|
59
|
+
if (!fields.environment && lastEntityContext.environment) {
|
|
60
|
+
fields.environment = lastEntityContext.environment;
|
|
61
|
+
}
|
|
62
|
+
if (!fields.service && lastEntityContext.service) {
|
|
63
|
+
fields.service = lastEntityContext.service;
|
|
64
|
+
}
|
|
65
|
+
const step = {
|
|
66
|
+
id: i + 1,
|
|
67
|
+
rawText: clause.original,
|
|
68
|
+
intent: parsed?.intent ?? null,
|
|
69
|
+
fields,
|
|
70
|
+
confidence: parsed?.confidence ?? 0,
|
|
71
|
+
condition: condition ?? (i === 0 ? "always" : "if_success"),
|
|
72
|
+
dependsOn: i > 0 ? [i] : [],
|
|
73
|
+
status: "pending",
|
|
74
|
+
};
|
|
75
|
+
steps.push(step);
|
|
76
|
+
// Update entity context for next step
|
|
77
|
+
if (fields.service)
|
|
78
|
+
lastEntityContext.service = fields.service;
|
|
79
|
+
if (fields.environment)
|
|
80
|
+
lastEntityContext.environment = fields.environment;
|
|
81
|
+
if (fields.target)
|
|
82
|
+
lastEntityContext.target = fields.target;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
originalText: rawText,
|
|
86
|
+
isMultiStep: true,
|
|
87
|
+
steps,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Split a complex sentence into individual action clauses.
|
|
92
|
+
*/
|
|
93
|
+
function splitIntoClauses(text) {
|
|
94
|
+
// Use compromise to detect sentence/clause boundaries first
|
|
95
|
+
const doc = nlp(text);
|
|
96
|
+
const sentences = doc.sentences().out("array");
|
|
97
|
+
if (sentences.length > 1) {
|
|
98
|
+
return sentences.map((s) => ({ text: cleanClause(s), original: s }));
|
|
99
|
+
}
|
|
100
|
+
// Single sentence — try splitting on conjunctions
|
|
101
|
+
let remaining = text;
|
|
102
|
+
const parts = [];
|
|
103
|
+
for (const pattern of SPLIT_PATTERNS) {
|
|
104
|
+
const split = remaining.split(pattern).filter((s) => s.trim().length > 0);
|
|
105
|
+
if (split.length > 1) {
|
|
106
|
+
parts.push(...split);
|
|
107
|
+
remaining = "";
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (parts.length === 0) {
|
|
112
|
+
return [{ text: cleanClause(text), original: text }];
|
|
113
|
+
}
|
|
114
|
+
return parts.map((p) => ({ text: cleanClause(p), original: p.trim() }));
|
|
115
|
+
}
|
|
116
|
+
function cleanClause(text) {
|
|
117
|
+
// Remove leading conjunctions and conditional phrases
|
|
118
|
+
return text
|
|
119
|
+
.replace(/^(and then|then|and|but|or|after that|followed by|next)\s+/i, "")
|
|
120
|
+
.replace(/\bif (?:it |that )?(?:works|succeeds|passes|fails|breaks|is down|is up)\s*/i, "")
|
|
121
|
+
.replace(/\botherwise\s*/i, "")
|
|
122
|
+
.trim();
|
|
123
|
+
}
|
|
124
|
+
function detectCondition(text) {
|
|
125
|
+
for (const { pattern, condition } of CONDITIONAL_PATTERNS) {
|
|
126
|
+
if (pattern.test(text))
|
|
127
|
+
return condition;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Format a plan for display.
|
|
133
|
+
*/
|
|
134
|
+
export function formatPlan(plan) {
|
|
135
|
+
const c = {
|
|
136
|
+
reset: "\x1b[0m",
|
|
137
|
+
bold: "\x1b[1m",
|
|
138
|
+
dim: "\x1b[2m",
|
|
139
|
+
green: "\x1b[32m",
|
|
140
|
+
yellow: "\x1b[33m",
|
|
141
|
+
red: "\x1b[31m",
|
|
142
|
+
cyan: "\x1b[36m",
|
|
143
|
+
};
|
|
144
|
+
const lines = [];
|
|
145
|
+
if (plan.isMultiStep) {
|
|
146
|
+
lines.push(`${c.bold}Execution Plan${c.reset} (${plan.steps.length} steps):`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
lines.push(`${c.bold}Single-step command:${c.reset}`);
|
|
150
|
+
}
|
|
151
|
+
lines.push("");
|
|
152
|
+
for (const step of plan.steps) {
|
|
153
|
+
const statusIcon = step.status === "completed" ? `${c.green}✓${c.reset}` :
|
|
154
|
+
step.status === "failed" ? `${c.red}✗${c.reset}` :
|
|
155
|
+
step.status === "running" ? `${c.yellow}⟳${c.reset}` :
|
|
156
|
+
step.status === "skipped" ? `${c.dim}⊘${c.reset}` :
|
|
157
|
+
`${c.dim}○${c.reset}`;
|
|
158
|
+
const intentLabel = step.intent ?? `${c.red}unknown${c.reset}`;
|
|
159
|
+
const condLabel = step.condition === "if_success" ? `${c.dim}(if previous succeeds)${c.reset}` :
|
|
160
|
+
step.condition === "if_failure" ? `${c.yellow}(if previous fails)${c.reset}` : "";
|
|
161
|
+
lines.push(` ${statusIcon} Step ${step.id}: ${c.cyan}${intentLabel}${c.reset} ${condLabel}`);
|
|
162
|
+
lines.push(` ${c.dim}"${step.rawText}"${c.reset}`);
|
|
163
|
+
const fields = Object.entries(step.fields).filter(([, v]) => v !== undefined);
|
|
164
|
+
if (fields.length > 0) {
|
|
165
|
+
lines.push(` ${fields.map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
if (step.result) {
|
|
168
|
+
lines.push(` ${c.green}Result: ${step.result.split("\n")[0]}${c.reset}`);
|
|
169
|
+
}
|
|
170
|
+
if (step.error) {
|
|
171
|
+
lines.push(` ${c.red}Error: ${step.error}${c.reset}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|