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.
Files changed (64) hide show
  1. package/AGENTS.md +57 -0
  2. package/README.md +479 -0
  3. package/agent/__init__.py +10 -0
  4. package/agent/agent.py +269 -0
  5. package/agent/agents/__init__.py +34 -0
  6. package/agent/agents/registry.py +212 -0
  7. package/agent/builtin_tools.py +591 -0
  8. package/agent/chat.py +354 -0
  9. package/agent/config.py +67 -0
  10. package/agent/context.py +63 -0
  11. package/agent/edits.py +167 -0
  12. package/agent/gitaware.py +126 -0
  13. package/agent/hardware.py +331 -0
  14. package/agent/knowledge.py +133 -0
  15. package/agent/memory.py +98 -0
  16. package/agent/ollama_client.py +133 -0
  17. package/agent/permissions.py +93 -0
  18. package/agent/providers/__init__.py +51 -0
  19. package/agent/providers/base.py +78 -0
  20. package/agent/providers/image.py +110 -0
  21. package/agent/providers/ollama.py +111 -0
  22. package/agent/providers/video.py +96 -0
  23. package/agent/providers/web.py +155 -0
  24. package/agent/router.py +200 -0
  25. package/agent/rules.py +52 -0
  26. package/agent/runner.py +168 -0
  27. package/agent/skills.py +161 -0
  28. package/agent/tools.py +102 -0
  29. package/agent/verification.py +80 -0
  30. package/agent/workspace.py +487 -0
  31. package/bin/eljay +5 -0
  32. package/bin/eljay.cmd +4 -0
  33. package/bin/myagent +4 -0
  34. package/bin/myagent.cmd +4 -0
  35. package/eljay-ai-1.1.0.tgz +0 -0
  36. package/eljay.js +56 -0
  37. package/eljay.py +150 -0
  38. package/install.ps1 +28 -0
  39. package/knowledge/reference/diffusers.md +32 -0
  40. package/knowledge/reference/video-providers.md +21 -0
  41. package/knowledge/setup/comfyui.md +35 -0
  42. package/knowledge/setup/image-providers.md +16 -0
  43. package/knowledge/test-category/test-entry.md +5 -0
  44. package/myagent.py +30 -0
  45. package/package.json +40 -0
  46. package/skills/coding/code-review.md +3 -0
  47. package/skills/coding/fix-attempt-protocol.md +9 -0
  48. package/skills/debugging/root-cause-analysis.md +9 -0
  49. package/skills/general/communication.md +7 -0
  50. package/skills/laravel/authentication.md +15 -0
  51. package/skills/mysql/performance.md +9 -0
  52. package/skills/php/standards.md +8 -0
  53. package/skills/react/component-best-practices.md +8 -0
  54. package/skills/research/source-tracking.md +7 -0
  55. package/skills/security/input-validation.md +9 -0
  56. package/skills/testing/pytest-best-practices.md +7 -0
  57. package/tests/run_all.py +34 -0
  58. package/tests/test_agent_core.py +385 -0
  59. package/tests/test_capabilities.py +169 -0
  60. package/tests/test_edits.py +117 -0
  61. package/tests/test_eljay.py +209 -0
  62. package/tests/test_runner.py +114 -0
  63. package/tests/test_universal.py +458 -0
  64. package/tests/test_workspace.py +221 -0
@@ -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)