Skip to content

Commit 81f7c75

Browse files
committed
feat: token-efficient script/automation tools and YAML helpers
1 parent adb7cf6 commit 81f7c75

6 files changed

Lines changed: 175 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [2.10.9] - 2026-01-20
6+
7+
### ✨ Token‑efficient access to scripts and automations
8+
9+
**Focused YAML access to save tokens and speed up AI workflows**
10+
11+
-**Lightweight listing endpoints**: Added support for listing script IDs and automation IDs without returning full YAML bodies, so AI tools can cheaply discover what exists before deciding what to open
12+
-**Single‑entity configuration fetch**: New read‑only endpoints return configuration for a single script or automation by its ID, instead of loading entire `scripts.yaml` / `automations.yaml`
13+
-**Token and context savings**: Designed specifically to reduce prompt/context size and token usage when working with large YAML files, especially in conversational AI scenarios
14+
-**Faster request handling**: By focusing responses on the one script/automation you are currently working with, the agent can answer faster and IDEs can keep conversations narrow and relevant
15+
516
## [2.10.8] - 2025-12-18
617

718
### 🔧 Git Versioning: Auto/Manual Mode & Shadow Repository

app/api/addons.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,16 @@ async def add_repository(request: RepositoryRequest):
495495
data={'repository_url': request.repository_url}
496496
)
497497
except Exception as e:
498+
error_msg = str(e)
499+
# Extract more user-friendly error message from Supervisor API response
500+
if "git clone" in error_msg or "fatal:" in error_msg:
501+
if "could not read Username" in error_msg or "No such device or address" in error_msg:
502+
error_msg = f"Repository not found or not accessible: {request.repository_url}. Please check that the repository exists and is publicly accessible."
503+
elif "repository not found" in error_msg.lower():
504+
error_msg = f"Repository not found: {request.repository_url}. Please verify the repository URL is correct."
505+
elif "authentication" in error_msg.lower() or "permission" in error_msg.lower():
506+
error_msg = f"Authentication failed for repository: {request.repository_url}. Private repositories are not supported."
507+
498508
logger.error(f"Error adding repository {request.repository_url}: {e}")
499-
return Response(success=False, message=f"Failed to add repository: {str(e)}")
509+
return Response(success=False, message=f"Failed to add repository: {error_msg}")
500510

app/api/automations.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,108 @@
1414
logger = logging.getLogger('ha_cursor_agent')
1515

1616
@router.get("/list")
17-
async def list_automations():
17+
async def list_automations(ids_only: bool = Query(False, description="If true, return only automation IDs without full configurations")):
1818
"""
19-
List all automations
19+
List all automations from automations.yaml
2020
21-
Returns automations from automations.yaml
21+
**Parameters:**
22+
- `ids_only` (optional): If `true`, returns only list of automation IDs. If `false` (default), returns full automation configurations.
23+
24+
**Example response (ids_only=false):**
25+
```json
26+
{
27+
"success": true,
28+
"count": 2,
29+
"automations": [
30+
{"id": "my_automation", "alias": "...", "trigger": [...]},
31+
{"id": "another", ...}
32+
]
33+
}
34+
```
35+
36+
**Example response (ids_only=true):**
37+
```json
38+
{
39+
"success": true,
40+
"count": 2,
41+
"automation_ids": ["my_automation", "another"]
42+
}
43+
```
2244
"""
2345
try:
2446
# Read automations.yaml
2547
content = await file_manager.read_file('automations.yaml')
2648
automations = yaml.safe_load(content) or []
2749

50+
if ids_only:
51+
# Extract IDs from automations list
52+
automation_ids = [a.get('id') for a in automations if a.get('id')]
53+
return {
54+
"success": True,
55+
"count": len(automation_ids),
56+
"automation_ids": automation_ids
57+
}
58+
2859
return {
2960
"success": True,
3061
"count": len(automations),
3162
"automations": automations
3263
}
3364
except FileNotFoundError:
65+
if ids_only:
66+
return {"success": True, "count": 0, "automation_ids": []}
3467
return {"success": True, "count": 0, "automations": []}
3568
except Exception as e:
3669
logger.error(f"Failed to list automations: {e}")
3770
raise HTTPException(status_code=500, detail=str(e))
3871

