eljay-ai 1.1.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/AGENTS.md +57 -0
- package/README.md +479 -0
- package/agent/__init__.py +10 -0
- package/agent/agent.py +269 -0
- package/agent/agents/__init__.py +34 -0
- package/agent/agents/registry.py +212 -0
- package/agent/builtin_tools.py +591 -0
- package/agent/chat.py +354 -0
- package/agent/config.py +67 -0
- package/agent/context.py +63 -0
- package/agent/edits.py +167 -0
- package/agent/gitaware.py +126 -0
- package/agent/hardware.py +331 -0
- package/agent/knowledge.py +133 -0
- package/agent/memory.py +98 -0
- package/agent/ollama_client.py +133 -0
- package/agent/permissions.py +93 -0
- package/agent/providers/__init__.py +51 -0
- package/agent/providers/base.py +78 -0
- package/agent/providers/image.py +110 -0
- package/agent/providers/ollama.py +111 -0
- package/agent/providers/video.py +96 -0
- package/agent/providers/web.py +155 -0
- package/agent/router.py +200 -0
- package/agent/rules.py +52 -0
- package/agent/runner.py +168 -0
- package/agent/skills.py +161 -0
- package/agent/tools.py +102 -0
- package/agent/verification.py +80 -0
- package/agent/workspace.py +487 -0
- package/bin/eljay +5 -0
- package/bin/eljay.cmd +4 -0
- package/bin/myagent +4 -0
- package/bin/myagent.cmd +4 -0
- package/eljay-ai-1.1.0.tgz +0 -0
- package/eljay.js +56 -0
- package/eljay.py +150 -0
- package/install.ps1 +28 -0
- package/knowledge/reference/diffusers.md +32 -0
- package/knowledge/reference/video-providers.md +21 -0
- package/knowledge/setup/comfyui.md +35 -0
- package/knowledge/setup/image-providers.md +16 -0
- package/knowledge/test-category/test-entry.md +5 -0
- package/myagent.py +30 -0
- package/package.json +40 -0
- package/skills/coding/code-review.md +3 -0
- package/skills/coding/fix-attempt-protocol.md +9 -0
- package/skills/debugging/root-cause-analysis.md +9 -0
- package/skills/general/communication.md +7 -0
- package/skills/laravel/authentication.md +15 -0
- package/skills/mysql/performance.md +9 -0
- package/skills/php/standards.md +8 -0
- package/skills/react/component-best-practices.md +8 -0
- package/skills/research/source-tracking.md +7 -0
- package/skills/security/input-validation.md +9 -0
- package/skills/testing/pytest-best-practices.md +7 -0
- package/tests/run_all.py +34 -0
- package/tests/test_agent_core.py +385 -0
- package/tests/test_capabilities.py +169 -0
- package/tests/test_edits.py +117 -0
- package/tests/test_eljay.py +209 -0
- package/tests/test_runner.py +114 -0
- package/tests/test_universal.py +458 -0
- package/tests/test_workspace.py +221 -0
package/agent/skills.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Skills system for ElJay AI.
|
|
2
|
+
|
|
3
|
+
A *skill* is a reusable bundle of instructions/knowledge for a domain
|
|
4
|
+
(e.g. "Laravel authentication", "React debugging", "security review").
|
|
5
|
+
Skills live in the ``skills/`` directory, organized by category:
|
|
6
|
+
|
|
7
|
+
skills/
|
|
8
|
+
├── coding/
|
|
9
|
+
├── debugging/
|
|
10
|
+
├── testing/
|
|
11
|
+
├── react/
|
|
12
|
+
├── laravel/
|
|
13
|
+
├── php/
|
|
14
|
+
├── mysql/
|
|
15
|
+
├── security/
|
|
16
|
+
├── research/
|
|
17
|
+
├── image-generation/
|
|
18
|
+
├── video-generation/
|
|
19
|
+
└── general/
|
|
20
|
+
|
|
21
|
+
Each skill is a single ``.md`` or ``.txt`` file whose first line is a
|
|
22
|
+
one-sentence description. The system loads only relevant skills per request,
|
|
23
|
+
keeping context lean.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import os
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class Skill:
|
|
34
|
+
"""Metadata + content for one skill."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
category: str
|
|
38
|
+
source: str # file path
|
|
39
|
+
description: str
|
|
40
|
+
content: str # full instructions
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Category ordering for /skills display.
|
|
44
|
+
DEFAULT_CATEGORIES = [
|
|
45
|
+
"coding", "debugging", "testing", "react", "laravel", "php", "mysql",
|
|
46
|
+
"security", "research", "image-generation", "video-generation", "general",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Skills:
|
|
51
|
+
"""Load and query skills from the project's ``skills/`` directory."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, root: str | os.PathLike):
|
|
54
|
+
self.root = str(root)
|
|
55
|
+
self.skills_dir = os.path.join(self.root, "skills")
|
|
56
|
+
self._cache: list[Skill] | None = None
|
|
57
|
+
|
|
58
|
+
def _scan(self) -> list[Skill]:
|
|
59
|
+
if not os.path.isdir(self.skills_dir):
|
|
60
|
+
return []
|
|
61
|
+
found: list[Skill] = []
|
|
62
|
+
for category in sorted(os.listdir(self.skills_dir)):
|
|
63
|
+
cat_dir = os.path.join(self.skills_dir, category)
|
|
64
|
+
if not os.path.isdir(cat_dir):
|
|
65
|
+
continue
|
|
66
|
+
for fname in sorted(os.listdir(cat_dir)):
|
|
67
|
+
if not fname.endswith((".md", ".txt")):
|
|
68
|
+
continue
|
|
69
|
+
fpath = os.path.join(cat_dir, fname)
|
|
70
|
+
try:
|
|
71
|
+
content = open(fpath, encoding="utf-8").read()
|
|
72
|
+
except OSError:
|
|
73
|
+
continue
|
|
74
|
+
lines = content.splitlines()
|
|
75
|
+
description = lines[0].lstrip("# ").strip() if lines else ""
|
|
76
|
+
found.append(
|
|
77
|
+
Skill(
|
|
78
|
+
name=fname.rsplit(".", 1)[0],
|
|
79
|
+
category=category,
|
|
80
|
+
source=fpath,
|
|
81
|
+
description=description,
|
|
82
|
+
content=content,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
return found
|
|
86
|
+
|
|
87
|
+
def all_skills(self) -> list[Skill]:
|
|
88
|
+
if self._cache is None:
|
|
89
|
+
self._cache = self._scan()
|
|
90
|
+
return self._cache
|
|
91
|
+
|
|
92
|
+
def by_category(self) -> dict[str, list[Skill]]:
|
|
93
|
+
"""Group skills by category (excluding empty categories)."""
|
|
94
|
+
result: dict[str, list[Skill]] = {}
|
|
95
|
+
for skill in self.all_skills():
|
|
96
|
+
result.setdefault(skill.category, []).append(skill)
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
def search(self, query: str) -> list[Skill]:
|
|
100
|
+
"""Return skills whose name/category/description mentions *query*."""
|
|
101
|
+
q = query.lower()
|
|
102
|
+
return [
|
|
103
|
+
s for s in self.all_skills()
|
|
104
|
+
if q in s.name.lower()
|
|
105
|
+
or q in s.category.lower()
|
|
106
|
+
or q in s.description.lower()
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
def relevant_to(self, query: str) -> list[Skill]:
|
|
110
|
+
"""Return skills relevant to a user query (keyword overlap)."""
|
|
111
|
+
q_lower = query.lower()
|
|
112
|
+
q_words = set(w.strip(".,!?") for w in q_lower.split())
|
|
113
|
+
results: list[tuple[Skill, int]] = []
|
|
114
|
+
for skill in self.all_skills():
|
|
115
|
+
score = 0
|
|
116
|
+
for word in q_words:
|
|
117
|
+
if word in skill.name.lower():
|
|
118
|
+
score += 3
|
|
119
|
+
if word in skill.category.lower():
|
|
120
|
+
score += 2
|
|
121
|
+
if word in skill.description.lower():
|
|
122
|
+
score += 1
|
|
123
|
+
if score > 0:
|
|
124
|
+
results.append((skill, score))
|
|
125
|
+
results.sort(key=lambda x: -x[1])
|
|
126
|
+
return [s for s, _ in results]
|
|
127
|
+
|
|
128
|
+
def summary(self) -> str:
|
|
129
|
+
"""One-line summary for the banner."""
|
|
130
|
+
n = len(self.all_skills())
|
|
131
|
+
return f"{n} skill(s) in {len(self.by_category())} categor(ies)" if n else "no skills"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def ensure_default_skills(root) -> None:
|
|
135
|
+
"""Create the default skills/ directory with a few starter skills.
|
|
136
|
+
|
|
137
|
+
This is called by the CLI on first run. It only writes if the directory
|
|
138
|
+
doesn't already exist.
|
|
139
|
+
"""
|
|
140
|
+
skills_dir = os.path.join(str(root), "skills")
|
|
141
|
+
if os.path.isdir(skills_dir):
|
|
142
|
+
return
|
|
143
|
+
os.makedirs(os.path.join(skills_dir, "general"), exist_ok=True)
|
|
144
|
+
starter = [
|
|
145
|
+
("coding", "code-review.md",
|
|
146
|
+
"# Code Review\n\nCheck for: correctness, edge cases, security issues, "
|
|
147
|
+
"readability, and adherence to existing project conventions."),
|
|
148
|
+
("general", "communication.md",
|
|
149
|
+
"# Communication\n\nBe concise, accurate, and helpful. Never fabricate "
|
|
150
|
+
"facts. If unsure, say so and suggest how to find out."),
|
|
151
|
+
("research", "source-tracking.md",
|
|
152
|
+
"# Source Tracking\n\nAlways cite sources. Distinguish between direct "
|
|
153
|
+
"quotes, paraphrased information, and your own reasoning."),
|
|
154
|
+
]
|
|
155
|
+
for category, fname, content in starter:
|
|
156
|
+
cat_dir = os.path.join(skills_dir, category)
|
|
157
|
+
os.makedirs(cat_dir, exist_ok=True)
|
|
158
|
+
fpath = os.path.join(cat_dir, fname)
|
|
159
|
+
if not os.path.exists(fpath):
|
|
160
|
+
with open(fpath, "w", encoding="utf-8") as f:
|
|
161
|
+
f.write(content)
|
package/agent/tools.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Tool abstraction and registry.
|
|
2
|
+
|
|
3
|
+
A tool is a named function the model may ask the agent to run. Each tool
|
|
4
|
+
carries a JSON-schema description (so models that support native tool calling
|
|
5
|
+
can use it) and a `risk` level that the permission system will enforce from
|
|
6
|
+
Phase 8 onward. In Phase 3 the risk level is metadata only — nothing is gated
|
|
7
|
+
yet, because only safe tools exist so far.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
# Risk levels: "safe" (auto-run), "ask" (needs confirmation), "high" (explicit).
|
|
18
|
+
RISK_SAFE = "safe"
|
|
19
|
+
RISK_ASK = "ask"
|
|
20
|
+
RISK_HIGH = "high"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Tool:
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
parameters: dict[str, Any]
|
|
28
|
+
func: Callable[..., Any]
|
|
29
|
+
risk: str = RISK_SAFE
|
|
30
|
+
# Optional: builds a human-readable preview of a call, shown before asking
|
|
31
|
+
# permission (e.g. a diff of the change). Receives the call's arguments.
|
|
32
|
+
preview: Callable[..., str] | None = None
|
|
33
|
+
# Optional: computes the risk for a SPECIFIC call from its arguments. Used by
|
|
34
|
+
# tools whose danger depends on the arguments (e.g. run_command).
|
|
35
|
+
risk_func: Callable[..., str] | None = None
|
|
36
|
+
|
|
37
|
+
def schema(self) -> dict[str, Any]:
|
|
38
|
+
"""Return the OpenAI/Ollama-style function schema for this tool."""
|
|
39
|
+
return {
|
|
40
|
+
"type": "function",
|
|
41
|
+
"function": {
|
|
42
|
+
"name": self.name,
|
|
43
|
+
"description": self.description,
|
|
44
|
+
"parameters": self.parameters,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def run(self, arguments: dict[str, Any]) -> Any:
|
|
49
|
+
"""Execute the tool. Raises on failure; the caller handles errors."""
|
|
50
|
+
return self.func(**arguments)
|
|
51
|
+
|
|
52
|
+
def risk_for(self, arguments: dict[str, Any]) -> str:
|
|
53
|
+
"""Risk level for this specific call (falls back to the static risk)."""
|
|
54
|
+
if self.risk_func is not None:
|
|
55
|
+
try:
|
|
56
|
+
return self.risk_func(**arguments)
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
return self.risk
|
|
59
|
+
return self.risk
|
|
60
|
+
|
|
61
|
+
def make_preview(self, arguments: dict[str, Any]) -> str:
|
|
62
|
+
"""Return a human-readable preview of what this call would do."""
|
|
63
|
+
if self.preview is not None:
|
|
64
|
+
try:
|
|
65
|
+
return self.preview(**arguments)
|
|
66
|
+
except (TypeError, ValueError):
|
|
67
|
+
pass
|
|
68
|
+
try:
|
|
69
|
+
rendered = json.dumps(arguments, ensure_ascii=False)
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
rendered = str(arguments)
|
|
72
|
+
return f"{self.name}({rendered})"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ToolRegistry:
|
|
76
|
+
"""Holds the tools available to the agent."""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
self._tools: dict[str, Tool] = {}
|
|
80
|
+
|
|
81
|
+
def register(self, tool: Tool) -> None:
|
|
82
|
+
if tool.name in self._tools:
|
|
83
|
+
raise ValueError(f"Tool '{tool.name}' is already registered")
|
|
84
|
+
self._tools[tool.name] = tool
|
|
85
|
+
|
|
86
|
+
def get(self, name: str) -> Tool | None:
|
|
87
|
+
return self._tools.get(name)
|
|
88
|
+
|
|
89
|
+
def __contains__(self, name: object) -> bool:
|
|
90
|
+
return name in self._tools
|
|
91
|
+
|
|
92
|
+
def __len__(self) -> int:
|
|
93
|
+
return len(self._tools)
|
|
94
|
+
|
|
95
|
+
def tools(self) -> list[Tool]:
|
|
96
|
+
return list(self._tools.values())
|
|
97
|
+
|
|
98
|
+
def names(self) -> list[str]:
|
|
99
|
+
return list(self._tools)
|
|
100
|
+
|
|
101
|
+
def schemas(self) -> list[dict[str, Any]]:
|
|
102
|
+
return [t.schema() for t in self._tools.values()]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Detect the smallest useful verification commands for a project.
|
|
2
|
+
|
|
3
|
+
Serves the rule "use the smallest useful verification first": we look at the
|
|
4
|
+
project's own files and suggest lint / typecheck / test / build commands, ordered
|
|
5
|
+
smallest-and-cheapest first. We never run anything here — that goes through
|
|
6
|
+
run_command and the permission gate.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .workspace import Workspace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Check:
|
|
20
|
+
command: str
|
|
21
|
+
purpose: str
|
|
22
|
+
weight: int # lower = smaller / faster
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _read_json(path: Path) -> dict:
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_checks(ws: Workspace) -> list[Check]:
|
|
33
|
+
root = ws.root
|
|
34
|
+
checks: list[Check] = []
|
|
35
|
+
|
|
36
|
+
package_json = root / "package.json"
|
|
37
|
+
if package_json.exists():
|
|
38
|
+
scripts = _read_json(package_json).get("scripts", {}) or {}
|
|
39
|
+
wanted = (
|
|
40
|
+
("lint", "Lint the frontend", 1),
|
|
41
|
+
("typecheck", "Type-check the frontend", 2),
|
|
42
|
+
("type-check", "Type-check the frontend", 2),
|
|
43
|
+
("test", "Run frontend tests", 3),
|
|
44
|
+
("build", "Build the frontend", 4),
|
|
45
|
+
)
|
|
46
|
+
for name, purpose, weight in wanted:
|
|
47
|
+
if name in scripts:
|
|
48
|
+
checks.append(Check(f"npm run {name}", purpose, weight))
|
|
49
|
+
|
|
50
|
+
if (root / "composer.json").exists():
|
|
51
|
+
checks.append(Check("composer validate --no-check-publish", "Validate composer.json", 1))
|
|
52
|
+
if (root / "artisan").exists():
|
|
53
|
+
checks.append(Check("php artisan test", "Run Laravel tests", 3))
|
|
54
|
+
|
|
55
|
+
if (root / "pyproject.toml").exists() or (root / "requirements.txt").exists() or (root / "setup.py").exists() or (root / "tests").is_dir():
|
|
56
|
+
checks.append(Check("python -m pytest -q", "Run Python tests", 3))
|
|
57
|
+
|
|
58
|
+
if (root / "go.mod").exists():
|
|
59
|
+
checks.append(Check("go build ./...", "Compile Go packages", 3))
|
|
60
|
+
if (root / "Cargo.toml").exists():
|
|
61
|
+
checks.append(Check("cargo check", "Compile the Rust project", 3))
|
|
62
|
+
|
|
63
|
+
deduped: list[Check] = []
|
|
64
|
+
seen: set[str] = set()
|
|
65
|
+
for check in sorted(checks, key=lambda c: (c.weight, c.command)):
|
|
66
|
+
if check.command not in seen:
|
|
67
|
+
seen.add(check.command)
|
|
68
|
+
deduped.append(check)
|
|
69
|
+
return deduped
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def format_checks(ws: Workspace) -> str:
|
|
73
|
+
checks = detect_checks(ws)
|
|
74
|
+
if not checks:
|
|
75
|
+
return "No standard test/build commands were detected for this project."
|
|
76
|
+
lines = ["Suggested verification (smallest first):"]
|
|
77
|
+
for check in checks:
|
|
78
|
+
lines.append(f" ({check.weight}) {check.command} -- {check.purpose}")
|
|
79
|
+
lines.append("Prefer the smallest check that covers your change.")
|
|
80
|
+
return "\n".join(lines)
|