llms-py 3.0.24__py3-none-any.whl → 3.0.26__py3-none-any.whl
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.
- llms/extensions/github_auth/README.md +169 -0
- llms/extensions/github_auth/__init__.py +271 -0
- llms/extensions/github_auth/ui/index.mjs +66 -0
- llms/extensions/providers/anthropic.py +1 -1
- llms/extensions/providers/google.py +1 -1
- llms/extensions/providers/nvidia.py +1 -1
- llms/extensions/skills/__init__.py +137 -138
- llms/extensions/skills/ui/index.mjs +8 -10
- llms/llms.json +1 -9
- llms/main.py +140 -298
- llms/ui/App.mjs +4 -1
- llms/ui/ai.mjs +1 -2
- llms/ui/modules/chat/ChatBody.mjs +1 -2
- llms/ui/modules/layout.mjs +1 -57
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/METADATA +1 -1
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/RECORD +20 -17
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/WHEEL +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.24.dist-info → llms_py-3.0.26.dist-info}/top_level.txt +0 -0
|
@@ -9,9 +9,6 @@ import aiohttp
|
|
|
9
9
|
|
|
10
10
|
from .parser import read_properties
|
|
11
11
|
|
|
12
|
-
g_skills = {}
|
|
13
|
-
g_home_skills = None
|
|
14
|
-
|
|
15
12
|
# Example of what's returned from https://skills.sh/api/skills?limit=5000&offset=0 > ui/data/skills-top-5000.json
|
|
16
13
|
# {
|
|
17
14
|
# "id": "vercel-react-best-practices",
|
|
@@ -47,84 +44,27 @@ def get_skill_files(skill_dir: Path) -> list:
|
|
|
47
44
|
return files
|
|
48
45
|
|
|
49
46
|
|
|
50
|
-
def reload_skill(name: str, location: str, group: str):
|
|
51
|
-
"""Reload a single skill's metadata."""
|
|
52
|
-
global g_skills
|
|
53
|
-
skill_dir = Path(location).resolve()
|
|
54
|
-
if not skill_dir.exists():
|
|
55
|
-
if name in g_skills:
|
|
56
|
-
del g_skills[name]
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
props = read_properties(skill_dir)
|
|
60
|
-
files = get_skill_files(skill_dir)
|
|
61
|
-
|
|
62
|
-
skill_props = props.to_dict()
|
|
63
|
-
skill_props.update(
|
|
64
|
-
{
|
|
65
|
-
"group": group,
|
|
66
|
-
"location": str(skill_dir),
|
|
67
|
-
"files": files,
|
|
68
|
-
}
|
|
69
|
-
)
|
|
70
|
-
g_skills[props.name] = skill_props
|
|
71
|
-
return skill_props
|
|
72
|
-
|
|
73
|
-
|
|
74
47
|
def sanitize(name: str) -> str:
|
|
75
48
|
return name.replace(" ", "").replace("_", "").replace("-", "").lower()
|
|
76
49
|
|
|
77
50
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sanitized_name = sanitize(name)
|
|
84
|
-
for k, v in g_skills.items():
|
|
85
|
-
if sanitize(k) == sanitized_name:
|
|
86
|
-
skill = v
|
|
87
|
-
break
|
|
51
|
+
def resolve_user_skills_path(ctx, user):
|
|
52
|
+
if not user:
|
|
53
|
+
raise ValueError("User is required")
|
|
54
|
+
user_path = ctx.get_user_path(user)
|
|
55
|
+
return os.path.join(user_path, "skills")
|
|
88
56
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
if file:
|
|
96
|
-
if file.startswith(location):
|
|
97
|
-
file = file[len(location) + 1 :]
|
|
98
|
-
if not os.path.exists(os.path.join(location, file)):
|
|
99
|
-
return f"Error: File {file} not found in skill {name}. Available files: {', '.join(skill.get('files', []))}"
|
|
100
|
-
with open(os.path.join(location, file)) as f:
|
|
101
|
-
return f.read()
|
|
102
|
-
|
|
103
|
-
with open(os.path.join(location, "SKILL.md")) as f:
|
|
104
|
-
content = f.read()
|
|
105
|
-
|
|
106
|
-
files = skill.get("files")
|
|
107
|
-
if files and len(files) > 1:
|
|
108
|
-
content += "\n\n## Skill Files:\n```\n"
|
|
109
|
-
for file in files:
|
|
110
|
-
content += f"{file}\n"
|
|
111
|
-
content += "```\n"
|
|
112
|
-
return content
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def install(ctx):
|
|
116
|
-
global g_skills, g_home_skills
|
|
57
|
+
def resolve_skills_write_path(ctx, user=None):
|
|
58
|
+
if user:
|
|
59
|
+
user_skills_path = resolve_user_skills_path(ctx, user)
|
|
60
|
+
os.makedirs(user_skills_path, exist_ok=True)
|
|
61
|
+
return user_skills_path
|
|
117
62
|
home_skills = ctx.get_home_path(os.path.join(".agent", "skills"))
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if not os.path.exists(home_skills):
|
|
121
|
-
os.makedirs(ctx.get_home_path(os.path.join(".agent")), exist_ok=True)
|
|
122
|
-
ctx.log(f"Creating initial skills folder: {home_skills}")
|
|
123
|
-
# os.makedirs(home_skills)
|
|
124
|
-
# copy ui/skills to home_skills
|
|
125
|
-
ui_skills = os.path.join(ctx.path, "ui", "skills")
|
|
126
|
-
shutil.copytree(ui_skills, home_skills)
|
|
63
|
+
os.makedirs(home_skills, exist_ok=True)
|
|
64
|
+
return home_skills
|
|
127
65
|
|
|
66
|
+
def resolve_all_skills(ctx, user=None):
|
|
67
|
+
home_skills = ctx.get_home_path(os.path.join(".agent", "skills"))
|
|
128
68
|
skill_roots = {}
|
|
129
69
|
|
|
130
70
|
# add .claude skills first, so they can be overridden by .agent skills
|
|
@@ -142,7 +82,13 @@ def install(ctx):
|
|
|
142
82
|
local_skills = str(Path(local_skills).resolve())
|
|
143
83
|
skill_roots[LLMS_LOCAL_SKILLS] = local_skills
|
|
144
84
|
|
|
145
|
-
|
|
85
|
+
user_skills_path = None
|
|
86
|
+
if user:
|
|
87
|
+
user_skills_path = resolve_user_skills_path(ctx, user)
|
|
88
|
+
if os.path.exists(user_skills_path):
|
|
89
|
+
skill_roots[f"{user}/skills"] = user_skills_path
|
|
90
|
+
|
|
91
|
+
ret = {}
|
|
146
92
|
for group, root in skill_roots.items():
|
|
147
93
|
if not os.path.exists(root):
|
|
148
94
|
continue
|
|
@@ -164,18 +110,55 @@ def install(ctx):
|
|
|
164
110
|
rel_path = full_path[len(str(skill_dir)) + 1 :]
|
|
165
111
|
files.append(rel_path)
|
|
166
112
|
|
|
113
|
+
writable = False
|
|
114
|
+
if ctx.is_auth_enabled():
|
|
115
|
+
writable = user_skills_path and is_safe_path(user_skills_path, skill_dir)
|
|
116
|
+
else:
|
|
117
|
+
writable = is_safe_path(home_skills, skill_dir) or is_safe_path(local_skills, skill_dir)
|
|
118
|
+
|
|
167
119
|
skill_props = props.to_dict()
|
|
168
120
|
skill_props.update(
|
|
169
121
|
{
|
|
170
122
|
"group": group,
|
|
171
123
|
"location": str(skill_dir),
|
|
172
124
|
"files": files,
|
|
125
|
+
"writable": bool(writable),
|
|
173
126
|
}
|
|
174
127
|
)
|
|
175
|
-
|
|
128
|
+
ret[props.name] = skill_props
|
|
176
129
|
|
|
177
130
|
except OSError:
|
|
178
131
|
pass
|
|
132
|
+
return ret
|
|
133
|
+
|
|
134
|
+
def assert_valid_location(ctx, location, user):
|
|
135
|
+
if ctx.is_auth_enabled() and not user:
|
|
136
|
+
raise Exception("Unauthorized")
|
|
137
|
+
|
|
138
|
+
# if user is specified, only allow modifications to skills in user directory
|
|
139
|
+
if user:
|
|
140
|
+
write_skill_path = resolve_skills_write_path(ctx, user=user)
|
|
141
|
+
if not is_safe_path(write_skill_path, location):
|
|
142
|
+
raise Exception("Cannot modify skills outside of allowed user directory")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
home_skills_path = ctx.get_home_path(os.path.join(".agent", "skills"))
|
|
146
|
+
local_skills_path = os.path.join(".agent", "skills")
|
|
147
|
+
|
|
148
|
+
# Otherwise only allow modifications to skills in home or local .agent directory
|
|
149
|
+
if not is_safe_path(home_skills_path, location) and not is_safe_path(local_skills_path, location):
|
|
150
|
+
raise Exception("Cannot modify skills outside of allowed directories")
|
|
151
|
+
|
|
152
|
+
def install(ctx):
|
|
153
|
+
home_skills = ctx.get_home_path(os.path.join(".agent", "skills"))
|
|
154
|
+
# if not folder exists
|
|
155
|
+
if not os.path.exists(home_skills):
|
|
156
|
+
os.makedirs(ctx.get_home_path(os.path.join(".agent")), exist_ok=True)
|
|
157
|
+
ctx.log(f"Creating initial skills folder: {home_skills}")
|
|
158
|
+
# os.makedirs(home_skills)
|
|
159
|
+
# copy ui/skills to home_skills
|
|
160
|
+
ui_skills = os.path.join(ctx.path, "ui", "skills")
|
|
161
|
+
shutil.copytree(ui_skills, home_skills)
|
|
179
162
|
|
|
180
163
|
g_available_skills = []
|
|
181
164
|
try:
|
|
@@ -186,7 +169,8 @@ def install(ctx):
|
|
|
186
169
|
pass
|
|
187
170
|
|
|
188
171
|
async def get_skills(request):
|
|
189
|
-
|
|
172
|
+
skills = resolve_all_skills(ctx, user=ctx.get_username(request))
|
|
173
|
+
return aiohttp.web.json_response(skills)
|
|
190
174
|
|
|
191
175
|
ctx.add_get("", get_skills)
|
|
192
176
|
|
|
@@ -220,35 +204,22 @@ def install(ctx):
|
|
|
220
204
|
if not source:
|
|
221
205
|
raise Exception(f"Skill '{id}' has no source repository")
|
|
222
206
|
|
|
207
|
+
user = ctx.assert_username(request)
|
|
208
|
+
write_skill_path = resolve_skills_write_path(ctx, user=user)
|
|
209
|
+
|
|
223
210
|
# Install from GitHub
|
|
224
211
|
from .installer import install_from_github
|
|
225
212
|
|
|
213
|
+
ctx.log(f"Installing skill '{id}' from '{source}' to '{write_skill_path}'")
|
|
226
214
|
result = await install_from_github(
|
|
227
215
|
repo_url=f"https://github.com/{source}.git",
|
|
228
216
|
skill_names=[id],
|
|
229
|
-
target_dir=
|
|
217
|
+
target_dir=write_skill_path,
|
|
230
218
|
)
|
|
231
219
|
|
|
232
220
|
if not result.get("success"):
|
|
233
221
|
raise Exception(result.get("error", "Installation failed"))
|
|
234
222
|
|
|
235
|
-
# Reload the installed skills into the registry
|
|
236
|
-
for installed in result.get("installed", []):
|
|
237
|
-
skill_path = installed.get("path")
|
|
238
|
-
if skill_path and os.path.exists(skill_path):
|
|
239
|
-
skill_dir = Path(skill_path).resolve()
|
|
240
|
-
props = read_properties(skill_dir)
|
|
241
|
-
files = get_skill_files(skill_dir)
|
|
242
|
-
skill_props = props.to_dict()
|
|
243
|
-
skill_props.update(
|
|
244
|
-
{
|
|
245
|
-
"group": LLMS_HOME_SKILLS,
|
|
246
|
-
"location": str(skill_dir),
|
|
247
|
-
"files": files,
|
|
248
|
-
}
|
|
249
|
-
)
|
|
250
|
-
g_skills[props.name] = skill_props
|
|
251
|
-
|
|
252
223
|
return aiohttp.web.json_response(result)
|
|
253
224
|
|
|
254
225
|
ctx.add_post("install/{id}", install_skill)
|
|
@@ -256,7 +227,8 @@ def install(ctx):
|
|
|
256
227
|
async def get_skill(request):
|
|
257
228
|
name = request.match_info.get("name")
|
|
258
229
|
file = request.query.get("file")
|
|
259
|
-
|
|
230
|
+
user = ctx.assert_username(request)
|
|
231
|
+
return aiohttp.web.Response(text=skill(name, file, user=user))
|
|
260
232
|
|
|
261
233
|
ctx.add_get("contents/{name}", get_skill)
|
|
262
234
|
|
|
@@ -264,8 +236,10 @@ def install(ctx):
|
|
|
264
236
|
"""Get the content of a specific file in a skill."""
|
|
265
237
|
name = request.match_info.get("name")
|
|
266
238
|
file_path = request.match_info.get("path")
|
|
239
|
+
user = ctx.assert_username(request)
|
|
240
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
267
241
|
|
|
268
|
-
skill_info =
|
|
242
|
+
skill_info = skills.get(name)
|
|
269
243
|
if not skill_info:
|
|
270
244
|
raise Exception(f"Skill '{name}' not found")
|
|
271
245
|
|
|
@@ -288,7 +262,7 @@ def install(ctx):
|
|
|
288
262
|
ctx.add_get("file/{name}/{path:.*}", get_file_content)
|
|
289
263
|
|
|
290
264
|
async def save_file(request):
|
|
291
|
-
"""Save/update a file in a skill. Only works for skills in home directory."""
|
|
265
|
+
"""Save/update a file in a skill. Only works for skills in user home or local directory."""
|
|
292
266
|
name = request.match_info.get("name")
|
|
293
267
|
|
|
294
268
|
try:
|
|
@@ -302,15 +276,15 @@ def install(ctx):
|
|
|
302
276
|
if not file_path or content is None:
|
|
303
277
|
raise Exception("Missing 'path' or 'content' in request body")
|
|
304
278
|
|
|
305
|
-
|
|
279
|
+
user = ctx.assert_username(request)
|
|
280
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
281
|
+
skill_info = skills.get(name)
|
|
306
282
|
if not skill_info:
|
|
307
283
|
raise Exception(f"Skill '{name}' not found")
|
|
308
284
|
|
|
309
285
|
location = skill_info.get("location")
|
|
310
286
|
|
|
311
|
-
|
|
312
|
-
if not is_safe_path(home_skills, location) and not (local_skills and is_safe_path(local_skills, location)):
|
|
313
|
-
raise Exception("Cannot modify skills outside of allowed directories")
|
|
287
|
+
assert_valid_location(ctx, location, user)
|
|
314
288
|
|
|
315
289
|
full_path = os.path.join(location, file_path)
|
|
316
290
|
|
|
@@ -324,10 +298,10 @@ def install(ctx):
|
|
|
324
298
|
f.write(content)
|
|
325
299
|
|
|
326
300
|
# Reload skill metadata
|
|
327
|
-
|
|
328
|
-
|
|
301
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
302
|
+
skill_info = skills.get(name)
|
|
329
303
|
|
|
330
|
-
return aiohttp.web.json_response({"path": file_path, "skill":
|
|
304
|
+
return aiohttp.web.json_response({"path": file_path, "skill": skill_info})
|
|
331
305
|
except Exception as e:
|
|
332
306
|
raise Exception(str(e)) from e
|
|
333
307
|
|
|
@@ -341,15 +315,14 @@ def install(ctx):
|
|
|
341
315
|
if not file_path:
|
|
342
316
|
raise Exception("Missing 'path' query parameter")
|
|
343
317
|
|
|
344
|
-
|
|
318
|
+
user = ctx.assert_username(request)
|
|
319
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
320
|
+
skill_info = skills.get(name)
|
|
345
321
|
if not skill_info:
|
|
346
322
|
raise Exception(f"Skill '{name}' not found")
|
|
347
323
|
|
|
348
324
|
location = skill_info.get("location")
|
|
349
|
-
|
|
350
|
-
# Only allow modifications to skills in home or local .agent directory
|
|
351
|
-
if not is_safe_path(home_skills, location) and not (local_skills and is_safe_path(local_skills, location)):
|
|
352
|
-
raise Exception("Cannot modify skills outside of allowed directories")
|
|
325
|
+
assert_valid_location(ctx, location, user)
|
|
353
326
|
|
|
354
327
|
full_path = os.path.join(location, file_path)
|
|
355
328
|
|
|
@@ -376,10 +349,10 @@ def install(ctx):
|
|
|
376
349
|
break
|
|
377
350
|
|
|
378
351
|
# Reload skill metadata
|
|
379
|
-
|
|
380
|
-
|
|
352
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
353
|
+
skill_info = skills.get(name)
|
|
381
354
|
|
|
382
|
-
return aiohttp.web.json_response({"path": file_path, "skill":
|
|
355
|
+
return aiohttp.web.json_response({"path": file_path, "skill": skill_info})
|
|
383
356
|
except Exception as e:
|
|
384
357
|
raise Exception(str(e)) from e
|
|
385
358
|
|
|
@@ -405,7 +378,9 @@ def install(ctx):
|
|
|
405
378
|
if len(skill_name) > 40:
|
|
406
379
|
raise Exception("Skill name must be 40 characters or less")
|
|
407
380
|
|
|
408
|
-
|
|
381
|
+
user = ctx.assert_username(request)
|
|
382
|
+
write_skill_path = resolve_skills_write_path(ctx, user=user)
|
|
383
|
+
skill_dir = os.path.join(write_skill_path, skill_name)
|
|
409
384
|
|
|
410
385
|
if os.path.exists(skill_dir):
|
|
411
386
|
raise Exception(f"Skill '{skill_name}' already exists")
|
|
@@ -419,8 +394,9 @@ def install(ctx):
|
|
|
419
394
|
try:
|
|
420
395
|
import subprocess
|
|
421
396
|
|
|
397
|
+
ctx.log(f"Creating skill '{skill_name}' in '{write_skill_path}'")
|
|
422
398
|
result = subprocess.run(
|
|
423
|
-
[sys.executable, init_script, skill_name, "--path",
|
|
399
|
+
[sys.executable, init_script, skill_name, "--path", write_skill_path],
|
|
424
400
|
capture_output=True,
|
|
425
401
|
text=True,
|
|
426
402
|
timeout=30,
|
|
@@ -431,21 +407,9 @@ def install(ctx):
|
|
|
431
407
|
|
|
432
408
|
# Load the new skill
|
|
433
409
|
if os.path.exists(skill_dir):
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
skill_props = props.to_dict()
|
|
439
|
-
skill_props.update(
|
|
440
|
-
{
|
|
441
|
-
"group": LLMS_HOME_SKILLS,
|
|
442
|
-
"location": str(skill_dir_path),
|
|
443
|
-
"files": files,
|
|
444
|
-
}
|
|
445
|
-
)
|
|
446
|
-
g_skills[props.name] = skill_props
|
|
447
|
-
|
|
448
|
-
return aiohttp.web.json_response({"skill": skill_props, "output": result.stdout})
|
|
410
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
411
|
+
skill_info = skills.get(skill_name)
|
|
412
|
+
return aiohttp.web.json_response({"skill": skill_info, "output": result.stdout})
|
|
449
413
|
|
|
450
414
|
raise Exception("Skill directory not created")
|
|
451
415
|
|
|
@@ -460,27 +424,26 @@ def install(ctx):
|
|
|
460
424
|
"""Delete an entire skill. Only works for skills in home directory."""
|
|
461
425
|
name = request.match_info.get("name")
|
|
462
426
|
|
|
463
|
-
|
|
427
|
+
user = ctx.assert_username(request)
|
|
428
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
429
|
+
skill_info = skills.get(name)
|
|
464
430
|
|
|
465
431
|
if skill_info:
|
|
466
432
|
location = skill_info.get("location")
|
|
467
433
|
else:
|
|
468
|
-
# Check if orphaned directory exists on disk (not loaded in
|
|
434
|
+
# Check if orphaned directory exists on disk (not loaded in skills)
|
|
469
435
|
potential_location = os.path.join(home_skills, name)
|
|
470
|
-
if os.path.exists(potential_location)
|
|
436
|
+
if os.path.exists(potential_location):
|
|
471
437
|
location = potential_location
|
|
472
438
|
else:
|
|
473
439
|
raise Exception(f"Skill '{name}' not found")
|
|
474
440
|
|
|
475
|
-
# Only allow deletion of skills in
|
|
476
|
-
|
|
477
|
-
raise Exception("Cannot delete skills outside of allowed directories")
|
|
441
|
+
# Only allow deletion of skills in allowed directories
|
|
442
|
+
assert_valid_location(ctx, location, user)
|
|
478
443
|
|
|
479
444
|
try:
|
|
480
445
|
if os.path.exists(location):
|
|
481
446
|
shutil.rmtree(location)
|
|
482
|
-
if name in g_skills:
|
|
483
|
-
del g_skills[name]
|
|
484
447
|
|
|
485
448
|
return aiohttp.web.json_response({"deleted": name})
|
|
486
449
|
except Exception as e:
|
|
@@ -488,6 +451,42 @@ def install(ctx):
|
|
|
488
451
|
|
|
489
452
|
ctx.add_delete("skill/{name}", delete_skill)
|
|
490
453
|
|
|
454
|
+
def skill(name: Annotated[str, "skill name"], file: Annotated[str | None, "skill file"] = None, user=None):
|
|
455
|
+
"""Get the content of a skill or a specific file within a skill."""
|
|
456
|
+
ctx.log(f"skill tool '{name}', file='{file}', user='{user}'")
|
|
457
|
+
|
|
458
|
+
skills = resolve_all_skills(ctx, user=user)
|
|
459
|
+
skill = skills.get(name)
|
|
460
|
+
|
|
461
|
+
if not skill:
|
|
462
|
+
sanitized_name = sanitize(name)
|
|
463
|
+
for k, v in skills.items():
|
|
464
|
+
if sanitize(k) == sanitized_name:
|
|
465
|
+
skill = v
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
if not skill:
|
|
469
|
+
return f"Error: Skill {name} not found. Available skills: {', '.join(skills.keys())}"
|
|
470
|
+
location = skill.get("location")
|
|
471
|
+
if not location or not os.path.exists(location):
|
|
472
|
+
return f"Error: Skill {name} not found at location {location}"
|
|
473
|
+
|
|
474
|
+
if file:
|
|
475
|
+
if file.startswith(location):
|
|
476
|
+
file = file[len(location) + 1 :]
|
|
477
|
+
if not os.path.exists(os.path.join(location, file)):
|
|
478
|
+
return f"Error: File {file} not found in skill {name}. Available files: {', '.join(skill.get('files', []))}"
|
|
479
|
+
with open(os.path.join(location, file)) as f:
|
|
480
|
+
return f.read()
|
|
481
|
+
|
|
482
|
+
with open(os.path.join(location, "SKILL.md")) as f:
|
|
483
|
+
content = f.read()
|
|
484
|
+
|
|
485
|
+
files = skill.get("files")
|
|
486
|
+
if files and len(files) > 1:
|
|
487
|
+
content += "\n\n## Skill Files:\n```\n"
|
|
488
|
+
return content
|
|
489
|
+
|
|
491
490
|
ctx.register_tool(skill, group="core_tools")
|
|
492
491
|
|
|
493
492
|
|
|
@@ -3,9 +3,6 @@ import { leftPart } from "@servicestack/client"
|
|
|
3
3
|
|
|
4
4
|
let ext
|
|
5
5
|
|
|
6
|
-
const LLMS_HOME_SKILLS = "~/.llms/.agent/skills"
|
|
7
|
-
const LLMS_LOCAL_SKILLS = ".agent/skills"
|
|
8
|
-
|
|
9
6
|
const SkillSelector = {
|
|
10
7
|
template: `
|
|
11
8
|
<div class="px-4 py-4 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 max-h-[80vh] overflow-y-auto">
|
|
@@ -122,10 +119,10 @@ const SkillSelector = {
|
|
|
122
119
|
skills
|
|
123
120
|
}))
|
|
124
121
|
|
|
125
|
-
// Sort groups: writable
|
|
122
|
+
// Sort groups: writable first, then alphabetically
|
|
126
123
|
definedGroups.sort((a, b) => {
|
|
127
|
-
const aEditable = a.
|
|
128
|
-
const bEditable = b.
|
|
124
|
+
const aEditable = a.skills.some(s => s.writable)
|
|
125
|
+
const bEditable = b.skills.some(s => s.writable)
|
|
129
126
|
if (aEditable !== bEditable) return aEditable ? -1 : 1
|
|
130
127
|
return a.name.localeCompare(b.name)
|
|
131
128
|
})
|
|
@@ -392,6 +389,7 @@ const SkillPage = {
|
|
|
392
389
|
const editorRef = ref(null)
|
|
393
390
|
const expandedSkills = ref({})
|
|
394
391
|
const skills = computed(() => ctx.state.skills || {})
|
|
392
|
+
|
|
395
393
|
const skillGroups = computed(() => {
|
|
396
394
|
const grouped = {}
|
|
397
395
|
const query = searchQuery.value.toLowerCase()
|
|
@@ -402,8 +400,8 @@ const SkillPage = {
|
|
|
402
400
|
grouped[group].push(skill)
|
|
403
401
|
})
|
|
404
402
|
return Object.entries(grouped).sort((a, b) => {
|
|
405
|
-
const aEditable = a[
|
|
406
|
-
const bEditable = b[
|
|
403
|
+
const aEditable = a[1].some(s => s.writable)
|
|
404
|
+
const bEditable = b[1].some(s => s.writable)
|
|
407
405
|
if (aEditable !== bEditable) return aEditable ? -1 : 1
|
|
408
406
|
return a[0].localeCompare(b[0])
|
|
409
407
|
}).map(([name, skills]) => ({ name, skills: skills.sort((a, b) => a.name.localeCompare(b.name)) }))
|
|
@@ -426,8 +424,8 @@ const SkillPage = {
|
|
|
426
424
|
return tree.sort((a, b) => { if (a.isFile !== b.isFile) return a.isFile ? 1 : -1; return a.name.localeCompare(b.name) })
|
|
427
425
|
}
|
|
428
426
|
const hasUnsavedChanges = computed(() => isEditing.value && editContent.value !== fileContent.value)
|
|
429
|
-
function isGroupEditable(groupName) { return
|
|
430
|
-
function isEditable(skill) { return skill?.
|
|
427
|
+
function isGroupEditable(groupName) { return Object.values(skills.value).some(s => s.group === groupName && s.writable) }
|
|
428
|
+
function isEditable(skill) { return skill?.writable }
|
|
431
429
|
function isSkillExpanded(name) { return !!expandedSkills.value[name] }
|
|
432
430
|
function toggleSkillExpand(skill) {
|
|
433
431
|
expandedSkills.value[skill.name] = !expandedSkills.value[skill.name]
|
llms/llms.json
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"auth": {
|
|
4
|
-
"enabled": false,
|
|
5
|
-
"github": {
|
|
6
|
-
"client_id": "GITHUB_CLIENT_ID",
|
|
7
|
-
"client_secret": "GITHUB_CLIENT_SECRET",
|
|
8
|
-
"redirect_uri": "http://localhost:8000/auth/github/callback",
|
|
9
|
-
"restrict_to": "GITHUB_USERS"
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
3
|
"disable_extensions": [],
|
|
13
4
|
"defaults": {
|
|
14
5
|
"headers": {
|
|
@@ -133,6 +124,7 @@
|
|
|
133
124
|
}
|
|
134
125
|
},
|
|
135
126
|
"limits": {
|
|
127
|
+
"client_timeout": 120,
|
|
136
128
|
"client_max_size": 20971520
|
|
137
129
|
},
|
|
138
130
|
"convert": {
|