72+
73+
@router.get("/get/{automation_id}")
74+
async def get_automation_config(automation_id: str):
75+
"""
76+
Get configuration for a single automation.
77+
78+
Returns the YAML configuration object for a specific automation_id from automations.yaml.
79+
80+
**Example response:**
81+
```json
82+
{
83+
"success": true,
84+
"automation_id": "my_automation",
85+
"config": {
86+
"id": "my_automation",
87+
"alias": "My Automation",
88+
"trigger": [...],
89+
"condition": [...],
90+
"action": [...],
91+
"mode": "single"
92+
}
93+
}
94+
```
95+
"""
96+
try:
97+
# Read automations.yaml
98+
content = await file_manager.read_file('automations.yaml')
99+
automations = yaml.safe_load(content) or []
100+
101+
# Find automation by id
102+
for automation in automations:
103+
if automation.get('id') == automation_id:
104+
return {
105+
"success": True,
106+
"automation_id": automation_id,
107+
"config": automation,
108+
}
109+
110+
raise HTTPException(status_code=404, detail=f"Automation not found: {automation_id}")
111+
except HTTPException:
112+
raise
113+
except FileNotFoundError:
114+
raise HTTPException(status_code=404, detail=f"Automation not found: {automation_id}")
115+
except Exception as e:
116+
logger.error(f"Failed to get automation {automation_id}: {e}")
117+
raise HTTPException(status_code=500, detail=str(e))
118+
39119
@router.post("/create", response_model=Response)
40120
async def create_automation(automation: AutomationData):
41121
"""

app/api/helpers.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,22 @@
1515

1616
CONFIG_FILE = "/config/configuration.yaml"
1717

18-
# Each helper type gets its own file
18+
# Each helper-type domain gets its own YAML file which is referenced from configuration.yaml via:
19+
# <domain>: !include <file>
20+
# For example:
21+
# group: !include groups.yaml
22+
# utility_meter: !include utility_meter.yaml
1923
HELPER_FILES = {
2024
'input_boolean': '/config/input_boolean.yaml',
2125
'input_text': '/config/input_text.yaml',
2226
'input_number': '/config/input_number.yaml',
2327
'input_datetime': '/config/input_datetime.yaml',
24-
'input_select': '/config/input_select.yaml'
28+
'input_select': '/config/input_select.yaml',
29+
# YAML-based helpers that behave similarly to input_* from AI perspective
30+
# These are not "input helpers" in HA UI, but are commonly used as building blocks
31+
# and can be safely managed via YAML with the same pattern.
32+
'group': '/config/groups.yaml',
33+
'utility_meter': '/config/utility_meter.yaml',
2534
}
2635

2736

@@ -97,7 +106,7 @@ async def debug_services():
97106

98107
# Extract helper-related services
99108
helper_services = {}
100-
for domain in ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select']:
109+
for domain in ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select', 'group', 'utility_meter']:
101110
if domain in all_services:
102111
helper_services[domain] = all_services[domain]
103112

@@ -112,14 +121,16 @@ async def debug_services():
112121
@router.get("/list")
113122
async def list_helpers():
114123
"""
115-
List all input helpers
124+
List all helper-like entities
116125
117-
Returns all entities from helper domains:
126+
Returns all entities from helper-related domains:
118127
- input_boolean
119128
- input_text
120129
- input_number
121130
- input_datetime
122131
- input_select
132+
- group
133+
- utility_meter
123134
124135
Example response:
125136
```json
@@ -140,8 +151,8 @@ async def list_helpers():
140151
# Get all entities
141152
all_states = await ha_client.get_states()
142153

143-
# Filter helper entities
144-
helper_domains = ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select']
154+
# Filter helper-like entities
155+
helper_domains = ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select', 'group', 'utility_meter']
145156
helpers = [
146157
entity for entity in all_states
147158
if any(entity['entity_id'].startswith(f"{domain}.") for domain in helper_domains)
@@ -161,16 +172,18 @@ async def list_helpers():
161172
@router.post("/create", response_model=Response)
162173
async def create_helper(helper: HelperCreate):
163174
"""
164-
Create input helper via YAML configuration
175+
Create helper via YAML configuration
165176
166-
**Method:** Writes to helpers.yaml and reloads the integration
177+
**Method:** Writes to dedicated YAML file per domain and reloads the integration
167178
168-
**Helper types:**
179+
**Helper types (YAML-managed):**
169180
- `input_boolean` - Toggle/switch
170181
- `input_text` - Text input
171182
- `input_number` - Number slider
172183
- `input_datetime` - Date/time picker
173184
- `input_select` - Dropdown selection
185+
- `group` - Entity groups (living_room_lights, etc.)
186+
- `utility_meter` - Utility meter tracking (daily_energy, monthly_gas, etc.)
174187
175188
**Example request (Boolean):**
176189
```json
@@ -201,11 +214,11 @@ async def create_helper(helper: HelperCreate):
201214
"""
202215
try:
203216
# Validate helper type
204-
valid_types = ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select']
217+
valid_types = ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select', 'group', 'utility_meter']
205218
if helper.type not in valid_types:
206219
raise HTTPException(status_code=400, detail=f"Invalid helper type. Must be one of: {', '.join(valid_types)}")
207220

208-
# Extract name from config (required)
221+
# Extract name from config (required for all supported helper-like types)
209222
if 'name' not in helper.config:
210223
raise HTTPException(status_code=400, detail="config must include 'name' field")
211224

@@ -276,7 +289,7 @@ async def delete_helper(entity_id: str, commit_message: Optional[str] = Query(No
276289
domain, helper_id = entity_id.split('.', 1)
277290

278291
# Validate domain
279-
valid_types = ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select']
292+
valid_types = ['input_boolean', 'input_text', 'input_number', 'input_datetime', 'input_select', 'group', 'utility_meter']
280293
if domain not in valid_types:
281294
raise HTTPException(status_code=400, detail=f"Invalid helper domain. Must be one of: {', '.join(valid_types)}")
282295

app/api/scripts.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,53 @@
1414
logger = logging.getLogger('ha_cursor_agent')
1515

1616
@router.get("/list")
17-
async def list_scripts():
18-
"""List all scripts from scripts.yaml"""
17+
async def list_scripts(ids_only: bool = Query(False, description="If true, return only script IDs without full configurations")):
18+
"""
19+
List all scripts from scripts.yaml
20+
21+
**Parameters:**
22+
- `ids_only` (optional): If `true`, returns only list of script IDs. If `false` (default), returns full script configurations.
23+
24+
**Example response (ids_only=false):**
25+
```json
26+
{
27+
"success": true,
28+
"count": 3,
29+
"scripts": {
30+
"my_script": {"alias": "...", "sequence": [...]},
31+
"another_script": {...}
32+
}
33+
}
34+
```
35+
36+
**Example response (ids_only=true):**
37+
```json
38+
{
39+
"success": true,
40+
"count": 3,
41+
"script_ids": ["my_script", "another_script", "third_script"]
42+
}
43+
```
44+
"""
1945
try:
2046
content = await file_manager.read_file('scripts.yaml')
2147
scripts = yaml.safe_load(content) or {}
2248

49+
if ids_only:
50+
return {
51+
"success": True,
52+
"count": len(scripts),
53+
"script_ids": list(scripts.keys())
54+
}
55+
2356
return {
2457
"success": True,
2558
"count": len(scripts),
2659
"scripts": scripts
2760
}
2861
except FileNotFoundError:
62+
if ids_only:
63+
return {"success": True, "count": 0, "script_ids": []}
2964
return {"success": True, "count": 0, "scripts": {}}
3065
except Exception as e:
3166
logger.error(f"Failed to list scripts: {e}")

app/models/schemas.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ class FileAppend(BaseModel):
1717

1818
class HelperCreate(BaseModel):
1919
"""Helper creation model"""
20-
type: str = Field(..., description="Helper type: input_boolean, input_text, input_number, input_datetime, input_select")
20+
type: str = Field(
21+
...,
22+
description=(
23+
"Helper type: input_boolean, input_text, input_number, input_datetime, "
24+
"input_select, group, utility_meter"
25+
),
26+
)
2127
config: Dict[str, Any] = Field(..., description="Helper configuration including 'name' and other options")
2228
commit_message: Optional[str] = Field(None, description="Custom commit message for Git backup (e.g., 'Add helper: climate system enabled switch')")
2329

0 commit comments

Comments
 (0)