aru-code 0.8.0__tar.gz → 0.10.0__tar.gz
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.
- {aru_code-0.8.0/aru_code.egg-info → aru_code-0.10.0}/PKG-INFO +27 -2
- {aru_code-0.8.0 → aru_code-0.10.0}/README.md +26 -1
- aru_code-0.10.0/aru/__init__.py +1 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/agent_factory.py +7 -2
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/cli.py +12 -1
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/config.py +117 -16
- {aru_code-0.8.0 → aru_code-0.10.0/aru_code.egg-info}/PKG-INFO +27 -2
- {aru_code-0.8.0 → aru_code-0.10.0}/pyproject.toml +1 -1
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_config.py +231 -28
- aru_code-0.8.0/aru/__init__.py +0 -1
- {aru_code-0.8.0 → aru_code-0.10.0}/LICENSE +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/agents/__init__.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/agents/base.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/agents/executor.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/agents/planner.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/commands.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/completers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/context.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/display.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/permissions.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/providers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/runner.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/runtime.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/session.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/__init__.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/ast_tools.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/codebase.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/gitignore.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/mcp_client.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/ranker.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru/tools/tasklist.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru_code.egg-info/SOURCES.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru_code.egg-info/dependency_links.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru_code.egg-info/entry_points.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru_code.egg-info/requires.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/aru_code.egg-info/top_level.txt +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/setup.cfg +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_agents_base.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_ast_tools.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_advanced.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_base.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_completers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_new.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_run_cli.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_session.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_cli_shell.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_codebase.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_context.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_executor.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_gitignore.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_main.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_mcp_client.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_permissions.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_planner.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_providers.py +0 -0
- {aru_code-0.8.0 → aru_code-0.10.0}/tests/test_ranker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aru-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: A Claude Code clone built with Agno agents
|
|
5
5
|
Author-email: Estevao <estevaofon@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -302,6 +302,31 @@ Without any `aru.json` config, aru applies safe defaults:
|
|
|
302
302
|
|
|
303
303
|
Place an `AGENTS.md` file in your project root with custom instructions that will be appended to all agent system prompts.
|
|
304
304
|
|
|
305
|
+
### Instructions (Rules)
|
|
306
|
+
|
|
307
|
+
You can load additional instructions from local files, glob patterns, or remote URLs via the `instructions` field in `aru.json`:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"instructions": [
|
|
312
|
+
"CONTRIBUTING.md",
|
|
313
|
+
"docs/coding-standards.md",
|
|
314
|
+
"packages/*/AGENTS.md",
|
|
315
|
+
"https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Each entry is resolved as follows:
|
|
321
|
+
|
|
322
|
+
| Format | Example | Behavior |
|
|
323
|
+
|--------|---------|----------|
|
|
324
|
+
| **Local file** | `"CONTRIBUTING.md"` | Reads the file relative to the project root |
|
|
325
|
+
| **Glob pattern** | `"docs/**/*.md"` | Expands the pattern, respects `.gitignore` |
|
|
326
|
+
| **Remote URL** | `"https://example.com/rules.md"` | Fetches via HTTP (5s timeout, cached per session) |
|
|
327
|
+
|
|
328
|
+
All resolved content is combined and appended to the agent's system prompt alongside `AGENTS.md`. Individual files are capped at 10KB, and the total combined size is capped at 50KB to prevent context bloat. Missing files and failed URL fetches are skipped with a warning.
|
|
329
|
+
|
|
305
330
|
### `.agents/` Directory
|
|
306
331
|
|
|
307
332
|
```
|
|
@@ -315,7 +340,7 @@ Place an `AGENTS.md` file in your project root with custom instructions that wil
|
|
|
315
340
|
└── SKILL.md
|
|
316
341
|
```
|
|
317
342
|
|
|
318
|
-
Command files support frontmatter with `description` and
|
|
343
|
+
Command files support frontmatter with `description`, `agent`, and `model` fields, plus OpenCode-style argument placeholders: `$ARGUMENTS` (full string), `$1`/`$2` (positional), and `$ARGUMENTS[N]` (0-indexed).
|
|
319
344
|
|
|
320
345
|
### Custom Agents
|
|
321
346
|
|
|
@@ -255,6 +255,31 @@ Without any `aru.json` config, aru applies safe defaults:
|
|
|
255
255
|
|
|
256
256
|
Place an `AGENTS.md` file in your project root with custom instructions that will be appended to all agent system prompts.
|
|
257
257
|
|
|
258
|
+
### Instructions (Rules)
|
|
259
|
+
|
|
260
|
+
You can load additional instructions from local files, glob patterns, or remote URLs via the `instructions` field in `aru.json`:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"instructions": [
|
|
265
|
+
"CONTRIBUTING.md",
|
|
266
|
+
"docs/coding-standards.md",
|
|
267
|
+
"packages/*/AGENTS.md",
|
|
268
|
+
"https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"
|
|
269
|
+
]
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Each entry is resolved as follows:
|
|
274
|
+
|
|
275
|
+
| Format | Example | Behavior |
|
|
276
|
+
|--------|---------|----------|
|
|
277
|
+
| **Local file** | `"CONTRIBUTING.md"` | Reads the file relative to the project root |
|
|
278
|
+
| **Glob pattern** | `"docs/**/*.md"` | Expands the pattern, respects `.gitignore` |
|
|
279
|
+
| **Remote URL** | `"https://example.com/rules.md"` | Fetches via HTTP (5s timeout, cached per session) |
|
|
280
|
+
|
|
281
|
+
All resolved content is combined and appended to the agent's system prompt alongside `AGENTS.md`. Individual files are capped at 10KB, and the total combined size is capped at 50KB to prevent context bloat. Missing files and failed URL fetches are skipped with a warning.
|
|
282
|
+
|
|
258
283
|
### `.agents/` Directory
|
|
259
284
|
|
|
260
285
|
```
|
|
@@ -268,7 +293,7 @@ Place an `AGENTS.md` file in your project root with custom instructions that wil
|
|
|
268
293
|
└── SKILL.md
|
|
269
294
|
```
|
|
270
295
|
|
|
271
|
-
Command files support frontmatter with `description` and
|
|
296
|
+
Command files support frontmatter with `description`, `agent`, and `model` fields, plus OpenCode-style argument placeholders: `$ARGUMENTS` (full string), `$1`/`$2` (positional), and `$ARGUMENTS[N]` (0-indexed).
|
|
272
297
|
|
|
273
298
|
### Custom Agents
|
|
274
299
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.10.0"
|
|
@@ -8,7 +8,11 @@ from aru.providers import create_model
|
|
|
8
8
|
from aru.session import Session
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def create_general_agent(
|
|
11
|
+
def create_general_agent(
|
|
12
|
+
session: Session,
|
|
13
|
+
config: AgentConfig | None = None,
|
|
14
|
+
model_override: str | None = None,
|
|
15
|
+
):
|
|
12
16
|
"""Create the general-purpose agent."""
|
|
13
17
|
from agno.agent import Agent
|
|
14
18
|
from agno.compression.manager import CompressionManager
|
|
@@ -17,10 +21,11 @@ def create_general_agent(session: Session, config: AgentConfig | None = None):
|
|
|
17
21
|
from aru.runtime import get_ctx
|
|
18
22
|
|
|
19
23
|
extra = config.get_extra_instructions() if config else ""
|
|
24
|
+
model_ref = model_override or session.model_ref
|
|
20
25
|
|
|
21
26
|
return Agent(
|
|
22
27
|
name="Aru",
|
|
23
|
-
model=create_model(
|
|
28
|
+
model=create_model(model_ref, max_tokens=8192),
|
|
24
29
|
tools=GENERAL_TOOLS,
|
|
25
30
|
instructions=_build_instructions("general", extra),
|
|
26
31
|
markdown=True,
|
|
@@ -137,6 +137,8 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
137
137
|
console.print(f"[dim]Loaded {len(config.commands)} custom command(s): {', '.join(f'/{k}' for k in config.commands)}[/dim]")
|
|
138
138
|
if config.skills:
|
|
139
139
|
console.print(f"[dim]Loaded {len(config.skills)} skill(s): {', '.join(config.skills.keys())}[/dim]")
|
|
140
|
+
if config.rules_instructions:
|
|
141
|
+
console.print("[dim]Loaded custom instructions from aru.json[/dim]")
|
|
140
142
|
if config.custom_agents:
|
|
141
143
|
primary = [k for k, v in config.custom_agents.items() if v.mode == "primary"]
|
|
142
144
|
subagents = [k for k, v in config.custom_agents.items() if v.mode == "subagent"]
|
|
@@ -428,7 +430,16 @@ async def run_cli(skip_permissions: bool = False, resume_id: str | None = None):
|
|
|
428
430
|
prompt = render_command_template(cmd_def.template, cmd_args)
|
|
429
431
|
console.print(f"[bold magenta]Running /{cmd_name}...[/bold magenta]")
|
|
430
432
|
|
|
431
|
-
agent
|
|
433
|
+
if cmd_def.agent and cmd_def.agent in config.custom_agents:
|
|
434
|
+
agent_def = config.custom_agents[cmd_def.agent]
|
|
435
|
+
agent = create_custom_agent_instance(agent_def, session, config)
|
|
436
|
+
elif cmd_def.agent:
|
|
437
|
+
console.print(f"[yellow]Warning: agent '{cmd_def.agent}' not found, using default[/yellow]")
|
|
438
|
+
agent = create_general_agent(session, config, model_override=cmd_def.model)
|
|
439
|
+
elif cmd_def.model:
|
|
440
|
+
agent = create_general_agent(session, config, model_override=cmd_def.model)
|
|
441
|
+
else:
|
|
442
|
+
agent = create_general_agent(session, config)
|
|
432
443
|
session.add_message("user", user_input)
|
|
433
444
|
run_result = await run_agent_capture(agent, prompt, session)
|
|
434
445
|
if run_result.content:
|
|
@@ -10,12 +10,15 @@ Supports:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
|
+
import logging
|
|
13
14
|
import os
|
|
14
15
|
import re
|
|
15
16
|
from dataclasses import dataclass, field
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from typing import Any
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger("aru.config")
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
@dataclass
|
|
21
24
|
class CustomCommand:
|
|
@@ -24,6 +27,8 @@ class CustomCommand:
|
|
|
24
27
|
description: str
|
|
25
28
|
template: str
|
|
26
29
|
source_path: str
|
|
30
|
+
agent: str | None = None
|
|
31
|
+
model: str | None = None
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
@dataclass
|
|
@@ -54,6 +59,92 @@ class CustomAgent:
|
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
MAX_README_CHARS = 2000 # Reduced from 8000 to save ~1.7K tokens per request
|
|
62
|
+
MAX_RULE_FILE_SIZE = 10_000 # 10KB per rule file
|
|
63
|
+
URL_FETCH_TIMEOUT = 5 # seconds
|
|
64
|
+
MAX_TOTAL_RULES_SIZE = 50_000 # 50KB combined cap
|
|
65
|
+
|
|
66
|
+
# Module-level URL cache (session-scoped, persists for process lifetime)
|
|
67
|
+
_url_cache: dict[str, str | None] = {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_instructions(entries: list[str], root: Path) -> str:
|
|
71
|
+
"""Resolve instruction entries (local files, glob patterns, URLs) into combined text.
|
|
72
|
+
|
|
73
|
+
Each entry is classified as:
|
|
74
|
+
- URL: starts with http:// or https://
|
|
75
|
+
- Glob: contains *, ?, or [
|
|
76
|
+
- File path: everything else (resolved relative to root)
|
|
77
|
+
"""
|
|
78
|
+
from aru.tools.gitignore import is_ignored
|
|
79
|
+
|
|
80
|
+
parts: list[str] = []
|
|
81
|
+
total_size = 0
|
|
82
|
+
|
|
83
|
+
def _add_content(source: str, content: str) -> None:
|
|
84
|
+
nonlocal total_size
|
|
85
|
+
if not content.strip():
|
|
86
|
+
return
|
|
87
|
+
truncated = content[:MAX_RULE_FILE_SIZE]
|
|
88
|
+
if total_size + len(truncated) > MAX_TOTAL_RULES_SIZE:
|
|
89
|
+
remaining = MAX_TOTAL_RULES_SIZE - total_size
|
|
90
|
+
if remaining <= 0:
|
|
91
|
+
logger.warning("Total rules size cap reached, skipping: %s", source)
|
|
92
|
+
return
|
|
93
|
+
truncated = truncated[:remaining]
|
|
94
|
+
logger.warning("Total rules size cap reached, truncating: %s", source)
|
|
95
|
+
parts.append(f"## Rules: {source}\n\n{truncated}")
|
|
96
|
+
total_size += len(truncated)
|
|
97
|
+
|
|
98
|
+
def _read_file(filepath: Path, source_label: str) -> None:
|
|
99
|
+
try:
|
|
100
|
+
content = filepath.read_text(encoding="utf-8")
|
|
101
|
+
_add_content(source_label, content)
|
|
102
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
103
|
+
logger.warning("Failed to read instruction file %s: %s", filepath, exc)
|
|
104
|
+
|
|
105
|
+
for entry in entries:
|
|
106
|
+
if entry.startswith("http://") or entry.startswith("https://"):
|
|
107
|
+
# Remote URL
|
|
108
|
+
if entry in _url_cache:
|
|
109
|
+
cached = _url_cache[entry]
|
|
110
|
+
if cached is not None:
|
|
111
|
+
_add_content(entry, cached)
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
import httpx
|
|
115
|
+
with httpx.Client(timeout=URL_FETCH_TIMEOUT, follow_redirects=True) as client:
|
|
116
|
+
resp = client.get(entry)
|
|
117
|
+
resp.raise_for_status()
|
|
118
|
+
text = resp.text
|
|
119
|
+
_url_cache[entry] = text
|
|
120
|
+
_add_content(entry, text)
|
|
121
|
+
except Exception as exc:
|
|
122
|
+
_url_cache[entry] = None
|
|
123
|
+
logger.warning("Failed to fetch instruction URL %s: %s", entry, exc)
|
|
124
|
+
|
|
125
|
+
elif any(c in entry for c in ("*", "?", "[")):
|
|
126
|
+
# Glob pattern
|
|
127
|
+
matched = sorted(root.glob(entry))
|
|
128
|
+
for filepath in matched:
|
|
129
|
+
if not filepath.is_file():
|
|
130
|
+
continue
|
|
131
|
+
try:
|
|
132
|
+
rel = filepath.relative_to(root)
|
|
133
|
+
except ValueError:
|
|
134
|
+
continue
|
|
135
|
+
if is_ignored(str(rel), str(root)):
|
|
136
|
+
continue
|
|
137
|
+
_read_file(filepath, str(rel))
|
|
138
|
+
|
|
139
|
+
else:
|
|
140
|
+
# Local file path
|
|
141
|
+
filepath = root / entry
|
|
142
|
+
if filepath.is_file():
|
|
143
|
+
_read_file(filepath, entry)
|
|
144
|
+
else:
|
|
145
|
+
logger.warning("Instruction file not found: %s", filepath)
|
|
146
|
+
|
|
147
|
+
return "\n\n".join(parts)
|
|
57
148
|
|
|
58
149
|
|
|
59
150
|
@dataclass
|
|
@@ -61,6 +152,7 @@ class AgentConfig:
|
|
|
61
152
|
"""Loaded configuration from AGENTS.md, README.md, and .agents/ directory."""
|
|
62
153
|
readme_md: str = ""
|
|
63
154
|
agents_md: str = ""
|
|
155
|
+
rules_instructions: str = ""
|
|
64
156
|
commands: dict[str, CustomCommand] = field(default_factory=dict)
|
|
65
157
|
skills: dict[str, Skill] = field(default_factory=dict)
|
|
66
158
|
permissions: dict[str, Any] = field(default_factory=dict)
|
|
@@ -71,7 +163,7 @@ class AgentConfig:
|
|
|
71
163
|
|
|
72
164
|
@property
|
|
73
165
|
def has_instructions(self) -> bool:
|
|
74
|
-
return bool(self.agents_md) or bool(self.skills)
|
|
166
|
+
return bool(self.agents_md) or bool(self.skills) or bool(self.rules_instructions)
|
|
75
167
|
|
|
76
168
|
def get_extra_instructions(self, active_skills: list[str] | None = None, lightweight: bool = False) -> str:
|
|
77
169
|
"""Build extra instructions from README.md, AGENTS.md, and active skills.
|
|
@@ -85,6 +177,8 @@ class AgentConfig:
|
|
|
85
177
|
parts.append(f"## Project Overview (README.md)\n\n{self.readme_md}")
|
|
86
178
|
if self.agents_md:
|
|
87
179
|
parts.append(f"## Project Instructions (AGENTS.md)\n\n{self.agents_md}")
|
|
180
|
+
if self.rules_instructions:
|
|
181
|
+
parts.append(self.rules_instructions)
|
|
88
182
|
if active_skills:
|
|
89
183
|
for name in active_skills:
|
|
90
184
|
if name in self.skills:
|
|
@@ -233,6 +327,8 @@ def _load_commands(agents_dir: Path) -> dict[str, CustomCommand]:
|
|
|
233
327
|
description=description,
|
|
234
328
|
template=body,
|
|
235
329
|
source_path=str(filepath),
|
|
330
|
+
agent=metadata.get("agent") or None,
|
|
331
|
+
model=metadata.get("model") or None,
|
|
236
332
|
)
|
|
237
333
|
|
|
238
334
|
return commands
|
|
@@ -409,6 +505,10 @@ def load_config(cwd: str | None = None) -> AgentConfig:
|
|
|
409
505
|
config.model_aliases = data["model_aliases"]
|
|
410
506
|
if "plan_reviewer" in data:
|
|
411
507
|
config.plan_reviewer = bool(data["plan_reviewer"])
|
|
508
|
+
# Resolve instructions (local files, globs, URLs)
|
|
509
|
+
if "instructions" in data and isinstance(data["instructions"], list):
|
|
510
|
+
entries = [str(e) for e in data["instructions"] if isinstance(e, str)]
|
|
511
|
+
config.rules_instructions = _resolve_instructions(entries, root)
|
|
412
512
|
# Agent-level permission overrides from aru.json
|
|
413
513
|
if "agent" in data and isinstance(data["agent"], dict):
|
|
414
514
|
for agent_name, agent_data in data["agent"].items():
|
|
@@ -422,26 +522,17 @@ def load_config(cwd: str | None = None) -> AgentConfig:
|
|
|
422
522
|
return config
|
|
423
523
|
|
|
424
524
|
|
|
425
|
-
def
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
Also supports $SELECTION (empty if not provided) for future use.
|
|
430
|
-
"""
|
|
431
|
-
result = template.replace("$INPUT", user_input)
|
|
432
|
-
result = result.replace("$SELECTION", "")
|
|
433
|
-
return result
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def render_skill_template(content: str, arguments: str) -> str:
|
|
437
|
-
"""Render a skill template with argument substitution (agentskills.io).
|
|
525
|
+
def render_template_arguments(
|
|
526
|
+
content: str, arguments: str, *, context_label: str = "Argument",
|
|
527
|
+
) -> str:
|
|
528
|
+
"""Render a template with $ARGUMENTS / $1 / $2 substitution.
|
|
438
529
|
|
|
439
530
|
Supports:
|
|
440
531
|
- $ARGUMENTS: Full argument string
|
|
441
532
|
- $ARGUMENTS[N]: Nth argument (0-indexed)
|
|
442
533
|
- $1, $2, ...: Nth argument (1-indexed, shell-style)
|
|
443
534
|
|
|
444
|
-
Also prepends an explicit
|
|
535
|
+
Also prepends an explicit context block so the agent cannot
|
|
445
536
|
miss or misread the user-supplied value.
|
|
446
537
|
"""
|
|
447
538
|
parts = arguments.split() if arguments else []
|
|
@@ -465,7 +556,17 @@ def render_skill_template(content: str, arguments: str) -> str:
|
|
|
465
556
|
|
|
466
557
|
# Prepend an explicit context block so the agent cannot miss the argument
|
|
467
558
|
if arguments and arguments.strip():
|
|
468
|
-
header = f"> **
|
|
559
|
+
header = f"> **{context_label}:** `{arguments.strip()}`\n> Use this value exactly where the instructions reference the argument.\n\n"
|
|
469
560
|
result = header + result
|
|
470
561
|
|
|
471
562
|
return result
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def render_command_template(template: str, user_input: str) -> str:
|
|
566
|
+
"""Render a command template with OpenCode-style argument substitution."""
|
|
567
|
+
return render_template_arguments(template, user_input, context_label="Command argument")
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def render_skill_template(content: str, arguments: str) -> str:
|
|
571
|
+
"""Render a skill template with argument substitution (agentskills.io)."""
|
|
572
|
+
return render_template_arguments(content, arguments, context_label="Skill argument")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aru-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: A Claude Code clone built with Agno agents
|
|
5
5
|
Author-email: Estevao <estevaofon@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -302,6 +302,31 @@ Without any `aru.json` config, aru applies safe defaults:
|
|
|
302
302
|
|
|
303
303
|
Place an `AGENTS.md` file in your project root with custom instructions that will be appended to all agent system prompts.
|
|
304
304
|
|
|
305
|
+
### Instructions (Rules)
|
|
306
|
+
|
|
307
|
+
You can load additional instructions from local files, glob patterns, or remote URLs via the `instructions` field in `aru.json`:
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"instructions": [
|
|
312
|
+
"CONTRIBUTING.md",
|
|
313
|
+
"docs/coding-standards.md",
|
|
314
|
+
"packages/*/AGENTS.md",
|
|
315
|
+
"https://raw.githubusercontent.com/my-org/shared-rules/main/style.md"
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Each entry is resolved as follows:
|
|
321
|
+
|
|
322
|
+
| Format | Example | Behavior |
|
|
323
|
+
|--------|---------|----------|
|
|
324
|
+
| **Local file** | `"CONTRIBUTING.md"` | Reads the file relative to the project root |
|
|
325
|
+
| **Glob pattern** | `"docs/**/*.md"` | Expands the pattern, respects `.gitignore` |
|
|
326
|
+
| **Remote URL** | `"https://example.com/rules.md"` | Fetches via HTTP (5s timeout, cached per session) |
|
|
327
|
+
|
|
328
|
+
All resolved content is combined and appended to the agent's system prompt alongside `AGENTS.md`. Individual files are capped at 10KB, and the total combined size is capped at 50KB to prevent context bloat. Missing files and failed URL fetches are skipped with a warning.
|
|
329
|
+
|
|
305
330
|
### `.agents/` Directory
|
|
306
331
|
|
|
307
332
|
```
|
|
@@ -315,7 +340,7 @@ Place an `AGENTS.md` file in your project root with custom instructions that wil
|
|
|
315
340
|
└── SKILL.md
|
|
316
341
|
```
|
|
317
342
|
|
|
318
|
-
Command files support frontmatter with `description` and
|
|
343
|
+
Command files support frontmatter with `description`, `agent`, and `model` fields, plus OpenCode-style argument placeholders: `$ARGUMENTS` (full string), `$1`/`$2` (positional), and `$ARGUMENTS[N]` (0-indexed).
|
|
319
344
|
|
|
320
345
|
### Custom Agents
|
|
321
346
|
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
"""Unit tests for aru.config module."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import pytest
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
5
7
|
from aru.config import (
|
|
6
8
|
AgentConfig,
|
|
7
9
|
CustomAgent,
|
|
8
10
|
CustomCommand,
|
|
11
|
+
MAX_RULE_FILE_SIZE,
|
|
12
|
+
MAX_TOTAL_RULES_SIZE,
|
|
9
13
|
Skill,
|
|
10
14
|
_discover_agents,
|
|
11
15
|
_parse_agent_metadata,
|
|
12
16
|
_parse_frontmatter,
|
|
13
17
|
_parse_skill_metadata,
|
|
18
|
+
_resolve_instructions,
|
|
19
|
+
_url_cache,
|
|
14
20
|
render_command_template,
|
|
15
21
|
render_skill_template,
|
|
22
|
+
render_template_arguments,
|
|
16
23
|
load_config,
|
|
17
24
|
MAX_README_CHARS,
|
|
18
25
|
)
|
|
@@ -25,13 +32,27 @@ class TestDataClasses:
|
|
|
25
32
|
cmd = CustomCommand(
|
|
26
33
|
name="test",
|
|
27
34
|
description="Test command",
|
|
28
|
-
template="Do $
|
|
35
|
+
template="Do $ARGUMENTS",
|
|
29
36
|
source_path="/path/to/test.md",
|
|
30
37
|
)
|
|
31
38
|
assert cmd.name == "test"
|
|
32
39
|
assert cmd.description == "Test command"
|
|
33
|
-
assert cmd.template == "Do $
|
|
40
|
+
assert cmd.template == "Do $ARGUMENTS"
|
|
34
41
|
assert cmd.source_path == "/path/to/test.md"
|
|
42
|
+
assert cmd.agent is None
|
|
43
|
+
assert cmd.model is None
|
|
44
|
+
|
|
45
|
+
def test_custom_command_with_agent_and_model(self):
|
|
46
|
+
cmd = CustomCommand(
|
|
47
|
+
name="review",
|
|
48
|
+
description="Review code",
|
|
49
|
+
template="Review $ARGUMENTS",
|
|
50
|
+
source_path="/path/to/review.md",
|
|
51
|
+
agent="reviewer",
|
|
52
|
+
model="anthropic/claude-sonnet-4-5",
|
|
53
|
+
)
|
|
54
|
+
assert cmd.agent == "reviewer"
|
|
55
|
+
assert cmd.model == "anthropic/claude-sonnet-4-5"
|
|
35
56
|
|
|
36
57
|
def test_skill_creation(self):
|
|
37
58
|
skill = Skill(
|
|
@@ -192,37 +213,57 @@ class TestParseFrontmatter:
|
|
|
192
213
|
|
|
193
214
|
|
|
194
215
|
class TestRenderCommandTemplate:
|
|
195
|
-
"""Test command template rendering."""
|
|
216
|
+
"""Test command template rendering with OpenCode-style arguments."""
|
|
196
217
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
assert result == "Execute refactoring in the codebase"
|
|
218
|
+
def test_render_with_arguments(self):
|
|
219
|
+
result = render_command_template("Execute $ARGUMENTS in the codebase", "refactoring")
|
|
220
|
+
assert "Execute refactoring in the codebase" in result
|
|
201
221
|
|
|
202
|
-
def
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
assert result == "Run tests and verify tests works"
|
|
222
|
+
def test_render_positional_args(self):
|
|
223
|
+
result = render_command_template("File: $1, Line: $2", "main.py 42")
|
|
224
|
+
assert "File: main.py, Line: 42" in result
|
|
206
225
|
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
assert result == "Process data and "
|
|
226
|
+
def test_render_indexed_args(self):
|
|
227
|
+
result = render_command_template("$ARGUMENTS[0] and $ARGUMENTS[1]", "foo bar")
|
|
228
|
+
assert "foo and bar" in result
|
|
211
229
|
|
|
212
230
|
def test_render_no_placeholders(self):
|
|
213
231
|
template = "Just plain text"
|
|
214
232
|
result = render_command_template(template, "input")
|
|
215
|
-
|
|
233
|
+
# Header is prepended but template body is unchanged
|
|
234
|
+
assert "Just plain text" in result
|
|
216
235
|
|
|
217
236
|
def test_render_empty_input(self):
|
|
218
|
-
|
|
219
|
-
result = render_command_template(template, "")
|
|
237
|
+
result = render_command_template("Do $ARGUMENTS now", "")
|
|
220
238
|
assert result == "Do now"
|
|
221
239
|
|
|
222
240
|
def test_render_with_special_characters(self):
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
241
|
+
result = render_command_template("Run $ARGUMENTS", "pytest -v --cov")
|
|
242
|
+
assert "Run pytest -v --cov" in result
|
|
243
|
+
|
|
244
|
+
def test_render_missing_positional(self):
|
|
245
|
+
result = render_command_template("$1 and $3", "first second")
|
|
246
|
+
assert "first and " in result
|
|
247
|
+
|
|
248
|
+
def test_command_context_header(self):
|
|
249
|
+
result = render_command_template("Do $ARGUMENTS", "something")
|
|
250
|
+
assert result.startswith("> **Command argument:**")
|
|
251
|
+
|
|
252
|
+
def test_no_header_when_empty_args(self):
|
|
253
|
+
result = render_command_template("Do $ARGUMENTS", "")
|
|
254
|
+
assert not result.startswith(">")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class TestRenderTemplateArguments:
|
|
258
|
+
"""Test the shared render_template_arguments function."""
|
|
259
|
+
|
|
260
|
+
def test_custom_context_label(self):
|
|
261
|
+
result = render_template_arguments("Do $ARGUMENTS", "test", context_label="Custom label")
|
|
262
|
+
assert "> **Custom label:** `test`" in result
|
|
263
|
+
|
|
264
|
+
def test_delegates_correctly(self):
|
|
265
|
+
result = render_template_arguments("$1 + $2 = $ARGUMENTS", "a b")
|
|
266
|
+
assert "a + b = a b" in result
|
|
226
267
|
|
|
227
268
|
|
|
228
269
|
class TestLoadConfig:
|
|
@@ -268,16 +309,32 @@ class TestLoadConfig:
|
|
|
268
309
|
def test_load_config_with_commands(self, tmp_path):
|
|
269
310
|
commands_dir = tmp_path / ".agents" / "commands"
|
|
270
311
|
commands_dir.mkdir(parents=True)
|
|
271
|
-
|
|
312
|
+
|
|
272
313
|
cmd_file = commands_dir / "deploy.md"
|
|
273
|
-
cmd_file.write_text("---\ndescription: Deploy the app\n---\nDeploy $
|
|
274
|
-
|
|
314
|
+
cmd_file.write_text("---\ndescription: Deploy the app\n---\nDeploy $ARGUMENTS")
|
|
315
|
+
|
|
275
316
|
config = load_config(str(tmp_path))
|
|
276
317
|
assert "deploy" in config.commands
|
|
277
318
|
cmd = config.commands["deploy"]
|
|
278
319
|
assert cmd.name == "deploy"
|
|
279
320
|
assert cmd.description == "Deploy the app"
|
|
280
|
-
assert cmd.template == "Deploy $
|
|
321
|
+
assert cmd.template == "Deploy $ARGUMENTS"
|
|
322
|
+
assert cmd.agent is None
|
|
323
|
+
assert cmd.model is None
|
|
324
|
+
|
|
325
|
+
def test_load_config_command_with_agent_and_model(self, tmp_path):
|
|
326
|
+
commands_dir = tmp_path / ".agents" / "commands"
|
|
327
|
+
commands_dir.mkdir(parents=True)
|
|
328
|
+
|
|
329
|
+
cmd_file = commands_dir / "review.md"
|
|
330
|
+
cmd_file.write_text(
|
|
331
|
+
"---\ndescription: Review code\nagent: reviewer\nmodel: anthropic/claude-sonnet-4-5\n---\nReview $ARGUMENTS"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
config = load_config(str(tmp_path))
|
|
335
|
+
cmd = config.commands["review"]
|
|
336
|
+
assert cmd.agent == "reviewer"
|
|
337
|
+
assert cmd.model == "anthropic/claude-sonnet-4-5"
|
|
281
338
|
|
|
282
339
|
def test_load_config_command_without_frontmatter(self, tmp_path):
|
|
283
340
|
commands_dir = tmp_path / ".agents" / "commands"
|
|
@@ -403,7 +460,7 @@ class TestLoadConfig:
|
|
|
403
460
|
|
|
404
461
|
commands_dir = tmp_path / ".agents" / "commands"
|
|
405
462
|
commands_dir.mkdir(parents=True)
|
|
406
|
-
(commands_dir / "test.md").write_text("---\ndescription: Test\n---\nRun $
|
|
463
|
+
(commands_dir / "test.md").write_text("---\ndescription: Test\n---\nRun $ARGUMENTS")
|
|
407
464
|
|
|
408
465
|
skill_dir = tmp_path / ".agents" / "skills" / "review"
|
|
409
466
|
skill_dir.mkdir(parents=True)
|
|
@@ -523,7 +580,7 @@ class TestParseSkillMetadata:
|
|
|
523
580
|
class TestRenderSkillTemplate:
|
|
524
581
|
"""Test render_skill_template with argument substitution."""
|
|
525
582
|
|
|
526
|
-
_HEADER = "> **Skill argument:** `{arg}`\n> Use this value exactly where the
|
|
583
|
+
_HEADER = "> **Skill argument:** `{arg}`\n> Use this value exactly where the instructions reference the argument.\n\n"
|
|
527
584
|
|
|
528
585
|
def test_arguments_substitution(self):
|
|
529
586
|
result = render_skill_template("Review $ARGUMENTS carefully", "src/main.py")
|
|
@@ -801,4 +858,150 @@ class TestAgentPermissions:
|
|
|
801
858
|
}))
|
|
802
859
|
config = load_config(str(tmp_path))
|
|
803
860
|
# aru.json override should win
|
|
804
|
-
assert config.custom_agents["worker"].permission == {"edit": "deny"}
|
|
861
|
+
assert config.custom_agents["worker"].permission == {"edit": "deny"}
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
class TestInstructionsLoading:
|
|
865
|
+
"""Test the instructions field in aru.json (rules system)."""
|
|
866
|
+
|
|
867
|
+
def setup_method(self):
|
|
868
|
+
"""Clear URL cache before each test."""
|
|
869
|
+
_url_cache.clear()
|
|
870
|
+
|
|
871
|
+
def test_instructions_local_file(self, tmp_path):
|
|
872
|
+
(tmp_path / "CONTRIBUTING.md").write_text("Follow these contributing guidelines.")
|
|
873
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
874
|
+
"instructions": ["CONTRIBUTING.md"]
|
|
875
|
+
}))
|
|
876
|
+
config = load_config(str(tmp_path))
|
|
877
|
+
assert "Follow these contributing guidelines." in config.rules_instructions
|
|
878
|
+
assert "## Rules: CONTRIBUTING.md" in config.rules_instructions
|
|
879
|
+
|
|
880
|
+
def test_instructions_glob_pattern(self, tmp_path):
|
|
881
|
+
docs = tmp_path / "docs"
|
|
882
|
+
docs.mkdir()
|
|
883
|
+
(docs / "style.md").write_text("Style guide content")
|
|
884
|
+
(docs / "api.md").write_text("API guidelines")
|
|
885
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
886
|
+
"instructions": ["docs/*.md"]
|
|
887
|
+
}))
|
|
888
|
+
config = load_config(str(tmp_path))
|
|
889
|
+
assert "Style guide content" in config.rules_instructions
|
|
890
|
+
assert "API guidelines" in config.rules_instructions
|
|
891
|
+
|
|
892
|
+
def test_instructions_missing_file(self, tmp_path):
|
|
893
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
894
|
+
"instructions": ["nonexistent.md"]
|
|
895
|
+
}))
|
|
896
|
+
config = load_config(str(tmp_path))
|
|
897
|
+
assert config.rules_instructions == ""
|
|
898
|
+
|
|
899
|
+
def test_instructions_file_size_cap(self, tmp_path):
|
|
900
|
+
large_content = "x" * (MAX_RULE_FILE_SIZE + 5000)
|
|
901
|
+
(tmp_path / "large.md").write_text(large_content)
|
|
902
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
903
|
+
"instructions": ["large.md"]
|
|
904
|
+
}))
|
|
905
|
+
config = load_config(str(tmp_path))
|
|
906
|
+
# Header + content, content should be truncated
|
|
907
|
+
assert len(config.rules_instructions) < MAX_RULE_FILE_SIZE + 200 # header overhead
|
|
908
|
+
|
|
909
|
+
def test_instructions_url(self, tmp_path):
|
|
910
|
+
mock_response = MagicMock()
|
|
911
|
+
mock_response.text = "Remote rule content"
|
|
912
|
+
mock_response.raise_for_status = MagicMock()
|
|
913
|
+
|
|
914
|
+
mock_client = MagicMock()
|
|
915
|
+
mock_client.__enter__ = MagicMock(return_value=mock_client)
|
|
916
|
+
mock_client.__exit__ = MagicMock(return_value=False)
|
|
917
|
+
mock_client.get.return_value = mock_response
|
|
918
|
+
|
|
919
|
+
with patch("httpx.Client", return_value=mock_client):
|
|
920
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
921
|
+
"instructions": ["https://example.com/rules.md"]
|
|
922
|
+
}))
|
|
923
|
+
config = load_config(str(tmp_path))
|
|
924
|
+
assert "Remote rule content" in config.rules_instructions
|
|
925
|
+
|
|
926
|
+
def test_instructions_url_timeout(self, tmp_path):
|
|
927
|
+
import httpx
|
|
928
|
+
|
|
929
|
+
mock_client = MagicMock()
|
|
930
|
+
mock_client.__enter__ = MagicMock(return_value=mock_client)
|
|
931
|
+
mock_client.__exit__ = MagicMock(return_value=False)
|
|
932
|
+
mock_client.get.side_effect = httpx.ConnectTimeout("timeout")
|
|
933
|
+
|
|
934
|
+
with patch("httpx.Client", return_value=mock_client):
|
|
935
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
936
|
+
"instructions": ["https://example.com/timeout.md"]
|
|
937
|
+
}))
|
|
938
|
+
config = load_config(str(tmp_path))
|
|
939
|
+
assert config.rules_instructions == ""
|
|
940
|
+
|
|
941
|
+
def test_instructions_combined_in_extra(self, tmp_path):
|
|
942
|
+
(tmp_path / "rules.md").write_text("Custom rule")
|
|
943
|
+
(tmp_path / "AGENTS.md").write_text("Agent instructions")
|
|
944
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
945
|
+
"instructions": ["rules.md"]
|
|
946
|
+
}))
|
|
947
|
+
config = load_config(str(tmp_path))
|
|
948
|
+
extra = config.get_extra_instructions()
|
|
949
|
+
# Both AGENTS.md and rules should be present
|
|
950
|
+
assert "Agent instructions" in extra
|
|
951
|
+
assert "Custom rule" in extra
|
|
952
|
+
# Rules should come after AGENTS.md
|
|
953
|
+
agents_pos = extra.find("Agent instructions")
|
|
954
|
+
rules_pos = extra.find("Custom rule")
|
|
955
|
+
assert agents_pos < rules_pos
|
|
956
|
+
|
|
957
|
+
def test_instructions_total_size_cap(self, tmp_path):
|
|
958
|
+
# Create files that together exceed MAX_TOTAL_RULES_SIZE
|
|
959
|
+
docs = tmp_path / "docs"
|
|
960
|
+
docs.mkdir()
|
|
961
|
+
chunk = "y" * (MAX_RULE_FILE_SIZE - 100)
|
|
962
|
+
num_files = (MAX_TOTAL_RULES_SIZE // MAX_RULE_FILE_SIZE) + 3
|
|
963
|
+
for i in range(num_files):
|
|
964
|
+
(docs / f"rule{i:02d}.md").write_text(chunk)
|
|
965
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
966
|
+
"instructions": ["docs/*.md"]
|
|
967
|
+
}))
|
|
968
|
+
config = load_config(str(tmp_path))
|
|
969
|
+
assert len(config.rules_instructions) <= MAX_TOTAL_RULES_SIZE + 5000 # headers overhead
|
|
970
|
+
|
|
971
|
+
def test_instructions_empty_list(self, tmp_path):
|
|
972
|
+
(tmp_path / "aru.json").write_text(json.dumps({
|
|
973
|
+
"instructions": []
|
|
974
|
+
}))
|
|
975
|
+
config = load_config(str(tmp_path))
|
|
976
|
+
assert config.rules_instructions == ""
|
|
977
|
+
|
|
978
|
+
def test_instructions_has_instructions_property(self):
|
|
979
|
+
config = AgentConfig(rules_instructions="some rules")
|
|
980
|
+
assert config.has_instructions is True
|
|
981
|
+
|
|
982
|
+
def test_resolve_instructions_directly(self, tmp_path):
|
|
983
|
+
(tmp_path / "a.md").write_text("Content A")
|
|
984
|
+
(tmp_path / "b.md").write_text("Content B")
|
|
985
|
+
result = _resolve_instructions(["a.md", "b.md"], tmp_path)
|
|
986
|
+
assert "Content A" in result
|
|
987
|
+
assert "Content B" in result
|
|
988
|
+
|
|
989
|
+
def test_instructions_url_caching(self, tmp_path):
|
|
990
|
+
"""Second call should use cache, not fetch again."""
|
|
991
|
+
mock_response = MagicMock()
|
|
992
|
+
mock_response.text = "Cached content"
|
|
993
|
+
mock_response.raise_for_status = MagicMock()
|
|
994
|
+
|
|
995
|
+
mock_client = MagicMock()
|
|
996
|
+
mock_client.__enter__ = MagicMock(return_value=mock_client)
|
|
997
|
+
mock_client.__exit__ = MagicMock(return_value=False)
|
|
998
|
+
mock_client.get.return_value = mock_response
|
|
999
|
+
|
|
1000
|
+
url = "https://example.com/cached.md"
|
|
1001
|
+
with patch("httpx.Client", return_value=mock_client):
|
|
1002
|
+
result1 = _resolve_instructions([url], tmp_path)
|
|
1003
|
+
result2 = _resolve_instructions([url], tmp_path)
|
|
1004
|
+
assert "Cached content" in result1
|
|
1005
|
+
assert "Cached content" in result2
|
|
1006
|
+
# httpx.Client should only be called once (second uses cache)
|
|
1007
|
+
assert mock_client.get.call_count == 1
|
aru_code-0.8.0/aru/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.8.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|